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>
This commit is contained in:
Otto
2026-04-20 06:35:25 -06:00
committed by GitHub
co-authored by Josh Hawkins
parent d7f42735fc
commit 423ee2fe72
12 changed files with 678 additions and 17 deletions
+30 -5
View File
@@ -216,7 +216,11 @@ export default function HlsVideoPlayer({
const [tallCamera, setTallCamera] = useState(false);
const [isPlaying, setIsPlaying] = useState(true);
const [muted, setMuted] = useUserPersistence("hlsPlayerMuted", true);
const [persistedMuted, setPersistedMuted] = useUserPersistence(
"hlsPlayerMuted",
true,
);
const [temporaryMuted, setTemporaryMuted] = useState(false);
const [volume, setVolume] = useOverlayState("playerVolume", 1.0);
const [defaultPlaybackRate] = useUserPersistence("playbackRate", 1);
const [playbackRate, setPlaybackRate] = useOverlayState(
@@ -232,6 +236,16 @@ export default function HlsVideoPlayer({
height: number;
}>({ width: 0, height: 0 });
const muted = persistedMuted || temporaryMuted;
const onSetMuted = useCallback(
(muted: boolean) => {
setTemporaryMuted(false);
setPersistedMuted(muted);
},
[setPersistedMuted],
);
useEffect(() => {
if (!isDesktop) {
return;
@@ -297,7 +311,7 @@ export default function HlsVideoPlayer({
fullscreen: supportsFullscreen,
}}
setControlsOpen={setControlsOpen}
setMuted={(muted) => setMuted(muted)}
setMuted={onSetMuted}
playbackRate={playbackRate ?? 1}
hotKeys={hotKeys}
onPlayPause={onPlayPause}
@@ -404,9 +418,20 @@ export default function HlsVideoPlayer({
: undefined
}
onVolumeChange={() => {
setVolume(videoRef.current?.volume ?? 1.0, true);
if (!frigateControls) {
setMuted(videoRef.current?.muted);
if (!videoRef.current) {
return;
}
setVolume(videoRef.current.volume ?? 1.0, true);
if (frigateControls) {
if (videoRef.current.muted && !persistedMuted) {
setTemporaryMuted(true);
} else if (!videoRef.current.muted && temporaryMuted) {
setTemporaryMuted(false);
}
} else {
setPersistedMuted(videoRef.current.muted);
}
}}
onPlay={() => {
@@ -6,6 +6,7 @@ import {
calculateInpointOffset,
calculateSeekPosition,
} from "@/utils/videoUtil";
import { playWithTemporaryMuteFallback } from "@/utils/videoUtil.ts";
type PlayerMode = "playback" | "scrubbing";
@@ -107,7 +108,7 @@ export class DynamicVideoController {
return new Promise((resolve) => {
const onSeekedHandler = () => {
this.playerController.removeEventListener("seeked", onSeekedHandler);
this.playerController.play();
playWithTemporaryMuteFallback(this.playerController);
resolve(undefined);
};