音视频开发之旅(44)-ExoPlayer介绍及简单使用

目录

  1. ExoPlayer基本介绍
  2. ExoPlayer的基本使用
  3. 遇到的问题
  4. 资料
  5. 收获

从这篇开始我们进入阶段五 —— 一些音视频开源项目的学习使用分析,今天我们进入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的组件实现:

  1. MediaSource 加载媒体,通过ExoPlayer.prepare注册
  2. TrackSelector:音/视轨提取器,从MediaSource中提取出轨道的数据
  3. Render:对TrackSelector提取出来的数据进行渲染,AudioTrack播放音频、Surface渲染视频
  4. LoadControl:对MediaSource进行控制(什么时候开始缓冲、缓冲多少等)
    ExoPlayer为这些组件提供了默认的实现,如果需要定制可以自定义组件来扩展实现。

通过ExoPlayer的架构图,我们也可以看到其组件模块化的设计,这个架构设计值得学习,也是好的组件/SDK的一个重要要求。在我们的日常项目开发中,开发一个组件 从易用性和以扩展性方面考虑,既要保证使用者很容易上手使用(提供一套默认实现),又要有方便使用者根据自己的场景进行方便的扩展的能力。

1.3 状态机
在看ExoPlayer的状态机之前,我们先看下MeidaPlayer的状态机

可以看到MediaPlayer的状态比较多,使用时如果在不当的位置触发了不匹配的操作,直接回崩溃。
相比MediaPlayer,ExoPlayer的状态少了些,也更容易使用区分,不像MediaPlayer在没有prepared之前都不可以进行播放相关操作,ExoPlayer很多listener以及isplaying的API监听状态的变化。ExoPlayer的四种状态如下

 /**
   * 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

    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

 //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 播放监听
当前是否在播放中

public final boolean isPlaying() {
    return getPlaybackState() == Player.STATE_READY
        && getPlayWhenReady()
        && getPlaybackSuppressionReason() == PLAYBACK_SUPPRESSION_REASON_NONE;
  }

播放状态改变的listener、音频相关的listener、视频相关的listener

        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(会输出更详细的信息)

   //通过AnalyticsListener可以输出更多信息
        analyticsListener = EventLogger(DefaultTrackSelector())
        player.addAnalyticsListener(analyticsListener)

2.4 释放资源
在页面不可见/销毁(看是否需要后台播放)时要释放资源

    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

Failed to resolve: com.google.android.exoplayer:exoplayer: 2.13.3

2.13.3前多了一个空格,这个太….,细节有时候不注意就好浪费不少时间。

问题2

      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

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模拟器播放时偶尔会闪退,真机正常。机型设备的适配问题始终是一个大问题

四、资料

  1. Media streaming with ExoPlayer
  2. ExoPlayer blog
  3. ExoPlayer developer guide
  4. ExoPlayer播放音视频的使用介绍

五、 收获

通过本次学习实践收获如下:

  1. 了解ExoPlayer的背景以及相比MediaPlayer的优缺点
  2. 了解ExoPlayer的基本功能
  3. 简单实践

感谢你的阅读

下一篇我们继续学习实践ExoPlayer,实现一个简单的音频播放器,欢迎关注公众号“音视频开发之旅”,一起学习成长。

欢迎交流

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容