前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >[Android] 使用MediaProjection截屏

[Android] 使用MediaProjection截屏

作者头像
wOw
发布2018-09-18 14:55:49
10.4K0
发布2018-09-18 14:55:49
举报
文章被收录于专栏:wOw的Android小站wOw的Android小站

Background

Android5.0以上提供了MediaProjection,方便截屏录屏等功能。

详细代码参阅Github:https://github.com/wossoneri/ScreenCapture

一个完整的创建MediaProjection到结束的流程如下:

代码语言:javascript
复制
mProjectionManager = (MediaProjectionManager)getSystemService(Context.MEDIA_PROJECTION_SERVICE);
// init
startActivityForResult(mProjectionManager.createScreenCaptureIntent(), REQUEST_CODE);

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
  super.onActivityResult(requestCode, resultCode, data);
  if (RESULT_OK == resultCode && REQUEST_CODE == requestCode) {
    sMediaProjection = mProjectionManager.getMediaProjection(resultCode, data);
    ......
  }
}
// end
sMediaProjection.stop();

其中,需要使用startActivityForResult的唯一原因是,捕捉屏幕是需要用户确认权限才可以,这个权限对应的对话框就是由createScreenCaptureIntent创建的,在用户点击允许之后,在onActivityResult得到确认码,才可以拿到MediaProjection对象。

下面详细介绍相关知识点。

About MediaProjection

MediaProjection

授予应用程序捕获屏幕内容或记录系统音频的能力。授予的准确能力取决于MediaProjection的类型

可以通过createScreenCaptureIntent()捕获屏幕会话。它能获取屏幕内容,但无法获取系统音频。

它有4个公有方法:

createVirtualDisplay
代码语言:javascript
复制
VirtualDisplay createVirtualDisplay (String name, 
                int width, 
                int height, 
                int dpi, 
                int flags, 
                Surface surface, 
                VirtualDisplay.Callback callback, 
                Handler handler)

创建一个VirtualDisplay用来捕获屏幕内容。

参数:

  • name:String 名称,永不为空
  • width:int
  • height:int
  • dpi:int
  • flags:int DisplayManager定义的flag组合
  • surface:Surface virtual display的内容应该被渲染的surface,没有的话为空
  • callback:VirtualDisplay.Callback virtual display状态变化的回调
  • handler:Handler callback调用的Handler。如果回调在calling thread的主looper被调用,则为空?
registerCallback
代码语言:javascript
复制
void registerCallback (MediaProjection.Callback callback, 
                Handler handler)

注册一个listener接收MediaProjection变化状态的通知。

stop
代码语言:javascript
复制
void stop ()

停止projection

unregisterCallback
代码语言:javascript
复制
void unregisterCallback (MediaProjection.Callback callback)

取消注册MediaProjection的listener

MediaProjectionManager

管理获取到MediaProjection具体类型。

该类必须使用Context.getSystemService(Class)方法,参数用MediaProjectionManager.class或者Context.getSystemService(String)方法,参数用Context.MEDIA_PROJECTION_SERVICE两种方式实例化。

公有方法:

createScreenCaptureIntent
代码语言:javascript
复制
Intent createScreenCaptureIntent()

启动screen capture,必须把这个方法返回的Intent传递给startActivityForResult()。这个Activity会提示用户是否允许捕捉屏幕。用户的操作结果需要传递给getMediaProjection。

所以这里的目的就是提示用户,获取允许再抓屏。我用小米系统,这个提示只会弹一次。后面用nexus试试

getMediaProjection
代码语言:javascript
复制
MediaProjection getMediaProjection (int resultCode, Intent resultData)

成功获得用户允许后获取MediaProjection对象。如果授权失败,则得到空对象。

MediaProjection.Callback

projection session的回调 主要就一个方法:

onStop
代码语言:javascript
复制
void onStop()

当MediaProjection session不再有效时调用。一旦一个MediaProjection调用stop方法就回调到这里,然后由应用程序决定释放其可能持有的资源。如下示例代码,所有资源释放在这里进行。

代码语言:javascript
复制
private class MediaProjectionStopCallback extends MediaProjection.Callback {
  @Override
  public void onStop() {
    mHandler.post(new Runnable() {
      @Override
      public void run() {
        if (mVirtualDisplay != null) {
          mVirtualDisplay.release();
        }
        if (mImageReader != null) {
          mImageReader.setOnImageAvailableListener(null, null);
        }
        sMediaProjection.unregisterCallback(MediaProjectionStopCallback.this);
      }
    });
  }
}

