[关闭]
@begeekmyfriend 2016-12-28T07:16:00.000000Z 字数 14666 阅读 2224

ExoPlayer 2.x开发文档

ExoPlayer是Google团队维护的一款安卓开源多媒体播放器,它基于安卓MediaPlayer API提供了本地和网络音视频回放功能,许可证BSD,纯Java实现。本文主要基于ExoPlayer 2.x以上版本,针对应用层开发人员对播放器的定制和扩展进行解释说明。


1. 支持多媒体格式

Supported Formats


2. 安卓设备兼容性

Supported Devices


3. 运行Demo

3.1 运行环境搭建

3.2 加载节目单

节目单是以json文件存储的,有三种加载方式。

3.2.1 编辑assets/media.exolist.json

这个json文件维护了demo中的sample列表,改写它可以增删sample,格式如下([O]表示该项可不写):

  1. [
  2. {
  3. "name": "Name of heading",
  4. "samples": [
  5. {
  6. "name": "Name of sample",
  7. "uri": "The URI/URL of the sample",
  8. "extension": "[O] Sample type hint. Values: mpd, ism, m3u8",
  9. "prefer_extension_decoders": "[O] Boolean to prefer extension decoders",
  10. "drm_scheme": "[O] Drm scheme if protected. Values: widevine, playready",
  11. "drm_license_url": "[O] URL of the license server if protected",
  12. "drm_key_request_properties": "[O] Key request headers if protected"
  13. },
  14. ...etc
  15. ]
  16. },
  17. ...etc
  18. ]

播放列表(playlist)包含多个samples,格式如下:

  1. [
  2. {
  3. "name": "Name of heading",
  4. "samples": [
  5. {
  6. "name": "Name of playlist sample",
  7. "prefer_extension_decoders": "[O] Boolean to prefer extension decoders",
  8. "drm_scheme": "[O] Drm scheme if protected. Values: widevine, playready",
  9. "drm_license_url": "[O] URL of the license server if protected",
  10. "drm_key_request_properties": "[O] Key request headers if protected"
  11. "playlist": [
  12. {
  13. "uri": "The URI/URL of the first sample in the playlist",
  14. "extension": "[O] Sample type hint. Values: mpd, ism, m3u8"
  15. },
  16. {
  17. "uri": "The URI/URL of the first sample in the playlist",
  18. "extension": "[O] Sample type hint. Values: mpd, ism, m3u8"
  19. },
  20. ...etc
  21. ]
  22. },
  23. ...etc
  24. ]
  25. },
  26. ...etc
  27. ]

如果涉及数字版权管理(DRM),可将drm_key_request_properties一项指定为一组包含字符串属性的对象。

  1. "drm_key_request_properties": {
  2. "name1": "value1",
  3. "name2": "value2",
  4. ...etc
  5. }

3.2.2 加载外部exolist.json文件

外部json文件命名必须符合*.exolist.json规范。例如将json文件放入https://ximalaya.com/samples.exolist.json,可使用adb命令打开:

adb shell am start -d https://ximalaya.com/samples.exolist.json

在浏览器或者email中点击*.exolist.json链接照样能够在demo中加载,这样一来方便节目单的分发。

3.2.3 发起一个intent

Intent能够绕过节目单从而直接发起回放。你可以在com.google.android.exoplayer.demo.action.VIEW设置intent行为来播放一条URI相关的sample,使用adb命令如下:

adb shell am start -a com.google.android.exoplayer.demo.action.VIEW \
-d https://ximalaya.com/sample.mp4

单条sample发起intent的附加属性包括:

当使用adb shell am start发起一个intent,附加选项使用如下:

播放列表的intent行为可以在com.google.android.exoplayer.demo.action.VIEW_LIST中设置,使用名为uri_list的附加字符串数组:

adb shell am start -a com.google.android.exoplayer.demo.action.VIEW_LIST \
--esa uri_list https://a.com/sample1.mp4, https://b.com/sample2.mp4

附加选项包括:


4. 播放器开发的一般流程

以代码示例ExoPlayer二次开发,以及主要API使用方式。

4.1 创建播放器实例

