Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> VideoView播放視頻初體驗

VideoView播放視頻初體驗

編輯:關於Android編程

這幾個月一直在忙項目上的事情,所以已經有一段時間不寫博客,抽時間整理下最近的收藏夾,感覺還是有一些新的知識點可以分享的。

先從最近的說起,近期項目上需要支持視頻播放功能,雖然第三方的播放器在播放體驗上會比原生的好太多,而且基本上也不需要額外的處理。剛開始也考慮過要接入公司的播放器sdk,當然github上也有很多開源庫都是不錯的,但是唯一的弊病就是sdk太大了,所以就沒有考慮這一些第三方庫,而直接用了系統的videoView 原生視頻播放組件,過程中遇到的一些問題在這裡做一下簡單的記錄。

博主在這個過程中也參照了很多資料,有些文章知識點總結的還是比較到位的,先推薦幾篇相關的文章,

(1)android支持的視頻編碼格式
這裡寫圖片描述

(2)如何使用VideoView

        videoView = (VideoView) root.findViewById(R.id.videoView);
        //設置播放完成以後監聽
        videoView.setOnCompletionListener(mOnCompletionListener);
        //設置發生錯誤監聽,如果不設置videoview會向用戶提示發生錯誤
        videoView.setOnErrorListener(mOnErrorListener);
        //設置在視頻文件在加載完畢以後的回調函數
        videoView.setOnPreparedListener(mOnPreparedListener);
        Uri uri = Uri.parse(mUrl);
        videoView.setVideoURI(uri);

一般情況下我們需要自己能夠自定義視頻控制器,



    
    
    

    

MediaController 就是一個簡單的layout,可以包括seekbar 、暫停控制等,代碼配置如下:

        mediaController = (MediaController) root.findViewById(R.id.media_controller);
        mediaController.setVideoView(videoView);

VideoView提供諸多方法供開發者調用,開發簡單的播放器通常了解以下api 夠用了,

public void start()
//開始播放。

public void pause()
//暫停播放。

public void resume()
//恢復播放,從頭開始播放

public long getDuration()
//獲取視頻播放時長。

public long getCurrentPosition()
//獲取當前播放位置。

public void seekTo(long msec)
//設置播放位置,可以和seekbar聯合起來使用

(3)全屏播放 - 橫豎屏切換
activity 在Androidmanifest.xml 中依然定義豎屏, 可以在mediaController中定義一個方法控制全屏的切換,


這裡需要注意的是,需要在 VidioView 外層套一個容器,如布局中RelativeLayout ,此處定義為root,直接上代碼,

 public void switchSize(Activity activity, View root) {
        if (!fullscreen) {
            activity.setRequestedOrientation(orientation);
            WindowManager.LayoutParams attrs = activity.getWindow().getAttributes();
            attrs.flags |= WindowManager.LayoutParams.FLAG_FULLSCREEN;
            activity.getWindow().setAttributes(attrs);
            activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);

            RelativeLayout.LayoutParams rootParams = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT);
            root.setLayoutParams(rootParams);

            RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT);
            layoutParams.addRule(RelativeLayout.CENTER_IN_PARENT);

            videoView.setLayoutParams(layoutParams);
            videoView.getHolder().setSizeFromLayout();
            mediaController.setIconResource(R.drawable.icon_video_cancel_full);

            fullscreen = true;          
        } else {
            activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);

            WindowManager.LayoutParams attrs = activity.getWindow().getAttributes();
            attrs.flags &= (~WindowManager.LayoutParams.FLAG_FULLSCREEN);
            activity.getWindow().setAttributes(attrs);
            activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);

            RelativeLayout.LayoutParams rootParams = (RelativeLayout.LayoutParams) root.getLayoutParams();
            rootParams.height = DensityUtils.dp2px(activity, 200);
            root.setLayoutParams(rootParams);

            RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT);
            layoutParams.addRule(RelativeLayout.CENTER_IN_PARENT);
            videoView.setLayoutParams(layoutParams);
            videoView.getHolder().setSizeFromLayout();

            mediaController.setIconResource(R.drawable.icon_video_full);
            fullscreen = false;         
        }
    }

細心的朋友會發現,在切換全屏的時候並不是直接將視頻轉為橫屏,而是通過activity.setRequestedOrientation(orientation)來控制。這其實是一個細節問題,實際開發過程中,每個視頻的尺寸是不一樣的,考慮到視頻源的尺寸問題,並不是每一個視頻都是寬大於高,而且也不是所有的手機屏幕都是高大於寬的(如pad,模擬器等)。而orientation 可以在視頻加載onPrepared 回調後進行設置。

  private void treatOrientation(MediaPlayer mp) {
        int width = mp.getVideoWidth();
        int height = mp.getVideoHeight();
        if (width != 0 && height != 0) {
            int screenWidth = DeviceUtils.getScreenWidth(ContextUtils.getApplicationContext());
            int screenHeight = DeviceUtils.getScreenHeight(ContextUtils.getApplicationContext());
            if (width > height) {
                if (screenHeight > screenWidth) {
                    this.orientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
                } else {
                    this.orientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
                }
            } else {
                if (screenHeight > screenWidth) {
                    this.orientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
                } else {
                    this.orientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
                }
            }
        }
    }

以上設置的切換效果,視頻會按照原來的比例充滿屏幕,視頻不會得到拉伸,如果不考慮視頻的拉伸直接充滿屏幕,可以用下面的方式配置

  RelativeLayout.LayoutParams LayoutParams = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT);
        LayoutParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
        LayoutParams.addRule(RelativeLayout.ALIGN_PARENT_TOP);
        LayoutParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT);
        LayoutParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
        videoView.setLayoutParams(LayoutParams);

