by fanxiushu 2022-03-12 转载或引用请注明原作者。
接上文。
我们先来编译kvswebrtc开源代码。
首先得从github下载 ksvwebrtc源码,
分别需要  amazon-kinesis-video-streams-pic, amazon-kinesis-video-streams-producer-c,
amazon-kinesis-video-streams-webrtc-sdk-c 三个,其中amazon-kinesis-video-streams-producer-c其实只需要头文件即可,不必编译。

另外还必须要 usrsctp,libsrtp 两个开源库。openssl 开源库也是必须的。这些开源库都可以直接从github下载。
openssl如何编译这里就不罗嗦了,网上很多介绍。
先来看usrsctp和libsrtp如何编译
(这里以windows的编译为例,其他平台下尤其linux下,这些开源库都是非常容易编译的)
首先在windows中安装最新的cmake程序,
然后打开 cmake-gui程序,直接在cmake界面中操作即可,直接编译成 visual studio 的sln工程文件,
(libsrtp需要 openssl ,因此在 cmake-gui中需要配置 OPENSSL_ROOT_DIR 等变量)
打开sln,直接通过vs编译成lib静态库。编译生成 srtp2.lib 和 usrsctp.lib两个静态库。
就这么简单。
同样的,对于kvswebrtc的上面提到三个开源代码(其实只需要编译两个)
使用cmake生成amazon-kinesis-video-streams-pic 的sln工程文件的时候,需要注意在cmake中设置 BUILD_DEPENDENCIES=OFF,
这样就不会自动下载和编译关联项,因为我们只需要 amazon-kinesis-video-streams-pic,
编译成功之后,会生成 kvspic.lib, kvspicClient.lib, kvspicState.lib, kvspicUtils.lib 四个lib静态库。

接下来就是稍微麻烦点的 amazon-kinesis-video-streams-webrtc-sdk-c 编译问题。
首先,我们得把 各种头文件复制到某个公共目录中,比如新建一个 include 目录,
把 usrsctp, libsrtp, amazon-kinesis-video-streams-pic,amazon-kinesis-video-streams-producer-c ,
amazon-kinesis-video-streams-webrtc-sdk-c 里边的头文件复制到 include目录中,
同时,我们必须手工修改 amazon-kinesis-video-streams-webrtc-sdk-c  的CMakeList.txt文件。
把kvsWebrtcSignalingClient 相关工程部分屏蔽掉,因为只需要webrtc标准通信部分,至于信令我们自己实现即可。
编译 amazon-kinesis-video-streams-webrtc-sdk-c 的时候 cmake除了添加 BUILD_DEPENDENCIES=OFF,
也必须添加 OPENSSL_ROOT_DIR,
生成sln工程,使用VS打开sln之后,把上面的include目录加入到包含目录中,
还得修改某些代码,主要是屏蔽 libwebsocket的调用和注释掉kvsWebrtcSignalingClient 相关部分。
编译会生成 kvsWebrtcClient.lib 库。

所以,最终连接到我们程序中的就是上面编译成功的7个lib静态库文件,当然还包括openssl生成的两个静态库文件。

不管怎么说,这些编译和修改比起gstreamer的编译和修改(上一篇文件中描述编译gstreamer过程),简直是太简单了。
也比谷歌自己的WebRTC编译简单的多,更重要的是kvswebrtc全部使用纯C语言开发,在各种平台下编译都变得很友好。

接下来,我们再来介绍如何使用 kvswebrtc的API接口,其实也是非常简洁的。
API接口的调用方式接近javascript的webRTC接口.
首先,在程序开始的地方调用 initKvsWebRtc 函数,初始化 kvswebrtc
调用 createPeerConnection 函数创建 RtcPeerConnection ,
再然后调用 peerConnectionOnIceCandidate 和peerConnectionOnConnectionStateChange 设置 回调函数,
调用 addTransceiver 设置媒体通道的传输 Transceiver,
如果是数据通道,调用 createDataChannel 创建数据通道,接着调用 dataChannelOnOpen打开数据通道,
调用 dataChannelOnMessage  设置数据通道接收回调函数。

接着调用createOffer 创建OfferSDP,setLocalDescription 函数把OfferSDP设置到本地,调用此函数之后,
kvswebrtc开始收集本网络和ICE等信息,通过 peerConnectionOnIceCandidate  设置的回调函数返回给调用者。
然后我们把OfferSDP通过我们自己的通信协议发给浏览器客户端,同时也把生成的 IceCandidate等信息发给浏览器客户端。
当接收到浏览器客户端的AnswerSDP之后,调用setRemoteDescription 设置到本地,
接收到浏览器客户端的IceCandidate之后,调用addIceCandidate 添加到本地。
当成功建立webrtc连接之后(具体通过peerConnectionOnConnectionStateChange 设置的回调函数指示是否成功建立WebRTC)
我们要把已经编码成H264的视频流传输给浏览器客户端,直接调用kvswebrtc提供的writeFrame 函数即可。
于是整个kvswebrtc就这样跑通了。

