WebRTC视频 01 - 视频采集整体架构

一、前言:

我们从1对1通信说起,假如有一天,你和你情敌使用X信进行1v1通信,想象一下画面是不是一个大画面中有一个小画面?这在布局中就叫做PIP(picture in picture);这个随手一点,看似在1s不到就完成的动作,里面却经过了很多复杂的操作,我们今天开始写一系列文章介绍下这俩帅哥的图片怎么显示的。

二、宏观流程:

在这里插入图片描述

  1. 首先开始呼叫的时候会对摄像头和显示屏幕进行初始化。
  2. 摄像头采集完数据之后会进行分发,一路给本地进行渲染(你自己就画出来了),另外一路送给编码器进行编码。
  3. 编码后的数据通过网络模块发送(这里面其实非常复杂,后续专题分析)。
  4. 接收端通过网络模块接收数据,并进行一些排序,去掉RTP头等操作,得到编码后的视频帧。
  5. 视频帧送给解码模块进行解码。
  6. 将解码后的数据进行渲染(你女票也画出来了)。

三、类图:

在这里插入图片描述

  • 里面最重要的就是VideoCaptureModule,这是一个抽象类,不同平台有自己的实现。
  • VideoTrack是负责将整个链路创建起来。并不负责处理具体数据,数据由VideoCaptureModule、VcmCapture、VideoBroadcaster处理。
  • VcmCapture是PeerConnectionClient这个demo实现的一个类,里面包含了vcm_,又实现了TestVideoCapture(里面有broadcaster),所以这个类是左手连接数据源vcm,右手连接broadcaster;
  • VcmCapture包含的成员(其实是父类包含)VideoBroadCaster,负责分发给本地渲染器和Encoder;
  • 上面就是核心类,接下来看其他类,也就是看看怎么使用上面的核心类的。
    • 采集视频流的时候,首先会创建CaptureTrackSource。这个类里面会有capture(也就是VcmCapture);
    • 然后就是创建VideoTrack,里面有个成员变量video_source,就是上面创建的CaptureTrackSource的接口类指针,也就是说创建的收入接收了入参CaptureTrackSource,拥有了它;
    • 在PeerConnectionClient这个demo中,调用StartLocalRenderer的时候,就会调用VideoTrack(还记得吗,它是专门负责建立链路的)的AddOrUpdateSink,然后就会调用CaptureTrackSource的AddOrUpdateSink,接着调用VideoBroadCaster的AddOrUpdateSink。这个时候VideoBroadCaster就会将本地渲染器添加到自己的列表中,由于之前VideoCaptureModule****已经将采集模块启动起来了,因此,数据就源源不断的从VideoCaptureModule进入到VcmCapture,然后再进入VideoBroadCaster当中。
    • VideoBroadcaster收到数据,发现目标列表中有数据的时候,就会将数据转发给这些目标。也就是本地渲染器,这个时候本地就可以看到自己视频了;
    • 同样,在媒体协商进行到最后一步的时候,就会将编码器添加到VideoBroadCaster的目标列表当中,这样给本地渲染器分发数据的时候,同时也给编码器分发。

四、代码走读:

前面说了VideoTrack主要职责是建立数据链路,将数据源和数据消费者串起来,我们现在看看这条通道是怎么建立起来的。

我们先按照经验猜一下(其实我是看了代码的,假装猜一下):

  1. 首先,创建VcmCapture,因为它持有数据源VideoCaptureModule和分发器VideoBroadcaster,就可以把数据生产者和消费者连起来,形成通路;
  2. 有了VcmCapture,先给它创建数据源source,由于数据源CaptureTrackSource需要通过VideoTrack管理,因此,我们创建CaptureTrackSource之后,需要再创建一个VideoTrack,再将CaptureTrackSource交给VideoTrack持有,具体步骤:
    • 将上面的CaptureTrackSource对象video_device作为参数传入,创建VideoTrack,这样VideoTrack就持有了CaptureTrackSource(看上面类图);
  3. 至此,VcmCapture中就有了源,源产生的数据需要交给消费者,也就是VideoBroadcaster,然后VideoBroadcaster可以分发给本地渲染器和视频编码器;
  4. 三个重要对象创建好,就可以通过:VideoTrack -> CaptureTrackSource -> VcmCapture -> VideoBroadcaster 完成的链路搭建;

