天天看點

VideoView播放視訊案例

  前言

     最近公司要做一個釋出文章的功能可以添加圖檔和視訊,由于視訊功能在項目中應用的不是很多,是以就打算直接用原生VideoView進行視訊的播放,寫完我把通用的代碼抽取出來,這裡分享給大家。

  效果圖

VideoView播放視訊案例

  代碼

        1.布局檔案

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:id="@+id/rlVideo"
                android:layout_width="match_parent"
                android:layout_height="210dp"
                android:layout_marginLeft="@dimen/margin_two"
                android:layout_marginRight="@dimen/margin_two"
                android:background="@color/black">

    <VideoView
        android:id="@+id/videoView"
        android:layout_width="match_parent"
        android:layout_height="210dp"/>

    <RelativeLayout
        android:id="@+id/rlMask"
        android:layout_width="match_parent"
        android:layout_height="210dp"
        android:focusableInTouchMode="true">

        <ImageView
            android:id="@+id/ivPicture"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_centerInParent="true"
            android:scaleType="centerCrop"/>

        <ImageView
            android:id="@+id/ivPlay"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:scaleType="centerCrop"
            android:src="@drawable/btn_video_play_selector"/>

    </RelativeLayout>

    <RelativeLayout
        android:id="@+id/rlLoading"
        android:layout_width="match_parent"
        android:layout_height="210dp"
        android:focusableInTouchMode="true"
        android:visibility="gone">

        <ProgressBar
            android:id="@+id/progressbar"
            style="@android:style/Widget.ProgressBar.Small.Inverse"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@+id/progressbar"
            android:layout_centerHorizontal="true"
            android:layout_marginTop="@dimen/margin_five"
            android:text="緩沖中..."
            android:textColor="@android:color/white"
            android:textSize="@dimen/sp_10"/>
    </RelativeLayout>

    <FrameLayout
        android:id="@+id/flDelete"
        android:layout_width="@dimen/dp_36"
        android:layout_height="@dimen/dp_36"
        android:layout_alignParentRight="true"
        android:clickable="true">

        <ImageView
            android:id="@+id/ivDelete"
            android:layout_width="@dimen/margin_twenty"
            android:layout_height="@dimen/margin_twenty"
            android:layout_gravity="right|top"
            android:scaleType="fitXY"
            android:src="@drawable/btn_delete"
            android:visibility="gone"/>

    </FrameLayout>
</RelativeLayout>
           

    2. 播放管理類

/**
 * @Description 視訊播放管理類
 * Created by liuchao on 2017/10/24.
 */

public class VideoManager implements MediaPlayer.OnErrorListener, MediaPlayer.OnPreparedListener, MediaPlayer.OnInfoListener, MediaPlayer.OnCompletionListener, View.OnClickListener {

    private VideoView mVideoView;
    private View mPlayButton;
    private View mLoadingLayout;
    private View mCompleteLayout;
    private ImageView mThumbnailView;
    private OnErrorListener mErrorListener;
    private boolean mInitVideoView;


    public interface OnErrorListener {
        void onError(MediaPlayer mp, String errorMsg);
    }

    /**
     * 設定播放出錯回調
     *
     * @param listener
     * @return
     */
    public VideoManager setOnErrorListener(OnErrorListener listener) {
        this.mErrorListener = listener;
        return this;
    }

    /**
     * 建立VideoView播放視訊管理類
     *
     * @param videoView
     * @return
     */
    public static VideoManager create(VideoView videoView) {
        if (null == videoView) return null;
        return new VideoManager(videoView);
    }

    public VideoManager(VideoView videoView) {
        this.mVideoView = videoView;
        this.mVideoView.requestFocus();
    }

    /**
     * @param playButton     播放按鈕
     * @param loadingLayout  緩沖進度布局
     * @param completeLayout 播放完成布局
     * @param thumbnailView  第一幀顯示縮略圖布局
     * @return
     */
    public VideoManager bindView(View playButton, View loadingLayout, View completeLayout, ImageView thumbnailView) {
        this.mPlayButton = playButton;
        this.mLoadingLayout = loadingLayout;
        this.mCompleteLayout = completeLayout;
        this.mThumbnailView = thumbnailView;
        initController();
        initlistener();
        return this;
    }

