Zero, preface,

For video playback, Android has a built-in VideoView, which is very simple to use

This article starts with customizing VideoView to encapsulate 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")Copy the code

This paper focus on
[1]. Custom VideoView combines SurfaceView and MediaPlayer to play video [2]. Use the ContentProvider of the media library to query the videos in the phone and display them in a list [3]. Change the video width and height and adapt to the horizontal and portrait screen switch [4]. Customize the control interface and play at double speed [5]. Acquisition of video cover image (video frame) [6]. Playback of network video and second progress and cache progress monitoring of seekBarCopy the code

Simple version: MediaPlayer + SurfaceView + MediaController

Role: MediaPlayer Video processor SurfaceView Video display interface MediaController Video controllerCopy the code


1. Custom VideoView inherits from SurfaceView
/**
 * 作者:张风捷特烈<br/>
 * 时间:2019/3/8/008:12:43<br/>
 * 邮箱:[email protected]<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;
    }
}
Copy the code

2. Use the test based on the path

To keep it simple, you can use the system’s own controller: MediaController, which is ridiculously ugly

---->[activity_main.xml]------------------------------------------------ <? The 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. The constraint. ConstraintLayout > -- -- -- - > [use: PlayerActivity.kt]------------------------------------------------ id_vv.setMediaController(MediaController(this)) id_vv.setVideoPath("/sdcard/toly/sh.mp4")Copy the code

3. Get all the videos and arrange them in descending order according to the insertion time

/** * Author: Zhang Feng Jiete Lie <br/> * Time: 2018/10/300030 :18:38<br/> * Email: [email protected]<br/> * Description: Public class VideoScanner {static String[] projection = new String[]{ MediaStore. Video. Media _ID, / / ID MediaStore. Video. Media. The TITLE and name / / MediaStore. Video. Media. DURATION, / / the length MediaStore. Video. Media DATA, / / path MediaStore. Video. Media. The SIZE, / / SIZE MediaStore. Video. Media. DATE_ADDED / / added time}; / / private static List<VideoInfo> videos = new ArrayList<>(); Public static List<VideoInfo> loadVideo(final Context Context) {if (video.size ()! = 0) { return videos; } Cursor cursor = context.getContentResolver().query( MediaStore.Video.Media.EXTERNAL_CONTENT_URI, projection, "", null, "date_added desc", null); / / according to the field to obtain data in the database indexing 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); // Get 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); Add (new VideoInfo(videoId, title, dataUrl, duration, size, addDate)); } return videos; }}Copy the code

4.RecyclerView install Video information

About the cover preview and so on in reverse, layout what do not stick, write yourself

When clicked, jump to the previous play Activity, passing the video path with the Intent

---->[HomeAdapter#onBindViewHolder]------------------------------------------- holder.mIvCover.setOnClickListener(v -> {  Intent intent = new Intent(mContext, PlayerActivity.class); intent.putExtra("video-path", videoInfo.getDataUrl()); mContext.startActivity(intent); }); -- -- -- - > [throw in a video time conversion method] -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 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)Copy the code

OK simple version of the video player is OK.


Two, the interface horizontal and vertical screen problem

This turn the screen, D has become A, how can you endure, quickly fix it


1. About scaling
GetHolder ().setfixedSize (w,h) Resolution no change | - to search source / * * * Make the surface of a fixed size. It will never change from this size. * the When working with a {@link SurfaceView}, this must be called from the * same thread running the SurfaceView's window. It never changes in size. * When using SurfaceView, it must be called from the same thread that runs the SurfaceView window. * @param width The surface's width. Surface width * @param height The surface's height. Surface height */ public void setFixedSize(int width, int height);Copy the code

It looks like there’s no way out. We’ll have to find another way


2. Change the size of the View directly

