:用 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 編碼作為媒體源通過標(biāo)準(zhǔn) Offer/Answer 非 Trickle ICE 信令流程實時推送到瀏覽器播放。讀完本文你將掌握完整的 ffmpeg 造數(shù)據(jù)命令、示例程序的構(gòu)建與運行方式含 Linux/macOS 與 Windows 兩套操作、全部 CLI 參數(shù)含義以及示例源碼中媒體引擎配置、SDP 交換、媒體 pacing 與退出時序的源碼級實現(xiàn)細(xì)節(jié)。play-from-disk-vpx 是什么play-from-disk-vpx演示了如何把從磁盤讀取的 VP8/VP9 視頻和/或音頻發(fā)送給你的瀏覽器。從源碼 examples/play-from-disk-vpx/play-from-disk-vpx.rs 的整體結(jié)構(gòu)看它的媒體管線是用IVFReader/OggReader逐幀/逐頁解析磁盤文件來自rtc::media::io模塊把解析出的原始幀寫入TrackLocalStaticSample軌道由PeerConnection完成 Offer/Answer 協(xié)商、ICE 連接后軌道內(nèi)容被封裝成 RTP 包經(jīng) UDP 發(fā)出瀏覽器端通過一個帶兩個 text-area 和 Start Session 按鈕的 Web 信令頁接收并播放。與play-from-disk-h26xH.264/H.265 的 MP4 源不同本示例的輸入是IVF 容器output_vp8.ivf/output_vp9.ivf和OGG 容器output.ogg對應(yīng) WebRTC 生態(tài)中瀏覽器端最通用、兼容性最好的 VP8/VP9 Opus 組合。第一步用 ffmpeg 準(zhǔn)備媒體文件原文檔給出的造數(shù)據(jù)命令如下$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三個參數(shù)的取值與源碼實現(xiàn)是嚴(yán)格對應(yīng)的-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 來按真實時間軸逐頁發(fā)送。造文件時若 page 時長與發(fā)送節(jié)奏差異過大會造成播放抖動。原文檔要求生成的output_vp8.ivf/output_vp9.ivf以及output.ogg應(yīng)放在運行命令指定的路徑下例如rtc/examples/test-data/目錄當(dāng)前倉庫中rtc/是Cargo.toml聲明的 path 依賴rtc { version 0.21.1, path rtc }若該目錄內(nèi)沒有現(xiàn)成的 test-data 文件直接用上面的 ffmpeg 命令自行生成即可然后把路徑傳給-v/-a參數(shù)。第二步構(gòu)建示例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。該模塊的核心設(shè)計是示例與具體 async runtime 解耦——同一份源碼可分別跑在runtime-tokio和runtime-smol兩個 Cargo feature 之下common::runtime()通過OnceLock保證進(jìn)程內(nèi)只有一個共享的Arcdyn Runtime實例既驅(qū)動示例的sleep/interval定時器也通過PeerConnectionBuilder::with_runtime()注入給連接。默認(rèn) feature 為default [runtime-tokio, crypto-ring]因此默認(rèn)構(gòu)建即為 tokio ring。第三步打開 Web 信令頁面按原文檔說明應(yīng)打開一個帶有兩個 text-area 和一個 Start Session 按鈕的信令頁面README 中提供了一個 jsfiddle 在線頁面鏈接打開后你會看到兩個 text-area 和 Start Session 按鈕。這個頁面只做一件事在兩個文本框之間轉(zhuǎn)發(fā) SessionDescriptionbase64 編碼充當(dāng)手動信令通道——上框是瀏覽器側(cè)、下框是play-from-disk-vpx側(cè)點擊 Start Session 后瀏覽器會執(zhí)行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用管道把信令頁面上框瀏覽器側(cè)的 SessionDescription 復(fù)制為環(huán)境變量$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開關(guān)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參數(shù)直接指定 offer 文件路徑input_sdp_file非空時走fs::read_to_string分支這是 README 未提及但從 Cli 結(jié)構(gòu)體#L50-L73 可以確認(rèn)的第三種輸入方式對每次都要重定向 stdin的場景更方便。第五步把 answer 貼回瀏覽器開始播放play-from-disk-vpx處理完 offer 后會向 stdout 打印一行 base64 編碼的 answer把它復(fù)制進(jìn)信令頁下框第二 text-area然后點擊Start Session。此時瀏覽器上方應(yīng)開始播放視頻文件播完后play-from-disk-vpx會自行退出。完整 CLI 參數(shù)一覽結(jié)合源碼中的 clap 定義play-from-disk-vpx.rs#L50-L73該示例的全部參數(shù)如下參數(shù)默認(rèn)值說明-d, --debugfalse開啟 env_logger 詳細(xì)日志-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 而非默認(rèn)的 VP8-v與-a均為Option可以只推視頻、只推音頻或兩者都推video and/or audio。程序在啟動時即校驗文件存在性路徑錯誤會直接報錯video file: {} not exist/audio file: {} not exist退出。源碼解析SDP 交換與非 Trickle ICE拿到 offer 后程序執(zhí)行標(biāo)準(zhǔn)答案方流程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實現(xiàn)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觸發(fā)主流程才開始推流——媒體發(fā)送嚴(yán)格發(fā)生在 ICE 連接成功之后。此外PeerConnectionBuilder還做了兩件事值得注意play-from-disk-vpx.rs#L224-L232with_udp_addrs(vec![format!({}:0, signal::get_local_ip())])把 ICE 收發(fā)包的 UDP socket 綁定到本機 IP避免多網(wǎng)卡環(huán)境下 host candidate 混亂with_runtime(runtime.clone())顯式注入示例共享的運行時本庫無 ambient runtime所有連接級 I/O 都走注入的dyn Runtime。ICE 服務(wù)器配置為公共 STUNlet config RTCConfigurationBuilder::new() .with_ice_servers(vec![RTCIceServer { urls: vec![stun:stun.l.google.com:19302.to_string()], ..Default::default() }]) .build();源碼解析編解碼器注冊與媒體引擎MediaEngine決定本端能協(xié)商出哪些編解碼器。示例按 CLI 開關(guān)注冊 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 變?yōu)?VP9、payload type 從 96 變?yōu)?98不傳-a則根本不注冊 Opusanswer 里也就不會有音頻 m-line瀏覽器自然不會等待音軌payload type 只是初始提議真正發(fā)包用的 PT 必須取協(xié)商結(jié)果。negotiated_payload_type()通過sender.get_parameters()讀取協(xié)商后的rtp_parameters.codecs.first().payload_typeplay-from-disk-vpx.rs#L373-L384注釋明確說明write_sample會把它打到每個包上而底層write_rtp要求 PT 與協(xié)商出的 sender codec 匹配——這是自定義靜態(tài)軌道示例中最容易踩的坑。軌道側(cè)視頻/音頻分別創(chuàng)建TrackLocalStaticSample并add_track注冊進(jìn) PeerConnection軌道元數(shù)據(jù)stream id / track id / label與隨機ssrc都寫死為webrtc-rs-stream-id-video一類命名。源碼解析媒體 pacing——為什么用 ticker 而不是 sleep推流函數(shù)stream_videoplay-from-disk-vpx.rs#L386-L434與stream_audioplay-from-disk-vpx.rs#L436-L483的發(fā)送節(jié)奏設(shè)計是本示例最有復(fù)用價值的部分視頻——發(fā)送間隔由 IVF 頭部的 timebase 決定即每幀應(yīng)占據(jù)的時長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差值算出該頁包含的采樣數(shù)48kHz 采樣率據(jù)此得到精確的sample_duration再以 20ms 的intervalticker 逐頁發(fā)送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不會補償解析數(shù)據(jù)花費的時間逐幀累加下來會越來越慢ticker 以固定周期喚醒偏差不會累積繞過 Sleep 本身的延遲抖動問題。注釋同時點出這不是強制要求因為幀自帶時間戳但如果一次性把所有幀灌出去丟包率會高得多——這正是把本地文件當(dāng)作實時信源播放時必須按真實時間軸 pacing 的原因。pacing 依賴的interval來自 examples/common/mod.rs其內(nèi)部是runtime().interval(period)——即由注入的 Runtime 提供的可等待定時器這也是示例能做到 runtime 無關(guān)的關(guān)鍵。退出時序什么時候程序會結(jié)束原文檔說文件播到結(jié)尾時play-from-disk-vpx會退出。對應(yīng)實現(xiàn)是三路futures::select!play-from-disk-vpx.rs#L350-L366futures::select! { _ done_rx.recv().fuse() { println!(received done signal!); // PeerConnection 進(jìn)入 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狀態(tài)由事件處理器轉(zhuǎn)發(fā)done_tx、用戶 Ctrl-Cctrlc::set_handler轉(zhuǎn)發(fā)ctrlc_tx、媒體流全部播完video/audio 各自的 streaming task 結(jié)束后發(fā)送 done 信號注意未啟用的那一路會drop(tx)使對應(yīng) recv 立即完成。命中任一分支即close()退出。因此文件播完自動退出是常態(tài)路徑Ctrl-C 隨時停是交互路徑。與其他示例的配合反向錄制examples/save-to-disk-vpx/save-to-disk-vpx.rs 與本示例結(jié)構(gòu)幾乎鏡像——瀏覽器側(cè)發(fā)起 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 流程同系列進(jìn)階play-from-disk-fecFlexFEC 前向糾錯、play-from-disk-playlist-control播放列表控制、play-from-disk-renegotiation重協(xié)商、bandwidth-estimation-from-disk帶寬估計等示例都是在本文這個文件 → 軌道 → RTP → 瀏覽器骨架上做增量擴(kuò)展建議以本文為起點逐個閱讀。小結(jié)play-from-disk-vpx用一個約 500 行的示例覆蓋了 Rust webrtc 庫靜態(tài)軌道推流的完整閉環(huán)MediaEngine注冊編解碼器VP8/VP9 OpusPT 96/98/120→add_track創(chuàng)建靜態(tài)軌道 → 從 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 推流服務(wù)端。贊分享音視頻即時通訊通信【免費下載鏈接】webrtcAsync-friendly WebRTC implementation in Rust項目地址https://gitcode.com/gh_mirrors/webr/webrtc點擊查看免費下載相關(guān)推薦Pion WebRTC play-from-disk 實戰(zhàn)用 Go 將本地 IVF/OGG 媒體文件推流到瀏覽器Pion WebRTC play from disk 實戰(zhàn)用 Go 將本地 IVF/OGG 媒體文件推流到瀏覽器 play from disk 是 PionWebRTC音視頻即時通訊通信webrtc-rs 重協(xié)商實戰(zhàn)用 play-from-disk-renegotiation 在已建立的連接上動態(tài)增刪視頻軌webrtc rs 重協(xié)商實戰(zhàn)用 play from disk renegotiation 在已建立的連接上動態(tài)增刪視頻軌 本文圍繞 examples/pla音視頻即時通訊通信使用 Pion WebRTC 的 save-to-disk-av1 示例將瀏覽器 AV1 視頻流實時落盤為 IVF 文件使用 Pion WebRTC 的 save to disk av1 示例將瀏覽器 AV1 視頻流實時落盤為 IVF 文件 本指南以倉庫中 save to diWebRTC音視頻即時通訊通信上一篇Cockpit URL 路徑體系詳解應(yīng)用路由、資源緩存、Channel 與直連機器機制下一篇aiohttp 安全威脅模型深度解析基于 STRIDE 剖析 HTTP/1、WebSocket 與 Multipart 的安全邊界創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考