美文网首页
JetPack知识点实战系列十:ExoPlayer进行视频播放的

JetPack知识点实战系列十:ExoPlayer进行视频播放的

作者: chonglingliu | 来源:发表于2020-10-19 23:34 被阅读0次

    本节教程我们将来介绍下ExoPlayer的视频播放功能。

    效果

    我们在本节将主要介绍以下知识点:

    1. ExoPlayer高级自定义的实现
    2. 视频的全屏播放和退出全屏播放
    3. ExoPlayer在RecyclerView中的复用

    ExoPlayer介绍

    MediaPlayerExoPlayer是Google官方支持的两种播放器,但是ExoPlayerMediaPlayer多了支持基于 HTTP 的动态自适应流 (DASH)、SmoothStreaming 和通用加密等功能。

    并且重要的是它独立于Android代码框架,以一个开源代码库的形式存在,所以在自定义上更有优势。

    ExoPlayer简单的使用方法

    • 引入依赖库
    implementation 'com.google.android.exoplayer:exoplayer:2.12.0'
    
    • 布局中引入PlayerView

    播放视频我们需要使用PlayerView,我们简单来看下PlayerView的源码,其继承于FrameLayout,其中有三个重要的属性,

    public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider {
        @Nullable private final View surfaceView;
        @Nullable private final PlayerControlView controller;
        private Player player;
    }
    
    1. surfaceView是呈现视频的View,可以是TextureViewSurfaceView, 默认是SurfaceView
    2. controller是播放控制的View,上面提供一些控件可以控制视频的播放,暂停,显示当前进度等。默认是PlayerControlView
    3. player 是视频的播放器,在构造函数初始化的时候没有赋值,需要单独设置。

    总结:PlayerView通过player播放视频显示在surfaceView上,用户可以通过提供的controller进行播放的控制。

    介绍了基本的知识点后,我们在布局文件中引入PlayerView

    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout 
        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=".DefaultViewActivity">
    
        <com.google.android.exoplayer2.ui.PlayerView
            android:id="@+id/video_player"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:show_buffering="always"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"/>
    
    </androidx.constraintlayout.widget.ConstraintLayout>
    
    • 设置播放器
    val player: SimpleExoPlayer = SimpleExoPlayer.Builder(this@MainActivity).build().also { it.playWhenReady = true }
    video_player.player = player
    

    我们前面提到PlayerView的两个属性在构造函数调用时赋值了,但是player没有,需要主动设置。这里我们设置成SimpleExoPlayer对象。

    SimpleExoPlayer是库中提供的播放器,可以直接使用。

    • 设置播放源
    // play item
    val uri = Uri.parse("https://storage.googleapis.com/exoplayer-test-media-0/BigBuckBunny_320x180.mp4")
    val dataSourceFactory = DefaultHttpDataSourceFactory()
    val videoSource = ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri)
    // prepare
    player.prepare(videoSource)
    
    • 监听播放器的状态

    我们可以监听播放器的状态,代码如下:

    player.addListener(object: Player.EventListener {
        override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
        Log.d("JJMusic","playWhenReady: $playWhenReady playbackState: $playbackState")
        when (playbackState) {
            Player.STATE_BUFFERING ->
                Log.d("JJMusic","加载中")
            Player.STATE_READY ->
                Log.d("JJMusic","准备完毕")
            Player.STATE_ENDED ->
                Log.d("JJMusic","播放完成")
            }
        }
    
        override fun onPlayerError(error: ExoPlaybackException) {
            Log.e("JJMusic","ExoPlaybackException: $error")
        }
    })
    

    最后得到的效果如下所示:

    默认控制器

    ExoPlayer简单自定义

    我们目前使用的是默认的播放控制布局文件,我们可以修改播放的布局文件达到自定义效果。

    • 自定义播放控制的布局文件

    假设我们把布局文件设计如下所示:

    <!-- layout_video_simple.xml -->
    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout 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:id="@+id/constraint"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <ImageView
            android:id="@+id/exo_play"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:contentDescription="@null"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:srcCompat="@mipmap/exo_btn_play" />
    
        <ImageView
            android:id="@+id/exo_pause"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:contentDescription="@null"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:srcCompat="@mipmap/exo_btn_pause" />
    
        <TextView
            android:id="@+id/exo_position"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="6dp"
            android:layout_marginBottom="12dp"
            android:contentDescription="@null"
            android:text="1"
            android:textColor="@color/colorPrimary"
            android:textSize="12sp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent" />
    
        <TextView
            android:id="@+id/splash_tv"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="1dp"
            android:layout_marginBottom="12dp"
            android:contentDescription="@null"
            android:text="/"
            android:textColor="@color/colorPrimary"
            android:textSize="12sp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toEndOf="@+id/exo_position"
            tools:text="/" />
    
        <TextView
            android:id="@+id/exo_duration"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="1dp"
            android:layout_marginBottom="12dp"
            android:contentDescription="@null"
            android:text="1"
            android:textColor="@color/colorPrimary"
            android:textSize="12sp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toEndOf="@+id/splash_tv" />
    
        <com.google.android.exoplayer2.ui.DefaultTimeBar
            android:id="@+id/exo_progress"
            android:layout_width="0dp"
            android:layout_height="15dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:bar_height="2dp"
            app:unplayed_color="@color/exo_gray_ripple"
            app:played_color="@color/colorAccent"
            app:scrubber_color="@color/colorAccent"
            app:buffered_color="@color/colorPrimary"
            />
    
    </androidx.constraintlayout.widget.ConstraintLayout>
    
    1. idexo_play的按钮和idexo_pause的按钮在屏幕正中间位置
    2. idexo_position的文本和idexo_duration的文本在左下角
    3. idexo_progress的进度条在最底部。进度条的类是DefaultTimeBar,可以设置一些属性。譬如上面的bar_height(进度条的高度),unplayed_color(未缓冲部分的颜色),played_color(已播放部分的颜色)和buffered_color(已缓冲完部分的颜色)等等。

    注意:这些idPlayerControlView源代码中能找到的id,否则是没有效果的。

    • 修改PlayerView布局文件
    <com.google.android.exoplayer2.ui.PlayerView
        ...
        app:controller_layout_id="@layout/layout_video_simple"
        />
    

    其他的和前面的类似,只是加了个属性controller_layout_id,值为我们刚才设计的布局文件layout_video_simple

    简单自定义得到的效果如下所示:

    简单自定义

    ExoPlayer高级自定义

    简单的自定义我们只是更改了PlayerControlView的布局文件,复用了其中的id,能修改的很有限,没有涉及到源代码的修改。

    高级自定义就需要修改源代码了。其实就是修改PlayerViewPlayerControlView,甚至是TimeBar的源代码。

    接下来我们就用高级自定义来实现下网易云音乐的全屏播放功能,需要的效果如下:

    网易云音乐效果
    • 修改PlayerControlView

    新建一个JJPlayerControlView类,然后将PlayerControlView所有源代码拷贝在这个类中。

    public class JJPlayerControlView extends FrameLayout {
        // PlayerControlView内容
    }
    

    接下来在JJPlayerControlView中加入一个全屏按钮属性。

    public class JJPlayerControlView extends FrameLayout {
        // 全屏按钮
        private final ImageButton maxButton;
        // PlayerControlView内容
        public JJPlayerControlView(
                Context context,
                @Nullable AttributeSet attrs,
                int defStyleAttr,
                @Nullable AttributeSet playbackAttrs) {
            ...
            maxButton = findViewById(R.id.exo_max_btn);
            if (maxButton != null) {
                maxButton.setOnClickListener(componentListener);
            }
            ...
        }
    }
    
    • 修改PlayerView

    新建一个JJPlayerView类,然后将PlayerView所有源代码拷贝在这个类中。

    public class JJPlayerView extends FrameLayout implements AdsLoader.AdViewProvider {
    // PlayerView的内容
    }
    

    JJPlayerViewcontroller指定为JJPlayerControlView,即:

    public class JJPlayerView extends FrameLayout implements AdsLoader.AdViewProvider {
        @Nullable private final JJPlayerControlView controller;
        // PlayerView的其他内容
    }
    
    • 修改TimeBar

    如果需要修改进度条,新建一个JJTimeBar类,然后将DefaultTimeBar所有源代码拷贝在这个类中。

    public class JJTimeBar extends View implements TimeBar {
        ...
    }
    

    当然修改将JJPlayerControlView中的timeBar改为JJTimeBar类。

    public class JJPlayerControlView extends FrameLayout {
        // 全屏按钮
        private final ImageButton maxButton;
        // 自定义进度条
        @Nullable private JJTimeBar timeBar;
        // PlayerControlView内容
        public JJPlayerControlView(
                Context context,
                @Nullable AttributeSet attrs,
                int defStyleAttr,
                @Nullable AttributeSet playbackAttrs) {
            ...
            maxButton = findViewById(R.id.exo_max_btn);
            if (maxButton != null) {
                maxButton.setOnClickListener(componentListener);
            }
            ...
        }
    }
    
    • 修改JJPlayerControlView布局文件
    自定义的布局文件

    layout_video_recyclerview.xml相对前面,我们多添加了一个idexo_max_btn的按钮。

    为了看的更加明显,我把其他的按钮或者文本的id都改了,不再使用默认的id,这时候为了找到对应的控件,就需要修改对应的源代码了。譬如我把播放按钮的id改为了exo_play_btn

    public class JJPlayerControlView extends FrameLayout {
        // 代码修改
        playButton = findViewById(R.id.exo_play_btn);
        if (playButton != null) {
            playButton.setOnClickListener(componentListener);
        }
    }
    
    • JJPlayerView布局文件

    JJPlayerView使用JJPlayerControlView自定义的布局文件

    <com.johnny.jjmusic.exoplayer.JJPlayerView
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:show_buffering="always"
        app:controller_layout_id="@layout/layout_video_recyclerview"
        >
    
    </com.johnny.jjmusic.exoplayer.JJPlayerView>
    
    • 全屏和退出全屏的实现逻辑

    我们先来看一张图就能很清晰的了解全屏和退出全屏的逻辑了:

    全屏逻辑

    全屏的时候JJPlayerView放在ActivityR.id.content上,隐藏ActionBar,切换成横屏显示,退出全屏的时候就重新放在RecyclerViewItemView上,显示ActionBar,切换成竖屏显示。

    所以最后很简单,只要处理maxButton点击事件时实现这个功能就可以了。

    进入全屏播放

    fun enterFullScreen() {
        // 横竖屏状态判断
        if (viewModel.playMode == VideoPlayMode.MODE_FULL_SCREEN) return
        // 隐藏ActionBar
        playerView.context.hideActionBar()
        // 旋转屏幕
        playerView.context.activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
        // 将JJPlayerView从RecyclerView移除,加入Activity的R.id.content下
        playerView.context.activity?.let {
            val contentView = it.findViewById<ViewGroup>(android.R.id.content)
            // remove
            removePlayerView()
            viewModel.isVideoViewAdded = true
    
            // add
            val params = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
            contentView.addView(playerView, params)
    
            val frameParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
            playerView.controller?.timeBarContainer?.addView(timeBar, frameParams)
    
            viewModel.playMode = VideoPlayMode.MODE_FULL_SCREEN
        }
    
    }
    

    退出全屏播放

    /* 退出全屏 */
        fun exitFullScreen() {
        // 横竖屏状态判断
        if (viewModel.playMode == VideoPlayMode.MODE_NORMAL) return
        // 显示ActionBar
        playerView.context.showActionBar()
        // 旋转屏幕
        playerView.context.activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
        // 将JJPlayerView从Activity的R.id.content移除,加入RecyclerView的ItemView下
        playerView.context.activity?.let {
            // remove
            val contentView = it.findViewById<ViewGroup>(android.R.id.content)
            contentView.removeView(playerView)
            playerView.controller?.timeBarContainer?.removeView(timeBar)
    
            // add
            viewModel.viewModelScope.launch {
                delay(100)
                addPlayerView()
            }
    
            viewModel.playMode = VideoPlayMode.MODE_NORMAL
        }
    }
    

    上面代码中涉及到的几个扩展方法,也一同贴出来:

    //----------Activity----------
    val Context.activity: Activity?
        get() {
            return when (this) {
                is Activity -> {
                    this
                }
                is ContextWrapper -> {
                    this.baseContext.activity
                }
                else -> {
                    null
                }
            }
        }
    
    val Context.appCompActivity: AppCompatActivity?
        get() {
            return when (this) {
                is AppCompatActivity -> {
                    this
                }
                is ContextThemeWrapper -> {
                    this.baseContext.appCompActivity
                }
                else -> {
                    null
                }
            }
        }
    
    //---------- ActionBar ----------
    @SuppressLint("RestrictedApi")
    fun Context.showActionBar() {
        this.appCompActivity?.supportActionBar?.let {
            it.setShowHideAnimationEnabled(false)
            it.show()
        }
        this.activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
    }
    
    @SuppressLint("RestrictedApi")
    fun Context.hideActionBar() {
        this.appCompActivity?.supportActionBar?.let {
            it.setShowHideAnimationEnabled(false)
            it.hide()
        }
        this.activity?.window?.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN)
    }
    

    至此ExoPlayer的高级自定义就到此为止了。

    由于可以修改源码,所以进行高度自定义就变得可实现了。当然是在熟悉源码的前提下进行修改。

    ExoPlayer在RecyclerView中的复用

    复用

    上面的实现效果中,我们点击RecyclerView不同的Item,都能播放视频,如果每个ItemView都有一个PlayerView那是非常不合适的。对PlayerView是一个非常合适的解决方案。

    其实这个解决方案和全屏的方案也非常相似,就是将PlayerView在不同的Item中移除和加入。然后播放新的视频。

    其中有一些细节需要处理,譬如播放的进度需要记录下来,下次再点击的时候从上次停止的地方进行播放。还譬如需要监听RecyclerView.OnChildAttachStateChangeListener,当执行onChildViewDetachedFromWindow时候,如果在播放需要将播放器停止。等等

    有了思路,解决起来也就很简单了。这里不再贴代码了。

    相关文章

      网友评论

          本文标题:JetPack知识点实战系列十:ExoPlayer进行视频播放的

          本文链接:https://www.haomeiwen.com/subject/eehrmktx.html