Android屏幕共享-硬编码硬解码

本文介绍了如何在Android应用中使用Java实现屏幕共享,区分了软解码和硬解码的区别,详细阐述了硬编码(使用MediaCodec)和硬解码的过程,包括权限请求、编码设置、解码处理以及数据传输,最后提到使用WebSocket进行高效数据传输。
摘要由CSDN通过智能技术生成

Android屏幕共享-硬编码硬解码

说起Android之间的屏幕共享,第一次接触会比较陌生,不过大家多少有了解过ffmpeg,看上去是不是很熟悉?ffmpeg是一套处理音视频的开源程序,但对于C了解较少的同学,编译起来很复杂。

有同学问有没有纯JAVA操作的方法呢,还真有。

一、效果图

Demo界面

二、软解码和硬解码

  • 软解码

利用CPU的计算进行解码,比如使用FFmpeg解码,由于解码是通过CPU运算,所以加大CPU负担,增加耗电。

  • 硬解码

利用手机自带处理视频的芯片专门模块编码进行解码,如 dsp。对CPU要求比较低,主要依赖于硬件,所以解码芯片在不同的手机上,表现可能会有不一致的情况。好处是硬解由于是单独的处理芯片,所以速度比软解码要快。

三、代码分析

3.1 Android 硬编码

硬编码主要是使用MediaCodec访问底层的codec来实现编解码,它是Android提供的用于对音视频进行编解码的类。

整体来说步骤分为以下步骤:

详细点的代码如下:

  1. 申请录屏权限

private void requestCapturePermission() throws Exception {

if ((Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP)) {

//5.0 之后才允许使用屏幕截图

mMediaProjectionManager = (MediaProjectionManager) getSystemService(

Context.MEDIA_PROJECTION_SERVICE);

startActivityForResult(mMediaProjectionManager.createScreenCaptureIntent(),

REQUEST_MEDIA_PROJECTION);

} else {

throw new Exception("android版本低于5.0");

}

}

2.在确认的回调中得到MediaProjection

MediaProjection mediaProjection = mMediaProjectionManager.getMediaProjection(resultCode, data);
  1. 配置并获取MediaCodec

private MediaCodec prepareVideoEncoder() throws IOException {

MediaFormat format = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, mVideoEncodeConfig.width, mVideoEncodeConfig.height);

format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);

// format.setInteger(KEY_BIT_RATE, (int) (mVideoEncodeConfig.width * mVideoEncodeConfig.height * mVideoEncodeConfig.rate * mVideoEncodeConfig.factor));

format.setInteger(KEY_BIT_RATE, (int) (mVideoEncodeConfig.width * mVideoEncodeConfig.height * mVideoEncodeConfig.rate * mVideoEncodeConfig.factor));

format.setInteger(KEY_FRAME_RATE, mVideoEncodeConfig.rate); //帧

format.setInteger(KEY_I_FRAME_INTERVAL, mVideoEncodeConfig.i_frame);

// 该代码能够达到很强的清晰度,但是在华为nova 5i 10。0上不支持。参考:https://www.jianshu.com/p/a0873b4a92b6

// format.setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CQ);

// -----------------ADD BY XU.WANG 当画面静止时,重复最后一帧--------------------------------------------------------

format.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, 1000000 / 45);

//------------------MODIFY BY XU.WANG 为解决MIUI9.5花屏而增加...-------------------------------

if (Build.MANUFACTURER.equalsIgnoreCase("XIAOMI")) {

format.setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CQ);

} else {

format.setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR);

}

format.setInteger(MediaFormat.KEY_COMPLEXITY, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR);

MediaCodec mediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);

mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);

Surface surface = mediaCodec.createInputSurface();

mVirtualDisplay = mMediaProjection.createVirtualDisplay("-display", mVideoEncodeConfig.width, mVideoEncodeConfig.height, 1,

DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC, surface, null, null);

return mediaCodec;

}

4.启动MediaCodeC

mMediaCodec.start();

5.开启线程,不断的编码


mVideoEncodeThread = new Thread(new Runnable() {

@Override

public void run() {

while (mVideoCoding && !Thread.interrupted()) {

try {

ByteBuffer[] outputBuffers = mMediaCodec.getOutputBuffers();

int outputBufferId = mMediaCodec.dequeueOutputBuffer(vBufferInfo, 0);

if (outputBufferId >= 0) {

ByteBuffer bb = outputBuffers[outputBufferId];

onEncodedAvcFrame(bb, vBufferInfo);

mMediaCodec.releaseOutputBuffer(outputBufferId, false);

}

} catch (Exception e) {

e.printStackTrace();

break;

}

}

}

});

6.在onEncodedAvcFrame处理编码数据

7.传输数据

3.2 Android 硬解码

整体来说步骤分为以下步骤:

详细代码步骤如下:

  1. 接收数据

将接收到的二进制数据流放进解码的工具类


@Override

public void onReceive(byte[] packet) {

mediaDecodeUtil.decodeFrame(packet);

}

2.创建SurfaceView

