Android 视频播放器 (四):使用ExoPlayer播放视频

一、简介

ExoPlayer是一个Android应用层的媒体播放器,它提供了一套可替换Android MediaPlayer的API,可以播放本地或者是线上的音视频资源。ExoPlayer支持一些Android MediaPlayer不支持的特性,比如适配DASH和SmoothStreaming的播放。和MediaPlayer不同的是,ExoPlayer很容易自定义和扩展,并且它可以通过应用商店的应用程序更新来直接更新。

现在在Android设备上播放视频和音乐的应用是一个很热门的应用,Android框架提供的MediaPlayer可以使用很少的代码量快速的实现播放音视频的功能,而且它也提供了底层的API比如MediaCodec、AudioTrack和MediaDrm,它们同样可以创建自定义媒体播放器,而ExoPlayer是建立在底层音视频API之上的开源的应用级媒体播放器。

项目地址:https://github.com/google/ExoPlayer

ExoPlayer系列文章:https://medium.com/google-exoplayer

ExoPlayer开发文档:https://exoplayer.dev/

优点

对于Android内置的MediaPlayer来说,ExoPlayer有以下几个优点:

  1. 支持DASH和SmoothStreaming这两种数据格式的资源,而MediaPlayer对这两种数据格式都不支持。它还支持其它格式的数据资源,比如MP4, M4A, FMP4, WebM, MKV, MP3, Ogg, WAV, MPEG-TS, MPEG-PS, FLV and ADTS (AAC)等
  2. 支持高级的HLS特性,比如能正确的处理#EXT-X-DISCONTINUITY标签
  3. 无缝连接,合并和循环播放多媒体的能力
  4. 和应用一起更新播放器(ExoPlayer),因为ExoPlayer是一个集成到应用APK里面的库,你可以决定你所想使用的ExoPlayer版本,并且可以随着应用的更新把ExoPlayer更新到一个最新的版本。
  5. 较少的关于设备的特殊问题,并且在不同的Android版本和设备上很少会有不同的表现。
  6. 在Android4.4(API level 19)以及更高的版本上支持Widevine通用加密
  7. 为了符合你的开发需求,播放器支持自定义和扩展。其实ExoPlayer为此专门做了设计,并且允许很多组件可以被自定义的实现类替换。
  8. 使用官方的扩展功能可以很快的集成一些第三方的库,比如IMA扩展功能通过使用互动媒体广告SDK可以很容易地将视频内容货币化(变现)

缺点

  1. 比如音频在Android设备上的播放,ExoPlayer会比MediaPlayer消耗更多的电量。更多细节请参考文章:Battery consumption page

 二、ExoPlayer 使用

1.把ExoPlayer作为一个依赖添加到你的项目

添加仓库 第一步就是确保你在工程根目录的build.gradle文件里添加了Google和JCenter仓库:

repositories {
     google()
     jcenter()
 }

添加ExoPlayer模块 在你的app module 里面的build.gradle文件夹里添加一个ExoPlayer依赖。

下面是ExoPlayer的全量包的依赖方式:

implementation 'com.google.android.exoplayer:exoplayer:2.X.X'

上面的2.x.x是选择的版本。

你也可以只依赖你想要的模块,来代替全量包。比如当你的app想要播放DASH格式的内容的时候,可以只依赖Core,DASH和UI模块的库。

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全量的依赖库等同于把下面所有的依赖库都分别添加进去。

  1. exoplayer-core: 核心功能(必须的).
  2. exoplayer-dash: 支持DASH内容.
  3. exoplayer-hls: 支持HLS内容.
  4. exoplayer-smoothstreaming: 支持SmoothStreaming内容.
  5. exoplayer-ui: ExoPlayer所使用的UI组件和资源.

除了这些模块库之外,ExoPlayer还有很多可以提供额外功能的依赖于第三方库的扩展模块,可以参考extensions directory来了解更多信息

2. 打开对 java 8 的支持

如果还没有设置支持java8,那么你需要在所有依赖ExoPlayer的build.gradle文件里打开对java8的支持,通过在Android域中添加以下代码即可:

compileOptions {
   targetCompatibility JavaVersion.VERSION_1_8
}

记住如果你想在你的代码里用java8的特性,你需要添加下面额外的设置:

// For Java compilers:
 compileOptions {
   sourceCompatibility JavaVersion.VERSION_1_8
 }
 // For Kotlin compilers:
 kotlinOptions {
   jvmTarget = JavaVersion.VERSION_1_8
 }

3. 创建一个播放器

你可以使用ExoPlayerFactory创建一个ExoPlayer对象。为了不同的需求,这个工厂类提供了一系列方法来创建ExoPlayer实例,但是在大多数情况下,使用ExoPlayerFactory.newSimpleInstance方法就可以了。这些方法会返回SimpleExoPlayer类型的对象,它继承自ExoPlayer,并且添加了一些额外的高级的播放器功能。下面的代码展示了怎么创建一个SimpleExoPlayer对象的:
SimpleExoPlayer player = ExoPlayerFactory.newSimpleInstance(context);

