记录:Android(5.0)截屏的实现

摘要:最近的公司的签写demo,需要在之前的签写、拍照、抽奖的基础上追加一个投屏的功能。一番收索之后,时间就是一周过去,想想自己都尴尬了。考虑了种种实现方式,什么只截取程序本身的界面啊,让后通过socket进行传输。通过直播流阿…等等。最后不得不放弃了。原因一个自己“low“, 对编码问题一窍不通,更不说推流了。

这里只对截图做记录,采取直接截屏的方式原因因为,只截取程序自身的图片。也还是需要用户授权。对于截取音视频。哈. . hahah… . . 尴尬的笑笑

  开篇吐槽一下自己,从上班之后,由于公司没有什么活。就开启自己的抄抄模式。前期的时候一次性的转载了40片左右文章。原因也是在上班之际,白纸一张。那时候感觉抄着文章,感觉还可以能看懂。到了如今发现抄不动了。哎!!! 发现自己已经在奔溃中了。
  这里先从5.0开始。在5.0的时候系统提供了截屏的API,5.0之前需要Root权限,罗列三个链接,分别Google官方的demo,5.0截屏屏传输,5.0之前的截屏。
  1. googlesamples/android-ScreenCapture
  2. 屏幕录制(一)——MediaProjection 简介
  3. android-notes/androidScreenShareAndControl其中有反向控制,可以通过投屏的显示界面操作手机端
  对于截取图片的流程都是一样的,通过“MediaProjectionManager , MediaProjection , VirtualDisplay , ImageReader “ 四个类就可以完成截图。现在知道通过以上四个类就可以完成截图,并获取 Bitmap。下面就对这几个类做一个Google翻译的一个记录。
   MediaProjectionManager
  “Manager“ 看见这个就想到了这是一个管理类,需要通过getSystemService()去获取实例对象。类的注释为
/**
 * Manages the retrieval of certain types of {@link MediaProjection} tokens.
 *
 * <p>
 * Get an instance of this class by calling {@link
 * android.content.Context#getSystemService(java.lang.String)
 * Context.getSystemService()} with the argument {@link
 * android.content.Context#MEDIA_PROJECTION_SERVICE}.
 * </p>
 */
  可用的公用的方法只有两个,分别为“createScreenCaptureIntent() , getMediaProjection()“ ,从上面可以真正进行截屏操作的类还是“MediaProjection“ 。这里需要注意一下,并不同于其他的 XXXXManager再获取实例对象后就可以调用公有方法了。这里需要分为两步:
  第一步
          Intent captureIntent = projectionManager.createScreenCaptureIntent();
          startActivityForResult(captureIntent, RECORD_REQUEST_CODE);
  第二步
  @Override
  protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == RECORD_REQUEST_CODE) {
      mediaProjection = projectionManager.getMediaProjection(resultCode, data);
    }
  }
   MediaProjection
  仅仅是一个截图。不会获取到系统的声音。在这里里面出现了第三个类“VirtualDisplay“ ,一个虚拟的显示层。在截屏时,常常伴随一声‘咔嚓’的声音。是不是想到了相机???在自定自定义相机的时候,需要一个承载画面的SurfaceView或者TextureView。在这里截屏也需要一个承载层。到这里“ImageReader“ 也就登场了。
   打开“MediaProjection“ ,眼前一亮,可用公共方法也没有几个,一只手就数完了。“registerCallback(),unregisterCallback(),stop(),createVirtualDisplay()“ ,对于注册的一对方法,翻译意思为在状态改变时候进行回调。在学习的几个demo很少有注册使用的,除 yrom/ScreenRecorder,这里面有使用前两个方法。
  stop( )
  停止截屏
  createVirtualDisplay( )
  创建投影的承载层,这个方法的参数很多。最后调用的DispalyManager方法。对参数的意思做一个记录。
参数名称含义
String name实际的流媒体显示实体名字,不能为空
int width实际的流媒体显示实体的宽度,单位为像素,必须大于0
int height实际的流媒体显示实体的高度,单位为像素,必须大于0
int dpi实际的流媒体显示实体的像素密度,单位为dp,必须大于0
int flags实际的流媒体显示实体标志的结合。(看下一个表格)
Surface surface播放流媒体的surface实例,可为null
VirtualDisplay.Callback callback实际的流媒体显示实体状态改变时的回调方法,可能为null
Handler handler调用参数7回调方法的handler
  在上面的几个参数,后两个可以直接传递为null,surface在可以向Google Samples中传递自己的surface。也可以在只截图的时候传递imageReader.getSurface( ),如果为录制成视频的时候很麻烦,需要是MediaCodec.createInputSurface( ),其中需要自己去做处理。
  对于flags这个参数,不太清楚是是什么意思。(注 : 因为我在测试的环境是公司的平板,没有在手机上测试过)在demo测试的过程中,把 5 中flag都测试了一下,得到的结果都一样,都可以成功截取屏幕。没有遇到网友所的 “VIRTUAL_DISPLAY_FLAG_SECURE“ 情况下截取失败。但是,重要的事情在说一遍,我没有在手机上测试。有知道的大佬,希望告知一下或是一个链接,万分感谢。下面的表格解释来至于Google翻译。
