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 == -1) {
            return -1;
        }

        int newSeekProgress = mSeekDialogStartProgress + mDelSeekDialogProgress;
        if (newSeekProgress <= 0) newSeekProgress = 0;
        if (newSeekProgress >= mDuration) newSeekProgress = mDuration;
        return newSeekProgress;
    }

    public void hide() {
        mDuration = -1;
        mSeekDialogStartProgress = -1;
        mDelSeekDialogProgress = -1;
        setVisibility(GONE);
    }

}

调节系统属性弹框JsVideoSystemOverlay就不再贴出代码了,与上类似,这里我们只分享设计思路。

注意:

  • mDelSeekDialogProgress -= delProgress,因为向右滑动时传进来的delProgress是负数、向左滑动是正数,所以这里计算变化时是在自减。
  • if (newSeekProgress <= 0) newSeekProgress = 0;
    if (newSeekProgress >= mDuration) newSeekProgress = mDuration;
    做了边界控制,防止计算出的数据超出范围而导致出错。

JsVideoErrorView

这里写图片描述

从界面来看很简单了!

定义所有错误码常量(可换为枚举):

// 正常状态
public static final int STATUS_NORMAL = 0;
// 普通一场
public static final int STATUS_VIDEO_DETAIL_ERROR = 1;
// 资源错误
public static final int STATUS_VIDEO_SRC_ERROR = 2;
// 无WIFI
public static final int STATUS_UN_WIFI_ERROR = 3;
// 无网络
public static final int STATUS_NO_NETWORK_ERROR = 4;

另外就是显示的控制:

switch (status) {
    case STATUS_VIDEO_DETAIL_ERROR:
        video_error_info.setText("视频加载失败");
        video_error_retry.setText("点此重试");
        break;
    case STATUS_VIDEO_SRC_ERROR:
        video_error_info.setText("视频加载失败");
        video_error_retry.setText("点此重试");
        break;
    case STATUS_NO_NETWORK_ERROR:
        video_error_info.setText("网络连接异常,请检查网络设置后重试");
        video_error_retry.setText("重试");
        break;
    case STATUS_UN_WIFI_ERROR:
        video_error_info.setText("温馨提示:您正在使用非WiFi网络,播放将产生流量费用");
        video_error_retry.setText("继续播放");
        break;
}

注意:对重试按钮的点击事件:错误view内置了视频控制回调OnVideoControlListener,点击重试时执行回调的重试按钮。

JsVideoControllerView

先看布局:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <include
        android:id="@+id/video_controller_bottom"
        layout="@layout/video_controller_bottom" />

    <ImageView
        android:id="@+id/player_lock_screen"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_marginLeft="10dp"
        android:src="@mipmap/video_unlock"
        android:visibility="gone" />

    <com.jia.jsplayer.view.JsVideoErrorView
        android:id="@+id/video_controller_error"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <include
        android:id="@+id/video_controller_title"
        layout="@layout/video_controller_title" />

    <ImageView
        android:id="@+id/video_back"
        android:layout_width="32dp"
        android:layout_height="44dp"
        android:layout_alignTop="@id/video_controller_title"
        android:padding="12dp"
        android:scaleType="fitCenter"
        android:src="@mipmap/ic_back_white" />

    <RelativeLayout
        android:id="@+id/rl_pre"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#000"
        android:visibility="gone">
        <ImageView
            android:id="@+id/iv_pre_play"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@mipmap/ic_video_play"
            android:layout_centerInParent="true"/>
    </RelativeLayout>
</RelativeLayout>

能够看出:主要分为底部控制部分(播放按钮、当前位置、总时长、进度条),头部控制部分(返回键、标题),出错界面、锁屏按钮和填充全屏的默认图。

对控制器来说,我们应该关心这些:
这里写图片描述

首先我们必须传入MediaPlayer对象(关于它的封装稍后会详细介绍),因为我们各点击事件和拖动事件都在控制MediaPlayer,如播放按钮的点击事件,在控制视频的播放与暂停,进度条拖动完时,应控制视频定位 等。

