内容简介:对于视频的播放,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 支持
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Beginning ARKit for iPhone and iPad
Wallace Wang / Apress / 2018-11-5 / USD 39.99
Explore how to use ARKit to create iOS apps and learn the basics of augmented reality while diving into ARKit specific topics. This book reveals how augmented reality allows you to view the screen on ......一起来看看 《Beginning ARKit for iPhone and iPad》 这本书的介绍吧!
CSS 压缩/解压工具
在线压缩/解压 CSS 代码
Base64 编码/解码
Base64 编码/解码