diff --git a/frontend/src/hooks/useVideoSource.ts b/frontend/src/hooks/useVideoSource.ts index e2a0a38d7..e6997f00c 100644 --- a/frontend/src/hooks/useVideoSource.ts +++ b/frontend/src/hooks/useVideoSource.ts @@ -83,8 +83,23 @@ export function useVideoSource(props?: UseVideoSourceProps) { video .play() .then(() => { - // Draw video frame to canvas at original resolution - const drawFrame = () => { + // Use captureStream(0) for manual frame capture to avoid + // GPU stalls from automatic ReadPixels at mismatched rates. + // See: https://github.com/daydreamlive/scope/issues/509 + const stream = canvas.captureStream(0); + const videoTrack = stream.getVideoTracks()[0]; + + // Draw video frames to canvas at the target FPS rate only, + // then manually request a frame capture. This avoids the + // "GPU stall due to ReadPixels" warning caused by drawing + // at ~60fps via requestAnimationFrame while capturing at a + // lower rate. + const intervalMs = 1000 / fps; + const intervalId = setInterval(() => { + if (video.paused || video.ended) { + clearInterval(intervalId); + return; + } ctx.drawImage( video, 0, @@ -92,14 +107,24 @@ export function useVideoSource(props?: UseVideoSourceProps) { detectedResolution.width, detectedResolution.height ); - if (!video.paused && !video.ended) { - requestAnimationFrame(drawFrame); + // Manually request frame capture from the stream + if ( + videoTrack && + "requestFrame" in videoTrack && + videoTrack.readyState === "live" + ) { + ( + videoTrack as MediaStreamTrack & { + requestFrame: () => void; + } + ).requestFrame(); } - }; - drawFrame(); + }, intervalMs); - // Capture stream from canvas at original resolution - const stream = canvas.captureStream(fps); + // Clean up interval when track ends + videoTrack?.addEventListener("ended", () => { + clearInterval(intervalId); + }); resolve({ stream, resolution: detectedResolution }); }) diff --git a/src/scope/core/plugins/manager.py b/src/scope/core/plugins/manager.py index 5605893ff..8d1ac0294 100644 --- a/src/scope/core/plugins/manager.py +++ b/src/scope/core/plugins/manager.py @@ -660,8 +660,10 @@ def _list_plugins_sync(self) -> list[dict[str, Any]]: ) # For git packages, use the git URL; for PyPI, use package name + # Strip .git suffix - not needed for pip/uv and causes issues + # when web platform parses the URL for GitHub API calls (#508) package_spec = ( - f"git+{git_url}" + f"git+{git_url.removesuffix('.git')}" if source == "git" and git_url else package_name )