2020年如果问什么技术领域最火?毫无疑问:音视频。2020年远程办公和在线教育的强势发展,都离不开音视频的身影,视频会议、在线教学、娱乐直播等都是音视频的典型应用场景。

更加丰富的使用场景更需要我们考虑如何提供更多的可配置能力项,比如分辨率、帧率、码率等,以实现更好的用户体验。本文将主要从“分辨率”展开具体分享。

如何实现自定义编码分辨率

我们先来看看“分辨率”的定义。分辨率:是度量图像内像素数据量多少的一个参数,是衡量一帧图像或视频质量的关键指标。分辨率越高,图像体积(字节数)越大,画质越好。对于一个YUV i420 格式、分辨率 1080p 的视频流来说,一帧图像的体积为 1920x1080x1.5x8/1024/1024≈23.73Mbit,帧率 30,则 1s 的大小是 30x23.73≈711.9Mbit。可见数据量之大,对码率要求之高,所以在实际传输过程中就需要对视频进行压缩编码。因此,视频采集设备采集出的原始数据分辨率我们称采集分辨率,实际送进编码器的数据分辨率我们就称之为编码分辨率

视频画面是否清晰、比例是否合适,这些都会直接影响用户体验。摄像头采集分辨率的选择是有限的,有时我们想要的分辨率并不能直接通过摄像头采集到。那么,根据场景配置合适编码分辨率的能力就至关重要了。如何将采集到的视频转换成我们想要的编码分辨率去发送?这就是我们今天的主要分享的内容。

WebRTC 是 Google 开源的,功能强大的实时音视频项目,市面上大多开发者都是基于 WebRTC 构建实时音视频通信的解决方案。在 WebRTC 中各个模块都有很好的抽象解耦处理, 对我们进行二次开发非常友好。在我们构建实时音视频通信解决方案时,需要去了解和学习 WebRTC 的设计思想及代码模块,并具备二次开发和扩展的能力。本文我们基于 WebRTC Release 72 版本,聊聊如何实现自定义编码分辨率。

首先,我们思考下面几个问题:

  • 视频数据从采集到编码发送,其 Pipeline 是怎样的?
  • 怎么根据设置的编码分辨率选择合适的采集分辨率?
  • 怎么能得到想要的编码分辨率?

本文内容也将从以上三点展开具体分享。

视频数据的 Pipeline

首先,我们来了解一下视频数据的 Pipeline。视频数据由 VideoCapturer 产生,VideoCapturer 采集数据后经过 VideoAdapter 处理,然后经由 VideoSource 的 VideoBroadcaster 分发给注册的 VideoSink ,VideoSink 即编码器 Encoder Sink 和本地预览 Preview Sink。

对视频分辨率来说,流程是:将想要的分辨率设置给 VideoCapturer,VideoCapturer 选择合适的分辨率去采集,原始的采集分辨率数据再经过 VideoAdapter 计算,不符合预期后再进行缩放裁剪得到编码分辨率的视频数据,将数据再送进编码器编码后发送。

这里就有两个关键性问题:

  • VideoCapturer 如何选择合适的采集分辨率?
  • VideoAdapter 如何将采集分辨率转换成编码分辨率?

如何选择合适的采集分辨率

采集分辨率的选择

WebRTC 中对视频采集抽象出一个 Base 类:videocapturer.cc,我们把抽象称为 VideoCapturer,在 VideoCapturer 中设置参数属性,比如视频分辨率、帧率、支持的像素格式等,VideoCapturer 将根据设置的参数,计算出最佳的采集格式,再用这个采集格式去调用各个平台的 VDM(Video Device Module,视频硬件设备模块)。具体的设置如下:

代码摘自 WebRTC 中 src/media/base/videocapturer.h

VideoCapturer.h
bool GetBestCaptureFormat(const VideoFormat& desired, VideoFormat* best_format);//内部遍历设备支持的所有采集格式调用GetFormatDistance()计算出每个格式的distance,选出distance最小的那个格式
int64_t GetFormatDistance(const VideoFormat& desired, const VideoFormat& supported);//根据算法计算出设备支持的格式与我们想要的采集格式的差距,distance为0即刚好满足我们的设置
void SetSupportedFormats(const std::vector<VideoFormat>& formats);//设置采集设备支持的格式fps,resolution,NV12, I420,MJPEG等

