WebRTC视频 05 - 视频采集类 VideoCaptureDS 下篇

WebRTC视频 01 - 视频采集整体架构
WebRTC视频 02 - 视频采集类 VideoCaptureModule
WebRTC视频 03 - 视频采集类 VideoCaptureDS 上篇
WebRTC视频 04 - 视频采集类 VideoCaptureDS 中篇
WebRTC视频 05 - 视频采集类 VideoCaptureDS 下篇(本文)

一、前言:

前面介绍了视频数据采集,那么采集到的数据如何上传给上层应用的?本节看看。

二、流程图:

在这里插入图片描述

  1. CaptureFilterCamera采集到数据之后交给SinkFilter
  2. 通过SinkFilter获取到数据之后,会调用VideoCaptureImplIncomingFrame
  3. 接着调用DeliverCapturedFrame,里面会调用RegisterCaptureDataCallbackonFrame,将数据传给onFrame函数;
  4. 接下来再交给VcmCapturer::OnFrame,里面会交给VideoBroadcaster
  5. VideoBroadcaster::OnFrame会将采集到数据进行分两路,本地渲染和编码器(注意看数据有没有copy);
  6. 还记得本地渲染器和编码器什么时候加入到VideoBroadcaster当中的吗?本地渲染器是StartLocalRenderer时候加入的,编码器是媒体协商最后一步SetRemoteDescription的时候加入的;

三、代码走读:

首先看看SinkFilter接收数据的地方:

/*** 接收采集到的视频数据时候,首先会进入到这儿* @param media_sample:就是采集到的数据*/
STDMETHODIMP CaptureInputPin::Receive(IMediaSample* media_sample) {RTC_DCHECK_RUN_ON(&capture_checker_);// 通过Filter()获取到这个pin所属的filter,也就是sinkFilterCaptureSinkFilter* const filter = static_cast<CaptureSinkFilter*>(Filter());if (flushing_.load(std::memory_order_relaxed))return S_FALSE;if (runtime_error_.load(std::memory_order_relaxed))return VFW_E_RUNTIME_ERROR;// 确保采集线程id已经获得,并给线程起个名字webrtc_video_captureif (!capture_thread_id_) {// Make sure we set the thread name only once.capture_thread_id_ = GetCurrentThreadId();rtc::SetCurrentThreadName("webrtc_video_capture");}// 获取采样属性,看其中是否包含AM_SAMPLE_TYPECHANGED,如果包含了,会做一些处理AM_SAMPLE2_PROPERTIES sample_props = {};GetSampleProperties(media_sample, &sample_props);// Has the format changed in this sample?if (sample_props.dwSampleFlags & AM_SAMPLE_TYPECHANGED) {// Check the derived class accepts the new format.// This shouldn't fail as the source must call QueryAccept first.// Note: This will modify resulting_capability_.// That should be OK as long as resulting_capability_ is only modified// on this thread while it is running (filter is not stopped), and only// modified on the main thread when the filter is stopped (i.e. this thread// is not running).if (!TranslateMediaTypeToVideoCaptureCapability(sample_props.pMediaType,&resulting_capability_)) {// Raise a runtime error if we fail the media typeruntime_error_ = true;EndOfStream();Filter()->NotifyEvent(EC_ERRORABORT, VFW_E_TYPE_NOT_ACCEPTED, 0);return VFW_E_INVALIDMEDIATYPE;}}// 收到数据之后调用这个方法将数据从pin传给filterfilter->ProcessCapturedFrame(sample_props.pbBuffer, sample_props.lActual,resulting_capability_);return S_OK;
}

其实,关键的代码就是最后一步,ProcessCapturedFrame将数据从pin传给filter。

/*** 收到数据之后调用这个方法将数据从pin传给filter*/
void CaptureSinkFilter::ProcessCapturedFrame(unsigned char* buffer,size_t length,const VideoCaptureCapability& frame_info) {// Called on the capture thread.// CaptureSinkFilter的observer就是VideoCaptureImplcapture_observer_->IncomingFrame(buffer, length, frame_info);
}

由于使用的是观察者模式,这个capture_observer其实就是VideoCaptureImpl,于是我们去VideoCaptureImpl看看。

