【WebRTC-13】是在哪,什么时候,创建编解码器?

news2025/5/11 9:51:00

Android-RTC系列软重启,改变以往细读源代码的方式 改为 带上实际问题分析代码。增加实用性,方便形成肌肉记忆。同时不分种类、不分难易程度,在线征集问题切入点。

问题:编解码器的关键实体类是什么?在哪里&什么时候创建的?

这个问题是在分析webrtc如何增加第三方外置的编解码库时 额外提出来的,在找答案的过程中领略webrtc内部代码结构组织的划分。废话不多,这个问题的关键可以想到之前的一个问题  webrtc是如何确定双端的编解码类型?  是在sdp交换信息后,local和remote的description两者结合确认。那么可以在这基础上继续寻找,也就是去SdpOfferAnswerHandler找答案。

我们直接定位到 SdpOfferAnswerHandler:: ApplyLocalDescription / ApplyRemoteDescription。

RTCError SdpOfferAnswerHandler::ApplyLocalDescription(
    std::unique_ptr<SessionDescriptionInterface> desc,
    const std::map<std::string, const cricket::ContentGroup*>&  bundle_groups_by_mid) 
{
  pc_->ClearStatsCache();

  RTCError error = PushdownTransportDescription(cricket::CS_LOCAL, type);

  if (IsUnifiedPlan()) {
    UpdateTransceiversAndDataChannels(...)
  } else {
    ... ...
  }

  UpdateSessionState(type, cricket::CS_LOCAL,
                             local_description()->description(),
                             bundle_groups_by_mid);
  // Now that we have a local description, we can push down remote candidates.
  UseCandidatesInRemoteDescription();

  ... ...
}

大致的逻辑如上,这里关注 UpdateSessionState,继续深入。

RTCError SdpOfferAnswerHandler::UpdateSessionState(
    SdpType type,  cricket::ContentSource source,
    const cricket::SessionDescription* description,
    const std::map<std::string, const cricket::ContentGroup*>&  bundle_groups_by_mid) {
  // If this is answer-ish we're ready to let media flow.
  if (type == SdpType::kPrAnswer || type == SdpType::kAnswer) {
    EnableSending();
  }
  // Update the signaling state according to the specified state machine (see
  // https://w3c.github.io/webrtc-pc/#rtcsignalingstate-enum).
  if (type == SdpType::kOffer) {
    ChangeSignalingState(source == cricket::CS_LOCAL
                             ? PeerConnectionInterface::kHaveLocalOffer
                             : PeerConnectionInterface::kHaveRemoteOffer);
  } else if (type == SdpType::kPrAnswer) {
    ChangeSignalingState(source == cricket::CS_LOCAL
                             ? PeerConnectionInterface::kHaveLocalPrAnswer
                             : PeerConnectionInterface::kHaveRemotePrAnswer);
  } else {
    RTC_DCHECK(type == SdpType::kAnswer);
    ChangeSignalingState(PeerConnectionInterface::kStable);
    if (ConfiguredForMedia()) {
      transceivers()->DiscardStableStates();
    }
  }
  // Update internal objects according to the session description's media descriptions.
  return PushdownMediaDescription(type, source, bundle_groups_by_mid);
}

根据输入的type改变信令状态。注意最后的 PushdownMediaDescription,这里看函数名字有点奇怪,其核心功能是检索新的sdp信息,更新 rtp_transceivers 的channel

