第2章 MediaSource 构建实战:ProgressiveMediaSource、DashMediaSource、HlsMediaSource 的选择与配置

在 ExoPlayer 中,MediaSource 是媒体数据流的抽象层,负责从底层数据源读取原始数据,并按播放器要求的格式(单轨道、多轨道、分段索引)组装为可消费的媒体序列。本章深入剖析三种核心实现——ProgressiveMediaSource(渐进式)、DashMediaSource(DASH)和 HlsMediaSource(HLS)——的架构原理、构建流程及选型策略,并通过可运行的代码片段展示如何正确配置它们。

三种 MediaSource 构建架构对比 ProgressiveMediaSource 输入:单文件 URI (MP4 / WebM / MKV) 数据源:File / Http 轨道提取器 DefaultExtractorsFactory DashMediaSource 输入:MPD 清单 URI (DASH 流描述) DashChunkSource DashManifestParser AdaptiveTrackSelection HlsMediaSource 输入:M3U8 播放列表 URI (多码率/单码率) HlsChunkSource HlsPlaylistParser DefaultHlsExtractor ExoPlayer → 轨道渲染器(Video/Audio/Text) 三种 MediaSource 内部架构高度对称:输入解析 → 分块源 → 提取器/适配选择 → 播放器渲染。

原理说明

MediaSource 位于 ExoPlayer 的 媒体管道上层,负责将原始媒体资源转换为播放器能够理解的 MediaPeriod 序列。其核心职责可拆解为:

  • 资源解析:根据输入 URI 或清单文件(MPD/M3U8),解析出轨道元数据(时长、码率、分辨率、编码类型)。
  • 分段映射:将连续或分段媒体映射为 ChunkSource 可管理的 MediaChunk 列表。
  • 轨道暴露:向播放器暴露 TrackGroup 集合,供 TrackSelector 选择渲染轨道。
  • 自适应支持:DASH 和 HLS 内置码率自适应逻辑,DashMediaSource 和 HlsMediaSource 继承自 BaseMediaSource 并实现 CompositeSequenceableLoader 来切换分块。

三种 MediaSource 在底层数据源(DataSource)上完全统一,均通过 DataSource.Factory 接口创建读取器,从而共享缓存、重试、协议选择等能力。

选择策略

场景 推荐 MediaSource 典型输入 关键配置点
单文件本地/网络播放(MP4/WebM) ProgressiveMediaSource Uri.parse("file:///storage/video.mp4") 需配合 ExtractorsFactory;支持 seek 精确到关键帧
自适应流媒体(多码率 ABR) DashMediaSource Uri.parse("https://example.com/manifest.mpd") 需正确传递 DashChunkSource.Factory 和 TrackSelector
Apple HLS 流(TS/CMAF 分段) HlsMediaSource Uri.parse("https://example.com/playlist.m3u8") 需配置 HlsMediaSource.Factory 并设置 HlsChunkSource.Factory
直播低延迟(LL-DASH / LL-HLS) DashMediaSource / HlsMediaSource 同上 + 启低延迟模式 设置 LiveConfiguration 和 MediaSource.Factory 的 livePlaybackSpeedControl

构建与配置实战

以下代码片段展示三种 MediaSource 的标准构建方式,所有示例均使用 MediaSource.Factory 子类进行统一配置。

ProgressiveMediaSource 构建

DataSource.Factory dataSourceFactory = new DefaultHttpDataSource.Factory();
ExtractorsFactory extractorsFactory = new DefaultExtractorsFactory();

ProgressiveMediaSource mediaSource =
    new ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory)
        .createMediaSource(MediaItem.fromUri(videoUri));

DashMediaSource 构建

DataSource.Factory dataSourceFactory = new DefaultHttpDataSource.Factory();
DashMediaSource.Factory dashFactory = new DashMediaSource.Factory(dataSourceFactory);

DashMediaSource mediaSource =
    dashFactory.createMediaSource(MediaItem.fromUri(manifestUri));

HlsMediaSource 构建

DataSource.Factory dataSourceFactory = new DefaultHttpDataSource.Factory();
HlsMediaSource.Factory hlsFactory = new HlsMediaSource.Factory(dataSourceFactory);

HlsMediaSource mediaSource =
    hlsFactory.createMediaSource(MediaItem.fromUri(playlistUri));

上述代码中,MediaItem.fromUri() 是推荐的方式,它自动根据 URI 后缀或 MIME 类型推断 MediaSource 类型(若使用 DefaultMediaSourceFactory)。但在自定义边界场景中,显式构造具体的 MediaSource 实现可以更精细地控制参数(例如 DashChunkSource 的加载行为、HLS 的播放列表类型)。

常见错误与调试技巧

错误 #1:ProgressiveMediaSource 传入分段流 URI

当服务器返回一个 MPD 或 M3U8 页面但以 .mp4 后缀伪装时,ProgressiveMediaSource 会尝试用 Extractor 解析清单文本,导致 “Content malformed” 异常。调试时查看 logcat 中的 ExoPlayerImplInternal 堆栈,若出现 UnrecognizedInputFormatException,则核实 URI 指向的真实媒体格式。

调试技巧:启用 ExoPlayer 内部日志

在构建播放器前设置 ExoPlayer.setLogTag(“ExoPlayer”); 并调高日志级别:

adb shell setprop log.tag.ExoPlayer VERBOSE
adb logcat -s ExoPlayer:* ExoPlayerImpl:V

关注 MediaSourcePeriodInfo 和 ChunkSource 的加载日志,可快速定位错误是发生在解析阶段还是下载阶段。

错误 #2:DataSource 未配置 Cookie 或 User-Agent

某些流媒体服务器要求携带特定的 HTTP 头部。若不设置 DefaultHttpDataSource 的 userAgent 或 cookieManager,可能返回 403 或 302 重定向循环。修复方式:

DataSource.Factory dataSourceFactory = new DefaultHttpDataSource.Factory()
    .setUserAgent("ExoPlayerDemo/1.0")
    .setAllowCrossProtocolRedirects(true);
// 对于 DASH/HLS,同样将此 factory 传入对应的 MediaSource.Factory

另注意:DASH 和 HLS 的清单及分段共享同一个 DataSource.Factory 实例。

错误 #3:DASH 多码率流选择失败

DashMediaSource 默认使用 AdaptiveTrackSelection.Factory。若播放器始终选择最低码率,检查 TrackSelector 是否配置了 BandwidthMeter 并启用了自适应:

BandwidthMeter bandwidthMeter = new DefaultBandwidthMeter.Builder(context).build();
TrackSelector trackSelector = new DefaultTrackSelector(context, new AdaptiveTrackSelection.Factory(bandwidthMeter));

同时确认 MPD 清单中多个 AdaptationSet 的码率存在显著差异,否则自适应算法可能认为质量相近而选择低码率以节省带宽。