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

栏目: IOS · Android · 发布时间: 5年前

内容简介:对于视频的播放,Android有内置的VideoView,用起来非常简单本篇从自定义VideoView来封装MediaPlayer开始说起简单一点,可以用系统自带的控制器:MediaController,不过丑到爆炸

对于视频的播放,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].视频封面图(视频帧)的获取
复制代码

一、简易版:MediaPlayer + SurfaceView + MediaController

角色:
MediaPlayer 视频处理器
SurfaceView 视频显示界面
MediaController 视频控制器
复制代码
Android多媒体之视频播放器(基于MediaPlayer)

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"/>

Android多媒体之视频播放器(基于MediaPlayer)
---->[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.获取所有的视频并根据插入时间降序排列

Android多媒体之视频播放器(基于MediaPlayer)
/**
 * 作者:张风捷特烈<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传递视频路径

Android多媒体之视频播放器(基于MediaPlayer)
---->[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)
复制代码
Android多媒体之视频播放器(基于MediaPlayer)

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

二、界面横竖屏问题

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

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

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的尺寸

Android多媒体之视频播放器(基于MediaPlayer)
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);
复制代码
Android多媒体之视频播放器(基于MediaPlayer)
Android多媒体之视频播放器(基于MediaPlayer)

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);
}
复制代码
Android多媒体之视频播放器(基于MediaPlayer)

三、定制操作界面

1.界面操作

自定义的界面就是根据VideoView中的Api自己实现控制逻辑,细心一点还是不难的,就是麻烦

界面如下,不贴布局了,比较简单,也挺多的,这里说一下显示面板后5秒后隐藏的逻辑

Android多媒体之视频播放器(基于MediaPlayer)
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.封面图的获取

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

基本上也就这么多了,最后讲一下视频封面帧图片的获取:数了一下这帧大概在15秒

测试了一下秒数越大,获取图片的速度越慢,也就是越卡,所以还是给0吧

如果在Adapter里实时加载会很卡,最好查询的时候就把bitmap放到实体类里,由于封面图不要很大

别把原图给放进去了,小心直接OOM。Bitmap的操作本文就不赘述了。

Android多媒体之视频播放器(基于MediaPlayer)
---->[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;
复制代码
Android多媒体之视频播放器(基于MediaPlayer)

Ok 本篇就这样,更多的功能可以自己去拓展

后记:捷文规范

1.本文成长记录及勘误表

项目源码 日期 备注
2018-3-9 Android多媒体之视频播放器(基于MediaPlayer)

2.更多关于我

笔名 QQ 微信 爱好
张风捷特烈 1981462002 zdl1994328 语言
我的github 我的简书 我的掘金 个人网站

3.声明

1----本文由张风捷特烈原创,转载请注明

2----欢迎广大编程爱好者共同交流

3----个人能力有限,如有不正之处欢迎大家批评指证,必定虚心改正

4----看到这里,我在此感谢你的喜欢与支持

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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

计算机和难解性

计算机和难解性

M.R 加里、D.S. 约翰逊 / 张立昂、沈泓 / 科学出版社 / 1987年 / 4.50

本书系统地介绍了NP完全性理论的概念和方法,全书共分为7章和两个附录。第一章粗略地介绍了计算复杂性的一些基本概念和NP完全性理论的意义。第二章至第五章介绍了NP完全性的基本理论和证明的方法。第六章集中研究NP难问题的近似算法。第七章概述了大量计算复杂性中的有关理论课题。 附录A收集了范围广泛、内容丰富的NP完全性和NP难的问题、附录B补充了NP问题的一些最新的进展,既有理论方面的,又有关于具体问题......一起来看看 《计算机和难解性》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换