RTCError SdpOfferAnswerHandler::PushdownMediaDescription(
    SdpType type,  cricket::ContentSource source,
    const std::map<std::string, const cricket::ContentGroup*>&  bundle_groups_by_mid) 
{
  const SessionDescriptionInterface* sdesc =
      (source == cricket::CS_LOCAL ? local_description() : remote_description());
  // Push down the new SDP media section for each audio/video transceiver.
  auto rtp_transceivers = transceivers()->ListInternal();

std::vector<std::pair<cricket::ChannelInterface*, const MediaContentDescription*>> 
 channels;

  for (const auto& transceiver : rtp_transceivers) {
    const ContentInfo* content_info =
        FindMediaSectionForTransceiver(transceiver, sdesc);
    cricket::ChannelInterface* channel = transceiver->channel();

    const MediaContentDescription* content_desc = content_info->media_description();

    channels.push_back(std::make_pair(channel, content_desc));
  }


  for (const auto& entry : channels) {
    std::string error;
    bool success = context_->worker_thread()->BlockingCall([&]() {
      return (source == cricket::CS_LOCAL)
                 ? entry.first->SetLocalContent(entry.second, type, error)
                 : entry.first->SetRemoteContent(entry.second, type, error);
    });
  }
  return RTCError::OK();
}

(这里的channel以后介绍)伪代码如上。可以看到关于ChannelInterface的关键方法 SetLocalContent 、SetRemoteContent。

文件位置  src/pc/channel.cc
bool BaseChannel::SetLocalContent(const MediaContentDescription* content,
                                  SdpType type, std::string& error_desc) {
  RTC_DCHECK_RUN_ON(worker_thread());
  TRACE_EVENT0("webrtc", "BaseChannel::SetLocalContent");
  return SetLocalContent_w(content, type, error_desc);
}
bool BaseChannel::SetRemoteContent(const MediaContentDescription* content,
                                   SdpType type, std::string& error_desc) {
  RTC_DCHECK_RUN_ON(worker_thread());
  TRACE_EVENT0("webrtc", "BaseChannel::SetRemoteContent");
  return SetRemoteContent_w(content, type, error_desc);
}

SetLocalContent_w / SetRemoteContent_w又到具体的媒体通道类VoiceChannel / VideoChannel实现,以VideoChannel为例,精简核心代码如下。

bool VideoChannel::SetLocalContent_w(const MediaContentDescription* content,
                                     SdpType type, std::string& error_desc) 
{
  RtpHeaderExtensions header_extensions =
      GetDeduplicatedRtpHeaderExtensions(content->rtp_header_extensions());

  media_send_channel()->SetExtmapAllowMixed(content->extmap_allow_mixed());

  VideoReceiverParameters recv_params = last_recv_params_;
  VideoSenderParameters send_params = last_send_params_;

  MediaChannelParametersFromMediaDescription(
      content, header_extensions,
      webrtc::RtpTransceiverDirectionHasRecv(content->direction()),
      &recv_params);
  
  media_receive_channel()->SetReceiverParameters(recv_params);
  media_send_channel()->SetSenderParameters(send_params);

  UpdateLocalStreams_w(content->streams(), type, error_desc)

  UpdateMediaSendRecvState_w();
}

这里的UpdateLocalStreams_w又回到了BaseChannel。这里有一大段注释比较关键,主要描述了:在媒体协商的过程中SSRC与StreamParams相关联,构成安全的 local_stream_成员对象。

bool BaseChannel::UpdateLocalStreams_w(const std::vector<StreamParams>& streams,
                                       SdpType type, std::string& error_desc) 
{
  // In the case of RIDs (where SSRCs are not negotiated), this method will
  // generate an SSRC for each layer in StreamParams. That representation will
  // be stored internally in `local_streams_`.
  // In subsequent offers, the same stream can appear in `streams` again
  // (without the SSRCs), so it should be looked up using RIDs (if available)
  // and then by primary SSRC.
  // In both scenarios, it is safe to assume that the media channel will be
  // created with a StreamParams object with SSRCs. However, it is not safe to
  // assume that `local_streams_` will always have SSRCs as there are scenarios
  // in which niether SSRCs or RIDs are negotiated.
  ... ...
  media_send_channel()->AddSendStream(new_stream);
}