在SurfaceView创建后和解码类关联


surface_view.getHolder().addCallback(new SurfaceHolder.Callback() {

@Override

public void surfaceCreated(SurfaceHolder holder) {

Log.d(TAG, "surfaceCreated");

try {

if (mediaDecodeUtil != null)

mediaDecodeUtil.onInit(surface_view);

} catch (IOException e) {

e.printStackTrace();

}

}

@Override

public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

Log.d(TAG, "surfaceChanged");

}

@Override

public void surfaceDestroyed(SurfaceHolder holder) {

Log.d(TAG, "surfaceDestroyed");

if (mediaDecodeUtil != null)

mediaDecodeUtil.onDestroy();

}

});


private void configDecoder(MediaFormat newMediaFormat, SurfaceView surfaceView) {

if (mediaCodec == null) return;

// 在SurfaceView加载完成前,调用以下方法会报错,此处TryCatch用以应付在OnCreate中执行初始化导致的崩溃

try {

mediaCodec.stop();

mediaFormat = newMediaFormat;

// MediaCodec配置对应的SurfaceView

// !!!注意,这行代码需要SurfaceView界面绘制完成之后才可以调用!!!

mediaCodec.configure(newMediaFormat, surfaceView.getHolder().getSurface(), null, 0);

// 解码模式设置

// mediaFormat.setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR) // 表示编码器会尽量把输出码率控制为设定值

mediaFormat.setInteger(

MediaFormat.KEY_BITRATE_MODE,

MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CQ

); // 示完全不控制码率,尽最大可能保证图像质量

// mediaFormat.setInteger( MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR) // 表示编码器会根据图像内容的复杂度(实际上是帧间变化量的大小)来动态调整输出码率,图像复杂则码率高,图像简单则码率低

mediaCodec.start();

// 设置视频保持纵横比,此方法必须在configure和start之后执行才有效

mediaCodec.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING);

} catch (Exception e) {

e.printStackTrace();

}

}

3.芯片解码

这部分的逻辑是这样的,每当接收到数据时,就去寻找有没有空闲的DSP解码芯片,有的话,就去处理,没有的话就设置了短暂时间循环去寻找空闲的解码芯片,因为只有当有空闲芯片时,才可以进行解码,不然会出现绿屏花屏现象。


private void decodeFrameDetail(byte[] bytes) {

// 找出dsp芯片可用区域的索引,如果有可用,则返回索引,如果没有,则返回 -1

int inIndex = mediaCodec.dequeueInputBuffer(TIME_OUT_US);

if (inIndex >= 0) {

// 取出对应索引的可用区域

ByteBuffer byteBuffer = mediaCodec.getInputBuffer(inIndex);

if (byteBuffer != null) {

// 把一帧的数据放入可用区域,

byteBuffer.put(bytes, 0, bytes.length);

mediaCodec.queueInputBuffer(inIndex, 0, bytes.length, 0, 0);

// mediaCodec.queueInputBuffer(inIndex, 0, bytes.length, System.currentTimeMillis(), 0);

}

} else {

// 如果没有可用的dsp,考虑用个for循环,循环5-10次查找可用的dsp。还不行就让他花屏把。

Log.d(TAG, "目前没有可用的dsp");

return;

}

// 取出编码好的数据

MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();

if (!isNeedContinuePlay) return;

int outIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, TIME_OUT_US);

// 编码好的全部取出来。

while (outIndex >= 0) {

mediaCodec.releaseOutputBuffer(outIndex, true);

outIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, TIME_OUT_US);

if (mIsNeedFixWH) {

fixHW();

mIsNeedFixWH = false;

}

}

}

3.3 传输

由于是编码好的字节流会发送频繁、并且数据量比较大,所以Android 原生的Socket使用TCP的会很麻烦,比如还要考虑分包粘包的场景,虽然这些问题可以解决,但为了方便起见,还是使用WebSocket,因为WebSocket的协议是基于包,而TCP基于流,既然WebSocket协议都已经帮忙做好了,那么Demo上就先用WebSocket。

  1. 首先需要引入依赖,因为不属于原生的范畴
implementation "org.java-websocket:Java-WebSocket:1.3.6"

2.封装两个工具类,一个客户端,一个服务端,使得我们拿到编码好的数据就可以放工具类中放。

客户端重点代码


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) {

// ...

}

@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.onClientReceive(buf);

}

@Override

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

// ...

}

@Override

public void onError(Exception ex) {

// ...

}

}

服务端重点代码


public class MWebSocketServer extends WebSocketServer {

@Override

public void onOpen(WebSocket webSocket, ClientHandshake handshake) {

}

@Override

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

}

@Override

public void onMessage(WebSocket conn, String message) {

}

@Override

public void onError(WebSocket conn, Exception ex) {

}

@Override

public void onStart() {

}

}

粉丝福利, 免费领取C++音视频学习资料包+学习路线大纲、技术视频/代码,内容包括(音视频开发,面试题,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,编解码,推拉流,srs)↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓

  • 11
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值