Android中實現視頻播放器的途徑有兩種:
- 使用
VideoView
- 通過
MediaPlayer
+SurfaceView
/TextureView
1. VideoView
VideoView
使用比較簡單,配合MediaController
可以達到控制播放、暫停、快進、快退、切換視頻、進度條顯示等,具體使用在這里不在贅述了。
2. MediaPlayer + SurfaceView / TextureView
實現一個相對完善的視頻播放器,可以使用 MediaPlayer+SurfaceView
/MediaPlayer+TextureView
。
這里為什么有兩種方式呢?然后哪種方式更適合用在項目中呢?接下來我們對比一下。
SurfaceView:SurfaceView
提供了嵌入視圖層次結構內部的專用繪圖面板,可以控制此面板的格式,也可以控制其大小,而且它提供了一個輔助線程用以渲染到屏幕中。
TextureView:TextureView
可用于顯示內容流,這樣的內容流可以是視頻或OpenGL
場景、可以是來自本地數據源或者是遠程數據源。TextureView
只能在有硬件加速的窗口中使用,當軟件渲染時,TextureView
將不繪制任何內容。與SurfaceView
不同,TextureView
不會創建單獨的窗口,而是充當常規View
。所以允許用戶對TextureView
進行移動,轉換,設置動畫等操作。
如果項目需求簡單,當然可以選擇SurfaceView
,但是在這里選擇了后者,原因是TextureView
更適合視頻流,以及具備普通View
的特性,可以在項目中達到想要的變化效果。選好了顯示的View
,接下來看看如何使用。
TextureView如何使用
可以通過調用getSurfaceTexture()
來獲取TextureView
的SurfaceTexture
。重要的是只有在將TextureView
附加到窗口(并onAttachedToWindow()
已被調用)后,SurfaceTexture
才可用。因此,在SurfaceTexture
可用時通過實現TextureView.SurfaceTextureListener
來獲取狀態的通知。要注意,只有一個生產者可以使用TextureView
。例如,如果使用TextureView
顯示相機預覽,則lockCanvas()
無法同時在TextureView
上繪制。
SurfaceTextureListener
的回調。
override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture?, width: Int, height: Int) {
//SurfaceTexture緩沖區大小更改時調用(這里的width、height是改變后的畫布大小)
}
override fun onSurfaceTextureUpdated(surface: SurfaceTexture?) {
//SurfaceTexture通過更新指定的值 時調用
}
override fun onSurfaceTextureDestroyed(surface: SurfaceTexture?): Boolean {
//當指定的SurfaceTexture對象即將被銷毀時調用。返回true,則調用此方法后,
//表面紋理內不應進行任何渲染。如果返回false,則客戶端需要SurfaceTexture.release()。
return false
}
override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
//當TextureView準備使用SurfaceTexture時調用(這里的width、height是原始畫布大小)
}
MediaPlaye如何使用
MediaPlayer可用于控制音頻/視頻文件和流的播放。
在官網給出的圖中,顯示了受支持的播放控制操作驅動的MediaPlayer
對象的生命周期和狀態。
- 橢圓形表示
MediaPlayer
對象可能駐留的狀態。 -
弧形表示驅動對象狀態轉換的回放控制操作,有兩種類型的弧,具有單箭頭的弧表示同步方法調用,而具有雙箭頭的弧表示異步方法調用。
MediaPlayer的生命周期和狀態
MediaPlayer的生命周期:
當新建對象或者在創建之后調用
reset()
時,MediaPlayer
對象處于Idle狀態。在調用release()
之后處于End狀態。在這兩種狀態之間是MediaPlayer
對象的生命周期。一旦不再使用MediaPlayer
對象,立即調用release()
釋放資源。MediaPlayer
對象處于End狀態,就無法再使用它,也無法將其恢復為其他任何狀態。某些回放控制操作可能由于各種原因而失敗,例如,不支持的音頻/視頻格式,音頻/視頻的交錯差,分辨率太高,流式傳輸超時等。在所有這些錯誤條件下,如果已經
setOnErrorListener()
,則內部播放器引擎將調用用戶提供的OnErrorListener.onError()
方法。setDataSource()
用于設置視頻資源,只能在Idle狀態下調用,其他狀態下會拋出IllegalStateException
,調用后處于Initialized狀態。MediaPlayer
需要Initialized狀態下才能進行準備工作,MediaPlayer
提供了兩個方法prepare()
(同步)、prepareAsync()
(異步)使其進入Prepared狀態,異步調用可以在OnPreparedListener
接口的回調方法onPrepared()
中進行Prepared之后的操作。當
MediaPlayer
在Prepared
狀態之后可以調用start()
使它進入Started狀態,并開始播放視頻,isPlaying()
可以測試MediaPlayer
對象是否處于Started狀態。在Started狀態時,通過setOnBufferingUpdateListener()
可以在其OnBufferingUpdateListener.onBufferingUpdate()
回調中獲取流式傳輸 音頻/視頻 時跟蹤緩沖狀態。播放可以暫停和停止,并且可以調整當前播放位置。
可以通過pause()
暫停播放。當調用pause()
返回時,MediaPlayer
對象將進入Pause狀態。注意在播放器引擎中,從Strated 狀態到Pause狀態的轉換是異步的,可能要花一些時間。調用start()
會重新變為Started狀態并開始播放。
可以通過stop()
停止播放,這時,Started,Paused,Prepared或PlaybackCompleted狀態的MediaPlayer
進入Stopped狀態。處于Stopped狀態,就無法開始播放,直到調用prepare()
或prepareAsync()
將MediaPlayer
對象重新設置為Prepared狀態。調整播放位置可以通過
seekTo()
方法,由于seekTo()
是異步的,實際上查找需要一定時間才能完成,實際的查找位置完成時會走setOnSeekCompleteListener()
的OnSeekComplete.onSeekComplete()
回調。
seekTo()
在Prepared,Paused和PlaybackCompleted 狀態下執行仍然會保持當前的狀態。
實際的當前播放位置可以通過getCurrentPosition()
獲取。當視頻播放完成之后默認會走
setOnCompletionListener()
中的OnCompletion.onCompletion()
回調,在回調后處于PlaybackCompleted狀態。
如果設置setLooping()
為true
時,會在播放完成后重新變為Started狀態并重新播放視頻。
在PlaybackCompleted 狀態下,調用start()
也可以從音頻/視頻源的開頭重新開始播放。
開始實現視頻播放器
這里簡單地畫了下播放器的流程圖(有點丑...)。
項目結構的話借鑒Google官方MVP
模式,能實現視頻播放器解耦,功能操作和UI邏輯分離,使用契約類實現Presenter
和Views
,Presenter
中實現各種功能邏輯,Views
負責UI展示。下面是項目的結構:
- 契約類
JVideoViewContract
的實現,分工明確,在Presenter
中實現播放器功能邏輯。
interface JVideoViewContract {
interface Views {
//設置presenter
fun setPresenter(presenter: Presenter)
//設置播放標題
fun setTitle(title:String)
//緩沖中
fun buffering(percent:Int)
/**
* 其他狀態下需實現的UI,例如:暫停、加載中、播放結束等
*/
}
interface Presenter {
//實現訂閱關系
fun subscribe()
//移除訂閱關系
fun unSubscribe()
//開始播放
fun startPlay(position: Int = 0)
//暫停播放
fun pausePlay()
/**
* 其他改變播放狀態或者播放器參數的功能,比如初始化播放器、開始播放,滑動快進、音量調節等
*/
}
}
- 播放器的播放狀態和播放模式等都寫成常量封裝在狀態類
JVideoState
中,避免使用時過于混亂。
class JVideoState {
//播放器狀態
class PlayState{
}
// 播放模式
class PlayMode{
}
//調節模式
class PlayAdjust{
}
}
- 播放器狀態常量
PlayState
。
/**
* 播放器狀態
*/
class PlayState{
companion object{
//播放錯誤
const val STATE_ERROR = -1
//播放未開始
const val STATE_IDLE = 0
//播放準備中
const val STATE_PREPARING = 1
// 播放準備就緒
const val STATE_PREPARED = 2
// 開始播放
const val STATE_START = 3
//正在播放
const val STATE_PLAYING = 4
// 暫停播放
const val STATE_PAUSED = 5
//正在緩沖(播放器正在播放時,緩沖區數據不足,進行緩沖,緩沖區數據足夠后恢復播放)
const val STATE_BUFFERING_PLAYING = 6
// 正在緩沖(播放器正在播放時,緩沖區數據不足,進行緩沖,此時暫停播放器,繼續緩沖,緩沖區
//數據足夠后恢復暫停
const val STATE_BUFFERING_PAUSED = 7
// 播放完成
const val STATE_COMPLETED = 8
}
}
- 考慮到后期能運用在其他項目中,
JVideoView
繼承LinearLayout
、實現JVideoViewContract.Views
,TextureView.SurfaceTextureListener
接口,同時布局由控制器界面和播放界面組成。
class JVideoView : LinearLayout, JVideoViewContract.Views, TextureView.SurfaceTextureListener {
/*具體邏輯*/
override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture?, width: Int, height: Int) {
//SurfaceTexture緩沖區大小改變時
}
override fun onSurfaceTextureUpdated(surface: SurfaceTexture?) {
// SurfaceTexture更新時
}
override fun onSurfaceTextureDestroyed(surface: SurfaceTexture?): Boolean {
//幕布銷毀時釋放資源
return false
}
override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
//幕布準備完畢,這里是原始畫布大小
}
}
- 在
JVideoView
中實現對UI的顯示控制。
override fun setPresenter(presenter: JVideoViewContract.Presenter) {
mPresenter = presenter
}
override fun setTitle(title: String) {
}
override fun preparedVideo(videoTime: String, max: Int) {
}
override fun startVideo(position: Int) {
}
override fun buffering(percent: Int) {
}
override fun continueVideo() {
}
override fun pauseVideo() {
}
override fun playing(videoTime: String, position: Int) {
}
override fun completedVideo() {
}
override fun showLoading(isShow: Boolean, text: String) {
}
- 在
JVideoViewPresenter
中持有JVideoView
和VideoRepository
(數據倉庫)的引用,里面實現具體的播放功能。
class JVideoViewPresenter(
private val mContext: Context,
private val mView: JVideoViewContract.Views,
private val mVideoRepository: VideoRepository
) : JVideoViewContract.Presenter {
init {
mView.setPresenter(this)
}
/*其他操作*/
}
-
JVideoViewPresenter
中實現數據的獲取以及功能實現后調用JVideoViewContract.Views
的方法。
override fun subscribe() {
}
override fun unSubscribe() {
}
override fun startPlay(position: Int) {
//開始播放
}
override fun pausePlay() {
//暫停播放視頻
}
override fun continuePlay() {
//繼續暫停播放視頻
}
override fun onPause() {
//onPause,可在此處暫停播放視頻
}
override fun onResume() {
//onResume,可在此處恢復播放視頻
}
override fun getDuration(): Int {
/*總進度獲取*/
}
override fun getPosition(): Int {
/*當前進度獲取*/
}
override fun getBufferPercent(): Int {
/*獲取緩沖*/
}
override fun releasePlay(destroyUi: Boolean) {
/*結束播放時釋放資源,如對一些強引用的置空等*/
}
- 在
JVideoViewPresenter
中實現數據源的獲取。
//獲取數據源
private fun loadVideosData() {
mVideoRepository.getVideos(object : VideoDataSource.LoadVideosCallback {
override fun onVideosLoaded(videos: List<Video>) {
/*數據源獲取成功*/
}
override fun onDataNotAvailable() {
/*數據源獲取失敗*/
}
})
}
- 關于
MediaPlayer
的設置
- 設置播放器的幕布以及播放的Url以及相關的音頻參數。
//設置視頻的url,本地或者網絡
mPlayer?.setDataSource(video.videoUrl)
//設置渲染畫板
mPlayer?.setSurface(surface)
//設置是否循環播放,默認可不寫
isLooping = false
//設置播放類型、音頻參數
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val attributes = AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.setFlags(AudioAttributes.FLAG_LOW_LATENCY)
.setUsage(AudioAttributes.USAGE_MEDIA)
.setLegacyStreamType(AudioManager.STREAM_MUSIC)
.build()
mPlayer?. setAudioAttributes(attributes)
} else {
mPlayer?.setAudioStreamType(AudioManager.STREAM_MUSIC)
}
//設置是否保持屏幕常亮
setScreenOnWhilePlaying(true)
- 在設置完
MediaPlayer
的基本參數之后設置各種監聽setOnCompletionListener
(播放完畢),setOnSeekCompleteListener
(seekTo查找完成),setOnPreparedListener
(預加載完成),setOnBufferingUpdateListener
(緩沖變化),setOnErrorListener
(播放錯誤),setOnInfoListener
(播放器信息),setOnVideoSizeChangedListener
(視頻尺寸修改),并修改相應的狀態表示。
//播放完成監聽
mPlayer?.setOnCompletionListener {
}
//seekTo()調用并實際查找完成之后
mPlayer?.setOnSeekCompleteListener {
}
//預加載監聽
mPlayer?.setOnPreparedListener {
}
//相當于緩存進度條
mPlayer?.setOnBufferingUpdateListener { mp, percent ->
}
//播放錯誤監聽
mPlayer?.setOnErrorListener { mp, what, extra ->
true
}
//播放信息監聽
mPlayer?. setOnInfoListener { mp, what, extra ->
true
}
//播放尺寸
mPlayer?.setOnVideoSizeChangedListener { mp, width, height ->
//這里是視頻的原始尺寸大小
}
- 幕布準備完畢也就是前面的
onSurfaceTextureAvailable()
回調中讓MediaPlayer
進入Prepared狀態。在這里用異步的方式prepareAsync
,在進入setOnPreparedListener
(預加載完成)之前都設為STATE_PREPARING
。
//預加載監聽
mPlayer?.setOnPreparedListener {
mPlayState = PlayState.STATE_PREPARED
//預加載后播放
mPlayer?.start()
}
//異步的方式裝載流媒體文件
prepareAsync()
- 視頻播放中會有緩沖,當緩沖區不足時會停止播放并進行緩沖加載,緩沖加載中如果用戶未暫停則設為
STATE_BUFFERING_PLAYING
狀態,如果暫停則設為STATE_BUFFERING_PAUSED
狀態,緩沖完成之后恢復到原來的播放狀態。
//播放信息監聽
mPlayer?. setOnInfoListener { mp, what, extra ->
when (what) {
MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START -> {
// 播放器開始渲染
mPlayState = PlayState.STATE_PLAYING
}
MediaPlayer.MEDIA_INFO_BUFFERING_START -> {
// MediaPlayer暫時不播放,以緩沖更多的數據
mPlayState = if (mPlayState == PlayState.STATE_PAUSED || mPlayState == PlayState.STATE_BUFFERING_PAUSED) {
PlayState.STATE_BUFFERING_PAUSED
} else {
PlayState.STATE_BUFFERING_PLAYING
}
}
MediaPlayer.MEDIA_INFO_BUFFERING_END -> {
// 填充緩沖區后,MediaPlayer恢復播放/暫停
if (mPlayState == PlayState.STATE_BUFFERING_PLAYING) {
mPlayState = PlayState.STATE_PLAYING
}
if (mPlayState == PlayState.STATE_BUFFERING_PAUSED) {
mPlayState = PlayState.STATE_PAUSED
}
}
MediaPlayer.MEDIA_INFO_NOT_SEEKABLE -> {
//無法seekTo
}
}
}
- 到這里一個簡單的播放器就完成了。但是~ ~,簡單是不行的,單純靠簡單直接使用
VideoView
不就行了,所以還要實現拖動改變進度,全屏播放,滑動改變進度、音量和亮度調節。
播放器的各種調節控制的實現
- 拖動改變進度的實現,拖動主要是在控制器底部寫一個
SeekBar
,仿照平時看劇的App就行,設置SeekBar
總進度和視頻的總長度一致,拖動時在SeekBar
的onStopTrackingTouch
中通知播放器設置進度。
override fun onStopTrackingTouch(seekBar: SeekBar) {
//seekBar滑動中的回調
mPlayer?.seekTo(seekBar.progress)
}
- 全屏播放的實現,由于播放器是繼承
LinearLayout
,通過設置layoutParams
達到全屏效果,記得一開始需要保存原始的params
,用于再次恢復正常模式。
//進入全屏模式
mPlayMode = PlayMode.MODE_FULL_SCREEN
// 隱藏ActionBar、狀態欄,并橫屏
(mContext as AppCompatActivity).supportActionBar?.hide()
mContext.window.setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN
)
mContext.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
//設置為充滿父布局
val params = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
//隱藏虛擬按鍵,并且全屏
mContext.window.decorView.systemUiVisibility =
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or View.SYSTEM_UI_FLAG_FULLSCREEN
//設置播放器為全屏
(mView as LinearLayout).layoutParams = params
- 滑動改變進度、音量和亮度調節的實現,通過對整個播放器的根部局的
setOnTouchListener
進行滑動監聽,在不同的位置滑動對播放器進行控制即可。AudioManager
的使用可以百度,具體代碼可以移步項目地址,這里不再贅述。
//亮度調節
val params = (mContext as AppCompatActivity).window.attributes
params.screenBrightness = light / 255f
mContext.window.attributes = params
// 音量調節
mAudioManager?.setStreamVolume(AudioManager.STREAM_MUSIC, volume, 0)
實現的界面
至此,一個簡單的有功能的播放器實現了,可能或多或少有待改進的地方,后續仍然會進行優化,歡迎批評指正。
項目地址:Jvideoview
參考文章: