Android屏幕共享-传输图片

2,679 阅读5分钟

这是我参与8月更文挑战的第12天,活动详情查看:8月更文挑战

Android屏幕共享-传输图片

该篇主要讲解安卓与安卓之间如何截图传输,属于安卓屏幕共享的入门篇。适用场景:安卓屏幕共享工具类应用。

一、效果图

效果图

手机QQ视频_20200720215510 (1).gif

demo界面

Screenshot_2020-07-20-21-21-44-227_com.talon.screen.quick.jpg

二、前言

目前安卓屏幕共享的方式有很多种软件已经实现,究其根源,无非就是采集-传输-播放的过程,而安卓端无论是截图传输还是硬解码,也一定离不开核心类MediaProjection,它就是Google开放了视频录制的接口(屏幕采集的接口)。

比较优秀的软件举例:

  • TeamViewer

  • 向日葵

  • Vysor

其中Vysor就是通过adb实现无root静默截图,然后传输,实现电脑控制手机。在整个屏幕共享的过程中,重点在于如何采集,采集到画质清晰内存又小的技术成为核心。

常用的安卓端采集技术有:

  • MediaProjection 实现截图

  • MediaProjection 硬解码

  • ffmpeg 软解码

  • adb 实现无root截图

三、功能点

  1. android5.0及以上免root截图

  2. 安卓屏幕变化的时候采集

  3. 多端共享拓展简单

  4. socket传输字节数组

  5. 尽可能都使用原生代码实现

四、功能讲解

4.1 截图

随着安卓系统的不断升级,权限也越来越收紧,无root并且不连接数据线做不到静默截图,这里指截取不属于该app的区域。

  • 在允许连接数据线的情况下,可以通过adb shell push 事先编译好的 dex 文件到手机中,实现静默截图。

  • 在没有链接数据线无root的情况下,只能显式的让用户有感知的进行截取。本篇截图使用的是 MediaProjection, 如下图:

Screenshot_2020-07-20-21-38-12-737_com.android.systemui.jpg

MediaProjection 是Android 5.0 开放的屏幕截图与录制视频的接口,是一个系统级的服务,在使用前需要动态申请权限,并在onActivityResult进行处理。

eg:


/**

* 申请截屏权限

*/

private void tryStartScreenShot() {

MediaProjectionManager mProjectionManager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);

if (mProjectionManager != null) {

startActivityForResult(mProjectionManager.createScreenCaptureIntent(), REQUEST_MEDIA_PROJECTION);

}

}


@Override

protected void onActivityResult(int requestCode, int resultCode, Intent data) {

if (requestCode == REQUEST_MEDIA_PROJECTION && data != null) {

if (resultCode == RESULT_OK) {

// 截屏的回调

ScreenShotHelper screenShotHelper = new ScreenShotHelper(this, resultCode, data, this);

screenShotHelper.startScreenShot();

} else if (resultCode == RESULT_CANCELED) {

LogWrapper.d(TAG, "用户取消");

}

}

}

在屏幕共享时,一直截图就会很费性能,如果在屏幕改变时进行截图就好了。

刚好,在ImageReader中有setOnImageAvailableListener可以让我们方便的拿到屏幕变化的数据。eg:


/**

* 屏幕发生变化时截取

*/

private class ImageAvailableListener implements ImageReader.OnImageAvailableListener {

@Override

public void onImageAvailable(ImageReader reader) {

try (Image image = reader.acquireLatestImage()) {

if (image != null) {

// todo

}

} catch (Exception e) {

e.printStackTrace();

}

}

}

4.2 图片处理

图片格式:

  • png:无损压缩图片格式,支持Alpha通道

  • jpeg:有损压缩图片格式,不支持背景透明

  • webp:“WebP 是 Android 4.2.1(API 级别 17)支持的较新图片格式。这种格式可为网络上的图片提供出色的无损压缩和有损压缩效果。使用 WebP,开发者可以创建更小、更丰富的图片。WebP 无损图片文件比 PNG 平均缩小了 26%。这些图片文件还支持透明度(也称为 Alpha 通道),只需增加 22% 的字节。

WebP 有损图片比采用等效 SSIM 质量指标的同等 JPG 图片缩小 25-34%。对于可以接受有损 RGB 压缩的情况,有损 WebP 也支持透明度,生成的文件大小通常比 PNG 小 3 倍。” -- 出自谷歌官方文档

笔者曾尝试过webp格式的编码,大小确实降低了50%左右,但是编码耗时却长了100%,相当于用CPU换网速。这样看来webp并不适用该场景。

图片压缩:

  • 压缩大小

  • 压缩图片质量

该Demo的代码中是将bitmap压缩到固定的大小

Server:


/**

* 压缩图片 (压缩后不代表实际大小,有差异)

*

* @param bitmap 被压缩的图片

* @param sizeLimit 大小限制 单位 k

* @return 压缩后的图片

*/

