mirror of
				https://github.com/blakeblackshear/frigate.git
				synced 2025-10-27 10:52:11 +01:00 
			
		
		
		
	Recording scrubbing fixes (#10439)
* use a single source of truth for scrubbing * simplify controller state * Cleanup scrubbing logic * Apply same logic to mobile --------- Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
This commit is contained in:
		
							parent
							
								
									2decdeadb4
								
							
						
					
					
						commit
						39a29d148e
					
				@ -9,7 +9,6 @@ export class DynamicVideoController {
 | 
			
		||||
  public camera = "";
 | 
			
		||||
  private playerController: HTMLVideoElement;
 | 
			
		||||
  private previewController: PreviewController;
 | 
			
		||||
  private setScrubbing: (isScrubbing: boolean) => void;
 | 
			
		||||
  private setFocusedItem: (timeline: Timeline) => void;
 | 
			
		||||
  private playerMode: PlayerMode = "playback";
 | 
			
		||||
 | 
			
		||||
@ -24,7 +23,6 @@ export class DynamicVideoController {
 | 
			
		||||
    previewController: PreviewController,
 | 
			
		||||
    annotationOffset: number,
 | 
			
		||||
    defaultMode: PlayerMode,
 | 
			
		||||
    setScrubbing: (isScrubbing: boolean) => void,
 | 
			
		||||
    setFocusedItem: (timeline: Timeline) => void,
 | 
			
		||||
  ) {
 | 
			
		||||
    this.camera = camera;
 | 
			
		||||
@ -32,7 +30,6 @@ export class DynamicVideoController {
 | 
			
		||||
    this.previewController = previewController;
 | 
			
		||||
    this.annotationOffset = annotationOffset;
 | 
			
		||||
    this.playerMode = defaultMode;
 | 
			
		||||
    this.setScrubbing = setScrubbing;
 | 
			
		||||
    this.setFocusedItem = setFocusedItem;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -50,11 +47,6 @@ export class DynamicVideoController {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  seekToTimestamp(time: number, play: boolean = false) {
 | 
			
		||||
    if (this.playerMode != "playback") {
 | 
			
		||||
      this.playerMode = "playback";
 | 
			
		||||
      this.setScrubbing(false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
      this.recordings.length == 0 ||
 | 
			
		||||
      time < this.recordings[0].start_time ||
 | 
			
		||||
@ -64,6 +56,10 @@ export class DynamicVideoController {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this.playerMode != "playback") {
 | 
			
		||||
      this.playerMode = "playback";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let seekSeconds = 0;
 | 
			
		||||
    (this.recordings || []).every((segment) => {
 | 
			
		||||
      // if the next segment is past the desired time, stop calculating
 | 
			
		||||
@ -126,7 +122,6 @@ export class DynamicVideoController {
 | 
			
		||||
    if (scrubResult && this.playerMode != "scrubbing") {
 | 
			
		||||
      this.playerMode = "scrubbing";
 | 
			
		||||
      this.playerController.pause();
 | 
			
		||||
      this.setScrubbing(true);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -21,6 +21,7 @@ type DynamicVideoPlayerProps = {
 | 
			
		||||
  onControllerReady: (controller: DynamicVideoController) => void;
 | 
			
		||||
  onTimestampUpdate?: (timestamp: number) => void;
 | 
			
		||||
  onClipEnded?: () => void;
 | 
			
		||||
  isScrubbing: boolean;
 | 
			
		||||
};
 | 
			
		||||
export default function DynamicVideoPlayer({
 | 
			
		||||
  className,
 | 
			
		||||
@ -31,6 +32,7 @@ export default function DynamicVideoPlayer({
 | 
			
		||||
  onControllerReady,
 | 
			
		||||
  onTimestampUpdate,
 | 
			
		||||
  onClipEnded,
 | 
			
		||||
  isScrubbing,
 | 
			
		||||
}: DynamicVideoPlayerProps) {
 | 
			
		||||
  const apiHost = useApiHost();
 | 
			
		||||
  const { data: config } = useSWR<FrigateConfig>("config");
 | 
			
		||||
@ -53,7 +55,6 @@ export default function DynamicVideoPlayer({
 | 
			
		||||
  const playerRef = useRef<HTMLVideoElement | null>(null);
 | 
			
		||||
  const [previewController, setPreviewController] =
 | 
			
		||||
    useState<PreviewController | null>(null);
 | 
			
		||||
  const [isScrubbing, setIsScrubbing] = useState(false);
 | 
			
		||||
  const [focusedItem, setFocusedItem] = useState<Timeline | undefined>(
 | 
			
		||||
    undefined,
 | 
			
		||||
  );
 | 
			
		||||
@ -67,8 +68,7 @@ export default function DynamicVideoPlayer({
 | 
			
		||||
      playerRef.current,
 | 
			
		||||
      previewController,
 | 
			
		||||
      (config.cameras[camera]?.detect?.annotation_offset || 0) / 1000,
 | 
			
		||||
      "playback",
 | 
			
		||||
      setIsScrubbing,
 | 
			
		||||
      isScrubbing ? "scrubbing" : "playback",
 | 
			
		||||
      setFocusedItem,
 | 
			
		||||
    );
 | 
			
		||||
    // we only want to fire once when players are ready
 | 
			
		||||
@ -133,6 +133,10 @@ export default function DynamicVideoPlayer({
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (playerRef.current) {
 | 
			
		||||
      playerRef.current.autoplay = !isScrubbing;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setSource(
 | 
			
		||||
      `${apiHost}vod/${camera}/start/${timeRange.start}/end/${timeRange.end}/master.m3u8`,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
@ -89,13 +89,16 @@ export function DesktopRecordingView({
 | 
			
		||||
  const [playerTime, setPlayerTime] = useState(startTime);
 | 
			
		||||
 | 
			
		||||
  const updateSelectedSegment = useCallback(
 | 
			
		||||
    (currentTime: number) => {
 | 
			
		||||
    (currentTime: number, updateStartTime: boolean) => {
 | 
			
		||||
      const index = timeRange.ranges.findIndex(
 | 
			
		||||
        (seg) => seg.start <= currentTime && seg.end >= currentTime,
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      if (index != -1) {
 | 
			
		||||
        setPlaybackStart(currentTime);
 | 
			
		||||
        if (updateStartTime) {
 | 
			
		||||
          setPlaybackStart(currentTime);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        setSelectedRangeIdx(index);
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
@ -108,7 +111,7 @@ export function DesktopRecordingView({
 | 
			
		||||
        currentTime > currentTimeRange.end + 60 ||
 | 
			
		||||
        currentTime < currentTimeRange.start - 60
 | 
			
		||||
      ) {
 | 
			
		||||
        updateSelectedSegment(currentTime);
 | 
			
		||||
        updateSelectedSegment(currentTime, false);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@ -126,40 +129,22 @@ export function DesktopRecordingView({
 | 
			
		||||
    updateSelectedSegment,
 | 
			
		||||
  ]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (!scrubbing) {
 | 
			
		||||
      if (
 | 
			
		||||
        currentTimeRange.start <= currentTime &&
 | 
			
		||||
        currentTimeRange.end >= currentTime
 | 
			
		||||
      ) {
 | 
			
		||||
        mainControllerRef.current?.seekToTimestamp(currentTime, true);
 | 
			
		||||
      } else {
 | 
			
		||||
        updateSelectedSegment(currentTime);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // we only want to seek when user stops scrubbing
 | 
			
		||||
    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
			
		||||
  }, [scrubbing]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (!scrubbing) {
 | 
			
		||||
      if (Math.abs(currentTime - playerTime) > 10) {
 | 
			
		||||
        mainControllerRef.current?.pause();
 | 
			
		||||
 | 
			
		||||
        if (
 | 
			
		||||
          currentTimeRange.start <= currentTime &&
 | 
			
		||||
          currentTimeRange.end >= currentTime
 | 
			
		||||
        ) {
 | 
			
		||||
          mainControllerRef.current?.seekToTimestamp(currentTime, true);
 | 
			
		||||
        } else {
 | 
			
		||||
          updateSelectedSegment(currentTime);
 | 
			
		||||
          updateSelectedSegment(currentTime, true);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    // we only want to seek when current time doesn't match the player update time
 | 
			
		||||
    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
			
		||||
  }, [currentTime]);
 | 
			
		||||
  }, [currentTime, scrubbing]);
 | 
			
		||||
 | 
			
		||||
  const onSelectCamera = useCallback(
 | 
			
		||||
    (newCam: string) => {
 | 
			
		||||
@ -234,6 +219,7 @@ export function DesktopRecordingView({
 | 
			
		||||
                onControllerReady={(controller) => {
 | 
			
		||||
                  mainControllerRef.current = controller;
 | 
			
		||||
                }}
 | 
			
		||||
                isScrubbing={scrubbing}
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
            <div className="w-full flex justify-center gap-2 overflow-x-auto">
 | 
			
		||||
@ -357,8 +343,24 @@ export function MobileRecordingView({
 | 
			
		||||
  // scrubbing and timeline state
 | 
			
		||||
 | 
			
		||||
  const [scrubbing, setScrubbing] = useState(false);
 | 
			
		||||
  const [currentTime, setCurrentTime] = useState<number>(
 | 
			
		||||
    startTime || Date.now() / 1000,
 | 
			
		||||
  const [currentTime, setCurrentTime] = useState<number>(startTime);
 | 
			
		||||
  const [playerTime, setPlayerTime] = useState(startTime);
 | 
			
		||||
 | 
			
		||||
  const updateSelectedSegment = useCallback(
 | 
			
		||||
    (currentTime: number, updateStartTime: boolean) => {
 | 
			
		||||
      const index = timeRange.ranges.findIndex(
 | 
			
		||||
        (seg) => seg.start <= currentTime && seg.end >= currentTime,
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      if (index != -1) {
 | 
			
		||||
        if (updateStartTime) {
 | 
			
		||||
          setPlaybackStart(currentTime);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        setSelectedRangeIdx(index);
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    [timeRange],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
@ -367,28 +369,36 @@ export function MobileRecordingView({
 | 
			
		||||
        currentTime > currentTimeRange.end + 60 ||
 | 
			
		||||
        currentTime < currentTimeRange.start - 60
 | 
			
		||||
      ) {
 | 
			
		||||
        const index = timeRange.ranges.findIndex(
 | 
			
		||||
          (seg) => seg.start <= currentTime && seg.end >= currentTime,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        if (index != -1) {
 | 
			
		||||
          setSelectedRangeIdx(index);
 | 
			
		||||
        }
 | 
			
		||||
        updateSelectedSegment(currentTime, false);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      controllerRef.current?.scrubToTimestamp(currentTime);
 | 
			
		||||
    }
 | 
			
		||||
  }, [currentTime, scrubbing, currentTimeRange, timeRange]);
 | 
			
		||||
  }, [
 | 
			
		||||
    currentTime,
 | 
			
		||||
    scrubbing,
 | 
			
		||||
    timeRange,
 | 
			
		||||
    currentTimeRange,
 | 
			
		||||
    updateSelectedSegment,
 | 
			
		||||
  ]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (!scrubbing) {
 | 
			
		||||
      controllerRef.current?.seekToTimestamp(currentTime, true);
 | 
			
		||||
      if (Math.abs(currentTime - playerTime) > 10) {
 | 
			
		||||
        if (
 | 
			
		||||
          currentTimeRange.start <= currentTime &&
 | 
			
		||||
          currentTimeRange.end >= currentTime
 | 
			
		||||
        ) {
 | 
			
		||||
          controllerRef.current?.seekToTimestamp(currentTime, true);
 | 
			
		||||
        } else {
 | 
			
		||||
          updateSelectedSegment(currentTime, true);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // we only want to seek when user stops scrubbing
 | 
			
		||||
    // we only want to seek when current time doesn't match the player update time
 | 
			
		||||
    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
			
		||||
  }, [scrubbing]);
 | 
			
		||||
  }, [currentTime, scrubbing]);
 | 
			
		||||
 | 
			
		||||
  // motion timeline data
 | 
			
		||||
 | 
			
		||||
@ -450,8 +460,12 @@ export function MobileRecordingView({
 | 
			
		||||
          onControllerReady={(controller) => {
 | 
			
		||||
            controllerRef.current = controller;
 | 
			
		||||
          }}
 | 
			
		||||
          onTimestampUpdate={setCurrentTime}
 | 
			
		||||
          onTimestampUpdate={(timestamp) => {
 | 
			
		||||
            setPlayerTime(timestamp);
 | 
			
		||||
            setCurrentTime(timestamp);
 | 
			
		||||
          }}
 | 
			
		||||
          onClipEnded={onClipEnded}
 | 
			
		||||
          isScrubbing={scrubbing}
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user