根据设置的参数,有时 GetBestCaptureFormat() 并不能得到比较符合我们设置的采集格式,因为不同的设备采集能力不同,iOS、Android、PC、Mac 原生的摄像采集和外置 USB 摄像采集对分辨率的支持是不同的,尤其外置 USB 摄像采集能力参差不齐。因此,我们需要对 GetFormatDistance() 稍作调整以满足我们的需求,下面我们就来聊聊具体应该如何进行代码调整以满足需求。

选择策略源码分析

我们先分析一下 GetFormatDistance() 的源码,摘取部分代码:

代码摘自 WebRTC 中 src/media/base/videocapturer.cc

// Get the distance between the supported and desired formats.
int64_t VideoCapturer::GetFormatDistance(const VideoFormat& desired,
const VideoFormat& supported) {
//....省略部分代码
// Check resolution and fps.
int desired_width = desired.width;//编码分辨率宽
int desired_height = desired.height;//编码分辨率高
int64_t delta_w = supported.width - desired_width;//宽的差 float supported_fps = VideoFormat::IntervalToFpsFloat(supported.interval);//采集设备支持的帧率
float delta_fps = supported_fps - VideoFormat::IntervalToFpsFloat(desired.interval);//帧率差
int64_t aspect_h = desired_width
? supported.width * desired_height / desired_width
: desired_height;//计算出设置的宽高比的高,采集设备的分辨率支持一般宽>高
int64_t delta_h = supported.height - aspect_h;//高的差
int64_t delta_fourcc;//设置的支持像素格式优先顺序,比如优先设置了NV12,同样分辨率和帧率的情况优先使用NV12格式采集 //....省略部分降级策略代码,主要针对设备支持的分辨率和帧率不满足设置后的降级策略 int64_t distance = 0;
distance |=
(delta_w << 28) | (delta_h << 16) | (delta_fps << 8) | delta_fourcc; return distance;
}

我们主要关注 Distance 这个参数。Distance 是 WebRTC 中的概念,它是设置的采集格式与设备支持的采集格式按照一定算法策略计算出的差值,差值越小代表设备支持的采集格式与设置想要的格式越接近,为 0 即刚好匹配。

Distance 由四部分组成 delta_w,delta_h,delta_fps,delta_fourcc,其中 delta_w(分辨率宽) 权重最重,delta_h(分辨率高) 其次,delta_fps(帧率) 再次,delta_fourcc(像素格式) 最后。这样导致的问题是宽的比重太高, 高的比重太低,无法匹配到比较精确支持的分辨率。

Example:

以 iPhone xs Max 800x800 fps:10 为例,我们摘取部分采集格式的 distance, 原生的 GetFormatDistance() 的算法是不满足需求的,想要的是 800x800,可以从下图看出结果 Best 是960x540,不符合预期:

Supported NV12 192x144x10 distance 489635708928
Supported NV12 352x288x10 distance 360789835776
Supported NV12 480x360x10 distance 257721630720
Supported NV12 640x480x10 distance 128880476160
Supported NV12 960x540x10 distance 43032248320
Supported NV12 1024x768x10 distance 60179873792
Supported NV12 1280x720x10 distance 128959119360
Supported NV12 1440x1080x10 distance 171869470720
Supported NV12 1920x1080x10 distance 300812861440
Supported NV12 1920x1440x10 distance 300742082560
Supported NV12 3088x2316x10 distance 614332104704
Best NV12 960x540x10 distance 43032248320

选择策略调整

为了获取我们想要的分辨率,按照我们分析,需要明确调整 GetFormatDisctance() 的算法,将分辨率的权重调整为最高,帧率其次,在没有指定像素格式的情况下,像素格式最后,那么修改情况如下:

int64_t VideoCapturer::GetFormatDistance(const VideoFormat& desired,
const VideoFormat& supported) {
//....省略部分代码
// Check resolution and fps.
int desired_width = desired.width; //编码分辨率宽
int desired_height = desired.height; //编码分辨率高
int64_t delta_w = supported.width - desired_width;
int64_t delta_h = supported.height - desired_height;
int64_t delta_fps = supported.framerate() - desired.framerate();
distance = std::abs(delta_w) + std::abs(delta_h);
//....省略降级策略, 比如设置了1080p,但是摄像采集设备最高支持720p,需要降级
distance = (distance << 16 | std::abs(delta_fps) << 8 | delta_fourcc);
return distance;
}

