Rust webrtc 实战:用 play-from-disk-vpx 将磁盘上的 VP8/VP9 视频与 Opus 音频推送到浏览器
音视频即时通讯通信【免费下载链接】webrtcAsync-friendly WebRTC implementation in Rust项目地址https://gitcode.com/gh_mirrors/webr/webrtc点击查看免费下载本文以 examples/play-from-disk-vpx/README.md 为主体讲解 how to 用 Rust 的 webrtc 库Async-friendly WebRTC implementation in Rust把保存在磁盘上的 IVF 视频文件VP8/VP9 编码和 OGG 音频文件Opus 编码作为媒体源通过标准 Offer/Answer 非 Trickle ICE 信令流程实时推送到浏览器播放。读完本文你将掌握完整的 ffmpeg 造数据命令、示例程序的构建与运行方式含 Linux/macOS 与 Windows 两套操作、全部 CLI 参数含义以及示例源码中媒体引擎配置、SDP 交换、媒体 pacing 与退出时序的源码级实现细节。play-from-disk-vpx 是什么play-from-disk-vpx演示了如何把从磁盘读取的 VP8/VP9 视频和/或音频发送给你的浏览器。从源码 examples/play-from-disk-vpx/play-from-disk-vpx.rs 的整体结构看它的媒体管线是用IVFReader/OggReader逐帧/逐页解析磁盘文件来自rtc::media::io模块把解析出的原始帧写入TrackLocalStaticSample轨道由PeerConnection完成 Offer/Answer 协商、ICE 连接后轨道内容被封装成 RTP 包经 UDP 发出浏览器端通过一个带两个 text-area 和 Start Session 按钮的 Web 信令页接收并播放。与play-from-disk-h26xH.264/H.265 的 MP4 源不同本示例的输入是IVF 容器output_vp8.ivf/output_vp9.ivf和OGG 容器output.ogg对应 WebRTC 生态中浏览器端最通用、兼容性最好的 VP8/VP9 Opus 组合。第一步用 ffmpeg 准备媒体文件原文档给出的造数据命令如下$INPUT_FILE为你任意一个输入媒体文件# VP8 视频IVF 容器GOP 间隔 30 帧 ffmpeg -i $INPUT_FILE -g 30 output_vp8.ivf # VP9 视频IVF 容器显式指定 libvpx-vp9 编码器 ffmpeg -i $INPUT_FILE -g 30 -c libvpx-vp9 output_vp9.ivf # Opus 音频只取音轨-map 0:a双声道-ac 2libopus 编码 # page_duration 20000微秒即 20ms 一个 Ogg page-vn 丢弃视频 ffmpeg -i $INPUT_FILE -map 0:a -c:a dca -ac 2 -c:a libopus -page_duration 20000 -vn output.ogg三个参数的取值与源码实现是严格对应的-g 30GOP 间隔视频是 I 帧周期为 30 帧的序列保证丢包场景下浏览器有足够多的同步点IVF 容器stream_video使用IVFReader::new(reader)?解析它会先读 IVF header 拿到timebase_numerator/timebase_denominator后面 pacing 就依赖这两个值见下文-page_duration 20000源码中定义了常量const OGG_PAGE_DURATION: Duration Duration::from_millis(20)play-from-disk-vpx.rs#L46stream_audio用一个 20ms 周期的 ticker 来按真实时间轴逐页发送。造文件时若 page 时长与发送节奏差异过大会造成播放抖动。原文档要求生成的output_vp8.ivf/output_vp9.ivf以及output.ogg应放在运行命令指定的路径下例如rtc/examples/test-data/目录当前仓库中rtc/是Cargo.toml声明的 path 依赖rtc { version 0.21.1, path rtc }若该目录内没有现成的 test-data 文件直接用上面的 ffmpeg 命令自行生成即可然后把路径传给-v/-a参数。第二步构建示例cargo build --example play-from-disk-vpx该 example 已在 Cargo.toml 中注册[[example]] name play-from-disk-vpx path examples/play-from-disk-vpx/play-from-disk-vpx.rs bench false注意示例代码通过#[path ../common/mod.rs] mod common;引入共享的运行时辅助模块examples/common/mod.rs。该模块的核心设计是示例与具体 async runtime 解耦——同一份源码可分别跑在runtime-tokio和runtime-smol两个 Cargo feature 之下common::runtime()通过OnceLock保证进程内只有一个共享的Arcdyn Runtime实例既驱动示例的sleep/interval定时器也通过PeerConnectionBuilder::with_runtime()注入给连接。默认 feature 为default [runtime-tokio, crypto-ring]因此默认构建即为 tokio ring。第三步打开 Web 信令页面按原文档说明应打开一个带有两个 text-area 和一个 Start Session 按钮的信令页面README 中提供了一个 jsfiddle 在线页面链接打开后你会看到两个 text-area 和 Start Session 按钮。这个页面只做一件事在两个文本框之间转发 SessionDescriptionbase64 编码充当手动信令通道——上框是浏览器侧、下框是play-from-disk-vpx侧点击 Start Session 后浏览器会执行setRemoteDescription并把 offer 填回上框。第四步运行 play-from-disk-vpx把浏览器的 SDP 作为 stdin 输入程序启动后会打印Paste offer from browser and press Enter:并从stdin读取一行 SessionDescription。源码中该逻辑是play-from-disk-vpx.rs#L292-L302// Wait for the offer to be pasted print!(Paste offer from browser and press Enter: ); let line if cli.input_sdp_file.is_empty() { signal::must_read_stdin()? // 从 stdin 读取一行 } else { fs::read_to_string(cli.input_sdp_file)? // 或从 -i 指定的文件读取 }; let desc_data signal::decode(line.as_str())?; // base64 解码也兼容裸 SDP let offer serde_json::from_str::RTCSessionDescription(desc_data)?;由于 stdin 是一次性通道原文档给出了三种喂入方式Linux/macOS用管道把信令页面上框浏览器侧的 SessionDescription 复制为环境变量$BROWSER_SDP然后运行echo $BROWSER_SDP | ./target/debug/examples/play-from-disk-vpx -v rtc/examples/test-data/output_vp8.ivf -a rtc/examples/test-data/output.oggVP9 版本加--vp9开关echo $BROWSER_SDP | ./target/debug/examples/play-from-disk-vpx -v rtc/examples/test-data/output_vp9.ivf -a rtc/examples/test-data/output.ogg --vp9Windows重定向文件把 SessionDescription 粘贴保存到一个文件如my_file运行./target/debug/examples/play-from-disk-vpx -v rtc/examples/test-data/output_vp8.ivf -a rtc/examples/test-data/output.ogg my_fileVP9 版本./target/debug/examples/play-from-disk-vpx -v rtc/examples/test-data/output_vp9.ivf -a rtc/examples/test-data/output.ogg --vp9 my_file补充源码还支持-i/--input-sdp-file参数直接指定 offer 文件路径input_sdp_file非空时走fs::read_to_string分支这是 README 未提及但从 Cli 结构体#L50-L73 可以确认的第三种输入方式对每次都要重定向 stdin的场景更方便。第五步把 answer 贴回浏览器开始播放play-from-disk-vpx处理完 offer 后会向 stdout 打印一行 base64 编码的 answer把它复制进信令页下框第二 text-area然后点击Start Session。此时浏览器上方应开始播放视频文件播完后play-from-disk-vpx会自行退出。完整 CLI 参数一览结合源码中的 clap 定义play-from-disk-vpx.rs#L50-L73该示例的全部参数如下参数默认值说明-d, --debugfalse开启 env_logger 详细日志-l, --log-levelINFO日志级别ERROR/WARN/INFO/DEBUG/TRACE-i, --input-sdp-file空从文件读取 offer为空则从 stdin 读取-o, --output-log-file空配合-d把日志写入文件而非 stdout-v, --video PATH无要推流的视频文件含 VP8 或 VP9 的.ivf-a, --audio PATH无要推流的音频文件.ogg/ Opus--vp9false视频使用 VP9 而非默认的 VP8-v与-a均为Option可以只推视频、只推音频或两者都推video and/or audio。程序在启动时即校验文件存在性路径错误会直接报错video file: {} not exist/audio file: {} not exist退出。源码解析SDP 交换与非 Trickle ICE拿到 offer 后程序执行标准答案方流程play-from-disk-vpx.rs#L304-L317peer_connection.set_remote_description(offer).await?; let answer peer_connection.create_answer(None).await?; peer_connection.set_local_description(answer).await?; // Block until ICE Gathering is complete (non-trickle ICE) let _ gather_complete_rx.recv().await;这里刻意采用非 Trickle ICE注册的事件处理器Handler实现PeerConnectionEventHandler在on_ice_gathering_state_change收到RTCIceGatheringState::Complete时通过 channel 通知主流程。主流程阻塞等待该信号确保local_description()返回的 answer 中已包含全部 ICE candidate再 base64 编码打印if let Some(local_desc) peer_connection.local_description().await { let json_str serde_json::to_string(local_desc)?; let b64 signal::encode(json_str); println!({b64}); }这样浏览器只需setRemoteDescription一次即可拿到完整候选简化了手工信令。连接真正建立RTCPeerConnectionState::Connected后connected_tx触发主流程才开始推流——媒体发送严格发生在 ICE 连接成功之后。此外PeerConnectionBuilder还做了两件事值得注意play-from-disk-vpx.rs#L224-L232with_udp_addrs(vec![format!({}:0, signal::get_local_ip())])把 ICE 收发包的 UDP socket 绑定到本机 IP避免多网卡环境下 host candidate 混乱with_runtime(runtime.clone())显式注入示例共享的运行时本库无 ambient runtime所有连接级 I/O 都走注入的dyn Runtime。ICE 服务器配置为公共 STUNlet config RTCConfigurationBuilder::new() .with_ice_servers(vec![RTCIceServer { urls: vec![stun:stun.l.google.com:19302.to_string()], ..Default::default() }]) .build();源码解析编解码器注册与媒体引擎MediaEngine决定本端能协商出哪些编解码器。示例按 CLI 开关注册 VP8/VP9 与 Opusplay-from-disk-vpx.rs#L160-L198let audio_codec RTCRtpCodecParameters { rtp_codec: RTCRtpCodec { mime_type: MIME_TYPE_OPUS.to_owned(), clock_rate: 48000, // Opus 固定 48kHz channels: 2, .. }, payload_type: 120, .. }; let video_codec RTCRtpCodecParameters { rtp_codec: RTCRtpCodec { mime_type: if is_vp9 { MIME_TYPE_VP9.to_owned() } else { MIME_TYPE_VP8.to_owned() }, clock_rate: 90000, // 视频固定 90kHz channels: 0, .. }, payload_type: if is_vp9 { 98 } else { 96 }, .. }; if audio_file.is_some() { media_engine.register_codec(audio_codec.clone(), RtpCodecKind::Audio)?; } if video_file.is_some() { media_engine.register_codec(video_codec.clone(), RtpCodecKind::Video)?; } let registry register_default_interceptors(Registry::new(), mut media_engine)?;要点只注册本次要推的编解码器--vp9时 mime 变为 VP9、payload type 从 96 变为 98不传-a则根本不注册 Opusanswer 里也就不会有音频 m-line浏览器自然不会等待音轨payload type 只是初始提议真正发包用的 PT 必须取协商结果。negotiated_payload_type()通过sender.get_parameters()读取协商后的rtp_parameters.codecs.first().payload_typeplay-from-disk-vpx.rs#L373-L384注释明确说明write_sample会把它打到每个包上而底层write_rtp要求 PT 与协商出的 sender codec 匹配——这是自定义静态轨道示例中最容易踩的坑。轨道侧视频/音频分别创建TrackLocalStaticSample并add_track注册进 PeerConnection轨道元数据stream id / track id / label与随机ssrc都写死为webrtc-rs-stream-id-video一类命名。源码解析媒体 pacing——为什么用 ticker 而不是 sleep推流函数stream_videoplay-from-disk-vpx.rs#L386-L434与stream_audioplay-from-disk-vpx.rs#L436-L483的发送节奏设计是本示例最有复用价值的部分视频——发送间隔由 IVF 头部的 timebase 决定即每帧应占据的时长let sleep_time Duration::from_millis( ((1000 * header.timebase_numerator) / header.timebase_denominator) as u64, ); let mut ticker interval(sleep_time); loop { let frame match ivf.parse_next_frame() { Ok((frame, _)) frame, Err(err) { println!(All video frames parsed and sent: {err}); break; } }; video_track .sample_writer(ssrc, payload_type) .write_sample(Sample { data: frame.freeze(), duration: Duration::from_secs(1), ..Sample::new(Instant::now()) }) .await?; let _ ticker.tick().await; }音频——每读出一个 Ogg page用相邻两个 page 的granule_position差值算出该页包含的采样数48kHz 采样率据此得到精确的sample_duration再以 20ms 的intervalticker 逐页发送let sample_count page_header.granule_position - last_granule; last_granule page_header.granule_position; let sample_duration Duration::from_millis(sample_count * 1000 / 48000);源码注释解释了必须使用ticker 而非 sleep的两个原因避免累积偏差——time.Sleep不会补偿解析数据花费的时间逐帧累加下来会越来越慢ticker 以固定周期唤醒偏差不会累积绕过 Sleep 本身的延迟抖动问题。注释同时点出这不是强制要求因为帧自带时间戳但如果一次性把所有帧灌出去丢包率会高得多——这正是把本地文件当作实时信源播放时必须按真实时间轴 pacing 的原因。pacing 依赖的interval来自 examples/common/mod.rs其内部是runtime().interval(period)——即由注入的 Runtime 提供的可等待定时器这也是示例能做到 runtime 无关的关键。退出时序什么时候程序会结束原文档说文件播到结尾时play-from-disk-vpx会退出。对应实现是三路futures::select!play-from-disk-vpx.rs#L350-L366futures::select! { _ done_rx.recv().fuse() { println!(received done signal!); // PeerConnection 进入 Failed } _ ctrlc_rx.recv().fuse() { println!(received ctrl-c signal!); // 用户 Ctrl-C } _ async { let _ video_done_rx.recv().await; let _ audio_done_rx.recv().await; }.fuse() { println!(All media streaming completed.); // 视频、音频都播完 } } peer_connection.close().await?;三个分支连接失败Failed状态由事件处理器转发done_tx、用户 Ctrl-Cctrlc::set_handler转发ctrlc_tx、媒体流全部播完video/audio 各自的 streaming task 结束后发送 done 信号注意未启用的那一路会drop(tx)使对应 recv 立即完成。命中任一分支即close()退出。因此文件播完自动退出是常态路径Ctrl-C 随时停是交互路径。与其他示例的配合反向录制examples/save-to-disk-vpx/save-to-disk-vpx.rs 与本示例结构几乎镜像——浏览器侧发起 offer 后它以Recvonlytransceiver 接收 VP8/VP9 Opus 并写回 IVF/OGG 文件适合先save再play的完整往返验证H.264/H.265 变体examples/play-from-disk-h26x/play-from-disk-h26x.rs 演示 MP4 源的同一套 Offer/Answer 非 Trickle ICE 流程同系列进阶play-from-disk-fecFlexFEC 前向纠错、play-from-disk-playlist-control播放列表控制、play-from-disk-renegotiation重协商、bandwidth-estimation-from-disk带宽估计等示例都是在本文这个文件 → 轨道 → RTP → 浏览器骨架上做增量扩展建议以本文为起点逐个阅读。小结play-from-disk-vpx用一个约 500 行的示例覆盖了 Rust webrtc 库静态轨道推流的完整闭环MediaEngine注册编解码器VP8/VP9 OpusPT 96/98/120→add_track创建静态轨道 → 从 stdin 或文件读取 base64 offer →set_remote_description/create_answer/set_local_description→ 等待 ICE Gathering 完成非 Trickle→ 打印 base64 answer → 连接建立后按 IVF timebase / Ogg granule 用 ticker pacing 逐帧推流 → 播完或 Ctrl-C 后close退出。掌握这套流程后把示例中的IVFReader/OggReader换成任意实时采集源即可得到自己的 WebRTC 推流服务端。赞分享音视频即时通讯通信【免费下载链接】webrtcAsync-friendly WebRTC implementation in Rust项目地址https://gitcode.com/gh_mirrors/webr/webrtc点击查看免费下载相关推荐Pion WebRTC play-from-disk 实战用 Go 将本地 IVF/OGG 媒体文件推流到浏览器Pion WebRTC play from disk 实战用 Go 将本地 IVF/OGG 媒体文件推流到浏览器 play from disk 是 PionWebRTC音视频即时通讯通信webrtc-rs 重协商实战用 play-from-disk-renegotiation 在已建立的连接上动态增删视频轨webrtc rs 重协商实战用 play from disk renegotiation 在已建立的连接上动态增删视频轨 本文围绕 examples/pla音视频即时通讯通信使用 Pion WebRTC 的 save-to-disk-av1 示例将浏览器 AV1 视频流实时落盘为 IVF 文件使用 Pion WebRTC 的 save to disk av1 示例将浏览器 AV1 视频流实时落盘为 IVF 文件 本指南以仓库中 save to diWebRTC音视频即时通讯通信上一篇Cockpit URL 路径体系详解应用路由、资源缓存、Channel 与直连机器机制下一篇aiohttp 安全威胁模型深度解析基于 STRIDE 剖析 HTTP/1、WebSocket 与 Multipart 的安全边界创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考