    public void init(String videoPath) {
        if (TextUtils.isEmpty(videoPath)) return;
        mInitVideoView = true;
        //加載顯示縮略圖
        loadThumbnail(videoPath);
        //設定網絡視訊路徑
        Uri uri;
        if (-1 != "http://".indexOf(videoPath) || -1 != "https://".indexOf(videoPath))
            uri = FileUtils.file2Uri(mVideoView.getContext(), new File(videoPath));
        else
            uri = Uri.parse(videoPath);
        mVideoView.setVideoURI(uri);
        setVideoViewLayoutParams(mVideoView, 0);
    }

    @Override
    public void onClick(View v) {
        mLoadingLayout.setVisibility(mInitVideoView ? View.VISIBLE : View.GONE);
        mInitVideoView = false;
        mVideoView.start();
        mVideoView.requestFocus();
        mCompleteLayout.setVisibility(View.GONE);
    }

    /**
     * 初始化VideoView的監聽
     */
    private void initlistener() {
        this.mVideoView.setOnErrorListener(this);
        this.mVideoView.setOnPreparedListener(this);
        this.mVideoView.setOnInfoListener(this);
        this.mVideoView.setOnCompletionListener(this);
        this.mPlayButton.setOnClickListener(this);
    }

    /**
     * 初始化VideoView的進度控制器
     */
    private void initController() {
        //初始化videoview控制條
        VideoController mediaController = new VideoController(mVideoView.getContext());
        //設定videoview的控制條
        this.mVideoView.setMediaController(mediaController);
        //設定顯示控制條/
        mediaController.show(0);
        mediaController.setPlayListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mLoadingLayout.setVisibility(mVideoView.isPlaying() ? View.GONE : View.VISIBLE);
                mCompleteLayout.setVisibility(mVideoView.isPlaying() ? View.VISIBLE : View.GONE);
            }
        });
    }

    @Override
    public void onCompletion(MediaPlayer mp) {
        mLoadingLayout.setVisibility(View.GONE);
        mCompleteLayout.setVisibility(View.VISIBLE);
    }

    @Override
    public boolean onInfo(MediaPlayer mp, int what, int extra) {
        if (what == MediaPlayer.MEDIA_INFO_BUFFERING_START) {
            this.mVideoView.setBackgroundColor(Color.TRANSPARENT);
            this.mLoadingLayout.setVisibility(View.VISIBLE);
        } else if (what == MediaPlayer.MEDIA_INFO_BUFFERING_END) {
            //此接口每次回調完START就回調END,若不加上判斷就會出現緩沖圖示一閃一閃的卡頓現象
            if (mp.isPlaying()) {
                mLoadingLayout.setVisibility(View.GONE);
            }
        }
        return true;
    }

    @Override
    public void onPrepared(MediaPlayer mp) {
        mInitVideoView = false;
        mLoadingLayout.setVisibility(View.GONE);//緩沖完成就隐藏
    }

    @Override
    public boolean onError(MediaPlayer mp, int what, int extra) {
        switch (what) {
            case MediaPlayer.MEDIA_ERROR_UNKNOWN:
                Log.e("text", "發生未知錯誤");
                if (null != mErrorListener) mErrorListener.onError(mp, "發生未知錯誤");
                break;
            case MediaPlayer.MEDIA_ERROR_SERVER_DIED:
                if (null != mErrorListener) mErrorListener.onError(mp, "媒體伺服器當機");
                Log.e("text", "媒體伺服器當機");
                break;
            default:
                Log.e("text", "onError+" + what);
                if (null != mErrorListener) mErrorListener.onError(mp, "未知錯誤");
                break;
        }
        switch (extra) {
            case MediaPlayer.MEDIA_ERROR_IO:
                //io讀寫錯誤
                Log.e("text", "檔案或網絡相關的IO操作錯誤");
                if (null != mErrorListener) mErrorListener.onError(mp, "檔案或網絡相關的IO操作錯誤");
                break;
            case MediaPlayer.MEDIA_ERROR_MALFORMED:
                //檔案格式不支援
                Log.e("text", "比特流編碼标準或檔案不符合相關規範");
                if (null != mErrorListener) mErrorListener.onError(mp, "比特流編碼标準或檔案不符合相關規範");
                break;
            case MediaPlayer.MEDIA_ERROR_TIMED_OUT:
                //一些操作需要太長時間來完成,通常超過3 - 5秒。
                Log.e("text", "操作逾時");
                if (null != mErrorListener) mErrorListener.onError(mp, "操作逾時");
                break;
            case MediaPlayer.MEDIA_ERROR_UNSUPPORTED:
                //比特流編碼标準或檔案符合相關規範,但媒體架構不支援該功能
                Log.e("text", "比特流編碼标準或檔案合相關規範,但媒體架構不支援該功能");
                if (null != mErrorListener)
                    mErrorListener.onError(mp, "比特流編碼标準或檔案符合相關規範,但媒體架構不支援該功能");
                break;
            default:
                Log.e("text", "onError+" + extra);
                if (null != mErrorListener) mErrorListener.onError(mp, "播放出錯了");
                break;
        }
        return false;
    }

    /**
     * 加載縮略圖
     *
     * @param videoPath
     */
    private void loadThumbnail(final String videoPath) {
        Observable.create(new Observable.OnSubscribe<Bitmap>() {
            @Override
            public void call(Subscriber<? super Bitmap> subscriber) {
                int width = ScreenUtils.getScreenWidth(mVideoView.getContext()) - 50;
                int height = DensityUtil.dip2px(mVideoView.getContext(), 220);
                if (null != videoPath && -1 != videoPath.indexOf("http://")) {
                    subscriber.onNext(VideoUtil.createVideoThumbnail(videoPath, width, height));
                } else {
                    subscriber.onNext(VideoUtil.getVideoThumbnail(videoPath, width, height, MediaStore.Images.Thumbnails.MICRO_KIND));
                }
            }
        }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(new Action1<Bitmap>() {
            @Override
            public void call(Bitmap bitmap) {
                mThumbnailView.setImageBitmap(bitmap);
            }
        });
    }

    /**
     * 設定videiview的全屏和視窗模式
     *
     * @param paramsType 辨別 1為全屏模式 2為視窗模式
     */
    public void setVideoViewLayoutParams(VideoView videoView, int paramsType) {
        //全屏模式
        if (1 == paramsType) {
            //設定充滿整個父布局
            RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
            //設定相對于父布局四邊對齊
            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添加屬性
            videoView.setLayoutParams(layoutParams);
        } else {
            //視窗模式
            //設定視窗模式距離邊框50
//            int videoHeight = DensityUtil.getScreenHeight(videoView.getContext()) - 50;
            int videoHeight = DensityUtil.dip2px(videoView.getContext(), 220);
//            int videoWidth = DensityUtil.getScreenWidth(videoView.getContext()) - 50;
            int videoWidth = RelativeLayout.LayoutParams.WRAP_CONTENT;
            RelativeLayout.LayoutParams LayoutParams = new RelativeLayout.LayoutParams(videoWidth, videoHeight);
            //設定居中
            LayoutParams.addRule(RelativeLayout.CENTER_IN_PARENT);
            //為VideoView添加屬性
            videoView.setLayoutParams(LayoutParams);
        }
    }
}
           

    3. 自定義MediaPlayerController

       3.1 重新MediaPlayerController,修改makeControllerView方法,把布局改成我們自己定義的布局檔案。

