frigate/web/src/components/player/dynamic/DynamicVideoController.ts
Nicolas Mowen 224cbdc2d6
Some checks failed
CI / AMD64 Build (push) Has been cancelled
CI / ARM Build (push) Has been cancelled
CI / Jetson Jetpack 6 (push) Has been cancelled
CI / AMD64 Extra Build (push) Has been cancelled
CI / ARM Extra Build (push) Has been cancelled
CI / Synaptics Build (push) Has been cancelled
CI / Assemble and push default build (push) Has been cancelled
Miscellaneous Fixes (#20989)
* Include DB in safe mode config

Copy DB when going into safe mode to avoid creating a new one if a user has configured a separate location

* Fix documentation for example log module

* Set minimum duration for recording segments

Due to the inpoint logic, some recordings would get clipped on the end of the segment with a non-zero duration but not enough duration to include a frame. 100 ms is a safe value for any video that is 10fps or higher to have a frame

* Add docs to explain object assignment for classification

* Add warning for Intel GPU stats bug

Add warning with explanation on GPU stats page when all Intel GPU values are 0

* Update docs with creation instructions

* reset loading state when moving through events in tracking details

* disable pip on preview players

* Improve HLS handling for startPosition

The startPosition was incorrectly calculated assuming continuous recordings, when it needs to consider only some segments exist. This extracts that logic to a utility so all can use it.

---------

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
2025-11-21 15:40:58 -06:00

171 lines
4.5 KiB
TypeScript

import { Recording } from "@/types/record";
import { DynamicPlayback } from "@/types/playback";
import { PreviewController } from "../PreviewPlayer";
import { TimeRange, TrackingDetailsSequence } from "@/types/timeline";
import {
calculateInpointOffset,
calculateSeekPosition,
} from "@/utils/videoUtil";
type PlayerMode = "playback" | "scrubbing";
export class DynamicVideoController {
// main state
public camera = "";
private playerController: HTMLVideoElement;
private previewController: PreviewController;
private setNoRecording: (noRecs: boolean) => void;
private setFocusedItem: (timeline: TrackingDetailsSequence) => void;
private playerMode: PlayerMode = "playback";
// playback
private recordings: Recording[] = [];
private timeRange: TimeRange = { after: 0, before: 0 };
private inpointOffset: number = 0;
private annotationOffset: number;
private timeToStart: number | undefined = undefined;
constructor(
camera: string,
playerController: HTMLVideoElement,
previewController: PreviewController,
annotationOffset: number,
defaultMode: PlayerMode,
setNoRecording: (noRecs: boolean) => void,
setFocusedItem: (timeline: TrackingDetailsSequence) => void,
) {
this.camera = camera;
this.playerController = playerController;
this.previewController = previewController;
this.annotationOffset = annotationOffset;
this.playerMode = defaultMode;
this.setNoRecording = setNoRecording;
this.setFocusedItem = setFocusedItem;
}
newPlayback(newPlayback: DynamicPlayback) {
this.recordings = newPlayback.recordings;
this.timeRange = newPlayback.timeRange;
this.inpointOffset = calculateInpointOffset(
this.timeRange.after,
this.recordings[0],
);
if (this.timeToStart) {
this.seekToTimestamp(this.timeToStart);
this.timeToStart = undefined;
}
}
play() {
this.playerController.play();
}
pause() {
this.playerController.pause();
}
isPlaying(): boolean {
return !this.playerController.paused && !this.playerController.ended;
}
seekToTimestamp(time: number, play: boolean = false) {
if (time < this.timeRange.after || time > this.timeRange.before) {
this.timeToStart = time;
return;
}
if (this.playerMode != "playback") {
this.playerMode = "playback";
}
const seekSeconds = calculateSeekPosition(
time,
this.recordings,
this.inpointOffset,
);
if (seekSeconds === undefined) {
this.setNoRecording(true);
return;
}
if (seekSeconds != 0) {
this.playerController.currentTime = seekSeconds;
if (play) {
this.waitAndPlay();
} else {
this.playerController.pause();
}
} else {
// no op
}
}
waitAndPlay() {
return new Promise((resolve) => {
const onSeekedHandler = () => {
this.playerController.removeEventListener("seeked", onSeekedHandler);
this.playerController.play();
resolve(undefined);
};
this.playerController.addEventListener("seeked", onSeekedHandler, {
once: true,
});
});
}
seekToTimelineItem(timeline: TrackingDetailsSequence) {
this.playerController.pause();
this.seekToTimestamp(timeline.timestamp + this.annotationOffset);
this.setFocusedItem(timeline);
}
getProgress(playerTime: number): number {
// take a player time in seconds and convert to timestamp in timeline
let timestamp = 0;
let totalTime = 0;
(this.recordings || []).every((segment) => {
if (totalTime + segment.duration > playerTime) {
// segment is here
timestamp = segment.start_time + (playerTime - totalTime);
return false;
} else {
totalTime += segment.duration;
return true;
}
});
return timestamp;
}
scrubToTimestamp(time: number, saveIfNotReady: boolean = false) {
const scrubResult = this.previewController.scrubToTimestamp(time);
if (!scrubResult && saveIfNotReady) {
this.previewController.setNewPreviewStartTime(time);
}
if (scrubResult && this.playerMode != "scrubbing") {
this.playerMode = "scrubbing";
this.playerController.pause();
}
}
hasRecordingAtTime(time: number): boolean {
if (!this.recordings || this.recordings.length == 0) {
return false;
}
return (
this.recordings.find(
(segment) => segment.start_time <= time && segment.end_time >= time,
) != undefined
);
}
}
export default typeof DynamicVideoController;