应用里面的某个线程一定可以访问ExoPlayer对象,在大多数情况下它一般是应用的主线程,并且只有在应用的主线程里才能使用ExoPlayer的UI组件和IMA扩展。

能够访问ExoPlayer对象的线程可以通过创建播放器实例的时候传入一个Looper被明确的指定,如果没有指定Looper,那么创建player的线程的Looper会被使用,或者这个线程也没有Looper,那么应用的主线程的Looper会被使用。在所有的情况下,能够访问播放器的线程的Looer能够通过Player.getApplicationLooper获取到。

4. 把这个播放器实例附着到一个View上

ExoPlayer库提供了一个PlayerView,它封装了一个PlayerControlView和一个能够渲染视频的Surface。一个PlayerView可以被加入到应用的布局文件中去。可以像这样把一个player绑定到一个View上。
// 绑定播放器到View上
playerView.setPlayer(player);
如果你需要更加精确的控制播放器和渲染视频的Surface,你可以使用SimpleExoPlayer的setVideoSurfaceView、setVideoTextureView、setVideoSurfaceHolder和setVideoSurface方法分别的设置播放器的属性SurfaceView、TextureView、SurfaceHolder和Surface。你还可以把PlayerControlView来当成一个单独的组件使用,或者实现自定义的播放控制类来和播放器进行直接交互。在播放的时候,setTextOutput和setId3Output可以被用来接收字幕和ID3元数据输出。

5. 准备播放器资源

在ExoPlayer里每一种媒体资源都是被MediaSource来代表的。如果想播放一种媒体资源,你首先要为它创建相应的MediaSource对象,然后把这个对象传递给ExoPlayer.prepare方法。ExoPlayer库提供了多种MediaSource的实现类,比如代表DASH资源的DashMediaSource,代表SmoothStreaming资源的SsMediaSource,代表HLS资源的HlsMediaSource和代表一般的多媒体文件的ExtractorMediaSource。下面的代码展示了如何为播放MP4文件的播放器准备适合的MediaSource。

 //创建一个DataSource对象,通过它来下载多媒体数据
 DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(context,
 Util.getUserAgent(context, "yourApplicationName"));
 //这是一个代表将要被播放的媒体的MediaSource
 MediaSource videoSource = new ExtractorMediaSource.Factory(dataSourceFactory)
     .createMediaSource(mp4VideoUri);
 //使用资源准备播放器
 player.prepare(videoSource);

6. 控制播放器

一旦播放器准备就绪,就可以调用player的方法进行播放了,例如调用setPlayWhenReady可以开始和暂停播放。不同的seekTo方法可以在媒体资源里进行搜索,setRepeatMode控制了多媒体如何循环播放,setShuffleModeEnabled控制了是否打乱播放列表,setPlaybackParameters用来调整播放的速度和音调。

如果player和PlayerView或者是PlayerControlView进行绑定了,那么用户和这些控件的交互将会调用player相对应的方法。

7. 监听播放器事件

改变播放状态或者是播放错误的事件将会被注册的Player.EventListener对象接收,很容易我们就可以注册一个接收这种事件的监听。

 // 添加一个监听来接收播放器事件.
 player.addListener(eventListener);

如果你只是对一部分事件感兴趣,那么你可以继承Player.DefaultEventListener而不是实现Player.EventListener,这样会让你只实现你想要的方法。

当使用SimpleExoPlayer的时候,也可以给player设置一些额外的监听。比如addVideoListener方法允许你获取到视频渲染相关的事件,它可以帮助你调整UI布局(渲染视频的Surface的长宽比)。addAnalyticsListener方法允许你接收更加详细的事件,它有助于你分析一些东西。

8. 释放播放器

当不在需要播放的时候释放掉播放器是非常重要的,以便释放掉有限的资源比如视频解码器供其它应用使用。释放掉播放器可以通过调用ExoPlayer.release实现。

9. MediaSource(播放资源)

在ExoPlayer里每一种媒体资源都是被MediaSource来代表的。ExoPlayer库提供了多种MediaSource的实现类,比如代表DASH资源的DashMediaSource,代表SmoothStreaming资源的SsMediaSource,代表HLS资源的HlsMediaSource和代表一般的多媒体文件的ExtractorMediaSource。你可以参考main demo app的PlayerActivity类看一下怎么实例化这四种MediaSource。

除了上面所描述的MediaSource实现类之外,ExoPlayer也提供了ConcatenatingMediaSource,ClippingMediaSource,LoopingMediaSource和MergingMediaSource。通过组合这些MediaSource的实现类可以实现更加复杂的播放功能。一些常用的使用功能会在下面描述。需要注意的是下面描述的是以视频播放为示例的,但是它们同样适用于音频的播放,以及适用任何所支持的媒体类型的播放。

