MediaPlayer+TextureView實現視頻播放器

Android中實現視頻播放器的途徑有兩種:

  • 使用VideoView
  • 通過MediaPlayer + SurfaceView/ TextureView
1. VideoView

VideoView使用比較簡單,配合MediaController可以達到控制播放、暫停、快進、快退、切換視頻、進度條顯示等,具體使用在這里不在贅述了。

2. MediaPlayer + SurfaceView / TextureView

實現一個相對完善的視頻播放器,可以使用 MediaPlayer+SurfaceView /MediaPlayer+TextureView

這里為什么有兩種方式呢?然后哪種方式更適合用在項目中呢?接下來我們對比一下。

SurfaceViewSurfaceView提供了嵌入視圖層次結構內部的專用繪圖面板,可以控制此面板的格式,也可以控制其大小,而且它提供了一個輔助線程用以渲染到屏幕中。

TextureViewTextureView可用于顯示內容流,這樣的內容流可以是視頻或OpenGL場景、可以是來自本地數據源或者是遠程數據源。TextureView只能在有硬件加速的窗口中使用,當軟件渲染時,TextureView將不繪制任何內容。與SurfaceView不同,TextureView不會創建單獨的窗口,而是充當常規View。所以允許用戶對TextureView進行移動,轉換,設置動畫等操作。

如果項目需求簡單,當然可以選擇SurfaceView,但是在這里選擇了后者,原因是TextureView更適合視頻流,以及具備普通View的特性,可以在項目中達到想要的變化效果。選好了顯示的View,接下來看看如何使用。

TextureView如何使用
可以通過調用getSurfaceTexture()來獲取TextureViewSurfaceTexture。重要的是只有在將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之后的操作。

  • MediaPlayerPrepared狀態之后可以調用start()使它進入Started狀態,并開始播放視頻,isPlaying()可以測試MediaPlayer對象是否處于Started狀態。在Started狀態時,通過setOnBufferingUpdateListener()可以在其OnBufferingUpdateListener.onBufferingUpdate()回調中獲取流式傳輸 音頻/視頻 時跟蹤緩沖狀態。

  • 播放可以暫停和停止,并且可以調整當前播放位置。
    可以通過pause()暫停播放。當調用 pause()返回時,MediaPlayer對象將進入Pause狀態。注意在播放器引擎中,從Strated 狀態到Pause狀態的轉換是異步的,可能要花一些時間。調用start()會重新變為Started狀態并開始播放。
    可以通過stop()停止播放,這時,StartedPausedPreparedPlaybackCompleted狀態的MediaPlayer進入Stopped狀態。處于Stopped狀態,就無法開始播放,直到調用prepare()prepareAsync()MediaPlayer對象重新設置為Prepared狀態。

  • 調整播放位置可以通過seekTo()方法,由于seekTo()是異步的,實際上查找需要一定時間才能完成,實際的查找位置完成時會走setOnSeekCompleteListener()OnSeekComplete.onSeekComplete()回調。
    seekTo()PreparedPausedPlaybackCompleted 狀態下執行仍然會保持當前的狀態。
    實際的當前播放位置可以通過getCurrentPosition()獲取。

  • 當視頻播放完成之后默認會走setOnCompletionListener()中的OnCompletion.onCompletion()回調,在回調后處于PlaybackCompleted狀態。
    如果設置setLooping()true時,會在播放完成后重新變為Started狀態并重新播放視頻。
    PlaybackCompleted 狀態下,調用start()也可以從音頻/視頻源的開頭重新開始播放。

開始實現視頻播放器
這里簡單地畫了下播放器的流程圖(有點丑...)。

流程圖

項目結構的話借鑒Google官方MVP模式,能實現視頻播放器解耦,功能操作和UI邏輯分離,使用契約類實現PresenterViewsPresenter中實現各種功能邏輯,Views負責UI展示。下面是項目的結構:

JVideoView項目結構

  • 契約類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中持有JVideoViewVideoRepository(數據倉庫)的引用,里面實現具體的播放功能。
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的設置
  1. 設置播放器的幕布以及播放的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)
  1. 在設置完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 ->
        //這里是視頻的原始尺寸大小
    }
  1. 幕布準備完畢也就是前面的onSurfaceTextureAvailable()回調中讓MediaPlayer進入Prepared狀態。在這里用異步的方式prepareAsync,在進入setOnPreparedListener(預加載完成)之前都設為STATE_PREPARING
    //預加載監聽
    mPlayer?.setOnPreparedListener {
        mPlayState = PlayState.STATE_PREPARED
        //預加載后播放
        mPlayer?.start()
    }
    //異步的方式裝載流媒體文件
     prepareAsync()
  1. 視頻播放中會有緩沖,當緩沖區不足時會停止播放并進行緩沖加載,緩沖加載中如果用戶未暫停則設為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總進度和視頻的總長度一致,拖動時在SeekBaronStopTrackingTouch中通知播放器設置進度。
    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
參考文章:

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,563評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,694評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,672評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,965評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,690評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,019評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,013評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,188評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,718評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,438評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,667評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,149評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,845評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,252評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,590評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,384評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內容