注意:

  • 对UI的更新全部提出方法,方便其他地方调用
  • 是否锁屏、控制器显示时长等都应有默认值
  • 显示控制器时,视频当前位置和更新的精度
  • 对全屏按钮、重试按钮的点击交给OnVideoControlListener来做

我想着重讲一下控制器的隐藏和显示

1、控制器一显示,就获取MediaPlayer的当前位置,更新UI(进度条,当前播放位置),并将当前位置返回:

    /**
     * 设置进度,同时也返回进度
     *
     * @return
     */
    private int setProgress() {
        if (mPlayer == null || mDragging) {
            return 0;
        }
        int position = mPlayer.getCurrentPosition();
        int duration = mPlayer.getDuration();
        if (mPlayerSeekBar != null) {
            if (duration > 0) {
                // use long to avoid overflow
                long pos = 1000L * position / duration;
                mPlayerSeekBar.setProgress((int) pos);
            }
            // 设置缓冲进度
            int percent = mPlayer.getBufferPercentage();
            mPlayerSeekBar.setSecondaryProgress(percent * 10);
        }

        mVideoProgress.setText(StringUtils.stringForTime(position));
        mVideoDuration.setText(StringUtils.stringForTime(duration));

        return position;
    }

2、控制各UI布局显示,开始发送消息

    /**
     * 显示控制器
     *
     * @param timeout 显示时长
     */
    public void show(int timeout) {
        setProgress();

        if (!isScreenLock) {
            mControllerBack.setVisibility(VISIBLE);
            mControllerTitle.setVisibility(VISIBLE);
            mControllerBottom.setVisibility(VISIBLE);
        } else {
            if (!DisplayUtils.isPortrait(getContext())) {
                mControllerBack.setVisibility(GONE);
            }
            mControllerTitle.setVisibility(GONE);
            mControllerBottom.setVisibility(GONE);
        }

        if (!DisplayUtils.isPortrait(getContext())) {
            mScreenLock.setVisibility(VISIBLE);
        }

        mShowing = true;

        updatePausePlay();

        // 开始显示
        post(mShowProgress);

        if (timeout > 0) {
            // 先移除之前的隐藏异步操作
            removeCallbacks(mFadeOut);
            //timeout后隐藏
            postDelayed(mFadeOut, timeout);
        }
    }
    /**
     * 异步操作隐藏
     */
    private final Runnable mFadeOut = new Runnable() {
        @Override
        public void run() {
            hide();
        }
    };

    /**
     * 异步操作显示
     */
    private final Runnable mShowProgress = new Runnable() {
        @Override
        public void run() {
            int pos = setProgress();
            if (!mDragging && mShowing && mPlayer.isPlaying()) {
                // 解决1秒之内的误差,使得发送消息正好卡在整秒
                Log.e("TAG", "run: " + (1000 - (pos % 1000)));
                postDelayed(mShowProgress, 1000 - (pos % 1000));
            }
        }
    };
  • 首先注意,每当开始发送消息,都应强制将之前的消息全部移除;
  • 发送两个消息:一个是计时的消息,每隔大约一秒获取当前位置并且更新UI,另一个是延迟显示时长后隐藏控制器;
  • 为什么每隔大约1秒更新一次UI呢,postDelayed(mShowProgress, 1000 - (pos % 1000)); 我做了一个修正操作,因为各消息可能会互相影响,其次就是发送消息时没有卡在视频的整秒位置上,而我们确实整1秒发送一条消息,会导致误差!

如果大家还想了解其他功能,可以去github阅读我的源码https://github.com/shuaijia/JsPlayer


5、MediaPlayer封装

主要封装了

  • openVideo:播放视频,处理各回调
  • start:开始播放
  • pause:暂停播放
  • seekTo:定位到
  • reset:视频重置
  • stop:停止播放
  • isPlaying:是否正在播放
  • getDuration:获取总时长
  • getCurrentPosition:获取当前进度
  • getBufferPercentage:获取缓冲进度 等