10. Playlists(播放列表)

使用ConcatenatingMediaSource支持播放列表,它可以连续的播放多种MediaSource资源。下面的例子展示了怎么实现由两个videos组成的playlists。

 MediaSource firstSource = new ExtractorMediaSource.Factory(...).createMediaSource(firstVideoUri);
 MediaSource secondSource = new ExtractorMediaSource.Factory(...).createMediaSource(secondVideoUri);
 //先播放第一个视频,再播放第二个视频
 ConcatenatingMediaSource concatenatedSource = new ConcatenatingMediaSource(firstSource, secondSource);

 

连接资源的转换是无缝的。这种连接不要求是相同格式的资源(例如可以把包含480P H264视频文件和包含720P VP9的视频文件很好地连接在一起)。它们甚至可以是不同的类型(比如可以将一个视频和一个纯音频流很好地连接在一起)。并且在一个连接里一个类型的MediaSource可以被多次使用。

在一个ConcatenatingMediaSource里可以通过添加,删除和移动MediaSource动态地修改播放列表。同样在播放视频之前或者是正在播放的过程中可以通过调用相应的ConcatenatingMediaSource方法动态修改播放列表。播放器会正确地自动处理这些动态修改。例如正在播放的MediaSource被移动了,播放不会中断并且播放完成后会自动播放它后面的一个MediaSource资源。如果正在播放的MediaSource被删除了,播放器会自动移动到第一个存在的后继者去播放,如果没有后继者的话,播放器将会转到结束的状态。

11. Clipping a video(剪辑视频)

ClippingMediaSource可以被用来剪辑视频,这样可以只播放它的一部分。下面的例子展示了怎么剪辑了一个视频,从第5s开始播放,到第10s结束播放。

MediaSource videoSource = new ExtractorMediaSource.Factory(...).createMediaSource(videoUri);
 // 从第5s开始剪辑到第10s
 ClippingMediaSource clippingSource = new ClippingMediaSource(videoSource, /* startPositionUs= */ 5_000_000, /* endPositionUs= */ 10_000_000);

如果只是从资源的开始进行剪辑,那么结束的位置可以被设置为C.TIME_END_OF_SOURCE。为了只剪辑到特定的持续时间,有一个构造函数可以接收一个durationUs参数。

当从一个视频文件的开始进行剪辑的时候,如果可能的话,尽量把开始位置和关键帧对齐。如果开始位置没有和关键帧对齐,那么在开始播放之前播放器需要解码然后丢弃掉前一个关键帧到开始位置之间的数据,这样使 在开始播放的时候,这将会产生一小段延迟,包括当播放器将ClippingMediaSource作为播放列表的一部分播放或循环播放时。

12. Looping a video(视频循环播放)

如果想无限循环播放,最好使用ExoPlayer.setRepeatMode而不是LoopingMediaSource。

使用LoopingMediaSource一个视频可以被无缝的循环一定次数。下面的例子展示了怎么播放一个视频两次:

 MediaSource source = new ExtractorMediaSource.Factory(...).createMediaSource(videoUri);
 // Plays the video twice.
 LoopingMediaSource loopingSource = new LoopingMediaSource(source, 2);

13. Side-loading a subtitle file(侧载一个字幕文件)

给一个视频文件和一个分开的字幕文件,MergingMediaSource可以用来把它们合并成一个单独的资源来播放。

 // 创建一个视频的 MediaSource.
 MediaSource videoSource = new ExtractorMediaSource.Factory(...).createMediaSource(videoUri);
 // 创建一个字幕的 MediaSource.
 Format subtitleFormat = Format.createTextSampleFormat(id, // 一个轨道的标志,可以为空
     MimeTypes.APPLICATION_SUBRIP, // The mime type. Must be set correctly.
     selectionFlags, // 轨道的选择标志
     language); // 字幕的语言,可以为空
 MediaSource subtitleSource = new SingleSampleMediaSource.Factory(...) .createMediaSource(subtitleUri, subtitleFormat, C.TIME_UNSET);
 // 播放带有字幕的视频
 MergingMediaSource mergedSource = new MergingMediaSource(videoSource, subtitleSource);

14. 高级组合

为了更多高级的功能有可能更进一步地合并组合的MediaSource。假如有两个视频A和B,下面的例子展示了怎么一起使用LoopingMediaSource和ConcatenatingMediaSource来播放A、A、B序列。

 MediaSource firstSource = new ExtractorMediaSource.Factory(...).createMediaSource(firstVideoUri);
 MediaSource secondSource = new ExtractorMediaSource.Factory(...).createMediaSource(secondVideoUri);
 // 播放第一个视频两次
 LoopingMediaSource firstSourceTwice = new LoopingMediaSource(firstSource, 2);
 // 播放第一个视频两次,然后再播放第二个视频
 ConcatenatingMediaSource concatenatedSource = new ConcatenatingMediaSource(firstSourceTwice, secondSource);