public void changeVideoFitSize(int videoW, int videoH, int surfaceW, Int surfaceH) {float videoSizeRate = videoW * 1.0f/videoH; Float widthRatePortrait = videoW * 1.0f/surfaceW; float widthRatePortrait = videoW * 1.0f/surfaceW; Float heightRatePortrait = videoH * 1.0f/surfaceH; Float widthRateLand = videoW * 1.0f/surfaceH; 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 {// In landscape mode 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); / / according to the video size change View RelativeLayout. LayoutParams params = new RelativeLayout. LayoutParams (videoW videoH); setLayoutParams(params); } | - use: -- -- -- - > [setOnVideoSizeChangedListener] -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- changeVideoFitSize (mVideoWidth, mVideoHeight, mSurfaceWidth, mSurfaceHeight);Copy the code

3. The screen is in the middle

As for how to center it, I naively thought I could just change it in the XML, but it doesn’t work because I’m playing LayoutParams, right

So in the center also use LayoutParams, no way, wave source.

---->[RelativeLayout#CENTER_IN_PARENT]--------------------- public static final int CENTER_IN_PARENT = 13; CENTER_IN_PARENT is an int. If you look at the source code for LayoutParams, there are only a few methods exposed. AddRule only takes an int. It is now -- -- -- - > [RelativeLayout. LayoutParams# addRule (int)] -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - public void addRule (int verb) { addRule(verb, TRUE); } -- -- -- - > [. VideoView# changeVideoFitSize (int, int, int, int)] -- -- -- -- -- -- -- -- -- -- -- -- -- - written statements, gently can params. AddRule (13);Copy the code


3. Customize the width and height scaling ratio
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); // Cannot directly set the video size, set the calculated video size to the surfaceView and let the video fill automatically. RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(videoW, videoH); params.addRule(13); setLayoutParams(params); }Copy the code


3. Customize the operation interface

1. Perform operations on the interface

Custom interface is based on VideoView Api to achieve their own control logic, careful or not difficult, is trouble

The interface is as follows, not paste the layout, relatively simple, but also a lot of, here to show the panel after 5 seconds after the hidden logic

Private val mHandler = Handler(looper.getMainLooper ()) root.setonClickListener {// Click the 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) }Copy the code

2. Play at double speed

2 times the speed of listening to the MV is funny, API 23 + is also a thing API, very convenient

/** * variable speed * @param speed */ public void changeSpeed(float speed) {//API 23 + support 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(); }}} | - use an array to control -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- private var speeds = floatArrayOf (0.5 f to 0.75 f, 1 f, f 1.25, 1.5 f, 1.75 f, 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" }Copy the code

3. Acquisition of cover image

Basically that’s it. Finally, let’s talk about the capture of the video cover frame: I counted it at about 15 seconds

If the image is loaded in real time, it is better to query the bitmap in the entity class, because the cover image should not be too big, do not put the original image in the entity class, be careful to direct to OOM. The operation of Bitmap is not covered in this article.

---->[HomeAdapter]------------------------ private final MediaMetadataRetriever retriever; retriever = new MediaMetadataRetriever(); Public Bitmap decodeFrame(String path,long 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; }Copy the code

This option is used in conjunction with {@link #getFrameAtTime(Long,int)} to retrieve frames (not necessarily keyframes) associated with a data source located near or at a given time. * 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; This option is used in conjunction with {@link #getFrameAtTime(Long,int)} to retrieve synchronization (or key) frames associated with the data source that is closest (in time) to or at a given time. * 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; This option is used in conjunction with {@link #getFrameAtTime(Long,int)} to retrieve synchronization (or key) frames associated with a data source that is after a given time or at a specified time. * 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; This option is used in conjunction with {@link #getFrameAtTime(Long,int)} to retrieve synchronization (or key) frames associated with a data source that is before a given time or at a specified time. * 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;Copy the code


4. Network video playback


1. Online videos

On the server, address: http://www.toly1994.com:8089/imgs/sh.mp4, is a word

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

2. The second progress of SeekBar
---->[drawable/seekbar_bg.xml]-------------------------------------------- <? The 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"Copy the code

3. Cache listening
---->[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; } use: id_vv setOnBufferingUpdateListener} {id_sb_progress. SecondaryProgress = itCopy the code

Ok, that’s it. More functions can be extended on their own,

Build a background, make a simple network player is also not impossible.


Postscript: Jie wen standard

1. Growth record and Errata of this paper
Program source code The date of note
There is no The 2018-3-9 Android Video Player (Based on MediaPlayer)
2. More about me
Pen name QQ WeChat hobby
Zhang Feng Jie te Li 1981462002 zdl1994328 language
My lot My Jane books I’m the nuggets Personal website
3. The statement

1—- This article is originally written by Zhang Fengjie, please note if reproduced

2—- welcome the majority of programming enthusiasts to communicate with each other 3—- personal ability is limited, if there is something wrong welcome to criticize and testify, must be humble to correct 4—- see here, I thank you here for your love and support