Android 为开发者提供了MediaRecorder的类,可以帮助录屏。但是重要的缺陷:
为了更好的效果,最终决定利用AudioRecord、MediaProjection、MediaCodec、MediaMuxer几个重要的组件进行录屏。
这几个组件都涉及到很多的音视频的知识,建议先看之前的音视频相关的文章介绍。
AudioRecord : Android音频录制的重要的模块,可以读取到PCM裸流数据;
MediaProjection:Android提供的截图、录屏的模块,可以提供一个surface;
MediaCodec:音视频硬编解码的重要模块,这里负责两项工作:
整体的难点在于如何协调音频录制、视屏录制、混合器的三个组件的流程,他们之前相互独立,但是在启动和结束又相互关系。
为此,设计了三个独立的线程,分别是音频录制线程、视频录制线程、混合线程,三个都是HandlerThread的机制,利用消息机制,推动线程进行消息处理。
(也就是handler.sendMessage()发送消息和onMessage()处理消息的机制、loop无线循环消息)
不过在视频录制上MediaCodec提供了回调,不需要自己维护一个HandlerThread。
初始状态 -> 预启动状态 -> 录制状态 -> 预结束状态 -> 结束状态
之所有需要一个预启动状态和一个预结束状态,是因为录制模块和合成模块相互之间是有依赖关系的,录制模块需要拿到合成模块的通道,合成模块需要让录制模块优先启动。
同理,预结束状态也是相互依赖的。
INFO_OUTPUT_FORMAT_CHANGED
状态,从混合器模块添加一个通道track,开始真正的音频编码,进入录制状态;BUFFER_FLAG_END_OF_STREAM
状态,进入预结束状态;以上三个模块的预启动状态、录制状态、预结束状态、结束状态都是相互独立的。
音视频的录制涉及到了多线程并行处理,单一线程录制音频、单一线程录制视频、单一线程合并音频帧和视频帧。因此需要非常注意维护多线程状态,小心陷入死锁问题。
音视频最常遇到的问题就是如何保障音视频的同步问题?
这里记下遇到的几个坑:
Android 视频帧获取的时间戳presentationTime是不可靠的,以及我们获取到的音频流没有时间戳的概念。
为了追求流畅感,一般都是以音频帧为标准,视频帧迁就音频帧,也就是说视频帧可以丢帧处理
同步音视频的方法:
对齐处理:音频和视频都利用System.currentTimeMill()获取时间戳,并且需要从时间戳必须要从0开始。
随着时间尺度拉长,你会发现视频帧过快,音频帧追不上视频帧的速度,时间差距越来越大,当差距超过规定的时间差,对视频帧做丢帧处理
如何对录屏进行暂停和恢复呢?
如果对mediaCodec停止重启或者对AudioRecord停止重启,都有可能失败的概率。为了保证不失败,笔者的做法是让音视频持续获取音频帧和视频帧,但是不做合成处理(即丢帧)
暂停:记录暂停时间,修改当前为暂停状态,对获取到的音频帧和视频帧多丢帧处理;
恢复:记录恢复时间,算出来此次暂停了多久时间,后续的所有视频帧和音频帧计算出来的时间戳都需要减去此次暂停的时间差
设置录制的页面长宽需要被16除尽
不同机型支持的分辨率是一样的,虽然你可以通过查看MediaCodecList支持的分辨率,但事实上获取到支持的分辨率和实际上支持的分辨率是不一致的,比如,某机型声称能支持1920,但实际上只能支持720。
解决方法可以设立不同flavor,对不同机型对应录制不同的分辨率
出现的场景是在静态页面下,录制出来的页面是正常的,一旦出现界面跳转之类的动态页面就会花屏或者糊。
一般是由于帧率过高且手机性能较差,如果你对帧率不怎么了解,可以看下图,解决方法是降低帧率
当只是录制纯视频不需要音量时候,一开始直接移除了音频帧的输入,仅保留视频帧,最后合成出来的mp4,播放的速度很快。
无论你如何调改时间戳也无济于事,最后笔者的处理方法是,继续引入音频帧,但是对获取的音频帧做处理成静音帧,即将对应的byte设置为0。如果你有更好的解决方法,希望能告诉我。
音视频的录制非常容易出现异常,基本都是native层的奔溃,大部分是由于状态不一致。
最后需要坚持的原则:
这里列举出音视频模块需要捕获异常的地方:
由于分段录制的需要,需要非常频繁地启动、结束,所以非常遇到上述异常,一般在容易出现在启动阶段、例如AudioRecord无法启动等问题,解决方法是可以再次启动AudioRecord,因为非常短的时间内,AudioRecord的状态还没被修改,再次启动时,native会爆出状态错误的运行时异常。
MediaCodec实际上是有数量限制的,不同平台支持的编解码器数量是不一样的,最少的6,最大的有32。所以在采取多线程并行编解码的时候是需要考虑到。
为了减少因为启动失败的问题导致中间较长时间无法录制,提出快速失败快速启动的方案,大概方案就是如果启动失败,即马上进入下次启动,不等待MediaCodec的释放停止,因为MediaCodec的启动是相对耗时的。
但是实际上由于MeidaCodec数量的限制,导致短时间的几次失败需要许多MediaCodec,最后放弃了这种方案。
同理,AudioRecord在同种类型下(例如麦克风)无法被同时占用的。
MediaProjection是需要用户权限动态申请,为了优化用户体验,hook修改了绕过系统权限检测
public static Intent getProjectionIntent(Context context, final int uid, final String packageName) {
MediaProjectionManager mediaProjectionManager =
(MediaProjectionManager)context.getSystemService(MEDIA_PROJECTION_SERVICE);
try {
Object mediaProjectionManagerService =
ReflectionUtil.getPrivateField(mediaProjectionManager, "mService");
Object mediaProjection = ReflectionUtil.invokePrivateMethod(
mediaProjectionManagerService,
"createProjection",
new Class[]{ int.class, String.class, int.class, boolean.class},
uid,
packageName,
TYPE_SCREEN_CAPTURE,
false);
Object binder = ReflectionUtil.invokePrivateMethod(
mediaProjection,
"asBinder");
Intent intent = new Intent();
Bundle bundle = new Bundle();
bundle.putBinder(EXTRA_MEDIA_PROJECTION, (IBinder) binder);
intent.putExtras(bundle);
return intent;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
一般是由于异常的出现,导致状态混乱出现死锁问题,属于代码逻辑问题。
Android系统提供了几种类型的AudioRecord音频采集的来源:
如果需要录制系统声音,可以利用REMOTE_SUBMIX进行录制。
那如何录制麦克风声音 + 系统声音?
Android无法同时选择录制麦克风+系统声音,如果你有好的解决方法,希望能告诉我。
之前刚好看到scrcpy的源码,其中利用了SurfaceControl反射创建了Display,避开了权限检测,而且速度增快了。
使用SurfaceControl
ScreenEncoder.java
SurfaceControl.java
为了保证播放连续性,希望能够至少每秒都输出key帧,好处是可以直接在任意秒处直接播放。
// 设置key帧间隔
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1000);
// 只在surface-input模式有用, 一帧之后多少毫秒如果没有新的数据进来,就重复这一帧
mediaFormat.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, 1000_000 / getFramerate());
但是实际上发现,KEY_I_FRAME_INTERVAL并不一定生效,所以在每一秒过后检测是否是key帧,否则强制生成
// 是否是key帧
boolean keyFrame = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0;
/**
* 强制生成key帧:播放时可以直接seek
*/
private void postKeyFrameEvent() {
Bundle params = new Bundle();
params.putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME, 0);
mMediaCodec.setParameters(params);
}