修改后:Distance 由三部分组成分辨率 (delta_w+delta_h),帧率 delta_fps,像素 delta_fourcc,其中 (delta_w+delta_h) 比重最高,delta_fps 其次,delta_fourcc 最后。

Example:

还是以 iPhone xs Max 800x800 fps:10 为例,我们摘取部分采集格式的 Distance, GetFormatDistance() 修改后, 我们想要的是 800x800, 选择的 Best 是1440x1080, 我们可以通过缩放裁剪得到 800x800, 符合预期(对分辨率要求不是特别精确的情况下,可以调整降级策略,选择1024x768):

Supported NV12 192x144x10 distance 828375040
Supported NV12 352x288x10 distance 629145600
Supported NV12 480x360x10 distance 498073600
Supported NV12 640x480x10 distance 314572800
Supported NV12 960x540x10 distance 275251200
Supported NV12 1024x768x10 distance 167772160
Supported NV12 1280x720x10 distance 367001600
Supported NV12 1440x1080x10 distance 60293120
Supported NV12 1920x1080x10 distance 91750400
Supported NV12 1920x1440x10 distance 115343360
Supported NV12 3088x2316x10 distance 249298944
Best NV12 1440x1080x10 distance 60293120

如何实现采集分辨率到编码分辨率

视频数据采集完成后,会经过 VideoAdapter (WebRTC中的抽象) 处理再分发到对应的 Sink (WebRTC中的抽象)。我们在 VideoAdapter 中稍作调整以计算出缩放裁剪所需的参数,再把视频数据用 LibYUV 缩放再裁剪到编码分辨率(为了尽可能保留多的画面图像信息,先用缩放处理,宽高比不一致时再裁剪多余的像素信息)。这里我们重点分析两个问题:

  • 还是选用上面的例子,我们想要的分辨率为 800x800 ,但是我们得到的最佳采集分辨率为 1440x1080,那么,如何从 1440x1080 采集分辨率得到设置的编码分辨率 800x800 呢?
  • 在视频数据从 VideoCapture 流到 VideoSink 的过程中会经过 VideoAdapter 的处理,VideoAdapter 具体做了哪些事呢?

下面我们就这两个问题展开具体的分析,我们先了解一下 VideoAdapter 是什么。

VideoAdapter 介绍

WebRTC 中对 VideoAdapter 是这样描述的:

VideoAdapter adapts an input video frame to an output frame based on the specified input and output formats. The adaptation includes dropping frames to reduce frame rate and scaling frames.VideoAdapter is

thread safe.

我们可以理解为:VideoAdapter 是数据输入输出控制的模块,可以对帧率、分辨率做对应的帧率控制和分辨率降级。在 VQC(Video Quality Control)视频质量控制模块里,通过对 VideoAdapter 的配置,可以做到在低带宽、高 CPU 情况下对帧率进行动态降帧,对分辨率进行动态缩放,以保证视频的流畅性,从而提高用户体验。

摘自 src/media/base/videoadapter.h

VideoAdapter.h
bool AdaptFrameResolution(int in_width,
int in_height,
int64_t in_timestamp_ns,
int* cropped_width,
int* cropped_height,
int* out_width,
int* out_height);
void OnOutputFormatRequest(
const absl::optional<std::pair<int, int>>& target_aspect_ratio,
const absl::optional<int>& max_pixel_count,
const absl::optional<int>& max_fps);
void OnOutputFormatRequest(const absl::optional<VideoFormat>& format);

VideoAdapter 源码分析

VideoAdapter 中根据设置的 desried_format,调用 AdaptFrameResolution(),可以计算出采集分辨率到编码分辨率应该缩放和裁剪的 cropped_width, cropped_height, out_width, out_height 参数, WebRTC 原生的 adaptFrameResolution 是根据计算像素面积计算缩放参数,而不能得到精确的宽&高:

摘自src/media/base/videoadapter.cc

bool VideoAdapter::AdaptFrameResolution(int in_width,
int in_height,
int64_t in_timestamp_ns,
int* cropped_width,
int* cropped_height,
int* out_width,
int* out_height) {
//.....省略部分代码
// Calculate how the input should be cropped.
if (!target_aspect_ratio || target_aspect_ratio->first <= 0 ||
target_aspect_ratio->second <= 0) {
*cropped_width = in_width;
*cropped_height = in_height;
} else {
const float requested_aspect =
target_aspect_ratio->first /
static_cast<float>(target_aspect_ratio->second);
*cropped_width =
std::min(in_width, static_cast<int>(in_height * requested_aspect));
*cropped_height =
std::min(in_height, static_cast<int>(in_width / requested_aspect));
}
const Fraction scale;//vqc 缩放系数 ....省略代码
// Calculate final output size.
*out_width = *cropped_width / scale.denominator * scale.numerator;
*out_height = *cropped_height / scale.denominator * scale.numerator;
}

