mirror of
https://github.com/alexhopeoconnor/frigate.git
synced 2026-10-19 01:41:31 +10:00
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 99fa5e1deebdc3a91a4b9015f5a46a36545bf349. * 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>
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
import { baseUrl } from "@/api/baseUrl.ts";
|
||||
|
||||
export const RECORDING_REVIEW_LINK_PARAM = "timestamp";
|
||||
|
||||
export type RecordingReviewLinkState = {
|
||||
camera: string;
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
export function parseRecordingReviewLink(
|
||||
value: string | null,
|
||||
): RecordingReviewLinkState | undefined {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const separatorIndex = value.lastIndexOf("_");
|
||||
|
||||
if (separatorIndex <= 0 || separatorIndex == value.length - 1) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const camera = value.slice(0, separatorIndex);
|
||||
const timestamp = value.slice(separatorIndex + 1);
|
||||
|
||||
if (!camera || !timestamp) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const parsedTimestamp = Number(timestamp);
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
if (!Number.isFinite(parsedTimestamp) || parsedTimestamp <= 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
camera,
|
||||
// clamp future timestamps to now
|
||||
timestamp: Math.min(Math.floor(parsedTimestamp), now),
|
||||
};
|
||||
}
|
||||
|
||||
export function createRecordingReviewUrl(
|
||||
pathname: string,
|
||||
state: RecordingReviewLinkState,
|
||||
): string {
|
||||
const url = new URL(baseUrl);
|
||||
url.pathname = pathname.startsWith("/") ? pathname : `/${pathname}`;
|
||||
url.searchParams.set(
|
||||
RECORDING_REVIEW_LINK_PARAM,
|
||||
`${state.camera}_${Math.floor(state.timestamp)}`,
|
||||
);
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
@@ -78,3 +78,20 @@ export function calculateSeekPosition(
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user