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.cc
  static 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对象 VideoCaptureImpl
      capturer = 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_ 是 CapturerTrackSource
  video_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是 TestVideoCapturer
  source()->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,也就是sinkFilter
  CaptureSinkFilter* const filter = static_cast<CaptureSinkFilter*>(Filter());

  // 收到数据之后调用这个方法将数据从pin传给filter
  filter->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,将数据转换为I420
  rtc::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的方法将数据转换成I420
  const 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 Cropping
      width, 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。并且交代了这几个类的主要职责,以及如何利用他们创建一条数据链路的。后续,对具体的引擎再做分析。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

SQL Servers审核提高数据库安全性

什么是SQL Server审核&#xff1f; SQL Server审核包括追踪和审查发生在SQL Server上的所有活动&#xff0c;检测潜在的威胁和漏洞&#xff0c;能够监控和记录对服务器设置的每次更改。此外&#xff0c;可以帮助管理员可以轻松地追踪数据库中特定表中的所有服务器活动&#xf…

STM32+AI语音识别智能家居系统

基于 STM32 和 AI 语音识别的智能家居系统的详细硬件和软件设计&#xff0c;包括各个模块的详细描述和代码示例。 一、硬件设计 1. 微控制器&#xff08;STM32&#xff09;&#xff1a; 选择 STM32F7 系列或更高性能的芯片&#xff0c;如 STM32F767ZIT6&#xff0c;以满足处理…

在 ASP.NET Core 6.0 中使用 Swagger/OpenAPI 丰富 Web API 文档

示例代码&#xff1a;https://download.csdn.net/download/hefeng_aspnet/89961435 介绍 在选择或尝试与 API 集成之前&#xff0c;大多数开发人员都会查看其 API 文档。保持 API 文档更新以反映软件更改是一项挑战&#xff0c;需要时间和精力。对于 Web API&#xff0c;我们…

萤石设备视频接入平台EasyCVR海康私有化视频平台监控硬盘和普通硬盘有何区别?

在现代安防监控领域&#xff0c;对于数据存储和视频处理的需求日益增长&#xff0c;特别是在需要长时间、高稳定性监控的环境中&#xff0c;选择合适的存储设备和监控系统显得尤为重要。本文将深入探讨监控硬盘与普通硬盘的区别&#xff0c;并详细介绍海康私有化视频平台EasyCV…

使用Matlab建立随机森林

综述 除了神经网络模型以外&#xff0c;树模型及基于树的集成学习模型是较为常用的效果较好的预测模型。我们以下构建一个随机森林模型。 随机森林是一种集成学习方法&#xff0c;通过构建多个决策树并结合其预测结果来提高模型的准确性和稳定性。在MATLAB中&#xff0c;可以…

WPS宏编辑器开发,单元格内容变更自动触发事件

WPS中Excel的“触发器” 写在前面宏的开发1、切换宏编辑器开发环境2、小练习&#xff1a;自定义函数3、完成功能需求&#xff1a;单元格内容变更自动触发事件 总结 写在前面 我先生用EXCEL做了一张学生存款表。设计得很简单&#xff0c;A学生已存款X元&#xff0c;A学生再次存…

HarmonyOS Next星河版笔记--界面开发(4)

布局 1.1.线性布局 线性布局通过线性容器column和row创建 column容器&#xff1a;子元素垂直方向排列row容器&#xff1a;子元素水平方向排列 1.1.1.排布主方向上的对齐方式&#xff08;主轴&#xff09; 属性&#xff1a;.justifyContent&#xff08;枚举FlexAlign&#…

【前端】深入浅出的React.js详解

React 是一个用于构建用户界面的 JavaScript 库&#xff0c;由 Facebook 开发并维护。随着 React 的不断演进&#xff0c;官方文档也在不断更新和完善。本文将详细解读最新的 React 官方文档&#xff0c;涵盖核心概念、新特性、最佳实践等内容&#xff0c;帮助开发者更好地理解…

Rust开发一个命令行工具(一,简单版持续更新)

依赖的包 cargo add clap --features derive clap命令行参数解析 项目目录 代码 main.rs mod utils;use clap::Parser; use utils::{editor::open_in_vscode,fs_tools::{file_exists, get_file, is_dir, list_dir, read_file}, }; /// 在文件中搜索模式并显示包含它的行。…

Xcode 16 使用 pod 命令报错解决方案

