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
?
?
|