基于 Agora Web SDK 自定义直播画面

1 什么是自定义直播画面

默认情况下,我们的直播是以摄像头的直出画面为数据源进行推流的,但很多时候默认的直出画面无法满足业务需要,例如在直播的时候我们可能想要在画面的右上角加上自己的Logo。这种时候就需要使用一些方法先捕获视频源进行预处理以后再进行发送。不同平台处理视频源的方法不同,接下来我们以使用Agora Web SDK 4.x中基于Web RTC的API来描述如何获取视频源并进行数字合成。

使用Agora Web SDK做直播的基本流程很简单,其中最重要的两个部分是“推流鉴权”和“设置音视频源”。默认情况下所有的操作都已经经过了Agora Web SDK的高度封装,但如果我们需要对已有的视频源进行二次合成,就需要修改默认的配置。Agora Web SDK提供了自定义视频采集的API,API提供了一个方法createCustomVideoTrack用来创建自己的视频轨道(Video Track),在使用这个API之前,我们先简单回顾一下浏览器的Media Streams API。

2 媒体捕获和流控制接口 (Media Streams API)

2.1 基本概念

现代浏览器支持一整套完善的API去应对各种功能需求,大大增强了浏览器的处理能力,我们现在要讨论的媒体捕获和流控制接口 (Media Capture and Streams API),简称为媒体流接口 (Media Streams API),是专门为WebRTC设计的一套流媒体数据API,他提供的类和方法可以用于处理数据流以及构成这些数据流的轨道 (track)。

Media Streams API主要和Media Steam打交道,Media Stream是一个用来表示音视频相关数据的对象,一个Media Stream由多个Media Stream Track对象构成 (也可以一个也不包含)。每个Media Stream Track对象可以包含多个channelchannelMedia Stream的最小数据单位。任何流数据结构都有输入和输出,Media Stream也不例外,他可以接受一个摄像头作为视频输入,并且输出到一个<video>标签里。

2.2 Media Stream Track

上面我们提到一个Media Stream可以由多个Media Stream Track组成。为了获取Media Stream,通常我们可以调用MediaDevices.getUserMedia()接口。

navigator.mediaDevices.getUserMedia({ video: true, audio: true })
.then(function(stream) {
    var video = document.querySelector('video'); 
    //把我们的视频流插入video标签  
    video.src = window.URL.createObjectURL(stream); 
}).catch(function(err){
    //错误处理
    console.log(err);
});

获取Stream后,把我们的视频流插入HTML里的video标签,一个最简单的例子就完成了。其中,{ video: true, audio: true }是一个MediaStreamConstraints对象,用于说明请求的媒体类型。如果浏览器无法找到制定的媒体类型,那么就会进入到错误处理代码块。更多详细的使用方法可以参考官方文档

2.3 如何修改Stream

目前,如果我们需要直接对这个Stream进行二次修改,最常用的方法是借助于Canvas,把视频流内容输出到canvas元素中,然后对其进行后处理,最后把Canvas里的内容再次作为Media Stream Track添加到Media Stream中,就能达到二次处理的目的了。在看具体的代码之前,我们先回顾一下数字视频合成的一些基本概念,对这部分不感兴趣的读者可以直接跳到第4节。

3 数字视频合成

3.1 数字视频制作管线

现代数字视频制作管线通常分为三个阶段:前处理 (Pre-Production) ,生产 (Production) 和后处理 (Post-Production) [1]。前处理主要包含视频创作的设计、团队构建以及其他准备工作,生产阶段就是拍摄,后处理则包含剪辑、音效、音乐、颜色调整以及视觉效果。可见,后处理的比重占了很大一块,我们现在看到的很多软件,诸如NukeDaVinci ResolveAfter Effects都是用于做后处理的。数字视频制作管线可以简单的总结为下面的流程图 [2]。

其中,实心红色箭头代表图像,虚线红色箭头代表摄像机信息,黑色箭头代表3D数据。

3.2 数字合成

从上一节的流程图可以看出,数字合成也是后处理的一部分,并且所有数据最终都会流向数字合成中,因此数字合成理论在整个视频制作管线中非常重要。最早的数字合成理论要追溯到1981年Wallace的研究 [3]。现代主流数字合成基于两种方式:节点 (Node) 和图层 (Layer) [4],无论哪种方式,其底层图像处理的概念是一致的,在数字合成理论中,最重要的两个概念是透明度 (Alpha) 和预乘 (Premultiplication)。透明度就是我们熟知的RGBA中的A值。无论是3D建模软件还会2D图像处理软件都要根据这个数值进行最终的图像数据合成。预乘则是一个可选的操作,顾名思义就是预先把颜色值和透明度值相乘。例如,RGB (1.0, 0.5, 0.7)在透明度为0.5的情况下,预乘后的值是RGB(0.5, 0.25, 0.35)。从计算的角度,预乘可以提升合成阶段的性能,因为透明度值已经被预处理过了,但预乘也有缺陷,会影响到诸如颜滤镜、渐变以及绿背景移除等操作的准确度。