Example:

以 iPhone xs Max 800x800 fps:10 为例,设置编码分辨率为 800x800,采集分辨率是 1440x1080,根据原生的算法,计算得到的新的分辨率为 720x720, 不符合预期。

VideoAdapter 调整

VideoAdapter 是 VQC(视频质量控制模块)中对视频质量做调整的重要部分,VQC 之所以可以完成帧率控制、分辨率缩放等操作,主要依赖于 VideoAdapter,因此修改需要考虑对 VQC 的影响。

为了能精确获得想要的分辨率,且不影响 VQC 模块对分辨率的控制,我们对 AdaptFrameResolution() 做以下调整:

bool VideoAdapter::AdaptFrameResolution(int in_width,
int in_height,
int64_t in_timestamp_ns,
int* cropped_width,
int* cropped_height,
int* out_width,
int* out_height) {
//....省略部分代码
bool in_more =
(static_cast<float>(in_width) / static_cast<float>(in_height)) >=
(static_cast<float>(desired_width_) /
static_cast<float>(desired_height_));
if (in_more) {
*cropped_height = in_height;
*cropped_width = *cropped_height * desired_width_ / desired_height_;
} else {
*cropped_width = in_width;
*cropped_height = *cropped_width * desired_height_ / desired_width_;
}
*out_width = desired_width_;
*out_height = desired_height_;
//....省略部分代码
return true;
}

Example:

同样以 iPhone xs Max 800x800 fps:10 为例,设置编码分辨率为 800x800,采集分辨率是 1440x1080,根据调整后的算法,计算得到的编码分辨率为 800x800, 符合预期。

总结

本文主要介绍了基于 WebRTC 如何实现编码分辨率的配置。当我们要对视频编码分辨率进行修改时,就需要去了解视频数据的采集、传递、处理、编码等整个流程,这里也再对今天分享几个关键步骤进行归纳,当我们要实现自定义编码分辨率发送时:

  • 首先,设置想要的编码分辨率;
  • 修改 VideoCapturer.cc,根据编码分辨率选择合适的采集分辨率;
  • 修改 VideoAdapter.cc,计算出采集分辨率缩放和裁剪到编码分辨率所需的参数;
  • 根据缩放和裁剪参数使用 libyuv 将原始数据缩放裁剪到编码分辨率;
  • 再将新数据送进编码器编码并发送;
  • 最后,Done。

同理我们也可以依据这种思路去做一些其他的调整。以上就是本文的全部介绍,我们也会持续分享更多音视频相关的技术实现,也欢迎留言与我们交流相关的技术。

5G 时代已经来临,音视频的应用领域会越来越宽泛,一切大有可为。

作者介绍

何敬敬,网易云信客户端音视频工程师,负责云信音视频跨平台 SDK 的研发工作。之前从事在线教育领域的音视频工作,对构建实时音视频解决方案和使用场景有一定的理解,喜欢钻研和解决复杂技术问题。

