Android屏幕共享-硬编码硬解码
说起Android之间的屏幕共享,第一次接触会比较陌生,不过大家多少有了解过ffmpeg,看上去是不是很熟悉?ffmpeg是一套处理音视频的开源程序,但对于C了解较少的同学,编译起来很复杂。
有同学问有没有纯JAVA操作的方法呢,还真有。
一、效果图
Demo界面
二、软解码和硬解码
- 软解码
利用CPU的计算进行解码,比如使用FFmpeg解码,由于解码是通过CPU运算,所以加大CPU负担,增加耗电。
- 硬解码
利用手机自带处理视频的芯片专门模块编码进行解码,如 dsp。对CPU要求比较低,主要依赖于硬件,所以解码芯片在不同的手机上,表现可能会有不一致的情况。好处是硬解由于是单独的处理芯片,所以速度比软解码要快。
三、代码分析
3.1 Android 硬编码
硬编码主要是使用MediaCodec访问底层的codec来实现编解码,它是Android提供的用于对音视频进行编解码的类。
整体来说步骤分为以下步骤:
详细点的代码如下:
- 申请录屏权限
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);
- 配置并获取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() {@Overridepublic 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 硬解码
整体来说步骤分为以下步骤:
详细代码步骤如下:
- 接收数据
将接收到的二进制数据流放进解码的工具类
@Overridepublic void onReceive(byte[] packet) {mediaDecodeUtil.decodeFrame(packet);}
2.创建SurfaceView
在SurfaceView创建后和解码类关联
surface_view.getHolder().addCallback(new SurfaceHolder.Callback() {@Overridepublic void surfaceCreated(SurfaceHolder holder) {Log.d(TAG, "surfaceCreated");try {if (mediaDecodeUtil != null)mediaDecodeUtil.onInit(surface_view);} catch (IOException e) {e.printStackTrace();}}@Overridepublic void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {Log.d(TAG, "surfaceChanged");}@Overridepublic 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芯片可用区域的索引,如果有可用,则返回索引,如果没有,则返回 -1int 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。
- 首先需要引入依赖,因为不属于原生的范畴
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;}@Overridepublic void onOpen(ServerHandshake handshakedata) {// ...}@Overridepublic void onMessage(String message) {// ...}@Overridepublic void onMessage(ByteBuffer bytes) {byte[] buf = new byte[bytes.remaining()];bytes.get(buf);if (mCallBack != null)mCallBack.onClientReceive(buf);}@Overridepublic void onClose(int code, String reason, boolean remote) {// ...}@Overridepublic void onError(Exception ex) {// ...}}
服务端重点代码
public class MWebSocketServer extends WebSocketServer {@Overridepublic void onOpen(WebSocket webSocket, ClientHandshake handshake) {}@Overridepublic void onClose(WebSocket conn, int code, String reason, boolean remote) {}@Overridepublic void onMessage(WebSocket conn, String message) {}@Overridepublic void onError(WebSocket conn, Exception ex) {}@Overridepublic void onStart() {}}
粉丝福利, 免费领取C++音视频学习资料包+学习路线大纲、技术视频/代码,内容包括(音视频开发,面试题,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,编解码,推拉流,srs)↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