项目需要做一个简单的播放视频功能demo,后期会换成公司自己的组件,所以就没考虑使用第三方库了,直接上系统的VideoView,在这里记录下操作;
顺便吐槽下:一直都听说简书编辑器好用,第一次使用,有点失望,markdown跟效果分栏竟然不能同步滚动,也不支持[TOC],没有个目录实在很不习惯,表情图标也不能插入,代码区块加空行经常都识别不了啊,就不能让我简单滴从笔记中直接粘贴md文本吗… ==!
资源
视频播放原理:
系统会首先确定视频的格式,然后得到视频的编码..然后对编码进行解码,得到一帧一帧的图像,最后在画布上进行迅速更新,显然需要在独立的线程中完成,这时就需要使用surfaceView了
基本使用
1 | VideoView mVv = (VideoView) findViewById(R.id.vv); |
错误信息
1 | //常见错误: "无法播放此视频" -我测试的是:红米1s电信版4.4.4无法播放,但在三星s6(5.1.1)上就可以播放 |
有人说 用下面的方式可以处理该异常,但我是使用系统封装好的控件,这个操作不到吧? 先记录下:
1 | MediaPlayer player = MediaPlayer.create(this, Uri.parse(sound_file_path)); |
全屏播放 - 横竖屏切换
androidmanifest.xml
中依然还是定义竖屏,并定义一个切换横纵屏按钮btnChange
:1
2
3
4
5<activity
android:name="lynxz.org.video.VideoActivity"
android:configChanges="keyboard|orientation|screenSize"
android:screenOrientation="portrait"
android:theme="@style/Theme.AppCompat.Light.NoActionBar"/>布局:需要在
VidioView
外层套一个容器,比如:1
2
3
4
5
6
7
8
9
10
11
12
13
14<RelativeLayout
android:id="@+id/rl_vv"
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="@android:color/black"
android:minHeight="200dp"
android:visibility="visible">
<VideoView
android:id="@+id/vv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"/>
</RelativeLayout>这么做是为了在切换屏幕方向的时候对
rl_vv
进行拉伸,而内部的VideoView
会依据视频尺寸重新计算宽高,我们看看其onMeasure()
源码就明了了,但若是直接具体指定了view的宽高,则视频会被拉伸:1
2
3
4
5
6
7
8//VideoView.java
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = getDefaultSize(mVideoWidth, widthMeasureSpec);
int height = getDefaultSize(mVideoHeight, heightMeasureSpec);
......
setMeasuredDimension(width, height);
}
按钮监听,手动切换
1
2
3
4
5
6
7btnSwitch.setOnClickListener(View -> {
if (getRequestedOrientation() == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
} else {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
}
});设置VideoView布局尺寸
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (mVv == null) {
return;
}
if (this.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE){//横屏
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
getWindow().getDecorView().invalidate();
float height = DensityUtil.getWidthInPx(this);
float width = DensityUtil.getHeightInPx(this);
mRlVv.getLayoutParams().height = (int) width;
mRlVv.getLayoutParams().width = (int) height;
} else {
final WindowManager.LayoutParams attrs = getWindow().getAttributes();
attrs.flags &= (~WindowManager.LayoutParams.FLAG_FULLSCREEN);
getWindow().setAttributes(attrs);
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
float width = DensityUtil.getWidthInPx(this);
float height = DensityUtil.dip2px(this, 200.f);
mRlVv.getLayoutParams().height = (int) height;
mRlVv.getLayoutParams().width = (int) width;
}
}自定义工具类
1
2
3
4
5
6
7
8
9//DensityUtil.java
public static final float getHeightInPx(Context context) {
final float height = context.getResources().getDisplayMetrics().heightPixels;
return height;
}
public static final float getWidthInPx(Context context) {
final float width = context.getResources().getDisplayMetrics().widthPixels;
return width;
}另外,如果是将播放器放于fragment中进行横竖屏切换,则需要在onCreateView中
setRetainInstance(true);
,这样旋转后,才不会重新创建从头开始播放;
获取第一帧的内容作为封面
1 |
|
滑动改变屏幕亮度/音量
权限申请
1
2<uses-permission android:name="android.permission.WRITE_SETTINGS"/>
<uses-permission android:name="android.permission.VIBRATE"/> //按需申请修改亮度方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21/*设置当前屏幕亮度值 0--255,并使之生效*/
private void setScreenBrightness(float value) {
WindowManager.LayoutParams lp = getWindow().getAttributes();
lp.screenBrightness = lp.screenBrightness + value / 255.0f;
Vibrator vibrator;
if (lp.screenBrightness > 1) {
lp.screenBrightness = 1;
// vibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE);
// long[] pattern = {10, 200}; // OFF/ON/OFF/ON...
// vibrator.vibrate(pattern, -1);
} else if (lp.screenBrightness < 0.2) {
lp.screenBrightness = (float) 0.2;
// vibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE);
// long[] pattern = {10, 200}; // OFF/ON/OFF/ON...
// vibrator.vibrate(pattern, -1);
}
getWindow().setAttributes(lp);
// 保存设置的屏幕亮度值
// Settings.System.putInt(getContentResolver(), Settings.System.SCREEN_BRIGHTNESS, (int) value);
}设置屏幕亮度模式方法 (自动/手动)
1
2
3
4// value 可取值: Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC / SCREEN_BRIGHTNESS_MODE_MANUAL
private void setScreenMode(int value) {
Settings.System.putInt(getContentResolver(), Settings.System.SCREEN_BRIGHTNESS_MODE, value);
}监听播放区域
1
2
3
4
5
6mGestureDetector = new GestureDetector(this, mGestureListener);
vv.setOnTouchListener(this);
public boolean onTouch(View v, MotionEvent event) {
return mGestureDetector.onTouchEvent(event);
}onScroll的时候动态改变亮度
onDown()
/onScroll()
返回true1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40private android.view.GestureDetector.OnGestureListener mGestureListener = new GestureDetector.OnGestureListener() {
public boolean onDown(MotionEvent e) {
return true;
}
public void onShowPress(MotionEvent e) {
}
public boolean onSingleTapUp(MotionEvent e) {
return false;
}
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
final double FLING_MIN_VELOCITY = 0.5;
final double FLING_MIN_DISTANCE = 0.5;
if (e1.getY() - e2.getY() > FLING_MIN_DISTANCE
&& Math.abs(distanceY) > FLING_MIN_VELOCITY) {
setScreenBrightness(20);
}
if (e1.getY() - e2.getY() < FLING_MIN_DISTANCE
&& Math.abs(distanceY) > FLING_MIN_VELOCITY) {
setScreenBrightness(-20);
}
return true;
}
public void onLongPress(MotionEvent e) {
}
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
return true;
}
};
滑动修改音量
修改上方的 onScroll()
方法,调用以下操作
1 | private void setVoiceVolume(boolean volumeUp) { |
- 在页面关闭时可考虑恢复亮度/音量初始值
- 在onTouch的时候对触点进行判断,区分是修改音量或是改变亮度
需要处理的问题
拖动进度条,手动seekTo后,进度会跳动
断点跟踪后发现是native方法的问题,各大视频播放平台的客户端,比较普遍存在,暂无法处理:
找到些资源:
- 关于Android VideoView seekTo不准确的解决方案
- 视频关键帧提取
第一个提到的关键帧问题,我找了个视频测试了下,seekTo到固定的时间点,则跳变的位置也固定;
暂停/恢复 页面时,视频重新加载
现象: 在视频播放时,使页面 onPause()
,之后再恢复,则 videoView
会重新开始播放,临时的处理方案是在 onPause()
的时候记录当前播放进度位置,在 onResume()
的时候拖动到该进度位置,但是该方案仍会有黑屏现象,代码如下:
1 | int mPlayingPos = 0; |
找到些可能相关的文章,链接已失效,快照如下(还得去看看 surfaceView
啊 ~ ~# ):
另一篇类似的: android开发常见问题 问题7,也指明是 surfaceview
的原因,之所以是黑色的见后面的解释:
Activity 调用的顺序是 onPause() -> onStop()
SurfaceView 调用了 surfaceDestroyed() 方法
然后再切回程序
Activity 调用的顺序是 onRestart() -> onStart() -> onResume ()
SurfaceView` 调用了 surfaceChanged() -> surfaceCreated() 方法
按挂断键或锁定屏幕
Activity 只调用 onPause() 方法
解锁后 Activity 调用 onResume() 方法
SurfaceView 什么方法都不调用
网络变化/切换应用后恢复播放
播放过程中,假如只缓冲了一部分视频,则当播放完缓冲部分后,会抛出1004异常,即使此时网络连接已经恢复,控件也不会自动继续缓冲:MediaPlayer: error (1, -1004)
源码注释: File or network related operation errors
同时,由于SurfaceView在页面onStop()时会destroy,比如播放时,用户按下home键或切换到其他应用页面再返回时,视频播放停止,此时需要重新加载视频并播放到上次停止的位置;
另外,有测试发现在三星G9200手机上,报 1004
这个错的时候会弹出错误提示框,然后卡死重启…
对比了下日志:
1 | // API23 MediaPlayer.java |
1 | //API23 VideoView.java |
因此需要对网络变化进行监听:
1 | <uses-permission android:name="android.permission.INTERNET"/> |
1 |
|
seekbar变化超出缓冲长度
使用系统提供的控件 mVv.setMediaController(new MediaController(this));
的话,在断网时,仍可以拖动超出缓冲长度的范围,会报错,这个还是得自定义才能控制可拖动位置,不再赘述;
1 | MediaPlayer: Attempt to perform seekTo in wrong state: mPlayer=0x7f7ebbf5c0, mCurrentState=0 |
1 | //API 23 MediaController.java |
当断网后,用户拖动超出缓冲区长度的话mediaplayer报错,此时再次点击VideoView区域,不会触发显示控制条,真是各种不方便啊,还是建议自己写一个控制条;
SurfaceView
资源
- SurfaceView 源码分析及使用
这篇讲到了SurfaceView
会显示黑色区域的原因:SurfaceView 的 draw 和 dispatchDraw 方法中看到,SurfaceView 中,windownType变量被初始化为WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA,所以在创建绘制这个 View 的过程中整个 Canvas 会被涂成黑色
- 浮层视频效果,在另外一个Window使用SurfaceView无法正常显示的问题排查与解决
surfaceView黑屏
无内容时,默认会绘制黑色背景图
1
2
3
4
5
6
7
8
9
10
11
12//SurfaceView.java
int mWindowType = WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA;
public void draw(Canvas canvas) {
if (mWindowType != WindowManager.LayoutParams.TYPE_APPLICATION_PANEL) {
// draw() is not called when SKIP_DRAW is set
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == 0) {
// punch a whole in the view-hierarchy below us
canvas.drawColor(0, PorterDuff.Mode.CLEAR);
}
}
super.draw(canvas);
}感谢@尚弟很忙哒 的提醒, 设置页面主题为透明(
android:theme="@android:style/Theme.Translucent"
)时,在初始缓冲阶段,VideoView区域会变成透明:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<RelativeLayout
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="org.lynxz.androiddemos.VideoViewActivity">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="@android:color/holo_blue_bright">
<VideoView
android:id="@+id/vv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"/>
</RelativeLayout>
</RelativeLayout>在SurfaceView类开头有酱紫的一段注释,大概能解释为什么缓冲时候视频区域会透明了:
1
2The surface is Z ordered so that it is behind the window holding its SurfaceView;
the SurfaceView punches a hole in its window to allow its surface to be displayed.因此处理方案就变成将SurfaceView挪到上层即可:
1
mVv.setZOrderOnTop(true);
不过挪动之后就可以设置VideoView的背景,此时才不会遮盖实际的视频绘图了,xml中指定吧,这里省略,不过如果VideoView区域还有其他控件的话,会被遮盖,所以最后我就没设定zorderOnTop了,而是直接在xml中指定VideoView的背景色,然后在onPrepare回调的时候,去掉背景即可(按需延时,或者在有播放进度,要更新进度条的时候进行去掉背景操作都ok,不然可能会有一瞬间的透明):
1
mVv.setBackgroundColor(Color.TRANSPARENT);
之前是打算像网上说的给VideoView的holder添加一个callback,(
mVv.getHolder().addCallback(new SurfaceHolder.Callback() {...}
) ,在surfaceCreated()
的时候获取canvas并手动绘制背景色,但是holder.lockCanvas()
一直返回null
,log信息提示:1
2
3
4E/SurfaceHolder: Exception locking surface
java.lang.IllegalArgumentException
at android.view.Surface.nativeLockCanvas(Native Method)
.......看到native我暂时就没招了,打住,老实用变通方法吧;
手机 “菜单键” 导致应用被stop,虽然此时看起来可见
SurfaceView.java
的注释: 在调用菜单键的时候虽然页面貌似可见,但实际已经调用了onStop()方法了,而surface在window不可见时会销毁:The Surface will be created for you while the SurfaceView’s window is
visible; you should implement {@link SurfaceHolder.Callback#surfaceCreated}
and {@link SurfaceHolder.Callback#surfaceDestroyed} to discover when the
Surface is created and destroyed as the window is shown and hidden.VideoView无法播放f4v格式(三星s6可以播放,红米1s(4.4.4)播放失败)….
以后能力够了可以参考下这篇 :- Android平台Stagefright中增加flv/f4v支持及相关原理介绍
- Stagefright功能扩展 这篇论文前半部分有关于多媒体框架调用的介绍