/*** 通过 SinkFilter 获取到数据之后,会调用此函数,* 这个函数会将采集到的数据统一转换为I420格式的数据(因为用户request的格式是I420)*/
int32_t VideoCaptureImpl::IncomingFrame(uint8_t* videoFrame,size_t videoFrameLength,const VideoCaptureCapability& frameInfo,int64_t captureTime /*=0*/) {MutexLock lock(&api_lock_);const int32_t width = frameInfo.width;const int32_t height = frameInfo.height;TRACE_EVENT1("webrtc", "VC::IncomingFrame", "capture_time", captureTime);// Not encoded, convert to I420.// 判断采集的数据类型是不是kMJPEG(这个是已经编码的,比如外接了家里那种DV)// 如果不是kMJPEG,那么使用CalcBufferSize计算的缓冲区长度和视频帧长度不相等,则不能处理if (frameInfo.videoType != VideoType::kMJPEG &&CalcBufferSize(frameInfo.videoType, width, abs(height)) !=videoFrameLength) {RTC_LOG(LS_ERROR) << "Wrong incoming frame length.";return -1;}// 由于我们最终采集的数据肯定是YUV,下面计算一些YUV相关的参数int stride_y = width;int stride_uv = (width + 1) / 2;int target_width = width;int target_height = abs(height);// SetApplyRotation doesn't take any lock. Make a local copy here.// 采集到数据帧是否进行了旋转bool apply_rotation = apply_rotation_;// 如果进行了旋转,那么,还要旋转回来if (apply_rotation) {// Rotating resolution when for 90/270 degree rotations.if (_rotateFrame == kVideoRotation_90 ||_rotateFrame == kVideoRotation_270) {target_width = abs(height);target_height = width;}}// Setting absolute height (in case it was negative).// In Windows, the image starts bottom left, instead of top left.// Setting a negative source height, inverts the image (within LibYuv).// TODO(nisse): Use a pool?// 由于我们采集的数据不是I420,因此我们分配个I420的buffer,将数据转换为I420rtc::scoped_refptr<I420Buffer> buffer = I420Buffer::Create(target_width, target_height, stride_y, stride_uv, stride_uv);libyuv::RotationMode rotation_mode = libyuv::kRotate0;if (apply_rotation) {switch (_rotateFrame) {case kVideoRotation_0:rotation_mode = libyuv::kRotate0;break;case kVideoRotation_90:rotation_mode = libyuv::kRotate90;break;case kVideoRotation_180:rotation_mode = libyuv::kRotate180;break;case kVideoRotation_270:rotation_mode = libyuv::kRotate270;break;}}// 通过libyuv的方法将数据转换成I420const int conversionResult = libyuv::ConvertToI420(videoFrame, videoFrameLength, buffer.get()->MutableDataY(),buffer.get()->StrideY(), buffer.get()->MutableDataU(),buffer.get()->StrideU(), buffer.get()->MutableDataV(),buffer.get()->StrideV(), 0, 0,  // No Croppingwidth, height, target_width, target_height, rotation_mode,ConvertVideoType(frameInfo.videoType));if (conversionResult < 0) {RTC_LOG(LS_ERROR) << "Failed to convert capture frame from type "<< static_cast<int>(frameInfo.videoType) << "to I420.";return -1;}// 将转换后的数据重新封装成一个 VideoFrame 格式VideoFrame captureFrame =VideoFrame::Builder().set_video_frame_buffer(buffer).set_timestamp_rtp(0).set_timestamp_ms(rtc::TimeMillis()).set_rotation(!apply_rotation ? _rotateFrame : kVideoRotation_0).build();captureFrame.set_ntp_time_ms(captureTime);// 里面会调用 RegisterCaptureDataCallback 的onFrame,将数据传给onFrame函数DeliverCapturedFrame(captureFrame);return 0;
}

主要完成了:

  • 将我们采集的YUV数据转换成I420格式。
  • 将转换后的数据重新封装成一个 VideoFrame 格式。
  • 调用DeliverCapturedFrame将数据继续向上抛。

看看抛给谁了?

