Media3 ExoPlayer无法播放不带.m3u8后缀hls媒资

1.问题发现

播放报错None of the available extractors (没有可用的媒资提取器) , 错误码 ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED (表示解析媒体容器格式错误)

shell 复制代码
None of the available extractors (FlvExtractor, FlacExtractor, WavExtractor, FragmentedMp4Extractor, Mp4Extractor, AmrExtractor, PsExtractor, OggExtractor, TsExtractor, MatroskaExtractor, AdtsExtractor, Ac3Extractor, Ac4Extractor, Mp3Extractor, AviExtractor, JpegExtractor, PngExtractor, WebpExtractor, BmpExtractor, HeifExtractor, AvifExtractor) could read the stream. {contentIsMalformed=false, dataType=1}

2.问题分析

2.1 错误位置

找到报错的源码位置

Java 复制代码
@Override
public void init(
    DataReader dataReader,
    Uri uri,
    Map<String, List<String>> responseHeaders,
    long position,
    long length,
    ExtractorOutput output)
    throws IOException {
  //根据uri和响应头信息,通过工厂接口创建一组媒体提取器(Extractor)实例,用于后续尝试解析媒体流。
  Extractor[] extractors = extractorsFactory.createExtractors(uri, responseHeaders);
  ImmutableList.Builder<SniffFailure> sniffFailures =
      ImmutableList.builderWithExpectedSize(extractors.length);
  if (extractors.length == 1) {
    this.extractor = extractors[0];
  } else {
    for (Extractor extractor : extractors) {
      try {
          //嗅探(通过读取数据流的头部信息来判断格式匹配度)检测当前 Extractor 是否能够识别并处理输入的媒体数据流 
        if (extractor.sniff(extractorInput)) {
          this.extractor = extractor;
          break;
        } else {
          List<SniffFailure> sniffFailureDetails = extractor.getSniffFailureDetails();
          sniffFailures.addAll(sniffFailureDetails);
        }
      } catch (EOFException e) {
        // Do nothing.
      } finally {
        Assertions.checkState(this.extractor != null || extractorInput.getPosition() == position);
        extractorInput.resetPeekPosition();
      }
    }
    //没有找到合适的媒资提取器会抛出异常
    if (extractor == null) {
      //【sss】如果是没有.m3u8后缀的hls媒资,会走到这里
      throw new UnrecognizedInputFormatException(
          "None of the available extractors ("
              + Joiner.on(", ")
                  .join(
                      Lists.transform(
                          ImmutableList.copyOf(extractors),
                          extractor ->
                              extractor.getUnderlyingImplementation().getClass().getSimpleName()))
              + ") could read the stream.",
          Assertions.checkNotNull(uri),
          sniffFailures.build());
    }
  }
  extractor.init(output);
}
2.2 媒资信息

用ffmpeg命令查看媒资基本信息

arduino 复制代码
`ffprobe -v quiet -print_format json -show_format "http://xxx"`

输出结果:

java 复制代码
{
    "format": {
        "filename": "http://xxx",
        "nb_streams": 2,
        "nb_programs": 1,
        "nb_stream_groups": 0,
        "format_name": "hls",
        "format_long_name": "Apple HTTP Live Streaming",
        "start_time": "1.480000",
        "duration": "6438.880000",
        "size": "68",
        "probe_score": 100
    }
}

流媒体特征

  • format_name: hls - 确认这是HLS (HTTP Live Streaming) 格式
  • format_long_name: Apple HTTP Live Streaming - Apple开发的流媒体协议

通常.m3u8是最典型的HLS文件后缀,看下ExoPlayer怎么处理的HLS流

3.源码分析

我们设置媒体地址开始看起

Java 复制代码
player.setMediaItem(MediaItem.fromUri(url));

经过一系列调用

Java 复制代码
@Override
public void setMediaItems(List<MediaItem> mediaItems, boolean resetPosition) {
  verifyApplicationThread();
  setMediaSources(createMediaSources(mediaItems), resetPosition);
}

@UnstableApi
  @Override
  public MediaSource createMediaSource(MediaItem mediaItem) {
    Assertions.checkNotNull(mediaItem.localConfiguration);
    @Nullable String scheme = mediaItem.localConfiguration.uri.getScheme();
    //...
    //【sss】根据媒体项的 uri 和 设置的mimeType 类型推断出内容类型(如 HLS),用于后续选择合适的媒体源工厂进行处理
    @C.ContentType
    int type =
        Util.inferContentTypeForUriAndMimeType(
            mediaItem.localConfiguration.uri, mediaItem.localConfiguration.mimeType);
    if (mediaItem.localConfiguration.imageDurationMs != C.TIME_UNSET) {
      delegateFactoryLoader.setJpegExtractorFlags(JpegExtractor.FLAG_READ_IMAGE);
    }

    MediaSource.Factory mediaSourceFactory;
    try {
      //【sss】选择合适的媒体源工厂进行处理
      mediaSourceFactory = delegateFactoryLoader.getMediaSourceFactory(type);
    } catch (ClassNotFoundException e) {
      throw new IllegalStateException(e);
    }
    //...
  }

通过inferContentTypeForUriAndMimeType函数从Uri和可选MIME类型推断ContentType的功能。

