Android 开源库分析——ExoPlayer使用与分析

本文链接:https://rainmonth.github.io/posts/A191101.html

摘要

主要根据谷歌官方提供的demo,来了解ExoPlayer的基本使用。

[TOC]

基本概念

在介绍ExoPlayer之前,先介绍几个基本概念:

  • 流媒体通信协议
    • DASH,Dynamic Adaptive Streaming over HTTP的缩写,即基于HTTP的动态自适应流,国际标准组MPEG制定的技术标准;
    • HLS,HTTP Live Streaming的缩写,即动态码率自适应技术;
  • ExoPlayer中自交重要的概念
    • Timeline,用来指代媒体数据的灵活数据结构,本质上是一种数据的描述,由媒体片段(Period)和Window(由一个或多个Period组成)组成;
    • MediaPeriod,用来加载与Timeline.Period对应的Media,并是这段Media可读;

基本使用流程

  1. 添加ExoPlayer依赖,或者clone ExoPlayer到本地然后倒入module作为依赖;
  • 在项目根目录下的build.gradle中添加如下repositories:

    1
    2
    3
    4
    repositories {
    google()
    jcenter()
    }
  • 在项目的主module(app)的build.gradle文件中添加ExoPlayer依赖:

    1
    2
    3
    4
    5
    6
    # 添加全部依赖
    implementation 'com.google.android.exoplayer:exoplayer:2.X.X'
    # 添加部分依赖
    implementation 'com.google.android.exoplayer:exoplayer-core:2.X.X'
    implementation 'com.google.android.exoplayer:exoplayer-dash:2.X.X'
    implementation 'com.google.android.exoplayer:exoplayer-ui:2.X.X'

    可以添加的依赖如下:

    • exoplayer-core: ExoPlayer的核心库,必须添加;
    • exoplayer-dash: DASH内容支持;
    • exoplayer-hls: HLS内容支持;
    • exoplayer-smoothstreaming: SmoothStreaming内容支持;
    • exoplayer-ui: ExoPlayer提供的UI组件;
  • 在依赖ExoPlayer的Moudle都添加Java 8支持,在build.gradle的android闭包中添加:

    1
    2
    3
    compileOptions {
    targetCompatibility JavaVersion.VERSION_1_8
    }
  1. 创建Player实例
    ExoPlayer库提供了一个工厂方法来获取播放器实例,即ExoPlayerFactory.newInstance(...)和ExoPlayerFactory.newSimpleInstance(...)方法,最终都是通过ExoPlayerImpl(ExoPlayer接口的实现类)的构造方法来创建的,该类只有一个构造函数,最终的处理是通过内部实现类ExoPlayerImplInternal来实现的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    public ExoPlayerImpl(
    Renderer[] renderers,
    TrackSelector trackSelector,
    LoadControl loadControl,
    BandwidthMeter bandwidthMeter,
    Clock clock,
    Looper looper) {
    Log.i(TAG, "Init " + Integer.toHexString(System.identityHashCode(this)) + " ["
    + ExoPlayerLibraryInfo.VERSION_SLASHY + "] [" + Util.DEVICE_DEBUG_INFO + "]");
    Assertions.checkState(renderers.length > 0);
    this.renderers = Assertions.checkNotNull(renderers);
    this.trackSelector = Assertions.checkNotNull(trackSelector);
    this.playWhenReady = false;
    this.repeatMode = Player.REPEAT_MODE_OFF;
    this.shuffleModeEnabled = false;
    this.listeners = new CopyOnWriteArrayList<>();
    emptyTrackSelectorResult =
    new TrackSelectorResult(
    new RendererConfiguration[renderers.length],
    new TrackSelection[renderers.length],
    null);
    period = new Timeline.Period();
    playbackParameters = PlaybackParameters.DEFAULT;
    seekParameters = SeekParameters.DEFAULT;
    playbackSuppressionReason = PLAYBACK_SUPPRESSION_REASON_NONE;
    eventHandler =
    new Handler(looper) {
    @Override
    public void handleMessage(Message msg) {
    ExoPlayerImpl.this.handleEvent(msg);
    }
    };
    playbackInfo = PlaybackInfo.createDummy(/* startPositionUs= */ 0, emptyTrackSelectorResult);
    pendingListenerNotifications = new ArrayDeque<>();
    internalPlayer =
    new ExoPlayerImplInternal(
    renderers,
    trackSelector,
    emptyTrackSelectorResult,
    loadControl,
    bandwidthMeter,
    playWhenReady,
    repeatMode,
    shuffleModeEnabled,
    eventHandler,
    clock);
    internalPlayerHandler = new Handler(internalPlayer.getPlaybackLooper());
    }

    上面是最终调用的地方,在真正使用的时候很方便,使用ExoPlayer封装好的方法即可:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    // 是否需要根据扩展名来选择合适的解码器
    boolean preferExtensionDecoder = false;
    /**
    * 初始化Player
    */
    private void initPlayer() {
    if (player == null) {
    TrackSelection.Factory trackSelectionFactory = new RandomTrackSelection.Factory();
    trackSelector = new DefaultTrackSelector(trackSelectionFactory);

    RenderersFactory renderersFactory =
    buildRenderersFactory(preferExtensionDecoder);
    player = ExoPlayerFactory.newSimpleInstance(
    /* context= */ this, renderersFactory, trackSelector);
    player.addListener(new PlayerEventListener());
    player.setPlayWhenReady(startAutoPlay);
    player.addAnalyticsListener(new EventLogger(trackSelector));
    }
    }

    // 创建RendererFactory 以获取Renderer
    private RenderersFactory buildRenderersFactory(boolean preferExtensionDecoders) {
    @DefaultRenderersFactory.ExtensionRendererMode
    int extensionRendererMode = preferExtensionDecoders
    ? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
    : DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON;

    return new DefaultRenderersFactory(/* context= */ this)
    .setExtensionRendererMode(extensionRendererMode);
    }
  2. 与PlayerView绑定
    ExoPlayer已经封装好了对应的PlayerView(com.google.android.exoplayer2.ui.PlayerView),在布局文件声明好后,可以直接使用,具体代码如下:

    1
    2
    3
    4
    5
    6
    // 3. Player与PlayerView绑定
    playerView = findViewById(R.id.player_view);
    playerView.setControllerVisibilityListener(this);

    playerView.setPlayer(player);
    playerView.setPlaybackPreparer(this);// this为实现了com.google.android.exoplayer2.PlaybackPrepare接口的Activity

    上面的代码就完成了Player和PlayerView的绑定操作。

  3. 准备播放源(MediaSource)
    接下来就是播放的设置了,如下:

    1
    2
    3
    // 4. 准备MediaSource
    MediaSource mediaSource = getMediaSource();
    player.prepare(mediaSource);

    getMediaSource()代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    private MediaSource getMediaSource() {
    List<String> mediaList = Arrays.asList(getResources().getStringArray(R.array.music_media_source_list));
    // Uri uri = Uri.parse("https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4");
    Uri uri = Uri.parse("https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-137.mp4");
    DataSource.Factory dataSource = new DefaultDataSourceFactory(mContext, Util.getUserAgent(mContext, mContext.getPackageName()));
    MediaSource mediaSource = new ProgressiveMediaSource.Factory(dataSource).createMediaSource(uri);

    return mediaSource;
    }
  4. 播放控制
    封装的PlayerView中包含了控制播放的View实现,也可以自行封装,这里不做详述

  5. 资源释放
    调用ExoPlayer的release()方法进行资源的释放