/*** 里面会调用 RegisterCaptureDataCallback 的onFrame,将数据传给onFrame函数*/
int32_t VideoCaptureImpl::DeliverCapturedFrame(VideoFrame& captureFrame) {UpdateFrameCount();  // frame count used for local frame rate callback.if (_dataCallBack) {_dataCallBack->OnFrame(captureFrame);}return 0;
}

小插曲开始:

那么问题来了,这个_dataCallBack是谁?看上面流程图也可以知道,或者看下面代码也可以知道是VcmCapturer

bool VcmCapturer::Init(size_t width,size_t height,size_t target_fps,size_t capture_device_index) {// 创建 VideoCapture 对象vcm_ = webrtc::VideoCaptureFactory::Create(unique_name);if (!vcm_) {return false;}// 这儿传入了VcmCapture自己vcm_->RegisterCaptureDataCallback(this);
}
// 注册监听者
void VideoCaptureImpl::RegisterCaptureDataCallback(rtc::VideoSinkInterface<VideoFrame>* dataCallBack) {MutexLock lock(&api_lock_);_dataCallBack = dataCallBack;
}

小插曲完毕!

于是,我们来到VcmCapturerOnFrame函数看看:

// 接收采集到视频数据(格式已经转换成用户请求的了)
void VcmCapturer::OnFrame(const VideoFrame& frame) {TestVideoCapturer::OnFrame(frame);
}

继续向上层看:

/*** 从 VcmCapturer::OnFrame 抛上来的*/
void TestVideoCapturer::OnFrame(const VideoFrame& original_frame) {int cropped_width = 0;int cropped_height = 0;int out_width = 0;int out_height = 0;// 对原始视频帧进行处理(比如你加一些特效)VideoFrame frame = MaybePreprocess(original_frame);// 对初步处理后的视频帧分辨率进行调整(如果不是16:9或者4:3要调整成这样)if (!video_adapter_.AdaptFrameResolution(frame.width(), frame.height(), frame.timestamp_us() * 1000,&cropped_width, &cropped_height, &out_width, &out_height)) {// Drop frame in order to respect frame rate constraint.return;}if (out_height != frame.height() || out_width != frame.width()) {// Video adapter has requested a down-scale. Allocate a new buffer and// return scaled version.// For simplicity, only scale here without cropping.// 如果视频宽高和请求的不一致,进行缩放(不裁剪)rtc::scoped_refptr<I420Buffer> scaled_buffer =I420Buffer::Create(out_width, out_height);scaled_buffer->ScaleFrom(*frame.video_frame_buffer()->ToI420());VideoFrame::Builder new_frame_builder =VideoFrame::Builder().set_video_frame_buffer(scaled_buffer).set_rotation(kVideoRotation_0).set_timestamp_us(frame.timestamp_us()).set_id(frame.id());if (frame.has_update_rect()) {VideoFrame::UpdateRect new_rect = frame.update_rect().ScaleWithFrame(frame.width(), frame.height(), 0, 0, frame.width(), frame.height(),out_width, out_height);new_frame_builder.set_update_rect(new_rect);}// 缩放后的视频帧交给VideoBroadCasterbroadcaster_.OnFrame(new_frame_builder.build());} else {// 如果不需要缩放,那么直接交给 VideoBroadcaster 进行分发// No adaptations needed, just return the frame as is.broadcaster_.OnFrame(frame);}
}

里面做了几件事:

  • 可以对裸数据做一些处理,比如加特效什么的;
  • 对分辨率规范化;
  • 宽高和请求的不一致,进行一些缩放;
  • 最终都是调用了视频分发器VideoBroadcasterOnFrame方法,进入视频分发器就可以分为本地渲染器,或者编码发送出去,前面已经讲过不再赘述;

四、总结:

其实逻辑还是非常明确的,也是常见的设计模式(观察者模式)。将底层数据一层层抛给应用层,注意要关注,处理媒体数据,由于量非常大,尤其高清视频,我们所说的抛数据不是你一层层拷贝,留意每个环节怎么做的,哪些环节拷贝了数据,因为什么迫不得已拷贝的,这对诸位写出软件实时性有很大影响。

