Android 使用MediaProjection+ImageReader捕捉屏幕画面

#.简介
     Android5.0以后提供了MediaProjectionManager系统服务来获取手机屏幕画面。
    需要获取相应服务的权限,然后
创建虚拟显示器,物理屏幕画面会不断被投影到虚拟现实器,输出到创建虚拟显示器时设置的Surface上。 使用过程一般会结合ImageReader或OpenGL来进行。
    (在更低的版本,如Android4.4,获取屏幕画面需要通过ADB指令来进行。但目前市面上基本已经见不到比Android5.0版本还低的安卓手机)

#.基本使用流程如下

1.获取MediaProjectionManager服务实例
mProjectionManager = (MediaProjectionManager)activity
        .getSystemService(Context.MEDIA_PROJECTION_SERVICE);
2.通过MediaProjectionManager创建请求屏幕捕捉的隐式Intent,发送到目标Activity。
这时会显示一个弹窗,“xxx将开始截取您屏幕上显示的所有内容”,申请用户同意。
Intent captureIntent = mProjectionManager.createScreenCaptureIntent();
activity.startActivityForResult(captureIntent, SCREEN_CAPTURE_REQUEST_CODE);
3.在发送方Activity的onActivityResult(int requestCode, int resultCode, Intent data)处理请求结果,若用户同意了请求,就可以通过返回的结果获取MediaProjection对象执行后面的流程进行屏幕画面捕捉
MediaProjection mediaProjection = mProjectionManager.getMediaProjection(resultCode, data);
4.通过MediaProjection创建创建虚拟显示器对象,创建后物理屏幕画面会不断地投影到虚拟显示器VirtualDisplay上,输出到虚拟现实器创建时设定的输出Surface上。一般显示屏的刷新频率为60Hz,即会以60帧/s的频率来刷新Surface上的内容。
VirtualDisplay virtualDisplay = mediaProjection.createVirtualDisplay(
                //VirtualDisplay名称,必须非空
                VIRTUAL_DISPLAY_NAME,
                //VirtualDisplay的尺寸宽高,必须大于0
                mDisplayWidth, mDisplayHeight,
                //像素密度dpi,必须大于0,一般取1
                VIRTUAL_DISPLAY_DPI,
                //设置标识位为公共显示器,一般都是使用该标志位
                DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC,
                //VirtualDisplay的图像绘制目标Surface,
                //例如,可以是SurfaceView的Surface,用于显示;
                //     或者是MediaCodec的输入Surface,用于编码;
                //     或者是ImageReader的输入Surface,用于其它处理;
                //这里用SurfaceTexture中的Surface,直接将源画面转化成纹理
                displaySurface,
                //注册状态回调接口,当状态改变时触发
                //第二个参数为Handler,指明了回调方法的执行线程。
                // 回调方法会在该Handler所对应的Looper消息队列中被执行,即在其所在线程执行;
                // 若传入null则会在当前线程中执行。
//                mDisplayCallback, null );
               null, null );

4.1所以虚拟现实器创建时设定的输出Surface很重要:
   如果要截屏,那么可以使用ImageReader的输入Surface,然后通过ImageReader将Surface中的画面以Image形式读取出来;
   如果做录屏直播,
       可以不断取出上面ImageReader的画面,然后通过OpenGL绘制到MediaCodec的输入Surface去编码;
       也可以为画面捕捉的线程初始化EGL环境,直接创建一个SurfaceTexture,获取对应的Surface,设置给虚拟现实器,此时虚拟显示器的输出画面会被自动转化为OpenGL外部纹理。经过OpenGL中间处理后,最终将这个纹理绘制到MediaCodec的输入Surface去编码。
4.2另外,注意:MediaProjection捕获的屏幕画面是整个屏幕范围的画面,包括导航条Navigaiton Bar。
所以设置虚拟现实器尺寸时,要使用getRealMetrics来获取尺寸,如果使用getMetrics方法,获取高度不包含导航条。
若实际捕获画面尺寸大于虚拟现实器尺寸,会将捕获画面等比例放缩到虚拟现实器能够容纳的程度,然后空余位置会显示黑边,效果类似“centerInside”。
5.在使用完毕后,结束屏幕画面捕捉并释放相应资源
mediaProjection.stop();
virtualDisplay.release();