基本结构

先看核心库的目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
.
├── analytics # 分析模块,里面的AnalyticsCollector实现了ExoPlayer的各种listener
├── audio # 音频处理模块
├── decoder # 解码模块
├── drm # 数字版权管理模块
├── extractor # 从特定格式的容器中提取相应的媒体数据,支持amr、flv、mkv、mp3、mp4、ogg、rawcc、ts和wav等格式的提取
│   ├── amr
│   ├── flv
│   ├── mkv
│   ├── mp3
│   ├── mp4
│   ├── ogg
│   ├── rawcc
│   ├── ts # MPEG-2 TS
│   └── wav
├── mediacodec # 多媒体编解码模块,主要是对MediaCodec的封装操作
├── metadata # 元数据处理模块
│   ├── emsg # Event Message处理
│   ├── id3
│   └── scte35
├── offline # 离线播放支持模块(主要包含下载功能的封装)
├── scheduler # 服务调度模块
├── source # 多媒体源处理模块
│   ├── ads
│   └── chunk
├── text # 字幕模块,支持的字母格式:cea、dvb、pgs、ssa等
│   ├── cea
│   ├── dvb
│   ├── pgs
│   ├── ssa
│   ├── subrip
│   ├── ttml
│   ├── tx3g
│   └── webvtt
├── trackselection # 轨道选择模块
├── upstream # 数据流
│   ├── cache # 缓存
│   └── crypto # 加密处理
├── util # 辅助模块(包括工具类、辅助数据结构)
└── video # 视频模块
└── spherical

几个比较重要的接口

  • Player,定义了5个子接口如下,同时还提供了所有播放操作API;

    • AudioComponent,音频控件,提供音频监听设置、属性设置、Aux效果设置、声音设置等API;
    • VideoComponent,视频控件,提供视频监听设置、缩放模式设置、帧元数据监听设置以及Surface相关设置等API;
    • TextComponent,字幕控件,提供添加和移除字幕输出的设置API;
    • MetadataComponent,元数据控件,提供音视频基本信息输出控制API;
    • EventListener,事件监听,提供基本的播放状态改变监听API;
  • ExoPlayer,继承自Player,同时额外扩展出了一些方法(如prepare、retry、setSeekParameters等),对要播放的对象没有做过多额外的处理,而是通过播放组件来实现。

    • 常用四个组件介绍

      • MediaSource,定义多媒体数据源,该类的功能是从Uri中读取多媒体文件二进制数据,MediaSource对象在ExoPlayer的prepare方法中传入;

      • TrackSelector,轨道提取器,从MediaSource提取各个轨道的二进制数据,交给Renderer渲染;

      • Renderer,对多媒体中的各个轨道(音轨、视频轨、字幕轨等)数据进行渲染,即将二进制文件渲染成声音、画面播放出来。其对象在创建ExoPlayer时注入;

      • LoadControl,字面意思就是加载控制,可以对MediaSource进行控制,如什么时候缓存,缓存多少。(如果需要修改缓存策略,可以从这里入手)其对象也是在创建ExoPlayer时注入;

        上面这四种组件ExoPlayer都提供了默认的实现,能满足大部分需求,当然也能很方便的扩展。比如,可以自定义LoadControl来更改播放器的缓冲策略,或自定义Renderer来渲染Android本身不支持的编解码器。

    • ExoPlayer线程模型
      ExoPlayer的线程主要分为以下几种

      • Application Tread,通常指的是主线程,主要进行Player实例的获取、监听器的绑定;
      • Playback Tread,播放线程,注入ExpPlayer的组件在该线程中被调用
      • Background Tread,组件的一些初始化操作如数据加载(MediaSource)、轨道提取(TrackSelector)、渲染(Renderer)
      • Playback Tread和Application Thread通过Handler进行通信

几个比较重要的类

  • BasePlayer,实现Player接口部分方法,即提供部分操作的通用实现,后面的ExoPlayerImpl和SimpleExoPlayer都继承该类
  • SimpleExoPlayer,ExoPlayerImpl的一层包装,内部持有ExoPlayerImpl实例,其实就是ExoPlayerImpl的一层包装;
  • ExoPlayerImpl,ExoPlayer的实现类,提供Player和ExoPlayer各种API的具体实现;
  • ExoPlayerImplInternal,ExoPlayerImpl内部行为的实现类,ExoPlayerImpl的大多数操作最终通过该类来实现的。

MediaSource

