feat: add WebRTC streaming via str0m + portal session persistence
- Add src/webrtc.rs: HTTP signaling server + str0m Sans-IO WebRTC transport with H.264 Annex-B → RTP packetization and key-frame request handling - avhw: introduce FrameOutput enum (Muxer | Channel) so SwEncState can output to either MP4 muxer or crossbeam channel for WebRTC - cap_portal: support portal session restore tokens (PersistMode::ExplicitlyRevoked) to skip re-authorization dialog; add --no-persist flag to force fresh dialog - args: make --output optional when --port is used for WebRTC mode - state_portal: integrate WebRTC pipeline (encoder channel → RTP forwarding) with shorter GOP for WebRTC (fps/2, min 10) - main: redirect tracing to stderr; validate --output or --port required - Add dependencies: str0m 0.20, serde_json 1, dirs 6
This commit is contained in:
+58
-28
@@ -12,6 +12,7 @@
|
||||
// - crossbeam-channel: 高性能有界通道,用于线程间帧传递
|
||||
|
||||
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::thread::{self, JoinHandle};
|
||||
@@ -98,12 +99,10 @@ impl CapPortal {
|
||||
/// 4. 创建 eventfd 对,用于线程安全的关闭信号传递
|
||||
/// 5. 启动 PipeWire 捕获线程
|
||||
pub fn new(args: &Args) -> Result<Self> {
|
||||
// 创建独立的 Tokio 运行时,仅用于 setup_portal 中的异步 Portal D-Bus 调用
|
||||
let rt = Runtime::new()?;
|
||||
|
||||
// 通过 Portal 获取 PipeWire 连接 fd 和节点 ID
|
||||
// block_on 在此处同步等待异步 Portal 调用完成
|
||||
let (pw_fd, node_id) = rt.block_on(async { Self::setup_portal().await })?;
|
||||
let no_persist = args.no_persist;
|
||||
let (pw_fd, node_id) = rt.block_on(async { Self::setup_portal(no_persist).await })?;
|
||||
|
||||
let (frame_tx, frame_rx) = bounded(16);
|
||||
let (event_tx, event_rx) = bounded(8);
|
||||
@@ -172,44 +171,50 @@ impl CapPortal {
|
||||
/// 5. 打开 PipeWire 远程连接,获取文件描述符
|
||||
///
|
||||
/// 返回 (PipeWire fd, node_id),供 PipeWire 线程连接使用
|
||||
async fn setup_portal() -> Result<(OwnedFd, u32)> {
|
||||
async fn setup_portal(no_persist: bool) -> Result<(OwnedFd, u32)> {
|
||||
use ashpd::desktop::screencast::{
|
||||
CursorMode, Screencast, SelectSourcesOptions, SourceType,
|
||||
};
|
||||
use ashpd::desktop::PersistMode;
|
||||
|
||||
// 创建 Screencast D-Bus 代理,与桌面环境的 Portal 服务通信
|
||||
let proxy = Screencast::new()
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to create Screencast proxy: {e}"))?;
|
||||
|
||||
// 创建 ScreenCast 会话(每个会话对应一次屏幕录制请求)
|
||||
let session = proxy
|
||||
.create_session(Default::default())
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to create ScreenCast session: {e}"))?;
|
||||
|
||||
// 配置录制源选择参数:
|
||||
// - CursorMode::Embedded: 光标嵌入到帧数据中(而非单独的元数据)
|
||||
// - SourceType::Monitor: 仅捕获显示器(不捕获窗口)
|
||||
// - multiple: false: 不允许多源选择
|
||||
// - PersistMode::DoNot: 不持久化会话(每次需要重新授权)
|
||||
let version_supported = proxy.version() >= 4;
|
||||
|
||||
let (persist_mode, saved_token) = if !no_persist && version_supported {
|
||||
let token = load_restore_token();
|
||||
if token.is_some() {
|
||||
tracing::info!("Attempting to restore portal session with saved token");
|
||||
}
|
||||
(PersistMode::ExplicitlyRevoked, token)
|
||||
} else {
|
||||
(PersistMode::DoNot, None)
|
||||
};
|
||||
|
||||
let mut options = SelectSourcesOptions::default()
|
||||
.set_cursor_mode(CursorMode::Embedded)
|
||||
.set_sources(ashpd::enumflags2::BitFlags::from(SourceType::Monitor))
|
||||
.set_multiple(false)
|
||||
.set_persist_mode(persist_mode);
|
||||
|
||||
if let Some(ref token) = saved_token {
|
||||
options = options.set_restore_token(token.as_str());
|
||||
}
|
||||
|
||||
proxy
|
||||
.select_sources(
|
||||
&session,
|
||||
SelectSourcesOptions::default()
|
||||
.set_cursor_mode(CursorMode::Embedded)
|
||||
.set_sources(ashpd::enumflags2::BitFlags::from(SourceType::Monitor))
|
||||
.set_multiple(false)
|
||||
.set_persist_mode(PersistMode::DoNot),
|
||||
)
|
||||
.select_sources(&session, options)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
anyhow::anyhow!("屏幕共享权限被拒绝 / Screen sharing permission denied: {e}")
|
||||
anyhow::anyhow!("Screen sharing permission denied: {e}")
|
||||
})?;
|
||||
|
||||
// 启动录制会话,此时桌面环境会弹出权限确认对话框
|
||||
// 用户确认后返回包含 PipeWire 流信息的响应
|
||||
let response = proxy
|
||||
.start(&session, None, Default::default())
|
||||
.await
|
||||
@@ -217,18 +222,19 @@ impl CapPortal {
|
||||
.response()
|
||||
.map_err(|e| anyhow::anyhow!("ScreenCast response error: {e}"))?;
|
||||
|
||||
// 获取返回的第一个(也是唯一的)视频流
|
||||
// 每个流对应一个 PipeWire 节点
|
||||
if !no_persist && version_supported {
|
||||
if let Some(new_token) = response.restore_token() {
|
||||
save_restore_token(new_token);
|
||||
}
|
||||
}
|
||||
|
||||
let stream = response
|
||||
.streams()
|
||||
.first()
|
||||
.ok_or_else(|| anyhow::anyhow!("No streams returned from ScreenCast"))?;
|
||||
|
||||
// 提取 PipeWire 节点 ID,用于后续连接到该节点的视频流
|
||||
let node_id = stream.pipe_wire_node_id();
|
||||
|
||||
// 打开 PipeWire 远程连接,获取文件描述符
|
||||
// 这个 fd 允许直接与 PipeWire 守护进程通信
|
||||
let fd = proxy
|
||||
.open_pipe_wire_remote(&session, Default::default())
|
||||
.await
|
||||
@@ -240,6 +246,30 @@ impl CapPortal {
|
||||
}
|
||||
}
|
||||
|
||||
fn token_path() -> PathBuf {
|
||||
let base = dirs::cache_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("/tmp"));
|
||||
base.join("wl-webrtc").join("portal-restore-token")
|
||||
}
|
||||
|
||||
fn load_restore_token() -> Option<String> {
|
||||
let path = token_path();
|
||||
let token = std::fs::read_to_string(&path).ok()?;
|
||||
let trimmed = token.trim().to_string();
|
||||
if trimmed.is_empty() { None } else { Some(trimmed) }
|
||||
}
|
||||
|
||||
fn save_restore_token(token: &str) {
|
||||
let path = token_path();
|
||||
if let Some(parent) = path.parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
}
|
||||
match std::fs::write(&path, token) {
|
||||
Ok(()) => tracing::info!("Saved portal restore token"),
|
||||
Err(e) => tracing::warn!("Failed to save restore token: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for CapPortal {
|
||||
/// 析构时安全关闭 PipeWire 线程
|
||||
///
|
||||
|
||||
Reference in New Issue
Block a user