这是我参与8月更文挑战的第12天,活动详情查看:8月更文挑战
Android屏幕共享-传输图片
该篇主要讲解安卓与安卓之间如何截图传输,属于安卓屏幕共享的入门篇。适用场景:安卓屏幕共享工具类应用。
一、效果图
效果图
demo界面
二、前言
目前安卓屏幕共享的方式有很多种软件已经实现,究其根源,无非就是采集-传输-播放
的过程,而安卓端无论是截图传输还是硬解码,也一定离不开核心类MediaProjection
,它就是Google开放了视频录制的接口(屏幕采集的接口)。
比较优秀的软件举例:
-
TeamViewer
-
向日葵
-
Vysor
其中Vysor就是通过adb
实现无root静默截图,然后传输,实现电脑控制手机。在整个屏幕共享的过程中,重点在于如何采集,采集到画质清晰内存又小的技术成为核心。
常用的安卓端采集技术有:
-
MediaProjection 实现截图
-
MediaProjection 硬解码
-
ffmpeg 软解码
-
adb 实现无root截图
三、功能点
-
android5.0及以上免root截图
-
安卓屏幕变化的时候采集
-
多端共享拓展简单
-
socket传输字节数组
-
尽可能都使用原生代码实现
四、功能讲解
4.1 截图
随着安卓系统的不断升级,权限也越来越收紧,无root并且不连接数据线做不到静默截图,这里指截取不属于该app的区域。
-
在允许连接数据线的情况下,可以通过adb shell push 事先编译好的 dex 文件到手机中,实现静默截图。
-
在没有链接数据线无root的情况下,只能显式的让用户有感知的进行截取。本篇截图使用的是
MediaProjection
, 如下图:
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);
}
}