看代码:

代码入口:

// 代码路径:examples\peerconnection\client\conductor.cc
bool Conductor::InitializePeerConnection() {// 创建PeerConnection部分省略...// 添加track到PeerConnection中AddTracks();return peer_connection_ != nullptr;
}

AddTracks里面会:

  • 创建VcmCapture;
  • 创建数据生产者;
  • 创建数据消费者;
  • 并将生产者到消费者的链路建立起来;
  • 将VideoTrack这个管理者加入到PeerConnection当中;

已经删除非关键代码。

// 代码路径:examples\peerconnection\client\conductor.cc
void Conductor::AddTracks() {if (!peer_connection_->GetSenders().empty()) {return;  // Already added tracks.}// 1、构建一个数据源 CaptureTrackSource (里面会创建 VcmCapturer )rtc::scoped_refptr<CapturerTrackSource> video_device = CapturerTrackSource::Create();if (video_device) {// 2、构建一个 VideoTrack , 返回其代理类rtc::scoped_refptr<webrtc::VideoTrackInterface> video_track_(peer_connection_factory_->CreateVideoTrack(kVideoLabel, video_device));// 3、开始本地渲染main_wnd_->StartLocalRenderer(video_track_);// 4、将VideoTrack添加到PeerConnection当中管理result_or_error = peer_connection_->AddTrack(video_track_, {kStreamId});if (!result_or_error.ok()) {RTC_LOG(LS_ERROR) << "Failed to add video track to PeerConnection: "<< result_or_error.error().message();}}main_wnd_->SwitchToStreamingUI();
}

分开看下上面几个关键步骤:

1)创建CapturerTrackSource:

// 代码路径:examples\peerconnection\client\conductor.ccstatic rtc::scoped_refptr<CapturerTrackSource> Create() {const size_t kWidth = 640;const size_t kHeight = 480;const size_t kFps = 30;std::unique_ptr<webrtc::test::VcmCapturer> capturer;// 创建一个DeviceInfo对象,里面包含视频采集设备的属性信息std::unique_ptr<webrtc::VideoCaptureModule::DeviceInfo> info(webrtc::VideoCaptureFactory::CreateDeviceInfo());if (!info) {return nullptr;}// 获取采集设备数量(因为有些设备有多个摄像头),并遍历每个采集设备int num_devices = info->NumberOfDevices();for (int i = 0; i < num_devices; ++i) {// 为每个采集设备创建VcmCapture,里面会实例化vcm对象 VideoCaptureImplcapturer = absl::WrapUnique(webrtc::test::VcmCapturer::Create(kWidth, kHeight, kFps, i));if (capturer) {// 以VcmCapture为入参,创建CapturerTrackSource对象,并返回return new rtc::RefCountedObject<CapturerTrackSource>(std::move(capturer));}}return nullptr;}

上面就是创建了一个CapturerTrackSource对象,为什么我说是一个呢?因为,即使你有多个摄像头,找到第一个可用的,并创建了CapturerTrackSource就返回了。并且,在创建CapturerTrackSource对象的时候传入了一个VcmCapture对象,并持有了。这个VcmCapture里面又会创建具体的数据源采集类对象,即VideoCaptureImpl类型的capturer,由于人脑栈有限,先不深究capturer如何创建的,继续回头看主干,也就是Conductor::AddTracks()函数。

至此,拉皮条的VcmCapture有了,CapturerTrackSource有了,数据源VideoCaptureImpl有了,记住我们的目标是创建链路,那么还需要创建管理者VideoTrack,以及数据分发器VideoBroadcaster。

