这篇文章接上文 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 打造音视频播映器(二)

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

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。