protected View makeControllerView() {
        LayoutInflater inflate = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        mRoot = inflate.inflate(R.layout.view_video_controller, null);

        initControllerView(mRoot);

        return mRoot;
    }
           

      3.2 添加暫停按鈕回調監聽

private final View.OnClickListener mPauseListener = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            if (mPlayListener != null) mPlayListener.onClick(v);
            doPauseResume();
            show(sDefaultTimeout);
        }
    };
           

      3.3  注意自定義Controller的布局檔案最好用系統的然後調整布局(不要修改控件Id,否則還得修改源碼)

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:background="#CC000000"
              android:layoutDirection="ltr"
              android:orientation="vertical">

    <LinearLayout
        android:visibility="gone"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:orientation="horizontal"
        android:paddingTop="4dip">

        <ImageButton
            android:id="@+id/prev"
            style="@android:style/MediaButton.Previous"/>

        <ImageButton
            android:id="@+id/rew"
            style="@android:style/MediaButton.Rew"/>

       <!-- <ImageButton
            android:id="@+id/pause"
            style="@android:style/MediaButton.Play"/>-->

        <ImageButton
            android:id="@+id/ffwd"
            style="@android:style/MediaButton.Ffwd"/>

        <ImageButton
            android:id="@+id/next"
            style="@android:style/MediaButton.Next"/>

    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <ImageButton
            android:id="@+id/pause"
            android:layout_marginLeft="10dp"
            android:layout_width="20dip"
            android:layout_height="20dip"
            android:layout_gravity="center_vertical"
            style="@android:style/MediaButton.Play"/>

        <SeekBar
            android:id="@+id/mediacontroller_progress"
            style="?android:attr/progressBarStyleHorizontal"
            android:layout_width="0dip"
            android:layout_height="32dip"
            android:layout_weight="1"/>

        <TextView
            android:id="@+id/time_current"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:paddingEnd="4dip"
            android:paddingStart="4dip"
            android:paddingTop="4dip"
            android:textColor="#bebebe"
            android:textSize="14sp"
            android:textStyle="bold"/>

        <TextView
            android:visibility="gone"
            android:id="@+id/time"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:paddingEnd="4dip"
            android:paddingStart="4dip"
            android:paddingTop="4dip"
            android:textColor="#bebebe"
            android:textSize="14sp"
            android:textStyle="bold"/>
    </LinearLayout>

