Android项目小结——视频播放(MediaPlayer与SurfaceView实现)

一直在做安卓的项目,想着找个时间总结一下,可能太懒了,一直没总结。
代码参考了许多Blog和Github,修修补补改改挺多地方,记录一下,侵删私信或注明出处。

播放

主要的类

三个Interface:

  • IMyPlayer:定义了设置资源播放暂停停止获取播放时长当前位置定位到、以及Activity状态等函数方法。
  • IMyDisplay:定义了获取播放视图(View),播放的SurfaveHolder的方法。
  • IMyPlayerListener:封装了播放,停止,恢复等方法。
  • MyPlayerException:封装了异常。
  • MySimpleDisplay:简单实现了IMyDisplay。
  • MediaPlayerHelper:实现了IMyPlayer,并实现MediaPlayer相对应的一些方法。

播放流程

(1)初始化辅助类
(2)设置播放的View
(3)设置播放资源
(4)播放、暂停、停止

代码实现

IMyPlayer

public interface IMyPlayer {
    /**
     * 设置资源
     *
     * @param url 资源路径
     * @throws MyPlayerException
     */
    void setSource(String url) throws MyPlayerException;

    /**
     * 设置显示视频的载体
     *
     * @param display 视频播放的载体及相关界面
     */
    void setDisplay(IMyDisplay display);

    /**
     * 播放视频
     *
     * @throws MyPlayerException
     */
    void play() throws MyPlayerException;

    /**
     * 暂停视频
     */
    void pause();

    /**
     * 停止播放
     */
    void stop();

    /**
     * 获取资源的总时长
     *
     * @return 时长毫秒数
     */
    int getSourceDuration() throws MyPlayerException;

    /**
     * 获取当前播放的位置
     *
     * @return 毫秒数
     */
    int getCurrentPosition() throws MyPlayerException;

    /**
     * <p>移动播放位置</p>
     * <p>若当前是播放状态,则<font color='red'>暂停</font>播放</p>
     *
     * @param time 移动到的位置,毫秒数表示
     */
    void seekTo(long time) throws MyPlayerException;


    /**
     * 用于Activity的onPause状态
     */
    void onPause();

    /**
     * 用于Activity的onResume状态
     */
    void onResume();

    /**
     * 用于Activity的onDestroy状态
     */
    void onDestroy();
}

IMyDisplay

public interface IMyDisplay extends IMyPlayerListener {

    /**
     * @return 获取播放载体的View, SurfaceView或TextureView或SurfaceTexture
     */
    View getDisplayView();

    /**
     * @return 获取SurfaceHolder
     */
    SurfaceHolder getHolder();
}

IMyPlayerListener

public interface IMyPlayerListener {

    void onStart(IMyPlayer player);

    void onPause(IMyPlayer player);

    void onResume(IMyPlayer player);

    void onComplete(IMyPlayer player);
}

MySimpleDisplay(简单实现接口)

public class MySimpleDisplay implements IMyDisplay {
    private SurfaceView surfaceView;

    public MySimpleDisplay(SurfaceView surfaceView) {
        this.surfaceView = surfaceView;
    }

    @Override
    public View getDisplayView() {
        return surfaceView;
    }

    @Override
    public SurfaceHolder getHolder() {
        return surfaceView.getHolder();
    }

    @Override
    public void onStart(IMyPlayer player) {
        LogUtil.getInstance().d("MySimpleDisplay", "播放开始");
    }

    @Override
    public void onPause(IMyPlayer player) {
        LogUtil.getInstance().d("MySimpleDisplay", "播放暂停");
    }

    @Override
    public void onResume(IMyPlayer player) {
        LogUtil.getInstance().d("MySimpleDisplay", "播放恢复");
    }

    @Override
    public void onComplete(IMyPlayer player) {
        LogUtil.getInstance().d("MySimpleDisplay", "播放完毕");
    }
}

MediaPlayerHelper

重点类,基于MediaPlayer。
(1)初始化MediaPlayer

    /**
     * 创建MediaPlayer
     */
    private void createPlayer() {
        if (null == mediaPlayer) {
            mediaPlayer = new MediaPlayer();
            mediaPlayer.setScreenOnWhilePlaying(true);
            mediaPlayer.setOnBufferingUpdateListener(this);
            mediaPlayer.setOnVideoSizeChangedListener(this);
            mediaPlayer.setOnCompletionListener(this);
            mediaPlayer.setOnPreparedListener(this);
            mediaPlayer.setOnSeekCompleteListener(this);
            mediaPlayer.setOnErrorListener(this);
        }
    }

