PhxDemo.Render (phx_demo v0.1.1)

Copy Markdown View Source

Turns a demo's raw recording into shareable files with ffmpeg.

Input: a demo directory containing Playwright's page@<hash>.webm (or a previously renamed raw.webm) and timeline.json.

Output, named after the directory (the demo slug):

  • <slug>.mp4: H.264, trimmed and auto-zoomed (see PhxDemo.Plan and PhxDemo.Zoom), with a soft caption track (mov_text) and chapters from scenes. Narration audio is mixed in when say events carry an audio file (see PhxDemo.TTS), and so is background music when the :music setting is a path or a generation prompt (see music_filter/3 and PhxDemo.MusicGen). Input audio (upload_audio and use_microphone with audible: true, see PhxDemo.Audio) is mixed in at its timeline offset, and the music ducks under it like under narration.
  • <slug>.srt, <slug>.vtt: sidecar captions from say events
  • <slug>.gif: when :gif is in :formats

Requires ffmpeg and ffprobe on PATH.

Summary

Functions

Narration clips as {path, at_ms} from say events that have audio.

Second pass: copy all streams and add chapters/title from FFMETADATA.

The magenta sync flash in the raw video as {start_ms, clear_ms}: its first frame, and the first frame after it that isn't magenta (nil if the flash outlasts the first 5 seconds). nil when there's no flash.

Where the magenta sync flash first appears in the raw video, in ms, or nil. Looks at the center of the first 5 seconds.

Audible input audio (audio_input events with audible: true) as {path, at_ms, duration_ms, loop?}, for a video of total_ms.

ffmpeg arguments for the MP4 encode pass. Public for testing.

Filter chain for the background music input, ending in [out].

Pairs of {input_file, say_text} where an audible input overlaps a narration line. Narration and input audio would talk over each other.

Finds the raw recording and renames it to raw.webm. Playwright names it page@<hash>.webm.

Renders one demo directory.

Narration windows as {at_ms, duration_ms} for clips that carry audio.

Types

result()

@type result() :: %{optional(atom()) => Path.t()}

Functions

audio_clips(map)

Narration clips as {path, at_ms} from say events that have audio.

chapter_args(encoded, meta, out)

Second pass: copy all streams and add chapters/title from FFMETADATA.

detect_sync(raw)

The magenta sync flash in the raw video as {start_ms, clear_ms}: its first frame, and the first frame after it that isn't magenta (nil if the flash outlasts the first 5 seconds). nil when there's no flash.

The page paints a frame or two after the step that follows the flash is recorded, so cutting at a timeline time alone can leave magenta frames at the start; PhxDemo.Plan never starts the cut before clear_ms.

detect_sync_ms(raw)

Where the magenta sync flash first appears in the raw video, in ms, or nil. Looks at the center of the first 5 seconds.

input_windows(map, total_ms)

Audible input audio (audio_input events with audible: true) as {path, at_ms, duration_ms, loop?}, for a video of total_ms.

An upload plays for the file's length. A microphone plays from its play event until the file ends (never, if it loops), the next play or stop event, or the end of the video. loop? says the input needs -stream_loop.

mp4_args(raw, plan, map)

ffmpeg arguments for the MP4 encode pass. Public for testing.

Inputs, in order: the raw video (trimmed to the plan), one input per narration clip, then captions (if any).

music_filter(index, duration_ms, opts \\ [])

Filter chain for the background music input, ending in [out].

Input index is opened with -stream_loop -1, so it loops; the chain trims it to duration_ms, sets the level, ducks 10 dB (with short ramps) around each {at_ms, duration_ms} in :windows, then fades in (1s) and out (2s). Fades shrink on very short videos.

Options: :windows, :volume_db (default -18), :out (default "aout").

overlaps(timeline, total_ms)

Pairs of {input_file, say_text} where an audible input overlaps a narration line. Narration and input audio would talk over each other.

raw_video(dir)

Finds the raw recording and renames it to raw.webm. Playwright names it page@<hash>.webm.

render(dir, opts \\ [])

@spec render(Path.t(), keyword()) :: {:ok, result()} | {:error, String.t()}

Renders one demo directory.

Options:

  • :formats: list of :mp4 and/or :gif (default [:mp4]). A GIF is made from the finished MP4.
  • :gif_width: default 800
  • :gif_fps: default 12
  • :crf: H.264 quality, lower is better (default 20)

say_windows(map)

Narration windows as {at_ms, duration_ms} for clips that carry audio.