在之前的项目中其实很少有用到VideoView的场景,只是去年看书的时候有看到音视频的一篇功能介绍,趁着最近有时间就学习整理了一下关于VideoView的blog ~
-
视频停止,释放资源
-
保持屏幕常亮
-
循环播放
-
监听上一首、下一首切换功能
-
监听视频是否播放完毕
-
获取视频当前播放时长与视频总时长
-
隐藏视频操作栏,如隐藏切换、快进、播放等功能
-
重新设定快进、后退时间
-
横竖屏适配
-
长按快进、快退功能实践
-
解决播放视频时,采用三方应用播放音乐,导致音视频声音并发的问题
-
每次加载视频时,会黑屏一刹那
-
无法加载视频,显示黑屏、'无法播放此视屏' 等
-
视屏加载成功,但报出'无法播放此视屏'的弹框
-
流量消耗巨大,出现流量损失
基础效果
其实实际效果比你当前看的效果要好,因为视频缓存过后的加载都比较快 ~
基础知识
在Android中的视屏功能,大部分使用的都是
VideoView
控件,关于控制
VideoView
的方法,一般除了其本身自带方法以外,都搭配了
MediaController
操作视频,这里主要记录我学习的一个过程和结果 ~
VideoView控件引用
<VideoView
android:id="@+id/video"
android:layout_width="match_parent"
android:layout_height="200dp"
/>
基础方法
VideoView、MediaController 常用方法
method
|
含义
|
start()
|
开始播放视频
|
pause()
|
暂停播放视频
|
resume()
|
重新播放
|
getDuration()
|
获取视屏时长
|
getCurrentPosition()
|
获取当前已播放视频时长
|
setVideoPath()
|
设置视频文件路径
|
setVideoURI()
|
设置视频文件路径
|
seekTo(int pos)
|
滑动到指定播放进度
|
isPlaying()
|
判断视屏是否在播放
|
canPause()
|
是否禁用暂停按钮功能
|
canSeekBackward()
|
是否禁用滑动进度条功能
|
canSeekForward()
|
是否功能快进功能
|
getAudioSessionId()
|
获取音频会话ID
|
suspend()
|
将VideoView所占用的资源释放掉
|
如何区别VideoView、MediaController的方法?
直接通过
MediaController
源码可以看到以下这些接口方法就都是
MediaController
的方法咯
视频来源
VideoView播放的视屏来源,主要有以下三种
-
本地内存(加载快,无卡顿,不过资源单一,如果要提速的话,可以将网络资源下载到本地)
//加载本地视频,每个人存储地址不同,新手别抄 ~
File file = new File(Environment.getExternalStorageDirectory() + "/video.mp4");
if (file.exists()) {
//设置视频地址
mVideo.setVideoPath(file.getAbsolutePath());
} else {
Toast.makeText(this, "视频不存在", Toast.LENGTH_SHORT).show();
}
-
项目raw资源(加载快,无卡顿,不过资源单一)
//加载项目内视频, "xxx": 视频名称(在res目录下新建raw,将xxx视频放在raw文件夹中)
Uri uri = Uri.parse("android.resource://$packageName/${R.raw.xxx}");
mVideo.setVideoURI(uri);
-
网络加载(资源丰富,不过随着视频的大小,加载的时间也不尽相同,可以采用预加载优化用户体检验)
//加载网络视频,记得适配 6.0,7.0,9.0
String videoPath = "https://vfx.mtime.cn/Video/2019/07/12/mp4/190712140656051701.mp4";
mVideo.setVideoPath(videoPath);
开发实践
此处仅为一个初级使用的小demo,会有部分思考的问题
注意
-
需自行适配6.0动态权限
-
需自行适配7.0临时授权
AndroidMainfest 加入权限
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET"/>
MainActivity
package nk.com.video;
import android.content.res.Configuration;
import android.media.MediaPlayer;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.ImageButton;
import android.widget.MediaController;
import android.widget.Toast;
import android.widget.VideoView;
public class MainActivity extends AppCompatActivity {
private VideoView mVideo;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mVideo = findViewById(R.id.video);
//网络加载 - 视屏地址
String videoPath = "https://vfx.mtime.cn/Video/2019/07/12/mp4/190712140656051701.mp4";
//视屏控制器
MediaController mediaController = new MediaController(MainActivity.this);
//VideoView绑定控制器
mVideo.setMediaController(mediaController);
//设置视频地址
mVideo.setVideoPath(videoPath);
//获取焦点
mVideo.requestFocus();
//播放视频
mVideo.start();
}
activity_main
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:keepScreenOn="true"
tools:context=".MainActivity">
<VideoView
android:id="@+id/video"
android:layout_width="match_parent"
android:layout_height="200dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
视频控制器
在正式开发中很少有用到原始的MediaController控制View,一般都会定制化视图Controller ~
原始效果与自定义效果
-
MediaController自带的控制器
-
效果 - 自定义Controller操作视图
实现过程
-
隐藏原始控制器
//不设置以下控制器属性
mVideo.setMediaController(mediaController);
//如果已设置,则通过mediaController 隐藏操作栏
mediaController.setVisibility(View.GONE);
-
自定义控制器视图
这里我仅写了一个基础事件来进行效果展示,如果要写的完善一点可以参考原始封装的MediaController类
伪代码
//Video基础设置
tring videoPath = "https://media.w3.org/2010/05/sintel/trailer.mp4";
MediaController mediaController = new MediaController(MainActivity.this);
mVideo.setVideoPath(videoPath);
mVideo.requestFocus();
mVideo.start();
//自定义控制器功能
TextView pause = findViewById(R.id.pause_start);
pause.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mVideo.isPlaying()){
mVideo.pause();
pause.setText("播放");
}else{
mVideo.start();
pause.setText("暂停");
});
伪代码
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<VideoView
android:layout_below="@+id/tv_display"
android:layout_width="match_parent"
android:layout_height="200dp"
android:id="@+id/video"
<TextView
android:layout_centerHorizontal="true"
android:background="#ff0"
android:layout_width="35dp"
android:gravity="center"
android:id="@+id/pause_start"
android:layout_height="35dp"
android:text="暂停"
</RelativeLayout>
功能扩展
从 VideoView + 默认MediaController实现的功能来看,其主要覆盖视频播放、暂停、快进、后退、上一首、下一首的功能,这里首先讲一下这些带给我的思考 ~
Look here:我在解决VideoView相关功能时查看MediaController,发现MediaController本身就是系统封装好的一款自定义控件,所以遇到问题直接通过此类排查问题就行 ~
视频停止,释放资源
以下方法都有视频停止并且释放内存的作用,但又稍有不同
-
stopPlayback()
通过底层代码可以发现并没有重置,所以应该只是释放内存,并没有释放配置资源
-
suspend()
则更加彻底的释放了所有配置信息和内存.
//停止播放视频,并且释放
mVideoView.stopPlayback();
//在任何状态下释放媒体播放器
mVideoView.suspend();
保持屏幕常亮
音视频开发的基本操作,
在xml的根布局添加以下属性
android:keepScreenOn="true"
循环播放
关于音视频循环播放的方式,主要有以下三种
-
方式1
:视频播放完成后,在
onCompletion
进行监听,重新播放视频
主要调用VideoView的resume()方法
mVideo.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
mVideo.resume();
});
-
方式2
:通过
MediaPlayer
控制器实现循环播放
这种方式 - 方法可用在
OnPreparedListener加载回调
和
OnInfoListener视频信息回调
,但
不能用在OnCompletionListener播放完毕的回调
中,在播放完毕时再调用此方法并不会让视频循环播放。
还有,设置了视频循环播放后,下一轮的播放不会再触发OnPreparedListener和OnInfoListener,但一样会触发在OnCompletionListener和异常回调。
-
方式3
:这三种方式当视频重复播放时都会触发信息回调,与
mp.setLooping(true)
不太一样。
mVideo.setOnInfoListener(new MediaPlayer.OnInfoListener() {
@Override
public boolean onInfo(MediaPlayer mp, int what, int extra) {
return false;
});
但VedioView.resume()方法还会触发加载状态回调OnPreparedListener,所以说resume()方法其实是再一次加载了这个视频内容,然后从头开始播放,与前二种方式是有所区别,前两种方式是再次播放已经加载好的视频,所以不会再触发OnPreparedListener这个回调。
监听上一首、下一首切换功能
主要调用MediaController控件封装的PrevNextListeners切换监听
mediaController.setPrevNextListeners(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.e("tag", "下一首");
}, new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.e("tag", "上一首");
});
监听视频是否播放完毕
VideoView提供了获取视频当前播放时长、总时长,以及状态监听的功能
mVideo.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
Log.e("tag","播放完成");
});
获取视频当前播放时长与视频总时长
主要涉及
getCurrentPosition、getDuration
俩个方法,其实在基础部分都有讲明 ~
int currentPosition = mVideo.getCurrentPosition();
Log.e("tag", "当前时长:" + currentPosition);
int duration = mVideo.getDuration();
Log.e("tag", "视频时长:" + duration);
隐藏视频操作栏,如隐藏切换、快进、播放等功能
方式一:
VideoView
不调用
setMediaController()
即可
方式二:
MediaController
本身就是自定义控件,直接用
setVisibility()
即可
//隐藏操作栏
mediaController.setVisibility(View.GONE);
示例
重新设定快进、后退时间
嗯哼,我找了挺多blog发现都没有人讲这方面的功能,我尝试在 MediaController 找重写快进、后退的方法,发现并没有... 后来发现源码中直接把快进时间和后退时间写固定了,具体如下
MediaController
关于快进功能的实现
MediaController 关于后退功能的实现
好吧,既然不支持重写,我想了俩种方法,不过这俩种方式最终没有实现快进、后退的时间修改,仅作为思想延伸,想法扩展的记录
方式一(
无效
):自己往
MediaController
里面加一加重写快进、后退的代码(因为没锁,所以可编译)
虽然在原始代码中加了设置方式,但是系统压根不识别,所以此路无效
方式二(
无效
):
因为第一种方法无效,所以我只能copy出原始MediaController,然后自己改改咯
(copy后有很多报错,首先要更改调用的包名,其次删除部分无用功能,我们主要尝试去重写快进、后退方法)
在上方我们已经写好我们的视频控制类了,然后然后… 什么鬼!白写!!!系统压根不认 ~ 无语,当然我们还有一种方式,就是弃用他的控制类,我们自己写一套操作视频的布局和功能(自定义控件也行 ~)
横竖屏适配
很多视频相关功能都支持用户横竖屏观看,根据以下设置后,我发现横竖屏并不会影响到视频重播
常规思路:监听用户横竖屏改变时,记录用户视频的播放进度,当方向转换后,重新将视频播放进度设置到之前记录的播放进度处
考虑问题
视频适配
-
修改承载横竖屏Activity的configChanges属性,如下
android:configChanges="orientation|keyboard|layoutDirection|screenSize"
示例
<activity android:name=".MainActivity"
android:configChanges="orientation|keyboard|layoutDirection|screenSize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
-
对应Activity内重写onConfigurationChanged监听屏幕方向的改变
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
Toast.makeText(getApplicationContext(), "横屏", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(getApplicationContext(), "竖屏", Toast.LENGTH_SHORT).show();
}
长按快进、快退功能实践
常规的长按事件处理,只能触发一次,即点击一次,快进一次(不适用于当前场景)
implements View.OnLongClickListener,
mPreviousView.setOnLongClickListener(this)
@Override
public boolean onLongClick(View view) {
if (view == mNextView) {
if (null != mPlaylistListener) {
Log.d("hrl", "onLongClick");
mPlaylistListener.onFastPlay();
return true;
if (view == mPreviousView) {
if (null != mPlaylistListener) {
Log.d("hrl", "onLongClick");
mPlaylistListener.onBackPlay();
return true;
return false;
}
适用方案 - 自定义VideoView中加入以下设置
(不可完全copy,需要有选择的借鉴)
private int mLastMotionX, mLastMotionY;
//是否移动了
private boolean isMoved;
//长按的runnable
private Runnable mLongPressFastRunnable = new Runnable() {
@Override
public void run() {
//调用快进函数
mPlaylistListener.onFastPlay();
Log.d("hrl", "mLongPressFastRunnable");
//若是postDelay(),会发生左右摇摆的现象,原因是获取当前位置的时候会出现延时,使得俩次postDelay获取到的pos一致。
post(this);
private Runnable mLongPressBackRunnable = new Runnable() {
@Override
public void run() {
mPlaylistListener.onBackPlay();
Log.d("hrl", "mLongPressBackRunnable");
post(this);
//移动的阈值
private static final int TOUCH_SLOP = 20;
private boolean longPress = false;
@Override
public boolean onTouch(View view, MotionEvent event) {
if (view == mNextView) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.d("hrl", "mNextView ACTION_DOWN");
mLastMotionX = x;
mLastMotionY = y;
isMoved = false;
postDelayed(mLongPressFastRunnable, ViewConfiguration.getLongPressTimeout());
break;
case MotionEvent.ACTION_MOVE:
Log.d("hrl", "mNextView ACTION_MOVE");
if (isMoved) break;
if (Math.abs(mLastMotionX - x) > TOUCH_SLOP
|| Math.abs(mLastMotionY - y) > TOUCH_SLOP) {
//移动超过阈值,则表示移动了
isMoved = true;
break;
case MotionEvent.ACTION_UP:
Log.d("hrl", "mNextView ACTION_UP");
removeCallbacks(mLongPressFastRunnable);
break;
if (view == mPreviousView) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.d("hrl", "mNextView ACTION_DOWN");
mLastMotionX = x;
mLastMotionY = y;
isMoved = false;
postDelayed(mLongPressBackRunnable, ViewConfiguration.getLongPressTimeout());
break;
case MotionEvent.ACTION_MOVE:
Log.d("hrl", "mNextView ACTION_MOVE");
if (isMoved) break;
if (Math.abs(mLastMotionX - x) > TOUCH_SLOP
|| Math.abs(mLastMotionY - y) > TOUCH_SLOP) {
//移动超过阈值,则表示移动了
isMoved = true;
break;
case MotionEvent.ACTION_UP:
Log.d("hrl", "mNextView ACTION_UP");
removeCallbacks(mLongPressBackRunnable);
break;
return false;
}
解决播放视频时,采用三方应用播放音乐,导致音视频声音并发的问题
借鉴的2016年以为前辈的
blog
,具体效果未亲自尝试
解决音视频并发问题,可以在自定义的xxxVideoView中或视频播放的xxxActivity中添加如下代码;
//用AudioManager获取音频焦点避免音视频声音并发问题
private AudioManager mAudioManager;
private OnAudioFocusChangeListener mAudioFocusChangeListener;
//在播放视频的时候请求音频焦点,第三方应用在失去音频焦点后会暂停播放(音视频应用一般都会遵守音频焦点机制,在失去焦点的回调中做暂停等处理);
@Override
public void start() {
if (requestTheAudioFocus() == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
//焦点获取成功,播放操作
}else {
//提示用户关闭其他音频再播放,不然用户以为是bug呢...
//在暂停视频、播放完成或退到后台时释放音频焦点;
@Override
public void pause() {
releaseTheAudioFocus(mAudioFocusChangeListener);
//暂停逻辑
}
请求音频焦点,并设置监听器
//请求音频焦点 设置监听
private int requestTheAudioFocus() {
if (Build.VERSION.SDK_INT < 8) {//Android 2.2开始(API8)才有音频焦点机制
return 0;
if (mAudioManager == null) {
mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
if (mAudioFocusChangeListener == null) {
mAudioFocusChangeListener = new OnAudioFocusChangeListener() {//监听器
@Override
public void onAudioFocusChange(int focusChange) {
switch (focusChange) {
case AudioManager.AUDIOFOCUS_GAIN:
case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT:
case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK:
//播放操作
break;
case AudioManager.AUDIOFOCUS_LOSS:
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
//暂停操作
break;
default:
break;
//下面两个常量参数试过很多 都无效,最终反编译了其他app才搞定,汗~
int requestFocusResult = mAudioManager.requestAudioFocus(mAudioFocusChangeListener,
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK,
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
return requestFocusResult;
}
释放音频焦点 - 暂停、播放完成或退到后台
//暂停、播放完成或退到后台释放音频焦点
private void releaseTheAudioFocus(OnAudioFocusChangeListener mAudioFocusChangeListener) {
if (mAudioManager != null && mAudioFocusChangeListener != null) {
mAudioManager.abandonAudioFocus(mAudioFocusChangeListener);
}
问题锦集
大多记载着我在项目中遇到的实战问题,有的问题往往会卡我半天,一天的 ~
每次加载视频时,会黑屏一刹那
通过查询很多人都是
通过setOnPreparedListener()的setOnInfoListener()将VideoView背景设置成透明颜色
,这确实是一种解决方案,但是我个人认为
也可以通过动画的形式使切换视频加载的效果看起来更流畅
~
mVideoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1)
@Override
public void onPrepared(MediaPlayer mp) {
mp.setOnInfoListener(new MediaPlayer.OnInfoListener() {
@Override
public boolean onInfo(MediaPlayer mp, int what, int extra) {
if (what == MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START)
mVideoView.setBackgroundColor(Color.TRANSPARENT);
return true;
});
无法加载视频,显示黑屏、‘无法播放此视屏’ 等
权限问题 - 不论 Video 是网络数据,亦或本地数据,加入以下权限基本都够用了
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />
使用方式
首先关于视频加载的方式主要有俩种
-
setVideoPath(String path)
:加载 path 文件所代表的视频。
-
setVideoURI(Uri uri)
:加载uri所对应的视频。
常规设置ViewView的方式
关于所谓的两种加载方式都可以使用,因为内部实现一样,只是setVideoPaht将Uri.parse做了内部封装
//VideoView基础设置
tring videoPath = "https://media.w3.org/2010/05/sintel/trailer.mp4";
//方式1:网络加载\本地加载
mVideo.setVideoPath(videoPath);
//方式2:本地加载
//mVideo.setVideoURI(Uri.parse(videoPath));
mVideo.requestFocus();
mVideo.start();
扩展:通过源码可以发现以上俩种方法其实本质是一样的,只是内部进行转换,最终实现
setVideoURI的三参方法
/**
* Sets video path.
* @param path the path of the video.
public void setVideoPath(String path) {
setVideoURI(Uri.parse(path));
* Sets video URI.
* @param uri the URI of the video.
public void setVideoURI(Uri uri) {
setVideoURI(uri, null);
* Sets video URI using specific headers.
* @param uri the URI of the video.
* @param headers the headers for the URI request.
* Note that the cross domain redirection is allowed by default, but that can be
* changed with key/value pairs through the headers parameter with
* "android-allow-cross-domain-redirect" as the key and "0" or "1" as the value
* to disallow or allow cross domain redirection.
public void setVideoURI(Uri uri, Map<String, String> headers) {
mUri = uri;
mHeaders = headers;
mSeekWhenPrepared = 0;
openVideo();
requestLayout();
invalidate();
}
视屏加载成功,但报出’无法播放此视屏’的弹框
这个问题断断续续卡了我小一天,后来无意之间看到了几年前的一篇blog中2015年的一句只言片语,才知道了折中方案...
我面临的场景就像上方说的一样,我视频可以正常加载,就是老出弹框…
问题分析
-
既然视频可以正常加载,我们就可以
排除权限问题和加载问题
-
有的人说
网上找的三方视频会出现保护机制?
导致出现类似问题,那么是否可以理解为:出现问题的时机分为首次加载和二次加载?如果首次加载都没问题的话,二次加载按理也不会有问题。为了排除这方面的问题,我们可以
自己随便拍一个视频传到后台或本地进行加载测试
明确一下是自己的弹框,还是VideoView自带的弹框
,如果是自己弹出去的框,
自行修改逻辑
就行,如果是系统弹出的,请看
下方的问题处理
问题处理
在VideoView有个ErrorListener错误监听,我们手动监听后,直接返回true就好了...
mVideoView.setOnErrorListener(new MediaPlayer.OnErrorListener() {
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
return true;
});
扩展
:上面就是我遇到的问题,经过查询也有一种别的错误场景 ,放在下方当给大家做个思维扩展吧
流量消耗巨大,出现流量损失
我在物联网的售货机做过一个不太合适的视频逻辑展示,曾导致半天跑了1.7G的流量
首先要明确,出现该问题的场景
-
视频可以正常加载
-
使用的网络链接形式的加载方式
-
是否针对同一个VideoView重复进行加载,因为每重新加载一次就消耗一次的流量(重要:因为针对我的业务是我是使用同一个VideoView加载不同的视频)
所以,我自己总结了一下,如何避免消耗用户大量流量
- 通过查询后,发现Github存在关于视频缓存的成熟三方框架 - AndroidVideoCache ,如有诉求,也可直接使用
-
减少VideoView频繁加载网络视频的场景
-
针对同一个VideoViiew需要加载多次时,最好做本地缓存
-
因为视频流量普遍消耗较大,最好是下载到本地,然后加载本地资源
-
可以将视频压缩后进行加载,如我们看视频时经常有清晰、高清、蓝光等画质(我虽未如此操作,但是这个方案我认为很成熟)