定义了视频播放的所用状态值常量

    //出错状态
    public static final int STATE_ERROR = -1;
    //通常状态
    public static final int STATE_IDLE = 0;
    //视频正在准备
    public static final int STATE_PREPARING = 1;
    //视频已经准备好
    public static final int STATE_PREPARED = 2;
    //视频正在播放
    public static final int STATE_PLAYING = 3;
    //视频暂停
    public static final int STATE_PAUSED = 4;
    //视频播放完成
    public static final int STATE_PLAYBACK_COMPLETED = 5;
    // 播放核心使用MediaPlayer
    private MediaPlayer player;
    // 当前状态
    private int curState = STATE_IDLE;
    // 当前缓冲进度
    private int currentBufferPercentage;
    // *视频路径
    private String path;

    // 播放监听
    private OnPlayerCallback onPlayerListener;
    // 播放视频承载的view
    private SurfaceHolder surfaceHolder;

封装了视频播放状态的判断

    public boolean isInPlaybackState() {
        return (player != null &&
                curState != STATE_ERROR &&
                curState != STATE_IDLE &&
                curState != STATE_PREPARING);
    }

此方法会在其他的所有方法执行之前判断,如果返回false,则不进行开始播放、重新播放、拖动定位等操作。

同时这些操作执行完后都会更新当前播放状态,防止视频不能播的情况下操作报错。如

    /**
     * 开始播放
     */
    public void start() {
        if (isInPlaybackState()) {
            player.start();
            setCurrentState(STATE_PLAYING);
        }
    }

在openVideo中:

    public void openVideo() {
        if (path == null || surfaceHolder == null) {
            return;
        }

        reset();

        player = new MediaPlayer();

        // 准备好的监听
        player.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
            @Override
            public void onPrepared(MediaPlayer mp) {
                //因为后面播放时要判断当前视频状态,所以在此一定要先将状态改变为STATE_PREPARED
                //即已经准备好,否则在第一次打开视频时无法自动播放
                setCurrentState(STATE_PREPARED);
                if (onPlayerListener != null) {
                    onPlayerListener.onPrepared(mp);
                }

            }
        });
        // 缓冲监听
        player.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() {
            @Override
            public void onBufferingUpdate(MediaPlayer mp, int percent) {
                if (onPlayerListener != null) {
                    onPlayerListener.onBufferingUpdate(mp, percent);
                }
                currentBufferPercentage = percent;
            }
        });
        // 播放完成监听
        player.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
            @Override
            public void onCompletion(MediaPlayer mp) {
                if (onPlayerListener != null) {
                    onPlayerListener.onCompletion(mp);
                }
                setCurrentState(STATE_PLAYBACK_COMPLETED);
            }
        });
        // 信息监听
        player.setOnInfoListener(new MediaPlayer.OnInfoListener() {
            @Override
            public boolean onInfo(MediaPlayer mp, int what, int extra) {
                if (onPlayerListener != null) {
                    // 701 加载中
                    if (what == MediaPlayer.MEDIA_INFO_BUFFERING_START) {
                        onPlayerListener.onLoadingChanged(true);
                        // 702 加载完成
                    } else if (what == MediaPlayer.MEDIA_INFO_BUFFERING_END) {
                        onPlayerListener.onLoadingChanged(false);
                    }
                }
                return false;
            }
        });
        // 出错监听
        player.setOnErrorListener(onErrorListener);
        // 视频大小切换监听
        player.setOnVideoSizeChangedListener(new MediaPlayer.OnVideoSizeChangedListener() {
            @Override
            public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {
                if (onPlayerListener != null) {
                    onPlayerListener.onVideoSizeChanged(mp, width, height);
                }
            }
        });

        currentBufferPercentage = 0;
        try {
            /**
             * 在这里开始真正的播放
             */
            player.setDataSource(path);
            player.setDisplay(surfaceHolder);
            player.setAudioStreamType(AudioManager.STREAM_MUSIC);
            player.setScreenOnWhilePlaying(true);
            player.prepareAsync();
            Log.e(TAG, "openVideo: " );
            setCurrentState(STATE_PREPARING);
        } catch (Exception e) {
            Log.e(TAG, "openVideo: " + e.toString());
            setCurrentState(STATE_ERROR);
            onErrorListener.onError(player, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
        }
    }

