Bin
2025-12-16 9e0b2ba2c317b1a86212f24cbae3195ad1f3dbfa
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
import { type MutableRefObject, useCallback, useEffect, useRef } from "react";
import { FF_VIDEO_FRAME_SEEK_PRECISION, isFF } from "../../../utils/feature-flags";
import type { VideoRef } from "../VideoCanvas";
 
type UseLoopRangeProps = {
  loopFrameRange?: boolean;
  selectedFrameRange?: { start: number; end: number };
  framerate: number;
  onRedrawRequest?: () => void;
  videoRef: MutableRefObject<HTMLVideoElement | undefined>;
  refSource: VideoRef;
};
 
type UseLoopRangeReturn = {
  prepareLoop: () => void;
};
 
export const useLoopRange = ({
  loopFrameRange,
  selectedFrameRange,
  framerate,
  onRedrawRequest,
  videoRef,
  refSource,
}: UseLoopRangeProps): UseLoopRangeReturn => {
  const framerateRef = useRef(framerate);
  framerateRef.current = framerate;
  const sourceRef = useRef(refSource);
  sourceRef.current = refSource;
  const onRedrawRequestRef = useRef(onRedrawRequest);
  onRedrawRequestRef.current = onRedrawRequest;
  const videoFrameCallbackIdRef = useRef<number | null>(null);
  const loopFrameRangeRef = useRef(loopFrameRange ?? false);
  loopFrameRangeRef.current = loopFrameRange ?? false;
  const handeFrameChange = useCallback(
    (_timestamp: number, { mediaTime }: { mediaTime: number }) => {
      const video = videoRef.current;
      if (!video || video.paused) return;
      if (!selectedFrameRange) return;
      const startFrame = selectedFrameRange.start;
      const endFrame = selectedFrameRange.end;
 
      const currentFrame = isFF(FF_VIDEO_FRAME_SEEK_PRECISION)
        ? Math.ceil(mediaTime * framerateRef.current)
        : Math.round(mediaTime * framerateRef.current);
 
      if (currentFrame < startFrame) {
        // If current time is before the start of the range, seek to the start
        sourceRef.current.goToFrame(startFrame);
      } else if (currentFrame >= endFrame) {
        if (loopFrameRangeRef.current) {
          // If looping is enabled, reset to the start of the range
          sourceRef.current.goToFrame(endFrame);
          onRedrawRequestRef.current?.();
          videoFrameCallbackIdRef.current = video.requestVideoFrameCallback(() => {
            sourceRef.current.goToFrame(startFrame);
            onRedrawRequestRef.current?.();
            videoFrameCallbackIdRef.current = video.requestVideoFrameCallback(handeFrameChange);
          });
          return;
        }
        // If not looping, pause the video at the end of the range
        sourceRef.current.pause();
        sourceRef.current.goToFrame(endFrame);
        onRedrawRequestRef.current?.();
        return;
      }
      videoFrameCallbackIdRef.current = video.requestVideoFrameCallback(handeFrameChange);
    },
    [selectedFrameRange],
  );
  const watchFrameChange = useCallback(() => {
    const cancelCallback = () => {
      if (videoFrameCallbackIdRef.current) {
        videoRef.current?.cancelVideoFrameCallback(videoFrameCallbackIdRef.current);
        videoFrameCallbackIdRef.current = null;
      }
    };
 
    cancelCallback();
    const video = videoRef.current;
    if (video && selectedFrameRange) {
      videoFrameCallbackIdRef.current = video.requestVideoFrameCallback(handeFrameChange);
    }
    return cancelCallback;
  }, [selectedFrameRange]);
  useEffect(() => {
    return watchFrameChange();
  }, [selectedFrameRange]);
 
  const prepareLoop = useCallback(() => {
    if (selectedFrameRange && videoRef.current) {
      const startTime = (selectedFrameRange.start - 1) / framerateRef.current;
      const endTime = (selectedFrameRange.end - 1) / framerateRef.current;
 
      if (videoRef.current.currentTime < startTime || videoRef.current.currentTime >= endTime) {
        sourceRef.current.goToFrame(selectedFrameRange.start);
      }
      watchFrameChange();
    }
  }, [selectedFrameRange]);
 
  return { prepareLoop };
};