SimpleExoPlayer是对ExoPlayer的默认扩展实现,创建时需要注册以下几个组件:BandwidthMeter以回调的形式通知上层播放过程中数据流量的监测;TrackSelector负责播放过程中对媒体数据源自带格式参数的动态切换和选择;LoadControl内部实现了播放器数据缓存的控制。组件的具体说明见四大组件一章。

  1. // 1. Create a default TrackSelector
  2. Handler mainHandler = new Handler();
  3. BandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
  4. TrackSelection.Factory videoTrackSelectionFactory = new AdaptiveVideoTrackSelection.Factory(bandwidthMeter);
  5. TrackSelector trackSelector = new DefaultTrackSelector(mainHandler, videoTrackSelectionFactory);
  6. // 2. Create a default LoadControl
  7. LoadControl loadControl = new DefaultLoadControl();
  8. // 3. Create the player
  9. SimpleExoPlayer player = ExoPlayerFactory.newSimpleInstance(context, trackSelector, loadControl);

4.2 绑定到View控件

SimplePlayerView封装了多媒体渲染的SurfaceViewUI控件,它生成于安卓应用layout目录中的xml文件。绑定如下:

  1. // Bind the player to the view.
  2. simpleExoPlayerView.setPlayer(player);

4.3 进入准备状态

你需要创建一个MediaSource实例并注册给player,下面代码演示了播放MP4文件的样例:

  1. // Measures bandwidth during playback. Can be null if not required.
  2. DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
  3. // Produces DataSource instances through which media data is loaded.
  4. DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(this, Util.getUserAgent(this, "yourApplicationName"), bandwidthMeter);
  5. // Produces Extractor instances for parsing the media data.
  6. ExtractorsFactory extractorsFactory = new DefaultExtractorsFactory();
  7. // This is the MediaSource representing the media to be played.
  8. MediaSource videoSource = new ExtractorMediaSource(mp4VideoUri, dataSourceFactory, extractorsFactory, null, null);
  9. // Prepare the player with the source.
  10. player.prepare(videoSource);

player.prepare发送了一个异步消息,指示播放器进入缓冲状态并等待媒体数据内容的到来,读取后送入解码器,就可以自动播放多媒体内容了。

4.4 播放器控制

一旦进入准备状态,你可以通过player对象各种方法,比如player.setPlayWhenReady启动和暂停播放,player.seekTo对播放媒体进行seek等等。这些操作都是通过异步消息实现,不会阻塞应用程序。更多操作方法参见API参考文档一章。

4.5 释放播放器

退出播放状态请调用player.release,它将释放播放器相关资源,比如解码器、数据缓存、消息机制等。

特别提示:Activity线程和播放器线程之间通信需要handler和listener两个句柄(MainHandlerEventLogger),这有可能导致资源泄漏的风险。比如当Activity释放时,如果播放器还在发送消息或者延迟发送,这种消息是无法被接收以及处理的,系统内部消息队列仍是工作状态,handler和listener对象就不会被垃圾收集自动析构。由于Activity拥有这两个句柄的强引用,所以实际上整个Activity资源都不会析构。所以退出PlayActivity的时候记得一定要调用player.release确保不存在资源泄漏,而不能写成player = null了事。


5. 四大组件

在创建播放器或者进入播放状态时,我们可以通过注册组件实现参数调整和定制,但需要确保上层业务逻辑和播放器内部状态松耦合,ExoPlayer线程模型保证了这一点,它分离了应用线程、内部播放线程和后台线程。应用线程负责与下层交互,基本属于以下三种:

前两种通过消息队列异步操作,不会对播放器造成阻塞,状态查询仅仅涉及对上层可见部分,同播放器内部状态解耦。上层与组件的通信机制,是通过创建播放器并注册组件的同时,传递一个mainHandler句柄下去,以便底层向应用线程发送消息,对应地,传递eventLogger对象就是为了在应用线程中监听并处理事件。

exoplayer-threading-model

ExoPlayer内部是各种主循环,也是根据不同播放模式区分的。主循环依赖很多组件,组件之间基本上是通过对象的包含和封装联系起来的,这里主要介绍四大组件。

5.1 MediaSource

MediaSource定义了一个媒体内容的实例,内部实现了对媒体数据的加载、解析和传输。你需要在调用player.prepare时传递这个对象参数来注册一个媒体实例。

播放器默认替我们实现了四个基本MediaSource扩展,涵盖了所有媒体格式,它们分别是ExtractorMediaSourceHlsMediaSourceDashMediaSourceSsMediaSource。其中由容器封装格式的多媒体(比如MP4、FLV等)都属于ExtractorMediaSource,其它网络流媒体分别是后三种,包括HLSDashSmoothStreaming

另外还有三个高级扩展,它们分别是:MergingMediaSourceLoopingMediaSourceConcatenatingMediaSource。分别实现了对上述四种基本媒体内容的合成、循环和拼接,从而实现各种更高级的播放模式。

MediaSource实例的创建一般需要以下5个对象参数:

5.2 TrackSelector

对上层来说其实等价于MappingTrackSelector的扩展,因为它实际上将播放器内部的renderers和tracks映射起来,用途就是可以在播放过程中对媒体自带的格式参数(比如多个分辨率、码率)进行动态切换和选择,也可以查询渲染器的类型和状态等信息。

TrackSelector实例需要在播放器创建中注册,但一般不需要上层开发人员去定制。在MediaSource加载和解析媒体数据内容时内部自动填充,在上层对播放器进行控制操作时内部自动更改状态。换句话说,TrackSelector实例对上层来说,状态不可更改(immutable),但是可见的。你可以通过getCurrentMappedTrackInfo方法获取MappedTrackInfo实例来查询关联的tracks,你也可以通过setRendererDisabled方法来使能和禁用renders。

5.3 LoadControl

LoadControl主要针对媒体数据源缓冲加载控制。比如设置数据缓冲时间最大值和最小值,缓冲多大时才能恢复播放等等。一般创建实例设置默认参数,开发人员可以更改默认值。LoadControl在每次新创建播放器时注册以生效。

5.4 Renderer

Renderer通过扩展安卓自带的MediaCodecAudioTrack等多媒体原生接口实现音视频的渲染。在播放器内部默认自动创建实例,一般包括video、audio、subtitle和text四种类型。一般不需要应用开发人员关心,但可以通过player.getRendererType方法获取其类型,通过trackSelector.getRendererDisabled方法获取其状态。


6 Timeline组件

虽然Timeline在官方文档里不属于四大组件,但上层仍需要关注并使用它,故单独列出说明。对于上层来说,Timeline实例可视为播放状态下具有一段时长的媒体,对于直播这种动态不确定时长的媒体,一个Timeline实例可视为当前播放媒体的快照。对于上层来说,Timeline状态不可更改(immutable),但是可见,这一点类似于TrackSelector组件。

6.1 period和window

一个Timeline实例由一个或多个媒体时段(以下简称period)和窗口(以下简称window)对象构成。其中period实例表示逻辑概念上的媒体时段,比如一个媒体文件;window实例可以跨越一个或多个period,指定了当前可以播放的区域,以及一些附加信息(比如是否支持seek等),每个window定义一个默认位置,指明进入播放状态后从何处开始播放。

Timeline.Period

上图是从period视角观察window。假设一个Timeline实例有两个period对象以及一个window对象,window横跨两个periods,那么两个periods各自维护了一个windowIndex表示自己所属的window对象,同时各自维护一个positionInWindow表明period起始处相对于window对象起始处的偏移位置,其中period1.positionInWindow是负数,表示period1起始超前于所属window起始,period2.positionInWindow是正数,表示period2起始处滞后于所属window起始。

Timeline.Window

上图是从window视角观察period。同样假设一个Timeline实例有两个period对象以及一个window对象,window横跨两个periods,那么window维护了一个firstPeriodIndex和一个lastPeriodIndex,分别表示跨越的第一个period以及最后一个period。positionInFirstPeriod表示window起始处与第一个period起始处的偏移,duration表示window自身时长,defaultStartPosition表示播放点与window自身起始处的偏移。

6.2 Timeline与播放模式

下面枚举了Timeline在各个播放模式下的图例。

Single media file or on-demand stream

上图是单个媒体模式下的Timeline,各有一个period和window。window实例横跨整个period,表明period所有部分都可以播放,window的默认位置一般是period起始处。

Playlist of media files or on-demand streams

上图是多个媒体文件或者实时流的播放列表模式,由多个period和window实例构成,并且一一对应。每个window贯穿整个period,默认位置是起始点。其属性内容(比如时长和能否seek)只有在播放器缓冲到对应的媒体文件和流才能知道。

Live stream with limited availability