到这里我们先停顿一下,因为发现这里出现了众多 Channel 对象,ChannelInterface、BaseChannel、VoiceChannel/VideoChannel、media_send_channel/media_receive_channel。它们究竟是什么关系?我绘制了一张简易的UML图,这张图概括了Channel 以及之后要介绍的 Stream的内部关系,提取了核心代码的常见方法。大家一定要放大仔细看看!

小结:这部分阐述了 webrtc如何从sdp提取信息,根据ssrc创建并绑定网络传输通器中的 Channel。并通过代码,由BaseChannel->Video/VoiceChannel承接rtp数据包。


有了上面的UML图预热,在进入 media_send_channel()->AddSendStream的流程之前,我们要搞清楚这个 media_send_channel是怎么来的。

回到文章最开始的 SdpOfferAnswerHandler::ApplyLocalDescription 的UpdateTransceiversAndDataChannels。关键代码逻辑如下,可以看到涉及ChannelInterface的通道,是由RtpTransceiver内部创建的。

RTCError SdpOfferAnswerHandler::UpdateTransceiversAndDataChannels(
    cricket::ContentSource source,
    const SessionDescriptionInterface& new_session,
    const SessionDescriptionInterface* old_local_description,
    const SessionDescriptionInterface* old_remote_description,
    const std::map<std::string, const cricket::ContentGroup*>& bundle_groups_by_mid) 
{
    const ContentInfos& new_contents = new_session.description()->contents();
    
    for (size_t i = 0; i < new_contents.size(); ++i) {
      const cricket::ContentInfo& new_content = new_contents[i];

      auto transceiver_or_error =
          AssociateTransceiver(source, new_session.GetType(), i, new_content,
                               old_local_content, old_remote_content);
      auto transceiver = transceiver_or_error.Value();

      RTCError error= UpdateTransceiverChannel(transceiver, new_content, bundle_group);
    }
}

RTCError SdpOfferAnswerHandler::UpdateTransceiverChannel(
    rtc::scoped_refptr<RtpTransceiverProxyWithInternal<RtpTransceiver>> transceiver,
    const cricket::ContentInfo& content,
    const cricket::ContentGroup* bundle_group) 
{
    cricket::ChannelInterface* channel = transceiver->internal()->channel();
    if (!channel) {
      auto error = transceiver->internal()->CreateChannel(...);
    }
}

RtpTransceiver的CreateChannel内部,其核心是调用media_engine去创建对应的SendChannel / ReceiveChannel,最终组成RtpTransceiver的 VideoChannel / VoiceChannel。

RTCError RtpTransceiver::CreateChannel(
    absl::string_view mid,
    Call* call_ptr,
    const cricket::MediaConfig& media_config,
    bool srtp_required,
    CryptoOptions crypto_options,
    const cricket::AudioOptions& audio_options,
    const cricket::VideoOptions& video_options,
    VideoBitrateAllocatorFactory* video_bitrate_allocator_factory,
    std::function<RtpTransportInternal*(absl::string_view)> transport_lookup) 
{
  std::unique_ptr<cricket::ChannelInterface> new_channel;

  if (media_type() == cricket::MEDIA_TYPE_VIDEO) {
    std::unique_ptr<cricket::VideoMediaSendChannelInterface>
          media_send_channel = media_engine()->video().CreateSendChannel(...);

    std::unique_ptr<cricket::VideoMediaReceiveChannelInterface>
          media_receive_channel = media_engine()->video().CreateReceiveChannel(...);

    new_channel = std::make_unique<cricket::VideoChannel>(
          worker_thread(), network_thread(), signaling_thread(), 
          std::move(media_send_channel), std::move(media_receive_channel), ...);
  } else {
    // media_type() == cricket::MEDIA_TYPE_AUDIO
  }

  SetChannel(std::move(new_channel), transport_lookup);
  return RTCError::OK();
}