(2)播放相关API

    /**
     * 开始播放
     */
    private void playStart() {
        if (isVideoSizeMeasured && isMediaPrepared && isSurfaceCreated && isUserWantToPlay && isResumed) {
            mediaPlayer.setDisplay(display.getHolder());
            mediaPlayer.start();
            isCompleted = false;
            log("视频开始播放");
            display.onStart(this);
            if (myPlayerListener != null) {
                myPlayerListener.onStart(this);
            }
        }
    }

    /**
     * 暂停播放
     */
    private void playPause() {
        if (mediaPlayer != null && mediaPlayer.isPlaying()) {
            mediaPlayer.pause();
            display.onPause(this);
            if (myPlayerListener != null) {
                myPlayerListener.onPause(this);
            }
        }
    }

    /**
     * 停止播放,并释放资源
     */
    private void playStop() {
        if (mediaPlayer != null) {
            mediaPlayer.stop();
            mediaPlayer.release();
            mediaPlayer = null;
            display.onComplete(this);
            if (myPlayerListener != null) {
                myPlayerListener.onComplete(this);
            }
        }
    }
    /**
     * 播放前检查资源状态
     *
     * @return 播放资源是否准备好
     */
    private boolean checkSource() {
        return videoSource != null && videoSource.length() != 0;
    }
    /**
     * 视频状态
     *
     * @return 视频是否正在播放
     */
    public boolean isPlaying() {
        return mediaPlayer != null && mediaPlayer.isPlaying();
    }

    /**
     * 判断是否播放完毕
     */
    public boolean isCompleted() {
        return isCompleted;
    }

Activity状态绑定

    @Override
    public void onPause() {
        isResumed = false;
        playPause();
    }

    @Override
    public void onResume() {
        isResumed = true;
        playStart();
    }

    @Override
    public void onDestroy() {
        if (mediaPlayer != null) {
            mediaPlayer.release();
        }
    }

视频播放窗口大小相关

    /**
     * 设置是否裁剪视频,若裁剪,则视频按照DisplayView的父布局大小显示。
     * 若不裁剪,视频居中于DisplayView的父布局显示
     *
     * @param isCrop 是否裁剪视频
     */
    public void setCrop(boolean isCrop) {
        this.mIsCrop = isCrop;
        if (display != null && currentVideoWidth > 0 && currentVideoHeight > 0) {
            tryResetSurfaceSize(display.getDisplayView(), currentVideoWidth, currentVideoHeight);
        }
    }

    /**
     * 是否被裁剪
     */
    public boolean isCrop() {
        return mIsCrop;
    }
    /**
     * 根据设置和视频尺寸,调整视频播放区域的大小
     *
     * @param view        播放控件
     * @param videoWidth  视频宽度
     * @param videoHeight 视频高度
     */
    private void tryResetSurfaceSize(final View view, int videoWidth, int videoHeight) {
        ViewGroup parent = (ViewGroup) view.getParent();
        int width = parent.getWidth();
        int height = parent.getHeight();
        if (width > 0 && height > 0) {
            final RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) view.getLayoutParams();
            if (mIsCrop) {
                float scaleVideo = videoWidth / (float) videoHeight;
                float scaleSurface = width / (float) height;
                if (scaleVideo < scaleSurface) {
                    params.width = width;
                    params.height = (int) (width / scaleVideo);
                    params.setMargins(0, (height - params.height) / 2, 0, (height - params.height) / 2);
                } else {
                    params.height = height;
                    params.width = (int) (height * scaleVideo);
                    params.setMargins((width - params.width) / 2, 0, (width - params.width) / 2, 0);
                }
            } else {
                if (videoWidth > width || videoHeight > height) {
                    float scaleVideo = videoWidth / (float) videoHeight;
                    float scaleSurface = width / (float) height;
                    if (scaleVideo > scaleSurface) {
                        params.width = width;
                        params.height = (int) (width / scaleVideo);
                        params.setMargins(0, (height - params.height) / 2, 0, (height - params.height) / 2);
                    } else {
                        params.height = height;
                        params.width = (int) (height * scaleVideo);
                        params.setMargins((width - params.width) / 2, 0, (width - params.width) / 2, 0);
                    }
                }
            }
            view.setLayoutParams(params);
            view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
                @Override
                public void onGlobalLayout() {
                    view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                    // 通知大小改变
                    surfaceSizeChangeListener.sizeChange();
                }
            });
        }
    }

