Skip to content

上节课,我们提到了在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会话流程到怎么实现会议等等。