原文地址:https://medium.com/google-developers/building-a-video-player-app-in-android-part-1-5-d95770ef762d
Android的多媒體APIs可以讓你創(chuàng)建具有豐富的媒體體驗(yàn)的應(yīng)用,它們還提供藍(lán)牙耳機(jī),汽車音響系統(tǒng)痴柔,有線耳機(jī),甚至Google智能助理和Android Auto外部控制的控件木张。
本系列文章的目標(biāo)是讓您開(kāi)始使用ExoPlayer和MediaSession構(gòu)建簡(jiǎn)單但功能豐富的視頻播放器應(yīng)用程序。
這個(gè)app將會(huì)支持如下功能:
- 播放本地和遠(yuǎn)程的視頻資源
- 支持播放列表端三,以便于你可以將視頻列表串聯(lián)在一起
- 支持MediaSession舷礼,以便于藍(lán)牙耳機(jī)可以控制你的視頻,并且可以查看當(dāng)前播放的視頻郊闯。
- 支持音頻對(duì)焦妻献,以便您遵守Android的音頻對(duì)焦系統(tǒng),并在播放其他內(nèi)容或用戶收到電話時(shí)暫停播放虚婿。
- 在Android Oreo上支持畫中畫模式旋奢,以便在使用其他應(yīng)用程序時(shí)泳挥,您的應(yīng)用程序的視頻播放可以在最小化的窗口中繼續(xù)播放然痊。
我們將使用kotlin來(lái)開(kāi)發(fā)這個(gè)app。
ExoPlayer概述
為了使用ExoPlayer來(lái)播放網(wǎng)絡(luò)中或者是Apk中的視頻文件屉符,首先需要?jiǎng)?chuàng)建SimpleExoPlayer和MediaSource實(shí)例剧浸,你還需要一個(gè)包含PlayerView的Activity將用來(lái)實(shí)際呈現(xiàn)播放器加載的視頻內(nèi)容。
播放器
播放什么
你需要告訴播放器要加載什么媒體文件矗钟,并且從哪里加載唆香,MediaSource可以讓你做到這一點(diǎn),媒體源表示為Uris吨艇,它可以指向Apk中的assets文件夾或者是Http躬它。你提供的Uris用于加載準(zhǔn)備播放的內(nèi)容,ExtractorMediaSource允許您處理這些源东涡。
注意:為了適配格式冯吓,你可以使用DashMediaSource(DASH 資源),SsMediaSource(SmoothStreaming資源)疮跑,和HlsMediaSource(HLS資源)组贺。
如果你通過(guò)HTTP加載媒體文件需要在AndroidManifest.xml文件中添加網(wǎng)絡(luò)權(quán)限,如果是從資源文件中加載媒體文件祖娘,那么就不需要添加任何權(quán)限失尖。
<uses-permission android:name="android.permission.INTERNET" />
在哪里渲染播放
然后,你需要將播放器附加到PlayerView中,他將視頻呈現(xiàn)給用戶掀潮,并且還提供用于音視頻播放的UI控件菇夸。
你還必須準(zhǔn)備播放器,該播放器會(huì)通知開(kāi)始加載數(shù)據(jù)胧辽,因?yàn)閺木W(wǎng)絡(luò)上緩存數(shù)據(jù)到播放之前會(huì)有一段時(shí)間峻仇,您還必須將playWhenReady設(shè)置為true,這會(huì)告知ExoPlayer播放邑商。
playWhenReady = true -> PLAY(在足夠的數(shù)據(jù)被緩沖之后開(kāi)始)
playWhenReady = false -> PAUSE
為了支持ExoPlayer摄咆,需要在build.gradle中添加如下配置:
ext { ver = "2.7.0" }
dependencies {
implementation "com.google.android.exoplayer:exoplayer-core:$ver"
implementation “com.google.android.exoplayer:exoplayer-ui:$ver”
}
添加ExoPlayer依賴并不會(huì)明顯增加apk的大小。
創(chuàng)建ExoPlayer實(shí)例
此代碼演示了如何使用默認(rèn)的選項(xiàng)創(chuàng)建一個(gè)播放器人断,并且把它附加到PlayerView(必須要在顯示視頻的Activity的布局文件中聲明)吭从。
enum class SourceType{
local_audio, local_video, http_audio, http_video, playlist
}
data class PlayerState(var window: Int = 0,
var position: Long = 0,
var whenReady: Boolean = true,
var source: SourceType = local_audio)
class PlayerHolder(val context: Context,
val playerView: PlayerView,
val PlayerState: PlayerState) : AnkoLogger {
val player: ExoPlayer
init{
player = ExoPlayerFactory.newSimpleInstance(ctx, DefaultTrackSelector())
.also{
playerView.player = it
info { "SimpleExoPlayer created " }
}
}
fun start() {
// Load media.
prepare(buildMediaSource(selectMediaToPlay(state.source)))
// Start playback when media has buffered enough.
playWhenReady = true
}
fun selectMediaToPlay(source: Source): Uri {
return when (source) {
Source.local_audio -> Uri.parse("asset:///audio/file.mp3")
Source.local_video -> Uri.parse("asset:///video/file.mp4")
Source.http_audio -> Uri.parse("http://site.../file.mp3")
Source.http_video -> Uri.parse("http://site.../file.mp4")
}
}
private fun buildMediaSource(uri: Uri): ExtractorMediaSource {
return ExtractorMediaSource.Factory(
DefaultDataSourceFactory(ctx, "videoapp")).createMediaSource(uri)
}
fun stop() { ... }
fun release() { ... }
}
}
PlayerView需要在Activity的布局文件中進(jìn)行聲明,例如:
<com.google.android.exoplayer2.ui.PlayerView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/exoplayerview_activity_video"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
使用ExoPlayer加載APK中的貝蒂文件
DefaultDataSource允許通過(guò)Uris加載本地文件:
- file:///
- asset:///
- content:///
- rtmp
- data
- http(s)
你可以用如下方式加載assets中的文件(你也可以在assets文件夾下面創(chuàng)建嵌套的文件夾):
- Uri.parse("file:///android_asset/video/video.mp4")
- Uri.parse("asset:///video/video.mp4")
請(qǐng)注意:
- ExoPlayer不允許使用
Uri.parse("android.resource://${packageName}/${R.raw.id}")
加載res文件夾中的文件,并且恶迈,Android不允許在res文件夾中添加文件夾涩金。 - 你可以使用RawResourceDataSource去構(gòu)建一個(gè)Uri指向你的res文件夾中的資源id,例如:
RawResourceDataSource.buildRawResourceUri(R.raw.my_media_file)
釋放ExoPlayer資源
當(dāng)播放完畢時(shí)暇仲,要停止播放器步做,因?yàn)樗麜?huì)消耗網(wǎng)絡(luò),內(nèi)存和系統(tǒng)編解碼器等資源奈附。編解碼器是手機(jī)上共享的資源全度,根據(jù)手機(jī)和操作系統(tǒng)的版本斥滤,可能數(shù)量有限将鸵,所以在不使用的時(shí)候及時(shí)釋放很重要,如果需要的話佑颇,下面的代碼允許你再次重用相同的ExoPlayer實(shí)例
fun stop(){
player.stop(true)
info { "SimpleExoPlayer is stopped" }
}
fun release() {
player.release()
info{ "SimpleExoPlayer is released" }
}
這將釋放播放器持有的所有資源顶掉,為了再次使用播放器,請(qǐng)調(diào)用prepare并設(shè)置playWhenReady挑胸,如上面要點(diǎn)所示痒筒。
ExoPlayer.release()是一個(gè)與stop完全相同的方法,只有一個(gè)例外:他也停止播放線程茬贵,放置ExoPlayer實(shí)例被重用簿透,當(dāng)播放器不再使用時(shí),例如在Service的onDestory或者Activity的onDestroy方法中闷沥,調(diào)用該方法釋放資源萎战。
Activity的生命周期集成
您必須與Android的Activity生命周期集成才能創(chuàng)建,使用和釋放播放器舆逃,下面是一個(gè)簡(jiǎn)單的例子蚂维。
class VideoActivity : AppCompatActivity(){
lateinit var playerHolder: PlayerHolder
var state = PlayerState()
override fun onCreate(savedInstanceState: Bundle?){
...
playerHolder = PlayerHolder(this, exoplayerview_activity_video, state)
}
override fun onStart(){
super.onStart()
playerHolder.start()
}
override fun onStop(){
super.onStop()
playerHolder.stop()
}
override fun onDestroy(){
super.onDestroy()
playerHolder.release()
}
}
在onStart()和onStop()之間保持播放器狀態(tài)
PlayerState數(shù)據(jù)類用于在播放器第一次運(yùn)行之前加載播放器的狀態(tài)信息戳粒。當(dāng)播放器釋放了,一些播放器的狀態(tài)將會(huì)保存在對(duì)象中虫啥,當(dāng)一個(gè)新的播放器被創(chuàng)建蔚约,這個(gè)簡(jiǎn)單的狀態(tài)對(duì)象用于配置播放器以恢復(fù)實(shí)例以前停止的位置。
data class PlayerState(var window: Int = 0,
var position: Long = 0,
var whenReady: Boolean = true,
var source: SourceType = local_audio)
從用戶體驗(yàn)角度來(lái)說(shuō)涂籽,這意味著當(dāng)你運(yùn)行應(yīng)用程序苹祟,播放一些媒體文件,并點(diǎn)擊主頁(yè)按鈕评雌,播放器資源就會(huì)被釋放树枫,當(dāng)你切回該應(yīng)用程序時(shí),播放器會(huì)再次初始化景东,并且應(yīng)該恢復(fù)先前的狀態(tài)信息砂轻,以便用戶可以從停止播放的位置繼續(xù)播放。
在播放資源沒(méi)有被釋放以前斤吐,播放器的currentWindowIndex搔涝,currentPosition,playWhenReady和播放列表或媒體項(xiàng)目信息被保存到PlayerState對(duì)象和措。一旦播放器重新初始化庄呈,狀態(tài)就會(huì)恢復(fù),以下是來(lái)自PlayerHolder類的方法派阱,演示了如何完成此操作诬留。
class PlayerHolder(...) : AnkoPlayer {
...
fun start() {
player.prepare(...)
with(playerState) {
player.playWhenReady = whenReady
player.seekTo(window, position)
}
info { "SimpleExoPlayer is started" }
}
fun stop(){
with(player) {
with(state) {
position = currentPosition
window = currentWindowIndex
whenReady = playWhenReady
}
stop()
}
info {"SimpleExoPlayer is released"}
}
}
多一點(diǎn)控制播放器的功能
你也可以使用ExoPlayerFactor.newSimpleInstance(...)工廠方法的不同簽名來(lái)自定義您的播放器,例如:即使使用DefaultLoadControl類你也可以更改ExoPlayer的緩沖策略颁褂,更好的滿足您的需求故响。
player = ExoPlayerFactory.newSimpleInstance(
DefaultRenderersFactory(ctx),
DefaultTrackSelector(),
DefaultLoadControl()
).apply {...}
對(duì)于更復(fù)雜的用例傀广,您可以為傳遞給ExoPlayerFactory.newSimpleInstance(...)工廠方法的所有參數(shù)提供自己的實(shí)現(xiàn)颁独,這使您可以對(duì)ExoPlayer執(zhí)行的操作有很大的靈活性。
學(xué)習(xí)資源
ExoPlayer
- IO17 ExoPlayer codelab
- IO14 ExoPlayer Introduction Video
- IO17 ExoPlayer Session Video
- Why ExoPlayer?
- Latest changes for ExoPlayer v2.6.1
MediaSession, Audio Focus
DASH, HLS
Picture in Picture