基于 WebRTC 实现自定义编码分辨率发送的更多相关文章

  1. 关于基于webrtc的android-apk 和 webrtc-brows

    这一段时间我在做一些关于基于webrtc应用的一些研究,做个一个android的demo,详情如下: 手机客户端:   基于webrtc的 android apk   (webrtc 代码版本 R67 ...

  2. 基于django的自定义简单session功能

    基于django的自定义简单session功能 简单思路: 1.建立自定义session数据库 2.登入时将用户名和密码存入session库 3.将自定义的随机session_id写入cookie中 ...

  3. 数据分析:基于Python的自定义文件格式转换系统

    *:first-child { margin-top: 0 !important; } body>*:last-child { margin-bottom: 0 !important; } /* ...

  4. 基于 Python 的自定义分页组件

    基于 Python 的自定义分页组件 分页是网页中经常用到的地方,所以将分页功能分出来,作为一个组件可以方便地使用. 分页实际上就是不同的 url ,通过这些 url 获取不同的数据. 业务逻辑简介 ...

  5. 基于webrtc的资源释放问题(二)

    基于webrtc的资源释放问题(二) ——建立连接的过程中意外中断 应用背景: 我们在打电话的时候会不会遇到这种情况?打电话的时候未接通之前挂掉了电话,或者在接通之后建立的连接的过程中挂掉电话? 特别 ...

  6. 基于webrtc的资源释放问题(一)

    基于webrtc的资源释放问题(一) ——重复释放webrtc的相关资源 背景: 视频通讯大都只是作为一个功能存在于各种应用中,比如微信,qq .既然只是应用的一部分,这样就涉及反复的开启和关闭视频通 ...

  7. 基于webrtc技术session border controler (SBC)

    由于原来的文章 http://blog.csdn.net/voipmaker  转载注明出处. 我建了一个通信学习 交流群. 45211986, 欢迎增加. WebRTC技术致力于在浏览器端实现实时音 ...

  8. 基于 WebRTC 技术的实时通信服务开发实践

    随着直播的发展,直播实时互动性变得日益重要.又拍云在 WebRTC 的基础上,凭借多年的开发经验,结合当下实际情况,开发 UPRTC 系统,解决了网络延时.并发量大.客户端解码能力差等问题. WebR ...

  9. 基于 HtmlHelper 的自定义扩展Container

    基于 HtmlHelper 的自定义扩展Container Intro 基于 asp.net mvc 的权限控制系统的一部分,适用于对UI层数据呈现的控制,基于 HtmlHelper 的扩展组件 Co ...

  10. 基于哈夫曼编码的文件压缩(c++版)

    本博客由Rcchio原创 我了解到很多压缩文件的程序是基于哈夫曼编码来实现的,所以产生了自己用哈夫曼编码写一个压缩软件的想法,经过查阅资料和自己的思考,我用c++语言写出了该程序,并通过这篇文章来记录 ...

随机推荐

  1. Java中用ClassLoader载入各种资源(类、文件、web资源)的方法

    lassLoader主要对类的请求提供服务,当JVM需要某类时,它根据名称向ClassLoader要求这个类,然后由ClassLoader返回这个类的class对象. ClassLoader负责载入系 ...

  2. POJ 1038 Bugs Integrated, Inc.

    AC通道 神坑的一道题,写了三遍. 两点半开始写的, 第一遍是直接维护两行的二进制.理论上是没问题的,看POJ discuss 上也有人实现了,但是我敲完后准备开始调了.然后就莫名其妙的以为会超时,就 ...

  3. js字符串函数之indexOf()

    indexOf 返回字符串中指定字符首次出现的位置 var str="hello, I am Miss bean!"; str.indexOf("l")//结果 ...

  4. 最基本的Unix系统操作命令

    基本知识点: OSX 采用的Unix文件系统,所有文件都挂在跟目录 / 下面,所以不在要有Windows 下的盘符概念. 你在桌面上看到的硬盘都挂在 /Volumes 下. 比如接上个叫做 USBHD ...

  5. phantomjs Can not connect to the Service phantomjs错误

    尝试方法一: 打开hosts文件配置 cat /etc/hosts 添加 127.0.0.1 localhost 重新运行 尝试方法二: 1,抛开服务,直接调用phantomjs定位问题 由于我是从服 ...

  6. 【转】大数据分析中Redis怎么做到220万ops

    原文:http://www.cnblogs.com/nnhy/archive/2018/01/16/Redis220.html 大数据时代,海量数据分析就像吃饭一样,成为了我们每天的工作.为了更好的为 ...

  7. 牛客第二场A-run

    链接:https://www.nowcoder.com/acm/contest/140/A 来源:牛客网 White Cloud is exercising in the playground. Wh ...

  8. 特别翔实的adaboost分类算法讲解 转的

    转https://www.cnblogs.com/litthorse/p/9332370.html 作为(曾)被认为两大最好的监督分类算法之一的adaboost元算法(另一个为前几节介绍过的SVM算法 ...

  9. idea如何禁用SVN

    打开Intellij的setting(ctrl+alt+s),选择plugins,在右边搜索框输入“SVN”,搜索.选择“SVN disconnect”,安装此插件.  插件使用 点击菜单栏中的VCS ...

  10. Codeforces Round #424 B. Keyboard Layouts(字符串,匹配,map)

    #include <stdio.h> #include <string.h> ][]; ]; ]; int main(){ scanf(]); scanf(]); scanf( ...