效果展示
技术点:
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
网友评论