根据以往的文章,我们就可以快速定位到 src/media/engine/webrtc_video_engine / webrtc_audio_engine,找到SendChannel ReceiveChannel。至此,我们正式定位到了media_send_channel()的具体实现。

// 以media_type==video为例
std::unique_ptr<VideoMediaSendChannelInterface>
WebRtcVideoEngine::CreateSendChannel(
    webrtc::Call* call,
    const MediaConfig& config,
    const VideoOptions& options,
    const webrtc::CryptoOptions& crypto_options,
    webrtc::VideoBitrateAllocatorFactory* video_bitrate_allocator_factory) {
  return std::make_unique<WebRtcVideoSendChannel>(
      call, config, options, crypto_options, encoder_factory_.get(),
      decoder_factory_.get(), video_bitrate_allocator_factory);
}
std::unique_ptr<VideoMediaReceiveChannelInterface>
WebRtcVideoEngine::CreateReceiveChannel(
    webrtc::Call* call,
    const MediaConfig& config,
    const VideoOptions& options,
    const webrtc::CryptoOptions& crypto_options) {
  return std::make_unique<WebRtcVideoReceiveChannel>(
      call, config, options, crypto_options, decoder_factory_.get());
}
小结:根据m=session创建RtpTransceiver,Video/VoiceChannel由webrtc_medie_engine创建,并保存在RtpTransceiver网络传器者的成员变量。Video/VoiceChannel 内包含SendChannel和ReceiveChannel。


回头再看media_send_channel()->AddSendStream(new_stream),即WebRtcVideoSendChannel::AddSendStream,其核心逻辑很简单:

bool WebRtcVideoSendChannel::AddSendStream(const StreamParams& sp) 
{
  WebRtcVideoSendStream* stream = new WebRtcVideoSendStream(
      call_, sp, std::move(config), default_send_options_,
      video_config_.enable_cpu_adaptation, bitrate_config_.max_bitrate_bps,
      send_codec(), send_rtp_extensions_, send_params_);

  uint32_t ssrc = sp.first_ssrc();
  send_streams_[ssrc] = stream;
}

WebRtcVideoSendStream 的构造函数内容比较多,但都是属性赋值。我们这里只关心文章提出的问题,也就是构造函数里唯一调用的成员函数SetCodec。

// src/media/engine/webrtc_video_engine.cc
void WebRtcVideoSendChannel::WebRtcVideoSendStream::SetCodec(
    const VideoCodecSettings& codec_settings) 
{
  parameters_.encoder_config = CreateVideoEncoderConfig(codec_settings.codec);
  parameters_.config.rtp = ...
  parameters_.codec_settings = codec_settings;
  // TODO(bugs.webrtc.org/8830): Avoid recreation, it should be enough to call  ReconfigureEncoder.
  RTC_LOG(LS_INFO) << "RecreateWebRtcStream (send) because of SetCodec.";
  RecreateWebRtcStream();
}

void WebRtcVideoSendChannel::WebRtcVideoSendStream::RecreateWebRtcStream() 
{
  if (stream_ != NULL) {
    call_->DestroyVideoSendStream(stream_);
  }
  stream_ = call_->CreateVideoSendStream(std::move(config),
                                         parameters_.encoder_config.Copy());
  // Attach the source after starting the send stream to prevent frames from
  // being injected into a not-yet initializated video stream encoder.

  // rtc::VideoSourceInterface<webrtc::VideoFrame>* source_
  if (source_) {
    stream_->SetSource(source_, GetDegradationPreference());
  }
}

具体实现还要到Call::CreateVideoSendStream。这里有个细节,stream_->SetSource(webrtc::VideoFrame )出现了VideoFrame,显然路是找对了,继续往下。

