Android多媒体之视频播放器(基于MediaPlayer)

7,558 阅读9分钟

零、前言

对于视频的播放,Android有内置的VideoView,用起来非常简单
本篇从自定义VideoView来封装MediaPlayer开始说起

<VideoView
    android:id="@+id/id_vv"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

---->[使用:PlayerActivity.kt]------------------------------------------------
id_vv.setMediaController(MediaController(this))
id_vv.setVideoPath("/sdcard/toly/sh.mp4")

本文聚焦
[1].自定义VideoView结合SurfaceView和MediaPlayer来播放视频
[2].使用媒体库的ContentProvider查询手机中视频,并列表显示
[3].更改视频的宽高以及适应横竖屏切换
[4].自定义控制界面以及倍速播放
[5].视频封面图(视频帧)的获取
[6].播放网络视频及seekBar的第二进度和缓存进度监听

一、简易版:MediaPlayer + SurfaceView + MediaController

角色:
MediaPlayer 视频处理器
SurfaceView 视频显示界面
MediaController 视频控制器


1.自定义VideoView继承自SurfaceView
/**
 * 作者:张风捷特烈<br/>
 * 时间:2019/3/8/008:12:43<br/>
 * 邮箱:1981462002@qq.com<br/>
 * 说明:视频播放:MediaPlayer + SurfaceView + MediaController
 */
public class VideoView extends SurfaceView implements MediaController.MediaPlayerControl {
    private SurfaceHolder mSurfaceHolder;//SurfaceHolder
    private MediaPlayer mMediaPlayer;//媒体播放器
    private MediaController mMediaController;//媒体控制器
    
    private int mVideoHeight;//视频宽高
    private int mVideoWidth;//视频高
    private int mSurfaceHeight;//SurfaceView高
    private int mSurfaceWidth;//SurfaceView宽
    
    private boolean isPrepared;//是否已准备好
    private Uri mUri;//播放的地址
    private int mCurrentPos;//当前进度
    private int mDuration = -1;//当前播放视频时长
    private int mCurrentBufferPer;//当前缓冲进度--网络
    
