使用 ExoPlayer 流式传输媒体内容

1. 准备工作

526b239733391e74.png

屏幕截图:YouTube Android 版应用

ExoPlayer 是一款基于 Android 中的低层级媒体 API 构建的应用级媒体播放器。与 Android 内置的 MediaPlayer 相比,ExoPlayer 具有多项优势。它支持 MediaPlayer 支持的许多媒体格式,还支持 DASH 和 SmoothStreaming 等自适应格式。ExoPlayer 具有高度的可定制性和可扩展性,因此能够用于许多高级用例。它是 Google 应用(包括 YouTube 和 Google Play 影视)使用的开源项目

前提条件

  • 适度了解 Android 开发和 Android Studio

实践内容

  • 创建一个 SimpleExoPlayer 实例,用于准备和播放来自各种来源的媒体。
  • 将 ExoPlayer 与应用的 activity 生命周期集成,以在单窗口或多窗口环境中支持后台运行、前台运行和继续播放媒体内容功能。
  • 使用 MediaItem 创建播放列表。
  • 播放自适应视频串流(根据可用带宽调整媒体质量)。
  • 注册事件监听器,监听播放状态并展示如何使用监听器来衡量播放质量。
  • 使用标准 ExoPlayer 界面组件,然后根据应用的样式对其进行自定义。

所需条件

  • 最新的稳定版 Android Studio
  • 搭载 JellyBean (4.1) 或更高版本的 Android 设备(最好是 Nougat [7.1] 或更高版本,因为这些版本支持多窗口)。

2. 进行设置

获取代码

首先,请下载 Android Studio 项目:

或者,您也可以克隆 GitHub 代码库:

git clone https://github.com/googlecodelabs/exoplayer-intro.git

目录结构

通过克隆或解压缩,您会得到一个根文件夹 (exoplayer-intro),其中包含一个 Gradle 项目以及多个模块(一个应用模块,以及此 Codelab 的每个步骤的模块),还有您需要的所有资源。

导入项目

  1. 启动 Android Studio。
  2. 依次选择 File > New > Import Project
  3. 选择根 build.gradle 文件。

111b190903697765.png

屏幕截图:导入时的项目结构

构建完成后,您会看到 6 个模块:app 模块(类型为“application”),以及 5 个名为 exoplayer-codelab-N 的模块(其中的 N0004,,类型均为“library”)。app 模块实际上是空的,其中只包含一个清单。当使用 app/build.gradle 中的 Gradle 依赖项构建应用时,当前指定的 exoplayer-codelab-N 模块中的所有内容都会被合并。

app/build.gradle

dependencies {
   implementation project(":exoplayer-codelab-00")
}

媒体播放器 activity 保存在 exoplayer-codelab-N 模块中。之所以将其保存在单独的库模块中,是为了让您可以在针对不同目标平台(例如,移动设备和 Android TV)的 APK 之间共享它。它还允许您充分利用各种功能,例如 Dynamic Delivery。使用 Dynamic Delivery 功能后,只有当用户需要时,系统才允许安装您的媒体播放功能。

  1. 部署并运行应用,以检查是否一切正常。应用应该以黑色为背景填充屏幕。

2dae13fed92e6c8c.png

屏幕截图:空白应用正在运行

3. 流式传输!

添加 ExoPlayer 依赖项

ExoPlayer 是一个托管在 GitHub 上的开源项目。每个版本均通过 Google Maven 进行分发,后者是 Android Studio 和 Gradle 使用的默认软件包代码库之一。每个版本均以采用如下格式的字符串作为唯一标识:

com.google.android.exoplayer:exoplayer:X.X.X

您只需导入 ExoPlayer 的类和界面组件,即可将 ExoPlayer 添加到您的项目。它非常小巧,其收缩占用空间约为 70-300 kB,具体大小取决于包含的功能和支持的格式。ExoPlayer 库分为不同的模块,开发者只需导入自己需要的功能。如需详细了解 ExoPlayer 的模块化结构,请参阅添加 ExoPlayer 模块

  1. 打开 player-lib 模块的 build.gradle 文件。
  2. 将以下代码行添加到 dependencies 部分并同步项目。

