Android无线传屏功能实现

前言

通过Websocket进行图片流传输来实现

现在要实现Android采集屏幕通过Websocket在另一个Android设备上显示

那么我们就要采集屏幕=>生成二进制=>ws传输=>ws接收=>二进制转图片=>播放图片

本地测试

在接入websocket之前 我们现在本地实现采集屏幕=>生成二进制=>二进制转图片=>播放图片这样的流程

图片工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.view.View;

import java.io.ByteArrayOutputStream;

public class ZJViewUtil {

private static ZJViewUtil viewUtil = null;

public static ZJViewUtil instance() {
if (viewUtil == null) {
viewUtil = new ZJViewUtil();
}
return viewUtil;
}

/**
* 获取视图的Bitmap
*
* @param v
* @return
*/
public Bitmap loadBitmapFromViewBySystem(View v) {
if (v == null) {
return null;
}
v.setDrawingCacheEnabled(true);
v.buildDrawingCache();
Bitmap bitmap = v.getDrawingCache();
return bitmap;
}


/**
* Bitmap转字节
*
* @param bitmap
* @return
*/
public byte[] bitmap2byte(Bitmap bitmap) {
if (bitmap != null) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos);
return baos.toByteArray();
} else {
return new byte[0];
}
}

/**
* 字节转Bitmap
*
* @param b
* @return
*/
public Bitmap byte2bitmap(byte[] b) {
Bitmap bitmap = BitmapFactory.decodeByteArray(
b,
0,
b.length
);

return bitmap;
}

/**
* Bitmap缩放
*
* @param bitmap
* @param width
* @param height
* @return
*/
public static Bitmap zoomBitmap(Bitmap bitmap, int width, int height) {
int w = bitmap.getWidth();
int h = bitmap.getHeight();
Matrix matrix = new Matrix();
float scaleWidth = ((float) width / w);
float scaleHeight = ((float) height / h);
matrix.postScale(scaleWidth, scaleHeight);
Bitmap newbmp = Bitmap.createBitmap(bitmap, 0, 0, w, h, matrix, true);
return newbmp;
}
}

先看下BitmapFactory.Options里我们使用的主要属性

  • inBitmap:如果该值不等于空,则在解码时重新使用这个Bitmap。
  • inMutable:Bitmap是否可变的,如果设置了inBitmap,该值必须为true
  • inPreferredConfig:指定解码颜色格式。
  • inJustDecodeBounds:如果设置为true,将不会将图片加载到内存中,但是可以获得宽高。
  • inSampleSize:图片缩放的倍数,如果设置为2代表加载到内存中的图片大小为原来的2分之一,这个值总是和inJustDecodeBounds配合来加载大图片,在这里我直接设置为1,这样做实际上是有问题的,如果图片过大很容易发生OOM。

注意

我们在用BitmapFactory生成图片的时候如果不设置的option的话,每次都会生成新的Bitmap对象,频繁的生成释放会导致内存抖动,所以可以用inBitmap来防止,我这里暂时还没用,如果使用的话,我们可以定义一个图片池,循环利用其中的对象,但是一定要保证正在展示的对象不能被同时被修改,会导致显示有横线。

图片的编码格式用png和webp都可以,不知道为啥jpeg不行,很是奇怪!

图片播放器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.SurfaceTexture;
import android.util.AttributeSet;
import android.view.TextureView;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;

import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Consumer;


public class PicturePlayerView extends TextureView implements TextureView.SurfaceTextureListener {

private Paint mPaint;//画笔
private Rect mSrcRect;
private Rect mDstRect;

private boolean available = false;

private List<Bitmap> mReusableBitmaps = Collections.synchronizedList(new ArrayList<Bitmap>());

List<Disposable> dplist = new ArrayList<>();

public PicturePlayerView(Context context) {
super(context);
init();
}

public PicturePlayerView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}

public PicturePlayerView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}

private void init() {
setOpaque(false);//设置背景透明,记住这里是[是否不透明]
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);//创建画笔
mSrcRect = new Rect();
mDstRect = new Rect();
this.setSurfaceTextureListener(this);
}

private void beginRender() {
Disposable dp = Observable.interval(0, 25, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<Long>() {
@Override
public void accept(Long aLong) throws Exception {
if (mReusableBitmaps.size() > 0) {
Bitmap bitmap = mReusableBitmaps.remove(0);
drawBitmap(bitmap);
}
}
});
dplist.add(dp);
}

@Override
public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int i, int i1) {
available = true;
this.beginRender();
}

@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int i, int i1) {

}

@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
for (Disposable dp : dplist) {
dp.dispose();
}
return false;
}

