音视频即时通讯通信【免费下载链接】webrtcAsync-friendly WebRTC implementation in Rust项目地址https://gitcode.com/gh_mirrors/webr/webrtc点击查看免费下载本文以 webrtc-rswebrtccrate仓库中的rtcp-processing示例为主线讲清楚一个关键问题入站 RTCP 默认是拦截器链的内部控制流量应用默认什么都看不到。要看到 RTCP必须写一个自定义拦截器把想要的那部分包打上Attribute::DeliverToApplication标记。读完后你将掌握如何注册这样的拦截器、为什么“打标”比“复制转发”更合理以及如何通过TrackRemoteEvent::OnRtcpPacket在异步侧稳定接收关键帧请求PLI/FIR并能用仓库自带的集成测试验证双向行为。默认行为入站 RTCP 为什么到不了应用rtcp-processing示例README、主程序演示的核心就是在没有特殊处理时浏览器发来的 RTCP 包会被拦截器链在到达应用之前全部消费掉。从 TrackRemoteEvent::OnRtcpPacket 的文档 可以看到官方对这个默认行为的明确定义当远程 track 上收到新的 RTCP 包且有拦截器把它们标记给应用时触发。入站 RTCP 是拦截器要处理的控制流量——接收报告喂给发送端统计、NACK 要被应答、传输层反馈驱动带宽估计——所以它默认在链尾结束、事件不触发。要收到它需要加一个把Attribute::DeliverToApplication挂到你想要的包上的拦截器rtcp-processing示例正是这么做的。这里“没有事件”应理解为“没有任何包被担保放行”而不是“对端一个都没发”。换句话说“应用收不到 RTCP”是设计意图而非 bugReceiver Report、NACK、Transport-Wide CC 这些包链路上的默认拦截器已经各自消费完毕。真正需要应用亲自出面的是关键帧请求——因为只有应用自己知道这条流是谁生产的、怎么重发关键帧。拦截器实现RtcpForwarderInterceptor 全解析示例的核心是一个自定义拦截器RtcpForwarderInterceptor它把入站 RTCP 收窄为关键帧请求PLI 与 FIR并给保留下来的包打上Attribute::DeliverToApplication。完整实现见 rtcp-processing.rs关键部分如下。判定“什么是关键帧请求”/// Whether an RTCP packet is a request for a keyframe. fn is_keyframe_request(packet: Boxdyn rtc::rtcp::Packet) - bool { let payload packet.as_any(); payload.is::PictureLossIndication() || payload.is::FullIntraRequest() }判定逻辑非常直接对包做 downcast 检查PictureLossIndicationPLI或FullIntraRequestFIR二者之一即为关键帧请求。这两个类型来自rtc::rtcp::payload_feedbacks是 RTCP 反馈负载的标准定义。读路径收窄 打标拦截器实现的是Protocoltrait入站用handle_read/poll_read出站用handle_write/poll_write读路径是这个示例的精髓fn handle_read(mut self, mut msg: TaggedPacket) - Result(), Self::Error { if let Packet::Rtcp(packets) msg.message.packet { let requests: VecBoxdyn rtc::rtcp::Packet packets .iter() .filter(|packet| is_keyframe_request(packet)) .cloned() .collect(); if requests.is_empty() { // Not the applications business, and the interceptors have already acted on it. return Ok(()); } msg.message.packet Packet::Rtcp(requests); // Inbound RTCP stops at the end of the chain unless something vouches for it. msg.message.add(Attribute::DeliverToApplication); } self.read_queue.push_back(msg); Ok(()) }这段代码做了三件事每一步都有源码注释佐证其意图过滤从 RTCP 复合包中筛出 PLI/FIR如果为空“不是应用该操心的事拦截器们也已经处理过了”直接return让原始包继续走读队列但不打标最终在链尾被消费收窄把msg.message.packet原地替换为只含关键帧请求的Packet::Rtcp(requests)后面环节看到的已经是精简后的内容打标msg.message.add(Attribute::DeliverToApplication)这就是“担保放行”的动作。为什么是“打标”而不是“复制转发”README 里专门用一节解释了这个设计决策值得完整理解标记之所以是必要机制是因为一份副本永远跑不过链尾链是一个扁平列表某个拦截器发出的包只会插回列表里它自己身后的位置而结束入站 RTCP 路径的那一级仍然在它前面。被打标的包则正常走完整条链那一级读到标记后放行。标记也让判定保持逐包进行——这正是要点这个示例为关键帧请求担保让接收报告继续停在它们一直停留的地方。也就是说拦截器链是“传送带”模型后发的包只会插回自己身后不存在一个位置能让包“越过链尾”。Attribute::DeliverToApplication让包正常走完行程由链尾的终止级读标记做决定同时判定保持逐包粒度——同一批包里PLI 被放行Receiver Report 依旧停在链尾。这正是“链级开关”无法做到的事。它必须放在链上什么位置示例代码中拦截器的文档注释写明Last这样在它收窄之前每一个读取 RTCP 的拦截器都已经完整看到了全部入站 RTCP。组装 registry 时对应这一句见 rtcp-processing.rs// Register default interceptors (NACK, reports, etc.) let registry register_default_interceptors(Registry::new(), mut media_engine)?; // Inbound RTCP is for the interceptors unless one of them marks a packet for the application. // Application-most, so every interceptor has already seen the whole of the inbound RTCP before // this one narrows it to keyframe requests and marks those. let registry registry.with(Slot::from(14_000), RtcpForwarderBuilder::new().build());先用register_default_interceptors注册默认拦截器NACK 应答、报告、拥塞控制等再用registry.with(Slot::from(14_000), ...)把自定义拦截器追加进 registry。Slot是链上的槽位序号14_000这个高序号把它排在默认拦截器之后——“靠后”保证了收窄动作不会截胡别人要读的 RTCP。完整构建流程从 MediaEngine 到 PeerConnection示例的run()函数展示了与仓库其他异步示例一致的PeerConnectionBuilder模式按顺序做以下几步注册编解码器MediaEngine 注册 VP8videopayload type 9690 kHz和 Opusaudiopayload type 11148 kHz双声道类型分别为RtpCodecKind::Video/RtpCodecKind::Audio注册拦截器即上面展示的register_default_interceptorsRtcpForwarderInterceptor配置 ICERTCConfigurationBuilder里放了一个stun:stun.l.google.com:19302服务器构建连接let peer_connection PeerConnectionBuilder::new() .with_configuration(config) .with_media_engine(media_engine) .with_interceptor_registry(registry) .with_handler(Arc::clone(handler) as Arcdyn PeerConnectionEventHandler) .with_runtime(runtime) .with_udp_addrs(vec![format!({}:0, signal::get_local_ip())]) .build() .await?;注意with_runtime本库没有环境默认运行时每个PeerConnection通过 builder 显式注入Arcdyn Runtime。示例里用的runtime()/block_on()等辅助函数来自 examples/common/mod.rs它用OnceLock缓存全进程一个default_runtime()并按 Cargo featureruntime-tokio或runtime-smol选择内置运行时——这也是为什么示例代码本身对运行时保持中立。添加 recvonly 收发器音频和视频各一个方向均为RTCRtpTransceiverDirection::Recvonlypeer_connection .add_transceiver_from_kind( RtpCodecKind::Video, Some(RTCRtpTransceiverInit { direction: RTCRtpTransceiverDirection::Recvonly, ..Default::default() }), ) .await?;这个 recvonly 身份对理解输出行为很关键本端只收不发所以下游浏览器方向不会向它要关键帧它自己打印什么完全取决于谁向它发了 PLI/FIR。构建、运行与命令行参数rtcp-processing在 Cargo.toml 中注册为[[example]]构建与运行命令来自 READMEcargo build --example rtcp-processing cargo run --example rtcp-processing # 带调试日志 cargo run --example rtcp-processing -- --debug # 从文件读取 SDP cargo run --example rtcp-processing -- --input-sdp-file offer.txt适用前提Cargo.toml的默认 feature 是[runtime-tokio, crypto-ring]按默认 feature 构建即可直接运行。CLI 定义用clapderive完整参数如下rtcp-processing.rs参数短选项默认值作用--debug-dfalse初始化env_logger按文件:行号 [级别] 时间戳 - 内容格式输出日志--log-level-lINFO日志级别过滤log::LevelFilter::from_str解析--input-sdp-file-i空非空时从该文件读取 offer SDP替代从 stdin 读--output-log-file-o空非空且--debug时日志写入该文件覆盖写否则输出到 stdout一个值得注意的细节--log-level只在--debug分支里生效——只有开了--debug才会初始化 loggerlog_level才是那套env_logger::Builder的过滤级别不传--debug时程序不产生日志。信令流程与浏览器配合README 给出的信令流程是从浏览器粘贴 base64 编码的 SDP offer把程序打印的 base64 answer 贴回浏览器浏览器开始发送音视频观察按远程 track 打印的入站 RTCP 包。浏览器那一半由仓库提供的 jsfiddle 页面承担jsfiddle 演示README 中给出的在线地址为 https://jsfiddle.net/zurq6j7x/ 。页面上有文本域、Start Session按钮和Copy browser SessionDescription to clipboard。其 demo.js 的逻辑是用getUserMedia拿到音视频流并addTrackcreateOffersetLocalDescription后等 ICE gathering 结束onicecandidate收到null候选时把 base64 的localDescription填入输入框startSession()则把粘贴进来的 base64 解码后setRemoteDescription。程序侧的对应代码rtcp-processing.rsprint!(Paste offer from browser and press Enter: ); let line if input_sdp_file.is_empty() { signal::must_read_stdin()? } else { std::fs::read_to_string(input_sdp_file)? }; let desc_data signal::decode(line.as_str())?; // base64 解码 let offer serde_json::from_str::RTCSessionDescription(desc_data)?; peer_connection.set_remote_description(offer).await?; let answer peer_connection.create_answer(None).await?; peer_connection.set_local_description(answer).await?;等待 gathering 完成后answer 会被 base64 编码打印到终端let json_str serde_json::to_string(local_desc)?; let b64 signal::encode(json_str); println!(\nPaste this answer in your browser:\n{}\n, b64);异步侧接收TrackRemoteEvent::OnRtcpPacketREADME 给出的最小接收循环是while let Some(evt) track.poll().await { if let TrackRemoteEvent::OnRtcpPacket(rtcp_packets) evt { for packet in rtcp_packets { let header packet.header(); println! (Type: {:?}, header.packet_type); println! ({packet}); } } }示例的完整处理在on_track回调里拿到track_id、stream_id、kind、codec等元信息打印后向 runtime spawn 一个轮询任务。每到一批包rtcp-processing.rsTrackRemoteEvent::OnRtcpPacket(rtcp_packets) { let batch rtcp_count.fetch_add(1, Ordering::Relaxed) 1; println!( Keyframe request #{} (Track: {}) , batch, track_id); for (i, packet) in rtcp_packets.iter().enumerate() { let header packet.header(); println!( [{}] Type: {:?}, Length: {} words, i 1, header.packet_type, header.length ); for line in format!({packet}).lines() { println!( {}, line); } } } TrackRemoteEvent::OnEnded { /* 打印后 break */ } TrackRemoteEvent::OnError { /* 打印后 break */ }rtcp_count是共享的ArcAtomicU64退出时打印Total RTCP packets received。这里按 README 的说法打印的是“header 可读正文”header.packet_type给出类型如 PLI、FIRformat!({packet})是rtc::rtcp包实现的 Display 输出逐行打印。两个行为边界需要注意均来自 README没有拦截器打标时OnRtcpPacket根本不会触发——链在到达应用前就终止了入站 RTCP 路径本例只放行 PLI 和 FIR而且本端是 recvonly所以下游不会向它要关键帧在没有下游发起请求前它什么都不会打印。想看全部入站 RTCP 怎么办README 的答案是加宽拦截器的担保范围而不是删掉它。即在handle_read里对每个Packet::Rtcp无条件add(Attribute::DeliverToApplication)。仓库里的集成测试恰好就写了这样一个对照组DeliverAllRtcp——对matches!(msg.message.packet, Packet::Rtcp(_))的包一律打标rtcp_processing_webrtc2webrtc.rs。反例同样重要如果把拦截器整条删掉“就没有任何东西去给包打标”应用将彻底看不到 RTCP。集成测试双向 RTCP 投递的验证test_rtcp_processing_webrtc2webrtc 是这个示例的双端集成测试值得作为理解该机制的“标准答案”读一遍。它的拓扑是offerer 发送一条 VP8 流answerer 接收。两个端点各自装不同的拦截器offerer发送方用RtcpForwarderInterceptor收窄臂因为它会被请求关键帧answerer接收方用DeliverAllRtcp放行臂它只收不发不会被要关键帧读的是关于所收流的报告。测试里 answerer 每 200 ms 通过track.write_rtcp显式发送一对包PLI Receiver Report。这一设计让断言变得严格Receiver Report 是“一定会到达 offerer 链路、但必须被 forwarder 丢掉”的对照流量。最终断言rtcp_count 2answerer 通过TrackRemoteEvent::OnRtcpPacket看到了关于所收流的 RTCP接收侧local_rtcp_count 1offerer 通过TrackLocal::poll→TrackLocalEvent::OnRtcpPacket看到了关于自己所发流的 RTCP 反馈发送侧leaked 0收窄臂没有让任何一个非 PLI/FIR 的包漏到应用——“drop 半边”是对已确认到达的流量断言的而不是对着“没有事件”断言的。发送侧事件TrackLocalEvent::OnRtcpPacket的文档track_local/mod.rs与接收侧完全对称例如 Receiver Report或 PLI/FIR 关键帧请求。SFU 会把这类反馈向上传递给发布者。仅对拦截器打了Attribute::DeliverToApplication的反馈触发默认链上永远不会触发——这里的沉默意味着没有包被担保而非对端没发。小结入站 RTCP 默认属于拦截器Receiver Report 喂统计、NACK 被应答、TWCC 驱动带宽估计应用默认看不到任何 RTCP让应用看到 RTCP 的唯一手段是拦截器逐包打标Attribute::DeliverToApplication而不是复制转发链是扁平列表副本只会插回自己身后或删掉拦截器删了就没人打标了RtcpForwarderInterceptor的完整姿势过滤 PLI/FIR → 原地收窄复合包 → 打标 →registry.with(Slot::from(14_000), ...)放在默认拦截器之后应用侧通过TrackRemoteEvent::OnRtcpPacket接收流与TrackLocalEvent::OnRtcpPacket发送流两条事件通道接收想全量放行时把拦截器判定加宽为无条件打标即可tests/rtcp_processing_webrtc2webrtc.rs 里的DeliverAllRtcp就是现成参照。延伸阅读examples/README.md 对rtcp-processing的一句话索引、track_remote 与 track_local 的 API 文档、以及 examples/common/mod.rs 中示例共享的运行时辅助。赞分享音视频即时通讯通信【免费下载链接】webrtcAsync-friendly WebRTC implementation in Rust项目地址https://gitcode.com/gh_mirrors/webr/webrtc点击查看免费下载相关推荐Docmost拦截器HTTP请求响应处理Docmost拦截器HTTP请求响应处理 在现代化的Web应用开发中拦截器Interceptor扮演着至关重要的角色。Docmost作为一款开源的协作文后端前端内容协同知识库Electron.NET 请求拦截实战用 WebRequest 拦截与改写 HTTP 请求Electron.NET 请求拦截实战用 WebRequest 拦截与改写 HTTP 请求 导读 WebRequest 是 Electron 会话 Ses桌面应用跨平台Changes邮件通知系统配置实时构建状态推送方案终极指南Changes邮件通知系统配置实时构建状态推送方案终极指南 在持续集成和持续交付的现代软件开发流程中 实时构建状态通知 是确保团队高效协作的关键。Chang后端运维上一篇Flink 托管状态自定义序列化TypeSerializer 与 TypeSerializerSnapshot 完整实践指南下一篇pandoc 测试用例 6709 解析--file-scope 模式下制表符仍须按 --tab-stop 展开创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考