下面这个例子也可以实现这个效果,这说明了不止有一种方法来实现相同的效果。

MediaSource firstSource = new ExtractorMediaSource.Builder(firstVideoUri, ...).build();
 MediaSource secondSource = new ExtractorMediaSource.Builder(secondVideoUri, ...).build();
 // 播放第一个视频两次,然后再播放第二个视频
 ConcatenatingMediaSource concatenatedSource = new ConcatenatingMediaSource(firstSource, firstSource, secondSource);

15. 轨道选择

轨道选择决定了哪一个可用的媒体轨道可以被播放器的渲染器播放。轨道选择由TrackSelector负责,无论什么时候创建一个ExoPlayer实例,都要给它提供一个TrackSelector对象。

 DefaultTrackSelector trackSelector = new DefaultTrackSelector();
 SimpleExoPlayer player = ExoPlayerFactory.newSimpleInstance(context, trackSelector);

DefaultTrackSelector是一个灵活的TrackSelector,适合更多使用场景。当使用一个DefaultTrackSelector的时候,通过修改它的参数可以控制哪一个tracks被它选择,这种选择可以在播放前完成。例如下面的代码告诉选择器将视频轨道限制为SD,并且如果音频轨道只有一个就选择一个德语的音频轨道。

 trackSelector.setParameters(trackSelector.buildUponParameters().setMaxVideoSizeSd().setPreferredAudioLanguage("deu"));

这是一个基于约束的轨道选择的例子,在这个例子中,在不知道实际可用轨道的情况下指定约束。可以使用参数指定许多不同类型的约束。参数还可以用来从可用的轨道中选择特定的轨道。有关详细信息,请参阅DefaultTrackSelectorParameters和ParametersBuilderParametersBuilder文档。

16. 发送消息给组件

可以向ExoPlayer组件发送消息。这些消息可以使用createMessage创建,然后使用PlayerMessage.send发送。默认情况下,消息会尽快在播放线程上传递,但是这是可以自定义的通过设置另一个回调线程(使用PlayerMessage.setHandler)或指定一个传递消息的播放位置(使用PlayerMessage.setPosition)。通过ExoPlayer发送消息可以确保操作的执行顺序与在播放器上执行的任何其他操作一致。

大多数ExoPlayer的开箱即用渲染器都支持在播放期间更改配置的消息。例如,音频渲染器接受消息来设置音量,而视频渲染器接受消息来设置Surface。这些消息应当在播放线程上传递,以确保线程安全。

17.自定义播放器

与Android的MediaPlayer相比,ExoPlayer的主要优势之一是能够自定义和扩展播放器,以更好地适应开发人员的用例。ExoPlayer库是专门为此而设计的,它定义了许多接口和抽象基类,可以使应用程序开发人员能够轻松替换库提供的默认实现。下面是一些用于构建自定义组件的用例:

Renderer——您可能想要实现一个自定义Renderer来处理库默认不支持的媒体类型。

TrackSelector——实现自定义TrackSelector允许应用程序开发人员更改MediaSource暴露tracks的方式。它会被每个可用的渲染器选择使用。

LoadControl—实现自定义LoadControl允许应用程序开发人员更改播放器的缓冲策略。

Extractor——如果您需要支持目前该库不支持的容器格式,请考虑实现一个定制的Extractor类,然后可以将其与ExtractorMediaSource一起用于播放该类型的媒体。

MediaSource——如果您希望以自定义的方式获取媒体样本以提供给渲染程序,或者希望实现自定义的MediaSource组合行为,那么实现自定义的MediaSource类可能是最好的选择。

DataSource——ExoPlayer的upstream包已经包含了许多不同用例的DataSource实现。您可能希望实现自己的DataSource,以另一种方式加载数据,例如通过自定义协议、使用自定义HTTP堆栈或从自定义持久缓存加载数据。

在构建自定义组件时,我们建议如下:

如果自定义组件需要向应用程序报告事件,我们建议使用与现有ExoPlayer组件相同的模型,其中事件监听器和Handler一起传递给组件的构造函数。

我们建议自定义组件使用与现有ExoPlayer组件相同的模型,以允许应用程序在播放期间进行重新配置,如Sending messages to components所说的。为此,您应该实现一个ExoPlayerComponent并在其handleMessage方法中接收配置更改。您的应用程序应该通过调用外部播放器的sendMessages和blockingSendMessages方法来传递配置更改。

posted @ 2019-05-06 22:42  灰色飘零  阅读(17215)  评论(0编辑  收藏  举报