//  src/call/call.cc
webrtc::VideoSendStream* Call::CreateVideoSendStream(
    webrtc::VideoSendStream::Config config,
    VideoEncoderConfig encoder_config,
    std::unique_ptr<FecController> fec_controller) 
{
  VideoSendStreamImpl* send_stream = new VideoSendStreamImpl(...);

  for (uint32_t ssrc : ssrcs) {
    RTC_DCHECK(video_send_ssrcs_.find(ssrc) == video_send_ssrcs_.end());
    video_send_ssrcs_[ssrc] = send_stream;
  }
  video_send_streams_.insert(send_stream);
  video_send_streams_empty_.store(false, std::memory_order_relaxed);
}

// src/video/video_send_stream_impl.cc
VideoSendStreamImpl::VideoSendStreamImpl(
    RtcEventLog* event_log,
    VideoSendStream::Config config,
    VideoEncoderConfig encoder_config,
    std::unique_ptr<FecController> fec_controller,
    const FieldTrialsView& field_trials,
    std::unique_ptr<VideoStreamEncoderInterface> video_stream_encoder_for_test)
//构造赋值
: video_stream_encoder_(
          video_stream_encoder_for_test
              ? std::move(video_stream_encoder_for_test)
              : CreateVideoStreamEncoder(...) 
  ), ... ...
//构造VideoStreamEncoder
std::unique_ptr<VideoStreamEncoderInterface> CreateVideoStreamEncoder() {
  std::unique_ptr<TaskQueueBase, TaskQueueDeleter> encoder_queue =
      task_queue_factory->CreateTaskQueue("EncoderQueue",
                                          TaskQueueFactory::Priority::NORMAL);
  TaskQueueBase* encoder_queue_ptr = encoder_queue.get();
  return std::make_unique<VideoStreamEncoder>(...std::move(encoder_queue), ...);
}

到这里我们就找到了webrtc的视频编码实体类VideoStreamEncoder(src/video/video_stream_encoder.cc),相对应的解码实体类VideoStreamDecoder。这里放一些关键方法,此类是个宝库,任何关于视频编码功能的细节,都可以在这找到参考。


总结答案:整篇文章跟踪的代码逻辑如下,归纳了从Sdp->RtpTransceiver->VideoChannel/VoiceChannel->Send&ReceiveChannel-> 然后根据sdp::ssrc创建VideoSendStream-> VideoStreamEncoder。

还待挖掘的细节非常多,奈何篇幅有限有所侧重。在写本文章的时候,其实是在研究icecandidate,stun服务端已经搭建起来了。希望有兴趣的同学联系我,一起深入成长。

SdpOfferAnswerHandler:: ApplyLocalDescription / ApplyRemoteDescription(sdp信息)
SdpOfferAnswerHandler::UpdateTransceiversAndDataChannels -> UpdateTransceiverChannel(创建RtpTransceiver->Video/VoiceChannel)
SdpOfferAnswerHandler::UpdateSessionState
SdpOfferAnswerHandler::PushdownMediaDescription
BaseChannel::SetLocalContent(const MediaContentDescription* content, ..)
VoiceChannel/VideoChannel::SetLocalContent_w
BaseChannel::UpdateLocalStreams_w(const std::vector<StreamParams>& streams, ..)

WebRtcVideoSendChannel::AddSendStream
WebRtcVideoSendChannel::WebRtcVideoSendStream::WebRtcVideoSendStream(Constructor)
WebRtcVideoSendChannel::WebRtcVideoSendStream::SetCodec|::RecreateWebRtcStream|::SetSenderParameters|::ReconfigureEncoder
Call::CreateVideoSendStream
VideoSendStreamImpl() -> VideoStreamEncoder(Interface)

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

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

相关文章

青少年编程与数学 02-019 Rust 编程基础 01课题、环境准备

青少年编程与数学 02-019 Rust 编程基础 01课题、环境准备 一、Rust核心特性应用场景开发工具社区与生态 二、Rust 和 Python 比较1. **内存安全与并发编程**2. **性能**3. **零成本抽象**4. **跨平台支持**5. **社区与生态系统**6. **错误处理**7. **安全性**适用场景总结 三、…

