Android开发经验谈Android开发Android技术知识

仿抖音全屏滑动播放功能

2023-12-20  本文已影响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代码地址
上一篇 下一篇

猜你喜欢

热点阅读