虚拟显示几个常量标志
字段含义
VIRTUAL_DISPLAY_FLAG_PUBLIC公共显示:公共虚拟显示和大多数连接到系统其他显示器(如:HDMOH或无线显示器)相同。应用程序可以在显示器上打开窗口,系统可以将其它显示的内容镜像到上面。

如果没有设置时,为Display#FLAG_PRIVATE,私有显示属于创建它的应用程序。只有所有者和已经在该显示器上的应用程序才允许在上面放置窗口。
VIRTUAL_DISPLAY_FLAG_PRESENTATION演示显示:当该标志被设置时,显示在display category注册为DISPLAY_CATEGORY_PRESENTATION,应用程序可以自动将其内容投影到演示文稿显示,以提供更丰富的第二屏幕体验。

这个标志未被设置时,虚拟显示不被登记为演示显示。 应用程序仍然可以在显示器上投影他们的内容,但是他们通常不会自动完成。 该选项适用于更多特殊用途的显示器
VIRTUAL_DISPLAY_FLAG_SECURE安全显示:当该标志被设置时,虚拟显示被认为是安全的,Display#FLAG_SECURE。,例如空中加密,以防止显示内容被拦截或记录在永久介质上。创建一个安全的虚拟显示需要CAPTURE_SECURE_VIDEO_OUTPUT权限。 此权限保留供系统组件使用,不适用于第三方应用程序。

当这个标志未被设置时,虚拟显示被认为是不安全的。 如果在此显示器上显示,则安全窗口的内容将被清空。
VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY当没有内容显示时,允许内容在专用显示器上被镜像:该标志与VIRTUAL_DISPLAY_FLAG_PUBLIC一起使用。 通常,公共虚拟显示器将自动镜像默认显示的内容,如果他们没有自己的窗口的话。 当这个标志被指定时,虚拟显示器将只显示它自己的内容,如果它没有窗口,它将被消隐。该标志与VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR互斥。 如果两个标志都被指定,那么仅适用于自己内容的行为将被应用。

只要VIRTUAL_DISPLAY_FLAG_PUBLIC和VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR都没有被设置,这个标志的行为就是隐含的。 这个标志只需要在创建公共显示时覆盖默认行为。
VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR当没有内容显示时,允许内容在专用显示器上被镜像。此标志与VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY互斥。 如果两个标志都被指定,那么仅适用于自己内容的行为将被应用。只要设置了VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY且VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY尚未设置,则此标志的行为就是隐含的。 只有在创建私人显示时,此标志才能覆盖默认行为。创建自动镜像虚拟显示需要CAPTURE_VIDEO_OUTPUT或CAPTURE_SECURE_VIDEO_OUTPUT权限。 这些权限被保留供系统组件使用,不适用于第三方应用程序。 或者,可以使用适当的MediaProjection来创建自动镜像虚拟显示。
   VirtualDisplay
  类注释:VirtualDisplay代表一个虚拟显示器,需要先用DisplayManager中的 createVirtualDisplay( )方法,将虚拟显示器的内容渲染在一个Surface空间上,当进程终止时虚拟显示器会被自动释放,并且所有的Window都会被强制移除。当不再使用的时候,应该主动调用release( )方法来释放资源。
  注意到其中可以使用的公用方法,同样没有几个,这太友好了。“getDisplay , getSurface , setSurface , resize , release“ 。
  getDisplay ( )
  获取虚拟的显示器。
  getSurface ( )
  获取虚拟显示器的surface。
  setSurface ( )
  设置虚拟显示器依靠的surface。移除虚拟显示器所依靠的surface相当于关闭屏幕的操作。需要手动的销毁surface。
  resize ( )
  此方法是运行应用程序使用虚拟现实器去适应改变的条件状态,而不用销毁再重建一个实例。
  release( )
  释放显示器,并且销毁其所依据的surface。
   ImageReader
  类注释:ImageReader类允许应用程序直接访问Surface图像数。据。图像数据被封装在Image中,并且可以同时访问多个图像,直到maxImages构造器参数指定的数目。发到image到ImageReader并进行排队。直到通过acquireLatestImage()或acquireNextImage()调用进行访问。由于內存限制,如果使用Imagereader的生产速率不等于释放速率,则图像将最终尝试在渲染surface时,停止或丢弃图像。这个类的公共方法太多了,标注出几个常用到的。
  newInstance( )
  创建指定大小和格式实例对象。参数还是以表格形式解释,maxImages在使用的时候,请求更多的缓冲区那么将使用更大的內存。所以需要注意这个数字的设置。其中单个的大小有取决于尺寸、格式、数据源(因为在类注释上面写明支持Android中的很多API)。注意不同的图片格式设置,会导致不一样读取方法。
