module

PNGGIF::VideoSource

Decodes a video file into animation frames using external ffmpeg (with ffprobe for dimensions/frame rate), so a consumer (e.g. Crysterm's Widget::Media backends) can play it like an animated GIF. ffmpeg scales frames to a small cap and emits raw RGBA on stdout; each frame is w*h*4 bytes, converted into a PNGGIF::Bitmap.

Two decoders, chosen per #mode's decode strategy (auto by default):

  • Eager (#decode) — reads every frame up front into a frame-backed PNGGIF::PNG (capped by max_frames). Best for short, looping clips: decode once, loop from RAM for free, instant resize re-sample.
  • Streaming (Stream) — a live ffmpeg pipe yielding frames on demand, constant memory regardless of length, fast first paint, re-decodes on loop. Best for long videos that can't fit in memory.

auto streams when the estimated frame count exceeds max_frames, else eager. Nothing here raises: failures yield nil and the caller falls back to its usual "could not load" state.

The sizing/rate/strategy knobs are plain parameters with the DEFAULT_* constants below as defaults; in Crysterm they were read from Crysterm::Config, and its adapter reinstates those config values as the parameter defaults.

Constants

DEFAULT_FPS = 15.0

Default cap on the sampled frame rate (the max_fps parameter of #decode/Stream.open). Mirrors the former video.fps crysterm config default.

DEFAULT_MAX_FRAMES = 600

Default safety cap on frames decoded eagerly into memory, and the Decode::Auto stream-vs-eager threshold (the max_frames parameter of #decode/#mode). Mirrors the former video.max_frames crysterm config default; longer videos are truncated to this many frames.

DEFAULT_MAX_SIZE = 240

Default long-edge pixel size frames are scaled to (the cap parameter of #decode/Stream.open). Mirrors the former video.max_size crysterm config default: terminal boxes are small; smaller = faster, less memory.

EXTENSIONS = ["mp4", "m4v", "mkv", "webm", "mov", "avi", "wmv", "flv", "mpg", "mpeg", "mpe", "ogv", "ts", "3gp"] of ::String

File extensions routed through ffmpeg as video. .ogg is excluded (commonly audio-only); use .ogv for Ogg video.

Instance methods

blank_bitmap(w : Int32, h : Int32) : PNGGIF::Bitmap

Allocates a w×h fully-transparent bitmap for in-place reuse by the Stream ping-pong. (PNGGIF::Pixel is a value struct, so Array.new fills the row with independent copies — no shared reference.) :nodoc:

Source
cap_size(sw : Int32, sh : Int32, cap : Int32) : Tuple(Int32, Int32)

Caps sw×sh to a cap long edge (preserving aspect), forcing even dimensions (yuv420-based codecs require even width/height). :nodoc:

Source
decode(file : String, cap : Int32 = DEFAULT_MAX_SIZE, max_fps : Float64 = DEFAULT_FPS, max_frames : Int32 = DEFAULT_MAX_FRAMES) : PNGGIF::PNG | Nil

Decodes file eagerly into a frame-backed PNGGIF::PNG, or nil on any failure. cap is the long-edge pixel size frames are scaled to; max_fps caps the sampled frame rate; max_frames caps how many frames are read into memory.

Source
dup_bitmap(bmp : PNGGIF::Bitmap) : PNGGIF::Bitmap

Deep-copies a bitmap (new outer + inner arrays) so a caller can retain a snapshot that won't be mutated when a ping-pong buffer is later reused. :nodoc:

Source
estimate_frames(file : String) : Int32 | Nil

Estimates the total frame count via ffprobe (nb_frames, else duration × fps), or nil when unknown.

Source
ffmpeg_args(file : String, w : Int32, h : Int32, fps : Float64) : Array(String)

ffmpeg argv that decodes file to raw RGBA frames of w×h at fps. :nodoc:

Source
ffprobe_fields(file : String, entries : String) : Hash(String, String) | Nil

Runs ffprobe for the first video stream, requesting entries (a comma-separated stream=… field list), and returns the printed key => value pairs — or nil if ffprobe is missing / fails. :nodoc:

Source
fill_bitmap(bmp : PNGGIF::Bitmap, buf : Bytes, w : Int32, h : Int32) : Nil

Overwrites bmp in place from the raw RGBA frame in buf, reusing the existing row/outer arrays (only the value-struct pixels are reassigned). bmp must already be sized w×h. :nodoc:

Source
frame_delay(fps : Float64) : Int32

Per-frame delay in ms for fps (at least 1). :nodoc:

Source
mode(file : String, decode : Decode = Decode::Auto, max_frames : Int32 = DEFAULT_MAX_FRAMES) : Mode

Resolves the decoder strategy for file from decode (auto | eager | stream). auto streams when the estimated frame count exceeds max_frames (an unknown length stays eager — the cap then protects memory by truncation).

Source
parse_frame_rate(val : String | Nil) : Float64

Parses an ffprobe avg_frame_rate value ("num/den") into fps, or 0.0 when unset/unusable. :nodoc:

Source
probe(file : String) : Info | Nil

Reads width / height / average frame rate via ffprobe. Returns nil if ffprobe is missing or the file has no usable video stream. :nodoc:

Source
read_full(io : IO, buf : Bytes) : Bool

Reads exactly buf.size bytes into buf, returning false at EOF (a short trailing read — e.g. a truncated final frame — is discarded). :nodoc:

Source
resolve_geometry(file : String, cap : Int32, max_fps : Float64) : Tuple(Int32, Int32, Float64) | Nil

Probes file and resolves the capped frame dimensions and sampled fps as {w, h, fps}, or nil when ffprobe can't read it. Shared setup for both eager #decode and streaming Stream.open. :nodoc:

Source
sample_fps(src_fps : Float64, max_fps : Float64) : Float64

The sampled fps: the source rate capped to max_fps (never below 1). :nodoc:

Source
to_bitmap(buf : Bytes, w : Int32, h : Int32) : PNGGIF::Bitmap

Converts one raw RGBA frame buffer into a PNGGIF::Bitmap. :nodoc:

Source
video?(file : String) : Bool

Whether file looks like a video this module should decode.

Source

Nested types