Scripted, screen-recorded demo sessions: ffmpeg-subprocess recording (MKV-record-then-remux for crash tolerance), window orchestration (class-matched find/move/focus with the wmctrl→xdotool fallback chain — physical focus produces TRUSTED input events synthetic DOM events cannot), and a typed phase runner with health-wait probes. Deliberately-persistent artifacts — the opposite contract to infrastructure-screen-capture (DLP stills, never-persist), hence a separate crate. v1 is Linux/X11; CaptureBackend/WindowBackend seams are cross-platform, unsupported platforms fail with typed errors.
| Tier | infrastructure |
| Role | unclassified (baselined) |
| Path | crates/infrastructure/screencast |
| Edition | 2021 |
| Targets | infrastructure_screencast |
| Public items | 58 across 5 modules |
| Tests | 36 |
What it is for
# infrastructure-screencast
Scripted, screen-recorded demo sessions as a composable library crate. Lifted from scripts/tools-demo-shopping{,-full}.sh via the tools-script-scan pipeline (Sprint 3.4); the shopping flow itself stays in the consumer.
Three capabilities, each usable alone:
recorder— ffmpeg-subprocess screen recording. Records to a
crash-tolerant Matroska temp file, stops gracefully (q on stdin → deadline → kill fallback), then remuxes to mp4 (-c copy) with an atomic rename. An unclean stop keeps the .mkv.part for diagnosis and never claims an mp4. Drop performs bounded best-effort cleanup only — finalization is always the explicit recorder::Recorder::finish.
window— find / move / resize / focus real windows. CLASS-matched
(wmctrl -lx) with the wmctrl→xdotool fallback chain the bash originals evolved from real breakage. Physical focus matters: real window focus + input produce TRUSTED events that synthetic DOM events cannot (the anti-bot humanization contract, and the known class where file inputs ignore synthetic events). Operator tiebreak (Gate 1.5): this layer is a correctness requirement, not demo cosmetics.
script— a typed phase runner: timestamped narration, health-waits
through a script::Probe seam (the library crate performs NO HTTP — an in-memory step log the caller may persist.
verify— post-remux integrity check on a FINISHED recording file
(ffprobe + a full -f null - decode pass) plus single-frame PNG extraction for visual confirmation. Distinct from infrastructure-screen-capture::capture_primary, which grabs the LIVE screen right now (sensitive, zeroizing, never-persist) — this reads an already-recorded file back off disk.
Relationship to infrastructure-screen-capture
Deliberately a SEPARATE crate: screen-capture's contract is sensitive, transient, zeroizing, never-persist (DLP stills); this crate's whole purpose is a persistent audiovisual artifact. A feature flag would blur that boundary (Gate 1.5, both AIs concurring).
Platforms
v1 implements Linux/X11. The recorder::CaptureBackend and window::WindowBackend seams are cross-platform by design; Windows (ddagrab + the windows crate behind cfg(windows)) and macOS (avfoundation + osascript, Screen-Recording TCC permission — same one infrastructure-screen-capture documents) are additive later work. Unsupported platforms fail with typed errors — never a silent no-op. Wayland has no global window control by design: recording fails fast, window operations report window::Outcome::Skipped loudly.
Capabilities
Caption
Burn timed text captions onto a finished recording, via ffmpeg drawtext.
| Item |
|---|
pub struct Caption |
Caption :: fn new(start_secs : f64, duration_secs : f64, text : impl Into <String>) -> Self |
fn build_drawtext_filter(captions : & Caption) -> String |
fn burn_captions(input : & Path, captions : & Caption, output : & Path, ffmpeg_bin : & str,) -> Result <(), CaptionError> |
CaptionError
Burn timed text captions onto a finished recording, via ffmpeg drawtext.
| Item |
|---|
pub enum CaptionError |
recorder (other)
ffmpeg-subprocess screen recording with crash-tolerant finalization.
| Item |
|---|
fn capture_args(config : & RecorderConfig, mkv_part : & Path,) -> Result <Vec <String>, RecorderError> |
fn remux_args(mkv : & Path, mp4_part : & Path) -> Vec <String> |
fn validate_config(config : & RecorderConfig) -> Result <(), RecorderError> |
fn x11_session_available(display : Option <& str>, xdg_session_type : Option <& str>,) -> Result <(), String> |
CaptureBackend
ffmpeg-subprocess screen recording with crash-tolerant finalization.
| Item |
|---|
pub enum CaptureBackend |
Recorder
ffmpeg-subprocess screen recording with crash-tolerant finalization.
| Item |
|---|
pub struct Recorder |
Recorder :: fn start(config : RecorderConfig) -> Result <Self, RecorderError> |
Recorder :: fn pause(& mut self) -> Result <(), RecorderError> |
Recorder :: fn resume(& mut self) -> Result <(), RecorderError> |
Recorder :: fn is_paused(& self) -> bool |
Recorder :: fn finish(mut self) -> Result <PathBuf, RecorderError> |
Recorder :: fn drop(& mut self) |
RecorderConfig
ffmpeg-subprocess screen recording with crash-tolerant finalization.
| Item |
|---|
pub struct RecorderConfig |
RecorderConfig :: fn x11_default(output : impl Into <PathBuf>) -> Self |
RecorderError
ffmpeg-subprocess screen recording with crash-tolerant finalization.
| Item |
|---|
pub enum RecorderError |
Demo
The typed demo phase-runner: narration, health-waits, keep-focus
| Item |
|---|
pub struct Demo |
Demo :: fn new(name : impl Into <String>) -> Self |
Demo :: fn phase(& mut self, name : impl Into <String>) |
Demo :: fn narrate(& mut self, line : impl Into <String>) |
Demo :: fn wait_ready(& mut self, label : & str, mut probe : impl Probe, timeout : Duration, interval : Duration,) -> Result <(), WaitError> |
Demo :: fn monitor_while(& mut self, label : & str, mut alive : impl FnMut() -> bool, interval : Duration, mut tick : impl FnMut(& mut Demo),) -> u32 |
Demo :: fn records(& self) -> & StepRecord |
Demo :: fn dropped_records(& self) -> usize |
F
The typed demo phase-runner: narration, health-waits, keep-focus
| Item |
|---|
F :: fn check(& mut self) -> Result <Readiness, ProbeError> |
Probe
The typed demo phase-runner: narration, health-waits, keep-focus
| Item |
|---|
pub trait Probe |
ProbeError
The typed demo phase-runner: narration, health-waits, keep-focus
| Item |
|---|
pub struct ProbeError |
ProbeError :: fn new(message : impl Into <String>) -> Self |
Readiness
The typed demo phase-runner: narration, health-waits, keep-focus
| Item |
|---|
pub enum Readiness |
StepRecord
The typed demo phase-runner: narration, health-waits, keep-focus
| Item |
|---|
pub struct StepRecord |
WaitError
The typed demo phase-runner: narration, health-waits, keep-focus
| Item |
|---|
pub enum WaitError |
verify (other)
Post-remux integrity verification: does a finished recording actually
| Item |
|---|
fn verify_playable(path : & Path, ffmpeg_bin : & str, ffprobe_bin : & str,) -> Result <VerifyReport, VerifyError> |
VerifyError
Post-remux integrity verification: does a finished recording actually
| Item |
|---|
pub enum VerifyError |
fn extract_frame(path : & Path, ffmpeg_bin : & str, at_secs : f64, output_png : & Path,) -> Result <(), VerifyError> |
VerifyReport
Post-remux integrity verification: does a finished recording actually
| Item |
|---|
pub struct VerifyReport |
window (other)
Real-window orchestration: find, move/resize, focus.
| Item |
|---|
fn match_window <'a>(windows : & 'a WindowInfo, matcher : & Matcher, pick : Pick,) -> Option <& 'a WindowInfo> |
fn binary_on_path(bin : & str) -> bool |
Geometry
Real-window orchestration: find, move/resize, focus.
| Item |
|---|
pub struct Geometry |
fn split_layout(screen_w : u32, screen_h : u32, left_w : u32) ->(Geometry, Geometry) |
Matcher
Real-window orchestration: find, move/resize, focus.
| Item |
|---|
pub enum Matcher |
Outcome
Real-window orchestration: find, move/resize, focus.
| Item |
|---|
pub enum Outcome |
Pick
Real-window orchestration: find, move/resize, focus.
| Item |
|---|
pub enum Pick |
WindowBackend
Real-window orchestration: find, move/resize, focus.
| Item |
|---|
pub trait WindowBackend |
WindowControl
Real-window orchestration: find, move/resize, focus.
| Item |
|---|
pub struct WindowControl<B : WindowBackend> |
WindowControl<B>
Real-window orchestration: find, move/resize, focus.
| Item |
|---|
WindowControl<B> :: fn new(backend : B) -> Self |
WindowControl<B> :: fn position_and_focus(& self, matcher : & Matcher, pick : Pick, geo : Geometry) -> Outcome |
WindowControl<B> :: fn focus(& self, matcher : & Matcher, pick : Pick) -> Outcome |
WindowInfo
Real-window orchestration: find, move/resize, focus.
| Item |
|---|
pub struct WindowInfo |
fn parse_wmctrl_lx(output : & str) -> Vec <WindowInfo> |
X11Backend
Real-window orchestration: find, move/resize, focus.
| Item |
|---|
pub struct X11Backend |
X11Backend :: fn default() -> Self |
X11Backend :: fn list(& self) -> Result <Vec <WindowInfo>, String> |
X11Backend :: fn move_resize(& self, id : & str, geo : Geometry) -> Result <(), String> |
X11Backend :: fn focus(& self, id : & str) -> Result <(), String> |
How to use it
No examples/ target and no doctest in this crate's rustdoc. The tests listed under Verification are the closest executable usage.
Module structure
infrastructure_screencast
captionsrecorderscriptverifywindow
flowchart TD n_infrastructure_screencast["infrastructure_screencast"] n_infrastructure_screencast --> n_captions["captions"] n_infrastructure_screencast --> n_recorder["recorder"] n_infrastructure_screencast --> n_script["script"] n_infrastructure_screencast --> n_verify["verify"] n_infrastructure_screencast --> n_window["window"]
Public surface
`captions`
| Item | What it is |
|---|---|
pub struct Caption | One on-screen caption, visible during [start_secs, start_secs + duration_secs). |
Caption :: fn new(start_secs : f64, duration_secs : f64, text : impl Into <String>) -> Self | — |
fn build_drawtext_filter(captions : & Caption) -> String | Pure: build the ffmpeg -vf filter graph for captions |
pub enum CaptionError | — |
fn burn_captions(input : & Path, captions : & Caption, output : & Path, ffmpeg_bin : & str,) -> Result <(), CaptionError> | Re-encode input with captions burned in, writing output |
`recorder`
| Item | What it is |
|---|---|
pub enum CaptureBackend | How the screen is captured |
pub struct RecorderConfig | Recorder configuration |
RecorderConfig :: fn x11_default(output : impl Into <PathBuf>) -> Self | 1920x1200 @15fps on :0.0 — the geometry the bash originals used. |
pub enum RecorderError | Recorder failures |
fn capture_args(config : & RecorderConfig, mkv_part : & Path,) -> Result <Vec <String>, RecorderError> | The ffmpeg capture argv for a backend (pure — unit-tested). |
fn remux_args(mkv : & Path, mp4_part : & Path) -> Vec <String> | The ffmpeg remux argv (pure — unit-tested) |
fn validate_config(config : & RecorderConfig) -> Result <(), RecorderError> | Validate a config before spawning anything (pure — unit-tested). |
fn x11_session_available(display : Option <& str>, xdg_session_type : Option <& str>,) -> Result <(), String> | Detect whether this session can host the X11 backend (pure — unit-tested) |
pub struct Recorder | A running screen recording |
Recorder :: fn start(config : RecorderConfig) -> Result <Self, RecorderError> | Start recording |
Recorder :: fn pause(& mut self) -> Result <(), RecorderError> | Suspend the capture process (SIGSTOP) |
Recorder :: fn resume(& mut self) -> Result <(), RecorderError> | Continue a paused capture (SIGCONT) |
Recorder :: fn is_paused(& self) -> bool | Whether the capture process is currently suspended via Self::pause. |
Recorder :: fn finish(mut self) -> Result <PathBuf, RecorderError> | Stop gracefully and finalize to mp4 |
Recorder :: fn drop(& mut self) | — |
`script`
| Item | What it is |
|---|---|
pub enum Readiness | What a probe observed. |
pub struct ProbeError | A probe failed outright (transport error, bad response…) |
ProbeError :: fn new(message : impl Into <String>) -> Self | — |
pub trait Probe | Readiness seam |
F :: fn check(& mut self) -> Result <Readiness, ProbeError> | — |
pub enum WaitError | Waiting on a probe failed. |
pub struct StepRecord | One narrated step, kept in memory |
pub struct Demo | — |
Demo :: fn new(name : impl Into <String>) -> Self | — |
Demo :: fn phase(& mut self, name : impl Into <String>) | Start a named phase (the ═══ Phase N ═══ banners of the originals). |
Demo :: fn narrate(& mut self, line : impl Into <String>) | Print + record one timestamped narration line. |
Demo :: fn wait_ready(& mut self, label : & str, mut probe : impl Probe, timeout : Duration, interval : Duration,) -> Result <(), WaitError> | Poll probe every interval until Ready or timeout |
Demo :: fn monitor_while(& mut self, label : & str, mut alive : impl FnMut() -> bool, interval : Duration, mut tick : impl FnMut(& mut Demo),) -> u32 | Run tick every interval while alive() — the keep-focus-while- the-browser-works choreography of the originals |
Demo :: fn records(& self) -> & StepRecord | The in-memory step log (caller persists if wanted). |
Demo :: fn dropped_records(& self) -> usize | Records dropped past the cap — nonzero means the log is TRUNCATED. |
`verify`
| Item | What it is |
|---|---|
pub struct VerifyReport | What a clean pass over the file found |
pub enum VerifyError | — |
fn verify_playable(path : & Path, ffmpeg_bin : & str, ffprobe_bin : & str,) -> Result <VerifyReport, VerifyError> | Verify path decodes cleanly end-to-end |
fn extract_frame(path : & Path, ffmpeg_bin : & str, at_secs : f64, output_png : & Path,) -> Result <(), VerifyError> | Extract one frame as a PNG for visual confirmation — the same "don't just trust the exit code, look at it" instinct as taking a browser screenshot after a tools-browser-test round trip |
`window`
| Item | What it is |
|---|---|
pub struct WindowInfo | One window as reported by wmctrl -lx. |
pub enum Matcher | How to select a window. |
pub enum Pick | Which match wins when several windows qualify. |
pub struct Geometry | Window geometry, in pixels. |
pub enum Outcome | What happened to a window operation |
fn parse_wmctrl_lx(output : & str) -> Vec <WindowInfo> | Parse wmctrl -lx output (pure — unit-tested) |
fn match_window <'a>(windows : & 'a WindowInfo, matcher : & Matcher, pick : Pick,) -> Option <& 'a WindowInfo> | Select a window (pure — unit-tested). |
fn split_layout(screen_w : u32, screen_h : u32, left_w : u32) ->(Geometry, Geometry) | The bash originals' layout: a fixed-width column on the left (terminal), the remainder on the right (browser) |
pub trait WindowBackend | Platform seam for window control |
pub struct X11Backend | X11 backend: wmctrl first, xdotool fallback — both as strict argv subprocesses with configurable (allowlist-by-config) binaries. |
X11Backend :: fn default() -> Self | — |
X11Backend :: fn list(& self) -> Result <Vec <WindowInfo>, String> | — |
X11Backend :: fn move_resize(& self, id : & str, geo : Geometry) -> Result <(), String> | — |
X11Backend :: fn focus(& self, id : & str) -> Result <(), String> | — |
pub struct WindowControl<B : WindowBackend> | High-level convenience over a backend: find-then-act, degrading to Outcome::Skipped instead of failing the demo. |
WindowControl<B> :: fn new(backend : B) -> Self | — |
WindowControl<B> :: fn position_and_focus(& self, matcher : & Matcher, pick : Pick, geo : Geometry) -> Outcome | Move/resize the matched window and bring it to the front. |
WindowControl<B> :: fn focus(& self, matcher : & Matcher, pick : Pick) -> Outcome | Bring the matched window to the front (the keep-focus choreography). |
fn binary_on_path(bin : & str) -> bool | A binary is plausibly present (used for preflight narration, not gating — the real error surfaces on first use). |
Re-exports. Exported here, defined elsewhere.
| Export | Defined in |
|---|---|
{CaptureBackend,Recorder,RecorderConfig,RecorderError} | recorder::{CaptureBackend,Recorder,RecorderConfig,RecorderError} |
{Demo,Probe,ProbeError,Readiness,StepRecord} | script::{Demo,Probe,ProbeError,Readiness,StepRecord} |
{Geometry,Matcher,Outcome,Pick,WindowBackend,WindowControl,WindowInfo,X11Backend,} | window::{Geometry,Matcher,Outcome,Pick,WindowBackend,WindowControl,WindowInfo,X11Backend,} |
{build_drawtext_filter,burn_captions,Caption,CaptionError} | captions::{build_drawtext_filter,burn_captions,Caption,CaptionError} |
{extract_frame,verify_playable,VerifyError,VerifyReport} | verify::{extract_frame,verify_playable,VerifyError,VerifyReport} |
Boundary
Depends on no other workspace tier.
Shares tier infrastructure with 82 other crates: infrastructure-acquire, infrastructure-adapters-google-calendar, infrastructure-adapters-google-gmail, infrastructure-adapters-google-places, infrastructure-adapters-google-trends, infrastructure-adapters-shodan, infrastructure-adapters-yelp, infrastructure-agent, … (82 total).
_What this crate deliberately does NOT own is a judgment. No committed registry records one for it, so none is stated here._
Where it sits
| Tier (ontology) | infrastructure |
| Architectural role (taxonomy) | unclassified (baselined) |
| Location | crates/infrastructure/screencast |
| Vocabulary in force (lexicon) | current |
Dependencies
Runtime, from outside the workspace.
| Crate | Requirement | Features | Optional | Only on |
|---|---|---|---|---|
chrono | ^0.4 | serde | no | always |
libc | ^0.2 | — | no | cfg(unix) |
serde | ^1 | derive | no | always |
serde_json | ^1 | — | no | always |
thiserror | ^2 | — | no | always |
Development. None.
Build. None.
Depended on by. 5 workspace crates.
Signal flow — what reaches this crate, and what it reaches.
flowchart LR n_tools_demo_buildsession["tools-demo-buildsession"] -->|uses| SELF n_tools_demo_shopping["tools-demo-shopping"] -->|uses| SELF n_tools_demo_systats["tools-demo-systats"] -->|uses| SELF n_tools_gui_harness["tools-gui-harness"] -->|uses| SELF n_tools_screen_record["tools-screen-record"] -->|uses| SELF SELF["infrastructure-screencast"] classDef self fill:#1f883d,stroke:#1f883d,color:#fff; class SELF self;
Feature flags
No Cargo features are defined: every capability is unconditional, so no consumer can receive a half-wired crate.
Targets
| Kind | Name | Source |
|---|---|---|
| lib | infrastructure_screencast | `src/lib.rs` |
Error model
| Error type | Named by |
|---|---|
CaptionError | burn_captions |
ProbeError | declared, no public signature returns it |
RecorderError | capture_args, validate_config |
VerifyError | extract_frame, verify_playable |
WaitError | declared, no public signature returns it |
Operational characteristics
| Property | Evidence |
|---|---|
| async public surface | none detected |
| async runtime | none detected |
| database access | none detected |
| network I/O | none detected |
| unsafe code | yes |
| environment variables | yes |
Unsafe code.
| Site | Source |
|---|---|
unsafe block | src/recorder.rs |
Configuration
| Variable | Read in |
|---|---|
CARGO_MANIFEST_DIR | src/recorder.rs |
DISPLAY | src/recorder.rs |
PATH | src/window.rs |
XDG_SESSION_TYPE | src/recorder.rs |
Related capabilities
5 workspace crates depend on this one: tools-demo-buildsession, tools-demo-shopping, tools-demo-systats, tools-gui-harness, tools-screen-record.
Verification
| Kind | Count |
|---|---|
| Unit tests | 36 |
| Integration tests | 0 |
| Examples | 0 |
| Doctests | 0 |
Evidence by module. How often each public module is named by something executable.
| Module | Tests | Examples | Consumers |
|---|---|---|---|
captions | 4 | 0 | 3 |
recorder | 8 | 0 | 11 |
script | 6 | 0 | 3 |
verify | 4 | 0 | 2 |
window | 12 | 0 | 26 |
What the tests establish, by name:
boundary_empty_captions_yields_empty_filter_string—src/captions.rsconstraint_end_time_is_start_plus_duration—src/captions.rsdeny_burn_captions_missing_binary_yields_spawn_error_not_a_panic—src/captions.rsdeny_burn_captions_with_empty_slice_errors_instead_of_running_ffmpeg—src/captions.rsdeny_colon_and_apostrophe_in_text_are_escaped_not_left_raw—src/captions.rsdeny_literal_backslash_in_text_is_doubled_not_quadrupled—src/captions.rsvalid_multiple_captions_are_comma_joined_into_one_filter_graph—src/captions.rsvalid_single_caption_produces_one_drawtext_clause—src/captions.rsconfig_validation_rejects_the_dangerous_shapes—src/recorder.rsfinish_after_pause_still_finalizes_cleanly—src/recorder.rsfinish_remuxes_and_renames_atomically_with_fake_ffmpeg—src/recorder.rspause_stops_the_process_and_resume_continues_it—src/recorder.rspausing_twice_is_an_error—src/recorder.rsremux_explicit_format_lets_ffmpeg_infer_from_the_dot_part_extension—src/recorder.rsremux_is_lossless_stream_copy_into_a_part_file—src/recorder.rsresuming_without_pausing_is_an_error—src/recorder.rsunimplemented_backends_are_typed_errors_not_silent—src/recorder.rswayland_fails_fast_and_x11_needs_a_display—src/recorder.rsx11_capture_args_declare_an_explicit_output_muxer—src/recorder.rsx11_capture_args_match_the_bash_original—src/recorder.rsmonitor_while_ticks_until_done—src/script.rsnarration_records_phase_and_line—src/script.rsprobe_errors_keep_polling_instead_of_aborting—src/script.rsrecord_cap_truncates_loudly_not_silently—src/script.rswait_ready_succeeds_after_not_ready_attempts—src/script.rswait_ready_times_out_reporting_the_last_detail—src/script.rsa_missing_file_is_a_probe_error_not_a_panic—src/verify.rsa_real_clean_recording_verifies_ok—src/verify.rsa_truncated_recording_is_rejected_not_silently_accepted—src/verify.rsextract_frame_past_a_truncated_files_end_is_a_typed_error_not_a_blank_png—src/verify.rs- _… 6 more_
Documentation coverage
| Measure | Documented | Total |
|---|---|---|
| Public items with rustdoc | 45 | 58 |
Public modules with a //! block | 5 | 5 |
pie showData
title Public items with rustdoc
"Documented" : 45
"No rustdoc detected" : 13
Metrics
| Metric | Value |
|---|---|
| Rust source files | 6 |
| Source lines | 2008 |
| Code lines | 1544 |
| Public API items | 58 |
| Public modules | 5 |
| Tests | 36 |
| Examples | 0 |
| Cargo features | 0 |
| Direct runtime dependencies | 5 |
| Workspace reverse dependencies | 5 |
pie showData
title Public API by kind
"enum" : 9
"function" : 12
"method" : 24
"struct" : 11
"trait" : 2
pie showData
title Rust source composition
"Code" : 1544
"Blank or comment" : 464
Generation
Rendered by tools-corpus corpus readme from repository evidence alone, renderer schema 2, lexicon current. No model, network service or database was consulted. Regenerate with tools-corpus corpus readme --write; verify with --check.