public static Bitmap compressBitmap(Bitmap bitmap, long sizeLimit) {

ByteArrayOutputStream baos = new ByteArrayOutputStream();

int quality = 90;

bitmap.compress(Bitmap.CompressFormat.JPEG, quality, baos);

// 循环判断压缩后图片是否超过限制大小

while (baos.toByteArray().length / 1024 > sizeLimit) {

baos.reset();

bitmap.compress(Bitmap.CompressFormat.JPEG, quality, baos);

quality -= 10;

}

return BitmapFactory.decodeStream(new ByteArrayInputStream(baos.toByteArray()), null, null);

}

4.3 socket传输

原生socket在发送大量数据时,会进行分包,接收的时候也就必须要合包;

有一些第三方socket框架会帮助我们处理合包的操作,websocket则是在它的协议标准中处理了这个问题。

所以该demo中选用了websocket进行通讯。拿到的截图的数据放进去就可以了。


import com.talon.screen.quick.util.LogWrapper;

import org.java_websocket.WebSocket;

import org.java_websocket.handshake.ClientHandshake;

import org.java_websocket.server.WebSocketServer;

import java.net.InetSocketAddress;

/**

* @author by Talon, Date on 2020-04-13.

* note: websocket 服务端

*/

public class MWebSocketServer extends WebSocketServer {

private final String TAG = "MWebSocketServer";

private WebSocket mWebSocket;

private boolean mIsStarted = false;

private CallBack mCallBack;

public MWebSocketServer(int port, CallBack callBack) {

super(new InetSocketAddress(port));

this.mCallBack = callBack;

setReuseAddr(true);

setConnectionLostTimeout(5 * 1000);

}

@Override

public void onOpen(WebSocket webSocket, ClientHandshake handshake) {

LogWrapper.d(TAG, "有用户链接");

mWebSocket = webSocket;

}

@Override

public void onClose(WebSocket conn, int code, String reason, boolean remote) {

LogWrapper.d(TAG, "有用户离开");

}

@Override

public void onMessage(WebSocket conn, String message) {

LogWrapper.e(TAG, "接收到消息:" + message);

}

@Override

public void onError(WebSocket conn, Exception ex) {

LogWrapper.e(TAG, "发生error:" + ex.toString());

}

@Override

public void onStart() {

updateServerStatus(true);

}

/**

* 停止服务器

*/

public void socketStop() {

try {

super.stop(100);

updateServerStatus(false);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

/**

* 发送二进制

*

* @param bytes

*/

public void sendBytes(byte[] bytes) {

if (mWebSocket != null)

mWebSocket.send(bytes);

}

private void updateServerStatus(boolean isStarted) {

mIsStarted = isStarted;

LogWrapper.e(TAG, "mIsStarted:" + mIsStarted);

// 回调

if (mCallBack != null)

mCallBack.onServerStatus(isStarted);

}

public boolean isStarted() {

LogWrapper.e(TAG, "mIsStarted:" + mIsStarted);

return mIsStarted;

}

public interface CallBack {

void onServerStatus(boolean isStarted);

}

}

Client:


import com.talon.screen.quick.util.BitmapUtils;

import com.talon.screen.quick.util.LogWrapper;

import org.java_websocket.client.WebSocketClient;

import org.java_websocket.handshake.ServerHandshake;

import java.net.SocketException;

import java.net.URI;

import java.nio.ByteBuffer;

/**

* @author by Talon, Date on 2020-04-13.

* note: websocket 客户端

*/

public class MWebSocketClient extends WebSocketClient {

private final String TAG = "MWebSocketClient";

private boolean mIsConnected = false;

private CallBack mCallBack;

public MWebSocketClient(URI serverUri, CallBack callBack) {

super(serverUri);

this.mCallBack = callBack;

}

@Override

public void onOpen(ServerHandshake handshakeData) {

LogWrapper.e(TAG, "onOpen");

updateClientStatus(true);

try {

getSocket().setReceiveBufferSize(5 * 1024 * 1024);

} catch (SocketException e) {

e.printStackTrace();

}

}

@Override

public void onMessage(String message) {

}

@Override

public void onMessage(ByteBuffer bytes) {

byte[] buf = new byte[bytes.remaining()];

bytes.get(buf);

if (mCallBack != null)

mCallBack.onBitmapReceived(BitmapUtils.decodeImg(buf));

}

@Override

public void onClose(int code, String reason, boolean remote) {

updateClientStatus(false);

}

@Override

public void onError(Exception ex) {

updateClientStatus(false);

}

private void updateClientStatus(boolean isConnected) {

mIsConnected = isConnected;

LogWrapper.d(TAG, "mIsConnected:" + mIsConnected);

// 回调

if (mCallBack != null)

mCallBack.onClientStatus(isConnected);

}

public boolean isConnected() {

LogWrapper.d(TAG, "mIsConnected:" + mIsConnected);

return mIsConnected;

}

public interface CallBack {

void onClientStatus(boolean isConnected);

void onBitmapReceived(Bitmap bitmap);

}

}

4.4 Demo 下载地址

下载Demo