开篇吐槽一下自己,从上班之后,由于公司没有什么活。就开启自己的抄抄模式。前期的时候一次性的转载了40片左右文章。原因也是在上班之际,白纸一张。那时候感觉抄着文章,感觉还可以能看懂。到了如今发现抄不动了。哎!!! 发现自己已经在奔溃中了。摘要:最近的公司的签写demo,需要在之前的签写、拍照、抽奖的基础上追加一个投屏的功能。一番收索之后,时间就是一周过去,想想自己都尴尬了。考虑了种种实现方式,什么只截取程序本身的界面啊,让后通过socket进行传输。通过直播流阿…等等。最后不得不放弃了。原因一个自己“low“, 对编码问题一窍不通,更不说推流了。
这里只对截图做记录,采取直接截屏的方式原因因为,只截取程序自身的图片。也还是需要用户授权。对于截取音视频。哈. . hahah… . . 尴尬的笑笑
这里先从5.0开始。在5.0的时候系统提供了截屏的API,5.0之前需要Root权限,罗列三个链接,分别Google官方的demo,5.0截屏屏传输,5.0之前的截屏。
- googlesamples/android-ScreenCapture
- 屏幕录制(一)——MediaProjection 简介
- android-notes/androidScreenShareAndControl其中有反向控制,可以通过投屏的显示界面操作手机端
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 |
对于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代表一个虚拟显示器,需要先用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。 |
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权限。没有什么实际的意义。软件商城里面有很多的截屏软件,对于具体实现并不知道。有知道的大佬可以告知一下,谢谢。
参考链接
- android屏幕共享及远程控制原理
- Android直播实现(一)Android端推流、播放
- 屏幕录制(一)——MediaProjection 简介
- yrom/ScreenRecorder
- Android实现录屏直播(一)ScreenRecorder的简单分析
- 有关Android截图与录屏功能的学习
- Android开发笔记(一百三十)截图和录屏
- 【拍照截图】Android 系统拍照和截图
- J Android: Image类浅析(结合YUV_420_888)
- Android多媒体学习一:Android中Image的简单实例。
- Android图像格式类及图像转换方法
- Java Code Examples for android.graphics.PixelFormat.RGBA_4444