上节课,我们提到了在MCU
服务器处理音视频的过程中,可能会涉及到合并媒体流,实际上,这个合并媒体流的“任务”,在客户端我们也可以借助某些巧妙设计做到的,比如Canvas
将两个媒体流通过“错位的”的方式贴合在一起,最终呈现出一个画面,但是画面是合成画面。
接下来,我们利用现成的开源组件库去实现这个目标。
组件基本应用
组件地址:Github。
演示地址:Demo。
首先安装组件依赖:
cnpm install video-stream-merger -S
实战
比如将一个 MP4 视频和摄像头的画面合成。
//组件引入
import "video-stream-merger";
//初始化合成画面容器
that.mergerVideo = new VideoStreamMerger(
{
width: 600, //设置容器的分辨率 宽
height: 400, //分辨率 高
fps: 25, //设置容器FPS
clearRect: true, //清除每一帧 从canvas
}
);
//加载Mp4画面作为底层画面
let videoFile = "http://localhost:8082/190318231014076505.mp4"
//创建视频承载DOM
var videoElement = document.createElement('video')
//设置DOM属性 playsinline 这个请注意 有些浏览器比如苹果手机浏览器 自动播放的时候会自动放大 而设置此属性可以不用放大
videoElement.playsinline = true;
//静音
videoElement.muted = true
videoElement.src = videoFile
videoElement.autoplay = true
//无限循环播放属性
videoElement.loop = true
videoElement.play()
//合成容器中添加创建的DOM元素
that.mergerVideo.addMediaElement('mp4',videoElement, {
x: 0,//画面帧起始位置
y: 0,
width: that.mergerVideo.width,//画面帧大小
height: that.mergerVideo.height,
mute: true //静音(重点设置哦)
});
//获取摄像头流 getLocalUserMedia()方法这里不再单独写了 完整代码见仓库
this.localstream = await this.getLocalUserMedia(null,null)
that.mergerVideo.addStream(this.localstream, {
x: 0,
y: 0,
width: 200,
height: 200,
mute: false //这个也是重点配置哦
});
//开始合并
that.mergerVideo.start();
//获取合并结果
console.log("merger.result",that.mergerVideo.result);
//挂载到DOM元素
await this.setDomVideoStream('videoElement',that.mergerVideo.result)
第一个要注意的点:我在上述代码中标记的重点,muted
参数,当你的两个媒体流都有自己的音频时,此时如果你将 muted
参数都设置为 False
,则输出的合成画面的音频也是混合的,因此听到的声音就是混乱的。所以在这里我们可以设置动态传参,手动设置可以控制要输出的音频,以便输出音质可以达到最佳。
第二个要注意的点:承载合成画面的分辨率,此分辨率直接控制了整体输出媒体的分辨率,因此,如果你决定在业务中使用合成流的时候,一定要根据当前网络状况谨慎设置。
效果
直播推合成流实战
接下来,将上述拿到的合成媒体流推送到我们的媒体服务器中,让媒体服务器分发直播。值得注意的是,以往我们在WebRTC
会话中是无法直接将普通的 MP4 或其他格式的本地视频流发送给对方的,通过此种方式,我们可以轻易地将在线地址、本地视频等“静态流”直接作为WebRTC
的媒体对等方。
async play(){
//获取上一步合成画面流
let megerVideo = await this.mergerVideoFC()
//推流到SRS中 推流成功则在右侧预览
await this.getPushSdp(this.streamId,megerVideo)
},
屏幕分享+摄像头合成推流
在以往视频会话过程中,如果遇到屏幕分享,自己的摄像头画面不会展示,这也是大多数会议系统常规的做法。但是,通过我们这节课学到的知识,就可以直接将屏幕分享流和摄像头流合并然后作为一个流分发,让对方不仅仅看到你的屏幕画面,同时能看到你的摄像头画面。
好了,接下来我们来看看怎么将两个动态媒体流组合起来。
//屏幕分享和摄像头
async mergerVideoSC(){
//摄像头流
this.localstream = await this.getLocalUserMedia(null,null)
//屏幕分享流
this.shareStream = await this.getShareMedia()
this.shareStatus= true
const that = this
//承载容器
that.mergerVideo = new VideoStreamMerger({ fps: 24, clearRect: true, });
//屏幕分享容器作为底层画面
that.mergerVideo.addStream(this.shareStream, {
x: 0,
y: 0,
width: that.mergerVideo.width,
height: that.mergerVideo.height,
mute: true
});
//摄像头左上角
that.mergerVideo.addStream(this.localstream, {
x: 0,
y: 0,
width: 200,
height: 150,
mute: false
});
//开始合成
that.mergerVideo.start();
//本地DOM挂载
await this.setDomVideoStream('videoElement',that.mergerVideo.result)
return that.mergerVideo.result
},
效果如下
合成的参数都是可以自己调的,比如位置在左上角或者右上角,自己按照实际情况来即可,设置的参数就是添加Stream
的时候指定即可。
以上的场景我们可以再具体下,当屏幕分享完成之后,想要关闭分享画面(关闭分享按钮在开始分享后下面会显示,注意动图中按钮的变化
),仅仅保留摄像头画面,怎么实现呢?实际上这个组件也是可以自动适配的,选择关闭后,对方的画面就只剩下一个了,但是请注意此时剩下那个画面的位置是有问题的,比如下面这样:
虽然对方看到的画面没有问题,但是我们这边显示的是有问题的,看着很别扭。因此我们可以按照常规做法,按照
我们前面反复提到的媒体控制(《10|会议实战:实时通话过程中音频、视频画面实时控制切换》),去直接替换RTCSender
中的发布流即可。
//直接置换发送器中的媒体流()
async changeVideo(){
//这里实际场景中一般不用重新获取 但是请一定要将此变量全局保存 以免无法直接控制正在发布的媒体流
//随意创建对浏览器资源和CPU消耗还是很大的
this.localStream= await this.getLocalUserMedia()
const [videoTrack] = this.localStream.getVideoTracks();
const senders = this.pc.getSenders();
const send = senders.find((s) => s.track.kind === 'video')
send.replaceTrack(videoTrack)
}
总结
通过这种方式可以解决多流传输而带来的宽带资源消耗的问题,同时也将服务端的“合流”能力以一种很巧妙的方式转移到了客户端,实际上,对客户端而言,也并没有消耗多少资源,因为本质上还是用的Canvas
画布,然后画布转换成流。
这样即使我们客户端有 N 多个画面:摄像头、静态视频播放、多个屏幕共享流,或者纯音频等都是可以合并到一个流中,如果将此功能在自己的业务中扩展,就可以实现一个画面编辑器,通过此在线编辑器组合任意流然后分发。
上面提到的在线编辑器像不像直播用到的直播姬
?将各种小的组件组合起来,然后作为直播的合成画面发布,观众看到的也是合成的,而且对于资源的占用也并不是很大。
相关源码
课后题
学完本节课,希望大家可以对前面学到的所有内容做个归纳,梳理一份自己会议系统相关的思维导图,从基础的WebRTC
会话流程到怎么实现会议等等。