Media3 - ExoPlayer 打造音视频播放器(一)

1,177 阅读4分钟

Jetpack Media3 是媒体库的新家,可让 Android 应用显示丰富的视听体验。媒体播放器是允许播放视频和音频文件的应用级组件,Media3 提供了一个 Player 接口,用于定义基本功能,如播放,暂停,跳转和显示曲目信息等功能,而 ExoPlayer 是 Media3 中此接口的默认实现。

引入依赖

implementation("androidx.media3:media3-exoplayer:1.2.1")
implementation("androidx.media3:media3-exoplayer-dash:1.2.1")
implementation("androidx.media3:media3-ui:1.2.1")

需要注意的是,使用此库需要将 compileSdk 升到34及其以上,不然会编译报错。

image.png

创建播放器

使用 ExoPlayer 打造一个视频播放器,先创建一个 ExoPlayer 实例,然后再将播放器附加到视图 PlayerView 上,然后填充播放列表并准备播放即可,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context=".ExoPlayerActivity">

        <androidx.media3.ui.PlayerView
            android:id="@+id/playView"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

    </LinearLayout>
</layout>
val player = ExoPlayer.Builder(context).build()
binding.playView.player = player
val mediaItem = MediaItem.fromUri(VIDEO_URL)
player.run {
    setMediaItem(mediaItem)
    prepare()
    play()
}

如果有多个媒体内容需要播放,想一个接一个播放,ExoPlayer 也支持播放列表,只需创建多个 MediaItem,然后再将其加入即可。

val player = ExoPlayer.Builder(context).build()
binding.playView.player = player

val mediaItem1 = MediaItem.fromUri(VIDEO_URL1)
val mediaItem2 = MediaItem.fromUri(VIDEO_URL2)
player.apply {
    addMediaItem(mediaItem1)
    addMediaItem(mediaItem2)
    prepare()
    play()
}

这样,一个简易的视频播放器就做好了,这是 ExoPlayer 的默认实现,如下图所示:

image.png

如果想要播放已经储存好的视频文件,直接把链接换成文件路径即可。

player = ExoPlayer.Builder(context).build()
binding.playView.player = player
val mediaItem = MediaItem.fromUri(Uri.parse("${getExternalFilesDir(null)}/test.mp4"))
player.run {
    setMediaItem(mediaItem)
    prepare()
    play()
}

记得在不需要播放器的时候将其释放,以便释放有限的资源。

override fun onDestroy() {
    super.onDestroy()
    player.release()
}

控制播放器

播放器就绪后,就可以调用 ExoPlayer 的一些方法来控制播放:

  • play:开始播放
  • pause:暂停播放
  • hasPreviousMediaItem:上一个媒体内容是否存在
  • seekToPreviousMediaItem:播放上一个媒体内容
  • hasNextMediaItem:下一个媒体内容是否存在
  • seekToNextMediaItem:播放下一个媒体内容
  • seekTo:跳转到媒体文件的指定位置进行播放,该方法接受一个时间戳参数,表示跳转的时间点,单位为毫秒,用于指定跳转的位置。
  • repeatMode:设置循环模式,有三个值:Player.REPEAT_MODE_OFF(默认模式,播放器不会自动重复播放任何内容),Player.REPEAT_MODE_ONE(当媒体播放结束时,播放器会自动重复播放当前媒体),Player.REPEAT_MODE_ALL(当媒体播放结束时,播放器会自动从队列中选择下一个媒体进行播放,并循环播放整个队列)
  • shuffleModeEnabled:控制播放器是否启用随机播放模式
  • setPlaybackParameters:调整播放速度和音调

在随机播放模式下,播放器将按预计算的随机顺序播放播放列表,所有项都会播放一次,此时 seekToNextMediaItem 也是根据重排顺序播放下一项,关闭随机播放模式后,系统会从当前项在播放列表中的原始位置继续播放。

setPlaybackParameters 接受一个 PlaybackParameters 对象作为参数,该对象包含了两个属性:speed 和 pitch。speed 表示播放速度,1.0 表示正常播放速度,小于 1.0 表示慢放,大于 1.0 表示快放。pitch 表示音调,1.0 表示原始音调,小于 1.0 表示降低音调,大于 1.0 表示升高音调。

val playbackParameters = PlaybackParameters(speed, pitch)
player.playbackParameters = playbackParameters

播放器事件

系统会将事件,例如状态更改或播放错误等,报告给已注册的 Player.Listener 实例,我们可以注册监听器来接收此类事件。

player.addListener(object : Player.Listener {
    //播放器状态变化
    override fun onPlaybackStateChanged(playbackState: Int) {
        super.onPlaybackStateChanged(playbackState)
        when (playbackState) {
            Player.STATE_IDLE -> {
                //空闲状态:表示当前没有任何媒体数据可供播放
            }

            Player.STATE_BUFFERING -> {
                //缓冲状态:表示当前正在获取媒体数据进行缓冲
            }

            Player.STATE_READY -> {
                //就绪状态:表示已经准备好播放媒体的数据
            }

            Player.STATE_ENDED -> {
                //结束状态:表示已经播放完所有媒体数据
            }

        }
    }

    //监听是否正在播放中状态,也可以通过 player.isPlaying 获取
    override fun onIsPlayingChanged(isPlaying: Boolean) {
        super.onIsPlayingChanged(isPlaying)
    }

    //播放错误
    override fun onPlayerError(error: PlaybackException) {
        super.onPlayerError(error)
        val cause = error.cause
        if (cause is HttpDataSource.HttpDataSourceException) {
            // An HTTP error occurred
        }

    }

    //检测播放何时转换为其他媒体项
    override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
        super.onMediaItemTransition(mediaItem, reason)
    }
})