各位看官,觉得写得还行,动动小手点个赞,真的通宵达旦在写文章!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/14338.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

ffmpeg 最强大的视频工具

文章目录 一、ffmpeg安装二、基本用法1、文件格式转换2、视频过滤器 filter3、剪切4、合并5、音频过滤器6、删除轨道7、简单应用&#xff1a;录屏 一、ffmpeg安装 windows下可以上官网 https://www.ffmpeg.org/download.html下载&#xff1a; 下载好后&#xff0c;解压缩&…

初识算法 · 位运算(2)

目录 前言&#xff1a; 判定字符是否唯一 丢失的数字 比特位计数 只出现一次的数字III 前言&#xff1a; ​本文的主题是位运算&#xff0c;通过四道题目讲解&#xff0c;一道是判断字符是否唯一&#xff0c;一道是只出现一次的数字III&#xff0c;一道是比特位计数&…

大数据新视界 -- 大数据大厂之 Impala 性能优化:基于数据特征的存储格式选择(上)(19/30)

&#x1f496;&#x1f496;&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎你们来到 青云交的博客&#xff01;能与你们在此邂逅&#xff0c;我满心欢喜&#xff0c;深感无比荣幸。在这个瞬息万变的时代&#xff0c;我们每个人都在苦苦追寻一处能让心灵安然栖息的港湾。而 我的…

[产品管理-76]:延续是创新与颠覆式创新的比较

目录 一、概述 1、定义与特征 2、市场影响与竞争策略 3、实施难度与风险 4、案例分析 二、示例 1. 延续性创新示例 2. 创新示例 3. 颠覆式创新示例 一、概述 延续性创新与颠覆式创新是技术创新领域的两种重要策略&#xff0c;它们在多个方面存在显著差异。 以下是对…

JAVA学习日记(十五) 数据结构

一、数据结构概述 数据结构是计算机底层存储、组织数据的方式。 数据结构是指数据相互之间以什么方式排列在一起的。 数据结构是为了更加方便的管理和使用数据&#xff0c;需要结合具体的业务场景来进行选择。 二、常见的数据结构 &#xff08;一&#xff09;栈 特点&…

自动化测试工具Ranorex Studio(三十)-代码模块中使用变量快照

为了在代码模块中使用数据连接器提供的值&#xff0c;你需要在代码中添加一个变量。使用右键菜单项’Insert Module Variable’。 添加一个新的变量到您的代码模块 指定变量名和默认值 通过添加一个新的变量&#xff0c;Ranorex Studio 会在光标位置插入一段新代码——由一个…

Python技巧:查询模块的版本号的方法

1,pycharm里面的 Python interpreter 或者 Python package 2&#xff0c;通过 __version_info__ import matplotlib print(matplotlib.__version_info__) 3&#xff0c;查看目录里面的 _version.py 文件

​​​​​​​15TS Series TVS 的解析

15TS Series 1500W Transient Voltage Suppresso指的是一系列高性能的瞬态电压抑制二极管&#xff08;Transient Voltage Suppressor&#xff0c;TVS&#xff09;&#xff0c;这些二极管由时源芯微&#xff08;TimeSource&#xff09;设计用于保护敏感的电子设备免受瞬态过电压…

Python学习从0到1 day27 Python 高阶技巧 ① 闭包

目录 一、闭包 作用 示例 二、nonlocal关键字 示例 三、atm取钱的闭包实现 四、闭包注意事项 优点 缺点 我陪你走了一段路&#xff0c;你最了解我不是吗 —— 24.11.11 一、闭包 在函数嵌套的前提下&#xff0c;内部函数使用了外部函数的变量&#xff0c;并且外部函数返回了内部…

python成长技能之网络编程

文章目录 一、初识Socket1.1 什么是 Socket?1.2 socket的基本操作1.3 socket常用函数 二、基于UDP实现客户端与服务端通信三、基于TCP实现客户端与服务端通信四、使用requests模块发送http请求 一、初识Socket 1.1 什么是 Socket? Socket又称"套接字"&#xff0c;…

[ACTF2020 新生赛]Upload 1--详细解析

