Chromium,WebRTC本地视频前处理

11 篇文章 11 订阅

喔~ 突然发现已经快一年没写过博客了。主要是这一年实在是太忙了,一直没有时间好好整理和规划一些内容。今天打算把自己前段时间在我们公司产品中做的一个小功能的原理拿出来简单讲讲。(注:本文基于Chromium 4183(Chromium 85)源码)

背景

我们公司的桌面客户端产品是基于Chromium内核的CEF框架(理论上同源的Electron也是适用的),前一段时间和国内某知名计算机视觉和深度学习原创技术公司进行合作,将他们公司的技术应用到我们的桌面客户端产品中来。大家知道,Chromium及其内置的WebRTC视频模块是不具备视频前处理接口的。所以我们期望在本地推流的视频上,附加一些AI功能(如基础美颜、人脸/形体识别、实时抠图换背景……),必须得修改源码来实现。

步骤

本地视频基本上就包括本地渲染和视频编码两个环节。通过阅读和调试Chromium源码。我最后决定在MediaStreamVideoTrack (third_party\blink\renderer\modules\mediastream)的子类 FrameDeliverer::DeliverFrameOnIO 方法中,将视频帧拦截下来做前处理后,再送还给后续流程。

实现思路

整个视频前处理共分为三个部分:

  1. 扩展MediaStreamTrack对象属性和方法,给Javscript调用(enable/disable,设置参数等)
  2. 修改Chromium源码,采集视频帧后拦截下来
  3. 通过跨进程方法,将视频帧送出来进行前处理后再送回Chromium给后续流程(渲染,编码)

下面我分别来介绍下这三个部分。

扩展MediaStreamTrack对象属性和方法

通常,js可以通过getUserMedia来获取到MediaStream对象,继而获取其VideoTrack对象。VideoTrack相对于native,是一个MediaStreamTrack。具体可以参考MDN:MediaStreamTrack 。在WebRTC标准中,MediaStreamTrack提供了若干属性和方法。

为了方便js来控制视频前处理行为,我的做法是在native层扩展MediaStreamTrack的属性和方法。如何做呢?

延伸知识:Chromium是通过Web IDL(Interface Definition Language)来实现js和native之间对象、方法的绑定的。关于IDL可以参考:Web IDL interfaces。Chromium中的blink项目使用了IDL来给js提供各种各样的对象属性方法,可以参考:Blink IDL Extended Attributes

首先,在 third_party\blink\renderer\modules\mediastream\media_stream_track.idl 中,添加扩展属性和方法。这里假设我们要增加视频美颜处理。可以这样写:

// https://w3c.github.io/mediacapture-main/#media-stream-track-interface-definition
[
    Exposed=Window,
    ActiveScriptWrappable
] interface MediaStreamTrack : EventTarget {
    ...
    
	attribute boolean enableBeautify;
    attribute DOMString beautifyParams;

    ...
    [CallWith=ScriptState] Promise<void> initBeautify();
};

上面,我们为MediaStreamTrack扩展了2个属性:enableBeautify 和 beautifyParams,以及1个方法: initBeautify()。这样,未来js端就可以通过上面MDN中定义的标准属性和方法来调用这些扩展属性和方法了。举例:

const videoTrack = null;

function gotStream(stream) {
  window.stream = stream;
  videoElement.srcObject = stream;
  videoTrack = stream.getVideoTracks()[0];
  if(videoTrack) {
    videoTrack.initBeautify()
    .then(() => {
      videoTrack.enableBeautify = true;
      videoTrack.beautifyParams = "";
    })
    .catch(e => {
      console.log("Failed to initialize video pre-process");
    })
  }
  return navigator.mediaDevices.enumerateDevices();
}

function handleError(error) {
  console.log('navigator.getUserMedia error: ', error.name);
}

navigator.mediaDevices.getUserMedia(constraints).then(gotStream).catch(handleError);

通过这种方式,js可以十分方便地按照WebRTC标准使用方法来enable美颜,设置美颜参数。有了这个基础,未来可以通过 IDL来扩展更多native对象给js使用。

OK,IDL增加了属性和方法,还需要实现对应的native代码。在Chromium源码中,每一个idl都对应一个类。例如我们刚才修改的 media_stream_track.idl,它对应 media_stream_track.h和.cc两个文件。这部分就比较简单了:

bool enableBeautify() const;
void setEnableBeautify(bool);
String beautifyParams() const;
void setBeautifyParams(String);

ScriptPromise initBeautify(ScriptState*);