openVideo就是播放视频的核心方法:新建MediaPlayer对象;将视频播放的各回调交给OnPlayerCallback处理;将外部传进来的SurfaceHolder设置给MediaPlayer,并且prepareAsync之后就可以播放了,当然,不要忘了更新状态!

SurfaceHolder是surface的抽象接口,使你可以控制surface的大小和格式, 以及在surface上编辑像素,和监视surace的改变。
SurfaceHolder用于显示视频流解析的帧图片,如果发现SurfaceView变为用户不可见的时候,则立即销毁SurfaceView的SurfaceHolder,以达到节约系统资源的目的。


6、手势控制

说到手势控制,主要是手势控制视频进度,手势控制音量和屏幕亮度。

对于手势控制,我自定义了BehaviorView:让其实现GestureDetector的OnGestureListener

public class VideoBehaviorView extends FrameLayout implements GestureDetector.OnGestureListener{

在此view中定义以下方法,实现更新UI,交由子类去复写

    // 更新进度UI,由子类重写
    protected void updateSeekUI(int delProgress) {
        // sub
    }

    // 更新音量UI,由子类重写
    protected void updateVolumeUI(int max, int progress) {
        // sub
    }

    // 更新亮度UI,由子类重写
    protected void updateLightUI(int max, int progress) {
        // sub
    }

我的思路是将view的触摸事件全部交给GestureDetector处理

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mGestureDetector.onTouchEvent(event);
        switch (event.getAction() & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_OUTSIDE:
            case MotionEvent.ACTION_CANCEL:
                endGesture(mFingerBehavior);
                break;
        }
        return true;
    }

当手指按下时,重置手指行为,获取当前音量、亮度

    @Override
    public boolean onDown(MotionEvent e) {
        //重置 手指行为
        mFingerBehavior = -1;
        mCurrentVolume = am.getStreamVolume(AudioManager.STREAM_MUSIC);
        try {
            mCurrentBrightness = (int) (activity.getWindow().getAttributes().screenBrightness * mMaxBrightness);
        } catch (Exception exception) {
            exception.printStackTrace();
        }
        return false;
    }

在onScroll方法中:

判断决定当前为何种类型手势:左右滑动为调节进度,左半屏上下滑动为调节亮度,右半屏上下滑动为调节音量

        /**
         * 根据手势起始2个点断言 后续行为. 规则如下:
         *  屏幕切分为:
         *  1.左右扇形区域为视频进度调节
         *  2.上下扇形区域 左半屏亮度调节 后半屏音量调节.
         */
        if (mFingerBehavior < 0) {
            float moveX = e2.getX() - e1.getX();
            float moveY = e2.getY() - e1.getY();
            // 如果横向滑动距离大于纵向滑动距离,则认为在调节进度
            if (Math.abs(moveX) >= Math.abs(moveY))
                mFingerBehavior = FINGER_BEHAVIOR_PROGRESS;
                // 否则为调节音量或亮度
                // 按下位置在屏幕左半边,则是调节亮度
            else if (e1.getX() <= width / 2) mFingerBehavior = FINGER_BEHAVIOR_BRIGHTNESS;
                // 按下位置在屏幕右半边,则是在调节音量
            else mFingerBehavior = FINGER_BEHAVIOR_VOLUME;
        }