Screen Capture

以上就是MediaProjection调用的几个接口。得到MediaProjection实例后怎么截屏呢?下面是截屏的核心步骤。

代码语言:javascript
复制
        //start capture reader
        mImageReader = ImageReader.newInstance(mWidth, mHeight,
                PixelFormat.RGBA_8888, 2);
        mVirtualDisplay = sMediaProjection.createVirtualDisplay(
                "ScreenShot",
                mWidth,
                mHeight,
                mDensity,
                DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY | DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC,
                mImageReader.getSurface(),
                null,
                mHandler);
        mImageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
            @Override
            public void onImageAvailable(ImageReader reader) {
                Image image = null;
                FileOutputStream fos = null;
                Bitmap bitmap = null;

                try {
                    image = reader.acquireLatestImage();
                    if (image != null) {
                        Image.Plane[] planes = image.getPlanes();
                        ByteBuffer buffer = planes[0].getBuffer();
                        int pixelStride = planes[0].getPixelStride();
                        int rowStride = planes[0].getRowStride();
                        int rowPadding = rowStride - pixelStride * mWidth;

                        bitmap = Bitmap.createBitmap(mWidth + rowPadding / pixelStride,
                                mHeight, Bitmap.Config.ARGB_8888);
                        bitmap.copyPixelsFromBuffer(buffer);

                        Date currentDate = new Date();
                        SimpleDateFormat date = new SimpleDateFormat("yyyyMMddhhmmss");
                        String fileName = STORE_DIR + "/myScreen_" + date.format(currentDate) + ".png";
                        fos = new FileOutputStream(fileName);
                        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos);
                        Log.d("WOW", "End now!!!!!!");
                        Toast.makeText(mContext, "Screenshot saved in " + fileName, Toast.LENGTH_LONG);
                        stopProjection();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    if (fos != null) {
                        try {
                            fos.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                    if (bitmap != null) {
                        bitmap.recycle();
                    }
                    if (image != null) {
                        image.close();
                    }
                }

            }
        }, mHandler);
        sMediaProjection.registerCallback(new MediaProjectionStopCallback(), mHandler);
}

ImageReader & Surface

Surface用来处理由屏幕合成器管理的raw buffer。

在Andorid的窗口实现里,每一个Window其实都会对应一个Surface,而每个Activity都会持有一个Window。可以理解为所有view的绘制都会绘制到surface上面去。

而ImageReader允许应用级别的直接访问render到surface上面的图像数据。

图像数据封装在Image对象中,并且可以同时访问多个此类对象,能够访问的数量由构造参数maxImages决定(稍后会看到这个参数)。通过Surface发送给ImageReader的图像都放在队列中,由acquireLatestImage()acquireNextImage() 两个方法取出。限于内存大小,如果ImageReader不能以与生成速率相同的速率获取和释放图像,那么图像源最终会在试图渲染到表面的过程中停止或删除图像。

相关代码说明:

newInstance
代码语言:javascript
复制
ImageReader newInstance (int width, 
                int height, 
                int format, 
                int maxImages)
代码语言:javascript
复制
mImageReader = ImageReader.newInstance(mWidth, mHeight, PixelFormat.RGBA_8888, 2);

使用newInstance方法实例化一个ImageReader。

前两个参数是ImageReader生成图像的尺寸,截屏当然是使用屏幕尺寸。

注意,用Display获取屏幕尺寸要用真实的尺寸,使用getRealMetrics方法。如果使用getMetrics方法,得到的高度是缺少Navigaiton Bar的高度的。 如果尺寸和屏幕不一致,最终得到的图像会是等比例缩放到屏幕大小的图像,然后空白的地方会显示黑边。

第三个参数是format类型,使用ImageFormat或者PixelFormat的一种。代码使用PixelFormat最高保真的类型。

第四个参数是maxImages,这个参数决定了可以同时从ImageReader对象获取最大图像对象的数量。所以请求更多的缓冲区要占用更多的内存,所以要根据需求选择最小数量。对截屏来说,要1张图像就够了,但是代码使用的是2,这个理由在后面说。