上面分别提供了2个扩展属性对应的get/set方法,以及扩展方法对应的成员函数。

接下来,需要将属性和行为路由到我们本文最早提到的 MediaStreamVideoTrack 中去影响视频帧。整个源码的分析过程此处略过,只说一下调用栈:

MediaStreamTrack::XXX() ->
MediaStreamComponent:::XXX() ->
WebPlatformMediaStreamTrack::XXX() ->
MediaStreamVideoTrack::XXX() ->
MediaStreamVideoTrack::FrameDeliverer::XXX()

这样,js的调用,就来到了我们最终的目标位置:MediaStreamVideoTrack::FrameDeliverer::DeliverFrameOnIO
这个函数中,结尾有一句:

for (const auto& entry : callbacks_) {
    entry.second.Run(video_frame, estimated_capture_time);
}

这句代码,会将视频帧分别送往渲染和编码模块。因此,我们在这句代码之前,对视频帧进行拦截处理是再好不过的了。

修改Chromium源码,采集视频帧后拦截下来

为了尽可能得少破坏Chromium原有的类结构和代码(因为还要考虑未来升级Chromium内核的问题),我这里设计了单独的视频前处理模块,并采用了跨进程处理的方式(跨进程的一个额外的考虑是,视频前处理模块一旦发生灾难,不会引起Chromium渲染进程崩溃),将视频帧送出来做处理。不过要说一下,采用了跨进程通讯,整个交互复杂度陡增。

大概的设计如下:
前处理
实现了一个独立的VideoPreProcessor类(分别实现了Windows和macOS两个版本),注入到blink::MediaStreamTrack::FrameDeliverer中。工作流程如下:
调用流程
前端 js 调用我们刚才为MediaStreamTrack增加的 initBeautify 方法,VideoPreProcessor启动一个独立的进程。之后接收指令、传递视频帧数据。

通过跨进程方法,将视频帧送出来进行前处理后再送回Chromium给后续流程

跨进程通讯手段众多,这里主要采用了以下几种主要的方法:

1. 命名管道
主要用于传递控制指令。例如启用、停止,设置前处理参数等等。一般来说就是简单的字符串。

2. 共享内存
视频帧数据一般来说传递的是I420数据,按照1920x1080分辨率计算,每一帧都需要至少3110400字节。因此,则采用共享内存的方法来传输是最佳选择。

3. 信号量
主要用于进程间数据同步:得到一帧数据后,写入共享内存,等待独立进程处理,写回共享内存,读取,交还给Chromium后续处理流程。整个过程中,都涉及同步处理。这个是整个处理中最复杂的部分。

独立进程就没有什么太多好说的了,主要是在共享内存提取出视频帧的I420数据,交给我们的合作伙伴进行相关 AI识别、处理,之后会得到处理结果,再写回共享内存送还给Chromium即可。

注意事项

时间戳修改

通过这种方式修改后,我们干预和破坏了原有的视频传输路径,人为增加了处理时间(共享内存的多次拷贝、视频帧前处理时间),在送还给Chromium做下一步处理之前,记得调用 media::VideoFrame::set_timestamp() 修改时间戳,将人为增加的时间加上去,否则将会引起帧率不足和不同步。伪代码:

auto start = std::chrono::steady_clock::now();
VideoPreProcessor::ProcessFrame(frame);
auto end = std::chrono::steady_clock::now();
long long process_time_ms = std::chrono::duration_cast<milliseconds>(end - start).count();
media::VideoFrame->set_timestamp(frame.timestamp() + base::TimeDelta::FromMilliseconds(process_time_ms));

处理时间

按照 25fps计算,平均到每帧的时间是 40ms。在不进行任何前处理干预的情况下,采集后送往渲染和编码之前,是没有任何耗时的。但经过干预处理后,就会产生一个人为的耗时,如果这个耗时太长,就会掉帧严重。

通过实际使用,我的经验是如果可以保证每帧的处理时间控制在30ms以内,处于勉强可接受的范围。<20ms,则人眼感受不明显。当然,如果你的应用场景要求是50fps及以上,我建议还是不要这么做了,因为就一些实时抠图这样的处理,即便是640x480的分辨率,每帧也需要至少15ms的处理时间。

OK, 最后来看看效果吧~ 下面是在网页里实时推(左侧)、拉(右侧)WebRTC流附加虚拟背景的效果。注:视频转GIF,为了减少尺寸,我处理成了5fps,保留了几秒,实际上是640x480@25fps。

实时抠图推拉流示意图

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值