SurfaceView+MediaPlayer封装之路

SurfaceView+MediaPlayer封装之路

我的播放器叫做JsPlayer,喜欢的话,就给个star喽^_^https://github.com/shuaijia/JsPlayer

这里我只介绍播放器封装思路,会贴出部分代码,如果大家想查看完整代码,可以去github查看,有不清楚或错误或改进的地方,可以issues 我!

写在之前

先上效果图:(1.5版本新增弹幕功能

这里写图片描述

这里写图片描述

这里写图片描述

为什么要用SurfaceView

它继承自类View,因此它本质上是一个View。但与普通View不同的是,它有自己的Surface。而SurfaceView自带一个Surface,这个Surface在WMS中有自己对应的WindowState,在SF中也会有自己的Layer。虽然在App端它仍在View hierachy中,但在Server端(WMS和SF)中,它与宿主窗口是分离的。这样的好处是对这个Surface的渲染可以放到单独线程去做,渲染时可以有自己的GL context。这对于一些游戏、视频等性能相关的应用非常有益,因为它不会影响主线程对事件的响应。

SurfaceView内部自己持有surface,surface 创建、销毁、大小改变时系统来处理的,通过surfaceHolder 的callback回调通知。当画布创建好时,可以将surface绑定到MediaPlayer中。SurfaceView如果为用户可见的时候,创建SurfaceView的SurfaceHolder用于显示视频流解析的帧图片,如果发现SurfaceView变为用户不可见的时候,则立即销毁SurfaceView的SurfaceHolder,以达到节约系统资源的目的。

关于更多SurfaceView的介绍,可参考我写的另一片文章:http://blog.csdn.net/jiashuai94/article/details/77882644

MediaPlayer

MediaPlayer其实是一个封装的很好的音频、视频流媒体操作类,如果查看其源码,会发现其内部是调用的native方法,所以它其实是有C++实现的。既然是一个流媒体操作类,那么必然涉及到,播放、暂停、停止等操作,实际上MediaPlayer也为我们提供了相应的方法来直接操作流媒体。

  • void statr():开始或恢复播放。
  • void stop():停止播放。
  • void pause():暂停播放。 
  • void setDataSource(String path):通过一个媒体资源的地址指定MediaPlayer的数据源,这里的path可以是一个本地路径,也可以是网络路径。

当然还有其他很多的方法,例如获取视频时长、获取当前位置、定位到某个位置等等方法,就不再一一列举,阅读JsPlayer的源码便会有所了解。

播放器结构

这里写图片描述

UML图

这里写图片描述


已经对SurfaceView+MediaPlayer封装视屏播放器有了大致的了解,接下来就开始视屏播放器的封装之旅吧!

1、工具类

工欲善其事,必先利其器!

想封装结构清晰,使用方便的视频播放器,工具类是少不了的!JsPlayer主要用了以下几个工具类:

  • DisplayUtils
  • NetworkUtils
  • StringUtils

DisplayUtils:负责界面展示相关工具,例如px、dp、sp的相互转换;获取屏幕宽高度;切换横屏、竖屏等;

NetworkUtils:判断手机是否联网;是否为wifi;是否是流量;网络状态等;

StringUtils:主要将long型毫秒转换为时间格式的字符串。
代码就不贴了,很简单。大家想了解,去github中查看吧。


2、实体类

为了在使用视频播放器时规范传入的数据,同时也方便使用者调用和封装,故定义了视频详情的接口:其包含两个抽象方法,分别返回视频地址和视频标题。

/**
 * 视频数据类
 * 请实现本接口
 */
public interface IVideoInfo extends Serializable {
   

    /**
     * 视频标题
     */
    String getVideoTitle();

    /**
     * 视频播放路径(本地或网络)
     */
    String getVideoPath();

}

用户可根据项目实际情况对其进行扩展(需实现此接口即可),比如默认图地址,点赞数,是否购买,弹幕信息等等。但视频标题和视频地址必须返回


3、回调相关

大家都知道,VideoView或其他视频播放器在使用时,有准备好监听、播放完成监听、错误监听等等,可供开发者在对应情况进行对应处理;而且我们有时也需要在用户点击播放暂停、全屏、拖动进度条等情况下获得操作回调。因此,我们封装了两个回调接口:

  • OnVideoControlListener:视频控制回调
  • OnPlayerCallback:视频状态回调
/**
 * 视频控制监听
 */
public interface OnVideoControlListener {
   

    /**
     * 开始播放按钮
     */
    void onStartPlay();

    /**
     * 返回
     */
    void onBack();

    /**
     * 全屏
     */
    void onFullScreen();

    /**
     * 错误后的重试
     */
    void onRetry(int errorStatus);

}
/**
 * 视频操作回调,是将系统MediaPlayer的常见回调封装
 */
public interface OnPlayerCallback {
   

    /**
     * 准备好
     */
    void onPrepared(MediaPlayer mp);

    /**
     * 视频size变化
     */
    void onVideoSizeChanged(MediaPlayer mp, int width, int height);

    /**
     * 缓存更新变化
     *
     * @param percent 缓冲百分比
     */
    void onBufferingUpdate(MediaPlayer mp, int percent);

    /**
     * 播放完成
     */
    void onCompletion(MediaPlayer mp);

    /**
     * 视频错误
     * @param what  错误类型
     * @param extra 特殊错误码
     */
    void onError(MediaPlayer mp, int what, int extra);

    /**
     * 视频加载状态变化
     *
     * @param isShow 是否显示loading
     */
    void onLoadingChanged(boolean isShow);

    /**
     * 视频状态变化
     */
    void onStateChanged(int curState);
}

当然了,各位使用上述两个回调时,必须先实现、再使用,当然也可以基于它拓展了!


4、自定义view

关于播放器中涉及到的、需要自定义的view主要有手势调节进度、音量、亮度时的弹框、控制器界面、错误界面。

当然我们的JsPlayer视频播放器也是一自定义view,其手势控制也封装了一个view,这些我们稍后会详细介绍。

  • JsVideoProgressOverlay: 调节进度 框
  • JsVideoSystemOverlay: 调节音量、亮度 框
  • JsVideoErrorView: 错误界面
  • JsVideoControllerView: 控制器

我的思路是这样的:将错误界面JsVideoErrorView再封装到控制器中JsVideoControllerView,这样便于在出错时的处理;而调节进度等弹框、控制器,当然还有SurfaceView,加载中等,它们会一同封装到视频播放器JsPlayer的自定义View中。

JsVideoProgressOverlay

这里写图片描述

/**
 * 滑动快进快退进度框
 */
public class JsVideoProgressOverlay extends FrameLayout {
   

    private ImageView mSeekIcon;
    private TextView mSeekCurProgress;
    private TextView mSeekDuration;

    private int mDuration = -1;
    private int mDelSeekDialogProgress = -1;
    private int mSeekDialogStartProgress = -1;

    public JsVideoProgressOverlay(Context context) {
        super(context);
        init();
    }

    public JsVideoProgressOverlay(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public JsVideoProgressOverlay(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        LayoutInflater.from(getContext()).inflate(R.layout.video_overlay_progress, this);

        mSeekIcon = (ImageView) findViewById(R.id.iv_seek_direction);
        mSeekCurProgress = (TextView) findViewById(R.id.tv_seek_current_progress);
        mSeekDuration = (TextView) findViewById(R.id.tv_seek_duration);
    }

    /**
     * 显示进度框
     *
     * @param delProgress 进度变化值
     * @param curPosition player当前进度
     * @param duration    player总长度
     */
    public void show(int delProgress, int curPosition, int duration) {
        if (duration <= 0) return;

        // 获取第一次显示时的开始进度
        if (mSeekDialogStartProgress == -1) {
            Log.i("DDD", "show: start seek = " + mSeekDialogStartProgress);
            mSeekDialogStartProgress = curPosition;
        }

        if (getVisibility() != View.VISIBLE) {
            setVisibility(View.VISIBLE);
        }

        mDuration = duration;
        mDelSeekDialogProgress -= delProgress;
        int targetProgress = getTargetProgress();

        if (delProgress > 0) {
            // 回退
            mSeekIcon.setImageResource(R.mipmap.ic_video_back);
        } else {
            // 前进
            mSeekIcon.setImageResource(R.mipmap.ic_video_speed);
        }
        mSeekCurProgress.setText(StringUtils.stringForTime(targetProgress));
        mSeekDuration.setText(StringUtils.stringForTime(mDuration));
    }

    /**
     * 获取滑动结束后的目标进度
     */
    public int getTargetProgress() {
        if (mDuration == -
  • 7
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值