From dc7abd5100459fe5bbae215e1cbc7350cecb2aee Mon Sep 17 00:00:00 2001 From: ausboss <102309304+ausboss@users.noreply.github.com> Date: Thu, 13 Aug 2026 22:58:46 -0500 Subject: [PATCH] Document the uninterruptible core video decode --- js/docs/AUSBOSS_NODES_SaveVideo.md | 21 ++++++++++++++++++++- nodes/node_save_video.py | 14 ++++++++++++-- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/js/docs/AUSBOSS_NODES_SaveVideo.md b/js/docs/AUSBOSS_NODES_SaveVideo.md index d2ad4e6..6fae597 100644 --- a/js/docs/AUSBOSS_NODES_SaveVideo.md +++ b/js/docs/AUSBOSS_NODES_SaveVideo.md @@ -24,7 +24,8 @@ exactly one saved-video surface per node. `audio` output. - **video** (optional): A core `VIDEO` handle, e.g. Load Video's `video` output. Connecting it supersedes the `frames` and `audio` inputs — the - video's own frames and track are encoded instead. + video's own frames and track are encoded instead. Core decodes it in one + uninterruptible step; see *The one phase Cancel cannot reach* below. ## Frame rate precedence @@ -47,3 +48,21 @@ while a long batch is written. The node fills its progress bar frame by frame, and **Cancel** stops the encode within a frame instead of at the end of the batch. Muxed audio is encoded after the picture, so the bar restarts and counts that track's AAC frames; **Cancel** stops there within one of them. + +### The one phase Cancel cannot reach + +Connecting `video` adds a step this node does not control. ComfyUI core reads +the whole file — every frame, decoded to float32 — before the first frame +reaches this pack, and the core call that does it, `VideoInput.get_components()`, +takes no progress or interrupt argument to pass one in. So that phase shows no +progress and does not stop on **Cancel**: the cancel is registered right away +(the decode runs off the execution thread, so the server still answers) and +takes effect at the first frame of the encode, once control returns here. + +How long that lasts scales with pixels, not with file size. Measured on one +desktop: ten seconds of 832x480 at 16 fps came back in about 0.6 s, while ten +seconds of 1080p at 30 fps took about 7 s and held 7.5 GB of frames in memory. +Minutes of high-resolution footage are the case to avoid — RAM runs out before +the wait does. Wiring Load Video's `frames` output instead of `video` keeps the +whole run interruptible and progress-tracked, since that decode is this pack's +own loop. diff --git a/nodes/node_save_video.py b/nodes/node_save_video.py index c93a5c2..3204e44 100644 --- a/nodes/node_save_video.py +++ b/nodes/node_save_video.py @@ -87,7 +87,11 @@ class AusBossSaveVideo: "Optional core VIDEO. When connected, its frames and audio " "supersede the frames and audio inputs, and its own frame " "rate wins over the fps widget; a differing widget rate is " - "logged once to the console." + "logged once to the console. ComfyUI core decodes the whole " + "file into memory before this node sees a frame: that phase " + "reports no progress and does not stop on Cancel, which " + "takes effect once the encode starts. Wiring frames instead " + "keeps the whole run interruptible." ), }, ), @@ -105,7 +109,13 @@ class AusBossSaveVideo: if folder_paths is None: raise RuntimeError("Save Video requires ComfyUI's folder_paths at runtime.") # Pulling a VIDEO's components decodes the whole file, so it joins the - # encode on a worker thread rather than stalling the event loop. + # encode on a worker thread rather than stalling the event loop. It is + # the one phase here that cannot be instrumented: core declares + # VideoInput.get_components() with no arguments (comfy_api/latest/ + # _input/video_types.py) and its VideoFromFile implementation runs a + # bare demux loop, so there is no progress or interrupt seam to pass. + # A Cancel raised during it is only noticed by the first frame check + # inside encode_video, once control comes back. source = await asyncio.to_thread(video_components, video) if source is not None: # A connected VIDEO supersedes the frames/audio inputs and keeps