起因
在某些业务场景下,可能会有刷短视频的需求,方便用户看到各式各样的效果,方便触达到用户,好处都不多说,第一个想到短视频这种产品形态的人简直就是天才
在我参与开发的某个项目的发展阶段,当然也碰到了要做这种竖屏短视频的需求,项目是做比较火的SD图片处理的,当然也涉及到一些视频生成的功能,有不同的视频模板提供个用户观看并选择.
在该场景中,需求是要求做有限刷新的视频流,当然其实要做无限刷新的也挺简单 两种方法,一种是intMaxValue,一种是54123
这两种方法孰优孰劣本文就不过多赘述了,本文是阐述如何进行视频播放的相关实现的
实现方案阐述
整体架构采用ViewPager2嵌套item实现,没有采用ViewPager2嵌套Fragment,item封装了不同的状态,断网,加载,显示视频等页面状态
在item中采用了texture来进行播放的相关显示,然后在Activity中采用ExoPlayer来进行视频的解码播放等职能
在一些头部APP中,会采用多个解码器来进行解码,有一个解码器的Pool吗,这样能使得播放与更加流畅,但是我没使用这个方案,主要是开发时间比较短导致的,而且也没有预载的需求,索性就不考虑预载机制了
ExoPlayer配置
由于新版的Exoplayer更名为Media3了,使用全新的包名来导入依赖
implementation 'androidx.media3:media3-exoplayer:1.1.0'
implementation 'androidx.media3:media3-ui:1.1.0'
因为media3仍在项目初期,有一些bug,在写这篇文章的时候,进行以下代码的编写会报错调用不稳定的Api,添加注释 @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
即可
ExoPlayer.Builder(this)
.setLoadControl(VideoCacheSingleton.loadControl).build().also { exoPlayer ->
exoPlayer.volume = 0F
exoPlayer.repeatMode = ExoPlayer.REPEAT_MODE_ONE
}
在这里主要是配置了加载器部分,也就是LoadControl的部分,
val minBufferMs = 10000 // 10 seconds
val maxBufferMs = 30000 // 30 seconds
val loadControl = DefaultLoadControl.Builder()
.setBufferDurationsMs(minBufferMs, maxBufferMs, DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS, DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS)
.build()
这里主要是用于控制播放器的缓存秒数,因为我配置为在内存中最大缓存30s,最小缓存10s,为什么要这样做呢,因为下方我们配置媒体源的时候需要使用渐进式的媒体源,这种做法的好处是方便缓存到本地,坏处是回吧所有的视频缓存进内存和本地,内存是很宝贵的资源,如果不加以限制缓存时间,内存触顶率绝对会相当的高,没开几个页面就会崩溃到怀疑人生
下面是媒体源的缓存配置
val MAX_CACHE_BYTE = 512 * 1024 * 1024L // 给512m的本地缓存
val cacheFile = File(PathUtils.getExternalAppCachePath()).resolve("video_cache")
dataSourceFactory = SimpleCache(cacheFile, LeastRecentlyUsedCacheEvictor(MAX_CACHE_BYTE), StandaloneDatabaseProvider(this))
下面是生成媒体源的配置
val mediaItem = MediaItem.fromUri(videoUrl)
val dataSourceFactory = CacheDataSource.Factory().setCache(VideoCacheSingleton.pageCache)
.setUpstreamDataSourceFactory(DefaultDataSource.Factory(this))
ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItem)
之后切换视频的时候动态生成媒体源并且设置进去
exoplayer.setMediaSource(mediaSource)
exoplayer.setVideoTextureView(texture)
这些是基本的渐进式媒体源和LoadContel的基本配置
下面将用一个Demo来进行说明
val bitmap = BitmapFactory.decodeStream(inputStream) val time = System.currentTimeMillis() val rect = getHasPixelRectSize(bitmap) Log.i(“TAG”, “onCreate: 耗时${System.currentTimeMillis() - time}”) Log.i(“TAG”, “onCreate: ${Rect(0, 0, bitmap.width, bitmap.height)}”) Log.i(“TAG”, “onCreate: $rect”) val ret: Bitmap = Bitmap.createBitmap(bitmap, rect.left, rect.top, rect.width(), rect.height()) binding.sampleImageView.setImageBitmap(ret)kotlin
Demo的内存消耗
这个波形我还是比较满意的,在界面退出后内存都及时释放掉了,还有一部分内存没有释放是Native层的内存,是ExoPlayer管理的弱引用
package org.phcbest.viewpager2viewtest
import android.os.Bundle
import android.view.TextureView
import androidx.appcompat.app.AppCompatActivity
import androidx.media3.common.MediaItem
import androidx.media3.database.StandaloneDatabaseProvider
import androidx.media3.datasource.DefaultDataSource
import androidx.media3.datasource.cache.CacheDataSource
import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor
import androidx.media3.datasource.cache.SimpleCache
import androidx.media3.exoplayer.DefaultLoadControl
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.ProgressiveMediaSource
import androidx.viewpager2.widget.ViewPager2
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
import com.blankj.utilcode.util.PathUtils
import org.phcbest.viewpager2viewtest.exo.ExoplayerTextureAdapter
import java.io.File
class ViewPagerActivity : AppCompatActivity() {
private val mVpView: ViewPager2 by lazy { findViewById<ViewPager2>(R.id.vp_view) }
private var exoplayer: ExoPlayer? = null
private var simpleCache: SimpleCache? = null
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_view_pager)
val minBufferMs = 10000 // 10 seconds
val maxBufferMs = 30000 // 30 seconds
val loadControl = DefaultLoadControl.Builder()
.setBufferDurationsMs(
minBufferMs,
maxBufferMs,
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS,
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS
)
.build()
exoplayer = ExoPlayer.Builder(this)
.setLoadControl(loadControl).build().also { exoPlayer ->
exoPlayer.volume = 0F
exoPlayer.repeatMode = ExoPlayer.REPEAT_MODE_ONE
}
val MAX_CACHE_BYTE = 512 * 1024 * 1024L // 给512m的本地缓存
val cacheFile = File(PathUtils.getExternalAppCachePath()).resolve("video_cache")
simpleCache = SimpleCache(
cacheFile,
LeastRecentlyUsedCacheEvictor(MAX_CACHE_BYTE),
StandaloneDatabaseProvider(this)
)
val exoplayerTextureAdapter = ExoplayerTextureAdapter()
mVpView.adapter = exoplayerTextureAdapter
exoplayerTextureAdapter.setNewInstance(VideoPath.pathList.toMutableList())
mVpView.registerOnPageChangeCallback(object : OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
val mediaItem = MediaItem.fromUri(VideoPath.pathList[position])
val dataSourceFactory = CacheDataSource.Factory().setCache(simpleCache!!)
.setUpstreamDataSourceFactory(DefaultDataSource.Factory(this@ViewPagerActivity))
val mediaSource =
ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItem)
exoplayer!!.setMediaSource(mediaSource)
val texture =
exoplayerTextureAdapter.getViewByPosition(position, R.id.texture) as TextureView
exoplayer!!.setVideoTextureView(texture)
exoplayer!!.prepare()
exoplayer!!.play()
}
})
}
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
override fun onDestroy() {
super.onDestroy()
exoplayer?.release()
simpleCache?.release()
exoplayer = null
}
}