</LinearLayout>  
           

        4.  使用VideoManager播放視訊

View rlMask = itemView.findViewById(R.id.rlMask);
        View showPlayerLoading = itemView.findViewById(R.id.rlLoading);
        ImageView ivPlay = (ImageView) itemView.findViewById(R.id.ivPlay);
        ImageView thumbnailView = (ImageView) itemView.findViewById(R.id.ivPicture);
        thumbnailView.setTag(R.id.tag_path, videoPath);
        //初始化VideoView
        VideoView videoView = (VideoView) itemView.findViewById(R.id.videoView);
        rlMask.setOnClickListener(this);
        VideoManager.create(videoView).bindView(ivPlay, showPlayerLoading, rlMask, thumbnailView).init(videoPath);
           

      5. 最後補上VideoUtil中兩個擷取本地或服務端視訊縮略圖代碼

/**
     * 擷取網路視訊縮略
     * 自己的背景線程中調用該方法得到網絡視訊的縮略圖bitmap
     * 然後在主線程中調用imageView.setImageBitmap(bitmap)即可
     *
     * @param url    網路視訊位址
     * @param width  指定輸出視訊縮略圖的寬度
     * @param height 指定輸出視訊縮略圖的高度度
     * @return
     */
    public static Bitmap createVideoThumbnail(String url, int width, int height) {
        Bitmap bitmap = null;
        MediaMetadataRetriever retriever = new MediaMetadataRetriever();
        int kind = MediaStore.Video.Thumbnails.MINI_KIND;
        try {
            if (Build.VERSION.SDK_INT >= 14) {
                retriever.setDataSource(url, new HashMap<String, String>());
            } else {
                retriever.setDataSource(url);
            }
            bitmap = retriever.getFrameAtTime();
        } catch (IllegalArgumentException ex) {
            // Assume this is a corrupt video file
        } catch (RuntimeException ex) {
            // Assume this is a corrupt video file.
        } finally {
            try {
                retriever.release();
            } catch (RuntimeException ex) {
                // Ignore failures while cleaning up.
            }
        }
        if (kind == MediaStore.Images.Thumbnails.MICRO_KIND && bitmap != null) {
            bitmap = ThumbnailUtils.extractThumbnail(bitmap, width, height, ThumbnailUtils.OPTIONS_RECYCLE_INPUT);
        }
        return bitmap;
    }

    /**
     * 擷取本地視訊的縮略圖
     * 先通過ThumbnailUtils來建立一個視訊的縮略圖,然後再利用ThumbnailUtils來生成指定大小的縮略圖。
     * 如果想要的縮略圖的寬和高都小于MICRO_KIND,則類型要使用MICRO_KIND作為kind的值,這樣會節省記憶體。
     *
     * @param videoPath 視訊的路徑
     * @param width     指定輸出視訊縮略圖的寬度
     * @param height    指定輸出視訊縮略圖的高度度
     * @param kind      參照MediaStore.Images.Thumbnails類中的常量MINI_KIND和MICRO_KIND。
     *                  其中,MINI_KIND: 512 x 384,MICRO_KIND: 96 x 96
     * @return 指定大小的視訊縮略圖
     */
    public static Bitmap getVideoThumbnail(String videoPath, int width, int height, int kind) {
        Bitmap bitmap = null;
        // 擷取視訊的縮略圖
        bitmap = ThumbnailUtils.createVideoThumbnail(videoPath, kind);
        System.out.println("w" + bitmap.getWidth());
        System.out.println("h" + bitmap.getHeight());
        bitmap = ThumbnailUtils.extractThumbnail(bitmap, width, height, ThumbnailUtils.OPTIONS_RECYCLE_INPUT);
        return bitmap;
    }
           

   總結

          這裡使用VideoManager進行播放視訊就是必須使用指定布局,傳入四個控件進行控制通過init方法設定播放路徑,如果确定播放布局是這樣的就可以重複利用布局檔案,每次在需要播放的地方把步驟4的代碼拷貝過去就好了,如果對布局要做調整,必須把VideoManager對應控件也做相應調整。