上图是一个period构成的直播流模式,时长未知,因为它会不断广播更多内容。如果其内容仅保留某个限定时段可播放,window会指定一个非0位置作为播放起始点。对于直播流,isDynamic属性将为true,其默认位置一般靠近直播边缘(live edge)。

Live stream with indefinite periods

上图同样是一个period构成的直播流模式,只是可播放时段是不定的。Timeline类似于上面限定时段直播流模式,但window实例可以跨越到period起始处,表明所有之前广播内容仍是有效的。

Live stream with multiple periods

上图是一个被分离成多个periods直播流模式,比如存在广告边界。Timeline类似于前面限定时段直播流模式,只是window实例可以横跨多个period。在不定时段直播流模式下也可能存在多个periods的情况。

On-demand pre-roll followed by live stream

上图是单个媒体文件、实时流和直播流,多个periods串联模式。比如当一段媒体的片头结束后,直播流将从靠近直播边缘的默认位置开始播放。

6.3 Timeline的使用

在demo的PlayerActivity.java中示范了如何使用Timeline在退出播放状态前保留媒体数据位置,以便下次进入播放状态时进行seek操作。代码如下:

  1. private void releasePlayer() {
  2. shouldAutoPlay = player.getPlayWhenReady();
  3. playerWindow = player.getCurrentWindowIndex();
  4. playerPosition = C.TIME_UNSET;
  5. Timeline timeline = player.getCurrentTimeline();
  6. if (timeline != null && timeline.getWindow(playerWindow, window).isSeekable) {
  7. playerPosition = player.getCurrentPosition();
  8. }
  9. player.release();
  10. ...
  11. }

player.getPlayWhenReady返回值表明下次是继续播放还是从头开始,player.getCurrentWindowIndex获取当前播放窗口索引,player.getCurrentTimeline获取当前播放时间线,timeline.getWindow获取当前播放窗口,如果该窗口是可以seek的,调用player.getCurrentPosition获取媒体位置,最后释放播放器。

  1. private void initializePlayer() {
  2. if (playerPosition == C.TIME_UNSET) {
  3. player.seekToDefaultPosition(playerWindow);
  4. } else {
  5. player.seekTo(playerWindow, playerPosition);
  6. }
  7. player.setPlayWhenReady(shouldAutoPlay);
  8. }

在创建播放器以及进入播放状态时,会去检查之前保留的playerPositionshouldAutoPlay。如果上次退出时媒体位置保留下来,则对上次保留的窗口进行seek操作;如果未保留位置或者媒体本身无法seek,则以窗口默认位置为播放点。


7. DataSource的定制

理论上ExoPlayer的所有组件都可以定制,只要包含Defaultxxx前缀的都是默认实现。此处我们只关注可以被移植到应用层的DataSource组件。

前面说过DataSource负责MediaSource中数据源的加载,因此上层开发人员可以通过DataSource定制开发实现数据源的业务逻辑。实际上DataSource.java声明了一个抽象接口,你需要实现以下回调:

  1. // 工厂实例
  2. interface Factory {
  3. // 创建一个DataSource实例
  4. DataSource createDataSource();
  5. }
  6. /**
  7. * 打开数据源并读取指定数据。
  8. * 注意:如果IOException被抛出,调用者必须调用close()来确保残余状态得到清理。
  9. * 参数:DataSpec,定义数据源对象。
  10. * 抛出:IOException,可扩展DataSourceException并自定义出错原因。
  11. * 返回:数据源可以被读取的字节数,即DataSpec.length的值。如果长度无法 * 解析,返回C.LENGTH_UNSET。
  12. */
  13. long open(DataSpec dataSpec) throws IOException;
  14. /**
  15. * 从打开数据源中读取字节,直到最大长度或者无数据可读为止,否则调用会阻塞直到最少一个字节数据被读取。
  16. * 参数:buffer,数据要写入的缓存;
  17. * 参数:offset,数据写入buffer的起始位置;
  18. * 参数:readLength,限定读取最大字节数。
  19. * 返回:实际读取字节数。如果没有数据可读,返回C.RESULT_END_OF_INPUT
  20. * 抛出:IOException
  21. */
  22. int read(byte[] buffer, int offset, int readLength) throws IOException;
  23. /**
  24. * 当数据源被打开,返回URI,等价于当初传递给open(DataSpec)中DataSpec的内容,除非发生重定向,URI内容也会改变。
  25. * 返回:数据源URI,如果数据源没有打开过,返回null。
  26. */
  27. Uri getUri();
  28. /**
  29. * 关闭数据源。
  30. * 注意:该方法在对应open方法抛出异常时也必须调用。
  31. * 抛出:IOException,当关闭发生错误。
  32. */
  33. void close() throws IOException;

