3. MediaSource 组合与扩展:ProgressiveMediaSource、DashMediaSource、HlsMediaSource、自定义 ConcatenatingMediaSource

3.1 原理:MediaSource 的角色与组合动机

ExoPlayer 的播放核心是 MediaSource 接口,它抽象了媒体数据的来源与解析过程。不同流媒体协议对应不同的实现:
• ProgressiveMediaSource:处理 MP4、WebM 等渐进式下载媒体,支持本地文件与 HTTP URL。
• DashMediaSource:解析 MPD 清单,处理基于 MPEG-DASH 的动态自适应流。
• HlsMediaSource:处理 M3U8 主播放列表与分片,支持 HLS 自适应流。

实际场景中经常需要将多个独立的媒体片段或不同协议的流串联播放(例如先播广告 HLS,再播正片 DASH)。ExoPlayer 提供了 ConcatenatingMediaSource 用于顺序合并多个 MediaSource,也支持自定义子类实现更复杂的组合逻辑。

3.2 组合流程与核心 API

步骤关键类 / 方法说明
1. 创建各协议 MediaSourceProgressiveMediaSource.Factory
DashMediaSource.Factory
HlsMediaSource.Factory
传入 URI 和 MediaItem,构建子源
2. 构建合并容器ConcatenatingMediaSource
构造方法传入 MediaSource... 或通过 addMediaSource()
子源按添加顺序播放
3. 设置到播放器player.setMediaSource(mediaSource)
player.prepare()
替换原本的 MediaItem 方式
4. 监听切换与错误Player.Listener
onMediaItemTransition
onPlayerError
在组合播放中每个子源视为独立期次

3.3 实现代码示例

以下代码演示创建 DASH、HLS、Progressive 三个媒体源并组合为一个连续的播放序列:

// 1. 构建子 MediaSource
MediaItem dashItem = MediaItem.fromUri("https://example.com/manifest.mpd");
MediaItem hlsItem = MediaItem.fromUri("https://example.com/playlist.m3u8");
MediaItem progressiveItem = MediaItem.fromUri("https://example.com/video.mp4");

DashMediaSource dashSource =
    new DashMediaSource.Factory(
        new DefaultDashChunkSource.Factory(createDataSourceFactory()),
        createDataSourceFactory())
    .createMediaSource(dashItem);

HlsMediaSource hlsSource =
    new HlsMediaSource.Factory(
        new DefaultHlsChunkSource.Factory(createDataSourceFactory()))
    .createMediaSource(hlsItem);

ProgressiveMediaSource progressiveSource =
    new ProgressiveMediaSource.Factory(createDataSourceFactory())
    .createMediaSource(progressiveItem);

// 2. 组合
ConcatenatingMediaSource concatenatedSource =
    new ConcatenatingMediaSource(dashSource, hlsSource, progressiveSource);

// 3. 设置到播放器
ExoPlayer player = new ExoPlayer.Builder(context).build();
player.setMediaSource(concatenatedSource);
player.prepare();
player.play();
注意:createDataSourceFactory() 通常返回 DefaultHttpDataSource.Factory,支持网络与缓存。若需本地文件,使用 FileDataSource.Factory 封装。

3.4 调试与常见错误

  • URI 格式错误:DASH 必须使用 .mpd 结尾的清晰清单 URL;HLS 要求 .m3u8;Progressive 支持常见容器格式。通过日志 player.addListener 监听 onPlayerError 可捕获 ExoPlaybackException 并判断 source 类型。
  • 跨协议缓存冲突:不同协议可能使用不同的缓存 key,若共用一个 CacheDataSource.Factory 可能造成碎片。建议为不同协议创建独立缓存实例。
  • ConcatenatingMediaSource 中的空源:若某个子源构建失败返回 null,会导致播放中断。应在添加前检查有效性。
  • 媒体时间线偏移:组合后每个子源的起始时间为前一个子源的结束时间,可通过 ConcatenatingMediaSource.setMediaSourceOrder() 调整。

3.5 性能与稳定性优化

  1. 预加载机制:使用 ConcatenatingMediaSource.setMediaSourcePreparer() 为后续子源提前准备,减少切换卡顿。
    concatenatedSource.setMediaSourcePreparer(
        new MediaSourcePreparer() {
            @Override
            public void prepare(MediaSource mediaSource, MediaSource.MediaSourceCaller caller,
                                MediaSource.MediaPeriodId id) {
                mediaSource.prepareSource(caller, id);
            }
        });
    
  2. 自适应码率优化:DASH 与 HLS 子源内部可设置 TrackSelector 和 DefaultBandwidthMeter,在切换时保留带宽估计值。
  3. 错误恢复:在 onPlayerError 回调中判断错误类型,对可恢复的错误(如网络超时)重试当前子源。
    player.setMediaSource(concatenatedSource, /* startPositionMs= */ player.getCurrentPosition());
    player.prepare();
    
  4. 内存管理:大量子源组合(如播放列表超 50 个)建议使用 ConcatenatingMediaSource.setReuseSource(boolean) 控制释放策略。
结构总览:下图展示了三种原生 MediaSource 与自定义组合容器的关系:ProgressiveMediaSource、DashMediaSource、HlsMediaSource 作为叶子节点,通过 ConcatenatingMediaSource 串联为单一时间线,最终传递给 ExoPlayer 播放引擎。
MediaSource 组合与扩展结构图 MediaSource 组合拓扑 ProgressiveMediaSource DashMediaSource HlsMediaSource ConcatenatingMediaSource (自定义组合容器) ExoPlayer 自定义 ConcatenatingMediaSource 可重写 createPeriod、getInitialTimeline 实现高级组合逻辑 (如动态插入广告、跳转、循环播放等)

3.6 自定义扩展:继承 ConcatenatingMediaSource

当标准串联不够用时(如需要动态添加、删除子源,或按条件跳过部分内容),可继承 ConcatenatingMediaSource 并覆盖关键方法:

public class CustomLoopMediaSource extends ConcatenatingMediaSource {
    private final int repeatCount;

    public CustomLoopMediaSource(int repeatCount, MediaSource... sources) {
        super(sources);
        this.repeatCount = repeatCount;
    }

    @Override
    public Timeline getInitialTimeline() {
        Timeline base = super.getInitialTimeline();
        // 返回重复多次后的时间线
        return new WrappingTimeline(base, repeatCount);
    }

    // 可重写 createPeriod 实现自适应分片
}
注意:重写时间线相关方法时需同时调整 getMediaItemCount() 与 getWindow(int, Timeline.Window, long) 保持一致,否则播放位置计算会出错。

3.7 部署与验证

在真机或模拟器上测试组合播放时,建议:
• 使用 player.getCurrentMediaItemIndex() 确认当前正在播放第几个子源。
• 通过 player.getDuration() 验证总时长是否等于各子源时长之和。
• 切换网络环境(如开启飞行模式)测试 HLS/DASH 的降级能力。
• 日志级别设为 Log.DEBUG 并过滤 "ExoPlayerImpl" 观察 MediaSource 生命周期。