2)创建VideoTrack:

下面代码就不是examples,了是webrtc内核代码了。

// 代码路径:pc\peer_connection_factory.cc
rtc::scoped_refptr<VideoTrackInterface> PeerConnectionFactory::CreateVideoTrack(const std::string& id,VideoTrackSourceInterface* source) {RTC_DCHECK(signaling_thread()->IsCurrent());// 构建一个VideoTrack对象rtc::scoped_refptr<VideoTrackInterface> track(VideoTrack::Create(id, source, worker_thread()));return VideoTrackProxy::Create(signaling_thread(), worker_thread(), track);
}

注意是工作线程,一定要记住自己在哪个线程执行。还有,返回的是一个VideoTrack的代理类。

// 代码路径:pc\video_track.cc
rtc::scoped_refptr<VideoTrack> VideoTrack::Create(const std::string& id,VideoTrackSourceInterface* source,rtc::Thread* worker_thread) {// 创建了一个带有引用计数的VideoTrack对象,并返回了指针rtc::RefCountedObject<VideoTrack>* track =new rtc::RefCountedObject<VideoTrack>(id, source, worker_thread);return track;
}

不理解这个智能指针的,可以去看看我的另外一篇博客:https://blog.csdn.net/Ziwubiancheng/article/details/142985264?spm=1001.2014.3001.5501

3)创建链路:

至此,我们创建好了VcmCapture,并且创建好了具体数的数据采集类VideoCaptureImpl,具体的数据分发器VideoBroadcaster,以及其管理者VideoTrack。那么,管理者VideoTrack什么时候(when),在哪儿(where),通过何种方式(how),创建了什么样(what)的数据链路呢?我们详细分析下:

首先,有两条链路,想想之前哪个视频PIP画面,因此,需要一条本地渲染链路,以及一条远端渲染链路。

a)本地渲染链路:

入口就在:Conductor::AddTracks()的main_wnd_->StartLocalRenderer(video_track_);

// 代码路径:examples\peerconnection\client\main_wnd.cc
//  开始本地渲染
void MainWnd::StartLocalRenderer(webrtc::VideoTrackInterface* local_video) {// VideoRenderer 构造函数里面会调用 AddOrUpdateSink,一路调用到 VideoBroadcaster 当中// 这个 local_video 是一个 VideoTrack 对象local_renderer_.reset(new VideoRenderer(handle(), 1, 1, local_video));
}

看看VideoRenderer构造函数:

// 代码路径:examples\peerconnection\client\main_wnd.cc
MainWnd::VideoRenderer::VideoRenderer(HWND wnd,int width,int height,webrtc::VideoTrackInterface* track_to_render): wnd_(wnd), rendered_track_(track_to_render) {::InitializeCriticalSection(&buffer_lock_);ZeroMemory(&bmi_, sizeof(bmi_));bmi_.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);bmi_.bmiHeader.biPlanes = 1;bmi_.bmiHeader.biBitCount = 32;bmi_.bmiHeader.biCompression = BI_RGB;bmi_.bmiHeader.biWidth = width;bmi_.bmiHeader.biHeight = -height;bmi_.bmiHeader.biSizeImage =width * height * (bmi_.bmiHeader.biBitCount >> 3);// 这是一个 VideoTrack 对象,会将this(渲染器)添加到 VideoTrack 当中rendered_track_->AddOrUpdateSink(this, rtc::VideoSinkWants());
}

再进去看看这个AddOrUpdateSink方法(注意看上面的VideoRenderer对于VideoTrack来说就是个sink)

// 代码路径:pc\video_track.cc
// AddOrUpdateSink and RemoveSink should be called on the worker
// thread.
void VideoTrack::AddOrUpdateSink(rtc::VideoSinkInterface<VideoFrame>* sink,const rtc::VideoSinkWants& wants) {RTC_DCHECK(worker_thread_->IsCurrent());VideoSourceBase::AddOrUpdateSink(sink, wants);rtc::VideoSinkWants modified_wants = wants;modified_wants.black_frames = !enabled();// video_source_ 是 CapturerTrackSourcevideo_source_->AddOrUpdateSink(sink, modified_wants);
}