手势处理

        switch (mFingerBehavior) {
            case FINGER_BEHAVIOR_PROGRESS: { // 进度变化
                // 默认滑动一个屏幕 视频移动八分钟.
                int delProgress = (int) (1.0f * distanceX / width * 480 * 1000);
                // 更新快进弹框
                updateSeekUI(delProgress);
                break;
            }
            case FINGER_BEHAVIOR_VOLUME: { // 音量变化
                float progress = mMaxVolume * (distanceY / height) + mCurrentVolume;

                // 控制调节临界范围
                if (progress <= 0) progress = 0;
                if (progress >= mMaxVolume) progress = mMaxVolume;

                am.setStreamVolume(AudioManager.STREAM_MUSIC, Math.round(progress), 0);
                updateVolumeUI(mMaxVolume, Math.round(progress));
                // 更新当前值
                mCurrentVolume = progress;
                break;
            }
            case FINGER_BEHAVIOR_BRIGHTNESS: { // 亮度变化
                try {
                    // 如果系统亮度为自动调节,则改为手动调节
                    if (Settings.System.getInt(getContext().getContentResolver(), Settings.System.SCREEN_BRIGHTNESS_MODE)
                            == Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC) {
                        Settings.System.putInt(getContext().getContentResolver(), Settings.System.SCREEN_BRIGHTNESS_MODE,
                                Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL);
                    }

                    int progress = (int) (mMaxBrightness * (distanceY / height) + mCurrentBrightness);

                    // 控制调节临界范围
                    if (progress <= 0) progress = 0;
                    if (progress >= mMaxBrightness) progress = mMaxBrightness;

                    Window window = activity.getWindow();
                    WindowManager.LayoutParams params = window.getAttributes();
                    params.screenBrightness = progress / (float) mMaxBrightness;
                    window.setAttributes(params);

                    updateLightUI(mMaxBrightness, progress);
                    // 更新当前值
                    mCurrentBrightness = progress;
                } catch (Exception e) {
                    e.printStackTrace();
                }
                break;
            }
        }

注意:

  • 所有的更新UI操作全部交由子类实现
  • 注意临界范围的控制
  • 控制进度时,百分比最后乘以8分钟,以达到较为适中的用户体验,防止视频时长过大或太小情况下,拖动调节进度变化太过明显或效果不明显。

7、播放器JsPlayer封装

先来看看布局

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <SurfaceView
        android:id="@+id/video_surface"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <com.jia.jsplayer.view.JsVideoControllerView
        android:id="@+id/video_controller"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

    <include
        android:id="@+id/video_loading"
        layout="@layout/video_controller_loading" />

    <com.jia.jsplayer.view.JsVideoSystemOverlay
        android:id="@+id/video_system_overlay"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:visibility="gone"/>

    <com.jia.jsplayer.view.JsVideoProgressOverlay
        android:id="@+id/video_progress_overlay"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:visibility="gone"/>

</RelativeLayout>

JsPlayer视频播放器集成自上一步中的VideoBehaviorView,注意复写VideoBehaviorView的更新UI方法。

    private SurfaceView surfaceView;
    private View loadingView;
    private JsVideoProgressOverlay progressView;
    private JsVideoSystemOverlay systemView;
    private JsVideoControllerView mediaController;

    private JsMediaPlayer mMediaPlayer;

内置封装过得JsMediaPlayer 对象,控制器、和SurfaceView,还有网络状态广播接收器。

初始化player,创建JsMediaPlayer对象,设置视频播放回调处理,然后将其设置给ControllerView。

