前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >1对1直播源码开发,Android获取实时屏幕画面

1对1直播源码开发,Android获取实时屏幕画面

作者头像
云豹科技程序员
修改2021-06-18 18:13:12
1.7K0
修改2021-06-18 18:13:12
举报

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的现象。

以下是关键部分的代码:

代码语言:javascript
复制
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进行编码格式设置,代码如下。

代码语言:javascript
复制
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即是我们需要用到的实时视频帧数据了。代码如下:

代码语言:javascript
复制
 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字段。

代码语言:javascript
复制
...
 
} 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通讯和转码的关键代码:

代码语言:javascript
复制
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上。

代码语言:javascript
复制
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 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云直播
云直播(Cloud Streaming Services,CSS)为您提供极速、稳定、专业的云端直播处理服务,根据业务的不同直播场景需求,云直播提供了标准直播、快直播、云导播台三种服务,分别针对大规模实时观看、超低延时直播、便捷云端导播的场景,配合腾讯云视立方·直播 SDK,为您提供一站式的音视频直播解决方案。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档