通过Websocket进行图片流传输来实现
现在要实现Android采集屏幕通过Websocket在另一个Android设备上显示
那么我们就要采集屏幕
=>生成二进制
=>ws传输
=>ws接收
=>二进制转图片
=>播放图片
在接入websocket之前 我们现在本地实现采集屏幕
=>生成二进制
=>二进制转图片
=>播放图片
这样的流程
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
,该值必须为true
。true
,将不会将图片加载到内存中,但是可以获得宽高。inJustDecodeBounds
配合来加载大图片,在这里我直接设置为1,这样做实际上是有问题的,如果图片过大很容易发生OOM。注意
我们在用BitmapFactory生成图片的时候如果不设置的option的话,每次都会生成新的Bitmap对象,频繁的生成释放会导致内存抖动,所以可以用inBitmap来防止,我这里暂时还没用,如果使用的话,我们可以定义一个图片池,循环利用其中的对象,但是一定要保证正在展示的对象不能被同时被修改,会导致显示有横线。 图片的编码格式用png和webp都可以,不知道为啥jpeg不行,很是奇怪!
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了,所以我在缓存中至多保留最新的三个,其它的丢弃
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对象被修改,导致图片中会产生横线。
考虑到以后二进制传输其它类型的数据,所以我这里定义了数据的格式
数据头
+JSON数据
+传输数据
数据头
用来保存JSON数据的长度,方便截取JSONJSON数据
中保存要传输的参数传输数据
才是真正要传输的二进制数据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)
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) 代码解释:
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有