用ExoPlayer在Android中预装和缓冲视频
每个Android开发者都需要了解如何预装视频,这样用户就不必在视频加载时等待,就像YouTube应用那样。
一个视频可以在播放之前被加载和缓存。这很有趣,因为我们把等待时间降到最低。
前提条件
要跟上本教程,读者应该具备以下条件。
- 具有创建Android应用程序的良好知识。
- 对Kotlin编程语言有良好的了解。
- 对使用工作管理器、ViewBinding和Kotlin Coroutines有基本了解。
什么是视频预加载和缓冲?
缓冲发生在视频流中,当软件在开始播放视频之前下载特定数量的数据。当文件的下一节在后台下载时,你可以将已经预载并存储在缓冲区的数据进行流式处理。
ExoPlayer是一个由谷歌开发的库。它为Android的MediaPlayer API提供了一个替代方案,用于在本地和互联网上播放音频和视频。ExoPlayer支持Android的MediaPlayer API目前不支持的功能。
开始使用
在本教程中,我们将创建一个简单的应用程序,从互联网上播放视频,并在用户观看之前将其缓存起来。
第1步 - 创建一个Android项目
启动你的Android Studio并创建一个空项目。

第2步 - 设置项目
在这一步,我们将添加必要的依赖项,以便继续进行。
def exoplayer_version = "2.16.1"
def work_version = "2.5.0"
implementation "com.google.android.exoplayer:exoplayer:$exoplayer_version"
implementation "com.google.android.exoplayer:exoplayer-ui:$exoplayer_version"
implementation "androidx.work:work-runtime-ktx:$work_version"
记住要启用
viewBinding。在你的Manifest文件中,添加互联网权限,因为我们将从互联网上传输视频。
第3步 - 创建一个用户界面
在activity_main.xml ,设计一个简单的布局,包含ExoplayerPlayerView 。
<com.google.android.exoplayer2.ui.PlayerView
android:id="@+id/player_view"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:show_buffering="when_playing"
app:show_shuffle_button="true" />

第4步 - 创建一个基础应用类
在这一步,我们将创建一个继承自Application 的基类。
class VideoApp : Application() {
companion object{
lateinit var cache: SimpleCache
}
private val cacheSize: Long = 90 * 1024 * 1024
private lateinit var cacheEvictor: LeastRecentlyUsedCacheEvictor
private lateinit var exoplayerDatabaseProvider: ExoDatabaseProvider
override fun onCreate() {
super.onCreate()
cacheEvictor = LeastRecentlyUsedCacheEvictor(cacheSize)
exoplayerDatabaseProvider = ExoDatabaseProvider(this)
cache = SimpleCache(cacheDir, cacheEvictor, exoplayerDatabaseProvider)
}
}
解释
在这个类中,我们已经定义了我们的应用程序将使用的缓存大小。我们还定义了用于清除缓存的缓存驱逐器,一个ExoDatabaseProvider ,并将它们传递给我们的cache 实例。
第5步 - 创建一个视频预加载工作者
在这里,我们将从Workmanager库中创建一个Worker 类,它将在后台进行预加载和预缓存工作。
class VideoPreloadWorker(private val context: Context, workerParameters: WorkerParameters) : Worker(context, workerParameters) {
private var videoCachingJob: Job? = null
private lateinit var mHttpDataSourceFactory: HttpDataSource.Factory
private lateinit var mDefaultDataSourceFactory: DefaultDataSourceFactory
private lateinit var mCacheDataSource: CacheDataSource
private val cache: SimpleCache = VideoApp.cache
...
}
在这个类里面,定义一个同伴对象,它将包含一个接收参数的方法,从那里将Worker类实例化。
companion object {
const val VIDEO_URL = "video_url"
fun buildWorkRequest(yourParameter: String): OneTimeWorkRequest {
val data = Data.Builder().putString(VIDEO_URL, yourParameter).build()
return OneTimeWorkRequestBuilder<VideoPreloadWorker>().apply { setInputData(data) }
.build()
}
}
对于视频缓存逻辑,让我们定义两个方法来完成这项工作。
private fun preCacheVideo(videoUrl: String?) {
val videoUri = Uri.parse(videoUrl)
val dataSpec = DataSpec(videoUri)
val progressListener = CacheWriter.ProgressListener { requestLength, bytesCached, _ ->
val downloadPercentage: Double = (bytesCached * 100.0 / requestLength)
// Do Something
}
videoCachingJob = GlobalScope.launch(Dispatchers.IO) {
cacheVideo(dataSpec, progressListener)
preCacheVideo(videoUrl)
}
}
private fun cacheVideo(mDataSpec: DataSpec, mProgressListener: CacheWriter.ProgressListener) {
runCatching {
CacheWriter(mCacheDataSource,mDataSpec,null,mProgressListener,).cache()
}.onFailure {
it.printStackTrace()
}
}
解释
第一个函数preCacheVideo ,接收一个视频URL并将其传入一个DataSpec ,该函数定义了资源中的一个数据区域。另外,我们还定义了一个CacheWriter.ProgressListener ,在缓存操作中接收进度更新。
然后在该函数中,我们做了视频缓存工作,该工作在一个CoroutineGlobalScope 内运行,并调用缓存方法。第二个函数cacheVideo ,在缓存相关的实用方法CacheWriter 的帮助下,做视频的缓存工作。
在定义了这两个方法后,在doWork 方法中,我们进行初始化并调用我们的preCacheVideo 函数。
override fun doWork(): Result {
try {
val videoUrl: String? = inputData.getString(VIDEO_URL)
mHttpDataSourceFactory = DefaultHttpDataSource.Factory()
.setAllowCrossProtocolRedirects(true)
mDefaultDataSourceFactory = DefaultDataSourceFactory(context, mHttpDataSourceFactory)
mCacheDataSource = CacheDataSource.Factory()
.setCache(cache)
.setUpstreamDataSourceFactory(mHttpDataSourceFactory)
.createDataSource()
preCacheVideo(videoUrl)
return Result.success()
} catch (e: Exception) {
return Result.failure()
}
}
第6步 - 缓存视频
当预先缓存一个视频时,最好在不同的Activity 或Fragment ,这样当用户导航到实际目的地时,他/她会发现视频已经准备好。就像在youtube上,视频被显示在一个列表中,当用户选择一个特定的视频时,就是他们被导航到一个不同的屏幕,视频在那里播放。
在某些情况下,开发者更喜欢在RecyclerView中显示视频的缩略图,然后,当用户选择一个特定的视频时,该视频会在不同的屏幕上播放。在我们的案例中,我们将定义一个活动来进行预加载,然后当用户点击播放视频Button ,他/她会被导航到另一个播放视频的活动中。
创建一个空的活动(我的将被称为FirstActivity )。
在其布局中,创建一个单一的按钮。