媒体内容

修改播放列表

//在播放列表的位置0添加媒体项目
player.addMediaItem(0, MediaItem.fromUri(VIDEO_URL))
//删除播放列表中位置为0媒体项目
player.removeMediaItem(0)
//将位置为0的媒体项目移动到位置2
player.moveMediaItem(0, 2)
//替换位置为0的媒体项目
player.replaceMediaItem(0, MediaItem.fromUri(VIDEO_URL))
//清空媒体列表
player.clearMediaItems()
//替换整个播放列表
val mediaItems = arrayListOf<MediaItem>()
mediaItems.add(MediaItem.fromUri(VIDEO_URL1))
mediaItems.add(MediaItem.fromUri(VIDEO_URL2))
mediaItems.add(MediaItem.fromUri(VIDEO_URL3))
player.replaceMediaItems(0, player.mediaItemCount, mediaItems)

查询 MediaItem

val firstMediaItem = player.getMediaItemAt(0)
val currentMediaItem = player.currentMediaItem
val nextMediaItemIndex = player.nextMediaItemIndex
val previousMediaItemIndex = player.previousMediaItemIndex

播放列表中的内容之间可以无缝切换,不要求它们的格式相同,也可以是不同的类型,比如一个播放列表中可以同时包含视频和音频流。

标识播放列表项

设置 mediaId

val mediaItem = MediaItem.Builder().setUri(VIDEO_URL).setMediaId("666").build()

也可以设置任何对象,将元数据附加到每项媒体内容。

val media = Media("id", "name", "url")
val mediaItem = MediaItem.Builder().setUri(VIDEO_URL).setTag(media).build()

然后根据 MediaItem 的 localConfiguration 获取到该 Tag

val data = player.currentMediaItem
val tag = data?.localConfiguration?.tag

剪辑媒体项

val mediaItem = MediaItem.Builder().setUri(VIDEO_URL)
    .setClippingConfiguration(
        MediaItem.ClippingConfiguration.Builder()
            .setStartPositionMs(startPositionMs)
            .setEndPositionMs(endPositionMs)
            .build()
    )
    .build()

直播

ExoPlayer 天然就支持大多数自适应直播,无需任何特殊配置。比方说,有 RTSP 的直播链接想要播放,需要引入额外的依赖。

implementation("androidx.media3:media3-exoplayer-rtsp:$media3_version")
player = ExoPlayer.Builder(context).build()
binding.playView.player = player
val mediaItem = MediaItem.fromUri(RTSP_URL)
player.run {
    setMediaItem(mediaItem)
    prepare()
    play()
}

但是,如果想要实现更加丰富的直播效果,个人还是不建议使用 ExoPlayer 的,坑比较多,就比如,我播放 RTMP 的时候就一直是失败的,明明有引入 media3 的 RTMP 库了,还是报错不支持的格式,我也不晓得是为啥?有知道的大佬可以告知一下,不胜感谢。

RTMP 和 RTSP 都是用于流媒体传输的协议,但在一些方面上有一些区别:

  • 功能和用途:RTMP 主要用于音视频的实时传输和播放,支持低延迟的双向通信。它可以传输音频、视频和数据,主要用于直播和视频点播应用。RTSP 并不传输音视频数据本身,而是负责控制媒体流的传输,它提供了对音视频流的控制,例如播放,暂停,快进,后退等操作,主要用于实时视频监控和安防监控等应用。
  • 传输方式:RTMP 使用 TCP 作为传输层协议,可以保证可靠性和稳定性。它将音视频数据打包成消息进行传输,并且支持实时的双向通信。RTSP 可以使用 TCP 或 UDP 进行传输,使用 TCP 时,可以保证可靠性,但延迟较高,使用 UDP 时,可以实现较低的延迟,但可能会出现丢包等问题。
  • 支持的编码格式:RTMP 支持多种音视频编码格式,包括H.264,AAC等。RTSP 本身并不限制支持的编码格式,它可以与其他协议一起使用,例如RTSP + RTP,用于传输实时音视频数据。

RTMP 在现代的流媒体应用中逐渐被其他协议所取代,比如 HLS 和 DASH,而 RTSP 仍然在一些特定领域中得到广泛应用。

线程问题

ExoPlayer 实例必须从单个应用线程访问,在绝大多数情况下,应该是应用的主线程。使用 ExoPlayer 的界面组件或 IMA 扩展程序时,必须使用应用的主线程。在任何情况下,都可以调用 player.applicationLooper 获取与 ExoPlayer 实例绑定的那个 Looper,这通常是应用的主 Looper。

也可以在创建播放器时传递 Looper,从而显式指定必须在哪个线程上访问 ExoPlayer 实例,如果未指定 Looper,则使用创建播放器的线程的 Looper,如果该线程没有 Looper,则使用应用主线程的 Looper。

player = ExoPlayer.Builder(context).setLooper(Looper.getMainLooper()).build()

举个例子,我在主线程创建一个 ExoPlayer,然后在子线程去操作这个对象。

player = ExoPlayer.Builder(context).build()
binding.playView.player = player

thread {
    player.run {
        setMediaItem(mediaItem1)
        prepare()
        play()
    }
}

这段代码就会造成异常闪退,因为在错误的线程访问了 ExoPlayer ,就会报错 java.lang.IllegalStateException: Player is accessed on the wrong thread.