    public VideoView(Context context) {
        this(context, null);
    }
    public VideoView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }
    private void init() {
        setFocusable(true);
        setFocusableInTouchMode(true);
        requestFocus();
        getHolder().addCallback(new SurfaceHolder.Callback() {
            @Override
            public void surfaceCreated(SurfaceHolder holder) {
                mSurfaceHolder = holder;
                openVideo();
            }
            @Override
            public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
                mSurfaceHeight = height;
                mSurfaceWidth = width;
                if (mMediaPlayer != null && isPrepared) {
                    initPosition();
                    mMediaPlayer.start();//开始播放
                    showCtrl();
                }
            }
            @Override
            public void surfaceDestroyed(SurfaceHolder holder) {
                mSurfaceHolder = null;
                hideController();
                releasePlayer();
            }
        });
    }
    /**
     * 显示控制器
     */
    private void showCtrl() {
        if (mMediaController != null) {
            mMediaController.show();
        }
    }
    /**
     * 隐藏控制器
     */
    private void hideController() {
        if (mMediaController != null) {
            mMediaController.hide();
        }
    }
    /**
     * 初始化最初位置
     */
    private void initPosition() {
        if (mCurrentPos != 0) {
            mMediaPlayer.seekTo(mCurrentPos);
            mCurrentPos = 0;
        }
    }
    private void openVideo() {
        if (mUri == null || mSurfaceHolder == null) {
            return;
        }
        isPrepared = false;//没有准备完成
        releasePlayer();
        mMediaPlayer = new MediaPlayer();
        try {
            mMediaPlayer.setDataSource(getContext(), mUri);
            mMediaPlayer.setDisplay(mSurfaceHolder);
            mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
            mMediaPlayer.setScreenOnWhilePlaying(true);//播放时屏幕一直亮着
            mMediaPlayer.prepareAsync();//异步准备
            attach2Ctrl();//绑定媒体控制器
        } catch (IOException e) {
            e.printStackTrace();
        }
        //准备监听
        mMediaPlayer.setOnPreparedListener(mp -> {
            isPrepared = true;
            if (mMediaController != null) {//控制器可用
                mMediaController.setEnabled(true);
            }
            if (mOnPreparedListener != null) {//补偿回调
                mOnPreparedListener.onPrepared(mp);
            }
            mVideoWidth = mp.getVideoWidth();
            mVideoHeight = mp.getVideoHeight();
            if (mVideoWidth != 0 && mVideoHeight != 0) {
                getHolder().setFixedSize(mVideoWidth, mVideoHeight);
                //开始初始化
                initPosition();
                if (mSurfaceWidth == mVideoWidth && mSurfaceHeight == mVideoHeight) {
                    if (!isPlaying() && mCurrentPos != 0 || getCurrentPosition() > 0) {
                        if (mMediaController != null) {
                            mMediaController.show(0);
                        }
                    }
                }
            }
        });
        //尺寸改变监听
        mMediaPlayer.setOnVideoSizeChangedListener((mp, width, height) -> {
            mVideoWidth = mp.getVideoWidth();
            mVideoHeight = mp.getVideoHeight();
            if (mOnSizeChanged != null) {
                mOnSizeChanged.onSizeChange();
            }
            if (mVideoWidth != 0 && mVideoHeight != 0) {
                getHolder().setFixedSize(mVideoWidth, mVideoHeight);
            }
        });
        //完成监听
        mMediaPlayer.setOnCompletionListener(mp -> {
            hideController();
            start();
            if (mOnCompletionListener != null) {
                mOnCompletionListener.onCompletion(mp);
            }
        });
        //错误监听
        mMediaPlayer.setOnErrorListener((mp, what, extra) -> {
            hideController();
            if (mOnErrorListener != null) {
                mOnErrorListener.onError(mp, what, extra);
            }
            return true;
        });
        mMediaPlayer.setOnBufferingUpdateListener((mp, pre) -> {
            mCurrentBufferPer = pre;
        });
    }
    /**
     * 释放播放器
     */
    private void releasePlayer() {
        if (mMediaPlayer != null) {
            mMediaPlayer.reset();
            mMediaPlayer.release();
            mMediaPlayer = null;
        }
    }

    private void attach2Ctrl() {
        if (mMediaPlayer != null && mMediaController != null) {
            mMediaController.setMediaPlayer(this);
            View anchor = this.getParent() instanceof View ? (View) this.getParent() : this;
            mMediaController.setAnchorView(anchor);
            mMediaController.setEnabled(true);
        }
    }
    
    public void setVideoPath(String path) {
        mUri = Uri.parse(path);
        setVideoURI(mUri);
    }
    public void setVideoURI(Uri uri) {
        mUri = uri;
        mCurrentPos = 0;
        openVideo();//打开视频
        requestLayout();//更新界面
        invalidate();
    }
    
    public void setMediaController(MediaController mediaController) {
        hideController();
        mMediaController = mediaController;
        attach2Ctrl();
    }
    
    public void stopPlay() {
        if (mMediaPlayer != null) {
            mMediaPlayer.stop();
            mMediaPlayer.release();
            mMediaPlayer = null;
        }
    }
    private void toggle() {
        if (mMediaController.isShowing()) {
            mMediaController.hide();
        } else {
            mMediaController.show();
        }
    }
    private boolean canPlay() {
        return mMediaPlayer != null && isPrepared;
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (isPrepared && mMediaController != null && mMediaPlayer != null) {
            toggle();
        }
        return false;
    }
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int w = adjustSize(mVideoWidth, widthMeasureSpec);
        int h = adjustSize(mVideoHeight, heightMeasureSpec);
        setMeasuredDimension(w, h);
    }
    public int adjustSize(int size, int measureSpec) {
        int result = 0;
        int mode = MeasureSpec.getMode(measureSpec);
        int len = MeasureSpec.getMode(measureSpec);
        switch (mode) {
            case MeasureSpec.UNSPECIFIED:
                result = size;
                break;
            case MeasureSpec.AT_MOST:
                result = Math.min(size, len);
                break;
            case MeasureSpec.EXACTLY:
                result = len;
                break;
        }
        return result;
    }
    //----------------------------------------------------------------
    //------------MediaPlayerControl接口函数---------------------------
    //----------------------------------------------------------------
    @Override
    public void start() {
        if (canPlay()) {
            mMediaPlayer.start();
        }
    }
    @Override
    public void pause() {
        if (canPlay() && mMediaPlayer.isPlaying()) {
            mMediaPlayer.pause();
        }
    }
    @Override
    public int getDuration() {
        if (canPlay()) {
            if (mDuration > 0) {
                return mDuration;
            }
            mDuration = mMediaPlayer.getDuration();
            return mDuration;
        }
        mDuration = -1;
        return mDuration;
    }
    @Override
    public int getCurrentPosition() {
        if (canPlay()) {
            return mMediaPlayer.getCurrentPosition();
        }
        return 0;
    }
    @Override
    public void seekTo(int pos) {
        if (canPlay()) {
            mMediaPlayer.seekTo(pos);
        } else {
            mCurrentPos = pos;
        }
    }
    @Override
    public boolean isPlaying() {
        if (canPlay()) {
            return mMediaPlayer.isPlaying();
        }
        return false;
    }
    @Override
    public int getBufferPercentage() {
        if (canPlay()) {
            return mCurrentBufferPer;
        }
        return 0;
    }
    @Override
    public boolean canPause() {
        return true;
    }
    @Override
    public boolean canSeekBackward() {
        return true;
    }
    @Override
    public boolean canSeekForward() {
        return true;
    }
    @Override
    public int getAudioSessionId() {
        return 0;
    }
    //----------------------------------------------------------------
    //------------补偿回调---------------------------
    //----------------------------------------------------------------
    private MediaPlayer.OnPreparedListener mOnPreparedListener;
    private MediaPlayer.OnCompletionListener mOnCompletionListener;
    private MediaPlayer.OnErrorListener mOnErrorListener;
    public void setOnPreparedListener(MediaPlayer.OnPreparedListener onPreparedListener) {
        mOnPreparedListener = onPreparedListener;
    }
    public void setOnCompletionListener(MediaPlayer.OnCompletionListener onCompletionListener) {
        mOnCompletionListener = onCompletionListener;
    }
    public void setOnErrorListener(MediaPlayer.OnErrorListener onErrorListener) {
        mOnErrorListener = onErrorListener;
    }
    public interface OnSizeChanged {
        void onSizeChange();
    }
    private OnSizeChanged mOnSizeChanged;
    public void setOnSizeChanged(OnSizeChanged onSizeChanged) {
        mOnSizeChanged = onSizeChanged;
    }
}