CaptureTrackSource是VideoTrackSource的子类,因此,会去调用VideoTrackSource:

// 代码路径:pc\video_track_source.cc
void VideoTrackSource::AddOrUpdateSink(rtc::VideoSinkInterface<VideoFrame>* sink,const rtc::VideoSinkWants& wants) {RTC_DCHECK(worker_thread_checker_.IsCurrent());// 直接调用 source里面的方法,这个source是 TestVideoCapturersource()->AddOrUpdateSink(sink, wants);
}

进去TestVideoCapturer看看:

void TestVideoCapturer::AddOrUpdateSink(rtc::VideoSinkInterface<VideoFrame>* sink,const rtc::VideoSinkWants& wants) {broadcaster_.AddOrUpdateSink(sink, wants);UpdateVideoAdapter();
}

TestVideoCapturer里面又调用了broadcaster的Add方法(记住我们设置渲染器的目标,就是最终设置给broadcaster)

看看VideoBroadcaster里面做了啥:

// 代码路径:media\base\video_broadcaster.cc
void VideoBroadcaster::AddOrUpdateSink(VideoSinkInterface<webrtc::VideoFrame>* sink,const VideoSinkWants& wants) {RTC_DCHECK(sink != nullptr);webrtc::MutexLock lock(&sinks_and_wants_lock_);if (!FindSinkPair(sink)) {// |Sink| is a new sink, which didn't receive previous frame.previous_frame_sent_to_all_sinks_ = false;}// 又调用了基类的方法VideoSourceBase::AddOrUpdateSink(sink, wants);UpdateWants();
}

进去看看其基类VideoSourceBase:

// 代码路径:media\base\video_source_base.cc
void VideoSourceBase::AddOrUpdateSink(VideoSinkInterface<webrtc::VideoFrame>* sink,const VideoSinkWants& wants) {RTC_DCHECK(sink != nullptr);SinkPair* sink_pair = FindSinkPair(sink);if (!sink_pair) {// 直接放到成员变量sinks里面了sinks_.push_back(SinkPair(sink, wants));} else {sink_pair->wants = wants;}
}

由于之前VideoCaptureModule已经将采集模块启动起来了,因此,数据就远远不断的从VideoCaptureModule进入到VcmCapture,然后,再进入VideoBroadCaster当中,broadcaster就会给sinks里面所有成员发一份数据。

至此,本地渲染链路就启动起来了。至于,拿到这些数据如何渲染到屏幕上,后续再分析。

b)远端发送链路:

上面创建了本地渲染链路,那么数据分发器VideoBroadCaster里面通常还会编码发送给远端。

  • 对于VideoStreamEncoder,在媒体协商之后,通过VideoTrack将VideoStreamEncoder添加到VideoBroadcaster当中。这样在VideoBroadcaster当中就有两个输出端。本地渲染器和Encoder;
  • 编码后的数据通过PacedSender传给网络传输模块;

由于大量媒体协商的内容在之前介绍过,我们就看下调用栈,关注我们视频相关内容即可。

调用栈:

AdaptedVideoTrackSource::AddOrUpdateSink
VideoSourceSinkController::SetSource
VideoStreamEncoder::SetSource
VideoSendStream::SetSource
WebRtcVideoChannel::WebRtcVideoSendStream::RecreateWebRtcStream
WebRtcVideoChannel::WebRtcVideoSendStream::SetCodec
WebRtcVideoChannel::WebRtcVideoSendStream::SetSendParameters
WebRtcVideoChannel::ApplyChangedParams
WebRtcVideoChannel::SetSendParameters(应用获取到的编码参数设置)
VideoChannel::SetRemoteContent_w
// 切换到工作线程
BaseChannel::SetRemoteContent
SdpOfferAnswerHandler::PushdownMediaDescription(根据SDP媒体部分的描述,更新内部对象)
SdpOfferAnswerHandler::UpdateSessionState(更新媒体协商状态机、媒体流、编解码器)
SdpOfferAnswerHandler::ApplyRemoteDescription
SdpOfferAnswerHandler::SetRemoteDescription
PeerConnection::SetRemoteDescription

AdaptedVideoTrackSource::AddOrUpdateSink当中:

// 代码路径:media\base\adapted_video_track_source.cc
void AdaptedVideoTrackSource::AddOrUpdateSink(rtc::VideoSinkInterface<webrtc::VideoFrame>* sink,const rtc::VideoSinkWants& wants) {// 添加到broadcaster当中了broadcaster_.AddOrUpdateSink(sink, wants);OnSinkWantsChanged(broadcaster_.wants());
}

至此,SetRemoteDescription的时候就将视频编码器添加进去视频分发器VideoBroadcaster了。

c)视频数据流动:

那么,视频数据究竟是如何进入到视频分发器VideoBroadcaster的呢?思路如下:

  1. 我们通过DirectShow采集到摄像头的视频数据之后,会通过Receive函数进入;
  2. 最终走到VcmCapture::OnFrame,再调用其父类TestVideoCapturer的OnFrame;
  3. 我们知道TestVideoCapture里面包含VideoBroadcaster,就可以通过它进行分发了;

具体调用栈如下:

TestVideoCapturer::OnFrame
VcmCapturer::OnFrame
VideoCaptureImpl::DeliverCapturedFrame
VideoCaptureImpl::IncomingFrame
CaptureSinkFilter::ProcessCapturedFrame
CaptureInputPin::Receive

看看具体函数:

// 代码路径:modules\video_capture\windows\sink_filter_ds.cc
/*** 接收采集到的视频数据时候,首先会进入到这儿* @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());// 收到数据之后调用这个方法将数据从pin传给filterfilter->ProcessCapturedFrame(sample_props.pbBuffer, sample_props.lActual,resulting_capability_);return S_OK;
}
// 代码路径:modules\video_capture\video_capture_impl.cc
/*** 通过 SinkFilter 获取到数据之后,会调用此函数,* 这个函数会将采集到的数据统一转换为I420格式的数据(因为用户request的格式是I420)*/
int32_t VideoCaptureImpl::IncomingFrame(uint8_t* videoFrame,size_t videoFrameLength,const VideoCaptureCapability& frameInfo,int64_t captureTime /*=0*/) {// 由于我们最终采集的数据肯定是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;
}

重点关注最后的DeliverCapturedFrame函数