重点是tryResetSurfaceSize方法的实现,根据视频的大小和View的大小,判断纵横比
(1)View更长,则将视频横充满,高度居中(设置Margin)。
(2)View更宽,则将视频高充满,宽度居中(设置Margin)。
设置的Margin,如横充满View,则用横乘上视频的纵横比(videoHeight/videoWidth),View的高度减去该值,偏移除以2就可以,若View不是在顶部,则用Y坐标+这个值即可。

实现的MediaPlayer的接口

MediaPlayer.OnCompletionListener
    @Override
    public void onCompletion(MediaPlayer mp) {
        display.onComplete(this);
        if (myPlayerListener != null) {
            myPlayerListener.onComplete(this);
        }
        isCompleted = true;
    }
MediaPlayer.OnVideoSizeChangedListener
    @Override
    public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {
        log("视频大小被改变-> [" + width + "," + height + "]");
        if (width > 0 && height > 0) {
            this.currentVideoWidth = width;
            this.currentVideoHeight = height;
            tryResetSurfaceSize(display.getDisplayView(), width, height);
            isVideoSizeMeasured = true;
            playStart();
        }
    }
MediaPlayer.OnPreparedListener
    @Override
    public void onPrepared(MediaPlayer mp) {
        log("视频准备完成");
        isMediaPrepared = true;
        playStart();
    }
MediaPlayer.OnSeekCompleteListener
    @Override
    public void onSeekComplete(MediaPlayer mp) {
        log("定位完成");
    }
MediaPlayer.OnErrorListener
    @Override
    public boolean onError(MediaPlayer mp, int what, int extra) {
        log("OnError - Error code: " + what + " Extra code: " + extra);
        switch (what) {
            case -1004:
                log("MEDIA_ERROR_IO");
                break;
            case -1007:
                log("MEDIA_ERROR_MALFORMED");
                break;
            case 200:
                log("MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK");
                break;
            case 100:
                log("MEDIA_ERROR_SERVER_DIED");
                break;
            case -110:
                log("MEDIA_ERROR_TIMED_OUT");
                break;
            case 1:
                log("MEDIA_ERROR_UNKNOWN");
                break;
            case -1010:
                log("MEDIA_ERROR_UNSUPPORTED");
                break;
        }
        switch (extra) {
            case 800:
                log("MEDIA_INFO_BAD_INTERLEAVING");
                break;
            case 702:
                log("MEDIA_INFO_BUFFERING_END");
                break;
            case 701:
                log("MEDIA_INFO_METADATA_UPDATE");
                break;
            case 802:
                log("MEDIA_INFO_METADATA_UPDATE");
                break;
            case 801:
                log("MEDIA_INFO_NOT_SEEKABLE");
                break;
            case 1:
                log("MEDIA_INFO_UNKNOWN");
                break;
            case 3:
                log("MEDIA_INFO_VIDEO_RENDERING_START");
                break;
            case 700:
                log("MEDIA_INFO_VIDEO_TRACK_LAGGING");
                break;
        }
        return false;
    }

根据Stack Overflow上的Question以及CSDN博客,补充的错误代码。
注:如果播放不了且代码-1,请尝试降低播放视频的分辨率。

SurfaceHolder.Callback
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        if (display != null && holder == display.getHolder()) {
            isSurfaceCreated = true;
            //此举保证以下操作下,不会黑屏。(或许还是会有手机黑屏)
            //暂停,然后切入后台,再切到前台,保持暂停状态
            if (mediaPlayer != null) {
                mediaPlayer.setDisplay(holder);
                //不加此句360f4不会黑屏、小米note1会黑屏,其他机型未测
                mediaPlayer.seekTo(mediaPlayer.getCurrentPosition());
            }
            log("surface被创建");
            playStart();
        }
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        log("surface大小改变:" + "[" + width + "," + height + "]");
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        if (display != null && holder == display.getHolder()) {
            log("surface被销毁");
            isSurfaceCreated = false;
        }
    }

总结

(1)视频播放不变形,通过视频纵横比与View纵横比进行对应的改变。
(2)Activity的状态绑定,防止后台前台切换,程序出错。
(3)视频Seek只能定位关键帧,但MediaCoder可以设置I帧间隔为0,每一帧都是关键帧(视频较大),gitHub有修改的Seek库,利用FFmpeg实现精准定位。
(4)播放还可以用VideoView,但封装太死,不灵活。
(5)播放还可以用MediaCodec进行硬解码,然后YUV转RGB(慢)进行播放。
(6)视频播放也可以用TextureView或TextureSurface,但SurfaceView具有双缓冲,可能速度比较快。

源码地址:https://github.com/shen511460468/MediaPlayerOnCamera2

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值