Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Android无线传屏功能实现

Android无线传屏功能实现

作者头像
码客说
发布于 2020-06-15 02:34:41
发布于 2020-06-15 02:34:41
1.3K00
代码可运行
举报
文章被收录于专栏:码客码客
运行总次数:0
代码可运行

前言

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

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

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

本地测试

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

图片工具类

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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不行,很是奇怪!

图片播放器

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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数据中保存要传输的参数
  • 传输数据才是真正要传输的二进制数据

数据传输

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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)

数据接收

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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的数组的长度
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2020-06-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Android经典实战之TextureView原理和高级用法
TextureView 是一个继承自 View 的类,其主要优势在于能够直接在硬件加速层进行渲染。它允许应用将内容绘制到一个 SurfaceTexture,并能够将这个 SurfaceTexture 的内容呈现在其视图层级中。与 SurfaceView 不同,TextureView 支持复杂的视图层次并且可以与其他视图时序混用。这意味着,TextureView 能真正像普通的 View 一样参与到视图的动画和变换中。
AntDream
2024/09/13
1K0
Android经典实战之TextureView原理和高级用法
Android利用SurfaceView显示Camera图像爬坑记(四)
通过前面几篇,利用SurfaceView显示Camera的图像已经没什么问题了,接下来我们就要打磨一下细节,主要就是手机旋转的问题,考虑到我们会用横屏和竖屏的不同的情况。
Vaccae
2019/07/25
2.8K0
Android Picasso实现圆形图片和圆角图片
Android Picasso实现圆形图片和圆角图片 1.实现圆形图片 1.1代码调用如下 Picasso.with(mContext).load(headpic).memoryPolicy(MemoryPolicy.NO_CACHE) .transform(new CircleTransform(mContext)).into(ivIcon); 1.2自定义圆形图片处理工具类(可以修改圆形图片的半径) import android.content.Co
程序员飞飞
2020/02/27
2.1K0
自定义圆形图片控件
自定义圆形ImageView 圆形ImageView在头像显示用的比较普遍了,今天对于实现圆形ImageView做个总结; 主要思路是 重写 onDraw() ;方法有两个: 使用paint的Shader(着色器)将图片印在一个圆的画板上 使用Bitmap创建一个空的Canvas(画板),在画板上画一个圆和显示的图片,paint图像混合模式显示 着色器 方式 不带边框 思路 将图片压缩到和控件的大小一致 创建Bitmap 着色器 创建画笔并设置着色器 使用带有着色器的画笔在画板上画圆 private voi
佛系编码
2018/05/22
1.2K0
android 实现倒影
首先,文章中出现的Gallery 已经不再适用,替代方法请看我的另一篇文章http://blog.csdn.net/xiangzhihong8/article/details/51120460 不过对于文章中说的倒影的原理是可以借鉴的。 1.图片的显示以及切换主要是自定义了一个Gallery 下面是代码myGallery.java: import android.content.Context;   import android.graphics.Camera;   import androi
xiangzhihong
2018/02/01
1.5K0
android 实现倒影
相关推荐
Android经典实战之TextureView原理和高级用法
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验