注意:

  • 在准备好的监听中,mediaPlayer执行开始播放,控制器展示,错误界面隐藏。
  • 在播放出错时控制器检查错误类型并展示
  • 在加载状态发生改变时隐藏和展示加载中
    private void initPlayer() {
        mMediaPlayer = new JsMediaPlayer();

        // todo 这里可以优化,将这些回调全部暴露出去
        mMediaPlayer.setOnPlayerListener(new OnPlayerCallback() {
            @Override
            public void onPrepared(MediaPlayer mp) {
                Log.e(TAG, "onPrepared: " );
                mMediaPlayer.start();
                mediaController.show();
                mediaController.hideErrorView();
            }

            @Override
            public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {

            }

            @Override
            public void onBufferingUpdate(MediaPlayer mp, int percent) {

            }

            @Override
            public void onCompletion(MediaPlayer mp) {
                mediaController.updatePausePlay();
            }

            @Override
            public void onError(MediaPlayer mp, int what, int extra) {
                mediaController.checkShowError(false);
            }

            @Override
            public void onLoadingChanged(boolean isShow) {
                if (isShow) showLoading();
                else hideLoading();
            }

            @Override
            public void onStateChanged(int curState) {
                switch (curState) {
                    case JsMediaPlayer.STATE_IDLE:
                        am.abandonAudioFocus(null);
                        break;
                    case JsMediaPlayer.STATE_PREPARING:
                        am.requestAudioFocus(null, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
                        break;
                }
            }
        });

        mediaController.setMediaPlayer(mMediaPlayer);
    }

给SurfaceView设置Callback,返回SurfaceHolder后设置给JsMediaPlayer

        surfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
            @Override
            public void surfaceCreated(SurfaceHolder holder) {
                Log.e(TAG, "surfaceCreated: " );
                initWidth = getWidth();
                initHeight = getHeight();

                if (mMediaPlayer != null) {
                    mMediaPlayer.setSurfaceHolder(holder);
                }
            }

            @Override
            public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

            }

            @Override
            public void surfaceDestroyed(SurfaceHolder holder) {

            }
        });

设置路径,开始播放

    public void setPath(final IVideoInfo video) {
        if (video == null) {
            return;
        }

        mMediaPlayer.reset();

        String videoPath = video.getVideoPath();
        mediaController.setVideoInfo(video);
        mMediaPlayer.setPath(videoPath);

    }

    public void startPlay(){
        mMediaPlayer.openVideo();
    }

更新UI

    @Override
    protected void updateSeekUI(int delProgress) {
        progressView.show(delProgress, mMediaPlayer.getCurrentPosition(), mMediaPlayer.getDuration());
    }

    @Override
    protected void updateVolumeUI(int max, int progress) {
        systemView.show(JsVideoSystemOverlay.SystemType.VOLUME, max, progress);
    }

    @Override
    protected void updateLightUI(int max, int progress) {
        systemView.show(JsVideoSystemOverlay.SystemType.BRIGHTNESS, max, progress);
    }

当然不会忘记封装播放、暂停、停止、定位、获取总时长等等的基本方法,这里就不再累赘。


8、使用

涉及到播放网路视频,权限少不了

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

播放本地视频别忘了6.0权限适配

布局中添加

代码中

        player = (JsPlayer) findViewById(R.id.player);

        player.setOnVideoControlListener(new OnVideoControlListener() {
            @Override
            public void onStartPlay() {
                player.startPlay();
            }

            @Override
            public void onBack() {

            }

            @Override
            public void onFullScreen() {
                DisplayUtils.toggleScreenOrientation(MainActivity.this);
            }

            @Override
            public void onRetry(int errorStatus) {

            }
        });

        player.setPath(new VideoInfo("艺术人生", path));

生命周期绑定

    @Override
    protected void onStop() {
        super.onStop();
        player.onStop();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        player.onDestroy();
    }

全屏操作


    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
            getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
        } else if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
            getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
        }
    }

    @Override
    public void onBackPressed() {
        if (!DisplayUtils.isPortrait(this)) {
            if (!player.isLock()) {
                DisplayUtils.toggleScreenOrientation(this);
            }
        } else {
            super.onBackPressed();
        }
    }

注意所在Activity在清单文件中应设置android:configChanges=”orientation|keyboardHidden|screenSize”

这样就ok了,播放器封装完美完成!

希望对大家有所帮助!

这里写图片描述

扫描二维码,加入我们,获取更多资讯!

猜你喜欢

转载自blog.csdn.net/jiashuai94/article/details/78020994
今日推荐