// 代码路径:modules\video_capture\video_capture_impl.cc
/*** 里面会调用 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;
}

然后就到了VcmCpaturer

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

到了熟悉的TestVideoCapturer

/*** 从 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);if (out_height != frame.height() || out_width != frame.width()) {// 缩放部分省略...} else {// 如果不需要缩放,那么直接交给 VideoBroadcaster 进行分发// No adaptations needed, just return the frame as is.broadcaster_.OnFrame(frame);}
}

这样,就通过broadcaster分发给其内部已经添加的sink了。

五、总结:

本章主要介绍了视频数据采集的关键类VcmCapture、VideoTrack、VideoBroadcaster,VideoCapture。并且交代了这几个类的主要职责,以及如何利用他们创建一条数据链路的。后续,对具体的引擎再做分析。

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

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

相关文章

【大数据学习 | HBASE高级】rowkey的设计,hbase的预分区和压缩

1. rowkey的设计 ​ RowKey可以是任意字符串&#xff0c;最大长度64KB&#xff0c;实际应用中一般为10~100bytes&#xff0c;字典顺序排序&#xff0c;rowkey的设计至关重要&#xff0c;会影响region分布&#xff0c;如果rowkey设计不合理还会出现region写热点等一系列问题。 …

Spring Boot编程训练系统:架构设计与实现技巧

1系统概述 1.1 研究背景 随着计算机技术的发展以及计算机网络的逐渐普及&#xff0c;互联网成为人们查找信息的重要场所&#xff0c;二十一世纪是信息的时代&#xff0c;所以信息的管理显得特别重要。因此&#xff0c;使用计算机来管理编程训练系统的相关信息成为必然。开发合适…

刘知远LLM——大模型微调:prompt-learningdelta tuning

文章目录 背景&概览Prompt-learningdelta tuning增量式指定式重参数化式 OpenPrompt工具包 对应视频P41-P57 如何高效使用大模型&#xff1f;涉及到NLP的前沿技术&#xff0c;如prompt-learning&delta tuning。 prompt-learning对学习大模型范式的改变&#xff0c;del…

Spring Boot编程训练系统:性能优化实践

摘要 随着信息技术在管理上越来越深入而广泛的应用&#xff0c;管理信息系统的实施在技术上已逐步成熟。本文介绍了编程训练系统的开发全过程。通过分析编程训练系统管理的不足&#xff0c;创建了一个计算机管理编程训练系统的方案。文章介绍了编程训练系统的系统分析部分&…

电子应用产品设计方案-4:基于物联网和人工智能的温度控制器设计方案

一、概述 本温度控制器旨在提供高精度、智能化、远程可控的温度调节解决方案&#xff0c;适用于各种工业和民用场景。 二、系统组成 1. 传感器模块 - 采用高精度的数字式温度传感器&#xff0c;如 TMP117&#xff0c;能够提供精确到 0.01C 的温度测量。 - 配置多个传感器分布在…

如何在 Ubuntu 24.04 上安装和配置 Fail2ban ?

确保你的 Ubuntu 24.04 服务器的安全是至关重要的&#xff0c;特别是如果它暴露在互联网上。一个常见的威胁是未经授权的访问尝试&#xff0c;特别是通过 SSH。Fail2ban 是一个强大的工具&#xff0c;可以通过自动阻止可疑活动来帮助保护您的服务器。 在本指南中&#xff0c;我…

同三维T610UDP-4K60 4K60 DP或HDMI或手机信号采集卡

1路DP/HDMI/TYPE-C&#xff08;手机/平板等&#xff09;视频信号输入1路MIC1路LINE OUT,带1路HDMI环出&#xff0c;USB免驱&#xff0c;分辨率4K60&#xff0c;可采集3路信号中其中1路&#xff0c;按钮切换&#xff0c;可采集带TYPE-C接口的各品牌手机/平板/笔记本电脑等 同三维…

Kafka--关于broker的夺命连环问

目录 1、zk在kafka集群中有何作用 2、简述kafka集群中的Leader选举机制 3、kafka是如何处理数据乱序问题的。 4、kafka中节点如何服役和退役 4.1 服役新节点 1&#xff09;新节点准备 2&#xff09;执行负载均衡操作 4.2 退役旧节点 5、Kafka中Leader挂了&#xff0c;…

Web项目版本更新及时通知

背景 单页应用&#xff0c;项目更新时&#xff0c;部分用户会出更新不及时&#xff0c;导致异常的问题。 技术方案 给出版本号&#xff0c;项目每次更新时通知用户&#xff0c;版本已经更新需要刷新页面。 版本号更新方案版本号变更后通知用户哪些用户需要通知&#xff1f;…

Android音视频直播低延迟探究之:WLAN低延迟模式

Android WLAN低延迟模式 Android WLAN低延迟模式是 Android 10 引入的一种功能&#xff0c;允许对延迟敏感的应用将 Wi-Fi 配置为低延迟模式&#xff0c;以减少网络延迟&#xff0c;启动条件如下&#xff1a; Wi-Fi 已启用且设备可以访问互联网。应用已创建并获得 Wi-Fi 锁&a…

Appium配置2024.11.12

百度得知&#xff1a;谷歌从安卓9之后不再提供真机layout inspector查看&#xff0c;仅用于支持ide编写的app调试用 所以最新版android studio的android sdk目录下已经没有了布局查看工具... windows x64操作系统 小米k30 pro手机 安卓手机 Android 12 第一步&#xff1a…

前端使用Canvas实现网页电子签名(兼容移动端和PC端)

实现效果&#xff1a; 要使用Canvas实现移动端网页电子签名&#xff0c;可以按照以下步骤&#xff1a; 在HTML文件中创建一个Canvas元素&#xff0c;并设置其宽度和高度&#xff0c;以适配移动设备的屏幕大小。 // 创建一个canvas元素 let canvas document.createElement(&q…

使用 Python 实现高效网页爬虫——从获取链接到数据保存

前言 在这个时代,网络爬虫已成为数据分析与信息收集不可或缺的技术之一。本文将通过一个具体的Python项目来介绍如何构建一个简单的网络爬虫,它能够自动抓取指定网站的文章链接、标题、正文内容以及图片链接,并将这些信息保存为CSV文件。 目标网站 一、准备工作 在开始编…

跟着尚硅谷学vue2—进阶版4.0—Vuex1.0

5. Vuex 1. 理解 Vuex 1. 多组件共享数据-全局事件总线实现 红线是读&#xff0c;绿线是写 2. 多组件共享数据-vuex实现 vuex 不属于任何组件 3. 求和案例-纯vue版 核心代码 1.Count.vue <template><div><h1>当前求和为&#xff1a;{{ sum }}</h1&…

HTML之列表

练习题&#xff1a; 图所示为一个问卷调查网页&#xff0c;请制作出来。要求&#xff1a;大标题用h1标签&#xff1b;小题目用h3标签&#xff1b;前两个问题使用有序列表&#xff1b;最后一个问题使用无序列表。 代码&#xff1a; <!DOCTYPE html> <html> <he…

如何编写jenkins的流水线

如何编写jenkins的流水线 我们为什么需要编写流水线&#xff1f;新建一个jenkins pipeline的item初识pipeline界面pipeline代码关于取值声明和定义工具使用数据结构 我们为什么需要编写流水线&#xff1f; 这里假如你已经安装了好了jenkins&#xff0c;并且能够正常启动它。 通…

项目管理人员的自我评估与职业目标设定

在当今快速发展的商业环境中&#xff0c;项目管理人员的职业规划至关重要。它不仅涉及到个人职业发展的方向、目标和路径选择&#xff0c;还包括如何提升自身的专业技能、管理能力和行业知识。项目管理人员需要明确自己的职业目标、制定合理的职业发展计划、不断学习新知识和技…

状态空间方程离散化(Matlab符号函数)卡尔曼

// 卡尔曼滤波(4)&#xff1a;扩展卡尔曼滤波 - 知乎 // // matlab 连续系统状态空间表达式的离散化&状态转移矩阵求解_matlab状态方程离散化-CSDN博客 // // // %https://blog.csdn.net/weixin_44051006/article/details/107007916 clear all; clc; syms R1 R2 C1 C…

ubuntu24.04播放语音视频

直接打开ubuntu自带的video播放.mp4文件&#xff0c;弹窗报错如下&#xff1a; 播放此影片需要插件 MPEG-4 AAC 编码器安装方式&#xff1a; sudo apt install gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly sudo apt install ffmpeg验证AA…

音视频入门基础:MPEG2-TS专题(4)——使用工具分析MPEG2-TS传输流

一、引言 有很多工具可以分析MPEG2-TS文件/流&#xff0c;比如Elecard Stream Analyzer、PROMAX TS Analyser、easyice等。下面一一对它们进行简介&#xff08;个人感觉easyice功能更强大一点&#xff09;。 二、Elecard Stream Analyzer 使用Elecard Stream Analyzer工具可以…