参数名含义
int width 默认宽度 (以像素为单位)
int height 默认高度 (以像素为单位)
int format图片格式,可以是android.graphics.ImageFormat、android.graphics.PixelFormat其中的一种。这些格式也不是所有都支持(如: ImageFormat.NV21)。
int maxImages用户想要同时访问的最大图像数量。 这应该尽可能小,以限制内存使用。 一旦用户获得了maxImages图像,必须释放其中的一个图像,然后才能通过acquireLatestImage()或acquireNextImage()访问新的图像。 必须大于0。
  这里贴上我设置成1和10这两个值的截图
maxImages为1
maxImages为10

  getWidth()

  返回图像的实际宽度,因为这个surfaceView在前面提到是可以set的。

  getHeight()

   返回图像的实际高度,因为这个surfaceView在前面提到是可以set的。

  getImageFormat()

  获取设置的ImageReader的格式,这里需要注意一下,每一种图片的格式只与自身兼容。如果在ImageReader中设置为PixelFormat.RGBA_8888,在创建图片的时候就需要设置为Bitmap.Config.ARGB_8888。

  这里贴一段 PixelFormat和Bitmap.Config简短对应代码。具体的对应关系请看文章底部的参考链接

    // bitmap configure
    switch (manager.getDefaultDisplay().getPixelFormat()) {
        case PixelFormat.A_8:
            m_bitmap_config = Bitmap.Config.ALPHA_8;
            break;
        case PixelFormat.RGB_565:
            m_bitmap_config = Bitmap.Config.RGB_565;
            break;
        case PixelFormat.RGBA_4444:
            m_bitmap_config = Bitmap.Config.ARGB_4444;
            break;
        case PixelFormat.RGBA_8888:
            m_bitmap_config = Bitmap.Config.ARGB_8888;
            break;
    }

  getSurface()

  获取用于为此ImageReader生成Images的surface。acquireNextImage在没有有效数据的时候会一直返回null,在同一时间只可以呈现一个源的数据。虽然说这个Surface可以承载不同Android的API数据。

   close( )

   释放此ImageReader相关的所有资源。在调用此方法后,ImageReader上的任何方法和通过acquireLatestImage()、acquireNextImage获取的Images提供的方法。都将抛出IllegalStateException。且尝试恢复一下之前状态数据

  acquireLatestImage()

  从ImageReader的队列中获取最新的Image ,删除旧images 。 如果没有新图像可用,则返回null 。如果已经close了,那么将不会是最新的数据图像。对于大多情况可以使用acquireNextImage(),它更加适合处理实时数据.。在使用这个方法读取图片的时候,要注意maxImages不能小于2,从字面上和上面的知识我们了解到它是获取一张,丢弃一张。如果小于2的话可能会导致预期丢弃失败

  acquireNextImage()

  从ImageReader的队列中获取下一个Image。 如果没有新图像可用,则返回null 。注意皮球,警告:考虑使用acquireLatestImage() ,因为它会自动释放较旧的图像,并允许运行较慢的处理最新的帧。 建议在批处理/后台处理中使用acquireNextImage() 。错误地使用此功能可能会导致图像出现延迟不断增加,然后是完全失速,看起来没有新的图像出现。

  Image

   类注释:提到这个类,对于我们来说有些陌生,其实我们是使用过它的。回忆一下调用系统相机拍照,是不是有些记忆了。可以查看文章底部的参考链接。Image是一个完整的多媒体图像缓冲区,如:MediaCodec、camera2。可以通过一个或多个ByteBuffer高效直接的访问,每一个缓冲区都封装在Plane这个平面布中。这里直接获取的是缓存流,这个和Bitmap有直接的区别。Image通常是由硬件直接生成或使用的,因此它们是整个系统的共享资源,在不使用的时候,应该尽快的关闭。不能直接作为UI资源。在使用ImageReader从各种媒体源读出图像时,超过getMaxImages范围,不关闭旧的Image那么将阻止新Image的可用性。往往抛出IllegalStateException。这里不清楚这个一或多具体指的什么。初步估计是多张Image,理由是,在ImageReader中可以设置mMaxImages,而且也建议是2张。通从别人的代码中猜测的,代码如下:

                    img = imageReader.acquireLatestImage();
                    if (img != null) {
                        Image.Plane[] planes = img.getPlanes();
                        if (planes[0].getBuffer() == null) {
                            return;
                        }
                        ....
                       }

  对于方法的话基本都是抽象方法。也不太好翻译,主要是晓不到如何下手。把几个公共的方法和获取流的方法记录一下。

  setTimestamp( )

  设置与此帧关联的时间戳。时间戳以纳秒为单位,通常是单调递增。 来自不同来源的图像的时间戳可能具有不同的时间基准,因此可能不具有可比性。 时间戳的具体含义和时间基础取决于提供图像的来源。 有关更多详细信息,请参阅Camera , CameraDevice , MediaPlayer和MediaCodec

  getCropRect( )

  获取关联的裁剪矩形。

  setCropRect( )

  设置相关联的裁剪矩形。

  getPlanes( )

  获取此图像的像素平面阵列。 平面的数量由图像的格式决定。 如果图像格式为PRIVATE ,则应用程序将获得一个空数组,因为图像像素数据不可直接访问。 应用程序可以通过调用getFormat()来检查图像格式。

  通过 getPlanes( )可以获取到对应的数据流,现在怎么去把流转成图片问题接踵而来,里面有涉及到一些计算了。下面对Plane类中方法做一个记录:

  getRowStride( )

  此颜色平面的行跨度(以字节为单位)。这是图像中连续两行像素开始的距离。 请注意,对于某些格式(如RAW_PRIVATE ,stried未定义,对这些格式的图像调用getRowStride将导致抛出UnsupportedOperationException。 对于行跨度很好定义的格式,行跨度总是大于0。

   getPixelStride( )

  相邻像素采样之间的距离,以字节为单位。这是一行像素中两个连续像素值之间的距离。 它可能大于单个像素的大小,以考虑交错图像数据或填充格式。 请注意,某些格式(如RAW_PRIVATE像素跨距未定义,并且在这些格式的图像上调用getPixelStride将导致抛出UnsupportedOperationException。 对于像素跨度定义明确的格式,像素跨度总是大于0。

  getBuffer( )

  获取包含帧数据的直接ByteBuffer 。特别是,返回的缓冲区总是有isDirect = true ,所以底层数据可以被映射为JNI中的一个指针,而不用GetDirectBufferAddress做任何拷贝。对于原始格式,每个平面只保证包含最后一行中最后一个像素的数据。 换句话说,最后一行之后的步幅可能不会被映射到缓冲区中。 这是任何交错格式的必要条件。

  看了上面半天只有一句话,你在说什么腌。列出上面的数据只是单纯的想说明,这个获取的Buffer不可以直接转换成Bitmap的。需要通过计算的。计算的公式如下:

                try {
                    img = imageReader.acquireLatestImage();
                    if (img != null) {
                        Image.Plane[] planes = img.getPlanes();
                        if (planes[0].getBuffer() == null) {
                            return;
                        }
                        int width = img.getWidth();
                        int height = img.getHeight();
                        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);
                        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
                        int options_ = 30;//压缩分辨率,比如取值为30,那么压缩了30%
                        bitmap.compress(Bitmap.CompressFormat.JPEG, options_, byteArrayOutputStream);


                    }

                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    if (null != fos) {
                        try {
                            fos.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                    if (null != bitmap) {
                        bitmap.recycle();
                    }
                    if (null != img) {
                        img.close();
                    }

                }

  到此5.0以上的截屏大概记录完成了。其中的代码都是使用开源大佬们的demo。不再次进行贴出,三个demo的代码分别在文章开篇的三个链接文章里面。感谢作者,感谢开源。

  在5.0之下我们如何实现截屏了?在后面使用Android Stuio中有一个更加操作方便的功能就是可以在android studio的logcat界面中直接就截取屏幕。

  第一步,截取安卓图片保存到sd卡
  adb shell /system/bin/screencap -p /sdcard/screenshot.png(保存到SDCard)

  第二步,把图片传到电脑上
  adb pull /sdcard/screenshot.png d:/screenshot.png(保存到电脑)

  通过以上shell命令就可完成截图,并传输到当前电脑。对于把这个命令变成一个程序,需要运行需要Root权限。没有什么实际的意义。软件商城里面有很多的截屏软件,对于具体实现并不知道。有知道的大佬可以告知一下,谢谢。

参考链接

  1. android屏幕共享及远程控制原理
  2. Android直播实现(一)Android端推流、播放
  3. 屏幕录制(一)——MediaProjection 简介
  4. yrom/ScreenRecorder
  5. Android实现录屏直播(一)ScreenRecorder的简单分析
  6. 有关Android截图与录屏功能的学习
  7. Android开发笔记(一百三十)截图和录屏
  8. 【拍照截图】Android 系统拍照和截图
  9. J Android: Image类浅析(结合YUV_420_888)
  10. Android多媒体学习一:Android中Image的简单实例。
  11. Android图像格式类及图像转换方法
  12. Java Code Examples for android.graphics.PixelFormat.RGBA_4444
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页