This commit is contained in:
2026-04-09 18:19:03 -03:00
parent 5921cd6562
commit 5b467ffba8
18 changed files with 1793 additions and 43 deletions

179
media/Cargo.lock generated
View File

@@ -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"

View File

@@ -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
View 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
View 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
View File

@@ -0,0 +1,3 @@
pub mod capture;
pub mod encoder;
pub mod pipeline;

View File

@@ -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;
header: PacketHeader { let mut keepalive_interval = tokio::time::interval(std::time::Duration::from_secs(5));
packet_type: PacketType::Video,
flags: if keyframe { FLAG_KEYFRAME } else { 0 },
length: 1024,
timestamp_ns: ts,
},
payload: vec![0u8; 1024],
};
protocol::write_packet(&mut writer, &video).await?;
// Fake audio packet every 3 video frames loop {
if i % 3 == 0 { tokio::select! {
let audio = WirePacket { pkt = packet_rx.recv() => {
header: PacketHeader { match pkt {
packet_type: PacketType::Audio, Some(encoded) => {
flags: 0, let wire = WirePacket {
length: 512, header: PacketHeader {
timestamp_ns: ts, packet_type: PacketType::Video,
}, flags: if encoded.keyframe { FLAG_KEYFRAME } else { 0 },
payload: vec![0u8; 512], length: encoded.data.len() as u32,
}; timestamp_ns: pts_to_ns(
protocol::write_packet(&mut writer, &audio).await?; encoded.pts,
encoded.time_base_num,
encoded.time_base_den,
),
},
payload: encoded.data,
};
protocol::write_packet(&mut writer, &wire).await?;
video_count += 1;
if video_count % 300 == 1 {
info!("Sent {video_count} video packets");
writer.flush().await?;
}
}
None => {
info!("Pipeline channel closed");
break;
}
}
}
_ = keepalive_interval.tick() => {
let keepalive = ControlMessage::Keepalive;
protocol::write_packet(&mut writer, &keepalive.to_wire_packet()?).await?;
writer.flush().await?;
}
_ = tokio::signal::ctrl_c() => {
info!("Ctrl+C received, stopping...");
break;
}
} }
// Keepalive every 150 frames (~5s)
if i % 150 == 0 && i > 0 {
let keepalive = ControlMessage::Keepalive;
protocol::write_packet(&mut writer, &keepalive.to_wire_packet()?).await?;
}
tokio::time::sleep(std::time::Duration::from_nanos(frame_interval_ns)).await;
} }
// Send session_stop and flush pipeline.stop();
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)

View 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
View 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
View 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
View 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
View 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" "$@"

View 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"]
}

View 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 &#160;(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&#45;pipeline thread &#160;(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&#45;&gt;capture -->
<g id="edge1" class="edge">
<title>drm&#45;&gt;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&#45;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&#45;&gt;encoder -->
<g id="edge2" class="edge">
<title>vaapi&#45;&gt;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&#45;&gt;write -->
<g id="edge6" class="edge">
<title>session_start&#45;&gt;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 &#160;| &#160;keepalive &#160;| &#160;ctrl&#45;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&#45;&gt;keepalive -->
<g id="edge8" class="edge">
<title>mux&#45;&gt;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&#45;&gt;write -->
<g id="edge7" class="edge">
<title>mux&#45;&gt;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&#45;&gt;write -->
<g id="edge9" class="edge">
<title>keepalive&#45;&gt;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&#45;&gt;net -->
<g id="edge10" class="edge">
<title>write&#45;&gt;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&#45;&gt;encoder -->
<g id="edge3" class="edge">
<title>capture&#45;&gt;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&#45;&gt;chan -->
<g id="edge4" class="edge">
<title>encoder&#45;&gt;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&#45;&gt;mux -->
<g id="edge5" class="edge">
<title>chan&#45;&gt;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&lt;u8&gt; &#160;(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
View 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
View 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&#45;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&#45;sys&#45;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&#45;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&#45;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 &#160;(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 &#160;&#160;&#160;&#160;(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&#45;&gt;tokio -->
<g id="edge4" class="edge">
<title>common&#45;&gt;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&#45;&gt;serde -->
<g id="edge3" class="edge">
<title>common&#45;&gt;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&#45;&gt;tracing -->
<g id="edge5" class="edge">
<title>common&#45;&gt;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&#45;&gt;anyhow -->
<g id="edge6" class="edge">
<title>common&#45;&gt;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&#45;client &#160;[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 &#160;&#160;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 &#160;&#160;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 &#160;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 &#160;&#160;&#160;&#160;&#160;TCP transport + keepalive</text>
</g>
<!-- client&#45;&gt;ffmpeg_next -->
<g id="edge7" class="edge">
<title>client&#45;&gt;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&#45;&gt;tokio -->
<g id="edge8" class="edge">
<title>client&#45;&gt;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&#45;&gt;tracing -->
<g id="edge9" class="edge">
<title>client&#45;&gt;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&#45;&gt;anyhow -->
<g id="edge10" class="edge">
<title>client&#45;&gt;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&#45;&gt;common -->
<g id="edge1" class="edge">
<title>client&#45;&gt;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&#45;server &#160;[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 &#160;&#160;&#160;&#160;&#160;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"> &#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;counts packets, no decode yet</text>
</g>
<!-- server&#45;&gt;tokio -->
<g id="edge11" class="edge">
<title>server&#45;&gt;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&#45;&gt;tracing -->
<g id="edge12" class="edge">
<title>server&#45;&gt;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&#45;&gt;anyhow -->
<g id="edge13" class="edge">
<title>server&#45;&gt;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&#45;&gt;common -->
<g id="edge2" class="edge">
<title>server&#45;&gt;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
View 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>

View 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"]
}

View 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&#45;&gt;listener -->
<g id="edge1" class="edge">
<title>net&#45;&gt;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 &#160;(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&#45;&gt;decoder -->
<g id="edge2" class="edge">
<title>listener&#45;&gt;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 &#160;(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&#45;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&#45;&gt;scene -->
<g id="edge3" class="edge">
<title>decoder&#45;&gt;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 &#160;(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&#45;&gt;audio -->
<g id="edge6" class="edge">
<title>decoder&#45;&gt;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 &#160;(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&#45;&gt;writer -->
<g id="edge4" class="edge">
<title>decoder&#45;&gt;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 &#160;(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&#45;&gt;framebuf -->
<g id="edge5" class="edge">
<title>decoder&#45;&gt;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 &#160;(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&#45;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&#45;&gt;ipc -->
<g id="edge7" class="edge">
<title>scene&#45;&gt;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&#45;&gt;frames_dir -->
<g id="edge11" class="edge">
<title>scene&#45;&gt;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&#45;&gt;ipc -->
<g id="edge8" class="edge">
<title>audio&#45;&gt;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&#45;&gt;audio_dir -->
<g id="edge12" class="edge">
<title>audio&#45;&gt;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&#45;&gt;ipc -->
<g id="edge9" class="edge">
<title>writer&#45;&gt;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&#45;&gt;stream_dir -->
<g id="edge13" class="edge">
<title>writer&#45;&gt;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&#45;&gt;shm -->
<g id="edge14" class="edge">
<title>framebuf&#45;&gt;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&#45;&gt;python -->
<g id="edge10" class="edge">
<title>ipc&#45;&gt;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&#45;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