内容简介:对于视频的播放,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 视频控制器 复制代码
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"/>
---->[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.获取所有的视频并根据插入时间降序排列
/** * 作者:张风捷特烈<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传递视频路径
---->[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 了,怎么能忍,赶快修一下
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的尺寸
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); 复制代码
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); } 复制代码
三、定制操作界面
1.界面操作
自定义的界面就是根据VideoView中的Api自己实现控制逻辑,细心一点还是不难的,就是麻烦
界面如下,不贴布局了,比较简单,也挺多的,这里说一下显示面板后5秒后隐藏的逻辑
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.封面图的获取
基本上也就这么多了,最后讲一下视频封面帧图片的获取:数了一下这帧大概在15秒
测试了一下秒数越大,获取图片的速度越慢,也就是越卡,所以还是给0吧
如果在Adapter里实时加载会很卡,最好查询的时候就把bitmap放到实体类里,由于封面图不要很大
别把原图给放进去了,小心直接OOM。Bitmap的操作本文就不赘述了。
---->[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; 复制代码
Ok 本篇就这样,更多的功能可以自己去拓展
后记:捷文规范
1.本文成长记录及勘误表
项目源码 | 日期 | 备注 |
---|---|---|
无 | 2018-3-9 | Android多媒体之视频播放器(基于MediaPlayer) |
2.更多关于我
笔名 | 微信 | 爱好 | |
---|---|---|---|
张风捷特烈 | 1981462002 | zdl1994328 | 语言 |
我的github | 我的简书 | 我的掘金 | 个人网站 |
3.声明
1----本文由张风捷特烈原创,转载请注明
2----欢迎广大编程爱好者共同交流
3----个人能力有限,如有不正之处欢迎大家批评指证,必定虚心改正
4----看到这里,我在此感谢你的喜欢与支持
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 一个基于QT的多媒体播放器
- xplay 发布,专为树莓派设计的多媒体播放器
- VLC 3.0.10 发布,跨平台多媒体播放器
- VLC 3.0.11 发布,跨平台多媒体播放器
- VLC 3.0.7 发布,跨平台多媒体播放器
- 多媒体播放器 VLC 3.0.1 发布:改进 Chromecast 支持
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。