AM Read Video
Decode anything, skip the decode when you can
- image
- mask
- audio
- resolved_path
- info
- width
- height
- frame_rate
- frame_count
- video
Stock LoadVideo in ComfyUI is a thin thing: mp4 and h264, a limited frame range, no alpha. AM Read Video is the version that reads like a studio I/O node - PyAV under the hood, so it decodes h264, h265, ProRes, DNxHR, VP9 and friends, seeks frame-accurately, handles alpha-bearing codecs like ProRes 4444, and color-manages the result. And its video output socket is the pack's signature trick: wire only that and the decode never even happens.
It's part of comfyui-am-vfx-tools ("AM VFX Tools" category), Adrian Meyer's 13-node VFX toolkit. This is the node to pair with AM Write Video (PyAV on both ends) for a real video round-trip that stock nodes can't do.
How it works
PyAV wraps libav, so you get the whole codec zoo instead of the h264-only special. Frame-accurate seek means a range read seeks to the first frame you want rather than decoding everything up to it. Alpha-bearing pixel formats decode losslessly when the codec supports them - ProRes 4444 and 4444 XQ being the big ones - and the alpha lands on the mask socket per the stock convention (mask = 1 - alpha).
The widgets are the pack's standard I/O set: frame_mode (single/range/all) with before/after edge policies (hold/loop/bounce/black), input_colorspace → working_colorspace via OCIO (with raw_data to bypass), and an optional reformat block. The default input space is Display/Gamma 2.2 Rec.709 - the studio dailies standard - because most video containers don't carry a reliable colorspace tag, so an explicit value is usually more correct than trusting the file.
The trick: the video socket
This is the one worth understanding. The video output is a VideoFromFile that references the source on disk - it doesn't hold pixels. Wire only that socket downstream (say, into an AM Write Image or an API node that takes a file) and PyAV never decodes the frames; peak RAM stays at file-handle level. Wire both image and video in the same graph and you can run a color-managed IMAGE branch for inference and a raw VIDEO branch for an API call that wants the original file - parallel branches, both valid.
Caveat, straight from the author's tooltip: the video passthrough is raw - source colorspace, resolution, codec, untouched by the node's OCIO/Reformat settings. Those apply to image only. If you need the color transform, you either use the image branch or chain an AM transform node on the video.
Outputs
image, mask, audio (or a silent stub), resolved_path, info (dimensions/codec/pixfmt/fps/count), width, height, frame_rate (native, from the PyAV header), frame_count, and video.
Installing it
cd ComfyUI/custom_nodes
git clone https://github.com/am-pipeline-prod/comfyui-am-vfx-tools.git
cd comfyui-am-vfx-tools
pip install -r requirements.txt
Restart ComfyUI, or search comfyui-am-vfx-tools in ComfyUI Manager. The av>=14.0.0 (PyAV) dependency - and note the alpha support needs PyAV ≥ 11, which the pin covers - comes from that pip line.
Where people get burned
The raw-video surprise: people wire video, run a color transform, and see no change because the video socket is passthrough. That's by design. And if a Linux distro forces a system-FFmpeg link (pip install --no-binary av), verify ffmpeg -h encoder=prores_ks lists the 4444 profiles or alpha encode will silently fall apart. Default decode of a long clip also slurps the whole thing into RAM via the image branch - use the video socket when you only need the file.
Inputs (18)
| Name | Type | Default | Description |
|---|---|---|---|
| file_path | STRING | Absolute path to the video container. Use the 📂 Browse button to populate from the native dialog. | |
| frame_mode | COMBO | all | Which frames to decode from the container. single = only `first_frame`. range = `first_frame`..`last_frame` inclusive (with `before`/`after` policy outside the container). all = every frame in the container. |
| first_frame | INT | 1-999999–999999 | Frame index in single mode; lower bound in range mode (1-based). Ignored in all mode. The 🔍 Detect Range button auto-fills this. |
| last_frame | INT | -1-1–999999 | Range upper bound (inclusive, 1-based). -1 = auto = container's frame count. The 🔍 Detect Range button reads it from the PyAV header. |
| before | COMBO | hold | Edge policy below the container's frames when `frame_mode=range` and the requested range starts before frame 1. hold = clamp to first; loop = wrap; bounce = ping-pong; black = synthesize black. |
| after | COMBO | hold | Edge policy above the container's frames when the requested range extends past the last frame. Mirrors `before`. |
| input_colorspace | COMBO | Display/Gamma 2.2 Rec.709 - Display | Source colorspace of the container (HD video typically Gamma 2.2 Rec.709 in studio dailies, Rec.1886 Rec.709 for broadcast). The OCIO transform converts from this to `working_colorspace`. Pick `raw` to skip — but most video containers don't carry a reliable colorspace tag, so an explicit value is usually correct. |
| raw_data | BOOLEAN | false | When On, skip the OCIO transform — pixels pass through unchanged. `input_colorspace` and `working_colorspace` are ignored. |
| working_colorspace | COMBO | Display/sRGB - Display | Target colorspace for the IMAGE output — the space downstream nodes will see. |
| reformat_mode | COMBO | off | Reformat mode. off = bypass, output matches input. scale = uniform scale by `scale` (other widgets ignored). to_box = resize/crop to a target W×H from `preset` or `target_width`/`target_height`. |
| scale | FLOAT | 1.000.01–16 | Uniform scale factor. Used when `reformat_mode=scale`; ignored otherwise. Output dimensions are round(input × scale). |
| preset | COMBO | Width/Height | Named output format. Used when `reformat_mode=to_box`. `Width/Height` = use the `target_width` / `target_height` widgets below. Any other entry overrides those widgets with the preset's resolution. |
| target_width | INT | 19201–16384 | Target output width in pixels. Used when `reformat_mode=to_box` AND `preset=Width/Height`; ignored when a named preset is selected. |
| target_height | INT | 10801–16384 | Target output height in pixels. Used when `reformat_mode=to_box` AND `preset=Width/Height`; ignored when a named preset is selected. |
| resize_type | COMBO | fit | How input maps into the target box. Used when `reformat_mode=to_box`. width/height = scale uniformly to match that edge. fit = scale to fit inside the box (letterbox; black where the box exceeds the scaled image). fill = scale to cover the box (crops the overflow). distort = scale W and H independently to exactly match the box (changes aspect). none = no scale; place input centered in the box (crop if larger, pad if smaller). Cropped-away/padded regions are TRANSPARENT — RGB sources are promoted to RGBA with alpha=0 in the padded area so downstream compositing is clean. |
| filter | COMBO | cubic | Pixel filter for resampling. impulse = nearest-neighbor (mask passes, exact pixel preservation). linear = bilinear (cheap, smooth). cubic = bicubic (default; the safe Nuke-equivalent). Lanczos4 = sharpest; for high-quality stills / final delivery. area = best for downscaling — anti-aliased mean pooling, softer but artifact-free. |
| output_dtype | COMBO | fp16 | Output tensor dtype. fp32 = ComfyUI default (4 bytes/sample). fp16 = half memory + half VRAM (2 bytes/sample). EXR-native precision; fits the [0,1] LDR + scene-linear range with headroom up to ~65504. Some downstream nodes assume fp32 — flip back to fp32 if you hit dtype errors. |
| show_preview | BOOLEAN | true | Show a thumbnail of the first decoded frame on the node. |
Outputs (10)
| Name | Type | Description |
|---|---|---|
| image | IMAGE | Decoded frames as IMAGE (N×H×W×3 float in [0,1]). RGB only — alpha is split out to the `mask` socket. |
| mask | MASK | Alpha channel as MASK (N×H×W float in [0,1]). MASK CONVENTION (stock ComfyUI): mask = 1 - alpha white (1.0) = 'area to inpaint' (source was transparent) black (0.0) = 'keep' (source was opaque) empty mask = all zeros (source has no alpha = fully visible) This is the SD-inpainting convention every stock ComfyUI mask-using node expects (LoadImage, MaskComposite, SetLatentNoiseMask, ImpactPack mask pipeline, etc.). Drop-in compatible with all of them. If you want NUKE-STYLE natural alpha (mask = alpha, where 1.0 = opaque), wire a MaskInvert node between this socket and your downstream consumer. Populated when the source carries alpha: * Image: EXR / PNG / TIFF with α channel * Video: ProRes 4444 / 4444 XQ, QuickTime RLE, FFV1 (yuva*) Otherwise emits the empty mask (zeros = fully opaque source). |
| audio | AUDIO | Audio track from the container, or a silent stub if absent. |
| resolved_path | STRING | Resolved on-disk container path. |
| info | STRING | Human-readable summary: dimensions, codec, pixel format, fps, frame count. |
| width | INT | Frame width in pixels. |
| height | INT | Frame height in pixels. |
| frame_rate | FLOAT | Container's native frame rate (read from the PyAV header). |
| frame_count | INT | Number of frames decoded into the IMAGE batch. |
| video | VIDEO | `VideoFromFile` referencing the source on disk. Wiring ONLY this (not `image`) skips the PyAV decode — peak RAM stays at file-handle level. ⚠️ Raw passthrough: source colorspace, source resolution, source codec. OCIO/Reformat apply to `image` only. Both sockets can be wired in the same graph for parallel branches. |