frigate/web/src/utils/videoUtil.ts
Otto 423ee2fe72
Feature: Share Timestamped URL for Camera Footage History (#22537)
* Initial copy timestamp url implementation

* revise url format

* Implement share timestamp dialog

* Use translations

* Add comments

* Add validations to shared link

* Switch to searchEffect implementation

* Add missing accessibility related dialog description

* Change URL format to unix timestamps

* Remove unnecessary useEffect

* Remove duplicated dialog title

* Fixes/improvements based off PR review comments

* Add missing cancel button & separators to dialog

* Make share description clearer

* Bugfix: guard against showing toasts twice
Because this effect ends up running multiple times

* Clamp future timestamps to now

* Revert "Bugfix: guard against showing toasts twice"

This reverts commit 99fa5e1dee.

* Use normal separator

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>

* Fixes based off PR review comments

* Bugfix: Share dialog was not receiving the player timestamp after removing key that triggered remounts

* Defer `setRecording` and return true from hook for cleanup

* Remove timeout defer hack in favor of refactored hook

* Attempt to replay video muted on NotAllowedError

* Use separate persistent mute and temporary forced mute states

* Align cancel button with other dialogs

* Prevent wrapping on dialog title

* Remove extra "back" button on mobile drawer

* Fix back navigation when coming from direct shared timestamp links

* Use new timeformat hook

* Simplify dialog radio buttons

* Apply suggestions from code review

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>

---------

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
2026-04-20 06:35:25 -06:00

98 lines
2.9 KiB
TypeScript

import { Recording } from "@/types/record";
/** the HLS endpoint returns the vod segments with the first
* segment of the hour trimmed, meaning it will start at
* the beginning of the hour, cutting off any difference
* that the segment has.
*/
export function calculateInpointOffset(
timeRangeStart: number | undefined,
firstRecordingSegment: Recording | undefined,
): number {
if (!timeRangeStart || !firstRecordingSegment) {
return 0;
}
// if the first recording segment does not cross over
// the beginning of the time range then there is no offset
if (
firstRecordingSegment.start_time < timeRangeStart &&
firstRecordingSegment.end_time > timeRangeStart
) {
return timeRangeStart - firstRecordingSegment.start_time;
}
return 0;
}
/**
* Calculates the video player time (in seconds) for a given timestamp
* by iterating through recording segments and summing their durations.
* This accounts for the fact that the video is a concatenation of segments,
* not a single continuous stream.
*
* @param timestamp - The target timestamp to seek to
* @param recordings - Array of recording segments
* @param inpointOffset - HLS inpoint offset to subtract from the result
* @returns The calculated seek position in seconds, or undefined if timestamp is out of range
*/
export function calculateSeekPosition(
timestamp: number,
recordings: Recording[],
inpointOffset: number = 0,
): number | undefined {
if (!recordings || recordings.length === 0) {
return undefined;
}
// Check if timestamp is within the recordings range
if (
timestamp < recordings[0].start_time ||
timestamp > recordings[recordings.length - 1].end_time
) {
return undefined;
}
let seekSeconds = 0;
(recordings || []).every((segment) => {
// if the next segment is past the desired time, stop calculating
if (segment.start_time > timestamp) {
return false;
}
if (segment.end_time < timestamp) {
// Add the full duration of this segment
seekSeconds += segment.end_time - segment.start_time;
return true;
}
// We're in this segment - calculate position within it
seekSeconds +=
segment.end_time - segment.start_time - (segment.end_time - timestamp);
return true;
});
// Adjust for HLS inpoint offset
seekSeconds -= inpointOffset;
return seekSeconds >= 0 ? seekSeconds : undefined;
}
/**
* Attempts to play the video, and if it fails due to a NotAllowedError (often caused by browser autoplay restrictions),
* it temporarily mutes the video and tries to play again.
* @param video - The HTMLVideoElement to play
*/
export function playWithTemporaryMuteFallback(video: HTMLVideoElement) {
return video.play().catch((error: { name?: string }) => {
if (error.name === "NotAllowedError" && !video.muted) {
video.muted = true;
return video.play().catch(() => undefined);
}
throw error;
});
}