Android Multimedia实战(四)MediaProjection实现截图,与MediaMuxer实现录屏为MP4,Gif格式

魏君博
2023-12-01

MediaProjection可以用来捕捉屏幕,具体来说可以截取当前屏幕和录制屏幕视频 (5.0以上)

先总结下系统是如何实现组合键截屏的:
都应该知道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

 类似资料: