Compare commits
2 Commits
b0ed6548a6
...
9a5b09cd7f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a5b09cd7f | ||
|
|
46367ef6b5 |
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -2505,6 +2505,7 @@ dependencies = [
|
|||||||
"signal-hook",
|
"signal-hook",
|
||||||
"signal-hook-mio",
|
"signal-hook-mio",
|
||||||
"str0m",
|
"str0m",
|
||||||
|
"tempfile",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
|||||||
@@ -28,3 +28,6 @@ crossbeam-channel = "0.5"
|
|||||||
str0m = "0.20"
|
str0m = "0.20"
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
dirs = "6"
|
dirs = "6"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile = "3.27.0"
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ mod tests {
|
|||||||
// 测试辅助函数:构造指定后端参数的 Args 实例
|
// 测试辅助函数:构造指定后端参数的 Args 实例
|
||||||
fn make_args(backend: Option<&str>) -> Args {
|
fn make_args(backend: Option<&str>) -> Args {
|
||||||
Args {
|
Args {
|
||||||
output: "test.mp4".to_string(),
|
output: Some("test.mp4".to_string()),
|
||||||
output_name: None,
|
output_name: None,
|
||||||
fps: 30,
|
fps: 30,
|
||||||
codec: "h264".to_string(),
|
codec: "h264".to_string(),
|
||||||
@@ -189,6 +189,7 @@ mod tests {
|
|||||||
verbose: false,
|
verbose: false,
|
||||||
backend: backend.map(String::from),
|
backend: backend.map(String::from),
|
||||||
port: 0,
|
port: 0,
|
||||||
|
no_persist: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ fn main() -> Result<()> {
|
|||||||
println!(" (Select a screen to share in the portal dialog)");
|
println!(" (Select a screen to share in the portal dialog)");
|
||||||
|
|
||||||
let portal_args = Args {
|
let portal_args = Args {
|
||||||
output: bench_args.output.clone(),
|
output: Some(bench_args.output.clone()),
|
||||||
output_name: None,
|
output_name: None,
|
||||||
fps: 60,
|
fps: 60,
|
||||||
codec: "h264".to_string(),
|
codec: "h264".to_string(),
|
||||||
@@ -113,6 +113,7 @@ fn main() -> Result<()> {
|
|||||||
verbose: false,
|
verbose: false,
|
||||||
backend: Some("portal".to_string()),
|
backend: Some("portal".to_string()),
|
||||||
port: 0,
|
port: 0,
|
||||||
|
no_persist: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let cap = CapPortal::new(&portal_args)?;
|
let cap = CapPortal::new(&portal_args)?;
|
||||||
|
|||||||
@@ -871,7 +871,7 @@ fn main() -> Result<()> {
|
|||||||
println!(" (Select a screen to share in the portal dialog)");
|
println!(" (Select a screen to share in the portal dialog)");
|
||||||
|
|
||||||
let portal_args = Args {
|
let portal_args = Args {
|
||||||
output: bench_args.output.clone(),
|
output: Some(bench_args.output.clone()),
|
||||||
output_name: None,
|
output_name: None,
|
||||||
fps: 60,
|
fps: 60,
|
||||||
codec: "h264".to_string(),
|
codec: "h264".to_string(),
|
||||||
@@ -882,6 +882,7 @@ fn main() -> Result<()> {
|
|||||||
verbose: false,
|
verbose: false,
|
||||||
backend: Some("portal".to_string()),
|
backend: Some("portal".to_string()),
|
||||||
port: 0,
|
port: 0,
|
||||||
|
no_persist: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let cap = CapPortal::new(&portal_args)?;
|
let cap = CapPortal::new(&portal_args)?;
|
||||||
|
|||||||
@@ -246,27 +246,162 @@ impl CapPortal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn token_path() -> PathBuf {
|
fn token_path() -> Option<PathBuf> {
|
||||||
let base = dirs::cache_dir()
|
dirs::cache_dir().map(|base| base.join("wl-webrtc").join("portal-restore-token"))
|
||||||
.unwrap_or_else(|| PathBuf::from("/tmp"));
|
}
|
||||||
base.join("wl-webrtc").join("portal-restore-token")
|
|
||||||
|
/// Verify that `path` is a directory owned by the current user with no group/other permissions.
|
||||||
|
/// Rejects symlinks at the path itself (but allows the resolved target to be a real dir).
|
||||||
|
fn verify_secure_dir(path: &std::path::Path) -> bool {
|
||||||
|
use std::os::unix::fs::{MetadataExt, PermissionsExt};
|
||||||
|
|
||||||
|
match std::fs::symlink_metadata(path) {
|
||||||
|
Ok(meta) => {
|
||||||
|
if meta.file_type().is_symlink() {
|
||||||
|
tracing::warn!("Token parent dir is a symlink, rejecting: {}", path.display());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Must be a directory
|
||||||
|
if !meta.is_dir() {
|
||||||
|
tracing::warn!("Token parent path is not a directory: {}", path.display());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Must be owned by current user
|
||||||
|
if meta.uid() != unsafe { libc::getuid() } {
|
||||||
|
tracing::warn!("Token parent dir not owned by current user: {}", path.display());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// No group or other permissions (mode must be 0o700 exactly within the 0o777 mask)
|
||||||
|
let mode = meta.permissions().mode() & 0o777;
|
||||||
|
if mode != 0o700 {
|
||||||
|
tracing::warn!(
|
||||||
|
"Token parent dir has insecure permissions {:o}, expected 0700: {}",
|
||||||
|
mode,
|
||||||
|
path.display()
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!("Failed to stat token parent dir: {e}");
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensure the parent directory exists with restrictive permissions (0o700).
|
||||||
|
/// Returns false if the directory could not be created or is insecure.
|
||||||
|
fn ensure_secure_parent(parent: &std::path::Path) -> bool {
|
||||||
|
use std::os::unix::fs::{DirBuilderExt, PermissionsExt};
|
||||||
|
|
||||||
|
if parent.exists() {
|
||||||
|
// Directory exists — try to tighten permissions, then verify.
|
||||||
|
// set_permissions follows symlinks, which is fine here since
|
||||||
|
// we verify with symlink_metadata in verify_secure_dir.
|
||||||
|
if let Err(e) = std::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o700)) {
|
||||||
|
tracing::warn!("Failed to set directory permissions: {e}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return verify_secure_dir(parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create with restrictive mode — DirBuilderExt::mode bypasses umask.
|
||||||
|
let mut builder = std::fs::DirBuilder::new();
|
||||||
|
builder.recursive(true);
|
||||||
|
builder.mode(0o700);
|
||||||
|
if let Err(e) = builder.create(parent) {
|
||||||
|
tracing::warn!("Failed to create token directory: {e}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify after creation (belt-and-suspenders)
|
||||||
|
verify_secure_dir(parent)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_restore_token() -> Option<String> {
|
fn load_restore_token() -> Option<String> {
|
||||||
let path = token_path();
|
load_restore_token_from(token_path()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_restore_token_from(path: PathBuf) -> Option<String> {
|
||||||
|
use std::os::unix::fs::{MetadataExt, PermissionsExt};
|
||||||
|
|
||||||
|
let meta = match std::fs::symlink_metadata(&path) {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(_) => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if meta.file_type().is_symlink() {
|
||||||
|
tracing::warn!("Token file is a symlink, refusing to read: {}", path.display());
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if !meta.is_file() {
|
||||||
|
tracing::warn!("Token path is not a regular file: {}", path.display());
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if meta.uid() != unsafe { libc::getuid() } {
|
||||||
|
tracing::warn!("Token file not owned by current user: {}", path.display());
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mode = meta.permissions().mode() & 0o777;
|
||||||
|
if mode & 0o077 != 0 {
|
||||||
|
tracing::warn!(
|
||||||
|
"Token file has insecure permissions {:o}, refusing to read: {}",
|
||||||
|
mode,
|
||||||
|
path.display()
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
let token = std::fs::read_to_string(&path).ok()?;
|
let token = std::fs::read_to_string(&path).ok()?;
|
||||||
let trimmed = token.trim().to_string();
|
let trimmed = token.trim().to_string();
|
||||||
if trimmed.is_empty() { None } else { Some(trimmed) }
|
if trimmed.is_empty() { None } else { Some(trimmed) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn save_restore_token(token: &str) {
|
fn save_restore_token(token: &str) {
|
||||||
let path = token_path();
|
let Some(path) = token_path() else {
|
||||||
if let Some(parent) = path.parent() {
|
tracing::warn!("No secure cache directory available, skipping token save");
|
||||||
let _ = std::fs::create_dir_all(parent);
|
return;
|
||||||
|
};
|
||||||
|
save_restore_token_to(token, &path);
|
||||||
}
|
}
|
||||||
match std::fs::write(&path, token) {
|
|
||||||
|
fn save_restore_token_to(token: &str, path: &std::path::Path) {
|
||||||
|
use std::fs::OpenOptions;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::os::unix::fs::OpenOptionsExt;
|
||||||
|
|
||||||
|
let Some(parent) = path.parent() else {
|
||||||
|
tracing::warn!("Token path has no parent directory");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if !ensure_secure_parent(parent) {
|
||||||
|
tracing::warn!("Parent directory is insecure, refusing to save token");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a unique temp file to prevent symlink attacks.
|
||||||
|
// create_new(true) guarantees exclusive creation — fails if file already exists,
|
||||||
|
// and does NOT follow existing symlinks.
|
||||||
|
let tmp_path = path.with_extension(format!("{}.tmp", std::process::id()));
|
||||||
|
let result = (|| -> std::io::Result<()> {
|
||||||
|
let mut f = OpenOptions::new()
|
||||||
|
.write(true)
|
||||||
|
.create_new(true)
|
||||||
|
.mode(0o600)
|
||||||
|
.open(&tmp_path)?;
|
||||||
|
f.write_all(token.as_bytes())?;
|
||||||
|
f.sync_all()?;
|
||||||
|
std::fs::rename(&tmp_path, path)?;
|
||||||
|
Ok(())
|
||||||
|
})();
|
||||||
|
match result {
|
||||||
Ok(()) => tracing::info!("Saved portal restore token"),
|
Ok(()) => tracing::info!("Saved portal restore token"),
|
||||||
Err(e) => tracing::warn!("Failed to save restore token: {e}"),
|
Err(e) => {
|
||||||
|
let _ = std::fs::remove_file(&tmp_path);
|
||||||
|
tracing::warn!("Failed to save restore token: {e}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -645,6 +780,7 @@ fn spa_to_drm_fourcc(format: libspa::param::video::VideoFormat) -> u32 {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use drm_fourcc::DrmFourcc;
|
use drm_fourcc::DrmFourcc;
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn spa_to_drm_fourcc_all_32bit() {
|
fn spa_to_drm_fourcc_all_32bit() {
|
||||||
@@ -688,4 +824,160 @@ mod tests {
|
|||||||
use libspa::param::video::VideoFormat;
|
use libspa::param::video::VideoFormat;
|
||||||
assert_eq!(spa_to_drm_fourcc(VideoFormat::NV12), 0);
|
assert_eq!(spa_to_drm_fourcc(VideoFormat::NV12), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn token_path_never_uses_tmp() {
|
||||||
|
assert!(token_path().is_some(), "token_path should resolve on Linux");
|
||||||
|
let path = token_path().unwrap();
|
||||||
|
assert!(!path.starts_with("/tmp"), "must not fallback to /tmp");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn verify_secure_dir_rejects_wrong_permissions() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let path = dir.path();
|
||||||
|
|
||||||
|
// 0o700 should pass
|
||||||
|
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700)).unwrap();
|
||||||
|
assert!(verify_secure_dir(path));
|
||||||
|
|
||||||
|
// 0o755 should fail
|
||||||
|
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o755)).unwrap();
|
||||||
|
assert!(!verify_secure_dir(path));
|
||||||
|
|
||||||
|
// 0o777 should fail
|
||||||
|
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o777)).unwrap();
|
||||||
|
assert!(!verify_secure_dir(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn verify_secure_dir_rejects_non_directory() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let file_path = dir.path().join("not-a-dir");
|
||||||
|
std::fs::write(&file_path, b"test").unwrap();
|
||||||
|
assert!(!verify_secure_dir(&file_path));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ensure_secure_parent_creates_with_0700() {
|
||||||
|
let base = tempfile::tempdir().unwrap();
|
||||||
|
let new_dir = base.path().join("wl-test-new-dir");
|
||||||
|
assert!(!new_dir.exists());
|
||||||
|
|
||||||
|
assert!(ensure_secure_parent(&new_dir));
|
||||||
|
assert!(new_dir.is_dir());
|
||||||
|
|
||||||
|
let meta = std::fs::symlink_metadata(&new_dir).unwrap();
|
||||||
|
let mode = meta.permissions().mode() & 0o777;
|
||||||
|
assert_eq!(mode, 0o700, "created directory should be 0700, got {mode:o}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ensure_secure_parent_tightens_existing_dir() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let path = dir.path();
|
||||||
|
|
||||||
|
// Simulate an existing directory with loose permissions
|
||||||
|
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o755)).unwrap();
|
||||||
|
|
||||||
|
assert!(ensure_secure_parent(path));
|
||||||
|
|
||||||
|
let meta = std::fs::symlink_metadata(path).unwrap();
|
||||||
|
let mode = meta.permissions().mode() & 0o777;
|
||||||
|
assert_eq!(mode, 0o700, "tightened directory should be 0700, got {mode:o}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn save_creates_file_with_0600() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let token_path = dir.path().join("portal-restore-token");
|
||||||
|
|
||||||
|
save_restore_token_to("secret-token-123", &token_path);
|
||||||
|
|
||||||
|
assert!(token_path.exists());
|
||||||
|
let meta = std::fs::symlink_metadata(&token_path).unwrap();
|
||||||
|
let mode = meta.permissions().mode() & 0o777;
|
||||||
|
assert_eq!(mode, 0o600, "token file should be 0600, got {mode:o}");
|
||||||
|
assert_eq!(std::fs::read_to_string(&token_path).unwrap(), "secret-token-123");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_reads_secure_file() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let token_path = dir.path().join("portal-restore-token");
|
||||||
|
|
||||||
|
// Write a valid 0o600 token file
|
||||||
|
use std::os::unix::fs::OpenOptionsExt;
|
||||||
|
let mut f = std::fs::OpenOptions::new()
|
||||||
|
.write(true)
|
||||||
|
.create_new(true)
|
||||||
|
.mode(0o600)
|
||||||
|
.open(&token_path)
|
||||||
|
.unwrap();
|
||||||
|
std::io::Write::write_all(&mut f, b"my-secret\n").unwrap();
|
||||||
|
|
||||||
|
let result = load_restore_token_from(token_path);
|
||||||
|
assert_eq!(result, Some("my-secret".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_rejects_group_readable_file() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let token_path = dir.path().join("portal-restore-token");
|
||||||
|
|
||||||
|
// Write with 0o640 (group readable) — should be rejected
|
||||||
|
use std::os::unix::fs::OpenOptionsExt;
|
||||||
|
let mut f = std::fs::OpenOptions::new()
|
||||||
|
.write(true)
|
||||||
|
.create_new(true)
|
||||||
|
.mode(0o640)
|
||||||
|
.open(&token_path)
|
||||||
|
.unwrap();
|
||||||
|
std::io::Write::write_all(&mut f, b"leaked-token\n").unwrap();
|
||||||
|
|
||||||
|
let result = load_restore_token_from(token_path);
|
||||||
|
assert!(result.is_none(), "should reject group-readable token file");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_rejects_world_readable_file() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let token_path = dir.path().join("portal-restore-token");
|
||||||
|
|
||||||
|
use std::os::unix::fs::OpenOptionsExt;
|
||||||
|
let mut f = std::fs::OpenOptions::new()
|
||||||
|
.write(true)
|
||||||
|
.create_new(true)
|
||||||
|
.mode(0o604)
|
||||||
|
.open(&token_path)
|
||||||
|
.unwrap();
|
||||||
|
std::io::Write::write_all(&mut f, b"leaked-token\n").unwrap();
|
||||||
|
|
||||||
|
let result = load_restore_token_from(token_path);
|
||||||
|
assert!(result.is_none(), "should reject world-readable token file");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_rejects_symlink() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let real_path = dir.path().join("real-file");
|
||||||
|
let link_path = dir.path().join("portal-restore-token");
|
||||||
|
|
||||||
|
std::fs::write(&real_path, b"target-content\n").unwrap();
|
||||||
|
std::os::unix::fs::symlink(&real_path, &link_path).unwrap();
|
||||||
|
|
||||||
|
let result = load_restore_token_from(link_path);
|
||||||
|
assert!(result.is_none(), "should reject symlinked token file");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn save_then_load_roundtrip() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let token_path = dir.path().join("portal-restore-token");
|
||||||
|
|
||||||
|
save_restore_token_to("roundtrip-token", &token_path);
|
||||||
|
let loaded = load_restore_token_from(token_path);
|
||||||
|
|
||||||
|
assert_eq!(loaded, Some("roundtrip-token".to_string()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ fn run_wlr_screencopy(args: Args) -> Result<()> {
|
|||||||
let qhandle = queue.handle();
|
let qhandle = queue.handle();
|
||||||
// State 是 wlr-screencopy 后端的核心状态机,
|
// State 是 wlr-screencopy 后端的核心状态机,
|
||||||
// 内部管理输出探测、截屏请求、编码器构建、帧采集等阶段
|
// 内部管理输出探测、截屏请求、编码器构建、帧采集等阶段
|
||||||
let mut state = State::new(gm, args, qhandle);
|
let mut state = State::new(gm, args, qhandle)?;
|
||||||
|
|
||||||
// Extract the Wayland fd and consume any immediately-available events.
|
// Extract the Wayland fd and consume any immediately-available events.
|
||||||
// prepare_read() flushes outgoing requests; read() pulls whatever the
|
// prepare_read() flushes outgoing requests; read() pulls whatever the
|
||||||
@@ -246,6 +246,8 @@ fn run_wlr_screencopy(args: Args) -> Result<()> {
|
|||||||
// - Streaming: 正常采集中,请求下一帧
|
// - Streaming: 正常采集中,请求下一帧
|
||||||
state.queue_alloc_frame();
|
state.queue_alloc_frame();
|
||||||
|
|
||||||
|
state.poll_webrtc()?;
|
||||||
|
|
||||||
// 状态机遇到致命错误时退出
|
// 状态机遇到致命错误时退出
|
||||||
if state.errored {
|
if state.errored {
|
||||||
tracing::error!("Fatal error in state machine, exiting");
|
tracing::error!("Fatal error in state machine, exiting");
|
||||||
|
|||||||
117
src/state.rs
117
src/state.rs
@@ -41,10 +41,11 @@ use ffmpeg_next as ff;
|
|||||||
use ffmpeg_next::ffi;
|
use ffmpeg_next::ffi;
|
||||||
|
|
||||||
use crate::args::Args;
|
use crate::args::Args;
|
||||||
use crate::avhw::{AvHwDevCtx, EncState};
|
use crate::avhw::{AvHwDevCtx, EncState, SwEncState};
|
||||||
use crate::cap_wlr_screencopy::CapWlrScreencopy;
|
use crate::cap_wlr_screencopy::CapWlrScreencopy;
|
||||||
use crate::fps_limit::FpsLimit;
|
use crate::fps_limit::FpsLimit;
|
||||||
use crate::transform::{transpose_if_transform_transposed, Transform};
|
use crate::transform::{transpose_if_transform_transposed, Transform};
|
||||||
|
use crate::webrtc::WebRtcState;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// CaptureSource trait
|
// CaptureSource trait
|
||||||
@@ -113,6 +114,42 @@ struct WlrHeadInfo {
|
|||||||
/// User data for XdgOutput dispatch to identify which WlOutput it belongs to.
|
/// User data for XdgOutput dispatch to identify which WlOutput it belongs to.
|
||||||
pub struct OutputId(pub u32);
|
pub struct OutputId(pub u32);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// StreamingEncoder
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Wraps the two possible encoder backends for the streaming stage.
|
||||||
|
///
|
||||||
|
/// - `Mp4(EncState)` — hardware VAAPI encoder writing to an MP4 file
|
||||||
|
/// - `WebRtc(SwEncState)` — software encoder feeding H.264 NALUs into a WebRTC channel
|
||||||
|
pub enum StreamingEncoder {
|
||||||
|
Mp4(EncState),
|
||||||
|
WebRtc(SwEncState),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StreamingEncoder {
|
||||||
|
fn frames_rgb(&self) -> &crate::avhw::AvHwFrameCtx {
|
||||||
|
match self {
|
||||||
|
StreamingEncoder::Mp4(enc) => enc.frames_rgb(),
|
||||||
|
StreamingEncoder::WebRtc(enc) => enc.frames_rgb(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_frame(&mut self, hw_frame: &ffmpeg_next::frame::Video) -> anyhow::Result<()> {
|
||||||
|
match self {
|
||||||
|
StreamingEncoder::Mp4(enc) => enc.encode_frame(hw_frame),
|
||||||
|
StreamingEncoder::WebRtc(enc) => enc.encode_frame(hw_frame),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn flush(&mut self) -> anyhow::Result<()> {
|
||||||
|
match self {
|
||||||
|
StreamingEncoder::Mp4(enc) => enc.flush(),
|
||||||
|
StreamingEncoder::WebRtc(enc) => enc.flush(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// EncConstructionStage
|
// EncConstructionStage
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -142,7 +179,7 @@ pub enum EncConstructionStage<S: CaptureSource> {
|
|||||||
Streaming {
|
Streaming {
|
||||||
output_info: OutputInfo,
|
output_info: OutputInfo,
|
||||||
output: WlOutput,
|
output: WlOutput,
|
||||||
enc: EncState,
|
enc: StreamingEncoder,
|
||||||
cap: S,
|
cap: S,
|
||||||
screencopy_manager: ZwlrScreencopyManagerV1,
|
screencopy_manager: ZwlrScreencopyManagerV1,
|
||||||
dmabuf: ZwpLinuxDmabufV1,
|
dmabuf: ZwpLinuxDmabufV1,
|
||||||
@@ -182,6 +219,10 @@ pub struct State<S: CaptureSource> {
|
|||||||
pub qhandle: QueueHandle<State<S>>,
|
pub qhandle: QueueHandle<State<S>>,
|
||||||
pub drm_device: Option<PathBuf>,
|
pub drm_device: Option<PathBuf>,
|
||||||
pub drm_device_from_compositor: Option<PathBuf>,
|
pub drm_device_from_compositor: Option<PathBuf>,
|
||||||
|
pub webrtc: Option<WebRtcState>,
|
||||||
|
pub webrtc_tx: Option<crossbeam_channel::Sender<Vec<u8>>>,
|
||||||
|
webrtc_rx: Option<crossbeam_channel::Receiver<Vec<u8>>>,
|
||||||
|
webrtc_frames_sent: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -228,9 +269,18 @@ impl<S: CaptureSource> State<S> {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
impl<S: CaptureSource> State<S> {
|
impl<S: CaptureSource> State<S> {
|
||||||
pub fn new(gm: GlobalList, args: Args, qhandle: QueueHandle<State<S>>) -> Self {
|
pub fn new(gm: GlobalList, args: Args, qhandle: QueueHandle<State<S>>) -> Result<Self> {
|
||||||
let fps = args.fps;
|
let fps = args.fps;
|
||||||
let drm_device = args.drm_device.as_ref().map(PathBuf::from);
|
let drm_device = args.drm_device.as_ref().map(PathBuf::from);
|
||||||
|
|
||||||
|
let (webrtc, webrtc_tx, webrtc_rx) = if args.port > 0 {
|
||||||
|
let (tx, rx) = crossbeam_channel::bounded(32);
|
||||||
|
let wrtc = WebRtcState::new(args.port, args.fps)?;
|
||||||
|
(Some(wrtc), Some(tx), Some(rx))
|
||||||
|
} else {
|
||||||
|
(None, None, None)
|
||||||
|
};
|
||||||
|
|
||||||
let mut state = Self {
|
let mut state = Self {
|
||||||
stage: EncConstructionStage::ProbingOutputs {
|
stage: EncConstructionStage::ProbingOutputs {
|
||||||
outputs: Vec::new(),
|
outputs: Vec::new(),
|
||||||
@@ -255,6 +305,10 @@ impl<S: CaptureSource> State<S> {
|
|||||||
qhandle,
|
qhandle,
|
||||||
drm_device,
|
drm_device,
|
||||||
drm_device_from_compositor: None,
|
drm_device_from_compositor: None,
|
||||||
|
webrtc,
|
||||||
|
webrtc_tx,
|
||||||
|
webrtc_rx,
|
||||||
|
webrtc_frames_sent: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
// registry_queue_init consumes registry events internally during its
|
// registry_queue_init consumes registry events internally during its
|
||||||
@@ -262,7 +316,7 @@ impl<S: CaptureSource> State<S> {
|
|||||||
// We must manually bind the initial globals here.
|
// We must manually bind the initial globals here.
|
||||||
state.bind_initial_globals();
|
state.bind_initial_globals();
|
||||||
|
|
||||||
state
|
Ok(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Iterate over the GlobalList from registry_queue_init and bind all
|
/// Iterate over the GlobalList from registry_queue_init and bind all
|
||||||
@@ -581,6 +635,29 @@ impl<S: CaptureSource> State<S> {
|
|||||||
self.errored = true;
|
self.errored = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn poll_webrtc(&mut self) -> Result<()> {
|
||||||
|
let Some(ref mut wrtc) = self.webrtc else { return Ok(()) };
|
||||||
|
|
||||||
|
wrtc.handle_signaling()?;
|
||||||
|
wrtc.poll_and_feed()?;
|
||||||
|
|
||||||
|
if let Some(ref rx) = self.webrtc_rx {
|
||||||
|
let mut count = 0u32;
|
||||||
|
while let Ok(data) = rx.try_recv() {
|
||||||
|
count += 1;
|
||||||
|
if let Err(e) = wrtc.write_h264_frame(&data, self.webrtc_frames_sent, self.args.fps) {
|
||||||
|
tracing::debug!("WebRTC write frame error: {e}");
|
||||||
|
}
|
||||||
|
self.webrtc_frames_sent = self.webrtc_frames_sent.saturating_add(1);
|
||||||
|
}
|
||||||
|
if count > 0 {
|
||||||
|
tracing::info!("WebRTC forwarded {count} frames from channel");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn negotiate_format(&mut self, format: u32, width: u32, height: u32) {
|
pub fn negotiate_format(&mut self, format: u32, width: u32, height: u32) {
|
||||||
let stage_data = match mem::replace(&mut self.stage, EncConstructionStage::Intermediate) {
|
let stage_data = match mem::replace(&mut self.stage, EncConstructionStage::Intermediate) {
|
||||||
EncConstructionStage::EverythingButFmt {
|
EncConstructionStage::EverythingButFmt {
|
||||||
@@ -611,9 +688,34 @@ impl<S: CaptureSource> State<S> {
|
|||||||
.args
|
.args
|
||||||
.bitrate
|
.bitrate
|
||||||
.unwrap_or_else(|| 2 * (width as u64) * (height as u64) * (fps as u64) / 100);
|
.unwrap_or_else(|| 2 * (width as u64) * (height as u64) * (fps as u64) / 100);
|
||||||
let enc = match crate::avhw::create_encoder(
|
|
||||||
|
let enc = if let Some(ref tx) = self.webrtc_tx {
|
||||||
|
let (enc_w, enc_h) =
|
||||||
|
transpose_if_transform_transposed(output_info.transform, width as i32, height as i32);
|
||||||
|
let actual_gop_size = self.args.gop_size.unwrap_or((fps / 2).max(10));
|
||||||
|
match SwEncState::new_webrtc(
|
||||||
&drm_path,
|
&drm_path,
|
||||||
Path::new(self.args.output.as_deref().expect("output required for MP4 mode")),
|
width,
|
||||||
|
height,
|
||||||
|
enc_w as u32,
|
||||||
|
enc_h as u32,
|
||||||
|
fps,
|
||||||
|
bitrate,
|
||||||
|
actual_gop_size,
|
||||||
|
tx.clone(),
|
||||||
|
) {
|
||||||
|
Ok(enc) => StreamingEncoder::WebRtc(enc),
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("SwEncState::new_webrtc failed: {}", e);
|
||||||
|
self.errored = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let output_path = self.args.output.as_deref().expect("output required for MP4 mode");
|
||||||
|
match crate::avhw::create_encoder(
|
||||||
|
&drm_path,
|
||||||
|
Path::new(output_path),
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
fps,
|
fps,
|
||||||
@@ -622,12 +724,13 @@ impl<S: CaptureSource> State<S> {
|
|||||||
self.args.gop_size,
|
self.args.gop_size,
|
||||||
Some(hw_device_ctx),
|
Some(hw_device_ctx),
|
||||||
) {
|
) {
|
||||||
Ok(enc) => enc,
|
Ok(enc) => StreamingEncoder::Mp4(enc),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("EncState::new failed: {}", e);
|
tracing::error!("EncState::new failed: {}", e);
|
||||||
self.errored = true;
|
self.errored = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"Encoder initialized: {}x{} format={} bitrate={}",
|
"Encoder initialized: {}x{} format={} bitrate={}",
|
||||||
|
|||||||
@@ -424,7 +424,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn resolve_drm_device_explicit() {
|
fn resolve_drm_device_explicit() {
|
||||||
let args = Args {
|
let args = Args {
|
||||||
output: "test.mp4".to_string(),
|
output: Some("test.mp4".to_string()),
|
||||||
output_name: None,
|
output_name: None,
|
||||||
fps: 30,
|
fps: 30,
|
||||||
codec: "h264".to_string(),
|
codec: "h264".to_string(),
|
||||||
@@ -435,6 +435,7 @@ mod tests {
|
|||||||
verbose: false,
|
verbose: false,
|
||||||
backend: None,
|
backend: None,
|
||||||
port: 0,
|
port: 0,
|
||||||
|
no_persist: false,
|
||||||
};
|
};
|
||||||
let result = resolve_drm_device(&args).unwrap();
|
let result = resolve_drm_device(&args).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -446,7 +447,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn resolve_drm_device_none_when_not_specified() {
|
fn resolve_drm_device_none_when_not_specified() {
|
||||||
let args = Args {
|
let args = Args {
|
||||||
output: "test.mp4".to_string(),
|
output: Some("test.mp4".to_string()),
|
||||||
output_name: None,
|
output_name: None,
|
||||||
fps: 30,
|
fps: 30,
|
||||||
codec: "h264".to_string(),
|
codec: "h264".to_string(),
|
||||||
@@ -457,6 +458,7 @@ mod tests {
|
|||||||
verbose: false,
|
verbose: false,
|
||||||
backend: None,
|
backend: None,
|
||||||
port: 0,
|
port: 0,
|
||||||
|
no_persist: false,
|
||||||
};
|
};
|
||||||
let result = resolve_drm_device(&args).unwrap();
|
let result = resolve_drm_device(&args).unwrap();
|
||||||
assert_eq!(result, None);
|
assert_eq!(result, None);
|
||||||
|
|||||||
Reference in New Issue
Block a user