exoplayer-codelab-00/build.gradle

dependencies {
   [...]

implementation 'com.google.android.exoplayer:exoplayer-core:2.12.0'
implementation 'com.google.android.exoplayer:exoplayer-dash:2.12.0'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.12.0'

}

添加 PlayerView element

  1. 打开 exoplayer-codelab-00 模块中的布局资源文件 activity_player.xml
  2. 将光标放在 FrameLayout 元素内。
  3. 开始输入 <PlayerView,并让 Android Studio 自动补全 PlayerView 元素。
  4. widthheight 使用 match_parent
  5. 将 ID 声明为 video_view

activity_player.xml

<com.google.android.exoplayer2.ui.PlayerView
   android:id="@+id/video_view"
   android:layout_width="match_parent"
   android:layout_height="match_parent"/>

从现在起,您要引用该界面元素作为视频视图。

  1. PlayerActivity 中,您现在可以获取对在您刚才修改的 XML 文件中创建的视图树的引用。

PlayerActivity.kt

    private val viewBinding by lazy(LazyThreadSafetyMode.NONE) {
        ActivityPlayerBinding.inflate(layoutInflater)
    }
  1. 将视图树的根目录设为您的 activity 的内容视图。此外,还要检查 viewBinding 引用上的 videoView 属性是否可见,以及其类型是否为 PlayerView
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(viewBinding.root)
    }

创建 ExoPlayer

若要播放流媒体,您需要一个 ExoPlayer 对象。若要创建该对象,最简单的方法是使用 SimpleExoPlayer.Builder 类。顾名思义,就是使用构建器模式来构建 SimpleExoPlayer 实例。

SimpleExoPlayerExoPlayer 接口的一个方便、通用的实现。

添加私有方法 initializePlayer 来创建您的 SimpleExoPlayer

PlayerActivity.kt

private var player: SimpleExoPlayer? = null
[...]
   private fun initializePlayer() {
        player = SimpleExoPlayer.Builder(this)
            .build()
            .also { exoPlayer ->
                viewBinding.videoView.player = exoPlayer
            }
    }

根据您的上下文创建 SimpleExoPlayer.Builder,然后调用 build 来创建 SimpleExoPlayer 对象。然后,系统会将其分配给 player,您需要将其声明为成员字段。然后,您可以使用 viewBinding.videoView.player 可变属性将 player 绑定到其对应视图。

创建媒体项

现在,player 需要一些可以播放的内容。为此,您要创建一个 MediaItem。有许多不同类型的 MediaItem,但您首先要针对互联网上的 MP3 文件创建一个。

若要创建 MediaItem,最简单的方法是使用 MediaItem.fromUri,后者会接受媒体文件的 URI。使用 player.setMediaItemMediaItem 添加到 player

  1. 将以下代码添加到 also 块中的 initializePlayer

PlayerActivity.kt

private fun initializePlayer() {
    [...]
        .also { exoPlayer ->
            [...]
            val mediaItem = MediaItem.fromUri(getString(R.string.media_url_mp3))
            exoPlayer.setMediaItem(mediaItem)
        }
}

请注意,R.string.media_url_mp3strings.xml 中定义为 https://storage.googleapis.com/exoplayer-test-media-0/play.mp3

根据 activity 生命周期实现精细播放

我们的 player 可能会占用大量资源,包括内存、CPU、网络连接和硬件编解码器。其中许多资源都很短缺,尤其是对于硬件编解码器(可能只有一个)来说更是如此。当您不使用这些资源时(例如,当您的应用已置于后台时),请务必释放这些资源以供其他应用使用。