是不是比起谷歌的WebRTC和gstreamer的调用方式简单得太多,而且我测试的使用效果也并不差。
真是没有比较就没有伤害啊。
具体如何使用kvswebrtc,请去查阅kvswebrtc提供的例子代码。

上一篇文章中说过了,在开发WebRTC中,偶然发现 MSE 也可以提供低延迟,实时性的基于video标签的渲染。
于是接下来我们再来研究MSE的实现方式。
开始之前,先来张已经在xdisp_virt中实现了webRTC和MSE的截图用于提神:



上图中,video标签渲染部分就是xdisp_virt最新实现的WebRTC和MSE的功能,
在最新实现中,声音编码依然是通过原来的方式进行处理,只有视频编码才通过WebRTC或MSE进行处理。
下面的 “通用(WebGL渲染)”,就是xdisp_virt原先实现的功能,并且在其中还新添加了WebRTC数据通道传输音视频数据。
最新版本请关注GITHUB上的更新,最近会把新版本xdisp_virt发布上去。

我们接着再来阐述如何实现MSE功能,
MSE需要生成 fMP4 格式的流,然后再喂给 MSE,这样才能正常使用。
现在关键是如何生成 fMP4 的格式流,
并不打算在xdisp_virt程序端生成 fMP4,因为这样改动较大,还得专门增加一个协议来传输fMP4流,
于是最好的办法,保持现有的传输方式不变(WebSocket传输 H264 编码),直接在javascript端把H264编码流转成 fMP4 流。

现在的问题是如何使用javascript把H264编码转成 fMP4 流。
其实网上也有一些直接使用js实现的 转 fMP4 的代码,比如 jmuxer等,但使用并不理想,
主要是因为我这边xdisp_virt有自己的一套实现方式,而且更主要的当使用jmuxer运行MSE渲染,
然后把电脑从浏览器切换到其他程序,
再然后切换回来,结果要么浏览器上的画面卡死,要么就是非常神奇的画面快速播放,在极速清空切换后没有播放的画面。
也懒得去修改jmuxer了,况且可能会越改越乱。

于是再次想到了 ffmpeg 这个神器。既然前面实现中,统一使用生成wasm的ffmpeg来解码各种图像和音频编码。
现在再增加一个 生成 fMP4 的接口,自然也没任何问题。事实上也确实这样。

如何使用 ffmpeg框架,把 H264 编码流转成 fMP4 流呢?
其实跟 转成 本地MP4,flv,mkv这些视频等没啥区别。
还记得我很早前的文章中,有专门描述过如何利用 ffmpeg 转成RTSP,RTMP以及本地视频文件。地址:
Windows远程桌面实现之五(FFMPEG实现桌面屏幕RTSP,RTMP推流及本地保存)_fanxiushu的专栏-CSDN博客_ffmpeg 桌面推流
具体就是讲述远程桌面文章系列的第五篇文章所描述的内容,
并且在GITHUB上还提供了 stream_push 开源代码与此文相对应。
地址:https://github.com/fanxiushu/stream_push

现在,我们只需要把 stream_push 的源码稍微做些修改,就可以把 AnnexB格式的H264裸流 转成 fMP4 流。
1,首先在调用 avformat_write_header 写头之前,需要设置 fMP4属性,
      av_dict_set(&options, "movflags", "empty_moov+default_base_moof+frag_keyframe", 0);
      avformat_write_header(ofm_ctx, options ? &options : NULL);
      这样就能确保 生成 fMP4 格式的流。
2,当然需要删除stream_push里边某些不必要的代码,因为只需要生成 MP4 格式的视频。
     因此在stream_push工程的open函数中,也就是创建 AVFormatContext的过程中,按照如下方式创建:
      avioc_buffer_size = 8 * 1024 * 1024;
      avioc_buffer = (uint8_t *)av_malloc(avioc_buffer_size);
      ofm_ctx = avformat_alloc_context();
      ofm_ctx->oformat = av_guess_format("mp4", NULL, NULL);
      ofm_ctx->pb = avio_alloc_context(avioc_buffer, avioc_buffer_size, AVIO_FLAG_WRITE, this, NULL, write_packet, NULL);
    其中write_packet是个回调函数,表示当ffmpeg 生成fMP4流的时候,这个回调函数就会调用,
     通常是调用 av_write_frame 之后,或者ffmpeg缓存满了需要写出数据的时候调用。
    相当于是已经生成了fMP4流数据,按照普通方式是直接写到本地文件中,而在这里,我们需要这个数据,因此直接通过回调函数来获取。