原文请点击这个跳转 一、问题现象&#xff1a; 有人会遇到 Xcode 升级到 16 后&#xff0c;新建应用然后使用 pod init 命令会报错如下&#xff1a; Stack Ruby : ruby 3.3.5 (2024-09-03 revision ef084cc8f4) [x86_64-darwin23]RubyGems : 3.5.22Host : macOS 15.0 (24A335…

Linux 6.13 将提供对一系列 Pre-M1 苹果设备的基本支持

虽然不像苹果 M3/M4 设备支持上游主线 Linux 内核那样令人兴奋&#xff0c;但对于那些拥有一些较旧的苹果&#xff08;M1 之前&#xff09;设备的用户来说&#xff0c;即将发布的 Linux 6.13 内核将支持一些较旧的 SoC 和板卡。 即将到来的 Linux 6.13 合并窗口将支持大量旧版…

【蓝桥等考C++真题】蓝桥杯等级考试C++组第13级L13真题原题(含答案)-最大的数

CL13 最大的数(20 分) 输入一个有 n 个无重复元素的整数数组 a&#xff0c;输出数组中最大的数。提示&#xff1a;如使用排序库函数 sort()&#xff0c;需要包含头文件#include 。输入&#xff1a; 第一行是一个正整数 n(2<n<20)&#xff1b; 第二行包含 n 个不重复的整…

vue elementui el-dropdown-item设置@click无效的解决方案

如图&#xff0c;直接在el-dropdown-item上面设置click&#xff0c;相应的method并没有被触发&#xff0c;查找资料发现需要在它的上级 el-dropdown 处使用 command 方法触发。 【template】 <el-dropdown placement"bottom-end" command"handleCommand&quo…

flinkOnYarn并配置prometheus+grafana监控告警

flinkOnYarn并配置prometheusgrafana监控告警 一、相关服务版本&#xff1a; flink版本&#xff1a;1.17.2 pushgateway版本&#xff1a;1.10.0 prometheus版本&#xff1a;3.0.0 grafana-v11.3.0参考了网上的多个文档以及学习某硅谷的视频&#xff0c;总结了一下文档&#x…

Rocky、Almalinux、CentOS、Ubuntu和Debian系统初始化脚本v9版

Rocky、Almalinux、CentOS、Ubuntu和Debian系统初始化脚本 Shell脚本源码地址&#xff1a; Gitee&#xff1a;https://gitee.com/raymond9/shell Github&#xff1a;https://github.com/raymond999999/shell脚本可以去上面的Gitee或Github代码仓库拉取。 支持的功能和系统&am…

一文1800字使用Jmeter进行http接口性能测试!

接口测试是测试系统组件间接口的一种测试。接口测试主要用于检测外部系统与系统之间以及内部各个子系统之间的交互点。测试的重点是要检查数据的交换&#xff0c;传递和控制管理过程&#xff0c;以及系统间的相互逻辑依赖关系等。 为什么要做接口测试&#xff1f; 越底层发现b…

当使用key-value方式进行参数传递时,若key对应的是一个对象或数组结构,如何利用API Post工具进行模拟操作。

1. 后端服务代码如下 RequestMapping("/handle11")public Person handle11(Person person){System.out.println(person);return person;} 2. 后端入参结构 person是一个对象&#xff0c;对象结构如下&#xff1a; public class Person {private String username …

Pytorch学习--神经网络--完整的模型验证套路

一、选取的图片 全部代码依托于该博客 二、代码&#xff08;调用训练好的模型&#xff09; import torch import torchvision from PIL import Image from model import *img_path "dog.png" image Image.open(img_path)print(image.size)transform torchvisi…

PMP--一、二、三模--分类--变更

文章目录 技巧考试中的三大项目流程一 、变更流程 高频考点分析&#xff08;一、过程&#xff1b;二、人员&#xff09;一、过程&#xff1a;1.1 变更管理&#xff1a;1.1.1 瀑布型变更&#xff08;一次交付、尽量限制、确定性需求 &#xff1e;风险储备&#xff09;1.1.2 敏捷…

c语言选择排序

选择排序思想&#xff1a; 反复地从未排序部分选择最小&#xff08;或最大&#xff09;的元素&#xff0c;将其放到已排序部分的末尾&#xff1b; 首先用一个变量min来保存数组第一个元素的下标&#xff0c;然后用这个下标访问这个元素&#xff0c;将这个元素与它后面的元素相…