换句话说,播放器的生命周期应该与应用的生命周期相关联。若要实现这一点,您需要替换 PlayerActivity 的 4 个方法:onStartonResumeonPauseonStop

  1. PlayerActivity 处于打开状态时,依次点击 Code menu > Override methods…
  2. 选择 onStartonResumeonPauseonStop
  3. 根据 API 级别,在 onStartonResume 回调中初始化播放器。

PlayerActivity.kt

public override fun onStart() {
 super.onStart()
 if (Util.SDK_INT >= 24) {
   initializePlayer()
 }
}

public override fun onResume() {
 super.onResume()
 hideSystemUi()
 if ((Util.SDK_INT < 24 || player == null)) {
   initializePlayer()
 }
}

Android API 级别 24 及更高版本支持多窗口。由于您的应用在分屏模式下可见,但不处于活动状态,因此您需要在 onStart 中初始化播放器。由于 Android API 级别 24 及更低版本要求您尽可能多等些时间,直到您获取到资源为止,因此您要等到 onResume 再初始化播放器。

  1. 添加 hideSystemUi 方法。

PlayerActivity.kt

@SuppressLint("InlinedApi")
private fun hideSystemUi() {
 viewBinding.videoView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LOW_PROFILE
     or View.SYSTEM_UI_FLAG_FULLSCREEN
     or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
     or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
     or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
     or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION)
}

hideSystemUi 是在 onResume 中调用的辅助方法,借助该方法,您可以实现全屏体验。

  1. onPauseonStop 中使用 releasePlayer(稍后就会创建)释放资源。

PlayerActivity.kt

public override fun onPause() {
 super.onPause()
 if (Util.SDK_INT < 24) {
   releasePlayer()
 }
}

public override fun onStop() {
 super.onStop()
 if (Util.SDK_INT >= 24) {
   releasePlayer()
 }
}

对于 API 级别 24 及更低版本,系统并不一定会调用 onStop,因此您必须尽早在 onPause 中释放播放器。对于 API 级别 24 及更高版本(具备多窗口模式和分屏模式),系统一定会调用 onStop。在暂停状态下,您的 activity 仍然可见,因此您要等到 onStop 再释放播放器。

您现在需要创建一个 releasePlayer 方法,用于释放播放器的资源并销毁播放器。

  1. 将以下代码添加到相应 activity:

PlayerActivity.kt

private var playWhenReady = true
private var currentWindow = 0
private var playbackPosition = 0L
[...]

private fun releasePlayer() {
    player?.run {
        playbackPosition = this.currentPosition
        currentWindow = this.currentWindowIndex
        playWhenReady = this.playWhenReady
        release()
    }
    player = null
}

在释放和销毁播放器之前,请存储以下信息:

  • 使用 playWhenReady 存储播放/暂停状态。
  • 使用 currentPosition 存储当前播放位置。
  • 使用 currentWindowIndex 存储当前窗口索引。如需详细了解相关窗口,请参阅时间轴

这样一来,您即可从用户停止播放的位置继续播放。您需要做的就是在初始化播放器时提供这些状态信息。

最终准备

现在,您要做的就是在初始化期间将您保存在 releasePlayer 中的状态信息提供给播放器。

  1. 将以下代码添加到 initializePlayer 中:

PlayerActivity.kt

private fun initializePlayer() {
    [...]
    exoPlayer.playWhenReady = playWhenReady
    exoPlayer.seekTo(currentWindow, playbackPosition)
    exoPlayer.prepare()
}

此时会发生下列情况:

  • playWhenReady 告知播放器是否在获取所有播放资源后立即开始播放。由于 playWhenReady 最初为 true,因此在第一次运行应用时,播放会自动开始。
  • seekTo 告知播放器在特定窗口中寻找某个位置。currentWindowplaybackPosition 都初始化为零,以便在应用第一次运行时从头开始播放。
  • prepare 告知播放器获取播放所需的所有资源。

播放音频

最后,一切就绪!启动应用即可播放 MP3 文件并看到嵌入的海报图片。