2.根据路径使用测试

简单一点,可以用系统自带的控制器:MediaController,不过丑到爆炸
文件权限自理:<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

自带的默认控制器.png

---->[activity_main.xml]------------------------------------------------
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

    <com.toly1994.ivideo.widget.VideoView
            android:id="@+id/id_vv"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
</android.support.constraint.ConstraintLayout>


---->[使用:PlayerActivity.kt]------------------------------------------------
id_vv.setMediaController(MediaController(this))
id_vv.setVideoPath("/sdcard/toly/sh.mp4")

3.获取所有的视频并根据插入时间降序排列

查询所有的视频文件.png

/**
 * 作者:张风捷特烈<br/>
 * 时间:2018/10/30 0030:18:38<br/>
 * 邮箱:1981462002@qq.com<br/>
 * 说明:视频ContentProvide相关操作---生成视频List
 */
public class VideoScanner {
    static String[] projection = new String[]{
            MediaStore.Video.Media._ID,//ID
            MediaStore.Video.Media.TITLE,//名称
            MediaStore.Video.Media.DURATION,//时长
            MediaStore.Video.Media.DATA,//路径
            MediaStore.Video.Media.SIZE,//大小
            MediaStore.Video.Media.DATE_ADDED//添加的时间
    };
    /**
     * 歌曲集合
     */
    private static List<VideoInfo> videos = new ArrayList<>();
    /**
     * 读取音频
     */
    public static List<VideoInfo> loadVideo(final Context context) {
        if (videos.size() != 0) {
            return videos;
        }
        Cursor cursor = context.getContentResolver().query(
                MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
                projection, "", null,
                "date_added desc", null);
        // 根据字段获取数据库中数据的索引
        int songIdIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID);
        int titleIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE);
        int durationIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION);
        int dataUrlIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA);
        int sizeIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.SIZE);
        int addDateIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATE_ADDED);
        while (cursor.moveToNext()) {
                long videoId = cursor.getLong(songIdIdx);//获取id
                String title = cursor.getString(titleIdx);//获取名字
                String dataUrl = cursor.getString(dataUrlIdx);//获取路径
                long duration = cursor.getLong(durationIdx);//获取时长
                long size = cursor.getLong(sizeIdx);//获取大小
                long addDate = cursor.getLong(addDateIdx);//加入时间
                videos.add(new VideoInfo(videoId, title, dataUrl, duration, size, addDate));
        }
        return videos;
    }
}