信息收集 题目告诉我们是一道upload&#xff0c;也就是文件上传漏洞题目。 进入界面&#xff0c;是一个灯泡&#xff0c;将鼠标放在图标上就会出现文件上传的相应位置&#xff1a; 思路 文件上传漏洞&#xff0c;先看看有没有前端校验。 在js源码中找到了前端校验&#xff…

光伏设计软件怎么选?有哪些推荐?

在光伏电站的开发建设中&#xff0c;专业设计软件是提升电站能效、降低开发成本的重要工具。市场上存在许多优秀的光伏设计软件&#xff0c;能够通过还原现状和三维建模来呈现出最符合实际需求的设计方案&#xff0c;究竟该怎么选呢&#xff1f; -易用性&#xff1a;一些软件操…

刷题强训(day06) -- 大数加法、链表相加、大数乘法

目录 1、大数加法 1.1 题目 1.2 思路 1.3 代码实现 2、链表相加&#xff08;二&#xff09; 2.1 题目 2.2 思路 2.3 代码实现 3、大数乘法 3.1 题目 3.2 思路 3.3 代码实现 1、大数加法 1.1 题目 1.2 思路 这道题可以模拟列竖式相加解答&#xff0c; 将每一位都转…

雷池waf安装并部署防护站点

雷池waf安装并部署防护站点 最低配置要求 操作系统&#xff1a;Linux 指令架构&#xff1a;x86_64 软件依赖&#xff1a;Docker 20.10.14 版本以上 软件依赖&#xff1a;Docker Compose 2.0.0 版本以上 最小化环境&#xff1a;1 核 CPU / 1 GB 内存 / 5 GB 磁盘 写在前面 本文…

AI技术赋能电商行业:创新应用与未来展望

&#x1f493; 博客主页&#xff1a;倔强的石头的CSDN主页 &#x1f4dd;Gitee主页&#xff1a;倔强的石头的gitee主页 ⏩ 文章专栏&#xff1a;《热点时事》 期待您的关注 引言 随着科技的飞速发展&#xff0c;人工智能&#xff08;AI&#xff09;技术正逐步渗透到各行各业&a…

【Linux】进程(状态)

大家好呀&#xff0c;我是残念&#xff0c;希望在你看完之后&#xff0c;能对你有所帮助&#xff0c;有什么不足请指正&#xff01;共同学习交流哦 本文由&#xff1a;残念ing原创CSDN首发&#xff0c;如需要转载请通知 个人主页&#xff1a;残念ing-CSDN博客&#xff0c;欢迎各…

自动化测试框架的搭建详解

&#x1f345; 点击文末小卡片 &#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 最近好多小伙伴都在说接口自动化测试&#xff0c;那么究竟什么是接口自动化测试呢&#xff1f;让我们一起往下看就知道了&#xff0c;首先我们得先弄清楚下面这…

重拾CSS,前端样式精读-媒体查询

前言 本文收录于CSS系列文章中&#xff0c;欢迎阅读指正 说到媒体查询&#xff0c;大家首先想到的可能是有关响应式的知识点&#xff0c;除此之外&#xff0c;它还可以用于条件加载资源&#xff0c;字体大小&#xff0c;图像和视频的优化&#xff0c;用户界面调整等等方面&am…

4TS Series TVS 的解析

4TS Series 400W Transient Voltage Suppressor指的是时源芯微&#xff08;TimeSource&#xff09;生产的一系列瞬态电压抑制二极管&#xff08;Transient Voltage Suppressor&#xff0c;TVS&#xff09;&#xff0c;这些二极管专门设计用于保护敏感电子设备免受雷电、电源浪涌…

语义分割数据增强,图像和标签同步对应详细增强教程(附代码)

&#x1f4aa; 专业从事且热爱图像处理&#xff0c;图像处理专栏更新如下&#x1f447;&#xff1a; &#x1f4dd;《图像去噪》 &#x1f4dd;《超分辨率重建》 &#x1f4dd;《语义分割》 &#x1f4dd;《风格迁移》 &#x1f4dd;《目标检测》 &#x1f4dd;《图像增强》 &a…