或者繼承VideoView後重寫onMeasure方法

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  int width = getDefaultSize(mVideoWidth, widthMeasureSpec);
  int height = getDefaultSize(mVideoHeight, heightMeasureSpec);
  ......
  setMeasuredDimension(width, height);
}

(4)暫停/恢復 頁面時,視頻重新加載

 public void onPause() {
        videoPos = mediaController.getPosition();
        videoView.stopPlayback();       
    }

    public void onResume() {
        videoView.resume();
        mediaController.seekToPosition(videoPos);
    }

一般情況下在視頻播放時,頁面 onPause() 再onResume, videoView 會重新開始播放。
比較取巧的處理方案是在 onPause() 的時候記錄當前播放進度位置,在 onResume() 的時候拖動到該進度位置,但是這種情況下依然會有黑屏或者透明的現象,關於黑屏或者透明的原因博文後面會有介紹。

但是VideoView自身的加載機制就是這樣的,每次都會重新加載。如果播放的是本地視頻,那麼這個問題容易解決:在按下home或者關機鍵後保存當前播放的幀到本地變量中,然後重新可見的時候通過調用seekTo方法跳到之前保存的幀,同時調用pause()方法暫停播放,就可以達到效果。但是如果播放的是網絡視頻的話,就需要在視頻緩沖准備的時候設置一個緩沖背景(視頻正在緩沖中。。。),等加載完畢後再執行剛才的流程。

此外這裡還有一個細節要注意,在activity onPause 時調用的是mVideoView.stopPlayback()方法,而不是pause ,這個細節非常重要,通過源碼來看下這兩個方法的異同點。

public void stopPlayback() {
        if (mMediaPlayer != null) {
            mMediaPlayer.stop();
            mMediaPlayer.release();
            mMediaPlayer = null;
            mCurrentState = STATE_IDLE;
            mTargetState  = STATE_IDLE;
        }
    }
    public void pause() {
        if (isInPlaybackState()) {
            if (mMediaPlayer.isPlaying()) {
                mMediaPlayer.pause();
                mCurrentState = STATE_PAUSED;
            }
        }
        mTargetState = STATE_PAUSED;
    }

調用stopPlayback後系統會將mMediaPlayer 釋放,這樣可以解決視頻緩沖時activity退到後台導致的內存問題。 另外一點在activity onResume 的時候,為什麼需要調用videoView.resume(),而不是videoview 的start() 方法呢?

這個問題就比較好理解了,應用按home鍵後,activity會運行在後台,等系統低內存的時候會有概率將activity回收,當activity恢復到前台後需要重新再次調用mediaPlayer.prepareAsync 才能再次正常加載視頻, 所以如果直接調用start 方法再很多情況下是播不了視頻的。當然為了確保恢復到前台後的進度和之前一樣,我們在onResume 後還有一個seekto的操作 。

在拖動視頻播放進度條,手動seekTo後經常會碰到進度跳動的情況,這也是視頻播放器普遍存在的問題,主要還是和視頻格式和分片的位置有關系,暫時也沒有好的解決方法。感興趣的朋友,可以參考 關於Android VideoView seekTo不准確的解決方案。

(5)透明或者黑屏問題

VideoView 繼承於SurfaceView,關於SurfaceView出現黑屏或者透明的問題,[Android基礎] VideoView 這篇文章也提到了。

The surface is Z ordered so that it is behind the window holding its SurfaceView; the SurfaceView punches a hole in its window to allow its surface to be displayed. The view hierarchy will take care of correctly compositing with the Surface any siblings of the SurfaceView that would normally appear on top of it. This can be used to place overlays such as buttons on top of the Surface, though note however that it can have an impact on performance since a full alpha-blended composite will be performed each time the Surface changes.

這是SurfaceView類的說明,解釋了出現透明的原因,也提供了解決問題的思路,最簡單的解決方法就是將SurfaceView挪到上層。

videoView.setZOrderOnTop(true);

冰川孤辰 在文章中也提到了這種方法的缺點並給出了另外一種解決方案,

不過挪動之後就可以設置VideoView的背景,此時才不會遮蓋實際的視頻繪圖了,xml中指定吧,這裡省略,不過如果VideoView區域還有其他控件的話,會被遮蓋,所以最後我就沒設定zorderOnTop了,而是直接在xml中指定VideoView的背景色,然後在onPrepare回調的時候,去掉背景即可(按需延時,或者在有播放進度,要更新進度條的時候進行去掉背景操作都ok,不然可能會有一瞬間的透明):

但是解決的並不徹底,實際上onPrepared只是告訴我們視頻已經准備好了,確沒有真正開始播放,所以在onPrepare回調時把視頻的背景設為透明,在一些視頻緩沖慢的場景下依然會出現透明或者黑屏。
解決的方法可以在onPrepared回調後,加一個setOnInfoListener的監聽等到視頻真正開始播放後再去掉VideoView 的背景。

@Override
  public void onPrepared(MediaPlayer mp) {
    mp.setOnInfoListener(new MediaPlayer.OnInfoListener() {
      @Override
      public boolean onInfo(MediaPlayer mp, int what, int extra) {

        if (what == MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START) {
          // video started
           videoView.setBackgroundColor(Color.TRANSPARENT);
          return true;
        }
        return false;
      }
    });

還有很多地方沒有講到,如果大家有興趣可以留言,有問題的地方也歡迎留言指正,多謝!

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved