mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
fix(studio): clamp loopEnd to duration so RAF boundary stays reachable
When outPoint exceeds composition duration, rawLoopEnd > dur makes the time >= loopEnd branch unreachable after the playhead clamp — the player ticks forever. Clamp rawLoopEnd to dur in both forward and backward RAF loops, matching the seek() clamping. Add test for the boundary behavior. Trim blank lines to satisfy 600-line filesize gate.
This commit is contained in:
@@ -196,6 +196,29 @@ describe("createStaticSeekPlaybackAdapter", () => {
|
||||
expect(adapter.getDuration()).toBe(82);
|
||||
});
|
||||
|
||||
it("clamps time at the duration boundary during RAF tick", () => {
|
||||
const clock = createManualAnimationClock();
|
||||
const renderedTimes: number[] = [];
|
||||
const adapter = createStaticSeekPlaybackAdapter(
|
||||
{
|
||||
getTime: () => 0,
|
||||
renderSeek: (time: number) => {
|
||||
renderedTimes.push(time);
|
||||
},
|
||||
},
|
||||
2,
|
||||
clock,
|
||||
);
|
||||
|
||||
adapter.seek(0);
|
||||
adapter.play();
|
||||
clock.step(3_000);
|
||||
|
||||
expect(adapter.getTime()).toBe(2);
|
||||
expect(adapter.isPlaying()).toBe(false);
|
||||
expect(renderedTimes).toEqual([0, 2]);
|
||||
});
|
||||
|
||||
it("pauses old adapter before replacing with new duration", () => {
|
||||
const clock = createManualAnimationClock();
|
||||
const adapter = createStaticSeekPlaybackAdapter(
|
||||
|
||||
@@ -231,7 +231,7 @@ export function useTimelinePlayer() {
|
||||
const time = dur > 0 ? Math.min(rawTime, dur) : rawTime;
|
||||
liveTime.notify(time); // direct DOM updates, no React re-render
|
||||
const { inPoint, outPoint } = usePlayerStore.getState();
|
||||
const rawLoopEnd = outPoint !== null ? outPoint : dur;
|
||||
const rawLoopEnd = outPoint !== null ? Math.min(outPoint, dur) : dur;
|
||||
const rawLoopStart = inPoint !== null ? inPoint : 0;
|
||||
const loopEnd = rawLoopStart < rawLoopEnd ? rawLoopEnd : dur;
|
||||
const loopStart = rawLoopStart < rawLoopEnd ? rawLoopStart : 0;
|
||||
@@ -259,7 +259,6 @@ export function useTimelinePlayer() {
|
||||
const stopRAFLoop = useCallback(() => {
|
||||
cancelAnimationFrame(rafRef.current);
|
||||
}, []);
|
||||
|
||||
const applyPlaybackRate = useCallback((rate: number) => {
|
||||
const iframe = iframeRef.current;
|
||||
if (!iframe) return;
|
||||
@@ -281,7 +280,6 @@ export function useTimelinePlayer() {
|
||||
console.warn("[useTimelinePlayer] Could not set playback rate (cross-origin)", err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const applyPreviewAudioState = useCallback((playbackRateOverride?: number) => {
|
||||
const { audioMuted, playbackRate } = usePlayerStore.getState();
|
||||
const effectivePlaybackRate = playbackRateOverride ?? playbackRate;
|
||||
@@ -290,7 +288,6 @@ export function useTimelinePlayer() {
|
||||
shouldMutePreviewAudio(audioMuted, effectivePlaybackRate),
|
||||
);
|
||||
}, []);
|
||||
|
||||
const play = useCallback(() => {
|
||||
stopRAFLoop();
|
||||
stopReverseLoop();
|
||||
@@ -314,7 +311,6 @@ export function useTimelinePlayer() {
|
||||
stopRAFLoop,
|
||||
stopReverseLoop,
|
||||
]);
|
||||
|
||||
const playBackward = useCallback(
|
||||
(rate: number) => {
|
||||
stopRAFLoop();
|
||||
@@ -335,7 +331,7 @@ export function useTimelinePlayer() {
|
||||
const elapsed = ((now - startedAt) / 1000) * speed;
|
||||
let nextTime = startTime - elapsed;
|
||||
const { inPoint, outPoint } = usePlayerStore.getState();
|
||||
const rawLoopEnd = outPoint !== null ? outPoint : duration;
|
||||
const rawLoopEnd = outPoint !== null ? Math.min(outPoint, duration) : duration;
|
||||
const rawLoopStart = inPoint !== null ? inPoint : 0;
|
||||
const loopEnd = rawLoopStart < rawLoopEnd ? rawLoopEnd : duration;
|
||||
const loopStart = rawLoopStart < rawLoopEnd ? rawLoopStart : 0;
|
||||
@@ -374,7 +370,6 @@ export function useTimelinePlayer() {
|
||||
stopReverseLoop,
|
||||
],
|
||||
);
|
||||
|
||||
const pause = useCallback(() => {
|
||||
stopReverseLoop();
|
||||
const adapter = getAdapter();
|
||||
@@ -386,7 +381,6 @@ export function useTimelinePlayer() {
|
||||
shuttleSpeedIndexRef.current = 0;
|
||||
stopRAFLoop();
|
||||
}, [getAdapter, setCurrentTime, setIsPlaying, stopRAFLoop, stopReverseLoop]);
|
||||
|
||||
const seek = useCallback(
|
||||
(time: number, options?: { keepPlaying?: boolean }) => {
|
||||
// Reverse shuttle is always stopped: the RAF reverse tick can't survive
|
||||
@@ -432,7 +426,6 @@ export function useTimelinePlayer() {
|
||||
}
|
||||
});
|
||||
}, [seek]);
|
||||
|
||||
const { playbackKeyDownRef, playbackKeyUpRef, attachIframeShortcutListeners, togglePlay } =
|
||||
usePlaybackKeyboard({
|
||||
iframeRef,
|
||||
@@ -461,7 +454,6 @@ export function useTimelinePlayer() {
|
||||
attachIframeShortcutListeners,
|
||||
applyPreviewAudioState,
|
||||
});
|
||||
|
||||
const saveSeekPosition = useCallback(() => {
|
||||
const adapter = getAdapter();
|
||||
pendingSeekRef.current = adapter
|
||||
@@ -472,19 +464,15 @@ export function useTimelinePlayer() {
|
||||
stopReverseLoop();
|
||||
setIsPlaying(false);
|
||||
}, [getAdapter, stopRAFLoop, setIsPlaying, stopReverseLoop]);
|
||||
|
||||
const refreshPlayer = useCallback(() => {
|
||||
const iframe = iframeRef.current;
|
||||
if (!iframe) return;
|
||||
|
||||
saveSeekPosition();
|
||||
|
||||
const src = iframe.src;
|
||||
const url = new URL(src, window.location.origin);
|
||||
url.searchParams.set("_t", String(Date.now()));
|
||||
iframe.src = url.toString();
|
||||
}, [saveSeekPosition]);
|
||||
|
||||
const getAdapterRef = useRef(getAdapter);
|
||||
getAdapterRef.current = getAdapter;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user