在视频剪辑软件中,通常可以选用预乘来优化视频处理性能,而在直播视频画面二次处理中,我们的数字合成通常是实时计算的,在下面的章节中,我们将会介绍如何利用Agora Web SDK进行自定义的视频处理。

4 自定义Media Stream Track

4.1 基于Canvas的方案

我们基于Agora官方示例basicLive项目进行改造。自定义视频流遵循下面的流程。

image

首先我们在index.html页面中引入自己的videocanvas

<div class="col">
    <video controls="controls" src="" id="my-demo" style="display:none;"></video>
    <canvas id="mycanvas" width="800" height="600"></canvas>
</div>

然后在basicLive.js中下列代码

var imagesLoaded = 0;
var imgTitle = loadImage("./title.png", imageLoadCompleted);
var context = mycanvas.getContext("2d");
var video = document.getElementById("my-demo");

function loadImage(src, onload) {
  var img = new Image();
  img.onload = function() {
      onload();
  };
  img.src = src;
  return img;
}

function imageLoadCompleted() {
  imagesLoaded += 1;
  // 这里只有一张图需要加载,因此已加载图片数量大于等于1即可
  if(imagesLoaded>=1) {
    if (navigator.mediaDevices === undefined) {
      navigator.mediaDevices = {};
    }
    var constraints = { audio: true, video: { width: 1280, height: 720 } };

    navigator.mediaDevices.getUserMedia(constraints)
    .then(function(mediaStream) {
      var video = document.querySelector('video');
      video.srcObject = mediaStream;
      video.onloadedmetadata = function(e) {
        video.addEventListener('loadeddata', function() {
          updateVideoToCanvas();
        });
        video.play();
      };
    })
    .catch(function(err) { console.log(err.name + ": " + err.message); });
    }
}
            
function updateVideoToCanvas() {
  context.clearRect(0, 0, mycanvas.width, mycanvas.height);
  context.drawImage(video, 0, 0, 800, 600);
  context.globalAlpha = 1;
  context.drawImage(imgTitle, 0, 0);
  requestAnimationFrame(updateVideoToCanvas);
}

上述代码首先获取本地设备的视频输出,并写入video中,注意此处绑定了videoloadeddata事件,该事件处理函数中调用updateVideoToCanvas用来做最终的数字合成,该方法提取video的当前帧内容,并与图片进行合并,然后输出合并后的内容写入canvas,最后调用requestAnimationFrame渲染下一帧画面。

下一步,我们删除原来使用 AgoraRTC.createCameraVideoTrack()创建media stream的方法,替换为自定义media stream

canvasStream = mycanvas.captureStream(25);
const [videoTrack] = canvasStream.getVideoTracks();
let localVideoTrack = AgoraRTC.createCustomVideoTrack({
    mediaStreamTrack: videoTrack,
});
localTracks.videoTrack = localVideoTrack;

这样就成功取代了默认摄像头的画面。完整的代码可以参考我的仓库下的basicLive目录。

4.2 基于Insertable Stream的方案

实际上,上述借助于Canvas的方案略显繁琐,因为总会要使用一个canvas作为中转站。因此现在有一种新的 API 被提提出,这就是Insertable streams,也就是可插入流,这个 API 的核心思想在于我们需要把MediaStreamTrack的内容暴露成一个流的集合 (collection of streams),这些流可以被开发者直接操作,这样就避开了中间canvas多余的流程。

这套 API 目前还处于起步阶段,如果想了解更多内容,可以参考这个页面

4.3 动画合成

上面的例子我们只合成了一张静态带透明度的图片,实际上我们还可以添加更丰富的合成,例如合成一个SVG动画。合成动画需要考虑更多细节,例如动画的时间轴如何与视频流匹配。此外,我们还可以使用WebGL开发动态效果叠加在视频上,这样可以实现更多自定义的直播操作。不过这类内容涉及到更多图形领域的知识,一篇文章无法完全解明,感兴趣的读者可以阅读一下David Geary的《HTML5 Canvas核心技术》以及Kouichi Matsuda和Rodger Lea编写的《WebGL编程指南》。

5 参考文献

[1] The VES Handbook of Visual Effects, 3rd Edition (ISBN 9781138542204)
[2] Sebastian Sylwan, “The Application of Vision Algorithms to Visual Effects Production”. ACCV 2010, Part I, LNCS 6492, pp. 189–199, 2011.
[3] Wallace, Bruce A., Merging and Transformation of Raster Images for Cartoon Animation, Computer Graphics, Vol 15, No 3, Aug 1981, 253-262. SIGGRAPH’81 Conference Proceedings, doi:10.1145/800224.806813.
[4] Lee Lanier, Professional Digital Compositing: Essential Tools and Techniques, 2009 (ISBN 0470452617)

推荐阅读
相关专栏
开发者实践
182 文章
本专栏仅用于分享音视频相关的技术文章,与其他开发者和声网 研发团队交流、分享行业前沿技术、资讯。发帖前,请参考「社区发帖指南」,方便您更好的展示所发表的文章和内容。