如何用ExoPlayer在Android中预装和缓冲视频

3,592 阅读6分钟

用ExoPlayer在Android中预装和缓冲视频

每个Android开发者都需要了解如何预装视频,这样用户就不必在视频加载时等待,就像YouTube应用那样。

一个视频可以在播放之前被加载和缓存。这很有趣,因为我们把等待时间降到最低。

前提条件

要跟上本教程,读者应该具备以下条件。

  • 具有创建Android应用程序的良好知识。
  • 对Kotlin编程语言有良好的了解。
  • 对使用工作管理器、ViewBinding和Kotlin Coroutines有基本了解。

什么是视频预加载和缓冲?

缓冲发生在视频流中,当软件在开始播放视频之前下载特定数量的数据。当文件的下一节在后台下载时,你可以将已经预载并存储在缓冲区的数据进行流式处理。

ExoPlayer是一个由谷歌开发的库。它为Android的MediaPlayer API提供了一个替代方案,用于在本地和互联网上播放音频和视频。ExoPlayer支持Android的MediaPlayer API目前不支持的功能。

开始使用

在本教程中,我们将创建一个简单的应用程序,从互联网上播放视频,并在用户观看之前将其缓存起来。

第1步 - 创建一个Android项目

启动你的Android Studio并创建一个空项目。

New Android App

第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" />

Player View

第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步 - 缓存视频

当预先缓存一个视频时,最好在不同的ActivityFragment ,这样当用户导航到实际目的地时,他/她会发现视频已经准备好。就像在youtube上,视频被显示在一个列表中,当用户选择一个特定的视频时,就是他们被导航到一个不同的屏幕,视频在那里播放。

在某些情况下,开发者更喜欢在RecyclerView中显示视频的缩略图,然后,当用户选择一个特定的视频时,该视频会在不同的屏幕上播放。在我们的案例中,我们将定义一个活动来进行预加载,然后当用户点击播放视频Button ,他/她会被导航到另一个播放视频的活动中。

创建一个空的活动(我的将被称为FirstActivity )。

在其布局中,创建一个单一的按钮。

Play Button

在一个更复杂的情况下,你可以有一个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()

演示

这就是全部。当你运行这个应用程序时,你应该期待与此类似的东西。

Demo Gif

结论

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