仿抖音全屏滑动播放功能

作者: 奔跑吧李博 | 来源:发表于2023-12-20 01:46 被阅读0次
    抖音,记录别人的美好生活
    效果展示
    技术点:

    1.ViewPager2做页面全屏加载,并做页面预加载
    2.使用Exoplayer播放器播放网络视频
    3.利用Exoplayer自带缓存文件缓存播放功能,进行预加载播放缓存

    网络视频资源:
    https://wave.video/embed/6581e9eedd95d26a606f6c87/6581e9eedd95d26a606f6c85.mp4
    
    https://wave.video/embed/6581ea54848d5e0842fbfa17/6581ea5b75b04937fe24d841.mp4
    
    https://wave.video/embed/6581ea6344d90b30892873d9/6581ea6a75b04937fe24d903.mp4
    
    https://wave.video/embed/6581ea7e00de0e0d40e8e8ce/6581ea86dd95d26a606f7110.mp4
    
    https://wave.video/embed/6581eae4848d5e0842fbffad/6581eaebdd95d26a606f74bc.mp4
    
    https://wave.video/embed/6581eaf844d90b3089287941/6581eafe00de0e0d40e8ed1d.mp4
    
    https://wave.video/embed/6581eb08dd95d26a606f7599/6581eb0e848d5e0842fc00c5.mp4
    
    https://wave.video/embed/6581eb1700de0e0d40e8edf6/6581eb1e75b04937fe24de74.mp4
    

    自定义播放器VideoPlayer类:
    功能包括设置exoplayer,监听页面生命周期调用自身生命周期

    class VideoPlayer @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
        FrameLayout(context, attrs), Iplayer , DefaultLifecycleObserver {
    
        private val trackSelector: TrackSelector = DefaultTrackSelector(context)
        private val mPlayer : SimpleExoPlayer by lazy {
                SimpleExoPlayer.Builder(context)
                    .setTrackSelector(trackSelector)
    //                .setLoadControl(loadControl) //构建内存缓冲
                    .build()
        }
    
        // 自定义 DefaultLoadControl 参数
        val MIN_BUFFER_MS = 5_000 // 最小缓冲时间,
        val MAX_BUFFER_MS = 7_000 // 最大缓冲时间
        val PLAYBACK_BUFFER_MS = 700 // 最小播放缓冲时间,只有缓冲到达这个时间后才是可播放状态
        val REBUFFER_MS = 1_000 // 当缓冲用完,再次缓冲的时间
        val loadControl = DefaultLoadControl.Builder()
            .setPrioritizeTimeOverSizeThresholds(true)//缓冲时时间优先级高于大小
            .setBufferDurationsMs(MIN_BUFFER_MS, MAX_BUFFER_MS, PLAYBACK_BUFFER_MS, REBUFFER_MS)
            .build()
    
        private var binding: ViewPlayviewBinding = ViewPlayviewBinding.inflate(LayoutInflater.from(context), this, true)
        companion object {
            const val MAX_CACHE_BYTE: Long = 1024*1024*200  //200MB
        }
    
        init {
            initPlayer()
        }
    
        override fun onResume(owner: LifecycleOwner) {
            super.onResume(owner)
    
            //返回时,推荐页面可见,则继续播放视频
            if (MainActivity.curMainPage == 0 && MainFragment.Companion.curPage == 1) {
                play()
            }
        }
    
        override fun onPause(owner: LifecycleOwner) {
            super.onPause(owner)
    
            pause()
        }
    
        override fun onStop(owner: LifecycleOwner) {
            super.onStop(owner)
    
            stop()
        }
    
        override fun onDestroy(owner: LifecycleOwner) {
            super.onDestroy(owner)
    
            release()
        }
    
        private fun initPlayer() {
            binding.playerview.player = mPlayer
            binding.playerview.useController = false
            mPlayer.playWhenReady = true
            mPlayer.repeatMode = Player.REPEAT_MODE_ALL
        }
    
        /**
         * 使用本地缓存播放
         */
        fun playVideo(mediaSource: BaseMediaSource) {
            mPlayer.setMediaSource(mediaSource)
            mPlayer.prepare()
            mPlayer.play()
        }
    
        override fun getplayer(): SimpleExoPlayer {
            return mPlayer
        }
    
        override fun pause() {
            mPlayer.pause()
        }
    
        override fun play() {
            mPlayer.play()
        }
    
        override fun stop() {
            mPlayer.stop()
        }
    
        override fun release() {
            mPlayer?.let {
                it.release()
            }
        }
    
    
        override fun isPlaying(): Boolean = mPlayer.isPlaying
    }
    

    列表播放页面代码,功能包括ViewPager2设置预加载item,监听item切换,播放当前item视频资源,并做到一个播放器在列表共用。

    class RecommendFragment : BaseBindingFragment<FragmentRecommendBinding>({FragmentRecommendBinding.inflate(it)}) {
        private var adapter: VideoAdapter?= null
    
        /** 当前播放视频位置  */
        private var curPlayPos = -1
        private lateinit var videoView: VideoPlayer
    
        private var ivCurCover: ImageView? = null
        private var subscribe: Subscription?= null
    
        override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            super.onViewCreated(view, savedInstanceState)
    
            DataCreate()
            initRecyclerView()
            initVideoPlayer()
            setViewPagerLayoutManager()
            setRefreshEvent()
            observeEvent()
        }
    
        private fun initRecyclerView() {
            adapter  = VideoAdapter(requireContext(), binding.recyclerView.getChildAt(0) as RecyclerView)
            binding.recyclerView.adapter = adapter
            adapter?.appendList(DataCreate.datas)
        }
    
        private fun initVideoPlayer() {
            var params = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
            videoView = VideoPlayer(requireActivity())
            videoView.layoutParams = params
            lifecycle.addObserver(videoView)
        }
    
        override fun onDestroy() {
            super.onDestroy()
            subscribe?.unsubscribe()
            binding.recyclerView.unregisterOnPageChangeCallback(pageChangeCallback)
        }
    
        private fun setViewPagerLayoutManager() {
            with(binding.recyclerView) {
                orientation = ViewPager2.ORIENTATION_VERTICAL
                offscreenPageLimit = 1
                registerOnPageChangeCallback(pageChangeCallback)
            }
        }
    
        private val pageChangeCallback = object: OnPageChangeCallback() {
            override fun onPageSelected(position: Int) {
                playCurVideo(position)
            }
        }
    
        private fun setRefreshEvent() {
            binding.refreshLayout.setColorSchemeResources(R.color.color_link)
            binding.refreshLayout.setOnRefreshListener {
                object : CountDownTimer(1000, 1000) {
                    override fun onTick(millisUntilFinished: Long) {}
                    override fun onFinish() {
                        binding.refreshLayout!!.isRefreshing = false
                    }
                }.start()
            }
        }
    
        private fun playCurVideo(position: Int) {
            if (position == curPlayPos) {
                return
            }
            val itemView = adapter!!.getRootViewAt(position)
            val rootView = itemView!!.findViewById<ViewGroup>(R.id.rl_container)
            val likeView: LikeView = rootView.findViewById(R.id.likeview)
            val controllerView: ControllerView = rootView.findViewById(R.id.controller)
            val ivPlay = rootView.findViewById<ImageView>(R.id.iv_play)
            val ivCover = rootView.findViewById<ImageView>(R.id.iv_cover)
    
            //播放暂停事件
            likeView.setOnPlayPauseListener(object: LikeView.OnPlayPauseListener {
                override fun onPlayOrPause() {
                    if (videoView!!.isPlaying()) {
                        videoView?.pause()
                        ivPlay.visibility = View.VISIBLE
                    } else {
                        videoView?.play()
                        ivPlay.visibility = View.GONE
                    }
                }
    
            })
    
            //评论点赞事件
            likeShareEvent(controllerView)
    
            //切换播放视频的作者主页数据
            RxBus.getDefault().post(CurUserBean(DataCreate.datas[position]?.userBean!!))
            curPlayPos = position
    
            //切换播放器位置
            dettachParentView(rootView)
            autoPlayVideo(curPlayPos, ivCover)
        }
    
        /**
         * 移除videoview父view
         */
        private fun dettachParentView(rootView: ViewGroup) {
            //1.添加videoView到当前需要播放的item中,添加进item之前,保证videoView没有父view
            videoView?.parent?.let {
                (it as ViewGroup).removeView(videoView)
            }
    
            rootView.addView(videoView, 0)
        }
    
        /**
         * 自动播放视频
         */
        private fun autoPlayVideo(position: Int, ivCover: ImageView) {
            videoView.playVideo(adapter!!.getDatas()[position].mediaSource!!)
    
            videoView.getplayer()?.addListener(object: Player.Listener {
                override fun onPlaybackStateChanged(state: Int) {
                    // 播放状态发生变化时的回调
                    // 播放状态包括:Player.STATE_IDLE、Player.STATE_BUFFERING、Player.STATE_READY、Player.STATE_ENDED
                    if (state == Player.STATE_READY) {
    
                    }
                }
    
                fun onPlayerError(error: ExoPlaybackException?) {
                    // 播放发生错误时的回调
                }
    
                override fun onIsPlayingChanged(isPlaying: Boolean) {
                    // 播放状态变为播放或暂停时的回调
                }
    
                override fun onRenderedFirstFrame() {
                    //第一帧已渲染,隐藏封面
                    ivCover.visibility = View.GONE
                    ivCurCover = ivCover
                }
            })
        }
    }
    

    VideoAdapter代码,功能包括构造SimpleCache缓存,构建CacheDataSource本地缓存文件作为提供播放资源。

    class VideoAdapter(val context: Context, val recyclerView: RecyclerView): BaseAdapter<VideoViewHolder, VideoBean>() {
    
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VideoViewHolder {
            return VideoViewHolder(ItemVideoBinding.inflate(LayoutInflater.from(parent.context), parent, false))
        }
    
        override fun onBindViewHolder(holder: VideoViewHolder, position: Int) {
            ...
            //利用预加item,提前加载缓存资源
            mList[position].mediaSource = buildMediaSource(mList[position].videoRes)
        }
    
        /**
         * 构建一个共用缓存文件
         */
        val cache: SimpleCache by lazy {
            //构建缓存文件
            val cacheFile = context.cacheDir.resolve("tiktok_cache_file")
            //构建simpleCache缓存实例
            SimpleCache(cacheFile, LeastRecentlyUsedCacheEvictor(VideoPlayer.MAX_CACHE_BYTE), StandaloneDatabaseProvider(context))
        }
    
        /**
         * 构建当前url视频的缓存
         */
        private fun buildMediaSource(url: String): ProgressiveMediaSource {
            //开启缓存文件
            val mediaItem = MediaItem.fromUri(url)
            //构建 DataSourceFactory
            val dataSourceFactory = CacheDataSource.Factory().setCache(cache).setUpstreamDataSourceFactory(DefaultDataSource.Factory(context))
            //构建 MediaSource
            return ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItem)
        }
    }
    

    参考:https://blog.csdn.net/weixin_61845324/article/details/130558439

    详情见Github代码地址

    相关文章

      网友评论

        本文标题:仿抖音全屏滑动播放功能

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