Redis持久化存储介质评估:NFS与Ceph的适用性分析

#作者&#xff1a;朱雷 文章目录 一、背景二、Redis持久化的必要性与影响1. 持久化的必要性2. 性能与稳定性问题 三、NFS作为持久化存储介质的问题1. 性能瓶颈2. 数据一致性问题3. 存储服务单点故障4. 高延迟影响持久化效率.5. 吞吐量瓶颈 四、Ceph作为持久化存储介质的问题1.…

Ceph 原理与集群配置

一、Ceph 工作原理 1.1.为什么学习 Ceph&#xff1f; 在学习了 NFS 存储之后&#xff0c;我们仍然需要学习 Ceph 存储。这主要是因为不同的存储系统适用于不同的场景&#xff0c;NFS 虽然有其适用之处&#xff0c;但也存在一定的局限性。而 Ceph 能够满足现代分布式、大规模、…

天线的PCB设计

目录 天线模块设计的重要性 天线模块的PCB设计 天线模块设计的重要性 当智能手表突然断连、无人机信号飘忽不定——你可能正在经历一场来自天线模块的"无声抗议"。这个隐藏在电子设备深处的关键组件&#xff0c;就像数字世界的隐形信使&#xff0c;用毫米级的精密结…

C++笔记-set和map的使用(包含multiset和multimap的讲解)

1.序列式容器和关联式容器 前面我们已经接触过STL中的部分容器如:string、vector、list、deque、array、forward_list等&#xff0c;这些容器统称为序列式容器&#xff0c;因为逻辑结构为线性序列的数据结构&#xff0c;两个位置存储的值之间一般没有紧密的关联关系&#xff0…

Linux `ifconfig` 指令深度解析与替代方案指南

Linux `ifconfig` 指令深度解析与替代方案指南 一、核心功能与现状1. 基础作用2. 版本适配二、基础语法与常用操作1. 标准语法2. 常用操作速查显示所有接口信息启用/禁用接口配置IPv4地址修改MAC地址(临时)三、高级配置技巧1. 虚拟接口创建2. MTU调整3. 多播配置4. ARP控制四…

Python pandas 向excel追加数据,不覆盖之前的数据

最近突然看了一下pandas向excel追加数据的方法&#xff0c;发现有很多人出了一些馊主意&#xff1b; 比如用concat,append等方法&#xff0c;这种方法的会先将旧数据df_1读取到内存&#xff0c;再把新数据df_2与旧的合并&#xff0c;形成df_new,再覆盖写入&#xff0c;消耗和速…

【金仓数据库征文】政府项目数据库迁移:从MySQL 5.7到KingbaseES的蜕变之路

摘要&#xff1a;本文详细阐述了政府项目中将 MySQL 5.7 数据库迁移至 KingbaseES 的全过程&#xff0c;涵盖迁移前的环境评估、数据梳理和工具准备&#xff0c;迁移实战中的数据源与目标库连接配置、迁移任务详细设定、执行迁移与过程监控&#xff0c;以及迁移后的质量验证、系…

Go语言——goflow工作流使用

一、引入依赖 这个很坑&#xff0c;他不允许连接带密码的redis&#xff0c;只能使用不带密码的redis&#xff0c;要带密码的话得自己改一下源代码&#xff0c;无语 go get github.com/s8sg/goflow二、画出我们的工作流程 三、编写代码 package mainimport ("encoding/j…

yarn npm pnpm

1 下载方式 npm 之前串行下载 现在并行下载 yarn 并行下载 加入缓存复用 pnpm 硬连接 避免重复下载&#xff0c;先检查本地是否存在&#xff0c;存在的话直接连接过去

Block Styler——字符串控件

