diff --git a/web/power_load_video.js b/web/power_load_video.js index 0ec9461..f0fb98a 100644 --- a/web/power_load_video.js +++ b/web/power_load_video.js @@ -141,16 +141,19 @@ class PowerLoadVideoTopRowWidget extends RgthreeBaseWidget { // Setup event handlers this.hitAreas.refreshButton.onClick = async () => { if (node.timelineWidget) { - // Stop current playback, reset to frame 1 + // Stop current playback, reset to [ marker position node.timelineWidget.stopPlayback(); node.timelineWidget.value.isPlaying = false; - node.timelineWidget.value.currentFrame = 1; + const startMarker = node.timelineWidget.startFrameMarker || 1; + node.timelineWidget.value.currentFrame = startMarker; // Apply new FPS as playback rate node.timelineWidget.applyPlaybackRate(); } - if (node.videoElement) { - // Reset video to start - node.videoElement.currentTime = 0; + if (node.videoElement && node.timelineWidget) { + // Reset video to [ marker position + const nativeFPS = node.timelineWidget.nativeFPS || 24; + const startMarker = node.timelineWidget.startFrameMarker || 1; + node.videoElement.currentTime = (startMarker - 1) / nativeFPS; } // Restart playback at the new FPS if (node.timelineWidget) { @@ -285,12 +288,15 @@ class PowerLoadVideoTopRowWidget extends RgthreeBaseWidget { node.widgets_values[0] = uploadedName; } - // Load the video into the display if loadVideoIntoDisplay is available + // Load the video into the display directly (bypassing execute) if (node.loadVideoIntoDisplay && typeof node.loadVideoIntoDisplay === 'function') { node.loadVideoIntoDisplay(uploadedName); } console.log('[PowerLoadVideo] Video uploaded:', uploadedName); + + // Force canvas redraw to show the video + app.graph.setDirtyCanvas(true, true); } else { console.error('[PowerLoadVideo] Upload failed:', resp.status, resp.statusText); } @@ -417,9 +423,9 @@ class PowerLoadVideoTimelineWidget extends RgthreeBaseWidget { setNativeFPS(node, fps) { if (fps && fps > 0) { this.nativeFPS = fps; - // Recalculate total frames if we have duration + // Recalculate total frames if we have duration - use ceiling to avoid truncation if (this.videoElement && this.videoElement.duration && isFinite(this.videoElement.duration)) { - const totalFrames = Math.round(this.videoElement.duration * this.nativeFPS); + const totalFrames = Math.ceil(this.videoElement.duration * this.nativeFPS); this.setTotalFrames(node, totalFrames); } // Re-apply playback rate with new native FPS @@ -746,7 +752,8 @@ class PowerLoadVideoTimelineWidget extends RgthreeBaseWidget { } /** - * Start video playback animation + * Start video playback using native HTML5 video element + * The animation loop now reads from the video's actual position instead of driving it. */ startPlayback(node) { // Fix #3: Cancel any existing animation first for safety against memory leaks @@ -759,32 +766,58 @@ class PowerLoadVideoTimelineWidget extends RgthreeBaseWidget { const frameInterval = 1000 / fps; const nativeFPS = this.nativeFPS || 24; + // Enable loop mode and start playback + if (this.videoElement && this.videoElement.duration) { + // Use native looping for seamless playback within bounds + this.videoElement.loop = true; + + // Start actual video playback - let the browser handle buffering naturally + const playPromise = this.videoElement.play(); + if (playPromise) { + playPromise.catch(err => { + console.warn('[PowerLoadVideo] Play promise rejected:', err); + this.value.isPlaying = false; + if (node._stopHoverAudio) node._stopHoverAudio(); + }); + } + } + + // Animation loop reads from video position instead of driving it const animate = () => { - const now = performance.now(); + if (!this.videoElement || !this.value.isPlaying) { + this.stopPlayback(); + return; + } - if (now - this.lastUpdateTime >= frameInterval) { - // Check if we're about to loop - const wasAtEnd = this.value.currentFrame >= this.value.totalFrames; + // Read current frame from video's actual playback position + if (this.videoElement.duration && !isNaN(this.videoElement.currentTime)) { + let currentTime = this.videoElement.currentTime; - this.value.currentFrame++; + // Read marker positions dynamically each frame (not captured in closure) + const startMarker = this.startFrameMarker || 1; + const endMarker = this.endFrameMarker !== null && this.endFrameMarker >= 1 + ? this.endFrameMarker + : this.value.totalFrames; + const startTime = (startMarker - 1) / nativeFPS; + const endTime = (endMarker - 1) / nativeFPS; + + // Handle looping: if we've gone past endMarker, seek back to start + if (currentTime >= endTime) { + currentTime = startTime; + this.videoElement.currentTime = startTime; - // Loop at end - if (this.value.currentFrame > this.value.totalFrames) { - this.value.currentFrame = 1; // Restart hover audio on loop if still hovering and playing - if (wasAtEnd && node._isHovering && node._restartHoverAudioOnLoop) { + if (node._isHovering && node._restartHoverAudioOnLoop) { node._restartHoverAudioOnLoop(); } } - // Update video element time using native FPS for correct mapping - if (this.videoElement && this.videoElement.duration) { - const newTime = (this.value.currentFrame - 1) / nativeFPS; - this.videoElement.currentTime = Math.min(newTime, this.videoElement.duration); + // Convert time to frame number for display + const currentFrame = Math.floor(currentTime * nativeFPS) + 1; + if (this.value.currentFrame !== currentFrame) { + this.value.currentFrame = currentFrame; + node.setDirtyCanvas(true, true); } - - this.lastUpdateTime = now; - node.setDirtyCanvas(true, true); } this.animationId = requestAnimationFrame(animate); @@ -795,9 +828,20 @@ class PowerLoadVideoTimelineWidget extends RgthreeBaseWidget { } /** - * Stop video playback animation + * Stop video playback and pause the video element */ stopPlayback() { + // Pause the actual video element + if (this.videoElement && !this.videoElement.paused) { + this.videoElement.pause(); + } + + // Disable native looping when stopped + if (this.videoElement) { + this.videoElement.loop = false; + } + + // Cancel the animation loop if (this.animationId) { cancelAnimationFrame(this.animationId); this.animationId = null; @@ -822,7 +866,8 @@ class PowerLoadVideoTimelineWidget extends RgthreeBaseWidget { if (this.videoElement && this.videoElement.duration) { const nativeFPS = this.nativeFPS || 24; const newTime = (newFrame - 1) / nativeFPS; - this.videoElement.currentTime = Math.min(newTime, this.videoElement.duration); + const clampedTime = Math.min(newTime, this.videoElement.duration); + this.videoElement.currentTime = clampedTime; } node.setDirtyCanvas(true, true); @@ -1369,9 +1414,10 @@ app.registerExtension({ display: block; `; videoElement.muted = true; - videoElement.preload = 'auto'; + videoElement.preload = 'metadata'; // Changed from 'auto' - let browser manage buffering videoElement.playsInline = true; + // Store reference on node for later access this.videoElement = videoElement; @@ -1446,16 +1492,15 @@ app.registerExtension({ this.widgets_values[0] = uploadedName; } - // Directly load the video into our custom display (bypassing combo widget) - // The loadVideoIntoDisplay function is defined later in onNodeCreated - if (typeof loadVideoIntoDisplay === 'function') { - loadVideoIntoDisplay(uploadedName); + // Directly load the video into our custom display using node method + if (typeof this.loadVideoIntoDisplay === 'function') { + this.loadVideoIntoDisplay(uploadedName); } else { console.error('[PowerLoadVideo] loadVideoIntoDisplay not available yet, will retry'); // Retry after a short delay setTimeout(() => { - if (typeof loadVideoIntoDisplay === 'function') { - loadVideoIntoDisplay(uploadedName); + if (typeof this.loadVideoIntoDisplay === 'function') { + this.loadVideoIntoDisplay(uploadedName); } }, 50); } @@ -1620,8 +1665,8 @@ app.registerExtension({ } }, 0); - // Function to load video into custom display - const loadVideoIntoDisplay = (videoFilename) => { + // Function to load video into custom display - attach to node for external access + this.loadVideoIntoDisplay = (videoFilename) => { if (!videoFilename || String(videoElement.src).includes(videoFilename)) { return; } @@ -1660,7 +1705,13 @@ app.registerExtension({ this.timelineWidget.setFPS(this, detectedFPS); // Use accurate frame count from server - const totalFrames = meta.frame_count > 0 ? meta.frame_count : Math.round(videoElement.duration * meta.fps); + let totalFrames; + if (meta.frame_count > 0) { + totalFrames = meta.frame_count; + } else { + // Calculate from duration with ceiling to ensure we don't truncate the last frame + totalFrames = Math.ceil(videoElement.duration * meta.fps); + } this.timelineWidget.setTotalFrames(this, totalFrames); this.timelineWidget.setStartFrame(1, this); this.timelineWidget.setEndFrame(totalFrames, this); @@ -1675,7 +1726,9 @@ app.registerExtension({ // Fallback: estimate with current FPS const nativeFPS = this.timelineWidget.nativeFPS || this.timelineWidget.value.fps || 24; - const totalFrames = Math.round(videoElement.duration * nativeFPS); + // Use ceiling to ensure we capture the full video duration without truncation + // Add small buffer (1 frame) to account for any edge case timing issues + const totalFrames = Math.ceil(videoElement.duration * nativeFPS) + 1; this.timelineWidget.setTotalFrames(this, totalFrames); this.timelineWidget.setStartFrame(1, this); this.timelineWidget.setEndFrame(totalFrames, this); @@ -1721,7 +1774,9 @@ app.registerExtension({ videoFilename = this.videoFilename; } - loadVideoIntoDisplay(videoFilename); + if (typeof this.loadVideoIntoDisplay === 'function') { + this.loadVideoIntoDisplay(videoFilename); + } }.bind(this); // Watch for drag-drop changes on the combo widget (before execute) @@ -1731,11 +1786,13 @@ app.registerExtension({ // Store original callback if any const originalCallback = videoWidget.callback; videoWidget.callback = function(value) { - loadVideoIntoDisplay(value); + if (typeof this.loadVideoIntoDisplay === 'function') { + this.loadVideoIntoDisplay(value); + } if (originalCallback) { originalCallback.apply(this, arguments); } - }; + }.bind(this); } }, 100); }; @@ -1761,7 +1818,8 @@ app.registerExtension({ // Recalculate total frames with accurate native FPS + duration if (this.timelineWidget && this.videoElement && this.videoElement.duration && isFinite(this.videoElement.duration)) { - const accurateFrames = Math.round(this.videoElement.duration * this.timelineWidget.nativeFPS); + // Use ceiling to ensure full video playback without truncation at the end + const accurateFrames = Math.ceil(this.videoElement.duration * this.timelineWidget.nativeFPS); if (accurateFrames > 0) { this.timelineWidget.setTotalFrames(this, accurateFrames); this.timelineWidget.setStartFrame(1, this);