在一个更复杂的情况下,你可以有一个
RecyclerView。
FirstActivity逻辑
首先,让我们定义一个变量,用来保存我们要缓存的视频的URL。
private val videoUrl = "VIDEO_URL"
然后定义一个方法来安排我们的预加载工作。
private fun schedulePreloadWork(videoUrl: String) {
val workManager = WorkManager.getInstance(applicationContext)
val videoPreloadWorker = VideoPreloadWorker.buildWorkRequest(videoUrl)
workManager.enqueueUniqueWork(
"VideoPreloadWorker",
ExistingWorkPolicy.KEEP,
videoPreloadWorker
)
}
解释
schedulePreloadWork 函数进行实例化WorkManager ,并传递要缓存的视频的URL。然后我们对工作进行排队,并添加一个ExistingWorkPolicy.KEEP 策略。如果现有的待处理(未完成)的工作具有相同的唯一名称,则不做任何事情。
在我们FirstActivity的onCreate 方法中,我们将调用schedulePreloadWork 方法并传递videoUrl 。同时,为按钮设置一个OnClickListener ,这样我们就可以导航到MainActivity ,携带将要播放的视频的URL。
第7步 - 播放视频
对于MainActivity,让我们定义与我们在Worker类中定义的相同的变量。
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var mHttpDataSourceFactory: HttpDataSource.Factory
private lateinit var mDefaultDataSourceFactory: DefaultDataSourceFactory
private lateinit var mCacheDataSourceFactory: DataSource.Factory
private lateinit var exoPlayer: SimpleExoPlayer
private val cache: SimpleCache = VideoApp.cache
...
在onCreate 方法中初始化这些变量。
val videoUrl = intent.getStringExtra("VIDEO_URL")
mHttpDataSourceFactory = DefaultHttpDataSource.Factory()
.setAllowCrossProtocolRedirects(true)
this.mDefaultDataSourceFactory = DefaultDataSourceFactory(
applicationContext, mHttpDataSourceFactory)
mCacheDataSourceFactory = CacheDataSource.Factory()
.setCache(cache)
.setUpstreamDataSourceFactory(mHttpDataSourceFactory)
.setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)
我们将初始化exoPlayer ,并传递一个CacheDataSourceFactory 作为其默认的媒体源工厂。然后我们解析我们的视频URL并将其传递给MediaSource 。
exoPlayer = SimpleExoPlayer.Builder(applicationContext)
.setMediaSourceFactory(DefaultMediaSourceFactory(mCacheDataSourceFactory)).build()
val videoUri = Uri.parse(videoUrl)
val mediaItem = MediaItem.fromUri(videoUri)
val mediaSource =
ProgressiveMediaSource.Factory(mCacheDataSourceFactory).createMediaSource(mediaItem)
然后我们把我们的exoPlayer 绑定到activity_main.xml 中的playerView ,并给exoPlayer 设置一些属性,如准备好时播放,寻找(0,0) ,并给它一个MediaSource 。
binding.playerView.player = exoPlayer
exoPlayer.playWhenReady = true
exoPlayer.seekTo(0, 0)
exoPlayer.setMediaSource(mediaSource, true)
exoPlayer.prepare()
演示
这就是全部。当你运行这个应用程序时,你应该期待与此类似的东西。

结论
在本教程中,我们了解了什么是视频预加载和预缓存。我们使用Exoplayer和Workmanager来安排后台工作,在视频播放前进行预加载。