getSurface
代码语言:javascript
复制
Surface getSurface ()
代码语言:javascript
复制
mImageReader.getSurface()

获取可以用来为当前ImageReader生产Images的Surface对象。

在有效的图像数据render到这个Surface之前, acquireNextImage() 方法将会返回空值。

acquireLatestImage
代码语言:javascript
复制
Image acquireLatestImage ()
代码语言:javascript
复制
image = reader.acquireLatestImage();

从ImageReader的队列获取最新的图像,删除旧的图像。如果没有新图像,返回null

这个方法将会获得来自ImageReader的所有图像,close()掉所有不是最新的图像。对于大多数用例,建议使用这个方法,而不是acquireNextImage()方法,因为它更加适合实时处理。

注意,对于acquireLatestImage方法来说,maxImages应该至少为2,因为它的机制,要获取最新的图像,同时丢弃旧的图像,因此一次至少需要获取两个图像。

acquireNextImage
代码语言:javascript
复制
Image acquireNextImage ()

从ImageReader队列获取下一张图像。还是建议使用acquireNextImage方法,因为它会自动释放旧图片。

错误的使用这个方法,会导致图像不断增加的延时,最终导致没有新图像产生。

setOnImageAvailableListener

注册listener,当ImageReader有新的图像时候会回调到这里。

代码语言:javascript
复制
mImageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
    @Override
    public void onImageAvailable(ImageReader reader) {
        ...
        image = reader.acquireLatestImage();
    }
}

DisplayManager & VirtualDisplay

VirtualDisplay表示一个虚拟显示,显示的内容render到 createVirtualDisplay()参数的Surface。

因为virtual display内容render到应用程序提供的surface,所以当进程终止时,它将会自动释放,并且所以剩余的窗口都会被强制删除。但是,你仍然需要在使用完后显式地调用release()方法。

DisplayManager管理加载的display的属性

实例化方法有两种:

  • Context.getSystemService(DisplayManager.class)
  • Context.getSYstemService(Context.DISPLAY_SERVICE)

DisplayManager的几个常量:

DISPLAY_CATEGORY_PRESENTATION

String类型。

Display category: Presentation displays. (不知道如何翻译准确)

这一类别可用于识别适合用于presentation displays的第二级的displays,比如HDMI或者Wireless displays。应用程序可以自动地投射他们的内容到presentation displays以提供更丰富的第二屏体验。

关键字:Android双屏异显 可以参阅Android Presentation接口说明,实现双屏异显的功能。

VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR

int类型(后面皆是)。允许在没有显示内容的情况下在私有display上显示内容。

这个flag和VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY互相排斥,如果两者同时指定,后者将被应用。

当设置VIRTUAL_DISPLAY_FLAG_PUBLIC ,同时没有设置VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY时,这个flag会被隐含。只有在创建私有display时,才需要使用该flag来覆盖默认行为。

创建一个自动mirror的virtual display需要 CAPTURE_VIDEO_OUTPUT或者 CAPTURE_SECURE_VIDEO_OUTPUT 权限。这些权限保留为系统组件使用,对第三方应用不可用。或者,可以使用适当的MediaProjection创建一个自动mirror的virtual display。

VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY

只展示这个display自己的内容,不镜像其他display的内容。

这个flag与VIRTUAL_DISPLAY_FLAG_PUBLIC一起使用。通常,public virtual display在没有自己的窗口时会自动镜像显示默认display的内容。当设定这个flag后,virtual display只会显示自己的内容,如果自己没有窗口,就会为空白。

VIRTUAL_DISPLAY_FLAG_PUBLIC或者 VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR 没有设置时,该flag是隐含的。只有在创建公开display时,才需要使用该标志来覆盖默认行为。

VIRTUAL_DISPLAY_FLAG_PRESENTATION

创建一个presentation display

这个和双屏异显有关。设置成该flag,virtual display就注册为一个Presentation display。

VIRTUAL_DISPLAY_FLAG_PUBLIC

创建一个public display

一个公共virtual display 和其他任何与系统连接的display一样,比如HDMI或Wireless display。应用程序可以在display上打开窗口,系统也可以镜像其他display的内容在其上面。

Creating a public virtual display that isn’t restricted to own-content only implicitly creates an auto-mirroring display.

当没有设置这个flag,就是私有的display。私有的virtual display属于创建它的应用程序。只有这个display的拥有者才能往上面放置windows。私有的virtual display也不参与display mirroring,它既不接收其他的镜像,也不会把自己镜像出去。更准确的说,只有相同的UID的应用程序或者已经在display上的activities可以使用私有display

VIRTUAL_DISPLAY_FLAG_SECURE

创建一个secure的display

调用者将采取一些合理的措施,比如无线加密,来防止display的内容被拦截或被持久的媒介记录。

需要系统级别的 CAPTURE_SECURE_VIDEO_OUTPUT权限。

Process with Image

代码语言:javascript
复制
image = reader.acquireLatestImage();
if (image != null) {
  Image.Plane[] planes = image.getPlanes();
  ByteBuffer buffer = planes[0].getBuffer();
  int pixelStride = planes[0].getPixelStride();
  int rowStride = planes[0].getRowStride();
  int rowPadding = rowStride - pixelStride * mWidth;

  bitmap = Bitmap.createBitmap(mWidth + rowPadding / pixelStride,
                               mHeight, Bitmap.Config.ARGB_8888);
  bitmap.copyPixelsFromBuffer(buffer);
}

这部分将Image对象的字节流写进Bitmap里,但是Bitmap接收的是像素格式的。

先获取图片的buffer数据,然后要把这一行buffer包含的图片宽高找出来。

获取pixelStride。因为是RGBA4个通道,所以每个像素的间距是4。

得到每行的宽度rowStride。

因为内存对齐的缘故,所以buffer的宽度会有不同。用图片宽度×像素间距得到一个大概的宽度。然后拿获取得到的宽度减去计算出的宽度,找到内存对齐的padding。

由于计算的padding还是把4通道展开一行的宽度,拿给图像就需要rowPadding / pixelStride统一单位和mWidth相加。

Image.Plane的几个方法说明
getBuffer

获得一个包含帧数据的ByteBuffer

In particular, the buffer returned will always have isDirect return true, so the underlying data could be mapped as a pointer in JNI without doing any copies with GetDirectBufferAddress.

For raw formats, each plane is only guaranteed to contain data up to the last pixel in the last row. In other words, the stride after the last row may not be mapped into the buffer. This is a necessary requirement for any interleaved format.

getPixelStride

The distance between adjacent pixel samples, in bytes.

This is the distance between two consecutive pixel values in a row of pixels. It may be larger than the size of a single pixel to account for interleaved image data or padded formats. Note that pixel stride is undefined for some formats such as RAW_PRIVATE, and calling getPixelStride on images of these formats will cause an UnsupportedOperationException being thrown. For formats where pixel stride is well defined, the pixel stride is always greater than 0.

getRowStride

The row stride for this color plane, in bytes.

This is the distance between the start of two consecutive rows of pixels in the image. Note that row stried is undefined for some formats such as RAW_PRIVATE, and calling getRowStride on images of these formats will cause an UnsupportedOperationException being thrown. For formats where row stride is well defined, the row stride is always greater than 0.

Problems

使用过程中问题总结。

  • 截屏有黑边 mDisplay.getMetrics(metrics);导致的。这个方法获取到的屏幕是不包含NavigationBar的高度的,所以得到的尺寸比真实的全屏要小。需要使用mDisplay.getRealMetrics(metrics);解决这个问题。
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2018-04-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Background
  • About MediaProjection
    • MediaProjection
      • createVirtualDisplay
      • registerCallback
      • stop
      • unregisterCallback
    • MediaProjectionManager
      • createScreenCaptureIntent
      • getMediaProjection
    • MediaProjection.Callback
      • onStop
  • Screen Capture
    • ImageReader & Surface
      • newInstance
      • getSurface
      • acquireLatestImage
      • acquireNextImage
      • setOnImageAvailableListener
    • DisplayManager & VirtualDisplay
      • DISPLAY_CATEGORY_PRESENTATION
      • VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR
      • VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY
      • VIRTUAL_DISPLAY_FLAG_PRESENTATION
      • VIRTUAL_DISPLAY_FLAG_PUBLIC
      • VIRTUAL_DISPLAY_FLAG_SECURE
    • Process with Image
      • Image.Plane的几个方法说明
  • Problems
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档