Universal Video Unpack
Frames, sync audio, and mask β in one wire
- media_data
- frames
- mask
- audio
- frame_count
- fps
- orig_fps
- duration
- orig_duration
- width
- height
- megapixels
- trim_start
- trim_end
- filename
Where UniversalImageUnpack samples a video for three keyframes, this node does the full job: it decodes a video or animated GIF into every frame as a batched IMAGE tensor, pulls the audio track along with it, and applies your trim, target framerate, crop, and mask. It's the pack's answer to the role VHS LoadVideo plays in most ComfyUI video workflows - with the difference that the trimming, cropping, and masking were already done visually in the loader instead of by typing numbers into widgets.
How it works
All the heavy lifting is Python-side, in this pack's video loader:
- Decoding runs through PyAV (
av), the same library ComfyUI core bundles. GIFs take a different path - Pillow'sImageSequence- and the node figures out the GIF's native fps from its frame durations, which is something a lot of naive loaders get wrong. - Trimming uses the
trim_start/trim_endseconds you set on the timeline in the loader, and it applies to both the frames and the audio, so they stay in sync. - Framerate resampling: toggle custom FPS in the loader and the decode subsamples to your target. Want 60fps footage at 24? It hands you frames at 24.
- Post-processing matches the other unpacks: crop from the visual box, optional megapixel clamping, and 32px grid snapping.
- If the file has no audio track, you get a silent placeholder
AUDIOdict instead of a None - a small courtesy that keeps downstream audio nodes from exploding.
The outputs that matter
frames- batched tensor[B, H, W, C], normalized 0.0β1.0. This is your whole video, ready for the next stage.mask- the mask you painted, repeated across every frame so it matches the frame count.audio- the synchronized, trimmed audio as a ComfyUIAUDIOdict.frame_count/fps- feed these into anything that needs to know how long the video is, like conditioning schedulers or frame-interpolation math.orig_fpsis the source's native rate;fpsis what came out.duration/orig_duration- trimmed versus original length, in seconds.width/height/megapixels,trim_start/trim_end,filename- the usual metadata breadcrumbs.
In a video-gen workflow (LTXV, Wan, AnimateDiff style), this is the front door: decode β extract frames β encode them as latent for image-to-video or ControlNet conditioning, and use fps and frame_count to keep the scheduling honest.
Installing
ComfyUI Manager: search "Universal Media Loader", or:
cd ComfyUI/custom_nodes/
git clone https://github.com/Fictiverse/ComfyUI_UniversalMediaLoader.git
Restart ComfyUI. Zero extra dependencies and no models - it uses what ComfyUI already has.
The honest warnings
- It decodes the whole video into one tensor in memory. A 30-second, 60fps 1080p clip is ~1800 frames. That's a lot of VRAM, and it will OOM on consumer cards. This is the biggest trap in the pack: trim aggressively and set the megapixel clamp in the loader before you queue. The resample isn't magic either - "upsampling" 24fps to 60fps just duplicates frames, it doesn't invent motion.
- Decode is CPU-bound PyAV, not hardware-accelerated. Big files take a real few seconds before generation even starts. Annoying, but the trade for getting crop/mask/trim in one place.
- Files must live in ComfyUI's
input/folder for a saved workflow to re-run - standard ComfyUI behavior, easy to forget when you shared the.jsonbut not the.mp4.
Inputs (1)
| Name | Type | Default | Description |
|---|---|---|---|
| media_dataopt | MEDIA_DATA | β |
Outputs (14)
| Name | Type | Description |
|---|---|---|
| frames | IMAGE | β |
| mask | MASK | β |
| audio | AUDIO | β |
| frame_count | INT | β |
| fps | FLOAT | β |
| orig_fps | FLOAT | β |
| duration | FLOAT | β |
| orig_duration | FLOAT | β |
| width | INT | β |
| height | INT | β |
| megapixels | FLOAT | β |
| trim_start | FLOAT | β |
| trim_end | FLOAT | β |
| filename | STRING | β |