Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Runtime support

The modules that everything else depends on: configuration, errors, logging, metrics, and the command-line entry point.

config/ : configuration

Files: mod.rs (Config, parsing, asset resolution, server, storage, packaging, and asset sections, tests), limits.rs (LimitsConfig), cors.rs (CorsConfig), logging.rs (LoggingConfig, LogLevel, LogFormat), resolver.rs (resolver choice, mapper client, registry, and remote-media settings, plus the redacting Secret).

Config::load(path) reads a TOML file and calls Config::parse(contents, config_directory), which deserializes into the private RawConfig, validates, and returns the public Config. The split exists because the file format (relative paths, optional sections) differs from what the rest of the program wants (canonical absolute paths, defaults applied).

Every section uses #[serde(deny_unknown_fields)], so a misspelled key is an error rather than silently ignored.

SectionTypeNotes
[server]ServerConfiglisten, shutdown_delay_ms (default 0), shutdown_grace_ms (default 30,000, must be greater than zero)
[storage]StorageConfigmedia_root, resolved against the config file’s directory if relative
[packaging]PackagingConfigsegment_duration_ms (default 6000, must be greater than zero)
[logging]LoggingConfiglevel, format (json or compact), buffer_capacity
[limits]LimitsConfigResource limits, all greater than zero (table below)
[cors]CorsConfigSee TDD 0003
[assets.<id>]AssetConfigpath relative to media_root; static resolver only
[resolver], [resolver.http]ResolverSettings, MapperConfigtype = "static" (default) or "http"; the two forms are mutually exclusive with [assets] and are validated together
[registry]RegistryConfigResolution cache size, load queue timeout, and preload
[remote_media]RemoteMediaConfigHost allow-list, address policy, and limits for http locations

Asset resolution. The media root is canonicalized and must be a directory. Each asset ID must be ASCII letters, digits, -, or _ (validate_asset_id) and each path must be relative. The path is joined to the root and canonicalized (which resolves symlinks), and the result must still start with the root and be a regular file. This blocks .. and symlink escapes. At least one asset is required, and the count is capped by limits.max_assets.

Limits

LimitDefaultEnforced in
max_assets1,000Config::parse
max_source_bytes1 TiBmp4::parse
max_metadata_bytes64 MiBMetadata::fetch: moov plus every moof
max_fragments20,000Metadata::fetch: moof boxes in one fragmented file
metadata_concurrency16Metadata::fetch: fragments fetched at once through a sidx
tolerate_truncated_tailoffMetadata::fetch: drop a fragment the file is cut off inside, instead of refusing the file
max_tracks8mp4::parse
max_samples_per_track2,000,000mp4::parser::parse_samples
max_samples_per_segment100,000segment::plan
max_segment_bytes64 MiBfmp4::prepare_media_segment
max_segment_jobs2 per CPU, max 32AppState.segment_jobs
segment_queue_timeout_ms2,000acquire_segment_permit
stream_chunk_bytes256 KiBStreamJob
max_request_header_bytes16 KiBenforce_header_limit
request_timeout_ms30,000TimeoutLayer
max_startup_parses4Concurrent asset loads in the registry (also bounds startup preload)
max_concurrent_requests10,000shed_load
response_idle_timeout_ms30,000StreamJob::send
max_index_bytes4 GiBAppState::load
max_connections10,000http/server.rs accept loop
header_read_timeout_ms10,000hyper HTTP/1 header read, via http/server.rs

LimitsConfig::validate rejects a zero in any of them.

Design note: CorsConfig stores plain strings and is validated logically here (non-empty lists, no * mixed with origins, no credentials with wildcards, origin shape) while the parsing into HTTP types happens in http::cors_layer.

Adding an option: add the field to the raw struct with a default, validate it in parse, expose it on Config if needed, document it in vod.example.toml, and add a test in the config tests using the parse_with helper.

error.rs : the error type

One Error enum built with thiserror, and a Result<T> alias.

VariantMeaningHTTP mapping
InvalidRangeA byte range fell outside a source500
InvalidMedia(String)The file is malformed or inconsistent500 at request time, startup failure at load
Unsupported(String)Valid but unsupported media (codec, edit list, encryption, …)Startup failure at load
NotFound(&'static str)A track or segment does not exist404
Upstream(String)A remote origin or mapper misbehaved or was refused502
UpstreamUnavailable(String)A remote origin timed out or is overloaded (retryable)503
Io, Mp4, TomlWrapped library errors500
Configuration(String)Invalid configurationStartup failure
Logging(String)Logger initialization failureStartup failure

Use InvalidMedia for “the file is broken” and Unsupported for “the file is fine but we do not handle it”. Use NotFound only for things a client can legitimately ask for and not find.

observability/logging.rs : non-blocking logs

logging::init(&LoggingConfig) returns a LoggingGuard that must stay alive for the process lifetime.

  • Logs go through tracing-appender’s non-blocking writer with .lossy(true) and buffered_lines_limit(buffer_capacity). A dedicated writer thread does the actual stdout writes. When the queue is full, new lines are dropped instead of blocking request tasks.
  • The subscriber is JSON (flattened fields, current span included) or compact text, filtered by the configured level.
  • A monitor thread wakes every 10 seconds, copies the appender’s dropped-line count into a static atomic (read by /metrics through dropped_lines()), and prints a warning to stderr if it grew. It writes to stderr directly because the log queue is the thing that is saturated.
  • Dropping the guard stops the monitor and flushes the writer.

Level policy: info for lifecycle and asset loads, debug for per-request events, warn for rejected requests, error for internal failures. Never log at trace per sample or log media payload.

observability/metrics.rs

Described with the HTTP layer in HTTP server.

main.rs, lib.rs, and cli/ : entry point and CLI

main.rs is ten lines: a Tokio #[tokio::main] that calls segmentor::run() and returns its ExitCode. lib.rs declares the modules, defines APP_NAME, and implements run(), which calls cli::run with the process arguments and, on failure, prints segmentor: <error> and returns a failure code.

cli::run reads the first argument:

  • none: prints segmentor (used by tests/cli.rs);
  • serve --config <file> (cli/serve.rs): Config::load, logging::init, then http::serve;
  • package --input <mp4> --output <dir> [--segment-duration-ms N] (cli/package.rs).

Argument parsing is hand-written in ServeOptions::parse and PackageOptions::parse; unknown flags are errors.

package is a synchronous developer tool that runs the same pipeline as the server: open, mp4::parse, segment::plan, then for each track write <track>-init.mp4 and <track>-<n>.m4s using fmp4::write_init_segment and fmp4::write_media_segment, and print a summary. tests/package.rs runs it twice and asserts identical bytes (determinism).

fuzzing.rs exposes exercise_media_pipeline(path) and max_input_bytes() (hidden from rustdoc) so the fuzz crate can drive parse, plan, init writing, and fragment preparation without including source files by path.