4.RecyclerView装一下Video信息

关于封面预览图等会在倒腾,布局什么的就不贴了,自己写
当点击的时候,跳转到刚才的那个播放Activity,用Intent传递视频路径

列表展示.png

---->[HomeAdapter#onBindViewHolder]-------------------------------------------
holder.mIvCover.setOnClickListener(v -> {
    Intent intent = new Intent(mContext, PlayerActivity.class);
    intent.putExtra("video-path", videoInfo.getDataUrl());
    mContext.startActivity(intent);
});

---->[附赠一个视频时间转化的方法]----------------------------------------
private String format(long duration) {
    long time = duration / 1000;
    String result = "";
    long minus = time / 60;
    int hour = 0;
    if (minus > 60) {
        hour = (int) (minus / 60);
        minus = minus % 60;
    }
    long second = time % 60;
    if (hour < 60) {
        result = handleNum(hour) + ":" + handleNum(minus)+":"+handleNum(second);
    }
    return result;
}

private String handleNum(long num) {
    return num < 10 ? ("0" + num) : (num + "");
}

---->[PlayerActivity]-------------------------------------------
val path = intent.getStringExtra("video-path")
id_vv.setMediaController(MediaController(this))
id_vv.setUri(path)

OK 简易版的视频播放器就OK了。


二、界面横竖屏问题

这转个屏,D 都变成 A 了,怎么能忍,赶快修一下

屏幕转向问题.png


1.关于缩放
getHolder().setFixedSize(w,h)  测试了一下,然并卵,分辨率没有改变
|-- 来翻一下源码

/**
 * Make the surface a fixed size.  It will never change from this size.
 * When working with a {@link SurfaceView}, this must be called from the
 * same thread running the SurfaceView's window.
 * 使surface的大小固定。它的大小永远不会改变。
 * 当使用SurfaceView时,必须从运行SurfaceView窗口的同一线程调用它。
 * @param width The surface's width. surface宽
 * @param height The surface's height. surface高
 */
public void setFixedSize(int width, int height);

看来此路不通,那只能求他路


2.直接变更View的尺寸

直接变更View的尺寸.png

public void changeVideoFitSize(int videoW, int videoH, int surfaceW, int surfaceH) {
    float videoSizeRate = videoW * 1.0f / videoH;
    //横屏下的切换 -- 正常宽高比例
    float widthRatePortrait = videoW * 1.0f / surfaceW;
    float heightRatePortrait = videoH * 1.0f / surfaceH;
    //横屏下的切换 View宽高互换-- 宽高比例
    float widthRateLand = videoW * 1.0f / surfaceH;
    float heightRateLand = videoH * 1.0f / surfaceW;
    float ratio;
    if (getResources().getConfiguration().orientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) {//横屏
        //竖屏模式下
        ratio = Math.max(widthRatePortrait, heightRatePortrait);
    } else {
        //横屏模式下
        if (videoSizeRate > 1) {
            ratio = Math.min(widthRateLand, heightRateLand);
        } else {
            ratio = Math.max(widthRateLand, heightRateLand);
        }
    }
    //视频宽高分别/最大倍数值 计算出放大后的视频尺寸
    videoW = (int) Math.ceil(videoW * 1.0f / ratio);
    videoH = (int) Math.ceil(videoH * 1.0f / ratio);
    //根据将视频尺寸变更View
    RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(videoW, videoH);
    setLayoutParams(params);
}

|--- 使用:
---->[setOnVideoSizeChangedListener中]---------------------------------------------
changeVideoFitSize(mVideoWidth, mVideoHeight, mSurfaceWidth, mSurfaceHeight);

3.不满屏时居中

至于怎么居中,我天真的以为在xml里改一下就行了,but,并没用,因为这里是自己玩LayoutParams
所以居中也要用LayoutParams,没办法,走波源码呗。

---->[RelativeLayout#CENTER_IN_PARENT]---------------------
public static final int CENTER_IN_PARENT         = 13;

CENTER_IN_PARENT是一个int型控制的,看一下LayoutParams的源码,暴露的方法就那几个,
addRule恰只有一个int入参,应该就是它了

---->[RelativeLayout.LayoutParams#addRule(int)]---------------------
public void addRule(int verb) {
    addRule(verb, TRUE);
}

---->[.VideoView#changeVideoFitSize(int, int, int, int)]-------------
---- 轻轻写语句,即可
params.addRule(13);

居中设置.png

居中ok.png


3.自定义宽高缩放比例
public void changeVideoSize(float rateX, float rateY) {
    changeVideoFitSize(mVideoWidth, mVideoHeight, mSurfaceWidth, mSurfaceHeight, rateX, rateY);
}

public void changeVideoFitSize(
        int videoW, int videoH, int surfaceW, int surfaceH,
        float rateX, float rateY) {
    ...
    //视频宽高分别/最大倍数值 计算出放大后的视频尺寸
    videoW = (int) Math.ceil(videoW * 1.0f / ratio * rateX);
    videoH = (int) Math.ceil(videoH * 1.0f / ratio * rateY);
    //无法直接设置视频尺寸,将计算出的视频尺寸设置到surfaceView 让视频自动填充。
    RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(videoW, videoH);
    params.addRule(13);
    setLayoutParams(params);
}

设置比例.png


三、定制操作界面

1.界面操作

自定义的界面就是根据VideoView中的Api自己实现控制逻辑,细心一点还是不难的,就是麻烦
界面如下,不贴布局了,比较简单,也挺多的,这里说一下显示面板后5秒后隐藏的逻辑

控制界面.png

private val mHandler = Handler(Looper.getMainLooper())

root.setOnClickListener {//点击显示面板
    showPanel(mHandler)
}

private fun hidePanel() {
    id_ll_top.visibility = View.GONE
    id_ll_bottom.visibility = View.GONE
    id_iv_lock.visibility = View.GONE
}
private fun showPanel(handler: Handler) {
    id_ll_top.visibility = View.VISIBLE
    id_ll_bottom.visibility = View.VISIBLE
    id_iv_lock.visibility = View.VISIBLE
    handler.postDelayed(::hidePanel, 5000)
}

2.倍速播放

二倍速听mv挺搞笑的,API 23 + 也就是一句Api的事,很方便

/**
 * 变速
 * @param speed
 */
public void changeSpeed(float speed) {
    //API 23 + 支持
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        if (mMediaPlayer.isPlaying()) {
            mMediaPlayer.setPlaybackParams(mMediaPlayer.getPlaybackParams().setSpeed(speed));
        } else {
            mMediaPlayer.setPlaybackParams(mMediaPlayer.getPlaybackParams().setSpeed(speed));
            mMediaPlayer.pause();
        }
    }
}

|-- 使用数组来控制-----------------------
private var speeds = floatArrayOf(0.5f, 0.75f, 1f, 1.25f, 1.5f, 1.75f, 2.0f)
private var curSpeedIdx = 2

id_tv_speed.setOnClickListener {
    curSpeedIdx++
    if (curSpeedIdx == speeds.size) {
        curSpeedIdx = 0
    }
    val speed = speeds[curSpeedIdx]
    id_vv.changeSpeed(speed)
    id_tv_speed.text = "$speed X"
}

3.封面图的获取

获取帧.png

基本上也就这么多了,最后讲一下视频封面帧图片的获取:数了一下这帧大概在15秒
测试了一下秒数越大,获取图片的速度越慢,也就是越卡,所以还是给0吧
如果在Adapter里实时加载会很卡,最好查询的时候就把bitmap放到实体类里,由于封面图不要很大
别把原图给放进去了,小心直接OOM。Bitmap的操作本文就不赘述了。

图片尺寸.png

---->[HomeAdapter]------------------------
private final MediaMetadataRetriever retriever;    

retriever = new MediaMetadataRetriever();

/**
 * 获取视频某一帧
 *
 * @param path 路径
 * @param timeMs 毫秒
 */
public Bitmap decodeFrame(String path,long timeMs) {
    retriever.setDataSource(path);
    Bitmap bitmap = retriever.getFrameAtTime(timeMs * 1000, MediaMetadataRetriever.OPTION_CLOSEST);
    if (bitmap == null) {
        return null;
    }
    return bitmap;
}


此选项与{@link #getFrameAtTime(long,int)}一起使用,以检索与位于给定时间附近或给定时间的数据源相关联的帧(不一定是关键帧)。
 * This option is used with {@link #getFrameAtTime(long, int)} to retrieve
 * a frame (not necessarily a key frame) associated with a data source that
 * is located closest to or at the given time.
public static final int OPTION_CLOSEST          = 0x03;

此选项与{@link #getFrameAtTime(long,int)}一起使用,以检索与位于(时间上)最接近或给定时间的数据源相关联的同步(或键)帧。
 * This option is used with {@link #getFrameAtTime(long, int)} to retrieve
 * a sync (or key) frame associated with a data source that is located
 * closest to (in time) or at the given time.
public static final int OPTION_CLOSEST_SYNC     = 0x02;


此选项与{@link #getFrameAtTime(long,int)}一起使用,以检索与位于给定时间之后或指定时间的数据源关联的同步(或键)帧。
 * This option is used with {@link #getFrameAtTime(long, int)} to retrieve
 * a sync (or key) frame associated with a data source that is located
 * right after or at the given time.
public static final int OPTION_NEXT_SYNC        = 0x01;

此选项与{@link #getFrameAtTime(long,int)}一起使用,以检索与位于给定时间之前或指定时间的数据源关联的同步(或键)帧。
* This option is used with {@link #getFrameAtTime(long, int)} to retrieve
* a sync (or key) frame associated with a data source that is located
* right before or at the given time.
public static final int OPTION_PREVIOUS_SYNC    = 0x00;

获取帧.png


四、网络视频的播放


1.网络视频

放在服务器上了,地址:http://www.toly1994.com:8089/imgs/sh.mp4,就一句话

id_vv.setVideoPath("http://www.toly1994.com:8089/imgs/sh.mp4")

2.SeekBar的第二进度
---->[drawable/seekbar_bg.xml]--------------------------------------------
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@android:id/background">
        <shape>
            <solid android:color="#eee" />
        </shape>
    </item>
    <item android:id="@android:id/secondaryProgress">
        <clip>
            <shape>
                <solid android:color="#2db334"/>
            </shape>
        </clip>
    </item>
    <item android:id="@android:id/progress">
        <clip>
            <shape>
                <solid android:color="@color/colorAccent"/>
            </shape>
        </clip>
    </item>
</layer-list>

---->[layout/in_player_panel_bottom.xml]---------------------------
 <SeekBar
    ...
    android:progressDrawable="@drawable/seekbar_bg"

3.缓存监听
---->[com.toly1994.ivideo.widget.VideoView]------------------
mMediaPlayer.setOnBufferingUpdateListener((mp, pre) -> {
    mCurrentBufferPer = pre;
    if (mOnBufferingUpdateListener != null) {
        mOnBufferingUpdateListener.update(pre);
    }
});

public interface OnBufferingUpdateListener {
    void update(int pre);
}
private OnBufferingUpdateListener mOnBufferingUpdateListener;
public void setOnBufferingUpdateListener(OnBufferingUpdateListener onBufferingUpdateListener) {
    mOnBufferingUpdateListener = onBufferingUpdateListener;
}

使用:
id_vv.setOnBufferingUpdateListener {
    id_sb_progress.secondaryProgress = it
}

Ok 这样就完成了。本篇就这样,更多的功能可以自己去拓展,
搭个后台,弄个简单的网络播放器也未尝不可。


后记:捷文规范

1.本文成长记录及勘误表
项目源码日期备注
2018-3-9Android多媒体之视频播放器(基于MediaPlayer)
2.更多关于我
笔名QQ微信爱好
张风捷特烈1981462002zdl1994328语言
我的github我的简书我的掘金个人网站
3.声明

1----本文由张风捷特烈原创,转载请注明
2----欢迎广大编程爱好者共同交流
3----个人能力有限,如有不正之处欢迎大家批评指证,必定虚心改正
4----看到这里,我在此感谢你的喜欢与支持


icon_wx_200.png