#.ImageReader介绍

    ImageReader内部关联一个Surface,可以将该Surface交给Camera或VirtualDisplay等作为画面输出目标。
    ImageReader可以直接从Surface读取图像数据,其内部有图像数据缓存队列, 画面生产方不断把画面绘制到ImageReader的Surface上,并被存入缓存队列; 而画面使用方,不断通过ImageReader从其缓存队列上取出Image对象。
    maxImages设定了ImageReader最多缓存多少Image,当缓存数量达到maxImages后, 若老的Image若一直不close()为队列释放空间,将不会有新的Image放入队列, 那么Surface上的画面可能还未被存入队列就被新的画面刷新,结果就是所谓"丢帧"。
1.构造方法
//输出图像的宽、高,图像颜色格式、缓存队列最多缓存的Image数量
mImageReader = ImageReader.newInstance(width, height, format, maxImages);
2.取出Image
//从ImageReader队列中获取最新的一帧Image(队列末尾),并且将前面老的Image都close()掉,如果没有新的可用的Image则返回null。
//这是Android推荐用的方式,因为能获取到尽可能新的画面。
//   不过稍加思索可知,当maxImages=1时,其实与下面.acquireNextImage()的效果没有差异。
//注意用完Image,及时Image.close()
image = mImageReader.acquireLatestImage();

//从ImageReader的队列中获取下一帧Image,如果没有新的则返回null。
//注意用完Image,及时Image.close()
image = mImageReader.acquireNextImage();
3.回调接口
//有新的可用图像时的通知回调
//第二个参数为Handler,指明了回调方法的执行线程。
// 回调方法会在该Handler所对应的Looper消息队列中被执行,即在其所在线程执行;
// 若传入null则会在当前线程中执行。
mImageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
    @Override
    public void onImageAvailable(ImageReader reader) {

    }
}, null);
4.关键代码:将ImageReader中读取出的Image转换成对应Bitmap
/*将Image对象的ByteBuffer中字节数据写进Bitmap里,因为Bitmap接收的是像素格式的数据,所以需要做一些处理*/
//获取Image的图片像素宽、高
int width = image.getWidth();
int height = image.getHeight();
//获取Image中的ByteBuffer
Image.Plane[] planes = image.getPlanes();
ByteBuffer buffer = planes[0].getBuffer();
//获取Image中每个像素的Byte数(像素间距),这里因为是RGBA4个通道,所以每个像素的间距是4
int pixelStride = planes[0].getPixelStride();
//获取Buffer中每行像素的字节宽度rowStride
int rowStride = planes[0].getRowStride();
//因为内存对齐的缘故,所以buffer的行宽度与上面获取到的width*pixelStride会有差异
//内存对齐的padding字节数 = Buffer行宽 - Image中图片宽度×像素间距
int rowPadding = rowStride - pixelStride * width;
//接收ByteBuffer数据的Bitmap需要的像素宽度 = Image中图片宽度 + 内存对齐宽度/像素间距
//每行的对应位置会填充一些无效数据
//(其实直接写成rowStride/pixelStride也行,按步骤写只是为了让逻辑清晰)
Bitmap tempBmp = Bitmap.createBitmap(width + rowPadding / pixelStride, height, Bitmap.Config.ARGB_8888);
tempBmp.copyPixelsFromBuffer(buffer);
//释放Image,以便继续从输入Surface接收新的画面
image.close();
//将tempBmp中每行像素中的无效数据过滤掉
mLastBitmap = Bitmap.createBitmap(tempBmp, 0, 0, width, height);
//释放tempBmp
tempBmp.recycle();
return mLastBitmap;

#.当使用SurfaceTexture作为虚拟显示器输出目标时,非常关键易出错的一步:

//这一步非常关键,它设置了SurfaceTexture中Surface的大小,否则默认输出的纹理大小为1*1像素
//注意:实际运行代码发现,设置的大小,应该与屏幕捕获的画面大小一致,否则:
//   1.正常情况下,画面从Surface的左上角开始绘制,超出的部分会被裁减掉;
//     Surface上多余的部分会保留底色,而这部分底色一般显示出来会是黑色(要看显示的控件怎么处理底色)
//   2.若这里设置的宽、高与捕获画面的宽、高正好相反,则画面会居中缩放到Surface能容纳的大小,
//     不会发生裁剪,但是Surface上多余部分会保留底色。
//     利用这个特点,如果需要在横竖屏切换时裁掉多余黑色部分,可以调整OpenGL裁剪矩阵,将多余黑色部分裁减掉
mSurfaceTexture.setDefaultBufferSize(mDisplayWidth, mDisplayHeight);

猜你喜欢

转载自blog.csdn.net/u013914309/article/details/124757576