phase 2
This commit is contained in:
179
media/Cargo.lock
generated
179
media/Cargo.lock
generated
@@ -17,6 +17,24 @@ version = "1.0.102"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bindgen"
|
||||||
|
version = "0.72.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"cexpr",
|
||||||
|
"clang-sys",
|
||||||
|
"itertools",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"regex",
|
||||||
|
"rustc-hash",
|
||||||
|
"shlex",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.11.0"
|
version = "2.11.0"
|
||||||
@@ -29,6 +47,25 @@ version = "1.11.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cc"
|
||||||
|
version = "1.2.59"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b7a4d3ec6524d28a329fc53654bbadc9bdd7b0431f5d65f1a56ffb28a1ee5283"
|
||||||
|
dependencies = [
|
||||||
|
"find-msvc-tools",
|
||||||
|
"shlex",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cexpr"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
|
||||||
|
dependencies = [
|
||||||
|
"nom",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
@@ -41,6 +78,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"cht-common",
|
"cht-common",
|
||||||
|
"ffmpeg-next",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
@@ -69,6 +107,23 @@ dependencies = [
|
|||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clang-sys"
|
||||||
|
version = "1.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
|
||||||
|
dependencies = [
|
||||||
|
"glob",
|
||||||
|
"libc",
|
||||||
|
"libloading",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "either"
|
||||||
|
version = "1.15.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "errno"
|
name = "errno"
|
||||||
version = "0.3.14"
|
version = "0.3.14"
|
||||||
@@ -79,6 +134,58 @@ dependencies = [
|
|||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ffmpeg-next"
|
||||||
|
version = "8.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f7c4bd5ab1ac61f29c634df1175d350ded29cf74c3c6d4f7030431a5ae3c7d5d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"ffmpeg-sys-next",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ffmpeg-sys-next"
|
||||||
|
version = "8.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a314bc0e022a33a99567ed4bd2576bd58ffd8fcff7891c29194cfecc26a62547"
|
||||||
|
dependencies = [
|
||||||
|
"bindgen",
|
||||||
|
"cc",
|
||||||
|
"libc",
|
||||||
|
"num_cpus",
|
||||||
|
"pkg-config",
|
||||||
|
"vcpkg",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "find-msvc-tools"
|
||||||
|
version = "0.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "glob"
|
||||||
|
version = "0.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hermit-abi"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itertools"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.18"
|
version = "1.0.18"
|
||||||
@@ -97,6 +204,16 @@ version = "0.2.184"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
|
checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libloading"
|
||||||
|
version = "0.8.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
version = "0.4.14"
|
version = "0.4.14"
|
||||||
@@ -127,6 +244,12 @@ version = "2.8.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "minimal-lexical"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@@ -138,6 +261,16 @@ dependencies = [
|
|||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nom"
|
||||||
|
version = "7.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
"minimal-lexical",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nu-ansi-term"
|
name = "nu-ansi-term"
|
||||||
version = "0.50.3"
|
version = "0.50.3"
|
||||||
@@ -147,6 +280,16 @@ dependencies = [
|
|||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num_cpus"
|
||||||
|
version = "1.17.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b"
|
||||||
|
dependencies = [
|
||||||
|
"hermit-abi",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.4"
|
version = "1.21.4"
|
||||||
@@ -182,6 +325,12 @@ version = "0.2.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pkg-config"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.106"
|
version = "1.0.106"
|
||||||
@@ -209,6 +358,18 @@ dependencies = [
|
|||||||
"bitflags",
|
"bitflags",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex"
|
||||||
|
version = "1.12.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-automata",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-automata"
|
name = "regex-automata"
|
||||||
version = "0.4.14"
|
version = "0.4.14"
|
||||||
@@ -226,6 +387,12 @@ version = "0.8.10"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustc-hash"
|
||||||
|
version = "2.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@@ -284,6 +451,12 @@ dependencies = [
|
|||||||
"lazy_static",
|
"lazy_static",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shlex"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signal-hook-registry"
|
name = "signal-hook-registry"
|
||||||
version = "1.4.8"
|
version = "1.4.8"
|
||||||
@@ -431,6 +604,12 @@ version = "0.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vcpkg"
|
||||||
|
version = "0.2.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
version = "0.11.1+wasi-snapshot-preview1"
|
version = "0.11.1+wasi-snapshot-preview1"
|
||||||
|
|||||||
@@ -9,3 +9,4 @@ tokio = { workspace = true }
|
|||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
tracing-subscriber = { workspace = true }
|
tracing-subscriber = { workspace = true }
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
|
ffmpeg = { package = "ffmpeg-next", version = "8" }
|
||||||
|
|||||||
128
media/client/src/capture.rs
Normal file
128
media/client/src/capture.rs
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
//! Screen capture via KMS/DRM using ffmpeg's kmsgrab device.
|
||||||
|
//!
|
||||||
|
//! Opens `/dev/dri/card0` via ffmpeg's kmsgrab input format,
|
||||||
|
//! reads DRM/DMA-BUF frames at the compositor's refresh rate,
|
||||||
|
//! and decimates to the target FPS.
|
||||||
|
|
||||||
|
use std::ffi::CString;
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
/// Configuration for KMS screen capture.
|
||||||
|
pub struct CaptureConfig {
|
||||||
|
/// DRM device path (e.g. "/dev/dri/card0")
|
||||||
|
pub device: String,
|
||||||
|
/// Target framerate (source frames are decimated to this)
|
||||||
|
pub fps: u32,
|
||||||
|
/// Output width (0 = native)
|
||||||
|
pub width: u32,
|
||||||
|
/// Output height (0 = native)
|
||||||
|
pub height: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CaptureConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
device: "/dev/dri/card0".into(),
|
||||||
|
fps: 30,
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// KMS screen capture source.
|
||||||
|
///
|
||||||
|
/// Uses ffmpeg's kmsgrab input device. Requires DRM master or root.
|
||||||
|
/// Outputs DRM_PRIME frames that can be mapped to VAAPI for zero-copy encode.
|
||||||
|
pub struct KmsCapture {
|
||||||
|
input_ctx: ffmpeg::format::context::Input,
|
||||||
|
video_stream_idx: usize,
|
||||||
|
decoder: ffmpeg::decoder::Video,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KmsCapture {
|
||||||
|
/// Open KMS capture with the given config.
|
||||||
|
///
|
||||||
|
/// This is a blocking call (ffmpeg device open). Run on a dedicated thread.
|
||||||
|
pub fn open(config: &CaptureConfig) -> Result<Self> {
|
||||||
|
ffmpeg::init().context("ffmpeg init")?;
|
||||||
|
|
||||||
|
// Set up the kmsgrab input device
|
||||||
|
let mut opts = ffmpeg::Dictionary::new();
|
||||||
|
opts.set("device", &config.device);
|
||||||
|
opts.set("framerate", &config.fps.to_string());
|
||||||
|
|
||||||
|
let fmt_name = CString::new("kmsgrab").unwrap();
|
||||||
|
let fmt_ptr = unsafe { ffmpeg::ffi::av_find_input_format(fmt_name.as_ptr()) };
|
||||||
|
anyhow::ensure!(
|
||||||
|
!fmt_ptr.is_null(),
|
||||||
|
"kmsgrab input format not found — is ffmpeg built with --enable-libdrm?"
|
||||||
|
);
|
||||||
|
let fmt = unsafe { ffmpeg::format::Input::wrap(fmt_ptr as *mut _) };
|
||||||
|
|
||||||
|
let input_ctx = ffmpeg::format::open_with("-", &ffmpeg::Format::Input(fmt), opts)
|
||||||
|
.context("failed to open kmsgrab device")?
|
||||||
|
.input();
|
||||||
|
|
||||||
|
let video_stream = input_ctx
|
||||||
|
.streams()
|
||||||
|
.best(ffmpeg::media::Type::Video)
|
||||||
|
.context("no video stream from kmsgrab")?;
|
||||||
|
|
||||||
|
let video_stream_idx = video_stream.index();
|
||||||
|
|
||||||
|
// Set up decoder with DRM hardware context
|
||||||
|
let decoder_ctx = ffmpeg::codec::context::Context::from_parameters(video_stream.parameters())
|
||||||
|
.context("decoder context from parameters")?;
|
||||||
|
|
||||||
|
let decoder = decoder_ctx.decoder().video().context("open video decoder")?;
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"KMS capture opened: {}x{} @ {}fps, stream idx={}",
|
||||||
|
decoder.width(),
|
||||||
|
decoder.height(),
|
||||||
|
config.fps,
|
||||||
|
video_stream_idx,
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
input_ctx,
|
||||||
|
video_stream_idx,
|
||||||
|
decoder,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read the next raw (DRM_PRIME) frame from the capture device.
|
||||||
|
///
|
||||||
|
/// Blocking — call from a dedicated thread.
|
||||||
|
/// Returns None at EOF (shouldn't happen for live capture).
|
||||||
|
pub fn next_frame(&mut self) -> Result<Option<ffmpeg::frame::Video>> {
|
||||||
|
loop {
|
||||||
|
match self.input_ctx.packets().next() {
|
||||||
|
Some((stream, packet)) => {
|
||||||
|
if stream.index() != self.video_stream_idx {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
self.decoder.send_packet(&packet)?;
|
||||||
|
let mut frame = ffmpeg::frame::Video::empty();
|
||||||
|
match self.decoder.receive_frame(&mut frame) {
|
||||||
|
Ok(()) => return Ok(Some(frame)),
|
||||||
|
Err(ffmpeg::Error::Other { errno: ffmpeg::error::EAGAIN }) => continue,
|
||||||
|
Err(e) => return Err(e.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => return Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn width(&self) -> u32 {
|
||||||
|
self.decoder.width()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn height(&self) -> u32 {
|
||||||
|
self.decoder.height()
|
||||||
|
}
|
||||||
|
}
|
||||||
285
media/client/src/encoder.rs
Normal file
285
media/client/src/encoder.rs
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
//! VAAPI H.264 hardware encoding.
|
||||||
|
//!
|
||||||
|
//! Two-phase initialization:
|
||||||
|
//! - Phase 1 (`VaapiEncoder::new`): store config, no ffmpeg calls.
|
||||||
|
//! - Phase 2 (first call to `encode`): build filter graph using `hw_frames_ctx`
|
||||||
|
//! from the first DRM_PRIME frame to wire up hwmap, then open the codec.
|
||||||
|
//!
|
||||||
|
//! The split exists because the filter graph needs the DRM hardware device
|
||||||
|
//! context that kmsgrab attaches to each frame — that context isn't available
|
||||||
|
//! until the first frame arrives, so the graph can't be validated at
|
||||||
|
//! construction time.
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
/// Encoding configuration.
|
||||||
|
pub struct EncoderConfig {
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
pub fps: u32,
|
||||||
|
/// Quantization parameter (lower = higher quality). Default: 20
|
||||||
|
pub qp: u32,
|
||||||
|
/// Keyframe interval in frames. Default: 30 (1 keyframe/sec at 30fps)
|
||||||
|
pub gop_size: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for EncoderConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
fps: 30,
|
||||||
|
qp: 20,
|
||||||
|
gop_size: 30,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// VAAPI H.264 encoder with lazy filter graph initialization.
|
||||||
|
pub struct VaapiEncoder {
|
||||||
|
config: EncoderConfig,
|
||||||
|
inner: Option<EncoderInner>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct EncoderInner {
|
||||||
|
encoder: ffmpeg::encoder::Video,
|
||||||
|
filter_graph: ffmpeg::filter::Graph,
|
||||||
|
frame_count: u64,
|
||||||
|
time_base: ffmpeg::Rational,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VaapiEncoder {
|
||||||
|
/// Create an encoder from config. No ffmpeg resources are allocated yet.
|
||||||
|
pub fn new(config: EncoderConfig) -> Self {
|
||||||
|
Self { config, inner: None }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode a frame. On the first call, initializes the filter graph and
|
||||||
|
/// codec using the frame's `hw_frames_ctx`. Returns 0 or more packets.
|
||||||
|
pub fn encode(&mut self, frame: &ffmpeg::frame::Video) -> Result<Vec<EncodedPacket>> {
|
||||||
|
if self.inner.is_none() {
|
||||||
|
self.inner = Some(EncoderInner::open(&self.config, frame).context("encoder init")?);
|
||||||
|
}
|
||||||
|
self.inner.as_mut().unwrap().encode(frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Flush remaining packets out of the encoder.
|
||||||
|
pub fn flush(&mut self) -> Result<Vec<EncodedPacket>> {
|
||||||
|
match self.inner.as_mut() {
|
||||||
|
Some(inner) => inner.flush(),
|
||||||
|
None => Ok(vec![]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EncoderInner {
|
||||||
|
fn open(config: &EncoderConfig, first_frame: &ffmpeg::frame::Video) -> Result<Self> {
|
||||||
|
let time_base = ffmpeg::Rational::new(1, config.fps as i32);
|
||||||
|
let filter_graph = Self::build_filter_graph(config, first_frame)?;
|
||||||
|
|
||||||
|
let codec = ffmpeg::encoder::find_by_name("h264_vaapi")
|
||||||
|
.context("h264_vaapi encoder not found")?;
|
||||||
|
|
||||||
|
let mut encoder_ctx = ffmpeg::codec::context::Context::new_with_codec(codec)
|
||||||
|
.encoder()
|
||||||
|
.video()?;
|
||||||
|
|
||||||
|
encoder_ctx.set_width(config.width);
|
||||||
|
encoder_ctx.set_height(config.height);
|
||||||
|
encoder_ctx.set_time_base(time_base);
|
||||||
|
encoder_ctx.set_frame_rate(Some(ffmpeg::Rational::new(config.fps as i32, 1)));
|
||||||
|
encoder_ctx.set_gop(config.gop_size);
|
||||||
|
encoder_ctx.set_max_b_frames(0);
|
||||||
|
encoder_ctx.set_format(ffmpeg::format::Pixel::VAAPI);
|
||||||
|
|
||||||
|
let mut opts = ffmpeg::Dictionary::new();
|
||||||
|
opts.set("qp", &config.qp.to_string());
|
||||||
|
|
||||||
|
let encoder = encoder_ctx
|
||||||
|
.open_with(opts)
|
||||||
|
.context("open h264_vaapi encoder")?;
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"VAAPI encoder opened: {}x{} @ {}fps, qp={}, gop={}",
|
||||||
|
config.width, config.height, config.fps, config.qp, config.gop_size,
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(Self { encoder, filter_graph, frame_count: 0, time_base })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the filter graph: DRM_PRIME → hwmap(vaapi) → scale_vaapi → buffersink.
|
||||||
|
///
|
||||||
|
/// Mirrors the ffmpeg CLI approach:
|
||||||
|
/// -init_hw_device drm=drm:/dev/dri/card0
|
||||||
|
/// -init_hw_device vaapi=va@drm
|
||||||
|
/// -filter_hw_device va
|
||||||
|
///
|
||||||
|
/// We create both devices explicitly and set the VAAPI device on the graph,
|
||||||
|
/// then attach the frame's hw_frames_ctx to the buffersrc so hwmap can map
|
||||||
|
/// DRM_PRIME frames to VAAPI surfaces.
|
||||||
|
fn build_filter_graph(
|
||||||
|
config: &EncoderConfig,
|
||||||
|
first_frame: &ffmpeg::frame::Video,
|
||||||
|
) -> Result<ffmpeg::filter::Graph> {
|
||||||
|
let input_width = first_frame.width();
|
||||||
|
let input_height = first_frame.height();
|
||||||
|
|
||||||
|
// Create DRM device, then derive VAAPI from it (like -init_hw_device vaapi=va@drm)
|
||||||
|
let (drm_device, vaapi_device) = unsafe {
|
||||||
|
let mut drm_ref: *mut ffmpeg::ffi::AVBufferRef = std::ptr::null_mut();
|
||||||
|
let dev_path = std::ffi::CString::new("/dev/dri/card0").unwrap();
|
||||||
|
let ret = ffmpeg::ffi::av_hwdevice_ctx_create(
|
||||||
|
&mut drm_ref,
|
||||||
|
ffmpeg::ffi::AVHWDeviceType::AV_HWDEVICE_TYPE_DRM,
|
||||||
|
dev_path.as_ptr(),
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
anyhow::ensure!(ret >= 0, "av_hwdevice_ctx_create(drm): {ret}");
|
||||||
|
info!("DRM device created");
|
||||||
|
|
||||||
|
let mut vaapi_ref: *mut ffmpeg::ffi::AVBufferRef = std::ptr::null_mut();
|
||||||
|
let ret = ffmpeg::ffi::av_hwdevice_ctx_create_derived(
|
||||||
|
&mut vaapi_ref,
|
||||||
|
ffmpeg::ffi::AVHWDeviceType::AV_HWDEVICE_TYPE_VAAPI,
|
||||||
|
drm_ref,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
anyhow::ensure!(ret >= 0, "av_hwdevice_ctx_create_derived(vaapi): {ret}");
|
||||||
|
info!("VAAPI device derived from DRM");
|
||||||
|
|
||||||
|
(drm_ref, vaapi_ref)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut graph = ffmpeg::filter::Graph::new();
|
||||||
|
|
||||||
|
let args = format!(
|
||||||
|
"video_size={}x{}:pix_fmt={}:time_base=1/{}:pixel_aspect=1/1",
|
||||||
|
input_width,
|
||||||
|
input_height,
|
||||||
|
ffmpeg::format::Pixel::DRM_PRIME as i32,
|
||||||
|
config.fps,
|
||||||
|
);
|
||||||
|
|
||||||
|
graph
|
||||||
|
.add(&ffmpeg::filter::find("buffer").unwrap(), "in", &args)
|
||||||
|
.context("add buffersrc")?;
|
||||||
|
graph
|
||||||
|
.add(&ffmpeg::filter::find("buffersink").unwrap(), "out", "")
|
||||||
|
.context("add buffersink")?;
|
||||||
|
|
||||||
|
let filter_spec = format!(
|
||||||
|
"hwmap=derive_device=vaapi,scale_vaapi=w={}:h={}:format=nv12",
|
||||||
|
config.width, config.height,
|
||||||
|
);
|
||||||
|
|
||||||
|
graph.output("in", 0)?.input("out", 0)?.parse(&filter_spec)?;
|
||||||
|
|
||||||
|
// Set the VAAPI device on filter contexts that need it
|
||||||
|
// (equivalent to -filter_hw_device va in the CLI)
|
||||||
|
unsafe {
|
||||||
|
let graph_ptr = graph.as_mut_ptr();
|
||||||
|
for i in 0..(*graph_ptr).nb_filters {
|
||||||
|
let fctx = *(*graph_ptr).filters.add(i as usize);
|
||||||
|
(*fctx).hw_device_ctx = ffmpeg::ffi::av_buffer_ref(vaapi_device);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach hw_frames_ctx from the first DRM_PRIME frame to the buffersrc
|
||||||
|
unsafe {
|
||||||
|
let hw_frames_ctx = (*first_frame.as_ptr()).hw_frames_ctx;
|
||||||
|
anyhow::ensure!(
|
||||||
|
!hw_frames_ctx.is_null(),
|
||||||
|
"first KMS frame has no hw_frames_ctx"
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut buffersrc = graph.get("in").unwrap();
|
||||||
|
let par = ffmpeg::ffi::av_buffersrc_parameters_alloc();
|
||||||
|
anyhow::ensure!(!par.is_null(), "av_buffersrc_parameters_alloc OOM");
|
||||||
|
(*par).hw_frames_ctx = ffmpeg::ffi::av_buffer_ref(hw_frames_ctx);
|
||||||
|
let ret = ffmpeg::ffi::av_buffersrc_parameters_set(buffersrc.as_mut_ptr(), par);
|
||||||
|
ffmpeg::ffi::av_free(par as *mut _);
|
||||||
|
anyhow::ensure!(ret >= 0, "av_buffersrc_parameters_set: {ret}");
|
||||||
|
}
|
||||||
|
|
||||||
|
graph.validate().context("validate filter graph")?;
|
||||||
|
info!("Filter graph ready: {filter_spec}");
|
||||||
|
|
||||||
|
// Keep device refs alive — they're ref-counted by the graph now,
|
||||||
|
// but we drop our refs here.
|
||||||
|
unsafe {
|
||||||
|
ffmpeg::ffi::av_buffer_unref(&mut (drm_device as *mut _));
|
||||||
|
ffmpeg::ffi::av_buffer_unref(&mut (vaapi_device as *mut _));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(graph)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode(&mut self, frame: &ffmpeg::frame::Video) -> Result<Vec<EncodedPacket>> {
|
||||||
|
self.filter_graph
|
||||||
|
.get("in")
|
||||||
|
.unwrap()
|
||||||
|
.source()
|
||||||
|
.add(frame)
|
||||||
|
.context("buffersrc add")?;
|
||||||
|
|
||||||
|
let mut packets = Vec::new();
|
||||||
|
let mut filtered = ffmpeg::frame::Video::empty();
|
||||||
|
|
||||||
|
while self
|
||||||
|
.filter_graph
|
||||||
|
.get("out")
|
||||||
|
.unwrap()
|
||||||
|
.sink()
|
||||||
|
.frame(&mut filtered)
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
filtered.set_pts(Some(self.frame_count as i64));
|
||||||
|
self.frame_count += 1;
|
||||||
|
|
||||||
|
self.encoder.send_frame(&filtered)?;
|
||||||
|
|
||||||
|
let mut encoded = ffmpeg::Packet::empty();
|
||||||
|
while self.encoder.receive_packet(&mut encoded).is_ok() {
|
||||||
|
packets.push(EncodedPacket {
|
||||||
|
data: encoded.data().unwrap_or(&[]).to_vec(),
|
||||||
|
pts: encoded.pts().unwrap_or(0),
|
||||||
|
dts: encoded.dts().unwrap_or(0),
|
||||||
|
keyframe: encoded.is_key(),
|
||||||
|
time_base_num: self.time_base.numerator() as u32,
|
||||||
|
time_base_den: self.time_base.denominator() as u32,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(packets)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> Result<Vec<EncodedPacket>> {
|
||||||
|
self.encoder.send_eof()?;
|
||||||
|
let mut packets = Vec::new();
|
||||||
|
let mut encoded = ffmpeg::Packet::empty();
|
||||||
|
while self.encoder.receive_packet(&mut encoded).is_ok() {
|
||||||
|
packets.push(EncodedPacket {
|
||||||
|
data: encoded.data().unwrap_or(&[]).to_vec(),
|
||||||
|
pts: encoded.pts().unwrap_or(0),
|
||||||
|
dts: encoded.dts().unwrap_or(0),
|
||||||
|
keyframe: encoded.is_key(),
|
||||||
|
time_base_num: self.time_base.numerator() as u32,
|
||||||
|
time_base_den: self.time_base.denominator() as u32,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(packets)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An encoded video packet ready for transport.
|
||||||
|
pub struct EncodedPacket {
|
||||||
|
pub data: Vec<u8>,
|
||||||
|
pub pts: i64,
|
||||||
|
pub dts: i64,
|
||||||
|
pub keyframe: bool,
|
||||||
|
pub time_base_num: u32,
|
||||||
|
pub time_base_den: u32,
|
||||||
|
}
|
||||||
3
media/client/src/lib.rs
Normal file
3
media/client/src/lib.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod capture;
|
||||||
|
pub mod encoder;
|
||||||
|
pub mod pipeline;
|
||||||
@@ -7,6 +7,10 @@ use tokio::io::{AsyncWriteExt, BufWriter};
|
|||||||
use tokio::net::TcpStream;
|
use tokio::net::TcpStream;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
|
use cht_client::capture::CaptureConfig;
|
||||||
|
use cht_client::encoder::EncoderConfig;
|
||||||
|
use cht_client::pipeline::Pipeline;
|
||||||
|
|
||||||
const DEFAULT_SERVER: &str = "mcrndeb:4444";
|
const DEFAULT_SERVER: &str = "mcrndeb:4444";
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
@@ -23,14 +27,17 @@ async fn main() -> Result<()> {
|
|||||||
|
|
||||||
let mut writer = BufWriter::new(stream);
|
let mut writer = BufWriter::new(stream);
|
||||||
|
|
||||||
|
let capture_config = CaptureConfig::default();
|
||||||
|
let encoder_config = EncoderConfig::default();
|
||||||
|
|
||||||
// Send session_start
|
// Send session_start
|
||||||
let session_start = ControlMessage::SessionStart {
|
let session_start = ControlMessage::SessionStart {
|
||||||
id: chrono_session_id(),
|
id: session_id(),
|
||||||
video: VideoParams {
|
video: VideoParams {
|
||||||
width: 1920,
|
width: encoder_config.width,
|
||||||
height: 1080,
|
height: encoder_config.height,
|
||||||
codec: "h264".into(),
|
codec: "h264".into(),
|
||||||
fps: 30,
|
fps: encoder_config.fps,
|
||||||
},
|
},
|
||||||
audio: AudioParams {
|
audio: AudioParams {
|
||||||
sample_rate: 48000,
|
sample_rate: 48000,
|
||||||
@@ -39,60 +46,80 @@ async fn main() -> Result<()> {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
protocol::write_packet(&mut writer, &session_start.to_wire_packet()?).await?;
|
protocol::write_packet(&mut writer, &session_start.to_wire_packet()?).await?;
|
||||||
|
writer.flush().await?;
|
||||||
info!("Sent session_start");
|
info!("Sent session_start");
|
||||||
|
|
||||||
// Send test packets (placeholder — will be replaced by real capture)
|
// Start capture pipeline on a dedicated thread
|
||||||
let frame_interval_ns = 33_333_333u64; // ~30fps
|
let (packet_tx, mut packet_rx) = tokio::sync::mpsc::channel(64);
|
||||||
for i in 0u64..300 {
|
let mut pipeline = Pipeline::start(capture_config, encoder_config, packet_tx);
|
||||||
let ts = i * frame_interval_ns;
|
|
||||||
let keyframe = i % 30 == 0;
|
|
||||||
|
|
||||||
// Fake video packet
|
// Forward encoded packets to the server
|
||||||
let video = WirePacket {
|
let mut video_count = 0u64;
|
||||||
|
let mut keepalive_interval = tokio::time::interval(std::time::Duration::from_secs(5));
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
pkt = packet_rx.recv() => {
|
||||||
|
match pkt {
|
||||||
|
Some(encoded) => {
|
||||||
|
let wire = WirePacket {
|
||||||
header: PacketHeader {
|
header: PacketHeader {
|
||||||
packet_type: PacketType::Video,
|
packet_type: PacketType::Video,
|
||||||
flags: if keyframe { FLAG_KEYFRAME } else { 0 },
|
flags: if encoded.keyframe { FLAG_KEYFRAME } else { 0 },
|
||||||
length: 1024,
|
length: encoded.data.len() as u32,
|
||||||
timestamp_ns: ts,
|
timestamp_ns: pts_to_ns(
|
||||||
|
encoded.pts,
|
||||||
|
encoded.time_base_num,
|
||||||
|
encoded.time_base_den,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
payload: vec![0u8; 1024],
|
payload: encoded.data,
|
||||||
};
|
};
|
||||||
protocol::write_packet(&mut writer, &video).await?;
|
protocol::write_packet(&mut writer, &wire).await?;
|
||||||
|
video_count += 1;
|
||||||
|
|
||||||
// Fake audio packet every 3 video frames
|
if video_count % 300 == 1 {
|
||||||
if i % 3 == 0 {
|
info!("Sent {video_count} video packets");
|
||||||
let audio = WirePacket {
|
writer.flush().await?;
|
||||||
header: PacketHeader {
|
|
||||||
packet_type: PacketType::Audio,
|
|
||||||
flags: 0,
|
|
||||||
length: 512,
|
|
||||||
timestamp_ns: ts,
|
|
||||||
},
|
|
||||||
payload: vec![0u8; 512],
|
|
||||||
};
|
|
||||||
protocol::write_packet(&mut writer, &audio).await?;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Keepalive every 150 frames (~5s)
|
None => {
|
||||||
if i % 150 == 0 && i > 0 {
|
info!("Pipeline channel closed");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = keepalive_interval.tick() => {
|
||||||
let keepalive = ControlMessage::Keepalive;
|
let keepalive = ControlMessage::Keepalive;
|
||||||
protocol::write_packet(&mut writer, &keepalive.to_wire_packet()?).await?;
|
protocol::write_packet(&mut writer, &keepalive.to_wire_packet()?).await?;
|
||||||
|
writer.flush().await?;
|
||||||
|
}
|
||||||
|
_ = tokio::signal::ctrl_c() => {
|
||||||
|
info!("Ctrl+C received, stopping...");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tokio::time::sleep(std::time::Duration::from_nanos(frame_interval_ns)).await;
|
pipeline.stop();
|
||||||
}
|
|
||||||
|
|
||||||
// Send session_stop and flush
|
|
||||||
let stop = ControlMessage::SessionStop;
|
let stop = ControlMessage::SessionStop;
|
||||||
protocol::write_packet(&mut writer, &stop.to_wire_packet()?).await?;
|
protocol::write_packet(&mut writer, &stop.to_wire_packet()?).await?;
|
||||||
writer.flush().await?;
|
writer.flush().await?;
|
||||||
writer.shutdown().await?;
|
writer.shutdown().await?;
|
||||||
info!("Sent session_stop, done");
|
info!("Sent session_stop, {video_count} video packets total");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn chrono_session_id() -> String {
|
fn pts_to_ns(pts: i64, tb_num: u32, tb_den: u32) -> u64 {
|
||||||
|
if tb_den == 0 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
((pts as u128 * tb_num as u128 * 1_000_000_000) / tb_den as u128) as u64
|
||||||
|
}
|
||||||
|
|
||||||
|
fn session_id() -> String {
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
let secs = SystemTime::now()
|
let secs = SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
|
|||||||
106
media/client/src/pipeline.rs
Normal file
106
media/client/src/pipeline.rs
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
//! Capture pipeline: ties capture → encode → transport on a dedicated thread.
|
||||||
|
//!
|
||||||
|
//! This is the main loop that runs on a blocking thread, reading frames
|
||||||
|
//! from KMS, encoding with VAAPI, and sending encoded packets through a channel.
|
||||||
|
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use tracing::{error, info, warn};
|
||||||
|
|
||||||
|
use crate::capture::{CaptureConfig, KmsCapture};
|
||||||
|
use crate::encoder::{EncodedPacket, EncoderConfig, VaapiEncoder};
|
||||||
|
|
||||||
|
/// A running capture pipeline that produces encoded packets.
|
||||||
|
pub struct Pipeline {
|
||||||
|
thread: Option<std::thread::JoinHandle<()>>,
|
||||||
|
stop: Arc<AtomicBool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Pipeline {
|
||||||
|
/// Start the capture → encode pipeline on a dedicated thread.
|
||||||
|
///
|
||||||
|
/// Encoded packets are sent through `packet_tx`.
|
||||||
|
/// The pipeline runs until `stop()` is called or an error occurs.
|
||||||
|
pub fn start(
|
||||||
|
capture_config: CaptureConfig,
|
||||||
|
encoder_config: EncoderConfig,
|
||||||
|
packet_tx: tokio::sync::mpsc::Sender<EncodedPacket>,
|
||||||
|
) -> Self {
|
||||||
|
let stop = Arc::new(AtomicBool::new(false));
|
||||||
|
let stop_clone = stop.clone();
|
||||||
|
|
||||||
|
let thread = std::thread::Builder::new()
|
||||||
|
.name("capture-pipeline".into())
|
||||||
|
.spawn(move || {
|
||||||
|
if let Err(e) = run_pipeline(capture_config, encoder_config, packet_tx, stop_clone) {
|
||||||
|
error!("Pipeline error: {e:#}");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.expect("spawn capture thread");
|
||||||
|
|
||||||
|
Self {
|
||||||
|
thread: Some(thread),
|
||||||
|
stop,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Signal the pipeline to stop and wait for the thread to finish.
|
||||||
|
pub fn stop(&mut self) {
|
||||||
|
self.stop.store(true, Ordering::Relaxed);
|
||||||
|
if let Some(handle) = self.thread.take() {
|
||||||
|
let _ = handle.join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Pipeline {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_pipeline(
|
||||||
|
capture_config: CaptureConfig,
|
||||||
|
encoder_config: EncoderConfig,
|
||||||
|
packet_tx: tokio::sync::mpsc::Sender<EncodedPacket>,
|
||||||
|
stop: Arc<AtomicBool>,
|
||||||
|
) -> Result<()> {
|
||||||
|
info!("Opening KMS capture...");
|
||||||
|
let mut capture = KmsCapture::open(&capture_config).context("open capture")?;
|
||||||
|
|
||||||
|
info!("Encoder ready (will initialize on first frame)");
|
||||||
|
let mut encoder = VaapiEncoder::new(encoder_config);
|
||||||
|
|
||||||
|
info!("Pipeline running");
|
||||||
|
|
||||||
|
while !stop.load(Ordering::Relaxed) {
|
||||||
|
let frame = match capture.next_frame()? {
|
||||||
|
Some(f) => f,
|
||||||
|
None => {
|
||||||
|
warn!("Capture returned EOF");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let packets = encoder.encode(&frame).context("encode frame")?;
|
||||||
|
|
||||||
|
for pkt in packets {
|
||||||
|
if packet_tx.blocking_send(pkt).is_err() {
|
||||||
|
info!("Packet channel closed, stopping pipeline");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush encoder
|
||||||
|
info!("Flushing encoder...");
|
||||||
|
let flush_pkts = encoder.flush()?;
|
||||||
|
for pkt in flush_pkts {
|
||||||
|
let _ = packet_tx.blocking_send(pkt);
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Pipeline stopped");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
36
media/ctrl/build.sh
Executable file
36
media/ctrl/build.sh
Executable file
@@ -0,0 +1,36 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Build media transport targets
|
||||||
|
# Usage: ./build.sh [client|server|all] [--release]
|
||||||
|
# client build cht-client (default)
|
||||||
|
# server build cht-server
|
||||||
|
# all build both
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
MEDIA_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
CARGO="${CARGO:-$HOME/.cargo/bin/cargo}"
|
||||||
|
|
||||||
|
TARGET="${1:-client}"
|
||||||
|
shift || true
|
||||||
|
|
||||||
|
CARGO_FLAGS=()
|
||||||
|
for arg in "$@"; do
|
||||||
|
CARGO_FLAGS+=("$arg")
|
||||||
|
done
|
||||||
|
|
||||||
|
LOG_DIR="$MEDIA_DIR/logs"
|
||||||
|
mkdir -p "$LOG_DIR"
|
||||||
|
|
||||||
|
build() {
|
||||||
|
local pkg="$1"
|
||||||
|
local log="$LOG_DIR/build-$pkg.log"
|
||||||
|
echo "==> building $pkg ${CARGO_FLAGS[*]+"${CARGO_FLAGS[@]}"}"
|
||||||
|
"$CARGO" build -p "cht-$pkg" "${CARGO_FLAGS[@]}" --manifest-path "$MEDIA_DIR/Cargo.toml" 2>&1 | tee "$log"
|
||||||
|
return "${PIPESTATUS[0]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$TARGET" in
|
||||||
|
client) build client ;;
|
||||||
|
server) build server ;;
|
||||||
|
all) build client || true; build server ;;
|
||||||
|
*) echo "Usage: $0 [client|server|all] [--release]" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
21
media/ctrl/client.sh
Executable file
21
media/ctrl/client.sh
Executable file
@@ -0,0 +1,21 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Build and run the media client (sender)
|
||||||
|
# Requires DRM master access — runs under sudo unless already root.
|
||||||
|
# Usage: ./client.sh [server_addr] e.g. ./client.sh mcrndeb:4444
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
MEDIA_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
CARGO="${CARGO:-$HOME/.cargo/bin/cargo}"
|
||||||
|
|
||||||
|
LOG_DIR="$MEDIA_DIR/logs"
|
||||||
|
mkdir -p "$LOG_DIR"
|
||||||
|
"$CARGO" build -p cht-client --manifest-path "$MEDIA_DIR/Cargo.toml" 2>&1 | tee "$LOG_DIR/build-client.log"
|
||||||
|
if [ "${PIPESTATUS[0]}" -ne 0 ]; then exit 1; fi
|
||||||
|
|
||||||
|
BIN="$MEDIA_DIR/target/debug/cht-client"
|
||||||
|
|
||||||
|
if [ "$(id -u)" -ne 0 ]; then
|
||||||
|
exec sudo "$BIN" "$@"
|
||||||
|
else
|
||||||
|
exec "$BIN" "$@"
|
||||||
|
fi
|
||||||
21
media/ctrl/docs.sh
Executable file
21
media/ctrl/docs.sh
Executable file
@@ -0,0 +1,21 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Re-render all Graphviz diagrams to SVG.
|
||||||
|
# Run this after each phase when .dot files are updated.
|
||||||
|
# Usage: ./docs.sh
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DOCS_DIR="$(cd "$(dirname "$0")/../docs" && pwd)"
|
||||||
|
|
||||||
|
if ! command -v dot &>/dev/null; then
|
||||||
|
echo "graphviz not found — install with: sudo apt install graphviz" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
for f in "$DOCS_DIR"/*.dot; do
|
||||||
|
svg="${f%.dot}.svg"
|
||||||
|
echo "==> $(basename "$f") → $(basename "$svg")"
|
||||||
|
dot -Tsvg "$f" -o "$svg"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "==> done. Serving at http://localhost:9099 (ctrl-c to stop)"
|
||||||
|
cd "$DOCS_DIR" && python3 -m http.server 9099
|
||||||
15
media/ctrl/server.sh
Executable file
15
media/ctrl/server.sh
Executable file
@@ -0,0 +1,15 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Build and run the media server (receiver).
|
||||||
|
# Run this on mcrndeb directly.
|
||||||
|
# Usage: ./server.sh [port]
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
MEDIA_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
CARGO="${CARGO:-$HOME/.cargo/bin/cargo}"
|
||||||
|
|
||||||
|
LOG_DIR="$MEDIA_DIR/logs"
|
||||||
|
mkdir -p "$LOG_DIR"
|
||||||
|
"$CARGO" build -p cht-server --manifest-path "$MEDIA_DIR/Cargo.toml" 2>&1 | tee "$LOG_DIR/build-server.log"
|
||||||
|
if [ "${PIPESTATUS[0]}" -ne 0 ]; then exit 1; fi
|
||||||
|
|
||||||
|
exec "$MEDIA_DIR/target/debug/cht-server" "$@"
|
||||||
51
media/docs/client-pipeline.dot
Normal file
51
media/docs/client-pipeline.dot
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
// Client pipeline data flow — Phase 2
|
||||||
|
// Sender machine (Wayland, VAAPI GPU)
|
||||||
|
digraph client_pipeline {
|
||||||
|
graph [fontname="monospace" bgcolor="#1e1e2e" rankdir=TB pad="0.6" splines=polyline]
|
||||||
|
node [fontname="monospace" fontcolor="#cdd6f4" style=filled shape=box
|
||||||
|
fillcolor="#313244" color="#585b70" margin="0.25,0.12"]
|
||||||
|
edge [color="#585b70" fontname="monospace" fontcolor="#a6adc8" labelfontname="monospace"]
|
||||||
|
|
||||||
|
// Hardware
|
||||||
|
drm [label="/dev/dri/card0\n(KMS scanout)" shape=cylinder fillcolor="#1e3a2f" color="#a6e3a1"]
|
||||||
|
vaapi [label="/dev/dri/renderD128\n(VAAPI)" shape=cylinder fillcolor="#1e3a2f" color="#a6e3a1"]
|
||||||
|
net [label="TCP :4444\nmcrndeb" shape=parallelogram fillcolor="#1e2a3e" color="#89b4fa"]
|
||||||
|
|
||||||
|
// Thread boundary
|
||||||
|
subgraph cluster_main {
|
||||||
|
label="main thread (tokio async)" fontcolor="#a6adc8" color="#45475a" fontname="monospace"
|
||||||
|
|
||||||
|
session_start [label="session_start\ncontrol message" fillcolor="#2d2038" color="#cba6f7"]
|
||||||
|
mux [label="select!\npkt_rx | keepalive | ctrl-c" fillcolor="#2d2038" color="#cba6f7"]
|
||||||
|
keepalive [label="keepalive / 5s" fillcolor="#2d2038" color="#cba6f7"]
|
||||||
|
write [label="BufWriter\nwrite_packet()" fillcolor="#1e2d3e" color="#89b4fa"]
|
||||||
|
}
|
||||||
|
|
||||||
|
subgraph cluster_pipeline {
|
||||||
|
label="capture-pipeline thread (blocking)" fontcolor="#a6adc8" color="#45475a" fontname="monospace"
|
||||||
|
|
||||||
|
capture [label="KmsCapture\n─────────────────\nffmpeg kmsgrab device\ndecoder: passthrough\noutput: DRM_PRIME frames\n+ hw_frames_ctx (DRM device)"
|
||||||
|
fillcolor="#1e2d3e" color="#89b4fa"]
|
||||||
|
|
||||||
|
encoder [label="VaapiEncoder\n─────────────────\n[lazy init on frame 1]\nbuffersrc ← hw_frames_ctx\nhwmap derive_device=vaapi\nscale_vaapi NV12 1920×1080\nh264_vaapi QP=20 GOP=30"
|
||||||
|
fillcolor="#1e2d3e" color="#89b4fa"]
|
||||||
|
|
||||||
|
chan [label="mpsc::channel(64)\nEncodedPacket" shape=parallelogram fillcolor="#2d2038" color="#cba6f7"]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flow
|
||||||
|
drm -> capture [label="DMA-BUF\n(zero copy)"]
|
||||||
|
vaapi -> encoder [label="hw device\n(derived)" style=dashed color="#a6e3a1"]
|
||||||
|
capture -> encoder [label="AVFrame\nDRM_PRIME"]
|
||||||
|
encoder -> chan [label="EncodedPacket\n{ data, pts, keyframe, … }"]
|
||||||
|
chan -> mux
|
||||||
|
session_start -> write
|
||||||
|
mux -> write [label="WirePacket"]
|
||||||
|
mux -> keepalive [style=dashed]
|
||||||
|
keepalive -> write
|
||||||
|
write -> net
|
||||||
|
|
||||||
|
// Types note
|
||||||
|
types [label="EncodedPacket\n─────────────\ndata: Vec\<u8\> (H.264 NALUs)\npts / dts: i64\nkeyframe: bool\ntime_base: num/den"
|
||||||
|
shape=note fillcolor="#2a2a3e" color="#585b70"]
|
||||||
|
}
|
||||||
185
media/docs/client-pipeline.svg
Normal file
185
media/docs/client-pipeline.svg
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||||
|
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<!-- Generated by graphviz version 14.1.2 (0)
|
||||||
|
-->
|
||||||
|
<!-- Title: client_pipeline Pages: 1 -->
|
||||||
|
<svg width="779pt" height="1234pt"
|
||||||
|
viewBox="0.00 0.00 779.00 1234.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(43.2 1191.25)">
|
||||||
|
<title>client_pipeline</title>
|
||||||
|
<polygon fill="#1e1e2e" stroke="none" points="-43.2,43.2 -43.2,-1191.25 735.58,-1191.25 735.58,43.2 -43.2,43.2"/>
|
||||||
|
<g id="clust1" class="cluster">
|
||||||
|
<title>cluster_main</title>
|
||||||
|
<polygon fill="#1e1e2e" stroke="#45475a" points="8,-132.56 8,-403.15 473,-403.15 473,-132.56 8,-132.56"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="240.5" y="-385.85" font-family="monospace" font-size="14.00" fill="#a6adc8">main thread  (tokio async)</text>
|
||||||
|
</g>
|
||||||
|
<g id="clust2" class="cluster">
|
||||||
|
<title>cluster_pipeline</title>
|
||||||
|
<polygon fill="#1e1e2e" stroke="#45475a" points="110,-422.15 110,-966.77 492,-966.77 492,-422.15 110,-422.15"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="301" y="-949.47" font-family="monospace" font-size="14.00" fill="#a6adc8">capture-pipeline thread  (blocking)</text>
|
||||||
|
</g>
|
||||||
|
<!-- drm -->
|
||||||
|
<g id="node1" class="node">
|
||||||
|
<title>drm</title>
|
||||||
|
<path fill="#1e3a2f" stroke="#a6e3a1" d="M383.75,-1116.79C383.75,-1120.36 349.8,-1123.26 308,-1123.26 266.2,-1123.26 232.25,-1120.36 232.25,-1116.79 232.25,-1116.79 232.25,-1058.53 232.25,-1058.53 232.25,-1054.96 266.2,-1052.06 308,-1052.06 349.8,-1052.06 383.75,-1054.96 383.75,-1058.53 383.75,-1058.53 383.75,-1116.79 383.75,-1116.79"/>
|
||||||
|
<path fill="none" stroke="#a6e3a1" d="M383.75,-1116.79C383.75,-1113.21 349.8,-1110.31 308,-1110.31 266.2,-1110.31 232.25,-1113.21 232.25,-1116.79"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="308" y="-1091.61" font-family="monospace" font-size="14.00" fill="#cdd6f4">/dev/dri/card0</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="308" y="-1074.36" font-family="monospace" font-size="14.00" fill="#cdd6f4">(KMS scanout)</text>
|
||||||
|
</g>
|
||||||
|
<!-- capture -->
|
||||||
|
<g id="node8" class="node">
|
||||||
|
<title>capture</title>
|
||||||
|
<polygon fill="#1e2d3e" stroke="#89b4fa" points="441.5,-933.52 174.5,-933.52 174.5,-812.74 441.5,-812.74 441.5,-933.52"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="308" y="-911.58" font-family="monospace" font-size="14.00" fill="#cdd6f4">KmsCapture</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="308" y="-894.33" font-family="monospace" font-size="14.00" fill="#cdd6f4">─────────────────</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="308" y="-877.08" font-family="monospace" font-size="14.00" fill="#cdd6f4">ffmpeg kmsgrab device</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="308" y="-859.83" font-family="monospace" font-size="14.00" fill="#cdd6f4">decoder: passthrough</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="308" y="-842.58" font-family="monospace" font-size="14.00" fill="#cdd6f4">output: DRM_PRIME frames</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="308" y="-825.33" font-family="monospace" font-size="14.00" fill="#cdd6f4">+ hw_frames_ctx (DRM device)</text>
|
||||||
|
</g>
|
||||||
|
<!-- drm->capture -->
|
||||||
|
<g id="edge1" class="edge">
|
||||||
|
<title>drm->capture</title>
|
||||||
|
<path fill="none" stroke="#585b70" d="M308,-1051.56C308,-1022.73 308,-980.97 308,-945.2"/>
|
||||||
|
<polygon fill="#585b70" stroke="#585b70" points="311.5,-945.34 308,-935.34 304.5,-945.34 311.5,-945.34"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="353.38" y="-995.97" font-family="monospace" font-size="14.00" fill="#a6adc8">DMA-BUF</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="353.38" y="-978.72" font-family="monospace" font-size="14.00" fill="#a6adc8">(zero copy)</text>
|
||||||
|
</g>
|
||||||
|
<!-- vaapi -->
|
||||||
|
<g id="node2" class="node">
|
||||||
|
<title>vaapi</title>
|
||||||
|
<path fill="#1e3a2f" stroke="#a6e3a1" d="M692.38,-902.26C692.38,-905.83 649.18,-908.73 596,-908.73 542.82,-908.73 499.62,-905.83 499.62,-902.26 499.62,-902.26 499.62,-844 499.62,-844 499.62,-840.43 542.82,-837.53 596,-837.53 649.18,-837.53 692.38,-840.43 692.38,-844 692.38,-844 692.38,-902.26 692.38,-902.26"/>
|
||||||
|
<path fill="none" stroke="#a6e3a1" d="M692.38,-902.26C692.38,-898.68 649.18,-895.78 596,-895.78 542.82,-895.78 499.62,-898.68 499.62,-902.26"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="596" y="-877.08" font-family="monospace" font-size="14.00" fill="#cdd6f4">/dev/dri/renderD128</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="596" y="-859.83" font-family="monospace" font-size="14.00" fill="#cdd6f4">(VAAPI)</text>
|
||||||
|
</g>
|
||||||
|
<!-- encoder -->
|
||||||
|
<g id="node9" class="node">
|
||||||
|
<title>encoder</title>
|
||||||
|
<polygon fill="#1e2d3e" stroke="#89b4fa" points="433.25,-742.24 182.75,-742.24 182.75,-604.21 433.25,-604.21 433.25,-742.24"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="308" y="-720.3" font-family="monospace" font-size="14.00" fill="#cdd6f4">VaapiEncoder</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="308" y="-703.05" font-family="monospace" font-size="14.00" fill="#cdd6f4">─────────────────</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="308" y="-685.8" font-family="monospace" font-size="14.00" fill="#cdd6f4">[lazy init on frame 1]</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="308" y="-668.55" font-family="monospace" font-size="14.00" fill="#cdd6f4">buffersrc ← hw_frames_ctx</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="308" y="-651.3" font-family="monospace" font-size="14.00" fill="#cdd6f4">hwmap derive_device=vaapi</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="308" y="-634.05" font-family="monospace" font-size="14.00" fill="#cdd6f4">scale_vaapi NV12 1920×1080</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="308" y="-616.8" font-family="monospace" font-size="14.00" fill="#cdd6f4">h264_vaapi QP=20 GOP=30</text>
|
||||||
|
</g>
|
||||||
|
<!-- vaapi->encoder -->
|
||||||
|
<g id="edge2" class="edge">
|
||||||
|
<title>vaapi->encoder</title>
|
||||||
|
<path fill="none" stroke="#a6e3a1" stroke-dasharray="5,2" d="M545.17,-837.2C509.67,-812.81 460.92,-779.31 416.85,-749.03"/>
|
||||||
|
<polygon fill="#a6e3a1" stroke="#a6e3a1" points="419,-746.25 408.78,-743.48 415.04,-752.02 419,-746.25"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="514.92" y="-781.44" font-family="monospace" font-size="14.00" fill="#a6adc8">hw device</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="514.92" y="-764.19" font-family="monospace" font-size="14.00" fill="#a6adc8">(derived)</text>
|
||||||
|
</g>
|
||||||
|
<!-- net -->
|
||||||
|
<g id="node3" class="node">
|
||||||
|
<title>net</title>
|
||||||
|
<polygon fill="#1e2a3e" stroke="#89b4fa" points="384.3,-103.56 202.47,-103.56 155.7,0 337.53,0 384.3,-103.56"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="270" y="-55.73" font-family="monospace" font-size="14.00" fill="#cdd6f4">TCP :4444</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="270" y="-38.48" font-family="monospace" font-size="14.00" fill="#cdd6f4">mcrndeb</text>
|
||||||
|
</g>
|
||||||
|
<!-- session_start -->
|
||||||
|
<g id="node4" class="node">
|
||||||
|
<title>session_start</title>
|
||||||
|
<polygon fill="#2d2038" stroke="#cba6f7" points="175.88,-281.12 16.12,-281.12 16.12,-229.34 175.88,-229.34 175.88,-281.12"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="96" y="-259.18" font-family="monospace" font-size="14.00" fill="#cdd6f4">session_start</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="96" y="-241.93" font-family="monospace" font-size="14.00" fill="#cdd6f4">control message</text>
|
||||||
|
</g>
|
||||||
|
<!-- write -->
|
||||||
|
<g id="node7" class="node">
|
||||||
|
<title>write</title>
|
||||||
|
<polygon fill="#1e2d3e" stroke="#89b4fa" points="345.75,-192.34 194.25,-192.34 194.25,-140.56 345.75,-140.56 345.75,-192.34"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="270" y="-170.4" font-family="monospace" font-size="14.00" fill="#cdd6f4">BufWriter</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="270" y="-153.15" font-family="monospace" font-size="14.00" fill="#cdd6f4">write_packet()</text>
|
||||||
|
</g>
|
||||||
|
<!-- session_start->write -->
|
||||||
|
<g id="edge6" class="edge">
|
||||||
|
<title>session_start->write</title>
|
||||||
|
<path fill="none" stroke="#585b70" d="M146.8,-228.9C166.28,-219.18 188.7,-208 209.04,-197.85"/>
|
||||||
|
<polygon fill="#585b70" stroke="#585b70" points="210.4,-201.09 217.79,-193.49 207.28,-194.82 210.4,-201.09"/>
|
||||||
|
</g>
|
||||||
|
<!-- mux -->
|
||||||
|
<g id="node5" class="node">
|
||||||
|
<title>mux</title>
|
||||||
|
<polygon fill="#2d2038" stroke="#cba6f7" points="446.88,-369.9 155.12,-369.9 155.12,-318.12 446.88,-318.12 446.88,-369.9"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="301" y="-347.96" font-family="monospace" font-size="14.00" fill="#cdd6f4">select!</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="301" y="-330.71" font-family="monospace" font-size="14.00" fill="#cdd6f4">pkt_rx  |  keepalive  |  ctrl-c</text>
|
||||||
|
</g>
|
||||||
|
<!-- keepalive -->
|
||||||
|
<g id="node6" class="node">
|
||||||
|
<title>keepalive</title>
|
||||||
|
<polygon fill="#2d2038" stroke="#cba6f7" points="345.75,-273.23 194.25,-273.23 194.25,-237.23 345.75,-237.23 345.75,-273.23"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="270" y="-250.55" font-family="monospace" font-size="14.00" fill="#cdd6f4">keepalive / 5s</text>
|
||||||
|
</g>
|
||||||
|
<!-- mux->keepalive -->
|
||||||
|
<g id="edge8" class="edge">
|
||||||
|
<title>mux->keepalive</title>
|
||||||
|
<path fill="none" stroke="#585b70" stroke-dasharray="5,2" d="M292.03,-317.91C288.26,-307.36 283.86,-295.04 279.99,-284.2"/>
|
||||||
|
<polygon fill="#585b70" stroke="#585b70" points="283.36,-283.24 276.7,-275 276.77,-285.59 283.36,-283.24"/>
|
||||||
|
</g>
|
||||||
|
<!-- mux->write -->
|
||||||
|
<g id="edge7" class="edge">
|
||||||
|
<title>mux->write</title>
|
||||||
|
<path fill="none" stroke="#585b70" d="M322.96,-317.84C337.91,-300.7 355,-281.12 355,-281.12 355,-281.12 355,-229.34 355,-229.34 355,-229.34 334.59,-214.48 313.78,-199.32"/>
|
||||||
|
<polygon fill="#585b70" stroke="#585b70" points="315.94,-196.57 305.8,-193.51 311.82,-202.23 315.94,-196.57"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="396.25" y="-250.55" font-family="monospace" font-size="14.00" fill="#a6adc8">WirePacket</text>
|
||||||
|
</g>
|
||||||
|
<!-- keepalive->write -->
|
||||||
|
<g id="edge9" class="edge">
|
||||||
|
<title>keepalive->write</title>
|
||||||
|
<path fill="none" stroke="#585b70" d="M270,-237.09C270,-227.6 270,-215.44 270,-203.94"/>
|
||||||
|
<polygon fill="#585b70" stroke="#585b70" points="273.5,-204.3 270,-194.3 266.5,-204.3 273.5,-204.3"/>
|
||||||
|
</g>
|
||||||
|
<!-- write->net -->
|
||||||
|
<g id="edge10" class="edge">
|
||||||
|
<title>write->net</title>
|
||||||
|
<path fill="none" stroke="#585b70" d="M270,-140.16C270,-132.59 270,-123.93 270,-115.07"/>
|
||||||
|
<polygon fill="#585b70" stroke="#585b70" points="273.5,-115.32 270,-105.32 266.5,-115.32 273.5,-115.32"/>
|
||||||
|
</g>
|
||||||
|
<!-- capture->encoder -->
|
||||||
|
<g id="edge3" class="edge">
|
||||||
|
<title>capture->encoder</title>
|
||||||
|
<path fill="none" stroke="#585b70" d="M308,-812.48C308,-794.03 308,-773.39 308,-753.81"/>
|
||||||
|
<polygon fill="#585b70" stroke="#585b70" points="311.5,-753.94 308,-743.94 304.5,-753.94 311.5,-753.94"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="345.12" y="-781.44" font-family="monospace" font-size="14.00" fill="#a6adc8">AVFrame</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="345.12" y="-764.19" font-family="monospace" font-size="14.00" fill="#a6adc8">DRM_PRIME</text>
|
||||||
|
</g>
|
||||||
|
<!-- chan -->
|
||||||
|
<g id="node10" class="node">
|
||||||
|
<title>chan</title>
|
||||||
|
<polygon fill="#2d2038" stroke="#cba6f7" points="483.73,-533.71 193.05,-533.71 118.27,-430.15 408.95,-430.15 483.73,-533.71"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="301" y="-485.88" font-family="monospace" font-size="14.00" fill="#cdd6f4">mpsc::channel(64)</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="301" y="-468.63" font-family="monospace" font-size="14.00" fill="#cdd6f4">EncodedPacket</text>
|
||||||
|
</g>
|
||||||
|
<!-- encoder->chan -->
|
||||||
|
<g id="edge4" class="edge">
|
||||||
|
<title>encoder->chan</title>
|
||||||
|
<path fill="none" stroke="#585b70" d="M305.47,-603.89C304.77,-584.77 304,-564.09 303.31,-545.34"/>
|
||||||
|
<polygon fill="#585b70" stroke="#585b70" points="306.81,-545.31 302.94,-535.45 299.81,-545.57 306.81,-545.31"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="411.96" y="-572.91" font-family="monospace" font-size="14.00" fill="#a6adc8">EncodedPacket</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="411.96" y="-555.66" font-family="monospace" font-size="14.00" fill="#a6adc8">{ data, pts, keyframe, … }</text>
|
||||||
|
</g>
|
||||||
|
<!-- chan->mux -->
|
||||||
|
<g id="edge5" class="edge">
|
||||||
|
<title>chan->mux</title>
|
||||||
|
<path fill="none" stroke="#585b70" d="M301,-429.93C301,-413.88 301,-396.49 301,-381.64"/>
|
||||||
|
<polygon fill="#585b70" stroke="#585b70" points="304.5,-381.87 301,-371.87 297.5,-381.87 304.5,-381.87"/>
|
||||||
|
</g>
|
||||||
|
<!-- types -->
|
||||||
|
<g id="node11" class="node">
|
||||||
|
<title>types</title>
|
||||||
|
<polygon fill="#2a2a3e" stroke="#585b70" points="662.5,-1148.05 401.5,-1148.05 401.5,-1027.27 668.5,-1027.27 668.5,-1142.05 662.5,-1148.05"/>
|
||||||
|
<polyline fill="none" stroke="#585b70" points="662.5,-1148.05 662.5,-1142.05"/>
|
||||||
|
<polyline fill="none" stroke="#585b70" points="668.5,-1142.05 662.5,-1142.05"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="535" y="-1126.11" font-family="monospace" font-size="14.00" fill="#cdd6f4">EncodedPacket</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="535" y="-1108.86" font-family="monospace" font-size="14.00" fill="#cdd6f4">─────────────</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="535" y="-1091.61" font-family="monospace" font-size="14.00" fill="#cdd6f4">data: Vec<u8>  (H.264 NALUs)</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="535" y="-1074.36" font-family="monospace" font-size="14.00" fill="#cdd6f4">pts / dts: i64</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="535" y="-1057.11" font-family="monospace" font-size="14.00" fill="#cdd6f4">keyframe: bool</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="535" y="-1039.86" font-family="monospace" font-size="14.00" fill="#cdd6f4">time_base: num/den</text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 14 KiB |
51
media/docs/crates.dot
Normal file
51
media/docs/crates.dot
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
// Cargo workspace crate dependency graph
|
||||||
|
// Phase 2: client capture + encode implemented; server is a stub
|
||||||
|
digraph crates {
|
||||||
|
graph [fontname="monospace" bgcolor="#1e1e2e" pad="0.5"]
|
||||||
|
node [fontname="monospace" fontcolor="#cdd6f4" style=filled shape=box
|
||||||
|
fillcolor="#313244" color="#585b70" margin="0.2,0.1"]
|
||||||
|
edge [color="#585b70" fontname="monospace" fontcolor="#a6adc8"]
|
||||||
|
|
||||||
|
// External
|
||||||
|
ffmpeg_next [label="ffmpeg-next 8\n(ffmpeg-sys-next)" shape=component fillcolor="#1e3a2f" color="#a6e3a1"]
|
||||||
|
tokio [label="tokio 1\n(async runtime)" shape=component fillcolor="#1e2a3e" color="#89b4fa"]
|
||||||
|
serde [label="serde / serde_json" shape=component fillcolor="#2a2a3e" color="#cba6f7"]
|
||||||
|
tracing [label="tracing\ntracing-subscriber" shape=component fillcolor="#2a2a3e" color="#cba6f7"]
|
||||||
|
anyhow [label="anyhow" shape=component fillcolor="#2a2a3e" color="#cba6f7"]
|
||||||
|
|
||||||
|
// Workspace crates
|
||||||
|
common [label="cht-common\n─────────────\nprotocol.rs (wire framing)\nframe.rs (Frame, AudioBuffer)\nlogging.rs"
|
||||||
|
fillcolor="#2d2038" color="#cba6f7"]
|
||||||
|
|
||||||
|
client [label="cht-client [sender, Wayland]\n─────────────────────────────\ncapture.rs KMS/DRM → DRM_PRIME frames\nencoder.rs VAAPI H.264 (lazy init)\npipeline.rs capture→encode thread\nmain.rs TCP transport + keepalive"
|
||||||
|
fillcolor="#1e2d3e" color="#89b4fa"]
|
||||||
|
|
||||||
|
server [label="cht-server [receiver, mcrn]\n─────────────────────────────\nmain.rs TCP listener (stub)\n counts packets, no decode yet"
|
||||||
|
fillcolor="#2d1e1e" color="#f38ba8"]
|
||||||
|
|
||||||
|
// Deps
|
||||||
|
client -> common
|
||||||
|
server -> common
|
||||||
|
|
||||||
|
common -> serde
|
||||||
|
common -> tokio
|
||||||
|
common -> tracing
|
||||||
|
common -> anyhow
|
||||||
|
|
||||||
|
client -> ffmpeg_next
|
||||||
|
client -> tokio
|
||||||
|
client -> tracing
|
||||||
|
client -> anyhow
|
||||||
|
|
||||||
|
server -> tokio
|
||||||
|
server -> tracing
|
||||||
|
server -> anyhow
|
||||||
|
|
||||||
|
// Legend
|
||||||
|
subgraph cluster_legend {
|
||||||
|
label="Legend" fontcolor="#a6adc8" color="#585b70" fontname="monospace"
|
||||||
|
l1 [label="implemented" fillcolor="#1e2d3e" color="#89b4fa" shape=box]
|
||||||
|
l2 [label="stub / planned" fillcolor="#2d1e1e" color="#f38ba8" shape=box]
|
||||||
|
l3 [label="external crate" fillcolor="#1e3a2f" color="#a6e3a1" shape=component]
|
||||||
|
}
|
||||||
|
}
|
||||||
189
media/docs/crates.svg
Normal file
189
media/docs/crates.svg
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||||
|
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<!-- Generated by graphviz version 14.1.2 (0)
|
||||||
|
-->
|
||||||
|
<!-- Title: crates Pages: 1 -->
|
||||||
|
<svg width="1369pt" height="412pt"
|
||||||
|
viewBox="0.00 0.00 1369.00 412.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(36 375.75)">
|
||||||
|
<title>crates</title>
|
||||||
|
<polygon fill="#1e1e2e" stroke="none" points="-36,36 -36,-375.75 1333.17,-375.75 1333.17,36 -36,36"/>
|
||||||
|
<g id="clust1" class="cluster">
|
||||||
|
<title>cluster_legend</title>
|
||||||
|
<polygon fill="#1e1e2e" stroke="#585b70" points="829.17,-254.5 829.17,-331.75 1289.17,-331.75 1289.17,-254.5 829.17,-254.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1059.17" y="-314.45" font-family="monospace" font-size="14.00" fill="#a6adc8">Legend</text>
|
||||||
|
</g>
|
||||||
|
<!-- ffmpeg_next -->
|
||||||
|
<g id="node1" class="node">
|
||||||
|
<title>ffmpeg_next</title>
|
||||||
|
<polygon fill="#1e3a2f" stroke="#a6e3a1" points="196.7,-159.68 27.65,-159.68 27.65,-155.68 23.65,-155.68 23.65,-151.68 27.65,-151.68 27.65,-118.78 23.65,-118.78 23.65,-114.78 27.65,-114.78 27.65,-110.78 196.7,-110.78 196.7,-159.68"/>
|
||||||
|
<polyline fill="none" stroke="#a6e3a1" points="27.65,-155.68 31.65,-155.68 31.65,-151.68 27.65,-151.68"/>
|
||||||
|
<polyline fill="none" stroke="#a6e3a1" points="27.65,-118.78 31.65,-118.78 31.65,-114.78 27.65,-114.78"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="112.17" y="-139.18" font-family="monospace" font-size="14.00" fill="#cdd6f4">ffmpeg-next 8</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="112.17" y="-121.93" font-family="monospace" font-size="14.00" fill="#cdd6f4">(ffmpeg-sys-next)</text>
|
||||||
|
</g>
|
||||||
|
<!-- tokio -->
|
||||||
|
<g id="node2" class="node">
|
||||||
|
<title>tokio</title>
|
||||||
|
<polygon fill="#1e2a3e" stroke="#89b4fa" points="554.45,-48.9 401.9,-48.9 401.9,-44.9 397.9,-44.9 397.9,-40.9 401.9,-40.9 401.9,-8 397.9,-8 397.9,-4 401.9,-4 401.9,0 554.45,0 554.45,-48.9"/>
|
||||||
|
<polyline fill="none" stroke="#89b4fa" points="401.9,-44.9 405.9,-44.9 405.9,-40.9 401.9,-40.9"/>
|
||||||
|
<polyline fill="none" stroke="#89b4fa" points="401.9,-8 405.9,-8 405.9,-4 401.9,-4"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="478.17" y="-28.4" font-family="monospace" font-size="14.00" fill="#cdd6f4">tokio 1</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="478.17" y="-11.15" font-family="monospace" font-size="14.00" fill="#cdd6f4">(async runtime)</text>
|
||||||
|
</g>
|
||||||
|
<!-- serde -->
|
||||||
|
<g id="node3" class="node">
|
||||||
|
<title>serde</title>
|
||||||
|
<polygon fill="#2a2a3e" stroke="#cba6f7" points="383.82,-42.45 206.52,-42.45 206.52,-38.45 202.52,-38.45 202.52,-34.45 206.52,-34.45 206.52,-14.45 202.52,-14.45 202.52,-10.45 206.52,-10.45 206.52,-6.45 383.82,-6.45 383.82,-42.45"/>
|
||||||
|
<polyline fill="none" stroke="#cba6f7" points="206.52,-38.45 210.52,-38.45 210.52,-34.45 206.52,-34.45"/>
|
||||||
|
<polyline fill="none" stroke="#cba6f7" points="206.52,-14.45 210.52,-14.45 210.52,-10.45 206.52,-10.45"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="295.17" y="-19.78" font-family="monospace" font-size="14.00" fill="#cdd6f4">serde / serde_json</text>
|
||||||
|
</g>
|
||||||
|
<!-- tracing -->
|
||||||
|
<g id="node4" class="node">
|
||||||
|
<title>tracing</title>
|
||||||
|
<polygon fill="#2a2a3e" stroke="#cba6f7" points="749.82,-48.9 572.52,-48.9 572.52,-44.9 568.52,-44.9 568.52,-40.9 572.52,-40.9 572.52,-8 568.52,-8 568.52,-4 572.52,-4 572.52,0 749.82,0 749.82,-48.9"/>
|
||||||
|
<polyline fill="none" stroke="#cba6f7" points="572.52,-44.9 576.52,-44.9 576.52,-40.9 572.52,-40.9"/>
|
||||||
|
<polyline fill="none" stroke="#cba6f7" points="572.52,-8 576.52,-8 576.52,-4 572.52,-4"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="661.17" y="-28.4" font-family="monospace" font-size="14.00" fill="#cdd6f4">tracing</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="661.17" y="-11.15" font-family="monospace" font-size="14.00" fill="#cdd6f4">tracing-subscriber</text>
|
||||||
|
</g>
|
||||||
|
<!-- anyhow -->
|
||||||
|
<g id="node5" class="node">
|
||||||
|
<title>anyhow</title>
|
||||||
|
<polygon fill="#2a2a3e" stroke="#cba6f7" points="188.32,-42.45 110.02,-42.45 110.02,-38.45 106.02,-38.45 106.02,-34.45 110.02,-34.45 110.02,-14.45 106.02,-14.45 106.02,-10.45 110.02,-10.45 110.02,-6.45 188.32,-6.45 188.32,-42.45"/>
|
||||||
|
<polyline fill="none" stroke="#cba6f7" points="110.02,-38.45 114.02,-38.45 114.02,-34.45 110.02,-34.45"/>
|
||||||
|
<polyline fill="none" stroke="#cba6f7" points="110.02,-14.45 114.02,-14.45 114.02,-10.45 110.02,-10.45"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="149.17" y="-19.78" font-family="monospace" font-size="14.00" fill="#cdd6f4">anyhow</text>
|
||||||
|
</g>
|
||||||
|
<!-- common -->
|
||||||
|
<g id="node6" class="node">
|
||||||
|
<title>common</title>
|
||||||
|
<polygon fill="#2d2038" stroke="#cba6f7" points="592.7,-185.55 291.65,-185.55 291.65,-84.9 592.7,-84.9 592.7,-185.55"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="442.17" y="-165.05" font-family="monospace" font-size="14.00" fill="#cdd6f4">cht-common</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="442.17" y="-147.8" font-family="monospace" font-size="14.00" fill="#cdd6f4">─────────────</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="442.17" y="-130.55" font-family="monospace" font-size="14.00" fill="#cdd6f4">protocol.rs  (wire framing)</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="442.17" y="-113.3" font-family="monospace" font-size="14.00" fill="#cdd6f4">frame.rs     (Frame, AudioBuffer)</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="442.17" y="-96.05" font-family="monospace" font-size="14.00" fill="#cdd6f4">logging.rs</text>
|
||||||
|
</g>
|
||||||
|
<!-- common->tokio -->
|
||||||
|
<g id="edge4" class="edge">
|
||||||
|
<title>common->tokio</title>
|
||||||
|
<path fill="none" stroke="#585b70" d="M458.59,-84.61C461.33,-76.33 464.12,-67.9 466.71,-60.1"/>
|
||||||
|
<polygon fill="#585b70" stroke="#585b70" points="470.02,-61.23 469.84,-50.64 463.37,-59.03 470.02,-61.23"/>
|
||||||
|
</g>
|
||||||
|
<!-- common->serde -->
|
||||||
|
<g id="edge3" class="edge">
|
||||||
|
<title>common->serde</title>
|
||||||
|
<path fill="none" stroke="#585b70" d="M375.12,-84.61C358.64,-72.41 341.71,-59.89 327.7,-49.52"/>
|
||||||
|
<polygon fill="#585b70" stroke="#585b70" points="329.85,-46.76 319.73,-43.62 325.69,-52.38 329.85,-46.76"/>
|
||||||
|
</g>
|
||||||
|
<!-- common->tracing -->
|
||||||
|
<g id="edge5" class="edge">
|
||||||
|
<title>common->tracing</title>
|
||||||
|
<path fill="none" stroke="#585b70" d="M542.38,-84.45C563.19,-74.12 584.45,-63.56 603.13,-54.28"/>
|
||||||
|
<polygon fill="#585b70" stroke="#585b70" points="604.47,-57.52 611.87,-49.94 601.35,-51.25 604.47,-57.52"/>
|
||||||
|
</g>
|
||||||
|
<!-- common->anyhow -->
|
||||||
|
<g id="edge6" class="edge">
|
||||||
|
<title>common->anyhow</title>
|
||||||
|
<path fill="none" stroke="#585b70" d="M291.45,-84.91C260.03,-73.75 227.31,-61.46 197.17,-48.9 196.06,-48.44 194.94,-47.96 193.8,-47.47"/>
|
||||||
|
<polygon fill="#585b70" stroke="#585b70" points="195.49,-44.39 184.93,-43.48 192.62,-50.77 195.49,-44.39"/>
|
||||||
|
</g>
|
||||||
|
<!-- client -->
|
||||||
|
<g id="node7" class="node">
|
||||||
|
<title>client</title>
|
||||||
|
<polygon fill="#1e2d3e" stroke="#89b4fa" points="400.45,-339.45 49.9,-339.45 49.9,-221.55 400.45,-221.55 400.45,-339.45"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="225.17" y="-318.95" font-family="monospace" font-size="14.00" fill="#cdd6f4">cht-client  [sender, Wayland]</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="225.17" y="-301.7" font-family="monospace" font-size="14.00" fill="#cdd6f4">─────────────────────────────</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="225.17" y="-284.45" font-family="monospace" font-size="14.00" fill="#cdd6f4">capture.rs   KMS/DRM → DRM_PRIME frames</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="225.17" y="-267.2" font-family="monospace" font-size="14.00" fill="#cdd6f4">encoder.rs   VAAPI H.264 (lazy init)</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="225.17" y="-249.95" font-family="monospace" font-size="14.00" fill="#cdd6f4">pipeline.rs  capture→encode thread</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="225.17" y="-232.7" font-family="monospace" font-size="14.00" fill="#cdd6f4">main.rs      TCP transport + keepalive</text>
|
||||||
|
</g>
|
||||||
|
<!-- client->ffmpeg_next -->
|
||||||
|
<g id="edge7" class="edge">
|
||||||
|
<title>client->ffmpeg_next</title>
|
||||||
|
<path fill="none" stroke="#585b70" d="M179.21,-221.22C165.11,-203.35 150.11,-184.32 137.88,-168.82"/>
|
||||||
|
<polygon fill="#585b70" stroke="#585b70" points="140.9,-167 131.96,-161.31 135.4,-171.33 140.9,-167"/>
|
||||||
|
</g>
|
||||||
|
<!-- client->tokio -->
|
||||||
|
<g id="edge8" class="edge">
|
||||||
|
<title>client->tokio</title>
|
||||||
|
<path fill="none" stroke="#585b70" d="M227.82,-221.48C232.83,-178.38 246.37,-121.04 282.17,-84.9 286.38,-80.65 340.37,-64.43 390.61,-50"/>
|
||||||
|
<polygon fill="#585b70" stroke="#585b70" points="391.48,-53.39 400.13,-47.28 389.56,-46.66 391.48,-53.39"/>
|
||||||
|
</g>
|
||||||
|
<!-- client->tracing -->
|
||||||
|
<g id="edge9" class="edge">
|
||||||
|
<title>client->tracing</title>
|
||||||
|
<path fill="none" stroke="#585b70" d="M400.86,-230.22C412.46,-227.22 423.97,-224.3 435.17,-221.55 508.91,-203.44 544.84,-235.33 602.17,-185.55 638.8,-153.74 652.71,-97.11 657.98,-60.43"/>
|
||||||
|
<polygon fill="#585b70" stroke="#585b70" points="661.42,-61.14 659.21,-50.78 654.47,-60.26 661.42,-61.14"/>
|
||||||
|
</g>
|
||||||
|
<!-- client->anyhow -->
|
||||||
|
<g id="edge10" class="edge">
|
||||||
|
<title>client->anyhow</title>
|
||||||
|
<path fill="none" stroke="#585b70" d="M51.41,-221.15C38.48,-211.09 27.04,-199.32 18.17,-185.55 -6.06,-147.95 -6.06,-122.5 18.17,-84.9 35.92,-57.35 69.9,-42.42 98.8,-34.42"/>
|
||||||
|
<polygon fill="#585b70" stroke="#585b70" points="99.29,-37.9 108.12,-32.05 97.57,-31.12 99.29,-37.9"/>
|
||||||
|
</g>
|
||||||
|
<!-- client->common -->
|
||||||
|
<g id="edge1" class="edge">
|
||||||
|
<title>client->common</title>
|
||||||
|
<path fill="none" stroke="#585b70" d="M313.43,-221.22C327.97,-211.63 343.01,-201.7 357.45,-192.16"/>
|
||||||
|
<polygon fill="#585b70" stroke="#585b70" points="359.17,-195.22 365.58,-186.79 355.31,-189.38 359.17,-195.22"/>
|
||||||
|
</g>
|
||||||
|
<!-- server -->
|
||||||
|
<g id="node8" class="node">
|
||||||
|
<title>server</title>
|
||||||
|
<polygon fill="#2d1e1e" stroke="#f38ba8" points="819.82,-322.2 444.52,-322.2 444.52,-238.8 819.82,-238.8 819.82,-322.2"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="632.17" y="-301.7" font-family="monospace" font-size="14.00" fill="#cdd6f4">cht-server  [receiver, mcrn]</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="632.17" y="-284.45" font-family="monospace" font-size="14.00" fill="#cdd6f4">─────────────────────────────</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="632.17" y="-267.2" font-family="monospace" font-size="14.00" fill="#cdd6f4">main.rs      TCP listener (stub)</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="632.17" y="-249.95" font-family="monospace" font-size="14.00" fill="#cdd6f4">             counts packets, no decode yet</text>
|
||||||
|
</g>
|
||||||
|
<!-- server->tokio -->
|
||||||
|
<g id="edge11" class="edge">
|
||||||
|
<title>server->tokio</title>
|
||||||
|
<path fill="none" stroke="#585b70" d="M636.34,-238.44C638.44,-195.9 635.27,-129.51 602.17,-84.9 592.46,-71.81 579.12,-61.48 564.76,-53.36"/>
|
||||||
|
<polygon fill="#585b70" stroke="#585b70" points="566.52,-50.33 556.04,-48.79 563.27,-56.53 566.52,-50.33"/>
|
||||||
|
</g>
|
||||||
|
<!-- server->tracing -->
|
||||||
|
<g id="edge12" class="edge">
|
||||||
|
<title>server->tracing</title>
|
||||||
|
<path fill="none" stroke="#585b70" d="M654.37,-238.63C661.92,-222.57 669.39,-203.67 673.17,-185.55 682.02,-143.14 675.7,-93.22 669.37,-60.54"/>
|
||||||
|
<polygon fill="#585b70" stroke="#585b70" points="672.81,-59.89 667.37,-50.8 665.96,-61.3 672.81,-59.89"/>
|
||||||
|
</g>
|
||||||
|
<!-- server->anyhow -->
|
||||||
|
<g id="edge13" class="edge">
|
||||||
|
<title>server->anyhow</title>
|
||||||
|
<path fill="none" stroke="#585b70" d="M474.46,-238.36C383.66,-214.48 286.57,-188.52 282.17,-185.55 235.69,-154.22 242.52,-127.57 206.17,-84.9 196.39,-73.42 185,-61.31 174.98,-51.03"/>
|
||||||
|
<polygon fill="#585b70" stroke="#585b70" points="177.52,-48.63 168.01,-43.97 172.54,-53.55 177.52,-48.63"/>
|
||||||
|
</g>
|
||||||
|
<!-- server->common -->
|
||||||
|
<g id="edge2" class="edge">
|
||||||
|
<title>server->common</title>
|
||||||
|
<path fill="none" stroke="#585b70" d="M577.73,-238.44C558.9,-224.24 537.45,-208.07 517.26,-192.85"/>
|
||||||
|
<polygon fill="#585b70" stroke="#585b70" points="519.39,-190.07 509.3,-186.84 515.17,-195.66 519.39,-190.07"/>
|
||||||
|
</g>
|
||||||
|
<!-- l1 -->
|
||||||
|
<g id="node9" class="node">
|
||||||
|
<title>l1</title>
|
||||||
|
<polygon fill="#1e2d3e" stroke="#89b4fa" points="956.95,-298.5 837.4,-298.5 837.4,-262.5 956.95,-262.5 956.95,-298.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="897.17" y="-275.82" font-family="monospace" font-size="14.00" fill="#cdd6f4">implemented</text>
|
||||||
|
</g>
|
||||||
|
<!-- l2 -->
|
||||||
|
<g id="node10" class="node">
|
||||||
|
<title>l2</title>
|
||||||
|
<polygon fill="#2d1e1e" stroke="#f38ba8" points="1119.32,-298.5 975.02,-298.5 975.02,-262.5 1119.32,-262.5 1119.32,-298.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1047.17" y="-275.82" font-family="monospace" font-size="14.00" fill="#cdd6f4">stub / planned</text>
|
||||||
|
</g>
|
||||||
|
<!-- l3 -->
|
||||||
|
<g id="node11" class="node">
|
||||||
|
<title>l3</title>
|
||||||
|
<polygon fill="#1e3a2f" stroke="#a6e3a1" points="1281.32,-298.5 1137.02,-298.5 1137.02,-294.5 1133.02,-294.5 1133.02,-290.5 1137.02,-290.5 1137.02,-270.5 1133.02,-270.5 1133.02,-266.5 1137.02,-266.5 1137.02,-262.5 1281.32,-262.5 1281.32,-298.5"/>
|
||||||
|
<polyline fill="none" stroke="#a6e3a1" points="1137.02,-294.5 1141.02,-294.5 1141.02,-290.5 1137.02,-290.5"/>
|
||||||
|
<polyline fill="none" stroke="#a6e3a1" points="1137.02,-270.5 1141.02,-270.5 1141.02,-266.5 1137.02,-266.5"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1209.17" y="-275.82" font-family="monospace" font-size="14.00" fill="#cdd6f4">external crate</text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 13 KiB |
168
media/docs/index.html
Normal file
168
media/docs/index.html
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Media Transport — Architecture</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
height: 100vh;
|
||||||
|
font-family: monospace;
|
||||||
|
background: #1e1e2e;
|
||||||
|
color: #cdd6f4;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav {
|
||||||
|
width: 220px;
|
||||||
|
min-width: 220px;
|
||||||
|
background: #181825;
|
||||||
|
border-right: 1px solid #313244;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav h1 {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
color: #6c7086;
|
||||||
|
padding: 0 1rem 0.75rem;
|
||||||
|
border-bottom: 1px solid #313244;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav a {
|
||||||
|
display: block;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
color: #cdd6f4;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
border-left: 3px solid transparent;
|
||||||
|
transition: background 0.1s, border-color 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav a:hover { background: #313244; }
|
||||||
|
nav a.active { border-left-color: #89b4fa; color: #89b4fa; background: #1e2d3e; }
|
||||||
|
|
||||||
|
nav .subtitle {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: #6c7086;
|
||||||
|
padding: 0 1rem;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav .phase-badge {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
color: #a6e3a1;
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav .section {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: #6c7086;
|
||||||
|
padding: 1rem 1rem 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
padding: 0.75rem 1.25rem;
|
||||||
|
background: #181825;
|
||||||
|
border-bottom: 1px solid #313244;
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
header h2 { font-size: 0.95rem; }
|
||||||
|
header .desc { font-size: 0.75rem; color: #6c7086; }
|
||||||
|
|
||||||
|
.viewer {
|
||||||
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
background: #1e1e2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewer object,
|
||||||
|
.viewer img {
|
||||||
|
max-width: 100%;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 4px 24px rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder {
|
||||||
|
color: #6c7086;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-top: 4rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<nav>
|
||||||
|
<h1>Media Transport</h1>
|
||||||
|
|
||||||
|
<div class="section">Workspace</div>
|
||||||
|
<a href="#" data-svg="crates.svg" data-title="Crate Dependency Graph" data-desc="Workspace members, external deps, what's implemented vs stubbed">
|
||||||
|
Crate graph <span class="phase-badge">phase 2</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="section">Client (sender)</div>
|
||||||
|
<a href="#" data-svg="client-pipeline.svg" data-title="Client Pipeline" data-desc="KMS capture → VAAPI encode → TCP transport">
|
||||||
|
Pipeline <span class="phase-badge">phase 2</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="section">Server (receiver)</div>
|
||||||
|
<a href="#" data-svg="server-pipeline.svg" data-title="Server Pipeline" data-desc="Current stub + planned: NVDEC, scene detection, IPC, frame buffer">
|
||||||
|
Pipeline <span class="phase-badge">phase 2 stub</span>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<header>
|
||||||
|
<h2 id="title">Select a diagram</h2>
|
||||||
|
<span class="desc" id="desc"></span>
|
||||||
|
</header>
|
||||||
|
<div class="viewer" id="viewer">
|
||||||
|
<p class="placeholder">← pick a diagram from the sidebar</p>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const viewer = document.getElementById('viewer');
|
||||||
|
const titleEl = document.getElementById('title');
|
||||||
|
const descEl = document.getElementById('desc');
|
||||||
|
|
||||||
|
document.querySelectorAll('nav a').forEach(link => {
|
||||||
|
link.addEventListener('click', e => {
|
||||||
|
e.preventDefault();
|
||||||
|
document.querySelectorAll('nav a').forEach(l => l.classList.remove('active'));
|
||||||
|
link.classList.add('active');
|
||||||
|
|
||||||
|
titleEl.textContent = link.dataset.title;
|
||||||
|
descEl.textContent = link.dataset.desc;
|
||||||
|
|
||||||
|
// Use <object> so SVG internal text/links work
|
||||||
|
viewer.innerHTML = `<object type="image/svg+xml" data="${link.dataset.svg}"></object>`;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-select first
|
||||||
|
document.querySelector('nav a').click();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
54
media/docs/server-pipeline.dot
Normal file
54
media/docs/server-pipeline.dot
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
// Server pipeline — Phase 2 (stub) + planned architecture
|
||||||
|
// Receiver machine (X11, RTX 3080, NVDEC)
|
||||||
|
digraph server_pipeline {
|
||||||
|
graph [fontname="monospace" bgcolor="#1e1e2e" rankdir=TB pad="0.6" splines=polyline]
|
||||||
|
node [fontname="monospace" fontcolor="#cdd6f4" style=filled shape=box
|
||||||
|
fillcolor="#313244" color="#585b70" margin="0.25,0.12"]
|
||||||
|
edge [color="#585b70" fontname="monospace" fontcolor="#a6adc8"]
|
||||||
|
|
||||||
|
net [label="TCP :4444" shape=parallelogram fillcolor="#1e2a3e" color="#89b4fa"]
|
||||||
|
python [label="Python app\n(stream/manager.py)" shape=parallelogram fillcolor="#2a2a3e" color="#cba6f7"]
|
||||||
|
|
||||||
|
subgraph cluster_implemented {
|
||||||
|
label="Implemented (Phase 2)" fontcolor="#a6e3a1" color="#a6e3a1" fontname="monospace"
|
||||||
|
|
||||||
|
listener [label="Listener\n─────────────\nTCP accept loop\nspawns task per client\nreads WirePacket headers\ncounts video/audio pkts\nlogs keyframes + ts"
|
||||||
|
fillcolor="#1e2d3e" color="#89b4fa"]
|
||||||
|
}
|
||||||
|
|
||||||
|
subgraph cluster_planned {
|
||||||
|
label="Planned" fontcolor="#f38ba8" color="#f38ba8" fontname="monospace" style=dashed
|
||||||
|
|
||||||
|
decoder [label="Decoder (Phase 3)\n─────────────\nNVDEC H.264 → NV12\nGPU frames" fillcolor="#2d1e1e" color="#f38ba8"]
|
||||||
|
scene [label="Scene Detector (Phase 3)\n─────────────\nffmpeg select filter\nin-process (no subprocess)\nJPEG → frames/\nframes/index.json" fillcolor="#2d1e1e" color="#f38ba8"]
|
||||||
|
audio [label="Audio Extractor (Phase 4)\n─────────────\nAAC decode\nWAV chunks → audio/" fillcolor="#2d1e1e" color="#f38ba8"]
|
||||||
|
writer [label="Segment Writer (Phase 3)\n─────────────\nfMP4 segments → stream/\nkeyframe boundaries" fillcolor="#2d1e1e" color="#f38ba8"]
|
||||||
|
framebuf [label="Frame Buffer (Phase 6)\n─────────────\nGPU ring buffer ~300 frames\nscrub: GPU→CPU on demand\n→ /dev/shm/cht_scrub_frame" fillcolor="#2d1e1e" color="#f38ba8"]
|
||||||
|
ipc [label="IPC Server (Phase 5)\n─────────────\nUnix socket JSON-lines\ncommands: start/stop/get_frame\nevents: frame_detected/audio_chunk/…" fillcolor="#2d1e1e" color="#f38ba8"]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flow — implemented
|
||||||
|
net -> listener [label="WirePacket"]
|
||||||
|
|
||||||
|
// Flow — planned
|
||||||
|
listener -> decoder [style=dashed label="H.264 payload"]
|
||||||
|
decoder -> scene [style=dashed label="NV12 frame"]
|
||||||
|
decoder -> writer [style=dashed label="encoded pkt"]
|
||||||
|
decoder -> framebuf [style=dashed label="GPU frame"]
|
||||||
|
decoder -> audio [style=dashed label="audio pkt"]
|
||||||
|
scene -> ipc [style=dashed label="frame_detected"]
|
||||||
|
audio -> ipc [style=dashed label="audio_chunk"]
|
||||||
|
writer -> ipc [style=dashed label="segment_completed"]
|
||||||
|
ipc -> python [style=dashed label="JSON-lines\n(Unix socket)"]
|
||||||
|
|
||||||
|
// Outputs
|
||||||
|
frames_dir [label="frames/\nindex.json + *.jpg" shape=folder fillcolor="#2a2a3e" color="#585b70"]
|
||||||
|
audio_dir [label="audio/\n*.wav chunks" shape=folder fillcolor="#2a2a3e" color="#585b70"]
|
||||||
|
stream_dir [label="stream/\n*.mp4 segments" shape=folder fillcolor="#2a2a3e" color="#585b70"]
|
||||||
|
shm [label="/dev/shm/cht_scrub_frame\nraw RGBA pixels" shape=folder fillcolor="#2a2a3e" color="#585b70"]
|
||||||
|
|
||||||
|
scene -> frames_dir [style=dashed]
|
||||||
|
audio -> audio_dir [style=dashed]
|
||||||
|
writer -> stream_dir [style=dashed]
|
||||||
|
framebuf -> shm [style=dashed label="get_frame cmd"]
|
||||||
|
}
|
||||||
230
media/docs/server-pipeline.svg
Normal file
230
media/docs/server-pipeline.svg
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||||
|
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<!-- Generated by graphviz version 14.1.2 (0)
|
||||||
|
-->
|
||||||
|
<!-- Title: server_pipeline Pages: 1 -->
|
||||||
|
<svg width="1933pt" height="1038pt"
|
||||||
|
viewBox="0.00 0.00 1933.00 1038.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(43.2 994.44)">
|
||||||
|
<title>server_pipeline</title>
|
||||||
|
<polygon fill="#1e1e2e" stroke="none" points="-43.2,43.2 -43.2,-994.44 1890.2,-994.44 1890.2,43.2 -43.2,43.2"/>
|
||||||
|
<g id="clust1" class="cluster">
|
||||||
|
<title>cluster_implemented</title>
|
||||||
|
<polygon fill="#1e1e2e" stroke="#a6e3a1" points="417,-659.65 417,-838.93 667,-838.93 667,-659.65 417,-659.65"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="542" y="-821.63" font-family="monospace" font-size="14.00" fill="#a6e3a1">Implemented (Phase 2)</text>
|
||||||
|
</g>
|
||||||
|
<g id="clust2" class="cluster">
|
||||||
|
<title>cluster_planned</title>
|
||||||
|
<polygon fill="#1e1e2e" stroke="#f38ba8" stroke-dasharray="5,2" points="8,-166.06 8,-624.4 1080,-624.4 1080,-166.06 8,-166.06"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="544" y="-607.1" font-family="monospace" font-size="14.00" fill="#f38ba8">Planned</text>
|
||||||
|
</g>
|
||||||
|
<!-- net -->
|
||||||
|
<g id="node1" class="node">
|
||||||
|
<title>net</title>
|
||||||
|
<polygon fill="#1e2a3e" stroke="#89b4fa" points="656.3,-951.24 474.47,-951.24 427.7,-882.18 609.53,-882.18 656.3,-951.24"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="542" y="-912.03" font-family="monospace" font-size="14.00" fill="#cdd6f4">TCP :4444</text>
|
||||||
|
</g>
|
||||||
|
<!-- listener -->
|
||||||
|
<g id="node3" class="node">
|
||||||
|
<title>listener</title>
|
||||||
|
<polygon fill="#1e2d3e" stroke="#89b4fa" points="659,-805.68 425,-805.68 425,-667.65 659,-667.65 659,-805.68"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="542" y="-783.74" font-family="monospace" font-size="14.00" fill="#cdd6f4">Listener</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="542" y="-766.49" font-family="monospace" font-size="14.00" fill="#cdd6f4">─────────────</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="542" y="-749.24" font-family="monospace" font-size="14.00" fill="#cdd6f4">TCP accept loop</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="542" y="-731.99" font-family="monospace" font-size="14.00" fill="#cdd6f4">spawns task per client</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="542" y="-714.74" font-family="monospace" font-size="14.00" fill="#cdd6f4">reads WirePacket headers</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="542" y="-697.49" font-family="monospace" font-size="14.00" fill="#cdd6f4">counts video/audio pkts</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="542" y="-680.24" font-family="monospace" font-size="14.00" fill="#cdd6f4">logs keyframes + ts</text>
|
||||||
|
</g>
|
||||||
|
<!-- net->listener -->
|
||||||
|
<g id="edge1" class="edge">
|
||||||
|
<title>net->listener</title>
|
||||||
|
<path fill="none" stroke="#585b70" d="M542,-881.8C542,-863.5 542,-840.08 542,-817.47"/>
|
||||||
|
<polygon fill="#585b70" stroke="#585b70" points="545.5,-817.61 542,-807.61 538.5,-817.61 545.5,-817.61"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="583.25" y="-850.88" font-family="monospace" font-size="14.00" fill="#a6adc8">WirePacket</text>
|
||||||
|
</g>
|
||||||
|
<!-- python -->
|
||||||
|
<g id="node2" class="node">
|
||||||
|
<title>python</title>
|
||||||
|
<polygon fill="#2a2a3e" stroke="#cba6f7" points="609.83,-103.56 291.94,-103.56 210.17,0 528.06,0 609.83,-103.56"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="410" y="-55.73" font-family="monospace" font-size="14.00" fill="#cdd6f4">Python app</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="410" y="-38.48" font-family="monospace" font-size="14.00" fill="#cdd6f4">(stream/manager.py)</text>
|
||||||
|
</g>
|
||||||
|
<!-- decoder -->
|
||||||
|
<g id="node4" class="node">
|
||||||
|
<title>decoder</title>
|
||||||
|
<polygon fill="#2d1e1e" stroke="#f38ba8" points="634.25,-591.15 449.75,-591.15 449.75,-504.87 634.25,-504.87 634.25,-591.15"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="542" y="-569.21" font-family="monospace" font-size="14.00" fill="#cdd6f4">Decoder  (Phase 3)</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="542" y="-551.96" font-family="monospace" font-size="14.00" fill="#cdd6f4">─────────────</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="542" y="-534.71" font-family="monospace" font-size="14.00" fill="#cdd6f4">NVDEC H.264 → NV12</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="542" y="-517.46" font-family="monospace" font-size="14.00" fill="#cdd6f4">GPU frames</text>
|
||||||
|
</g>
|
||||||
|
<!-- listener->decoder -->
|
||||||
|
<g id="edge2" class="edge">
|
||||||
|
<title>listener->decoder</title>
|
||||||
|
<path fill="none" stroke="#585b70" stroke-dasharray="5,2" d="M542,-667.22C542,-646 542,-622.93 542,-602.84"/>
|
||||||
|
<polygon fill="#585b70" stroke="#585b70" points="545.5,-602.91 542,-592.91 538.5,-602.91 545.5,-602.91"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="595.62" y="-636.35" font-family="monospace" font-size="14.00" fill="#a6adc8">H.264 payload</text>
|
||||||
|
</g>
|
||||||
|
<!-- scene -->
|
||||||
|
<g id="node5" class="node">
|
||||||
|
<title>scene</title>
|
||||||
|
<polygon fill="#2d1e1e" stroke="#f38ba8" points="266.25,-451.62 15.75,-451.62 15.75,-330.84 266.25,-330.84 266.25,-451.62"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="141" y="-429.68" font-family="monospace" font-size="14.00" fill="#cdd6f4">Scene Detector  (Phase 3)</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="141" y="-412.43" font-family="monospace" font-size="14.00" fill="#cdd6f4">─────────────</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="141" y="-395.18" font-family="monospace" font-size="14.00" fill="#cdd6f4">ffmpeg select filter</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="141" y="-377.93" font-family="monospace" font-size="14.00" fill="#cdd6f4">in-process (no subprocess)</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="141" y="-360.68" font-family="monospace" font-size="14.00" fill="#cdd6f4">JPEG → frames/</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="141" y="-343.43" font-family="monospace" font-size="14.00" fill="#cdd6f4">frames/index.json</text>
|
||||||
|
</g>
|
||||||
|
<!-- decoder->scene -->
|
||||||
|
<g id="edge3" class="edge">
|
||||||
|
<title>decoder->scene</title>
|
||||||
|
<path fill="none" stroke="#585b70" stroke-dasharray="5,2" d="M449.34,-513.78C384.87,-490.66 306.43,-462.53 277.46,-451.7"/>
|
||||||
|
<polygon fill="#585b70" stroke="#585b70" points="278.72,-448.43 268.13,-448.08 276.19,-454.96 278.72,-448.43"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="410.88" y="-473.57" font-family="monospace" font-size="14.00" fill="#a6adc8">NV12 frame</text>
|
||||||
|
</g>
|
||||||
|
<!-- audio -->
|
||||||
|
<g id="node6" class="node">
|
||||||
|
<title>audio</title>
|
||||||
|
<polygon fill="#2d1e1e" stroke="#f38ba8" points="535.25,-434.37 284.75,-434.37 284.75,-348.09 535.25,-348.09 535.25,-434.37"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="410" y="-412.43" font-family="monospace" font-size="14.00" fill="#cdd6f4">Audio Extractor  (Phase 4)</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="410" y="-395.18" font-family="monospace" font-size="14.00" fill="#cdd6f4">─────────────</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="410" y="-377.93" font-family="monospace" font-size="14.00" fill="#cdd6f4">AAC decode</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="410" y="-360.68" font-family="monospace" font-size="14.00" fill="#cdd6f4">WAV chunks → audio/</text>
|
||||||
|
</g>
|
||||||
|
<!-- decoder->audio -->
|
||||||
|
<g id="edge6" class="edge">
|
||||||
|
<title>decoder->audio</title>
|
||||||
|
<path fill="none" stroke="#585b70" stroke-dasharray="5,2" d="M505.93,-504.72C489.77,-485.77 470.58,-463.26 453.62,-443.38"/>
|
||||||
|
<polygon fill="#585b70" stroke="#585b70" points="456.39,-441.23 447.24,-435.89 451.06,-445.77 456.39,-441.23"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="524.95" y="-473.57" font-family="monospace" font-size="14.00" fill="#a6adc8">audio pkt</text>
|
||||||
|
</g>
|
||||||
|
<!-- writer -->
|
||||||
|
<g id="node7" class="node">
|
||||||
|
<title>writer</title>
|
||||||
|
<polygon fill="#2d1e1e" stroke="#f38ba8" points="795.12,-434.37 552.88,-434.37 552.88,-348.09 795.12,-348.09 795.12,-434.37"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="674" y="-412.43" font-family="monospace" font-size="14.00" fill="#cdd6f4">Segment Writer  (Phase 3)</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="674" y="-395.18" font-family="monospace" font-size="14.00" fill="#cdd6f4">─────────────</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="674" y="-377.93" font-family="monospace" font-size="14.00" fill="#cdd6f4">fMP4 segments → stream/</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="674" y="-360.68" font-family="monospace" font-size="14.00" fill="#cdd6f4">keyframe boundaries</text>
|
||||||
|
</g>
|
||||||
|
<!-- decoder->writer -->
|
||||||
|
<g id="edge4" class="edge">
|
||||||
|
<title>decoder->writer</title>
|
||||||
|
<path fill="none" stroke="#585b70" stroke-dasharray="5,2" d="M578.07,-504.72C594.23,-485.77 613.42,-463.26 630.38,-443.38"/>
|
||||||
|
<polygon fill="#585b70" stroke="#585b70" points="632.94,-445.77 636.76,-435.89 627.61,-441.23 632.94,-445.77"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="653.38" y="-473.57" font-family="monospace" font-size="14.00" fill="#a6adc8">encoded pkt</text>
|
||||||
|
</g>
|
||||||
|
<!-- framebuf -->
|
||||||
|
<g id="node8" class="node">
|
||||||
|
<title>framebuf</title>
|
||||||
|
<polygon fill="#2d1e1e" stroke="#f38ba8" points="1072.38,-442.99 813.62,-442.99 813.62,-339.46 1072.38,-339.46 1072.38,-442.99"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="943" y="-421.05" font-family="monospace" font-size="14.00" fill="#cdd6f4">Frame Buffer  (Phase 6)</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="943" y="-403.8" font-family="monospace" font-size="14.00" fill="#cdd6f4">─────────────</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="943" y="-386.55" font-family="monospace" font-size="14.00" fill="#cdd6f4">GPU ring buffer ~300 frames</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="943" y="-369.3" font-family="monospace" font-size="14.00" fill="#cdd6f4">scrub: GPU→CPU on demand</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="943" y="-352.05" font-family="monospace" font-size="14.00" fill="#cdd6f4">→ /dev/shm/cht_scrub_frame</text>
|
||||||
|
</g>
|
||||||
|
<!-- decoder->framebuf -->
|
||||||
|
<g id="edge5" class="edge">
|
||||||
|
<title>decoder->framebuf</title>
|
||||||
|
<path fill="none" stroke="#585b70" stroke-dasharray="5,2" d="M634.74,-513.24C710.11,-485.8 804,-451.62 804,-451.62 804,-451.62 807.23,-450.24 812.71,-447.9"/>
|
||||||
|
<polygon fill="#585b70" stroke="#585b70" points="813.82,-451.23 821.64,-444.08 811.07,-444.79 813.82,-451.23"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="791.01" y="-473.57" font-family="monospace" font-size="14.00" fill="#a6adc8">GPU frame</text>
|
||||||
|
</g>
|
||||||
|
<!-- ipc -->
|
||||||
|
<g id="node9" class="node">
|
||||||
|
<title>ipc</title>
|
||||||
|
<polygon fill="#2d1e1e" stroke="#f38ba8" points="576.5,-277.59 243.5,-277.59 243.5,-174.06 576.5,-174.06 576.5,-277.59"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="410" y="-255.65" font-family="monospace" font-size="14.00" fill="#cdd6f4">IPC Server  (Phase 5)</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="410" y="-238.4" font-family="monospace" font-size="14.00" fill="#cdd6f4">─────────────</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="410" y="-221.15" font-family="monospace" font-size="14.00" fill="#cdd6f4">Unix socket JSON-lines</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="410" y="-203.9" font-family="monospace" font-size="14.00" fill="#cdd6f4">commands: start/stop/get_frame</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="410" y="-186.65" font-family="monospace" font-size="14.00" fill="#cdd6f4">events: frame_detected/audio_chunk/…</text>
|
||||||
|
</g>
|
||||||
|
<!-- scene->ipc -->
|
||||||
|
<g id="edge7" class="edge">
|
||||||
|
<title>scene->ipc</title>
|
||||||
|
<path fill="none" stroke="#585b70" stroke-dasharray="5,2" d="M235.26,-330.56C265.11,-311.66 290.5,-295.59 290.5,-295.59 290.5,-295.59 298.93,-290.74 311.31,-283.61"/>
|
||||||
|
<polygon fill="#585b70" stroke="#585b70" points="312.73,-286.84 319.65,-278.81 309.24,-280.77 312.73,-286.84"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="348.25" y="-299.54" font-family="monospace" font-size="14.00" fill="#a6adc8">frame_detected</text>
|
||||||
|
</g>
|
||||||
|
<!-- frames_dir -->
|
||||||
|
<g id="node10" class="node">
|
||||||
|
<title>frames_dir</title>
|
||||||
|
<polygon fill="#2a2a3e" stroke="#585b70" points="1272.25,-251.72 1269.25,-255.72 1248.25,-255.72 1245.25,-251.72 1087.75,-251.72 1087.75,-199.93 1272.25,-199.93 1272.25,-251.72"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1180" y="-229.77" font-family="monospace" font-size="14.00" fill="#cdd6f4">frames/</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1180" y="-212.52" font-family="monospace" font-size="14.00" fill="#cdd6f4">index.json + *.jpg</text>
|
||||||
|
</g>
|
||||||
|
<!-- scene->frames_dir -->
|
||||||
|
<g id="edge11" class="edge">
|
||||||
|
<title>scene->frames_dir</title>
|
||||||
|
<path fill="none" stroke="#585b70" stroke-dasharray="5,2" d="M266.72,-334.92C272.54,-332.36 276,-330.84 276,-330.84 276,-330.84 686,-312.84 686,-312.84 686,-312.84 1084,-277.59 1084,-277.59 1084,-277.59 1101.72,-268.22 1121.94,-257.53"/>
|
||||||
|
<polygon fill="#585b70" stroke="#585b70" points="1123.46,-260.68 1130.66,-252.92 1120.19,-254.5 1123.46,-260.68"/>
|
||||||
|
</g>
|
||||||
|
<!-- audio->ipc -->
|
||||||
|
<g id="edge8" class="edge">
|
||||||
|
<title>audio->ipc</title>
|
||||||
|
<path fill="none" stroke="#585b70" stroke-dasharray="5,2" d="M410,-347.72C410,-329.86 410,-308.72 410,-289.15"/>
|
||||||
|
<polygon fill="#585b70" stroke="#585b70" points="413.5,-289.36 410,-279.36 406.5,-289.36 413.5,-289.36"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="455.38" y="-299.54" font-family="monospace" font-size="14.00" fill="#a6adc8">audio_chunk</text>
|
||||||
|
</g>
|
||||||
|
<!-- audio_dir -->
|
||||||
|
<g id="node11" class="node">
|
||||||
|
<title>audio_dir</title>
|
||||||
|
<polygon fill="#2a2a3e" stroke="#585b70" points="1425.5,-251.72 1422.5,-255.72 1401.5,-255.72 1398.5,-251.72 1290.5,-251.72 1290.5,-199.93 1425.5,-199.93 1425.5,-251.72"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1358" y="-229.77" font-family="monospace" font-size="14.00" fill="#cdd6f4">audio/</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1358" y="-212.52" font-family="monospace" font-size="14.00" fill="#cdd6f4">*.wav chunks</text>
|
||||||
|
</g>
|
||||||
|
<!-- audio->audio_dir -->
|
||||||
|
<g id="edge12" class="edge">
|
||||||
|
<title>audio->audio_dir</title>
|
||||||
|
<path fill="none" stroke="#585b70" stroke-dasharray="5,2" d="M505.96,-347.7C527.44,-338.18 544,-330.84 544,-330.84 544,-330.84 1194,-312.84 1194,-312.84 1194,-312.84 1281,-277.59 1281,-277.59 1281,-277.59 1294.45,-268.72 1310.11,-258.4"/>
|
||||||
|
<polygon fill="#585b70" stroke="#585b70" points="1311.81,-261.47 1318.24,-253.04 1307.96,-255.62 1311.81,-261.47"/>
|
||||||
|
</g>
|
||||||
|
<!-- writer->ipc -->
|
||||||
|
<g id="edge9" class="edge">
|
||||||
|
<title>writer->ipc</title>
|
||||||
|
<path fill="none" stroke="#585b70" stroke-dasharray="5,2" d="M605.33,-347.72C573.77,-328.19 535.88,-304.74 501.9,-283.71"/>
|
||||||
|
<polygon fill="#585b70" stroke="#585b70" points="504.06,-280.93 493.72,-278.64 500.38,-286.88 504.06,-280.93"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="612.12" y="-299.54" font-family="monospace" font-size="14.00" fill="#a6adc8">segment_completed</text>
|
||||||
|
</g>
|
||||||
|
<!-- stream_dir -->
|
||||||
|
<g id="node12" class="node">
|
||||||
|
<title>stream_dir</title>
|
||||||
|
<polygon fill="#2a2a3e" stroke="#585b70" points="1594.75,-251.72 1591.75,-255.72 1570.75,-255.72 1567.75,-251.72 1443.25,-251.72 1443.25,-199.93 1594.75,-199.93 1594.75,-251.72"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1519" y="-229.77" font-family="monospace" font-size="14.00" fill="#cdd6f4">stream/</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1519" y="-212.52" font-family="monospace" font-size="14.00" fill="#cdd6f4">*.mp4 segments</text>
|
||||||
|
</g>
|
||||||
|
<!-- writer->stream_dir -->
|
||||||
|
<g id="edge13" class="edge">
|
||||||
|
<title>writer->stream_dir</title>
|
||||||
|
<path fill="none" stroke="#585b70" stroke-dasharray="5,2" d="M767.81,-347.7C788.81,-338.18 805,-330.84 805,-330.84 805,-330.84 1372,-312.84 1372,-312.84 1372,-312.84 1435,-277.59 1435,-277.59 1435,-277.59 1449.95,-268.55 1467.24,-258.11"/>
|
||||||
|
<polygon fill="#585b70" stroke="#585b70" points="1468.96,-261.16 1475.7,-252.99 1465.34,-255.17 1468.96,-261.16"/>
|
||||||
|
</g>
|
||||||
|
<!-- shm -->
|
||||||
|
<g id="node13" class="node">
|
||||||
|
<title>shm</title>
|
||||||
|
<polygon fill="#2a2a3e" stroke="#585b70" points="1847,-251.72 1844,-255.72 1823,-255.72 1820,-251.72 1613,-251.72 1613,-199.93 1847,-199.93 1847,-251.72"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1730" y="-229.77" font-family="monospace" font-size="14.00" fill="#cdd6f4">/dev/shm/cht_scrub_frame</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1730" y="-212.52" font-family="monospace" font-size="14.00" fill="#cdd6f4">raw RGBA pixels</text>
|
||||||
|
</g>
|
||||||
|
<!-- framebuf->shm -->
|
||||||
|
<g id="edge14" class="edge">
|
||||||
|
<title>framebuf->shm</title>
|
||||||
|
<path fill="none" stroke="#585b70" stroke-dasharray="5,2" d="M1072.84,-373.2C1246.56,-350.41 1533,-312.84 1533,-312.84 1533,-312.84 1604.65,-281.56 1661.53,-256.72"/>
|
||||||
|
<polygon fill="#585b70" stroke="#585b70" points="1662.81,-259.98 1670.57,-252.77 1660.01,-253.57 1662.81,-259.98"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="1624.31" y="-299.54" font-family="monospace" font-size="14.00" fill="#a6adc8">get_frame cmd</text>
|
||||||
|
</g>
|
||||||
|
<!-- ipc->python -->
|
||||||
|
<g id="edge10" class="edge">
|
||||||
|
<title>ipc->python</title>
|
||||||
|
<path fill="none" stroke="#585b70" stroke-dasharray="5,2" d="M410,-173.67C410,-155.38 410,-134.55 410,-115.39"/>
|
||||||
|
<polygon fill="#585b70" stroke="#585b70" points="413.5,-115.55 410,-105.55 406.5,-115.55 413.5,-115.55"/>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="463.62" y="-142.76" font-family="monospace" font-size="14.00" fill="#a6adc8">JSON-lines</text>
|
||||||
|
<text xml:space="preserve" text-anchor="middle" x="463.62" y="-125.51" font-family="monospace" font-size="14.00" fill="#a6adc8">(Unix socket)</text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 18 KiB |
Reference in New Issue
Block a user