先总结下系统是如何实现组合键截屏的:
都应该知道Android源码中对按键的捕获位于文件PhoneWindowManager.java中
当满足按键条件时会用一个mHandler 开始post一个runnable,进入这个runnable中执行takeScreenshot()方法。
使用AIDL绑定了service服务到”com.android.systemui.screenshot.TakeScreenshotService”,注意在service连接成功时,对message的msg.arg1和msg.arg2两个参数的赋值。其中在mScreenshotTimeout中对服务service做了超时处理。接着我们找到实现这个服务service的类TakeScreenshotService,该类在(frameworks/base/packages/SystemUI/src/com/android/systemui/screenshot包下
引用SurfaceControl类,调用了screenshot方法, 传入了屏幕的宽和高,这两个参数,接着进入SurfaceControl类中,位于frameworks/base/core/java/android/view目录下
最终到达native方法中nativeScreenshot
面就是java层的部分,接着到jni层,在\frameworks\base\core\jni\android_view_SurfaceControl.cpp中
到jni中,映射nativeScreenshot方法的是nativeScreenshotBitmap函数
最后辗转来到c++层,就是\frameworks\native\libs\gui下的SurfaceComposerClient.cpp中,实现ScreenshotClient声明的函数update
当进入到CAPTURE_SCREEN中,data会读取IGraphicBufferProducer生成出的图像buffe,接着调用 reply->writeInt32(res);返回给client.然后再回调到java层。以上就是系统截屏的原理。
那对于多媒体这块可以通过MediaProjection来实现截屏
实现思路:
首先获取MediaProjectionManager,和其他的Manager一样通过 Context.getSystemService() 传入参数MEDIA_PROJECTION_SERVICE获得实例。
接着调用MediaProjectionManager.createScreenCaptureIntent()弹出dialog询问用户是否授权应用捕捉屏幕,同时覆写onActivityResult()获取授权结果。
如果授权成功,通过MediaProjectionManager.getMediaProjection(int resultCode, Intent resultData)获取MediaProjection实例,通过MediaProjection.createVirtualDisplay(String name, int width, int height, int dpi, int flags, Surface surface, VirtualDisplay.Callback callback, Handler handler)创建VirtualDisplay实例。实际上在上述方法中传入的surface参数,是真正用来截屏或者录屏的。
截屏这里用到ImageReader类,这个类的getSurface()方法获取到surface直接传入MediaProjection.createVirtualDisplay()方法中,此时就可以执行截取。通过ImageReader.acquireLatestImage()方法即可获取当前屏幕的Image,经过简单处理之后即可保存为Bitmap。
private void startVirtual() {
if (mMpj != null) {
virtualDisplay();
} else {
setUpMediaProjection();
virtualDisplay();
}
}
private void setUpMediaProjection() {
int resultCode = ((MyApplication) getApplication()).getResultCode();
Intent data = ((MyApplication) getApplication()).getResultIntent();
mMpj = mMpmngr.getMediaProjection(resultCode, data);
}
private void virtualDisplay() {
mVirtualDisplay = mMpj.createVirtualDisplay("capture_screen", windowWidth, windowHeight, screenDensity,
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, mImageReader.getSurface(), null, null);
}
private void startCapture() {
mImageName = System.currentTimeMillis() + ".png";
Log.e(TAG, "image name is : " + mImageName);
Image image = mImageReader.acquireLatestImage();
int width = image.getWidth();
int height = image.getHeight();
final Image.Plane[] planes = image.getPlanes();
final ByteBuffer buffer = planes[0].getBuffer();
int pixelStride = planes[0].getPixelStride();
int rowStride = planes[0].getRowStride();
int rowPadding = rowStride - pixelStride * width;
Bitmap bitmap = Bitmap.createBitmap(width + rowPadding / pixelStride, height, Bitmap.Config.ARGB_8888);
bitmap.copyPixelsFromBuffer(buffer);
bitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height);
image.close();
if (bitmap != null) {
Log.e(TAG, "bitmap create success ");
try {
File fileFolder = new File(mImagePath);
if (!fileFolder.exists())
fileFolder.mkdirs();
File file = new File(mImagePath, mImageName);
if (!file.exists()) {
Log.e(TAG, "file create success ");
file.createNewFile();
}
FileOutputStream out = new FileOutputStream(file);
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
out.flush();
out.close();
Log.e(TAG, "file save success ");
Toast.makeText(this.getApplicationContext(), "截图成功", Toast.LENGTH_SHORT).show();
} catch (IOException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
}
}
}
录屏1 mp4
主体思路:
逻辑:录屏不需要操作视频原始数据,因此使用InputSurface作为编码器的输入。
视频:MediaProjection通过createVirtualDisplay创建的VirtualDisplay获取当前屏幕的数据。然后传入到MediaCodec中(即传入的Surface是通过MediaCodec的createInputSurface方法返回的),然后MediaCodec对数据进行编码,于是只需要在MediaCodec的输出缓冲区中拿到编码后的ByteBuffer即可。
简单说就是重定向了屏幕录制的数据的方向,这个Surface提供的是什么,录制的视频数据就传到哪里。Surface提供的是本地某个SurfaceView控件,那么就会将屏幕内容显示到这个控件上,提供MediaCodec就是作为编码器的输入源最终获得编码后的数据,提供ImageReader就会作为ImageReader的数据源,最终获得了视频的原始数据流。
音频:录制程序获得音频原始数据PCM,传给MediaCodec编码,然后从MediaCodec的输出缓冲区拿到编码后的ByteBuffer即可。
最终通过合并模块MediaMuxer将音视频混合。
小结:录屏需要用到MediaCadec,这个类将原始的屏幕数据编码,在通过MediaMuxer分装为mp4格式保存。MediaCodec.createInputSurface()获取一个surface对象,传入MediaProjection.createVirtualDisplay()即可获取屏幕原始多媒体数据.之后读取MediaCodec编码输出数据经过MediaMuxer封装处理为mp4即可播放,实现录屏。
private void recordVirtualDisplay() {
while (!mIsQuit.get()) {
int index = mMediaCodec.dequeueOutputBuffer(mBufferInfo, 10000);
Log.i(TAG, "dequeue output buffer index=" + index);
if (index == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {//后续输出格式变化
resetOutputFormat();
} else if (index == MediaCodec.INFO_TRY_AGAIN_LATER) {//请求超时
Log.d(TAG, "retrieving buffers time out!");
try {
// wait 10ms
Thread.sleep(10);
} catch (InterruptedException e) {
}
} else if (index >= 0) {//有效输出
if (!mMuxerStarted) {
throw new IllegalStateException("MediaMuxer dose not call addTrack(format) ");
}
encodeToVideoTrack(index);
mMediaCodec.releaseOutputBuffer(index, false);
}
}
}
private void resetOutputFormat() {
// should happen before receiving buffers, and should only happen once
if (mMuxerStarted) {
throw new IllegalStateException("output format already changed!");
}
MediaFormat newFormat = mMediaCodec.getOutputFormat();
Log.i(TAG, "output format changed.\n new format: " + newFormat.toString());
mVideoTrackIndex = mMuxer.addTrack(newFormat);
mMuxer.start();
mMuxerStarted = true;
Log.i(TAG, "started media muxer, videoIndex=" + mVideoTrackIndex);
}
录屏2 Gif
由于录制的是视频,得变成gif,有两种方案:
•提取视频文件->解析视频->提取 Bitmap 序列(使用 MediaMetadataRetriever 提取某一时刻的图片,然后把很多某一时刻的图片串联起来编码成 gif。看来其也正是 gif 的原理,但实现出来的效果极差,无法准确提取到准确的图片,导致合成的 gif 图也无法连贯播放,播放起来也跳帧跳得很厉害。惨不忍睹)
•利用FFmpeg直接转gif, 这种方法岗岗的。
之前我们演示过:
windows下编译最新版ffmpeg3.3-android,并通过CMake方式移植到Android studio2.3中 :http://blog.csdn.net/king1425/article/details/70338674
调用相关命令也可通过Jni实现。
github:https://github.com/WangShuo1143368701/VideoView/tree/master/mediaprojectionmediamuxer