diff --git a/src/components/video-editor/videoPlayback/videoEventHandlers.test.ts b/src/components/video-editor/videoPlayback/videoEventHandlers.test.ts index 52237aafa..0c272d5c2 100644 --- a/src/components/video-editor/videoPlayback/videoEventHandlers.test.ts +++ b/src/components/video-editor/videoPlayback/videoEventHandlers.test.ts @@ -2,12 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createVideoEventHandlers } from "./videoEventHandlers"; -type PresentedFrameCallback = (now: DOMHighResTimeStamp, metadata: { mediaTime?: number }) => void; - -type MockVideo = HTMLVideoElement & { - requestVideoFrameCallback?: (callback: PresentedFrameCallback) => number; - cancelVideoFrameCallback?: (handle: number) => void; -}; +type MockVideo = HTMLVideoElement; function createMutableRef(value: T) { return { current: value }; @@ -41,14 +36,16 @@ describe("createVideoEventHandlers", () => { vi.unstubAllGlobals(); }); - it("prefers requestVideoFrameCallback mediaTime when available", () => { - let presentedFrameCallback: PresentedFrameCallback | null = null; + it("advances from currentTime between presented video frames", () => { + let animationFrameCallback: FrameRequestCallback | null = null; + requestAnimationFrameMock.mockImplementation((callback: FrameRequestCallback) => { + animationFrameCallback = callback; + return 19; + }); + const requestVideoFrameCallback = vi.fn(() => 7); const video = createMockVideo({ - requestVideoFrameCallback: vi.fn((callback) => { - presentedFrameCallback = callback; - return 7; - }), - cancelVideoFrameCallback: vi.fn(), + currentTime: 0.5, + requestVideoFrameCallback, }); const onPlayStateChange = vi.fn(); const onTimeUpdate = vi.fn(); @@ -70,44 +67,14 @@ describe("createVideoEventHandlers", () => { handlers.handlePlay(); expect(onPlayStateChange).toHaveBeenCalledWith(true); - expect(video.requestVideoFrameCallback).toHaveBeenCalledTimes(1); - expect(requestAnimationFrameMock).not.toHaveBeenCalled(); - - presentedFrameCallback?.(0, { mediaTime: 1.25 }); - - expect(onTimeUpdate).toHaveBeenCalledWith(1.25); - expect(currentTimeRef.current).toBe(1250); - }); - - it("falls back to requestAnimationFrame when requestVideoFrameCallback is unavailable", () => { - let animationFrameCallback: FrameRequestCallback | null = null; - requestAnimationFrameMock.mockImplementation((callback: FrameRequestCallback) => { - animationFrameCallback = callback; - return 19; - }); - const video = createMockVideo({ currentTime: 0.75 }); - const onTimeUpdate = vi.fn(); - - const handlers = createVideoEventHandlers({ - video, - isSeekingRef: createMutableRef(false), - isPlayingRef: createMutableRef(false), - allowPlaybackRef: createMutableRef(true), - currentTimeRef: createMutableRef(0), - timeUpdateAnimationRef: createMutableRef(null), - onPlayStateChange: vi.fn(), - onTimeUpdate, - trimRegionsRef: createMutableRef([]), - speedRegionsRef: createMutableRef([]), - }); - - handlers.handlePlay(); expect(requestAnimationFrameMock).toHaveBeenCalledTimes(1); + expect(requestVideoFrameCallback).not.toHaveBeenCalled(); - video.paused = true; + video.currentTime = 1.25; animationFrameCallback?.(0); - expect(onTimeUpdate).toHaveBeenCalledWith(0.75); + expect(onTimeUpdate).toHaveBeenCalledWith(1.25); + expect(currentTimeRef.current).toBe(1250); }); it("skips removed footage when playback reaches a cut region", () => { @@ -139,12 +106,8 @@ describe("createVideoEventHandlers", () => { expect(onTimeUpdate).toHaveBeenLastCalledWith(2); }); - it("cancels a pending requestVideoFrameCallback on pause and dispose", () => { - const cancelVideoFrameCallback = vi.fn(); - const video = createMockVideo({ - requestVideoFrameCallback: vi.fn(() => 23), - cancelVideoFrameCallback, - }); + it("cancels a pending animation frame on pause and dispose", () => { + const video = createMockVideo(); const handlers = createVideoEventHandlers({ video, isSeekingRef: createMutableRef(false), @@ -160,12 +123,12 @@ describe("createVideoEventHandlers", () => { handlers.handlePlay(); handlers.handlePause(); - expect(cancelVideoFrameCallback).toHaveBeenCalledWith(23); + expect(cancelAnimationFrameMock).toHaveBeenCalledWith(11); - cancelVideoFrameCallback.mockClear(); + cancelAnimationFrameMock.mockClear(); handlers.handlePlay(); handlers.dispose(); - expect(cancelVideoFrameCallback).toHaveBeenCalledWith(23); + expect(cancelAnimationFrameMock).toHaveBeenCalledWith(11); }); it("skips removed footage after a paused seek", () => { diff --git a/src/components/video-editor/videoPlayback/videoEventHandlers.ts b/src/components/video-editor/videoPlayback/videoEventHandlers.ts index d3ac2db59..292066306 100644 --- a/src/components/video-editor/videoPlayback/videoEventHandlers.ts +++ b/src/components/video-editor/videoPlayback/videoEventHandlers.ts @@ -2,17 +2,6 @@ import type React from "react"; import { enablePitchPreservingPlayback } from "@/lib/mediaTiming"; import type { SpeedRegion, TrimRegion } from "../types"; -interface PresentedFrameMetadata { - mediaTime?: number; -} - -type PresentedFrameVideoElement = HTMLVideoElement & { - requestVideoFrameCallback?: ( - callback: (now: DOMHighResTimeStamp, metadata: PresentedFrameMetadata) => void, - ) => number; - cancelVideoFrameCallback?: (handle: number) => void; -}; - interface VideoEventHandlersParams { video: HTMLVideoElement; isSeekingRef: React.MutableRefObject; @@ -39,8 +28,6 @@ export function createVideoEventHandlers(params: VideoEventHandlersParams) { trimRegionsRef, speedRegionsRef, } = params; - const presentedFrameVideo = video as PresentedFrameVideoElement; - let videoFrameRequestId: number | null = null; enablePitchPreservingPlayback(video); const emitTime = (timeValue: number) => { @@ -84,14 +71,6 @@ export function createVideoEventHandlers(params: VideoEventHandlersParams) { cancelAnimationFrame(timeUpdateAnimationRef.current); timeUpdateAnimationRef.current = null; } - - if ( - videoFrameRequestId !== null && - typeof presentedFrameVideo.cancelVideoFrameCallback === "function" - ) { - presentedFrameVideo.cancelVideoFrameCallback(videoFrameRequestId); - videoFrameRequestId = null; - } }; const scheduleNextUpdate = () => { @@ -99,33 +78,17 @@ export function createVideoEventHandlers(params: VideoEventHandlersParams) { return; } - // Align editor state with the frame Chromium actually presented instead of - // polling `currentTime` on a generic animation frame. - if (typeof presentedFrameVideo.requestVideoFrameCallback === "function") { - videoFrameRequestId = presentedFrameVideo.requestVideoFrameCallback( - (_now, metadata) => { - videoFrameRequestId = null; - updateTime(metadata); - }, - ); - return; - } - + // Effects follow the continuous media clock even when a VFR source holds a frame. timeUpdateAnimationRef.current = requestAnimationFrame(() => { timeUpdateAnimationRef.current = null; updateTime(); }); }; - function getPresentedTime(metadata?: PresentedFrameMetadata): number { - const mediaTime = metadata?.mediaTime; - return Number.isFinite(mediaTime) ? (mediaTime ?? 0) : video.currentTime; - } - - function updateTime(metadata?: PresentedFrameMetadata) { + function updateTime() { if (!video) return; - const presentedTime = getPresentedTime(metadata); + const presentedTime = video.currentTime; const currentTimeMs = presentedTime * 1000; const activeTrimRegion = findActiveTrimRegion(currentTimeMs);