d92917867ee23ef8.png

屏幕截图:应用正在播放一个曲目。

测试 activity 生命周期

测试应用在 activity 生命周期的所有不同状态下是否都能正常运行。

  1. 启动另一个应用,然后重新将您的应用置于前台。您的应用是否在正确的位置继续播放?
  2. 暂停应用,将其移到后台,然后重新移到前台。在移到后台进入暂停状态后,它是否始终保持暂停状态?
  3. 旋转应用。如果您将屏幕方向从竖屏改为横屏,然后再改回来,它的行为是怎样的?

播放视频

如果要播放视频,只需将媒体项 URI 修改为 MP4 文件即可。

  1. initializePlayer 中的 URI 更改为 R.string.media_url_mp4
  2. 重新启动应用,同样在视频播放中将应用移到后台,并测试这之后的行为。

PlayerActivity.kt

private fun initializePlayer() {
  [...]
     val mediaItem = MediaItem.fromUri(getString(R.string.media_url_mp4));
  [...]
}

PlayerView 会完成所有相关操作。视频(而非海报图片)会全屏呈现。

425c6c65f78e8d46.png

屏幕截图:应用正在播放视频。

真厉害!您刚刚制作了一款能够在 Android 上全屏流式传输媒体的应用,以及配套的生命周期管理、保存状态和界面控件!

4. 创建播放列表

您当前的应用可以播放单个媒体文件,但如果您想让多个媒体文件相继播放,该怎么办呢?为此,您需要使用播放列表。

如需创建播放列表,您可以使用 addMediaItemplayer 添加多个 MediaItem。这样可以实现无缝播放,并且系统会在后台处理缓冲,因此在更改媒体项时,用户不会看到“正在缓冲”旋转图标。

  1. 将以下代码添加到 initializePlayer

PlayerActivity.kt

private void initializePlayer() {
  [...]
  exoPlayer.addMediaItem(mediaItem) // Existing code

  val secondMediaItem = MediaItem.fromUri(getString(R.string.media_url_mp3));
  exoPlayer.addMediaItem(secondMediaItem);
  [...]
}

检查播放器控件的行为方式。您可以使用 1f79fee4d082870f.png39627002c03ce320.png 在媒体项序列中跳转。

7b5c034dafabe1bd.png

屏幕截图:显示“下一个”和“上一个”按钮的播放控件

这很方便!如需了解详情,请参阅有关媒体项播放列表的开发者文档,以及有关 Playlist API 的这篇文章

5. 自适应流式传输

自适应流式传输是一种媒体流式传输技术,可根据可用网络带宽改变串流质量。借助该技术,用户可体验其带宽支持的最高质量的媒体内容。

通常,同一媒体内容会分为多个轨道,其质量(比特率和分辨率)各有不同。播放器会根据可用的网络带宽来选择轨道。

每个轨道会分成给定时长的数据块,其时长通常在 2 到 10 秒之间。这样一来,播放器就可以随着可用带宽的变化快速切换不同轨道。播放器负责将这些数据块拼接在一起以实现无缝播放。

自适应轨道选择

自适应流式传输的核心是为当前环境选择最合适的轨道。您可以使用自适应轨道选择功能将您的应用更新为播放流媒体。

  1. 请使用以下代码更新 initializePlayer

PlayerActivity.kt

private fun initializePlayer() {
   val trackSelector = DefaultTrackSelector(this).apply {
        setParameters(buildUponParameters().setMaxVideoSizeSd())
    }
   player = SimpleExoPlayer.Builder(this)
        .setTrackSelector(trackSelector)
        .build()
  [...]
}

首先,创建一个 DefaultTrackSelector,它将负责选择媒体项中的轨道。然后,告知 trackSelector 只选择标准清晰度或更低清晰度的轨道,这是以牺牲质量为代价节省用户流量的好方法。最后,将 trackSelector 传递给构建器,以便在构建 SimpleExoPlayer 实例时使用。

