从这篇开始我们进入阶段五 —— 一些音视频开源项目的学习使用分析,今天我们进入ExoPlayer部分的学习实践
一、ExoPlayer基本介绍
1.1 ExoPlayer优缺点 ExoPlayer是谷歌开源的一个应用级的音视频播放器。ExoPlayer 支持基于 HTTP 的动态自适应流 (DASH)、SmoothStreaming 和通用加密、以及可以很好的支持播放队列、播放源的无缝切换等功能。它采用易于自定义和扩展的设计。 内部的实现也是调用了低层API,比如:MediaCodec、AudioTrack等
画张表格来对比下ExoPlayer和MediaPlayer,更直观的了解
ExoPlayer的代码仓库地址是* https://github.com/google/ExoPlayer*
红色框框起来的,核心部分加ui的library也是我们这个系列学习使用重点。
1.2 ExoPlayer架构设计 ExoPlayer的核心是ExoPlayer的接口,其中定义了包涵传统播放器的功能(缓冲音视频、播放、暂停、seek等)。ExoPlayer没有设定可以播放的媒体类型、存储方式以及渲染方式,也没有直接实现加载和播放。而是在播放器被创建或者准备播放时将这些工作代理给注册的组件来实现。下面是一些常见ExoPlayer的组件实现:
- MediaSource 加载媒体,通过ExoPlayer.prepare注册
- TrackSelector:音/视轨提取器,从MediaSource中提取出轨道的数据
- Render:对TrackSelector提取出来的数据进行渲染,AudioTrack播放音频、Surface渲染视频
- LoadControl:对MediaSource进行控制(什么时候开始缓冲、缓冲多少等) ExoPlayer为这些组件提供了默认的实现,如果需要定制可以自定义组件来扩展实现。
通过ExoPlayer的架构图,我们也可以看到其组件模块化的设计,这个架构设计值得学习,也是好的组件/SDK的一个重要要求。在我们的日常项目开发中,开发一个组件 从易用性和以扩展性方面考虑,既要保证使用者很容易上手使用(提供一套默认实现),又要有方便使用者根据自己的场景进行方便的扩展的能力。
1.3 状态机 在看ExoPlayer的状态机之前,我们先看下MeidaPlayer的状态机
可以看到MediaPlayer的状态比较多,使用时如果在不当的位置触发了不匹配的操作,直接回崩溃。 相比MediaPlayer,ExoPlayer的状态少了些,也更容易使用区分,不像MediaPlayer在没有prepared之前都不可以进行播放相关操作,ExoPlayer很多listener以及isplaying的API监听状态的变化。ExoPlayer的四种状态如下
代码语言:javascript复制 /**
* Playback state. One of {@link #STATE_IDLE}, {@link #STATE_BUFFERING}, {@link #STATE_READY} or
* {@link #STATE_ENDED}.
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({STATE_IDLE, STATE_BUFFERING, STATE_READY, STATE_ENDED})
@interface State {}
/** The player does not have any media to play. */
int STATE_IDLE = 1;
/**
* The player is not able to immediately play from its current position. This state typically
* occurs when more data needs to be loaded.
*/
int STATE_BUFFERING = 2;
/**
* The player is able to immediately play from its current position. The player will be playing if
* {@link #getPlayWhenReady()} is true, and paused otherwise.
*/
int STATE_READY = 3;
/** The player has finished playing the media. */
int STATE_ENDED = 4;
STATE_IDLE:初始状态,此时播放器没有可以播放的资源,播放器停止播放或者播放失败后也会处于该状态 STATE_BUFFERING: 没有足够的数据可以加载播放,此时无法立即播放 STATE_READY : 播放器可以立即播放,是否播放取决于playWhenReady的值,该值表达了使用者的意愿,为true,将会开始播放,否则不播。 STATE_ENDED: 播放完了所有的资源后处于改状态
二、ExoPlayer的简单使用
这一小节我们学习实践ExoPlayer的使用
2.1 AS中引入library ExoPlayer有很好的扩展性和可定制性,可以根据项目需要进行选择对应的模块,也可以全部包含。
exoplayer-core: Core functionality (required). exoplayer-dash: Support for DASH content. exoplayer-hls: Support for HLS content. exoplayer-smoothstreaming: Support for SmoothStreaming content. exoplayer-ui: UI components and resources for use with ExoPlayer.
我们根据需要来添加library
代码语言:javascript复制 implementation 'com.google.android.exoplayer:exoplayer-core:2.13.3'
implementation 'com.google.android.exoplayer:exoplayer-ui: 2.13.3'
接下来出创建一个容器PlayerView以及ExoPlayerView进行播放
2.2 创建播放器、绑定播放器容器、设置数据源、prepare
代码语言:javascript复制 //1. 创建播放器
player = SimpleExoPlayer.Builder(this).build()
printCurPlaybackState("init") // 此时处于STATE_IDLE = 1;
//2. 播放器和播放器容器绑定
playerView.player = player
//3. 设置数据源
//音频
val mediaItem = MediaItem.fromUri(" https://storage.googleapis.com/exoplayer-test-media-0/play.mp3")
player.setMediaItem(mediaItem)
//4.当Player处于STATE_READY状态时,进行播放
player.playWhenReady = true
//5. 调用prepare开始加载准备数据,该方法时异步方法,不会阻塞ui线程
player.prepare()
printCurPlaybackState("prepare") // 此时处于 STATE_BUFFERING = 2;
2.3 播放监听 当前是否在播放中
代码语言:javascript复制public final boolean isPlaying() {
return getPlaybackState() == Player.STATE_READY
&& getPlayWhenReady()
&& getPlaybackSuppressionReason() == PLAYBACK_SUPPRESSION_REASON_NONE;
}
播放状态改变的listener、音频相关的listener、视频相关的listener
代码语言:javascript复制 playbackListener = PlaybackListener()
player.addListener(playbackListener)
player.addAudioListener(playbackListener)
player.addVideoListener(playbackListener)
class PlaybackListener : Player.EventListener, AudioListener, VideoListener {
override fun onPlaybackStateChanged(playbackState: Int) {
val stateString: String
stateString = when (playbackState) {
ExoPlayer.STATE_IDLE -> "ExoPlayer.STATE_IDLE -"
ExoPlayer.STATE_BUFFERING -> "ExoPlayer.STATE_BUFFERING -"
ExoPlayer.STATE_READY -> "ExoPlayer.STATE_READY -"
ExoPlayer.STATE_ENDED -> "ExoPlayer.STATE_ENDED -" //播放列表存在时播放最后一个播放完成才会回掉该方法
else -> "UNKNOWN_STATE -"
}
Log.d("ExoBaseUserActivity", "changed state to $stateString")
}
override fun onAudioSessionIdChanged(audioSessionId: Int) {
Log.d("ExoBaseUserActivity", "onAudioSessionIdChanged--sessionId=" audioSessionId)
}
override fun onAudioAttributesChanged(audioAttributes: AudioAttributes) {
Log.d("ExoBaseUserActivity", "onAudioAttributesChanged--audioAttributes=" audioAttributes.toString())
}
override fun onVolumeChanged(volume: Float) {
Log.d("ExoBaseUserActivity", "onVolumeChanged--volume=" volume)
}
override fun onSkipSilenceEnabledChanged(skipSilenceEnabled: Boolean) {
Log.d("ExoBaseUserActivity", "onSkipSilenceEnabledChanged--skipSilenceEnabled=" skipSilenceEnabled)
}
override fun onVideoSizeChanged(width: Int, height: Int, unappliedRotationDegrees: Int, pixelWidthHeightRatio: Float) {
Log.d("ExoBaseUserActivity", "onVideoSizeChanged--width=" width " height=" height " unappliedRotationDegrees=" unappliedRotationDegrees " pixelWidthHeightRatio=" pixelWidthHeightRatio)
}
override fun onSurfaceSizeChanged(width: Int, height: Int) {
Log.d("ExoBaseUserActivity", "onSurfaceSizeChanged--width=" width " height=" height)
}
override fun onRenderedFirstFrame() {
Log.d("ExoBaseUserActivity", "onRenderedFirstFrame")
}
}
用于分析用的listener(会输出更详细的信息)
代码语言:javascript复制 //通过AnalyticsListener可以输出更多信息
analyticsListener = EventLogger(DefaultTrackSelector())
player.addAnalyticsListener(analyticsListener)
2.4 释放资源 在页面不可见/销毁(看是否需要后台播放)时要释放资源
代码语言:javascript复制 override fun onDestroy() {
super.onDestroy()
player.removeAnalyticsListener(analyticsListener)
player.removeListener(playbackListener)
player.removeAudioListener(playbackListener)
player.removeVideoListener(playbackListener)
player.release()
}
完整代码已上传至 github https://github.com/ayyb1988/mediajourney
三、遇到的问题
问题1
代码语言:javascript复制Failed to resolve: com.google.android.exoplayer:exoplayer: 2.13.3
2.13.3前多了一个空格,这个太….,细节有时候不注意就好浪费不少时间。
问题2
代码语言:javascript复制 java.lang.SecurityException: Permission denied (missing INTERNET permission?)
at java.net.Inet6AddressImpl.lookupHostByName(Inet6AddressImpl.java:150)
at java.net.Inet6AddressImpl.lookupAllHostAddr(Inet6AddressImpl.java:103)
at java.net.InetAddress.getAllByName(InetAddress.java:1152)
at com.android.okhttp.Dns$1.lookup(Dns.java:41)
at com.android.okhttp.internal.http.RouteSelector.resetNextInetSocketAddress(RouteSelector.java:178)
at com.android.okhttp.internal.http.RouteSelector.nextProxy(RouteSelector.java:144)
at com.android.okhttp.internal.http.RouteSelector.next(RouteSelector.java:86)
at com.android.okhttp.internal.http.StreamAllocation.findConnection(StreamAllocation.java:176)
at com.android.okhttp.internal.http.StreamAllocation.findHealthyConnection(StreamAllocation.java:128)
at com.android.okhttp.internal.http.StreamAllocation.newStream(StreamAllocation.java:97)
at com.android.okhttp.internal.http.HttpEngine.connect(HttpEngine.java:289)
at com.android.okhttp.internal.http.HttpEngine.sendRequest(HttpEngine.java:232)
at com.android.okhttp.internal.huc.HttpURLConnectionImpl.execute(HttpURLConnectionImpl.java:465)
at com.android.okhttp.internal.huc.HttpURLConnectionImpl.connect(HttpURLConnectionImpl.java:131)
at com.android.okhttp.internal.huc.DelegatingHttpsURLConnection.connect(DelegatingHttpsURLConnection.java:90)
at com.android.okhttp.internal.huc.HttpsURLConnectionImpl.connect(HttpsURLConnectionImpl.java:30)
at com.google.android.exoplayer2.upstream.DefaultHttpDataSource.makeConnection(DefaultHttpDataSource.java:641)
at com.google.android.exoplayer2.upstream.DefaultHttpDataSource.makeConnection(DefaultHttpDataSource.java:528)
at com.google.android.exoplayer2.upstream.DefaultHttpDataSource.open(DefaultHttpDataSource.java:349)
at com.google.android.exoplayer2.upstream.DefaultDataSource.open(DefaultDataSource.java:201)
at com.google.android.exoplayer2.upstream.StatsDataSource.open(StatsDataSource.java:84)
at com.google.android.exoplayer2.source.ProgressiveMediaPeriod$ExtractingLoadable.load(ProgressiveMediaPeriod.java:1015)
at com.google.android.exoplayer2.upstream.Loader$LoadTask.run(Loader.java:415)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
at java.lang.Thread.run(Thread.java:919)
没有加网络权限的原因,Mainfest中静态注册后,在requesetPermission中动态的请求下。通过这个崩溃堆栈,我们可以看到ExoPlayer加载网络视频使用的是Okhttp
问题3
代码语言:javascript复制2021-05-15 18:41:17.414 11144-11144/? I/av.mediajourne: Not late-enabling -Xcheck:jni (already on)
2021-05-15 18:41:17.487 11144-11144/? E/av.mediajourne: Unknown bits set in runtime_flags: 0x8000
2021-05-15 18:41:17.489 11144-11144/? W/av.mediajourne: Unexpected CPU variant for X86 using defaults: x86
X86模拟器播放时偶尔会闪退,真机正常。机型设备的适配问题始终是一个大问题
四、资料
- Media streaming with ExoPlayer
- ExoPlayer blog
- ExoPlayer developer guide
- ExoPlayer播放音视频的使用介绍
五、 收获
通过本次学习实践收获如下:
- 了解ExoPlayer的背景以及相比MediaPlayer的优缺点
- 了解ExoPlayer的基本功能
- 简单实践
感谢你的阅读
下一篇我们继续学习实践ExoPlayer,实现一个简单的音频播放器,欢迎关注公众号“音视频开发之旅”,一起学习成长。
欢迎交流