定义并提供可供ExoPlayer播放的数据源,有两大职责:

  1. 开始播放播放源内容有改变时提供定义Media结构的TimeLine,主要通过调用在prepareSource传入的SourceInfoRefreshListener实例的onSourceInfoRefreshed来实现;

  2. 为当前时间线上的某段时间提供MediaPeriod实例,并提供读取媒体数据的方式
    其继承关系如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    MediaSource #接口定义
    BaseMediaSource #接口的基本实现,类实例的复用处理,并维护MediaSourceEventListener类表
    HlsMediaSource #Hls类型的MediaSource实现
    SilenceMediaSource
    ProgressiveMediaSource
    SingleSampleMediaSource
    DummyMediaSource in ConcatenatingMediaSource
    ExtractorMediaSource
    DashMediaSource #Dash类型的MediaSource实现
    SsMediaSource #Ss类型MediaSource实现
    CompositeMediaSource
    MergingMediaSource
    AdsMediaSource
    ConcatenatingMediaSource
    DynamicConcatenatingMediaSource
    LoopingMediaSource #支持循环的MediaSource
    ClippingMediaSource

    TrackSelector

    为播放器的渲染器(Renderer)选择合适的渲染轨道,ExoPlayer提供的默认实现(DefaultTrackSelector)能满足大多数情形。播放器(ExoPlayer)和轨道选择器(TrackSelector)之间的交互如下:

  • 播放器创建的时候,会调用TrackSelector的init方法来初始化TrackSelector的轨道失效监听器(InvalidationListener)和带宽监听器(BandwidthMeter);

  • 播放器需要选择轨道时会调用TrackSelector的selectTracks方法(通常发生在播放开始阶段以及监听失效时);

  • 在进行Media合并操作之前,也可能发生轨道选择的操作;

  • TrackSelector通过调用初始化时传递进来的InvalidationListener的回调方法来通知ExoPlayer轨道选择失效;

    Renderer

    将从SampleStream中读取的内容渲染出来,提供Renderer的状态控制,SampleStream的操作相关的API。其继承关系如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    Renderer #接口定义
    BaseRenderer #提供了Renderer和RendererCapabilities两个接口的基本实现,同时提供了一系列的on开头的毁掉方法
    SimpleDecoderAudioRenderer # 采用SimpleDecoder作为解码器解码渲染音频
    FfmpegAudioRenderer # 基于Ffmpeg解码器的实现
    LibflacAudioRenderer # 基于Flac解码器的实现
    LibopusAudioRenderer # 基于Opus解码器的实现
    MediaCodecRenderer # 采用MediaCodec进行解码渲染
    MediaCodecAudioRenderer # 采用MediaCodec进行音频的解码渲染
    MediaCodecVideoRenderer # 采用MediaCodec进行视频的解码渲染
    LibvpxVideoRenderer # 基于Vp9实现的视频渲染器
    CameraMotionRenderer # 解析运动追踪的渲染器
    MetadataRenderer # 元数据渲染器
    TextRenderer # 字幕渲染器
    NoSampleRenderer

    LoadControl

    主要职责是进行媒体资源的缓存控制,提供了基本的回调方法和缓存控制方法。ExoPlayer提供了一个默认实现(DefaultLoadControl,该类采用建造者模式实现),可以通过Builder.build() 来获取其实力

优点和不足

  • 优点
    • 默认支持提供倍速播放支持,通过设置给ExoPlayer设置PlaybackParameters即可;
    • TrackSelector提供了多种配置参数,具体参见DefaultTrackSelector#Parameters
  • 不足
    • 比较耗电

总结

ExoPlayer充分利用了组件化的思想,将多媒体播放流程中一些常用常见的概念抽象成组件,提供了组件的默认实现,同时也支持个性化扩展。

相关控件

TextureView

官方解释如下:

1
2
3
* A TextureView can be used to display a content stream. Such a content
* stream can for instance be a video or an OpenGL scene. The content stream
* can come from the application's process as well as a remote process.

大致意思就是,一个用来展示流内容(比如视频或者OpenGL的场景的View,流内容既可以由应用进程提供,也可以通过远程获取。值得注意的地方:

  1. 只能被用在硬件加速的窗口中,否则将啥都不绘制;
  2. SurfaceView不同的是,TextureView并不会单独开辟一个窗口,你可以像操作一个普通的View那样操作它;
  3. 使用非常简单,它内部依靠SurfaceTexture来进行内容的渲染,

SurfaceView

关于SurfaceView,将在Android SurfaceView详解中详细介绍

参考链接