构建自适应 MediaItem

DASH 是一种广泛使用的自适应流式传输格式。如需流式传输 DASH 内容,您需要像以前一样创建 MediaItem。不过,这一次,我们必须使用 MediaItem.Builder,而不是 fromUri

这是因为 fromUri 使用文件扩展名来确定底层媒体格式,但 DASH URI 没有文件扩展名,因此我们在构造 MediaItem 时必须提供 APPLICATION_MPDMIME 类型

  1. 更新 initializePlayer,具体代码如下所示:

PlayerActivity.kt

private void initializePlayer() {
  [...]

  // Replace this line
  val mediaItem = MediaItem.fromUri(getString(R.string.media_url_mp4));

  // With this
   val mediaItem = MediaItem.Builder()
        .setUri(getString(R.string.media_url_dash))
        .setMimeType(MimeTypes.APPLICATION_MPD)
        .build()

  // Also remove the following lines
  val secondMediaItem = MediaItem.fromUri(getString(R.string.media_url_mp3))
    exoPlayer.addMediaItem(secondMediaItem)
}
  1. 重启应用,并查看包含 DASH 的自适应视频串流的实际效果。借助 ExoPlayer,您可以轻松实现这项操作!

其他自适应流式传输格式

其他常用的自适应流式传输格式为 HLS (MimeTypes.APPLICATION_M3U8) 和 SmoothStreaming (MimeTypes.APPLICATION_SS),二者均受 ExoPlayer 支持。如需详细了解其他自适应媒体来源的构造方式,请参阅 ExoPlayer 演示版应用

6. 监听事件

在前面的步骤中,您学习了如何流式传输渐进式媒体串流和自适应媒体串流。ExoPlayer 会在幕后为您处理大量工作,其中包括以下各项:

  • 分配内存
  • 下载容器文件
  • 从容器中提取元数据
  • 解码数据
  • 向屏幕和扬声器呈现视频、音频和文本

有时,了解 ExoPlayer 在运行时执行的操作有助于了解和改进用户的播放体验。

例如,您可能想要通过执行以下操作来反映界面中的播放状态变化:

  • 在播放器进入缓冲状态时显示“正在加载”旋转图标
  • 在轨道播放结束后显示叠加层和“接下来观看”选项

ExoPlayer 提供了几个监听器接口,用于提供对有用事件的回调。您可以使用监听器来记录播放器所处的状态。

监听

  1. PlayerActivity 类之外创建一个 TAG 常量(后面您将使用该常量进行日志记录)。

PlayerActivity.kt

private const val TAG = "PlayerActivity"
  1. PlayerActivity 类之外的工厂函数中实现 Player.EventListener 接口。这用于通知您重要的播放器事件,其中包括错误和播放状态变化。
  2. 添加以下代码,替换 onPlaybackStateChanged

PlayerActivity.kt

private fun playbackStateListener() = object : Player.EventListener {
    override fun onPlaybackStateChanged(playbackState: Int) {
        val stateString: String = 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(TAG, "changed state to $stateString")
    }
}
  1. PlayerActivity 中声明一个 Player.EventListener 类型的私有成员。

PlayerActivity.kt

class PlayerActivity : AppCompatActivity() {
    [...]

    private val playbackStateListener: Player.EventListener = playbackStateListener()
}

当播放状态发生变化时,系统会调用 onPlaybackStateChanged。新状态由 playbackState 参数提供。

播放器可能会处于以下 4 种状态之一:

状态

说明

ExoPlayer.STATE_IDLE

播放器已实例化,但尚未准备就绪。

ExoPlayer.STATE_BUFFERING

播放器无法从当前位置开始播放,因为已缓冲的数据不足。

ExoPlayer.STATE_READY

播放器可以立即从当前位置开始播放。这意味着如果播放器的 playWhenReady 属性为 true,播放器将自动开始播放媒体。如果该属性为 false,播放器会暂停播放。