我们可以根据不同的播放模式定制DataSource,一般有三种播放模式:

下面从三种模式分别讲解DataSource的实现原理和定制方法。

7.1 本地文件

ExoPlayer已经为我们默认实现了几种,包括:AssertDataSourceFileDataSourceRawResourceDataSource等,一般只用FileDataSource即可。

从上层视角看,一个简易描述的播放流程如下:当用户点击SampleChooserActivity中的某条本地MP4的sample后,根据前面提到的播放器一般流程所描述那样,创建几个组件之后,注册给生成SimpleExoPlayer播放器实例。这里注意一下MediaSource组件的生成。

以上为上层发起流程,下面是回调实现。

此处顺便说一下如何估计播放过程中的码率(带宽),ExoPlayer已经有一个默认实现DefaultBandwidthMeter,它通过前面提到的TransferListener<Object>回调来对消耗的字节数和时间多次采样进行加权移动平均(Weighted Moving Average)计算,实现在SlidingPercentile.java中。注意,一次采样周期就是onTransferEnd调用。

7.2 网络点播(VOD)

网络点播一般选择HttpDataSource,ExoPlayer已经提供了一个DefaultHttpDataSource实现,你可以自己定制它。其框架与前一节本地播放所述基本一致,只是行为上将文件操作替换成了HttpURLConnection,连接方式一般为长连接(HLS是切片下载所以是短连接),由于是整个文件的分发,所以在响应头中可以看到Content-Length字段。

假设点播媒体后缀为mp4,则MediaSource组件为ExtractorMediaSource,参数包括URI、HttpDataSource工厂实例加载数据源、Mp4Extractor工厂实例解析媒体格式、MainHandler上传消息以及EventLogger监听消息。

若点播媒体后缀为m3u8,则属于HLS。MediaSource组件为HlsMediaSource,参数包括URI、HttpDataSource工厂实例加载数据源、MainHandler上传消息以及EventLogger监听消息。值得注意的是,此处没有parser工厂实例做参数,由于HLS包含variant列表,很有可能存在多样化的媒体分片,所以实际上播放器每次加载分片时自动去创建HlsChunkSource实例,多态地解析每一个分片格式。

若点播媒体后缀为mpd,则属于MPEG-DASH。MediaSource组件为DashMediaSource,参数包括URI、HttpDataSource工厂实例加载数据源、DashChunkSource工厂实例解析媒体格式、MainHandler上传消息以及EventLogger监听消息。

若点播媒体后缀为ism,则属于Smart Streaming。MediaSource组件为SsMediaSource,参数包括URI、HttpDataSource工厂实例加载数据源、SsChunkSource工厂实例解析媒体格式、MainHandler上传消息以及EventLogger监听消息。

MediaSource组件注册给player.prepare后,上层DataSource就可以等待open回调了。这里结合DefaultHttpDataSource着重讲解一下HLS点播回调过程。

7.3 网络直播(Live)

在直播模式下,播放端又叫拉流。按网络协议划分三种类型:HTTP-FLV、HLS和RTMP。直播都不支持seek,open回调方法返回值LENGTH_UNSET,表明媒体长度未知。

7.3.1 HTTP-FLV

要说明的是这不是用HTTP点播FLV文件,而是FLV格式的流。虽然URL一致,但与点播不同的是,你在响应头中看不到Content-Length字段,这样拉流端可以一直接受数据。

7.3.2 HLS

DataSource流程同点播,使用HttpDataSource。它的媒体数据内容是MPEG-TS格式的。

7.3.3 RTMP

ExoPlayer原生不支持RTMP,所以需要自己实现RTMP拉流代码,并适配成RtmpDataSource,与其它DataSource在接口上具备一致性。它的媒体数据内容是FLV格式的。

8 API参考文档

可由应用开发直接调用,主要参见demo里的PlayActivity.javaTrackSelectionHelper.java

ExoPlayer接口:播放控制操作;

MappingTrackSelector接口:媒体规格参数选择和切换。

Timeline接口:播放媒体时间线。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注