annie.media.rendering¶
Render pipeline: burn annotations into a browser-playable preview clip.
A render is submitted as a job and runs on a background thread, so the unpredictable, CPU-bound work of decoding and re-encoding never stalls the UI. The submit → poll → path contract is identical whether the work is backed by a thread pool (now) or an external queue (later), so the seam costs nothing today and upgrades cleanly.
The job tracker (submit / status / sweep) is pure Python and unit-tested with an
injected render function. The default render function (burn_clip()) needs
the media extra and a system FFmpeg; it decodes frames, draws the overlay
(annie.color.draw_overlay()), pipes raw RGB into an ffmpeg subprocess
(libx264), and muxes the source audio back. Outputs land in a temp dir that a TTL
sweeper purges, so rendered clips never accumulate on disk.
Attributes¶
Classes¶
Lifecycle states of a render job. |
|
A single render job and its current state. |
|
Thread-pool-backed render-job tracker with a TTL temp sweeper. |
Functions¶
|
Render a video's combined annotations into a browser-playable clip. |
Module Contents¶
- annie.media.rendering.RenderFn¶
Signature of a render function.
Given a video entry and an output path, produce a browser-playable annotated clip.
- annie.media.rendering.CRF = 26¶
x264 quality for rendered previews (lower is better; ffmpeg’s default is 23).
These clips are throwaway review aids swept within minutes, watched in a box a few hundred pixels wide, so preview-grade quality buys a materially smaller file and a faster render without affecting frame count or timing.
- annie.media.rendering.PRESET = 'veryfast'¶
x264 speed/compression trade-off. The reviewer is waiting on this render, so spend the time budget on latency rather than on the last few percent of bitrate.
- annie.media.rendering.FASTSTART = '+faststart'¶
-movflagsvalue that relocates the MP4moovatom ahead of the media data.Without it the atom lands at the end of the file and a browser must fetch the whole clip before the first frame can play.
- class annie.media.rendering.JobStatus[source]¶
Bases:
enum.StrEnumLifecycle states of a render job.
Initialize self. See help(type(self)) for accurate signature.
- class annie.media.rendering.RenderJob[source]¶
A single render job and its current state.
- job_id¶
Unique opaque id used to poll for status.
- row_key¶
The Browse row this render belongs to.
- output_path¶
Path to the finished clip once
statusisDONE.
- error¶
Error message when
statusisFAILED.
- created_at¶
Monotonic-ish wall-clock submit time (epoch seconds).
- class annie.media.rendering.RenderService(render_fn: RenderFn | None = None, *, temp_dir: str | pathlib.Path | None = None, max_workers: int | None = None, ttl_seconds: int | None = None)[source]¶
Thread-pool-backed render-job tracker with a TTL temp sweeper.
- Parameters:
render_fn – The function that actually produces a clip. Defaults to
burn_clip(); tests inject a fake to exercise the lifecycle without a media backend.temp_dir – Directory for rendered clips. Defaults to the configured temp dir.
max_workers – Maximum concurrent renders. Defaults to the configured value.
ttl_seconds – Age after which a finished clip is eligible for sweeping.
- submit(entry: annie.core.models.VideoEntry) str[source]¶
Enqueue a render for
entryand return its job id immediately.- Parameters:
entry – The video to render. Must have a video file to decode.
- Returns:
The job id to poll with
get().- Raises:
ValueError – If the entry has no video path to render.
- jobs() list[RenderJob][source]¶
Return a snapshot of all tracked jobs.
- Returns:
A list copy of the current jobs.
- sweep(*, now: float | None = None) int[source]¶
Delete rendered clips whose file is older than the TTL; forget their jobs.
File-based (by modification time) rather than job-based, so it also reclaims clips orphaned by a page refresh or disconnect — where the job is still tracked but the UI element that showed it is gone — and any
.mp4left by a previous process. A running job’s output is never touched.- Parameters:
now – Override for the current epoch time (for testing).
- Returns:
The number of clip files deleted.
- clear_all() tuple[int, int][source]¶
Delete every finished clip and forget all non-running jobs.
Unlike
sweep(), this ignores the TTL and also removes any orphaned.mp4files intemp_dirthat are not tracked by a job (e.g. left over from a previous process). Running jobs are left untouched so an in-flight render is not corrupted.- Returns:
A
(jobs_cleared, files_deleted)pair — the number of jobs removed from the tracker and the number of files actually unlinked from disk.
- annie.media.rendering.burn_clip(entry: annie.core.models.VideoEntry, output_path: pathlib.Path) None[source]¶
Render a video’s combined annotations into a browser-playable clip.
Decodes every frame of the source (a frame with no detections is written through unchanged, so the render always has the same frame count as the original) and draws, on each: the vdet boxes in flat blue, every track in its own stable unique colour (never blue/green), and the active / protagonist track in green. Frames are piped as raw RGB into an
ffmpeglibx264 subprocess, and the source audio is muxed back (the encode is not-shortest, so a shorter audio track never truncates the video).The output is H.264 in MP4 — already the most broadly playable web format, so there is nothing to switch to. What makes it web-friendly rather than merely web-compatible is
FASTSTART-style muxing (themoovatom is moved to the front, letting the browser start playing before the file has fully downloaded) plus a preview-gradeCRF. Neither touches frame count, duration, resolution, or frame timing — only how many bits each frame costs — so the render stays frame-for-frame aligned with the original.- Parameters:
entry – The video to render;
entry.video_pathmust be set and at least one of vdet/tracks should be present for boxes to appear.output_path – Where to write the encoded
.mp4.
- Raises:
annie.decode.MediaUnavailableError – If the
mediaextra is absent.ValueError – If the entry has no video path.
RuntimeError – If the ffmpeg subprocess fails.