1对1直播源码开发,Android获取实时屏幕画面是如何实现的呢?因为VirtualDisplay可以获取当前屏幕的视频流,创建VirtualDisplay只需通过MediaProjectionManager获取MediaProjection,然后通过MediaProjection创建VirtualDisplay即可。
那么1对1直播源码中视频数据的流向是怎样的呢?
首先,Display 会将画面投影到 VirtualDisplay中; 接着,VirtualDisplay 会将图像渲染到 Surface中,而这个Surface是由MediaCodec所创建的; 最后,用户可以在1对1直播源码中通过MediaCodec获取特定编码的视频流数据。
在这个场景下,MediaCodec只允许使用video/avc编码类型,也就是RAW H.264的视频编码,使用其他的编码会出现应用Crash的现象。
以下是关键部分的代码:
codec = MediaCodec.createEncoderByType(MIME_TYPE);
mSurface = codec.createInputSurface();
mVirtualDisplay = mMediaProjection.createVirtualDisplay(
name,
mWidth,
mHeight,
mDpi,
DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC,
mSurface, // 图像会渲染到Surface中
null,
null);
在1对1直播源码的视频流编码之前,我们还需要设置视频编码的一些格式信息,这里我们通过MediaFormat进行编码格式设置,代码如下。
private static final String MIME_TYPE = "video/avc"; // H.264编码
private static final int FRAME_RATE = 30; // 30 FPS
private static final int IFRAME_INTERVAL = 10; // I-frames间隔时间
private static final int TIMEOUT_US = 10000;
private void prepareEncoder() throws IOException {
MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, mWidth, mHeight);
format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
format.setInteger(MediaFormat.KEY_BIT_RATE, mBitRate);
format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL);
codec = MediaCodec.createEncoderByType(MIME_TYPE);
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
codec.start();
}
数据获取
紧接着,我们需要1对1直播源码实时获取视频流了,我们可以直接从MediaCodec中获取视频数据。
获取视频流有两种做法:
一种是通过异步的方式获取数据,使用回调来获取OutputBuffer。 另一种是同步获取的方式,由于是同步执行,为了不阻塞主线程,必然需要启动一个新线程来处理。
首先,程序会进入一个循环(可以设置变量进行停止),我们通过codec.dequeueOutputBuffer()方法获取到outputBufferId,接着通过ID获取buffer。这个buffer即是我们需要用到的实时视频帧数据了。代码如下:
MediaFormat outputFormat = codec.getOutputFormat(); // 方式二
codec.start();
for (;;) {
int outputBufferId = codec.dequeueOutputBuffer(mBufferInfo, 10000);
if (outputBufferId >= 0) {
ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
// 方式一
MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId);
codec.releaseOutputBuffer(outputBufferId, …);
} else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
outputFormat = codec.getOutputFormat(); // 方式二
}
}
codec.stop();
codec.release();
按照ScreenRecorder项目3的做法,接着他使用MediaMuxer的Muxer.writeSampleData()方法,直接将视频流outputBuffer写入了文件。
然而,我们需要的是实时推流至服务器。那么,接下去应该如何实现呢?
视频推流
H.264编码 众所周知,视频编码格式种类繁多,H.264也是其中一种编码,每一种编码都有其特点和适用场景,更多信息请自行搜索,这里不多做赘述。期间,我们尝试过将上面获取到的视频帧数据保存为文件,想研究视频文件为什么会呈现为绿屏的画面。经过翻阅资料和试验我们发现,H.264编码有着特殊的分层结构。
H.264 的功能分为两层:视频编码层(VCL, Video Coding Layer)和网络提取层(NAL, Network Abstraction Layer)。VCL 数据即编码处理的输出,它表示被压缩编码后的视频数据 序列。在 VCL 数据传输或存储之前,这些编码的 VCL 数据,先被映射或封装进 NAL 单元中。每个 NAL 单元包括一个原始字节序列负荷(RBSP, Raw Byte Sequence Payload)、一组对应于视频编码的 NAL 头信息。RBSP 的基本结构是:在原始编码数据的后面填加了结尾比特。一个bit“1”若干比特“0”,以便字节对齐。
NAL
因此,为了将帧序列变成合法的H.264编码,我们需要手动构建NAL单元。H.264的帧是以NAL单元为单位进行封装的,NAL单元的结构如上图所示。H.264分为Annexb和RTP两种格式,RTP格式更适合用于网络传输,因为其结构更加节省空间,但由于Android系统提供的数据本身就是Annexb格式的,因此我们采用Annexb格式进行传输。
按照Annexb格式的要求,我们需要将数据封装为如下格式:
0000 0001 + SPS + 0000 0001 + PPS + 0000 0001 + 视频帧(IDR帧) H.264的SPS和PPS串,包含了初始化H.264解码器所需要的信息参数,包括编码所用的profile,level,图像的宽和高,deblock滤波器等。
然后不断重复以上格式即可输出正确的H.264编码的视频流了。这里的SPS和PPS在每一个NAL单元中重复存在,主要是适用于流式传播的场景,设想一下如果流式传播过程中漏掉了开头的SPS和PPS,那么整个视频流将永远无法被正确解码。
我们在实践过程中,SPS和PPS只传递了一次,这样的方式比较适合我们的项目场景,也比较省流量。因此在我们的方案中,格式变为如下形式:
0000 0001 + SPS + 0000 0001 + PPS + 0000 0001 + 视频帧(IDR帧)+ 0000 0001 + 视频帧 + … H.264编码比较复杂。
介绍完H.264的基本原理,下面看看Android上具体的实现。其实Android系统的MediaCodec类库已经帮助我们完成了较多的工作,我们只需要在1对1直播源码开始录制时(或每一次传输视频帧前)在视频帧之前写入SPS和PPS信息即可。MediaCodec已经默认在数据流(视频帧和SPS、PPS)之前添加了start code(0x01),我们不需要手动填写。
SPS和PPS分别对应了bufferFormat中的csd-0和csd-1字段。
...
} else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
MediaFormat outputFormat = codec.getOutputFormat();
outputFormat.getByteBuffer("csd-0"); // SPS
outputFormat.getByteBuffer("csd-1"); // PPS
/* 然后直接写入传输流 */
}
服务器端
1对1直播源码中实时的数据流通过Socket(tcp)传输到服务器端,服务器端采用Node.js实现视频流转码和WebSocket转播。为了使Web前端可以播放实时的视频,我们必须将格式转换为前端支持的视频格式,这里解码使用FFmpeg的Node.js封装。以下是Socket通讯和转码的关键代码:
var Transcoder = require('stream-transcoder');
var net = require('net');
net.createServer(function(sock) {
sock.on('close', function(data) {
console.log('CLOSED: ' +
sock.remoteAddress + ' ' + sock.remotePort);
});
sock.on('error', (err) => {
console.log(err)
});
// 转码 H.264 => mpeg1video
new Transcoder(sock)
.size(width, height)
.fps(30)
.videoBitrate(500 * 1000)
.format('mpeg1video')
.channels(0)
.stream()
.on('data', function(data) {
// WebSocket转播
socketServer.broadcast(data, {binary:true});
})
}).listen(9091);
Web直播
紧接着,Web前端与服务器建立WebSocket连接,使用jsmpeg项目7对mpeg1video的视频流进行解码并呈现在Canvas上。
var client = new WebSocket('ws://127.0.0.1:9092/');
var canvas = document.getElementById('videoCanvas');
var player = new jsmpeg(client, {canvas:canvas});
后续还可以做一些灵活的配置以及错误处理,可以让整个直播的流程更加稳定。至于视频方面的优化,也可以继续尝试各种参数的调节等等。
以上就是1对1直播源码开发,Android获取实时屏幕画面的全部内容了,希望可以帮助到有需要的人。
本文系转载,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文系转载,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。