ExoPlayer.STATE_ENDED

播放器已完成媒体播放。

注册监听器

如需调用您的回调,您需要向播放器注册 playbackStateListener。该操作可在 initializePlayer 中完成。

  1. 请在播放准备就绪之前注册该监听器。

PlayerActivity.kt

private void initializePlayer() {
    [...]
    exoPlayer.seekTo(currentWindow, playbackPosition)
    exoPlayer.addListener(playbackStateListener)
    [...]
}

同样,您需要进行整理以避免来自播放器的悬空引用,这可能会导致内存泄漏。

  1. releasePlayer 中移除该监听器:

PlayerActivity.kt

private void releasePlayer() {
 player?.run {
   [...]
   removeListener(playbackStateListener)
   release()
 }
  player = null
}
  1. 打开 logcat 并运行应用。
  2. 使用界面控件对播放进行跳转、暂停和恢复。您应该会在日志中看到播放状态的变化。

继续深入

ExoPlayer 提供了许多其他监听器,有助于您了解用户的播放体验。其中包括音频视频的监听器,以及 AnalyticsListener(其中包含来自所有监听器的回调)。以下是一些最重要的方法:

  • 当视频的第一帧呈现时,系统会调用 onRenderedFirstFrame。根据这项信息,您可以计算用户必须等待多长时间才能在屏幕上看到有意义的内容。
  • 当视频丢帧时,系统会调用 onDroppedVideoFrames。丢帧表示播放不流畅,且用户体验可能很差。
  • 当发生音频欠载时,系统会调用 onAudioUnderrun。欠载会导致出现声音故障,并且比视频丢帧更明显。

可以使用 addAnalyticsListenerAnalyticsListener 添加到 player音频视频的监听器也有对应的方法。

您需要考虑哪些事件对您的应用和用户很重要。如需了解详情,请参阅监听播放器事件。事件监听器就讲到这里!

7. 自定义界面

到目前为止,您一直在使用 ExoPlayer 的 PlayerControlView 向用户显示播放控制器。

bcfe17eebcad9e13.png

屏幕截图:默认播放控制器

如果您想更改这些控件的功能或外观,该怎么办?幸运的是,这些控件是高度可定制的。

第一项简单的自定义是完全不使用控制器。这很容易实现,只需在 activity_player.xml 中的 PlayerView 元素上使用 use_controller 属性即可。

  1. use_controller 设为 false,控件就不会再显示了:

activity_player.xml

<com.google.android.exoplayer2.ui.PlayerView
   [...]
   app:use_controller="false"/>
  1. 将以下命名空间添加到 FrameLayout

activity_player.xml

<FrameLayout
  [...]
  xmlns:app="http://schemas.android.com/apk/res-auto">

现在试一下吧。

自定义行为

PlayerControlView 包含几个对其行为有影响的属性。例如,您可以使用 show_timeout 自定义从用户最后一次与控件互动到控件隐藏之间的延迟(以毫秒为单位)。为此,请执行以下操作:

  1. 移除 app:use_controller="false"
  2. 更改播放器视图以使用 show_timeout

activity_player.xml

<com.google.android.exoplayer2.ui.PlayerView
   android:id="@+id/video_view"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   app:show_timeout="10000"/>

PlayerControlView 的属性也可通过编程方式设置

自定义外观

好了,初步设置进展不错。但是,如果您想更改 PlayerControlView 的外观或显示的按钮,该怎么办?PlayerControlView 的实现并未假设任何按钮存在,因此您可以轻松地删除按钮并添加新按钮。

我们来看看如何自定义 PlayerControlView

  1. 在文件夹 player-lib/res/layout/ 中创建新的布局文件 custom_player_control_view.xml
  2. 从布局文件夹的上下文菜单中,依次选择 New - Layout resource file 并将文件命名为 custom_player_control_view.xml

ae1e3795726d4e4e.png

