这篇文章接上文 Media3 – ExoPlayer 打造音视频播映器(一),上文首要讲解了 Media3 – ExoPlayer 的基本理论,包含一些常用的特点和办法,这篇文章就首要讲讲实操吧!一般来说,音视频播映的界面都是林林总总的,默许的界面不能满足咱们的需求,这就需求咱们自界说播映器的界面了,运用 Media3 ExoPlayer 自界说播映界面可以供给愈加丰厚和个性化的用户体会。
操控器布局
在 Media3 PlayerView 中,特点 app:controller_layout_id 指向了一个自界说的操控器布局 control_layout.xml,这个布局文件界说了播映器操控器的外观和组成部分。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".VideoPlayActivity">
<androidx.media3.ui.PlayerView
android:id="@+id/playView"
android:layout_width="match_parent"
android:layout_height="366dp"
android:background="@color/black"
app:controller_layout_id="@layout/control_layout" />
</LinearLayout>
只要 controller_layout_id 指向了一个自界说的布局,ExoPlayer 默许的一切操控器作用都会消失,都需求自己去完成。这里在顶部界说了一个标题栏,用于显现标题和回来按钮,底部便是操控视频的一些按钮了,比方播映和暂停,上下集切换按钮,播映进展条等等。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:background="@color/black"
android:paddingTop="6dp"
android:paddingBottom="6dp">
<ImageView
android:id="@+id/go_back"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginStart="4dp"
android:src="@drawable/back" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="视频标题"
android:textColor="@color/white"
android:textSize="15sp" />
</RelativeLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:background="@color/black"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="6dp">
<ImageButton
android:id="@+id/pre_btn"
style="@style/ExoMediaButton.Previous"
android:layout_width="30dp"
android:layout_height="30dp"
android:visibility="gone" />
<ImageButton
android:id="@+id/play_btn"
android:layout_width="30dp"
android:layout_height="30dp"
android:background="@drawable/pause" />
<ImageButton
android:id="@+id/next_btn"
style="@style/ExoMediaButton.Next"
android:layout_width="30dp"
android:layout_height="30dp"
android:visibility="gone" />
<SeekBar
android:id="@+id/seekbar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<TextView
android:id="@+id/play_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="00:00/00:00"
android:textColor="@color/white"
android:textSize="15sp" />
</LinearLayout>
</RelativeLayout>
完成操控功能
咱们先在此参加三个媒体项
val mediaItem1 = MediaItem.fromUri(VIDEO_URL1)
val mediaItem2 = MediaItem.fromUri(VIDEO_URL2)
val mediaItem3 = MediaItem.fromUri(VIDEO_URL3)
exoPlayer = ExoPlayer.Builder(this).build()
playView.player = exoPlayer
exoPlayer.apply {
addMediaItem(mediaItem1)
addMediaItem(mediaItem2)
addMediaItem(mediaItem3)
prepare()
}
先来处理一下上一集,下一集,播映或暂停,直接调用 ExoPlayer 的相关办法即可,这个很简略。
preBtn.setOnClickListener {
if (exoPlayer.hasPreviousMediaItem()) {
exoPlayer.seekToPreviousMediaItem()
}
}
nextBtn.setOnClickListener {
if (exoPlayer.hasNextMediaItem()) {
exoPlayer.seekToNextMediaItem()
}
}
playBtn.setOnClickListener {
if (isPlaying) {
exoPlayer.pause()
playBtn.background =
ContextCompat.getDrawable(this@VideoPlayActivity, R.drawable.play)
} else {
exoPlayer.play()
playBtn.background =
ContextCompat.getDrawable(this@VideoPlayActivity, R.drawable.pause)
}
}
对于播映进展的处理,咱们可以弄一个计时器,每秒查看一次 currentPosition,这个回来的单位是毫秒,所以需求先转化成秒,以便转化为 00:00 的格局去做展示。同时,监听 seekBar 的拖动事件,根据拖动的方位跳转到媒体文件的对应的方位进行播映。
private fun setProgress() {
progressJob?.cancel()
progressJob = lifecycleScope.launch {
while (isActive) {
if (!isDragging) {
val currentPosition = exoPlayer.currentPosition
val currentTime = formatTimestamp(currentPosition / 1000)
val totalDuration = exoPlayer.duration
seekBar.max = totalDuration.toInt()
val totalTime = formatTimestamp(totalDuration / 1000)
playTime.text = "$currentTime/$totalTime"
seekBar.progress = currentPosition.toInt()
}
delay(1000)
}
}
seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
if (fromUser) { //如果是用户拖动的,则更新播映方位。
exoPlayer.seekTo(progress.toLong())
}
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {
isDragging = true
}
override fun onStopTrackingTouch(seekBar: SeekBar?) {
isDragging = false
}
})
}
private fun formatTimestamp(timestampInSeconds: Long): String {
val minutes = timestampInSeconds / 60
val seconds = timestampInSeconds % 60
return String.format("%02d:%02d", minutes, seconds)
}
seekBar 拖动开端会回调 onStartTrackingTouch,拖动结束会回调 onStopTrackingTouch。这里有个小细节,便是 isDragging,正在拖动的时候建议就不要去变换计时器中的播映方位了,避免引起一些不必要的问题。
然后在 onPlaybackStateChanged 中监听播映进展的改变即可
exoPlayer.addListener(object : Player.Listener {
override fun onPlaybackStateChanged(playbackState: Int) {
super.onPlaybackStateChanged(playbackState)
if (playbackState == Player.STATE_READY) {
//底部操控器处理
preBtn.visibility =
if (exoPlayer.hasPreviousMediaItem()) View.VISIBLE else View.GONE
nextBtn.visibility =
if (exoPlayer.hasNextMediaItem()) View.VISIBLE else View.GONE
setProgress()
}
}
override fun onIsPlayingChanged(isPlaying: Boolean) {
super.onIsPlayingChanged(isPlaying)
this@VideoPlayActivity.isPlaying = isPlaying
}
})
跟从生命周期
当咱们的宿主 Activity 处于后台时,需求暂停播映,处于前台时,持续播映,销毁时,释放资源,ExoPlayer 的状态需求跟从宿主的生命周期。
override fun onStart() {
super.onStart()
exoPlayer.play()
}
override fun onStop() {
super.onStop()
exoPlayer.pause()
}
override fun onDestroy() {
super.onDestroy()
exoPlayer.release()
}
整个视频播映的 Activity 如下:
class VideoPlayActivity : AppCompatActivity() {
private lateinit var exoPlayer: ExoPlayer
private lateinit var playBtn: ImageButton
private lateinit var playTime: TextView
private lateinit var seekBar: SeekBar
private lateinit var preBtn: ImageButton
private lateinit var nextBtn: ImageButton
private var progressJob: Job? = null
private var isPlaying = false
private var isDragging = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_video_play)
initView()
initListener()
}
private fun initView() {
val playView = findViewById<PlayerView>(R.id.playView)
val goBack = playView.findViewById<ImageView>(R.id.go_back)
preBtn = playView.findViewById(R.id.pre_btn)
nextBtn = playView.findViewById(R.id.next_btn)
playBtn = playView.findViewById(R.id.play_btn)
playTime = playView.findViewById(R.id.play_time)
seekBar = playView.findViewById(R.id.seekbar)
val mediaItem1 = MediaItem.fromUri(VIDEO_URL1)
val mediaItem2 = MediaItem.fromUri(VIDEO_URL2)
val mediaItem3 = MediaItem.fromUri(VIDEO_URL3)
exoPlayer = ExoPlayer.Builder(this).build()
playView.player = exoPlayer
exoPlayer.apply {
addMediaItem(mediaItem1)
addMediaItem(mediaItem2)
addMediaItem(mediaItem3)
prepare()
play()
}
preBtn.setOnClickListener {
if (exoPlayer.hasPreviousMediaItem()) {
exoPlayer.seekToPreviousMediaItem()
}
}
nextBtn.setOnClickListener {
if (exoPlayer.hasNextMediaItem()) {
exoPlayer.seekToNextMediaItem()
}
}
playBtn.setOnClickListener {
if (isPlaying) {
exoPlayer.pause()
playBtn.background =
ContextCompat.getDrawable(this@VideoPlayActivity, R.drawable.play)
} else {
exoPlayer.play()
playBtn.background =
ContextCompat.getDrawable(this@VideoPlayActivity, R.drawable.pause)
}
}
goBack.setOnClickListener {
finish()
}
}
private fun initListener() {
exoPlayer.addListener(object : Player.Listener {
override fun onPlaybackStateChanged(playbackState: Int) {
super.onPlaybackStateChanged(playbackState)
if (playbackState == Player.STATE_READY) {
//底部操控器处理
preBtn.visibility =
if (exoPlayer.hasPreviousMediaItem()) View.VISIBLE else View.GONE
nextBtn.visibility =
if (exoPlayer.hasNextMediaItem()) View.VISIBLE else View.GONE
setProgress()
}
}
override fun onIsPlayingChanged(isPlaying: Boolean) {
super.onIsPlayingChanged(isPlaying)
this@VideoPlayActivity.isPlaying = isPlaying
}
})
}
private fun formatTimestamp(timestampInSeconds: Long): String {
val minutes = timestampInSeconds / 60
val seconds = timestampInSeconds % 60
return String.format("%02d:%02d", minutes, seconds)
}
private fun setProgress() {
progressJob?.cancel()
progressJob = lifecycleScope.launch {
while (isActive) {
if (!isDragging) {
val currentPosition = exoPlayer.currentPosition
val currentTime = formatTimestamp(currentPosition / 1000)
val totalDuration = exoPlayer.duration
seekBar.max = totalDuration.toInt()
val totalTime = formatTimestamp(totalDuration / 1000)
playTime.text = "$currentTime/$totalTime"
seekBar.progress = currentPosition.toInt()
}
delay(1000)
}
}
seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
if (fromUser) { //如果是用户拖动的,则更新播映方位。
exoPlayer.seekTo(progress.toLong())
}
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {
isDragging = true
}
override fun onStopTrackingTouch(seekBar: SeekBar?) {
isDragging = false
}
})
}
override fun onStart() {
super.onStart()
exoPlayer.play()
}
override fun onStop() {
super.onStop()
exoPlayer.pause()
}
override fun onDestroy() {
super.onDestroy()
exoPlayer.release()
}
}
作用如下:

这里运用 Media3 ExoPlayer 做了一个简略的自界说视频播映界面,当然,实际开发中或许并不只是这些自界说需求,但万变不离其宗,只要把握了这些基操就行。