|
| 1 | +ExoPlayer简介 |
| 2 | +--- |
| 3 | + |
| 4 | +[ExoPlayer](https://github.com/google/ExoPlayer)是google开源的应用级媒体播放器项目。 |
| 5 | + |
| 6 | + |
| 7 | +与内置的MediaPlayer相比,ExoPlayer的优点主要有: |
| 8 | + |
| 9 | +- 支持通过Http(DASH)和SmoothStreaming进行动态自适应流,这两种都不受MediaPlayer的支持。它还支持其他格式的数据资源,比如MP4、M4A、FMP4、MKV、MP3、Ogg、WAV、FLV等 |
| 10 | +- 支持高级的HLS个性,比如正确处理#EXT-X-DISCONTINUITY标签 |
| 11 | +- 无缝连接,合并和循环播放多媒体的能力 |
| 12 | +- 和应用一起更新播放器(ExoPlayer),因为ExoPlayer是一个集成到应用APK的库,可以随着应用的更新把ExoPlayer更新到一个更新的版本。 |
| 13 | +- 在不同的Android版本和设备上很少会有不同的表现。 |
| 14 | +- 能够自定义和扩展播放器,以适应各种不同的需求 |
| 15 | +- 各个组件都可以自定义,还可以接入ffmpeg组件 |
| 16 | + |
| 17 | +缺点就是,在音频播放时ExoPlayer会比MediaPlayer消耗更多的电量。 |
| 18 | + |
| 19 | +集成 |
| 20 | +--- |
| 21 | + |
| 22 | +在app的module中的`build.gradle`文件添加如下依赖(这种方式是添加全部的依赖): |
| 23 | +`implementation 'com.google.android.exoplayer:exoplayer:2.X.X'` |
| 24 | + |
| 25 | +如果不需要依赖全部的类库,也可以只选择添加你需要的类库,例如下面的方式是只需要播放DASH内容的app,就是只添加Core、DASH和UI库: |
| 26 | +``` |
| 27 | +implementation 'com.google.android.exoplayer:exoplayer-core:2.X.X' |
| 28 | +implementation 'com.google.android.exoplayer:exoplayer-dash:2.X.X' |
| 29 | +implementation 'com.google.android.exoplayer:exoplayer-ui:2.X.X' |
| 30 | +``` |
| 31 | + |
| 32 | +下面列出了所有的依赖库: |
| 33 | + |
| 34 | +- `exoplayer-core`:核心功能库(基础库、必要) |
| 35 | +- `exoplayer-dash`:支持DASH |
| 36 | +- `exoplayer-hls`:支持HLS |
| 37 | +- `exoplayer-smoothstreaming`:支持SmoothStreaming |
| 38 | +- `exoplayer-ui`:使用ExoPlayer所需的UI部分和资源 |
| 39 | + |
| 40 | +上面讲到了DASH、HLS、SmoothStreaming,具体的介绍可以参考[流媒体通信协议]() |
| 41 | + |
| 42 | +除了上面的几个类库外,ExoPlayer还有很多扩展库提供一些额外的功能,有一些可以直接通过JCenter进行依赖,有一些需要手动去编译,具体的可以通过[扩展库目录](https://github.com/google/ExoPlayer/tree/release-v2/extensions/)中的README来查看详细的内容。 |
| 43 | +可以通过JCenter进行依赖的类库和扩展可以从[ExoPlayer的Binatry](https://bintray.com/google/exoplayer)上查看。 |
| 44 | + |
| 45 | +如果依赖完后仍然不行,你需要将build.gradle文件中的android节点打开Java 8的支持: |
| 46 | +``` |
| 47 | +compileOptions { |
| 48 | + targetCompatibility = 1.8 |
| 49 | +} |
| 50 | +``` |
| 51 | + |
| 52 | + |
| 53 | +ExoPlayer库的核心是ExoPlayer接口,ExoPlayer公开了传统的高级媒体播放器功能,例如缓冲、播放、暂停、seek等。在具体实现方面,该开源库对播放器的媒体类型、存储方式、位置、渲染方式等进行了最少的实现,旨在让开发者自定义各种特性。ExoPlayer的实现不是直接实现加载和呈现媒体,而是将这项工作委托给各种组件。主要有: |
| 54 | + |
| 55 | +- TrackSelector:轨道提取器,从MediaSource中提取各个轨道的二进制数据,交给Renderer渲染,创建播放器时传入。 |
| 56 | +- Renderer:对多媒体中的各个轨道(音轨、视频轨、字母轨等)数据进行渲染,渲染就是"播放",把二进制文件渲染成声音、画面,创建播放器时传入。 |
| 57 | +- MediaSource:定义多媒体数据源,这个类的功能就是从Uri中读取多媒体文件的二进制数据。MediaSource在播放开始时通过ExoPlayer.prepare()注入。 |
| 58 | +- LoadControl:对MediaSource进行控制,比如什么时候开始缓冲、缓冲多少等。 |
| 59 | + |
| 60 | +它们之间的关系是: |
| 61 | + |
| 62 | +渲染器(Render) ---刷数据--->提取器(Extraor) ----读取数据---> 加载控制器(LoadControl) ----控制数据加载方式---> 媒体源(MediaSource) |
| 63 | + |
| 64 | + |
| 65 | + |
| 66 | + |
| 67 | +该库提供了这些组件的默认实现,既能满足大部分需求,也可通过自定义来实现特殊的需求。例如可以通过自定义LoadControl来更改播放器的缓冲策略,或自定义Renderer来渲染Android本身不支持的编解码器。 |
| 68 | + |
| 69 | +[支持的格式](https://exoplayer.dev/supported-formats.html) |
| 70 | + |
| 71 | + |
| 72 | +### 创建播放器 |
| 73 | + |
| 74 | +为了满足不同的需求,ExoPlayer提供了一个工厂类ExoPlayerFactory,通过该工厂类来创建一个ExoPlayer实例,大多数情况下直接使用`ExoPlayerFactory.newSimpleInstance(context)`方法即可: |
| 75 | + |
| 76 | +``` |
| 77 | +public static SimpleExoPlayer newSimpleInstance(Context context) { |
| 78 | + return newSimpleInstance(context, new DefaultTrackSelector()); |
| 79 | +} |
| 80 | +``` |
| 81 | +它返回的SimpleExoPlayer是一个实现ExoPlayer接口并添加了一些额外的高级播放器功能。 |
| 82 | + |
| 83 | +### 把播放器实例附着到一个View上 |
| 84 | + |
| 85 | +ExoPlayer库提供了一个PlayerView(A high level view for Player media playbacks. It displays video, subtitles and album art during playback, and displays playback controls using a PlayerControlView.)类,他封装了PlayerControlView和渲染视频的一个默认的SurfaceView以及字幕等功能。可以通过xml中surface_type来指定视频播放的Surface类型,除了值spherical_view(这是球形视频播放一个特殊的值)时,允许值是surface_view,texture_view和none。如果视图仅用于音频播放,则应使用none以避免必须创建Surface,因为这样做可能耗费资源。 |
| 86 | + |
| 87 | +如果视图是用于常规视频播放那么surface_view或texture_view 应该使用。对于视频播放,相比TextureView,SurfaceView有许多好处: |
| 88 | + |
| 89 | +- 显着降低了许多设备的功耗。 |
| 90 | +- 更准确的帧定时,使视频播放更流畅。 |
| 91 | +- 播放受DRM保护的内容时支持安全输出。 |
| 92 | +因此,相比较于TextureView,SurfaceView应尽可能优先考虑。 TextureView只有在SurfaceView不符合您需求的情况下才能使用。一个示例是在Android N之前需要平滑动画或滚动视频表面,如下所述。对于这种情况,最好 TextureView 只在SDK_INT小于24(Android N)时使用, 否则,使用SurfaceView。 |
| 93 | + |
| 94 | +SurfaceView在Android N之前,渲染未与视图动画正确同步。在早期版本SurfaceView中,当放入滚动容器或受到动画影响时,这可能会导致不必要的效果 。这些效果包括视图的内容看起来略微落后于它应该显示的位置,并且视图在受到动画时变黑。为了在Android N之前实现流畅的动画或视频滚动,因此必须使用TextureView而不是SurfaceView。 |
| 95 | + |
| 96 | +xml声明: |
| 97 | +``` |
| 98 | +<com.google.android.exoplayer2.ui.PlayerView |
| 99 | + android:id="@+id/mPlayerView" |
| 100 | + android:layout_width="match_parent" |
| 101 | + android:layout_height="match_parent" /> |
| 102 | +``` |
| 103 | + |
| 104 | +```kotlin |
| 105 | +private lateinit var mPlayer: SimpleExoPlayer |
| 106 | +private fun initView(context: Context) { |
| 107 | + val view = View.inflate(context, R.layout.video_view, this) |
| 108 | + mPlayer = ExoPlayerFactory.newSimpleInstance(context) |
| 109 | + // 通过PlayerView.setPlayer方法来将ExoPlayer绑定到View上 |
| 110 | + mPlayerView.player = mPlayer |
| 111 | +} |
| 112 | +``` |
| 113 | + |
| 114 | +### 开始播放 |
| 115 | + |
| 116 | +ExoPlayer中将每一种媒体资源都封装成MediaSource,如果想要播放一种媒体资源,首先需要为他创建对应的MediaSource对象,然后把这个对象传递给ExoPlayer.prepared方法。ExoPlayer提供了多种MediaSource的实现类,例如播放[DASH](https://exoplayer.dev/dash.html)的DashMediaSource,[SmoothStreaming](https://exoplayer.dev/smoothstreaming.html)的SsMediaSource,[HLS](https://exoplayer.dev/hls.html)的HlsMediaSource,以及[常规媒体文件](https://exoplayer.dev/progressive.html)的ProgressiveMediaSource。 |
| 117 | + |
| 118 | + |
| 119 | +``` |
| 120 | + fun playSingleMp4Video(url: String) { |
| 121 | + val uri = Uri.parse(url) |
| 122 | + val mediaSource = buildMediaSource(uri) |
| 123 | + // setPlayWhenReady()方法可以设置prepared完成后是否自动播放,如果已经准备好了该方法可实现开始和暂停播放的功能 |
| 124 | + mPlayer.playWhenReady = true |
| 125 | + // setShuffleModeEnabled控制播放列表 setPlaybackParameters控制亮度和音量 |
| 126 | +
|
| 127 | + // 设置监听 |
| 128 | + mPlayer.addListener(mPlayerEventListener) |
| 129 | + mPlayer.addVideoListener(mPlayerVideoListener) |
| 130 | + // 移除监听 |
| 131 | + // mPlayer.removeListener(mPlayerEventListener) |
| 132 | + mPlayer.prepare(mediaSource, false, false) |
| 133 | + } |
| 134 | +
|
| 135 | + private fun buildMediaSource(uri: Uri): MediaSource { |
| 136 | + val dataSourceFactory = DefaultDataSourceFactory(context, Util.getUserAgent(context, "you application name")) |
| 137 | + return ProgressiveMediaSource.Factory(dataSourceFactory) |
| 138 | + .createMediaSource(uri) |
| 139 | + } |
| 140 | +
|
| 141 | + private var mPlayerEventListener = object : EventListener { |
| 142 | + override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) { |
| 143 | + when (playbackState) { |
| 144 | + Player.STATE_IDLE -> { |
| 145 | + // 出事状态、播放器停止或播放失败时的状态 |
| 146 | + } |
| 147 | + Player.STATE_BUFFERING -> { |
| 148 | + // 开始加载数据,无法立即从当前位置进行播放 |
| 149 | + } |
| 150 | + Player.STATE_READY -> { |
| 151 | + // ready状态,可以直接从当前位置播放 |
| 152 | + } |
| 153 | + Player.STATE_ENDED -> { |
| 154 | + // 播放完成 |
| 155 | + } |
| 156 | + } |
| 157 | + } |
| 158 | +
|
| 159 | + override fun onPlayerError(error: ExoPlaybackException?) { |
| 160 | + // error.getSourceException()可以获取更多信息 |
| 161 | + when(error?.type) { |
| 162 | + ExoPlaybackException.TYPE_SOURCE -> { |
| 163 | + // 加载资源时出错 |
| 164 | + } |
| 165 | +
|
| 166 | + ExoPlaybackException.TYPE_RENDERER -> { |
| 167 | + // 渲染时出错 |
| 168 | + } |
| 169 | +
|
| 170 | + ExoPlaybackException.TYPE_UNEXPECTED -> { |
| 171 | + // 意外 |
| 172 | + } |
| 173 | +
|
| 174 | + ExoPlaybackException.TYPE_OUT_OF_MEMORY -> { |
| 175 | + // OOM |
| 176 | + } |
| 177 | +
|
| 178 | + ExoPlaybackException.TYPE_REMOTE -> { |
| 179 | + // 远程错误 |
| 180 | + } |
| 181 | + } |
| 182 | + } |
| 183 | + } |
| 184 | +
|
| 185 | + private var mPlayerVideoListener = object : VideoListener { |
| 186 | + override fun onVideoSizeChanged( |
| 187 | + width: Int, |
| 188 | + height: Int, |
| 189 | + unappliedRotationDegrees: Int, |
| 190 | + pixelWidthHeightRatio: Float |
| 191 | + ) { |
| 192 | +
|
| 193 | + } |
| 194 | +
|
| 195 | + override fun onRenderedFirstFrame() { |
| 196 | +
|
| 197 | + } |
| 198 | +
|
| 199 | + override fun onSurfaceSizeChanged(width: Int, height: Int) { |
| 200 | +
|
| 201 | + } |
| 202 | + } |
| 203 | +``` |
| 204 | + |
| 205 | + |
| 206 | +### 资源释放 |
| 207 | + |
| 208 | +``` |
| 209 | +if (mPlayer != null) { |
| 210 | + mPlayer.removeListener(mPlayerEventListener) |
| 211 | + mPlayer.removeVideoListener(mPlayerVideoListener) |
| 212 | + mPlayer.release() |
| 213 | +} |
| 214 | +``` |
| 215 | + |
| 216 | + |
| 217 | +--- |
| 218 | + |
| 219 | +- 邮箱 :charon.chui@gmail.com |
| 220 | +- Good Luck! |
| 221 | + |
| 222 | + |
| 223 | + |
| 224 | + |
| 225 | + |
| 226 | + |
| 227 | + |
0 commit comments