屏幕截图:播放器控件视图的布局文件已创建。

  1. 原始布局文件从此处复制到 custom_player_control_view.xml
  2. 移除 ID 为 @id/exo_prev@id/exo_nextImageButton 元素。

如需使用自定义布局,您需要在 activity_player.xml 文件中设置 PlayerView 元素的属性 app:controller_layout_id

  1. 使用自定义文件的布局 ID,如以下代码段所示:

activity_player.xml

<com.google.android.exoplayer2.ui.PlayerView
   android:id="@+id/video_view"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   app:controller_layout_id="@layout/custom_player_control_view"/>
  1. 重新启动应用。此时,播放器控件视图不再具有“上一个”和“下一个”按钮。

89e6535a22c8e321.png

屏幕截图:没有“上一个”和“下一个”按钮的自定义播放器控件视图

您可以视需要在布局文件中应用任何更改。默认情况下,系统会选择 Android 主题的颜色。您可以替换此设置以匹配您应用的设计。

  1. 向每个 ImageButton 元素分别添加一个 android:tint 属性:

custom_player_control_view.xml

<ImageButton android:id="@id/exo_rew"
   android:tint="#FF00A6FF"
   style="@style/ExoMediaButton.Rewind"/>
  1. 将您在自定义文件中找到的所有 android:textColor 属性更改为同一颜色:#FF00A6FF

custom_player_control_view.xml

<TextView android:id="@id/exo_position"
   [...]
   android:textColor="#FF00A6FF"/>
<TextView android:id="@id/exo_duration"
   [...]
   android:textColor="#FF00A6FF"/>
  1. 运行应用。现在,您就得到了漂亮的彩色界面组件!

e9835d65d6dd0634.png

屏幕截图:彩色按钮和文本视图

替换默认样式

您刚刚创建了一个自定义布局文件,并在 activity_player.xml 中使用 controller_layout_id 引用了它。

另一种方法是替换 PlayerControlView 使用的默认布局文件。从 PlayerControlView 的源代码可以看出,它使用 R.layout.exo_player_control_view 进行布局。如果您使用相同的文件名创建自己的布局文件,PlayerControlView 就会改用您的文件。

  1. 移除刚刚添加的 controller_layout_id 属性。
  2. 删除文件 custom_player_control_view.xml

现在,activity_player.xml 中的 PlayerView 方法应如下所示:

activity_player.xml

<com.google.android.exoplayer2.ui.PlayerView
   android:id="@+id/video_view"
   android:layout_width="match_parent"
   android:layout_height="match_parent"/>
  1. 在库模块 player-libres/layout 文件夹中创建一个名为 exo_player_control_view.xml 的文件。
  2. 将以下代码插入 exo_player_control_view.xml 以添加播放按钮、暂停按钮和带有徽标的 ImageView

exo_player**_control_view.xml**

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
   xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
   android:layout_gravity="bottom"
   android:layoutDirection="ltr"
   android:background="#CC000000"
   android:orientation="vertical">

 <LinearLayout
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
   android:gravity="center"
   android:paddingTop="4dp"
   android:orientation="horizontal">

   <ImageButton android:id="@id/exo_play"
      style="@style/ExoMediaButton.Play"/>

   <ImageButton android:id="@id/exo_pause"
      style="@style/ExoMediaButton.Pause"/>

 </LinearLayout>

 <ImageView
     android:contentDescription="@string/logo"
     android:src="@drawable/google_logo"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"/>

</LinearLayout>

以上操作演示了如何在此处添加您自己的元素,并将它们与标准控件元素融为一体。现在,当与控件的互动保持不变时,ExoPlayerView 会使用您的自定义控件以及所有隐藏和显示逻辑。

8. 恭喜

恭喜!您已经学习了很多关于如何将 ExoPlayer 与您的应用集成的知识。

了解详情

如需详细了解 ExoPlayer,请查看开发者指南源代码,并订阅 ExoPlayer 博客