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() {
@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 硬解码
整体来说步骤分为以下步骤:
详细代码步骤如下:
- 接收数据
将接收到的二进制数据流放进解码的工具类
@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。
- 首先需要引入依赖,因为不属于原生的范畴
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)↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