Fix video combine preview playback

This commit is contained in:
Fillip
2026-08-07 13:15:09 -07:00
committed by Machine Delusions
parent 6f4326dc5e
commit 5e8322dc0c
3 changed files with 143 additions and 19 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyui_fill-nodes"
description = "Fill-Nodes is a versatile collection of custom nodes for ComfyUI that extends functionality across multiple domains. Features include advanced image processing (pixelation, slicing, masking), visual effects generation (glitch, halftone, pixel art), comprehensive file handling (PDF creation/extraction, Google Drive integration), AI model interfaces (GPT, DALL-E, Hugging Face), utility nodes for workflow enhancement, and specialized tools for video processing, captioning, and batch operations. The pack provides both practical workflow solutions and creative tools within a unified node collection."
version = "2.26.0"
version = "2.27.0"
license = {file = "LICENSE"}
dependencies = ["librosa", "sounddevice", "glitch_this", "PyOpenGL", "glfw", "scipy>=1.13.1", "requests", "aiohttp", "moviepy", "matplotlib", "reportlab", "openai", "PyPDF2", "pdf2image", "PyMuPDF", "reportlab", "PyPDF2", "ollama", "kornia", "opencv-python", "gdown", "open_clip_torch", "google-genai"]
+25
View File
@@ -356,5 +356,30 @@ class VideoCombineExecutionTests(unittest.TestCase):
self.assertIsNone(metadata)
class VideoCombineFrontendTests(unittest.TestCase):
def test_frontend_requires_explicit_playback_and_synchronizes_previews(self):
script = (pathlib.Path(__file__).parents[1] / "web" / "nodes" / "video" / "FL_VideoCombine.js").read_text(encoding="utf-8")
for behavior in (
'data-role="sync"',
"syncVideoCombinePreviews()",
"prepareForSynchronization()",
"this.video.currentTime = 0",
"maintainSynchronization()",
"Math.abs(video.currentTime - leader.currentTime) > 0.08",
'document.addEventListener("visibilitychange"',
'window.addEventListener("pagehide"',
"pauseAllVideoCombinePreviews()",
):
with self.subTest(behavior=behavior):
self.assertIn(behavior, script)
loaded_metadata = script.split('this.video.addEventListener("loadedmetadata"', 1)[1].split(
'this.video.addEventListener("error"', 1
)[0]
self.assertNotIn(".play(", loaded_metadata)
self.assertEqual(script.count("this.video.play().catch"), 1)
if __name__ == "__main__":
unittest.main()
+117 -18
View File
@@ -410,25 +410,72 @@ function formatTime(value) {
return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
}
function syncVideoCombinePreviews() {
const videos = [];
for (const node of app.graph?._nodes || []) {
if (node.comfyClass !== "FL_VideoCombine") continue;
const video = node._flVideoCombinePanel?.video;
if (!video?.src || video.readyState < HTMLMediaElement.HAVE_METADATA) continue;
try {
video.pause();
video.currentTime = 0;
videos.push(video);
} catch {
// A preview can become unavailable while the graph is changing.
const synchronizedPanels = new Set();
let synchronizationFrame = null;
function getVideoCombinePanels() {
return (app.graph?._nodes || [])
.filter((node) => node.comfyClass === "FL_VideoCombine")
.map((node) => node._flVideoCombinePanel)
.filter(Boolean);
}
function stopSynchronization() {
if (synchronizationFrame !== null) {
cancelAnimationFrame(synchronizationFrame);
synchronizationFrame = null;
}
synchronizedPanels.clear();
}
function removeSynchronizedPanel(panel) {
synchronizedPanels.delete(panel);
if (synchronizedPanels.size < 2 && synchronizationFrame !== null) {
cancelAnimationFrame(synchronizationFrame);
synchronizationFrame = null;
}
}
function maintainSynchronization() {
synchronizationFrame = null;
for (const panel of synchronizedPanels) {
if (!panel.isSynchronizationActive()) synchronizedPanels.delete(panel);
}
const panels = [...synchronizedPanels];
if (panels.length < 2 || document.hidden) return;
const leader = panels[0].video;
if (!leader.paused && leader.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
for (const panel of panels.slice(1)) {
const video = panel.video;
if (video.paused || video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) continue;
if (Number.isFinite(video.duration) && leader.currentTime >= video.duration) continue;
if (Math.abs(video.currentTime - leader.currentTime) > 0.08) {
video.currentTime = leader.currentTime;
}
}
}
for (const video of videos) {
video.play().catch(() => {});
synchronizationFrame = requestAnimationFrame(maintainSynchronization);
}
function syncVideoCombinePreviews() {
const panels = getVideoCombinePanels().filter((panel) => panel.hasPreview());
if (!panels.length) return;
stopSynchronization();
for (const panel of panels) panel.prepareForSynchronization();
for (const panel of panels) synchronizedPanels.add(panel);
for (const panel of panels) panel.requestPlayback(true);
if (panels.length > 1) {
synchronizationFrame = requestAnimationFrame(maintainSynchronization);
}
}
function pauseAllVideoCombinePreviews() {
stopSynchronization();
for (const panel of getVideoCombinePanels()) panel.pausePlayback(false);
}
class VideoCombinePanel {
constructor(node, settingsWidget, container) {
this.node = node;
@@ -439,6 +486,8 @@ class VideoCombinePanel {
this.configError = "";
this.handleDocumentPointerDown = null;
this.handleDocumentKeyDown = null;
this.playbackRequested = false;
this.restartAtStart = false;
this.node.properties ||= {};
if (!Number.isFinite(this.node.properties.previewVolume)) {
@@ -641,12 +690,16 @@ class VideoCombinePanel {
this.playButton.addEventListener("click", () => {
if (!this.video.src) return;
if (this.video.paused) {
this.video.play().catch(() => {});
this.requestPlayback();
} else {
this.video.pause();
this.pausePlayback();
}
});
this.video.addEventListener("play", () => {
if (!this.playbackRequested || document.hidden) {
this.pausePlayback();
return;
}
this.playButton.textContent = "❚❚";
});
this.video.addEventListener("pause", () => {
@@ -654,9 +707,12 @@ class VideoCombinePanel {
});
this.video.addEventListener("timeupdate", () => this.updateTime());
this.video.addEventListener("loadedmetadata", () => {
if (this.restartAtStart) {
this.video.currentTime = 0;
this.restartAtStart = false;
}
this.placeholder.style.display = "none";
this.updateTime();
this.video.play().catch(() => {});
});
this.video.addEventListener("error", () => {
if (!this.video.src) return;
@@ -675,6 +731,40 @@ class VideoCombinePanel {
this.syncButton.addEventListener("click", syncVideoCombinePreviews);
}
hasPreview() {
return Boolean(this.preview && this.video.src);
}
isSynchronizationActive() {
return this.playbackRequested && !this.video.ended;
}
requestPlayback(keepSynchronized = false) {
if (!this.video.src) return;
if (!keepSynchronized) removeSynchronizedPanel(this);
this.playbackRequested = true;
this.video.play().catch(() => {
this.playbackRequested = false;
removeSynchronizedPanel(this);
});
}
pausePlayback(removeFromSynchronization = true) {
this.playbackRequested = false;
this.video.pause();
if (removeFromSynchronization) removeSynchronizedPanel(this);
}
prepareForSynchronization() {
this.pausePlayback(false);
this.restartAtStart = true;
if (this.video.readyState !== HTMLMediaElement.HAVE_NOTHING) {
this.video.currentTime = 0;
this.restartAtStart = false;
this.updateTime();
}
}
setMenuOpen(open) {
this.settingsMenu.hidden = !open;
this.moreButton.setAttribute("aria-expanded", String(open));
@@ -766,6 +856,8 @@ class VideoCombinePanel {
loadPreview(preview) {
if (!preview?.filename) return;
this.pausePlayback();
this.restartAtStart = false;
this.preview = preview;
if (preview.preview_url) {
const separator = preview.preview_url.includes("?") ? "&" : "?";
@@ -818,13 +910,14 @@ class VideoCombinePanel {
}
dispose() {
removeSynchronizedPanel(this);
if (this.handleDocumentPointerDown) {
document.removeEventListener("pointerdown", this.handleDocumentPointerDown);
}
if (this.handleDocumentKeyDown) {
document.removeEventListener("keydown", this.handleDocumentKeyDown);
}
this.video.pause();
this.pausePlayback(false);
this.video.removeAttribute("src");
this.video.load();
if (this.node._flVideoCombinePanel === this) {
@@ -836,6 +929,12 @@ class VideoCombinePanel {
app.registerExtension({
name: "ComfyUI.FL_VideoCombine",
setup() {
document.addEventListener("visibilitychange", () => {
if (document.hidden) pauseAllVideoCombinePreviews();
});
window.addEventListener("pagehide", pauseAllVideoCombinePreviews);
},
nodeCreated(node) {
if (node.comfyClass !== "FL_VideoCombine") return;