@begeekmyfriend
2016-12-28T07:16:00.000000Z
字数 14666
阅读 2795
ExoPlayer是Google团队维护的一款安卓开源多媒体播放器,它基于安卓MediaPlayer API提供了本地和网络音视频回放功能,许可证BSD,纯Java实现。本文主要基于ExoPlayer 2.x以上版本,针对应用层开发人员对播放器的定制和扩展进行解释说明。
从git仓库中下载源代码。
使用Android Studio从根目录打开工程。
在Android Studio上方选择demo
运行配置选项。
启动后第一个页面是SampleChooserActivity
,从中选择一个sample,之后进入下一个播放页面PlayerActivity
,可以在界面进行回放控制和轨道选择。EventLogger
是各种回放状态事件回调。
软件解码器扩展。针对VP9、Opus、FLAC以及FFmpeg(音频)。构建方法,在Android Studio的Build Variant视界将demoDebug
改为demo_extDebug
,运行。
节目单是以json文件存储的,有三种加载方式。
assets/media.exolist.json
这个json文件维护了demo中的sample列表,改写它可以增删sample,格式如下([O]表示该项可不写):
[
{
"name": "Name of heading",
"samples": [
{
"name": "Name of sample",
"uri": "The URI/URL of the sample",
"extension": "[O] Sample type hint. Values: mpd, ism, m3u8",
"prefer_extension_decoders": "[O] Boolean to prefer extension decoders",
"drm_scheme": "[O] Drm scheme if protected. Values: widevine, playready",
"drm_license_url": "[O] URL of the license server if protected",
"drm_key_request_properties": "[O] Key request headers if protected"
},
...etc
]
},
...etc
]
播放列表(playlist)包含多个samples,格式如下:
[
{
"name": "Name of heading",
"samples": [
{
"name": "Name of playlist sample",
"prefer_extension_decoders": "[O] Boolean to prefer extension decoders",
"drm_scheme": "[O] Drm scheme if protected. Values: widevine, playready",
"drm_license_url": "[O] URL of the license server if protected",
"drm_key_request_properties": "[O] Key request headers if protected"
"playlist": [
{
"uri": "The URI/URL of the first sample in the playlist",
"extension": "[O] Sample type hint. Values: mpd, ism, m3u8"
},
{
"uri": "The URI/URL of the first sample in the playlist",
"extension": "[O] Sample type hint. Values: mpd, ism, m3u8"
},
...etc
]
},
...etc
]
},
...etc
]
如果涉及数字版权管理(DRM),可将drm_key_request_properties
一项指定为一组包含字符串属性的对象。
"drm_key_request_properties": {
"name1": "value1",
"name2": "value2",
...etc
}
外部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中加载,这样一来方便节目单的分发。
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的附加属性包括:
extension [mpd|ism|m3u8]
:表明sample类型。prefer_extension_decoders [TRUE|FALSE]
:扩展解码器是否设置为优先使用。drm_scheme_uuid [0123456789]
:DRM方案UUID。drm_license_url [https://proxy.uat.widevine.com]
:DRM许可证服务器URL。drm_key_request_properties [name1, value1, name2, value2]
:DRM请求秘钥头部。当使用adb shell am start
发起一个intent,附加选项使用如下:
--es
(e.g. --es extension m3u8
)--ez
(e.g. -ez prefer_extension_decoders TRUE
)--esa
(e.g. --esa drm_key_request_properties name1, value1, name2, value2
)播放列表的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
附加选项包括:
extension_list [mpd|ism|m3u8]
:表明sample类型,可写0个或多个。prefer_extension_decoders
, drm_scheme_uuid
, drm_license_url
以及drm_key_request_properties
,说明同上。4. 播放器开发的一般流程
以代码示例ExoPlayer二次开发,以及主要API使用方式。
SimpleExoPlayer
是对ExoPlayer的默认扩展实现,创建时需要注册以下几个组件:BandwidthMeter
以回调的形式通知上层播放过程中数据流量的监测;TrackSelector
负责播放过程中对媒体数据源自带格式参数的动态切换和选择;LoadControl
内部实现了播放器数据缓存的控制。组件的具体说明见四大组件一章。
// 1. Create a default TrackSelector
Handler mainHandler = new Handler();
BandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
TrackSelection.Factory videoTrackSelectionFactory = new AdaptiveVideoTrackSelection.Factory(bandwidthMeter);
TrackSelector trackSelector = new DefaultTrackSelector(mainHandler, videoTrackSelectionFactory);
// 2. Create a default LoadControl
LoadControl loadControl = new DefaultLoadControl();
// 3. Create the player
SimpleExoPlayer player = ExoPlayerFactory.newSimpleInstance(context, trackSelector, loadControl);
SimplePlayerView
封装了多媒体渲染的Surface
和View
UI控件,它生成于安卓应用layout
目录中的xml文件。绑定如下:
// Bind the player to the view.
simpleExoPlayerView.setPlayer(player);
你需要创建一个MediaSource
实例并注册给player,下面代码演示了播放MP4文件的样例:
// Measures bandwidth during playback. Can be null if not required.
DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
// Produces DataSource instances through which media data is loaded.
DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(this, Util.getUserAgent(this, "yourApplicationName"), bandwidthMeter);
// Produces Extractor instances for parsing the media data.
ExtractorsFactory extractorsFactory = new DefaultExtractorsFactory();
// This is the MediaSource representing the media to be played.
MediaSource videoSource = new ExtractorMediaSource(mp4VideoUri, dataSourceFactory, extractorsFactory, null, null);
// Prepare the player with the source.
player.prepare(videoSource);
player.prepare
发送了一个异步消息,指示播放器进入缓冲状态并等待媒体数据内容的到来,读取后送入解码器,就可以自动播放多媒体内容了。
一旦进入准备状态,你可以通过player对象各种方法,比如player.setPlayWhenReady
启动和暂停播放,player.seekTo
对播放媒体进行seek等等。这些操作都是通过异步消息实现,不会阻塞应用程序。更多操作方法参见API参考文档一章。
退出播放状态请调用player.release
,它将释放播放器相关资源,比如解码器、数据缓存、消息机制等。
特别提示:Activity线程和播放器线程之间通信需要handler和listener两个句柄(
MainHandler
和EventLogger
),这有可能导致资源泄漏的风险。比如当Activity释放时,如果播放器还在发送消息或者延迟发送,这种消息是无法被接收以及处理的,系统内部消息队列仍是工作状态,handler和listener对象就不会被垃圾收集自动析构。由于Activity拥有这两个句柄的强引用,所以实际上整个Activity资源都不会析构。所以退出PlayActivity
的时候记得一定要调用player.release
确保不存在资源泄漏,而不能写成player = null
了事。
5. 四大组件
在创建播放器或者进入播放状态时,我们可以通过注册组件实现参数调整和定制,但需要确保上层业务逻辑和播放器内部状态松耦合,ExoPlayer线程模型保证了这一点,它分离了应用线程、内部播放线程和后台线程。应用线程负责与下层交互,基本属于以下三种:
player
对象的方法;EventLogger
中实现;前两种通过消息队列异步操作,不会对播放器造成阻塞,状态查询仅仅涉及对上层可见部分,同播放器内部状态解耦。上层与组件的通信机制,是通过创建播放器并注册组件的同时,传递一个mainHandler
句柄下去,以便底层向应用线程发送消息,对应地,传递eventLogger
对象就是为了在应用线程中监听并处理事件。
ExoPlayer内部是各种主循环,也是根据不同播放模式区分的。主循环依赖很多组件,组件之间基本上是通过对象的包含和封装联系起来的,这里主要介绍四大组件。
5.1 MediaSource
MediaSource定义了一个媒体内容的实例,内部实现了对媒体数据的加载、解析和传输。你需要在调用player.prepare
时传递这个对象参数来注册一个媒体实例。
播放器默认替我们实现了四个基本MediaSource扩展,涵盖了所有媒体格式,它们分别是ExtractorMediaSource
、HlsMediaSource
、DashMediaSource
和SsMediaSource
。其中由容器封装格式的多媒体(比如MP4、FLV等)都属于ExtractorMediaSource
,其它网络流媒体分别是后三种,包括HLS、Dash和SmoothStreaming。
另外还有三个高级扩展,它们分别是:MergingMediaSource
、LoopingMediaSource
、ConcatenatingMediaSource
。分别实现了对上述四种基本媒体内容的合成、循环和拼接,从而实现各种更高级的播放模式。
MediaSource实例的创建一般需要以下5个对象参数:
ExtractorMediaSource
来说叫做Extractor
,对于其它来说叫做ChunkSource
;对上层来说其实等价于MappingTrackSelector
的扩展,因为它实际上将播放器内部的renderers和tracks映射起来,用途就是可以在播放过程中对媒体自带的格式参数(比如多个分辨率、码率)进行动态切换和选择,也可以查询渲染器的类型和状态等信息。
TrackSelector实例需要在播放器创建中注册,但一般不需要上层开发人员去定制。在MediaSource加载和解析媒体数据内容时内部自动填充,在上层对播放器进行控制操作时内部自动更改状态。换句话说,TrackSelector实例对上层来说,状态不可更改(immutable),但是可见的。你可以通过getCurrentMappedTrackInfo
方法获取MappedTrackInfo
实例来查询关联的tracks,你也可以通过setRendererDisabled
方法来使能和禁用renders。
LoadControl主要针对媒体数据源缓冲加载控制。比如设置数据缓冲时间最大值和最小值,缓冲多大时才能恢复播放等等。一般创建实例设置默认参数,开发人员可以更改默认值。LoadControl在每次新创建播放器时注册以生效。
Renderer通过扩展安卓自带的MediaCodec
和AudioTrack
等多媒体原生接口实现音视频的渲染。在播放器内部默认自动创建实例,一般包括video、audio、subtitle和text四种类型。一般不需要应用开发人员关心,但可以通过player.getRendererType
方法获取其类型,通过trackSelector.getRendererDisabled
方法获取其状态。
虽然Timeline在官方文档里不属于四大组件,但上层仍需要关注并使用它,故单独列出说明。对于上层来说,Timeline实例可视为播放状态下具有一段时长的媒体,对于直播这种动态不确定时长的媒体,一个Timeline实例可视为当前播放媒体的快照。对于上层来说,Timeline状态不可更改(immutable),但是可见,这一点类似于TrackSelector组件。
一个Timeline实例由一个或多个媒体时段(以下简称period)和窗口(以下简称window)对象构成。其中period实例表示逻辑概念上的媒体时段,比如一个媒体文件;window实例可以跨越一个或多个period,指定了当前可以播放的区域,以及一些附加信息(比如是否支持seek等),每个window定义一个默认位置,指明进入播放状态后从何处开始播放。
上图是从period视角观察window。假设一个Timeline实例有两个period对象以及一个window对象,window横跨两个periods,那么两个periods各自维护了一个windowIndex
表示自己所属的window对象,同时各自维护一个positionInWindow
表明period起始处相对于window对象起始处的偏移位置,其中period1.positionInWindow
是负数,表示period1起始超前于所属window起始,period2.positionInWindow
是正数,表示period2起始处滞后于所属window起始。
上图是从window视角观察period。同样假设一个Timeline实例有两个period对象以及一个window对象,window横跨两个periods,那么window维护了一个firstPeriodIndex
和一个lastPeriodIndex
,分别表示跨越的第一个period以及最后一个period。positionInFirstPeriod
表示window起始处与第一个period起始处的偏移,duration
表示window自身时长,defaultStartPosition
表示播放点与window自身起始处的偏移。
下面枚举了Timeline在各个播放模式下的图例。
上图是单个媒体模式下的Timeline,各有一个period和window。window实例横跨整个period,表明period所有部分都可以播放,window的默认位置一般是period起始处。
上图是多个媒体文件或者实时流的播放列表模式,由多个period和window实例构成,并且一一对应。每个window贯穿整个period,默认位置是起始点。其属性内容(比如时长和能否seek)只有在播放器缓冲到对应的媒体文件和流才能知道。
上图是一个period构成的直播流模式,时长未知,因为它会不断广播更多内容。如果其内容仅保留某个限定时段可播放,window会指定一个非0位置作为播放起始点。对于直播流,isDynamic
属性将为true,其默认位置一般靠近直播边缘(live edge)。
上图同样是一个period构成的直播流模式,只是可播放时段是不定的。Timeline类似于上面限定时段直播流模式,但window实例可以跨越到period起始处,表明所有之前广播内容仍是有效的。
上图是一个被分离成多个periods直播流模式,比如存在广告边界。Timeline类似于前面限定时段直播流模式,只是window实例可以横跨多个period。在不定时段直播流模式下也可能存在多个periods的情况。
上图是单个媒体文件、实时流和直播流,多个periods串联模式。比如当一段媒体的片头结束后,直播流将从靠近直播边缘的默认位置开始播放。
在demo的PlayerActivity.java
中示范了如何使用Timeline在退出播放状态前保留媒体数据位置,以便下次进入播放状态时进行seek操作。代码如下:
private void releasePlayer() {
shouldAutoPlay = player.getPlayWhenReady();
playerWindow = player.getCurrentWindowIndex();
playerPosition = C.TIME_UNSET;
Timeline timeline = player.getCurrentTimeline();
if (timeline != null && timeline.getWindow(playerWindow, window).isSeekable) {
playerPosition = player.getCurrentPosition();
}
player.release();
...
}
player.getPlayWhenReady
返回值表明下次是继续播放还是从头开始,player.getCurrentWindowIndex
获取当前播放窗口索引,player.getCurrentTimeline
获取当前播放时间线,timeline.getWindow
获取当前播放窗口,如果该窗口是可以seek的,调用player.getCurrentPosition
获取媒体位置,最后释放播放器。
private void initializePlayer() {
if (playerPosition == C.TIME_UNSET) {
player.seekToDefaultPosition(playerWindow);
} else {
player.seekTo(playerWindow, playerPosition);
}
player.setPlayWhenReady(shouldAutoPlay);
}
在创建播放器以及进入播放状态时,会去检查之前保留的playerPosition
和shouldAutoPlay
。如果上次退出时媒体位置保留下来,则对上次保留的窗口进行seek操作;如果未保留位置或者媒体本身无法seek,则以窗口默认位置为播放点。
理论上ExoPlayer的所有组件都可以定制,只要包含Defaultxxx
前缀的都是默认实现。此处我们只关注可以被移植到应用层的DataSource组件。
前面说过DataSource负责MediaSource中数据源的加载,因此上层开发人员可以通过DataSource定制开发实现数据源的业务逻辑。实际上DataSource.java
声明了一个抽象接口,你需要实现以下回调:
// 工厂实例
interface Factory {
// 创建一个DataSource实例
DataSource createDataSource();
}
/**
* 打开数据源并读取指定数据。
* 注意:如果IOException被抛出,调用者必须调用close()来确保残余状态得到清理。
* 参数:DataSpec,定义数据源对象。
* 抛出:IOException,可扩展DataSourceException并自定义出错原因。
* 返回:数据源可以被读取的字节数,即DataSpec.length的值。如果长度无法 * 解析,返回C.LENGTH_UNSET。
*/
long open(DataSpec dataSpec) throws IOException;
/**
* 从打开数据源中读取字节,直到最大长度或者无数据可读为止,否则调用会阻塞直到最少一个字节数据被读取。
* 参数:buffer,数据要写入的缓存;
* 参数:offset,数据写入buffer的起始位置;
* 参数:readLength,限定读取最大字节数。
* 返回:实际读取字节数。如果没有数据可读,返回C.RESULT_END_OF_INPUT
* 抛出:IOException
*/
int read(byte[] buffer, int offset, int readLength) throws IOException;
/**
* 当数据源被打开,返回URI,等价于当初传递给open(DataSpec)中DataSpec的内容,除非发生重定向,URI内容也会改变。
* 返回:数据源URI,如果数据源没有打开过,返回null。
*/
Uri getUri();
/**
* 关闭数据源。
* 注意:该方法在对应open方法抛出异常时也必须调用。
* 抛出:IOException,当关闭发生错误。
*/
void close() throws IOException;
我们可以根据不同的播放模式定制DataSource,一般有三种播放模式:
下面从三种模式分别讲解DataSource的实现原理和定制方法。
7.1 本地文件
ExoPlayer已经为我们默认实现了几种,包括:AssertDataSource
,FileDataSource
、RawResourceDataSource
等,一般只用FileDataSource
即可。
从上层视角看,一个简易描述的播放流程如下:当用户点击SampleChooserActivity
中的某条本地MP4的sample后,根据前面提到的播放器一般流程所描述那样,创建几个组件之后,注册给生成SimpleExoPlayer
播放器实例。这里注意一下MediaSource组件的生成。
exolist.json
列表中相应条目,发起一个intent,从中获取数据源URI,也即本地文件路径。ExtractorMediaSource
实例,因为这是MP4格式容器。FileDataSource
工厂实例、Mp4Extractor
工厂实例、用于下发消息的MainHandler
以及用于监听的EventLogger
。ExtractorMediaSource
实例注册给player.prepare
后,播放器进入准备状态,上层只需要等待FileDataSource
的open
回调了。以上为上层发起流程,下面是回调实现。
在open
回调方法中,我们使用RandomAccessFile
作为打开的文件容器,使得文件可以如同大型字节数组一般操作(比如seek),而且内部实现是native的,不会有性能上的损失。接下来作为参数的DataSpec中其实还保留了上一次关闭数据源后的position,这是提供给seek的参数,对于初次打开来说position等于0,即从头开始读取。我们还要计算整个媒体尚需读取的长度,由于是固定文件,所以可以计算出来,作为返回值。在返回以前,listener.onTransferStart
是一次事件通知的回调,专门在每个open
方法最后调用。
如果open
方法失败抛出异常,那么播放器就会调用close
方法。实现比较简单,直接关闭文件即可,播放器会去自动释放相关资源。在返回前也是专门的listener.onTransferEnd
事件通知。
如果open
方法成功,播放器内部发起一个任务循环,不停地读取媒体内容,对于上层来说就是read
回调方法。开发人员可以自己定制数据传输,一旦读完了就返回RESULT_END_OF_INPUT
,否则就返回实际字节数,最后的事件通知是listener.onBytesTransferred
。
对于close
方法,一般以下几种情况会由底层触发:
open
失败抛出异常(包括超时);read
返回EOF;open
,并从文件偏移position处读取数据。此处顺便说一下如何估计播放过程中的码率(带宽),ExoPlayer已经有一个默认实现DefaultBandwidthMeter
,它通过前面提到的TransferListener<Object>
回调来对消耗的字节数和时间多次采样进行加权移动平均(Weighted Moving Average)计算,实现在SlidingPercentile.java
中。注意,一次采样周期就是onTransferEnd
调用。
网络点播一般选择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点播回调过程。
苹果的官网上面有HLS的点播源,假设我们点播了bipbop_4x3_variant.m3u8播放列表(提示:你可以在这里了解一下HLS简明规范),该文本文件是一个媒体播放列表(media playlist),它列出了5个版本(variants),每个variant可视作一个m3u8的二级列表,下面列举了一系列时长10秒左右的ts文件分片,又叫做段(segment)。这个variant标明了下属所有分片的码率、分辨率、解码标志等参数。我们第一次open
和read
回调就会去下载这个m3u8文件并解析每个字段,之后read
返回RESULT_END_OF_INPUT
并回调close
方法。
ExoPlayer会根据带宽估值以及数据缓存大小等去评估一个最佳适配variant(假设为gear1/prog_index.m3u8,gear可视作不同码率挂挡的命名)。我们第二次open
和read
回调就会调整URI参数后下载并解析它,完成后read
返回RESULT_END_OF_INPUT
并回调close
方法。
接下来的open
和read
回调就会去下载并解析一系列ts分片,送到解码器播放。每个ts分片下载解析结束后回调close
方法,生成新的URI后重新回调open
。
ExoPlayer存在带宽估值和数据缓存检测功能,如果当前variant存在不同码率档次,那么在任意一个ts分片结束后,播放器可以根据内在逻辑决定是否重新open
和read
一个新的variant,主要针对码率提升还是降低做为准,由AdaptiveVideoTrackSelection
组件内部参数决定,大致上是根据缓存数据多少与码率之间的预估适配。
HLS还有更高级的组织架构,比如媒体播放列表上面(media playlist)还有主播放列表(master playlist),比如苹果官网提供了master.m3u8样例,里面包含了不同类型的媒体播放列表(比如audio和subtitle)。播放器只下载主列表一次,但可以在多个媒体列表中切换,具体说明见前面HLS规范。ExoPlayer实现了HLS这种高级列表的解析和调度,DataSource回调接口保持了一致性,因此开发人员不需要关注具体实现。
在直播模式下,播放端又叫拉流。按网络协议划分三种类型:HTTP-FLV、HLS和RTMP。直播都不支持seek,open
回调方法返回值LENGTH_UNSET
,表明媒体长度未知。
要说明的是这不是用HTTP点播FLV文件,而是FLV格式的流。虽然URL一致,但与点播不同的是,你在响应头中看不到Content-Length
字段,这样拉流端可以一直接受数据。
DataSource流程同点播,使用HttpDataSource
。它的媒体数据内容是MPEG-TS格式的。
ExoPlayer原生不支持RTMP,所以需要自己实现RTMP拉流代码,并适配成RtmpDataSource
,与其它DataSource在接口上具备一致性。它的媒体数据内容是FLV格式的。
8 API参考文档
可由应用开发直接调用,主要参见demo里的PlayActivity.java
和TrackSelectionHelper.java
。
ExoPlayer接口:播放控制操作;
MappingTrackSelector接口:媒体规格参数选择和切换。
Timeline接口:播放媒体时间线。