Java 复制代码
public static @ContentType int inferContentTypeForUriAndMimeType(
      Uri uri, @Nullable String mimeType) {
    //默认mimeType为null
    if (mimeType == null) {
      return inferContentType(uri);
    }
    //通过设置的mimeType去匹配
    switch (mimeType) {
      case MimeTypes.APPLICATION_MPD:
        return C.CONTENT_TYPE_DASH;
      case MimeTypes.APPLICATION_M3U8:
        return C.CONTENT_TYPE_HLS;
      case MimeTypes.APPLICATION_SS:
        return C.CONTENT_TYPE_SS;
      case MimeTypes.APPLICATION_RTSP:
        return C.CONTENT_TYPE_RTSP;
      default:
        return C.CONTENT_TYPE_OTHER;
    }
  }

public static @ContentType int inferContentType(Uri uri) {
    @Nullable String scheme = uri.getScheme();
    //rtsp匹配
    if (scheme != null
        && (Ascii.equalsIgnoreCase("rtsp", scheme) || Ascii.equalsIgnoreCase("rtspt", scheme))) {
      return C.CONTENT_TYPE_RTSP;
    }

    @Nullable String lastPathSegment = uri.getLastPathSegment();
    if (lastPathSegment == null) {
      return C.CONTENT_TYPE_OTHER;
    }
    int lastDotIndex = lastPathSegment.lastIndexOf('.');
    if (lastDotIndex >= 0) {
      @C.ContentType
      //取后缀名匹配(.m3u8识别为hls)
      int contentType = inferContentTypeForExtension(lastPathSegment.substring(lastDotIndex + 1));
      if (contentType != C.CONTENT_TYPE_OTHER) {
        // If contentType is TYPE_SS that indicates the extension is .ism or .isml and shows the ISM
        // URI is missing the "/manifest" suffix, which contains the information used to
        // disambiguate between Smooth Streaming, HLS and DASH below - so we can just return TYPE_SS
        // here without further checks.
        return contentType;
      }
    }
	//URI包含查询参数 format=m3u8-aapl 会被识别为hls
    Matcher ismMatcher = ISM_PATH_PATTERN.matcher(checkNotNull(uri.getPath()));
    if (ismMatcher.matches()) {
      @Nullable String extensions = ismMatcher.group(2);
      if (extensions != null) {
        if (extensions.contains(ISM_DASH_FORMAT_EXTENSION)) {
          return C.CONTENT_TYPE_DASH;
        } else if (extensions.contains(ISM_HLS_FORMAT_EXTENSION)) {
          return C.CONTENT_TYPE_HLS;
        }
      }
      return C.CONTENT_TYPE_SS;
    }

    return C.CONTENT_TYPE_OTHER;
  }

综合上所述,ExoPlayer识别HLS流需要下面任意条件(媒资不满足前2条)

  • 后缀为.m3u8
  • URI包含查询参数 format=m3u8-aapl
  • 显示指定mimeType为MimeTypes.APPLICATION_M3U8

HLS流的处理是在exoplayer-hls模块,区别于其它媒体提取器(Extractor),是单独的处理流程

4.问题处理

在错误码为ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED时,设置mimeType为MimeTypes.APPLICATION_M3U8重试一次播放。

Java 复制代码
@Override
public void onPlayerError(PlaybackException error) {
     if (error.errorCode == PlaybackException.ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED && retryCount == 0) {
           retryCount++;
           MediaItem.Builder build = new MediaItem.Builder().setUri(url);
		   build.setMimeType(MimeTypes.APPLICATION_M3U8);
		   MediaItem mediaItem = build.build();
           player.setPlayWhenReady(true);
		   player.setMediaItem(mediaItem);
		   player.prepare();
           return;
     }
     //其它错误场景处理
}
相关推荐
屏什么录4 天前
录屏超过两小时就卡顿崩盘?从参数调优到代码实现,一文彻底搞定长时间录屏
前端·音视频开发
字节跳动视频云技术团队5 天前
720P 直升 8K,解密 2026 抖音美好奇妙夜巨幕超分黑科技
人工智能·音视频开发
VimorphTech8 天前
文件名是 MP4 就能处理?用 ffprobe 检查视频流、音轨和时长
java·音视频开发
冬奇Lab10 天前
开源项目第242期:HyperFrames —— 写 HTML 就能渲染视频,专为 AI Agent 设计的确定性视频框架
人工智能·开源·音视频开发
屏什么录16 天前
电脑如何录屏幕视频和声音?从工具到代码,7 种方案帮你彻底搞定
前端·音视频开发
VimorphTech16 天前
转写结果怎么生成双语字幕?TypeScript 实现 SRT、WebVTT 与索引匹配
javascript·音视频开发
音视频牛哥21 天前
人形机器人的未来发展趋势是什么?从VLA、实时视觉到规模化运营
机器学习·机器人·音视频开发
霍营_托尼22 天前
纯前端播 40GB 本地视频?我把浏览器改造成了「磁盘流式」播放器
前端·javascript·音视频开发
花椒技术1 个月前
2.46 秒生成 5 秒视频:拆解 H3 Max 的模型、推理栈与硬件协同
算法·音视频开发·视频编码
代码小学僧1 个月前
从 0 到 1,记录我的第一个出海 SaaS 独立开发项目
前端·rpc·音视频开发