3,在上一篇文中讲述过,因为我们需要实时性的 fMP4 格式流,也就是意味着每来一帧 H264编码帧,都应该立即刷出 fMP4 流。
      这在 ffmpeg的实现中,是可以做到的。
     具体就是在 stream_push工程的 send_packet 函数中,在调用完成  av_interleaved_write_frame 函数之后,
     再次调用 av_write_frame(ofm_ctx, NULL); 记住最后一个参数是NULL,
     这样就会让 ffmpeg 把av_interleaved_write_frame 写入的数据帧,立即刷新出来,也就是 write_packet回调函数立即会被调用。

以上就是利用stream_push工程改造生成 fMP4 流的需要注意的三个要点。
有兴趣可以自己使用 stream_push去实现 fMP4 流的功能。
当然,最后再使用 emscripten 工具,编译生成 wasm,提供给js前端调用。

接下来,浏览器前端已经获取到H264转成的 fMP4 流,如何喂给MSE呢?
这个其实也是挺简单的,我们不要使用各种开源框架,直接使用MSE的接口函数就可以了。

按照如下方法创建:
    var mimeCodec = 'video/mp4; codecs="avc1.42E01E"'; //是否支持 H264

    if ('MediaSource' in window && MediaSource.isTypeSupported(mimeCodec)) {
    }
    else {
        alert('Can not Supported MSE(Media Source Extensions)');
        return;
    }

    /
    media_source = new MediaSource();
    canvas.src = URL.createObjectURL(media_source);
    media_source.addEventListener('sourceopen', function () {
        
        media_source.duration = Infinity; ///
       
        media_buffer = media_source.addSourceBuffer(mimeCodec);

        media_buffer.mode = 'sequence';
    });

这样在每次到来 fMP4流的时候,调用  media_buffer.appendBuffer( fmp4_stream_data ) 添加到MSE中。
真是要多简单就有多简单。

不过这里需要注意一些问题,应该在media_buffer 没有updating的时候添加fmp4流,否则会添加失败,这样会造成丢帧。
因此我们实际上是先把 fmp4 流数据拼接到 某个队列中,然后判断updating,再决定是否添加到 media_buffer 中
具体如下:
var media_queue = new Uint8Array();
function media_source_push_data(data)
{
    var t = new Uint8Array(media_queue.length + data.length);
    t.set(media_queue, 0);
    t.set(data, media_queue.length);
    media_queue = t;
    /
    if( media_buffer && media_queue.length > 0 && !media_buffer.updating){
        media_buffer.appendBuffer(media_queue);
        
        media_queue = new Uint8Array(); //reset
    }
    /
}

其次,因为我们需要实时性的MSE,因此一个非常重要做的事情,就是得不断清空多余的缓存,
我也不清楚现在的浏览器实现MSE的时候,为何总是喜欢缓存,缓存一旦变大,很大的延迟就会来了,
这对于远程桌面这类需求(需要非常高的实时性)是无法容忍的。
具体做法如下,(下面的canvas是 video标签的变量值)
设置一个每间隔100毫秒就执行的定时清理任务:
setInterval(function () {
        /// flush
        if (media_buffer && media_queue.length > 0 && !media_buffer.updating) {
            console.log('-- setInterval flush media queue.len=' + media_queue.length);
           
            media_buffer.appendBuffer(media_queue);
            
            media_queue = new Uint8Array(); //reset
        }

        ///剔除缓存太多的帧,目的是为了实现实时性,否则造容易成长期缓存,
        ///这么做的结果就是对于MSE的实时性支持差的浏览器,非常卡顿。
        if (canvas.buffered && canvas.buffered.length > 0 && !canvas.seeking) {
            const end = canvas.buffered.end(0);
            if ( (end - canvas.currentTime )*1000 >= 200 ) { // > 200 ms
               
                console.log('video delay seek to end, curr=' + canvas.currentTime +', end='+end );
                canvas.currentTime = end - 0.001;
            }
        }

       
    }, 100);

按照如上方式实现的MSE,在chrome内核浏览器上运行,实时性的效果是比较理想的,
不过比起 webRTC和WebGL渲染,依然有一些逊色。
至于在其他内核浏览器(其实主要是WebKit和Firefox浏览器,现在主流的浏览器内核无非就这三种)
则是非常卡顿,卡顿的原因是上面代码中,超过200毫秒的缓存就会被清除掉。
也就是说其他两个内核版本的浏览器,对低延迟的MSE,支持得比较差。

最后,gif图片简单演示一下xdisp_virt最新实现的 WebRTC和MSE功能的效果:

演示中,还可以看到xdisp_virt实现了 FrameBuffer截屏,
也就是可以截取到登录纯字符界面的linux。

因为上传图片大小有限,因此压缩的很厉害。
实际体验请去GITHUB下载最新版本的xdisp_virt ,
稍后会把添加WebRTC和MSE功能的xdisp_virt发布到GITHUB上:
https://github.com/fanxiushu/xdisp_virt

 

 

Logo

致力于链接即构和开发者,提供实时互动和元宇宙领域的前沿洞察、技术分享和丰富的开发者活动,共建实时互动世界。

更多推荐