字符串控件的应用 参考官方帮助案例&#xff1a;&#xff08;这个方式感觉更好&#xff0c;第二种方式也可以&#xff09;E:\NX1980\UGOPEN\SampleNXOpenApplications\C\BlockStyler\ColoredBlock 普通格式&#xff1a; 读取&#xff1a; //方法一 string0->GetProperti…

LangGraph(三)——添加记忆

目录 1. 创建MemorySaver检查指针2. 构建并编译Graph3. 与聊天机器人互动4. 问一个后续问题5. 检查State参考 1. 创建MemorySaver检查指针 创建MemorySaver检查指针&#xff1a; from langgraph.checkpoint.memory import MemorySavermemory MemorySaver()这是位于内存中的检…

【无标题】I/O复用(epoll)三者区别▲

一、SOCKET-IO复用技术 定义&#xff1a;SOCKET - IO复用技术是一种高效处理多个套接字&#xff08;socket&#xff09;的手段&#xff0c;能让单个线程同时监听多个文件描述符&#xff08;如套接字&#xff09;上的I/O事件&#xff08;像可读、可写、异常&#xff09;&#x…

ClassLoader类加载机制的核心引擎

ClassLoader类加载机制的核心引擎 文章目录 ClassLoader类加载机制的核心引擎1. ClassLoader基础1.1 什么是ClassLoader&#xff1f;1.2 ClassLoader的层次结构1.3 类加载的过程 2. 源码解析与工作原理2.1 ClassLoader的核心方法2.2 双亲委派模型的工作原理2.3 打破双亲委派模型…

tryhackme——Enumerating Active Directory

文章目录 一、凭据注入1.1 RUNAS1.2 SYSVOL1.3 IP和主机名 二、通过Microsoft Management Console枚举AD三、通过命令行net命令枚举四、通过powershell枚举 一、凭据注入 1.1 RUNAS 当获得AD凭证<用户名>:<密码>但无法登录域内机器时&#xff0c;runas.exe可帮助…

【Linux学习笔记】系统文件IO之重定向原理分析

【Linux学习笔记】系统文件IO之重定向原理分析 &#x1f525;个人主页&#xff1a;大白的编程日记 &#x1f525;专栏&#xff1a;Linux学习笔记 文章目录 【Linux学习笔记】系统文件IO之重定向原理分析前言一. 系统文件I/01.1 一种传递标志位的方法1.2 hello.c写文件:1.3 he…

SpringBoot中使用MCP和通义千问来处理和分析数据-连接本地数据库并生成实体类

文章目录 前言一、正文1.1 项目结构1.2 项目环境1.3 完整代码1.3.1 spring-mcp-demo的pom文件1.3.2 generate-code-server的pom文件1.3.3 ChatClientConfig1.3.4 FileTemplateConfig1.3.5 ServiceProviderConfig1.3.6 GenerateCodeController1.3.7 Columns1.3.8 Tables1.3.9 Fi…

实现滑动选择器从离散型的数组中选择

1.使用原生的input 详细代码如下&#xff1a; <template><div class"slider-container"><!-- 滑动条 --><inputtype"range"v-model.number"sliderIndex":min"0":max"customValues.length - 1"step&qu…

基于Credit的流量控制

流量控制(Flow Control)&#xff0c;也叫流控&#xff0c;它是控制组件之间发送和接收信息的过程。在总线中&#xff0c;流控的基本单位称为flit。 在标准同步接口中(比如AXI协议接口)&#xff0c;握手信号如果直接采用寄存器打拍的方式容易导致信号在不同的方向上出现偏离。因…

【金仓数据库征文】金仓数据库KingbaseES: 技术优势与实践指南(包含安装)

目录 前言 引言 一 : 关于KingbaseES,他有那些优势呢? 核心特性 典型应用场景 政务信息化 金融核心系统&#xff1a; 能源通信行业&#xff1a; 企业级信息系统&#xff1a; 二: 下载安装KingbaseES 三:目录一览表: 四:常用SQL语句 创建表&#xff1a; 修改表结构…