@Override
public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {

}

public void addBitmap(Bitmap bitmap) {
if (available) {
if (bitmap != null) {
if (mReusableBitmaps.size() > 3) {
Bitmap bt = mReusableBitmaps.remove(0);
recycleBitmap(bt);
}
mReusableBitmaps.add(bitmap);
}
}
}


private void drawBitmap(Bitmap bitmap) {
Canvas canvas = lockCanvas();//锁定画布
if (canvas != null) {
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);// 清空画布
mSrcRect.set(0, 0, bitmap.getWidth(), bitmap.getHeight());
mDstRect.set(0, 0, getWidth(), bitmap.getHeight() * getWidth() / bitmap.getWidth());
canvas.drawBitmap(bitmap, mSrcRect, mDstRect, mPaint);//将bitmap画到画布上
unlockCanvasAndPost(canvas);//解锁画布同时提交
recycleBitmap(bitmap);
}
}


private static void recycleBitmap(Bitmap bitmap) {
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle();
}
}
}

注意

图片播放是可以用ImageView来直接加载,但是问题是如果接收到的图片的间隔不一致的时候会感觉明显的卡顿,所以用自定义TextureView来处理,里面缓存要保存的图片,以每秒25帧播放,但是如果图片的产生速度较快的话,会导致缓存的图片越来越多,从而oom了,所以我在缓存中至多保留最新的三个,其它的丢弃

Activity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
private fun initTask() {
Observable.interval(0, 100, TimeUnit.MILLISECONDS)
.compose(this.bindToLifecycle())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
if (shownum < 10000) {
shownum += 1
} else {
shownum = 0
}
left_textview.text = "${shownum}"
renderImage()
}.isDisposed
}

private fun renderImage() {
val image = ZJViewUtil.instance().loadBitmapFromViewBySystem(leftview)
if (image != null) {
val imgnew = image.copy(Bitmap.Config.ARGB_8888, true)
//本地渲染
doAsync {
val bts = ZJViewUtil.instance().bitmap2byte(imgnew)
val bitmap = ZJViewUtil.instance().byte2bitmap(bts)
runOnUiThread {
picPlayerView.addBitmap(bitmap)
}
}
}
}

注意

image.copy(Bitmap.Config.ARGB_8888, true)

这句代码非常关键 在我们采集view的图片的时候,获取到的Bitmap对象的指针是不变的,如果不做copy,那么在我们异步转换二进制的时候就会中途Bitmap对象被修改,导致图片中会产生横线。

通过WS传输

考虑到以后二进制传输其它类型的数据,所以我这里定义了数据的格式

数据头+JSON数据+传输数据

  • 数据头用来保存JSON数据的长度,方便截取JSON
  • JSON数据中保存要传输的参数
  • 传输数据才是真正要传输的二进制数据

数据传输

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var msg = JSON.toJSONString(obj)
var bodyByteArr = msg.toByteArray(Charsets.UTF_8)
var bytelengthStr = "" + bodyByteArr.size
while (bytelengthStr.length < 6) {
bytelengthStr = "0" + bytelengthStr
}

var headByteArr = bytelengthStr.toByteArray(Charsets.UTF_8)

var data_arr = ByteArray(headByteArr.size + bodyByteArr.size + data.size)

System.arraycopy(headByteArr, 0, data_arr, 0, headByteArr.size)
System.arraycopy(bodyByteArr, 0, data_arr, headByteArr.size, bodyByteArr.size)
System.arraycopy(data, 0, data_arr, headByteArr.size + bodyByteArr.size, data.size)

数据接收

1
2
3
4
5
6
7
8
9
10
11
12
val dataHead = ByteArray(6)
System.arraycopy(dataAll, 0, dataHead, 0, 6)
val headStr = String(dataHead, Charsets.UTF_8)
L.e("headStr:${headStr}")
val jsonSize = headStr.toInt()
val dataJson = ByteArray(jsonSize)
System.arraycopy(dataAll, 6, dataJson, 0, jsonSize)
val jsonStr = String(dataJson, Charsets.UTF_8)
L.e("jsonStr:${jsonStr}")
val dataImageSize = dataAll.size - 6 - jsonSize
val dataImg = ByteArray(dataImageSize)
System.arraycopy(dataAll, 6 + jsonSize, dataImg, 0, dataImageSize)

方法介绍

public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
代码解释:

  • Object src : 原数组
  • int srcPos : 原数组的起始位置
  • Object dest : 目标数组
  • int destPos : 目标数组的起始位置
  • int length : 要Copy的数组的长度