frigate/web/src/components/player/HlsVideoPlayer.tsx

516 lines
15 KiB
TypeScript
Raw Normal View History

import {
MutableRefObject,
ReactNode,
useCallback,
useEffect,
useRef,
useState,
} from "react";
import Hls, { HlsConfig } from "hls.js";
import { isDesktop, isMobile } from "react-device-detect";
import { TransformComponent, TransformWrapper } from "react-zoom-pan-pinch";
import VideoControls from "./VideoControls";
import { VideoResolutionType } from "@/types/live";
import useSWR from "swr";
import { FrigateConfig } from "@/types/frigateConfig";
import { AxiosResponse } from "axios";
import { toast } from "sonner";
import { useOverlayState } from "@/hooks/use-overlay-state";
import { useUserPersistence } from "@/hooks/use-user-persistence";
import { cn } from "@/lib/utils";
import { ASPECT_VERTICAL_LAYOUT, RecordingPlayerError } from "@/types/record";
feat: add i18n (translation/localization) (#16877) * Translation module init * Add more i18n keys * fix: fix string wrong * refactor: use namespace translation file * chore: add more translation key * fix: fix some page name error * refactor: change Trans tag for t function * chore: fix some key not work * chore: fix SearchFilterDialog i18n key error Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * chore: fix en i18n file filter missing some keys * chore: add some i18n keys * chore: add more i18n keys again * feat: add search page i18n * feat: add explore model i18n keys * Update web/src/components/menu/GeneralSettings.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Update web/src/components/menu/GeneralSettings.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Update web/src/components/menu/GeneralSettings.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * feat: add more live i18n keys * feat: add more search setting i18n keys * fix: remove some comment * fix: fix some setting page url error * Update web/src/views/settings/SearchSettingsView.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * fix: add system missing keys * fix: update password update i18n keys * chore: remove outdate translation.json file * fix: fix exploreSettings error * chore: add object setting i18n keys * Update web/src/views/recording/RecordingView.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Update web/public/locales/en/components/filter.json Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Update web/src/components/overlay/ExportDialog.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * feat: add more i18n keys * fix: fix motionDetectionTuner html node * feat: add more page i18n keys * fix: cameraStream i18n keys error * feat: add Player i18n keys * feat: add more toast i18n keys * feat: change explore setting name * feat: add more document title i18n keys * feat: add more search i18n keys * fix: fix accessDenied i18n keys error * chore: add objectType i18n * chore: add inputWithTags i18n * chore: add SearchFilterDialog i18n * Update web/src/views/settings/ObjectSettingsView.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Update web/src/views/settings/ObjectSettingsView.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Update web/src/views/settings/ObjectSettingsView.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Update web/src/views/settings/ObjectSettingsView.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Update web/src/views/settings/ObjectSettingsView.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * chore: add some missing i18n keys * chore: remove most import { t } from "i18next"; --------- Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
2025-03-16 18:36:20 +03:00
import { useTranslation } from "react-i18next";
import ObjectTrackOverlay from "@/components/overlay/ObjectTrackOverlay";
import { useIsAdmin } from "@/hooks/use-is-admin";
2024-04-01 17:20:27 +03:00
// Android native hls does not seek correctly
const USE_NATIVE_HLS = false;
const HLS_MIME_TYPE = "application/vnd.apple.mpegurl" as const;
const unsupportedErrorCodes = [
MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED,
MediaError.MEDIA_ERR_DECODE,
];
export interface HlsSource {
playlist: string;
startPosition?: number;
}
type HlsVideoPlayerProps = {
videoRef: MutableRefObject<HTMLVideoElement | null>;
containerRef?: React.MutableRefObject<HTMLDivElement | null>;
visible: boolean;
currentSource: HlsSource;
hotKeys: boolean;
supportsFullscreen: boolean;
fullscreen: boolean;
frigateControls?: boolean;
inpointOffset?: number;
onClipEnded?: (currentTime: number) => void;
onPlayerLoaded?: () => void;
onTimeUpdate?: (time: number) => void;
onPlaying?: () => void;
2025-10-16 23:15:23 +03:00
onSeekToTime?: (timestamp: number, play?: boolean) => void;
setFullResolution?: React.Dispatch<React.SetStateAction<VideoResolutionType>>;
onUploadFrame?: (playTime: number) => Promise<AxiosResponse> | undefined;
toggleFullscreen?: () => void;
onError?: (error: RecordingPlayerError) => void;
isDetailMode?: boolean;
camera?: string;
currentTimeOverride?: number;
transformedOverlay?: ReactNode;
};
export default function HlsVideoPlayer({
videoRef,
containerRef,
visible,
currentSource,
hotKeys,
supportsFullscreen,
fullscreen,
frigateControls = true,
inpointOffset = 0,
onClipEnded,
onPlayerLoaded,
onTimeUpdate,
onPlaying,
onSeekToTime,
setFullResolution,
onUploadFrame,
toggleFullscreen,
onError,
isDetailMode = false,
camera,
currentTimeOverride,
transformedOverlay,
}: HlsVideoPlayerProps) {
feat: add i18n (translation/localization) (#16877) * Translation module init * Add more i18n keys * fix: fix string wrong * refactor: use namespace translation file * chore: add more translation key * fix: fix some page name error * refactor: change Trans tag for t function * chore: fix some key not work * chore: fix SearchFilterDialog i18n key error Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * chore: fix en i18n file filter missing some keys * chore: add some i18n keys * chore: add more i18n keys again * feat: add search page i18n * feat: add explore model i18n keys * Update web/src/components/menu/GeneralSettings.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Update web/src/components/menu/GeneralSettings.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Update web/src/components/menu/GeneralSettings.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * feat: add more live i18n keys * feat: add more search setting i18n keys * fix: remove some comment * fix: fix some setting page url error * Update web/src/views/settings/SearchSettingsView.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * fix: add system missing keys * fix: update password update i18n keys * chore: remove outdate translation.json file * fix: fix exploreSettings error * chore: add object setting i18n keys * Update web/src/views/recording/RecordingView.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Update web/public/locales/en/components/filter.json Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Update web/src/components/overlay/ExportDialog.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * feat: add more i18n keys * fix: fix motionDetectionTuner html node * feat: add more page i18n keys * fix: cameraStream i18n keys error * feat: add Player i18n keys * feat: add more toast i18n keys * feat: change explore setting name * feat: add more document title i18n keys * feat: add more search i18n keys * fix: fix accessDenied i18n keys error * chore: add objectType i18n * chore: add inputWithTags i18n * chore: add SearchFilterDialog i18n * Update web/src/views/settings/ObjectSettingsView.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Update web/src/views/settings/ObjectSettingsView.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Update web/src/views/settings/ObjectSettingsView.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Update web/src/views/settings/ObjectSettingsView.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Update web/src/views/settings/ObjectSettingsView.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * chore: add some missing i18n keys * chore: remove most import { t } from "i18next"; --------- Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
2025-03-16 18:36:20 +03:00
const { t } = useTranslation("components/player");
const { data: config } = useSWR<FrigateConfig>("config");
const isAdmin = useIsAdmin();
// for detail stream context in History
const currentTime = currentTimeOverride;
// playback
Update frontend to React 19 (#22275) * remove unused RecoilRoot and fix implicit ref callback Remove the vestigial recoil dependency (zero consumers) and convert the implicit-return ref callback in SearchView to block form to prevent React 19 interpreting it as a cleanup function. * replace react-transition-group with framer-motion in Chip Replace CSSTransition with framer-motion AnimatePresence + motion.div for React 19 compatibility (react-transition-group uses findDOMNode). framer-motion is already a project dependency. * migrate react-grid-layout v1 to v2 - Replace WidthProvider(Responsive) HOC with useContainerWidth hook - Update types: Layout (single item) → LayoutItem, Layout[] → Layout - Replace isDraggable/isResizable/resizeHandles with dragConfig/resizeConfig - Update EventCallback signature for v2 API - Remove @types/react-grid-layout (v2 includes its own types) * upgrade vaul, next-themes, framer-motion, react-zoom-pan-pinch - vaul: ^0.9.1 → ^1.1.2 - next-themes: ^0.3.0 → ^0.4.6 - framer-motion: ^11.5.4 → ^12.35.0 (React 19 native support) - react-zoom-pan-pinch: 3.4.4 → latest * upgrade to React 19, react-konva v19, eslint-plugin-react-hooks v5 Core React 19 upgrade with all necessary type fixes: - Update RefObject types to accept T | null (React 19 refs always nullable) - Add JSX namespace imports (no longer global in React 19) - Add initial values to useRef calls (required in React 19) - Fix ReactElement.props unknown type in config-form components - Fix IconWrapper interface to use HTMLAttributes instead of index signature - Add monaco-editor as dev dependency for type declarations - Upgrade react-konva to v19, eslint-plugin-react-hooks to v5 * upgrade typescript to 5.9.3 * modernize Context.Provider to React 19 shorthand Replace <Context.Provider value={...}> with <Context value={...}> across all project-owned context providers. External library contexts (react-icons IconContext, radix TooltipPrimitive) left unchanged. * add runtime patches for React 19 compatibility - Patch @radix-ui/react-compose-refs@1.1.2: stabilize useComposedRefs to prevent infinite render loops from unstable ref callbacks https://github.com/radix-ui/primitives/issues/3799 - Patch @radix-ui/react-slot@1.2.4: use useComposedRefs hook in SlotClone instead of inline composeRefs to prevent re-render cycles https://github.com/radix-ui/primitives/pull/3804 - Patch react-use-websocket@4.8.1: remove flushSync wrappers that cause "Maximum update depth exceeded" with React 19 auto-batching https://github.com/facebook/react/issues/27613 - Add npm overrides to ensure single hoisted copies of compose-refs and react-slot across all Radix packages - Add postinstall script for patch-package - Remove leftover react-transition-group dependency * formatting * use availableWidth instead of useContainerWidth for grid layout The useContainerWidth hook from react-grid-layout v2 returns raw container width without accounting for scrollbar width, causing the grid to not fill the full available space. Use the existing availableWidth value from useResizeObserver which already compensates for scrollbar width, matching the working implementation. * remove unused carousel component and fix React 19 peer deps Remove embla-carousel-react and its unused Carousel UI component. Upgrade sonner v1 → v2 for native React 19 support. Remove @types/react-icons stub (react-icons bundles its own types). These changes eliminate all peer dependency conflicts, so npm install works without --legacy-peer-deps. * fix React 19 infinite re-render loop on live dashboard The "Maximum update depth exceeded" error was caused by two issues: 1. useDeferredStreamMetadata returned a new `{}` default on every render when SWR data was undefined, creating an unstable reference that triggered the useEffect in useCameraLiveMode on every render cycle. Fixed by using a stable module-level EMPTY_METADATA constant. 2. useResizeObserver's rest parameter `...refs` created a new array on every render, causing its useEffect to re-run and re-observe elements continuously. Fixed by stabilizing refs with useRef and only reconnecting the observer when actual DOM elements change.
2026-03-05 17:42:38 +03:00
const hlsRef = useRef<Hls>(undefined);
const [useHlsCompat, setUseHlsCompat] = useState(false);
const [loadedMetadata, setLoadedMetadata] = useState(false);
const [bufferTimeout, setBufferTimeout] = useState<NodeJS.Timeout>();
const applyVideoDimensions = useCallback(
(width: number, height: number) => {
if (setFullResolution) {
setFullResolution({ width, height });
}
setVideoDimensions({ width, height });
if (height > 0) {
setTallCamera(width / height < ASPECT_VERTICAL_LAYOUT);
}
},
[setFullResolution],
);
const handleLoadedMetadata = useCallback(() => {
setLoadedMetadata(true);
if (!videoRef.current) {
return;
}
const width = videoRef.current.videoWidth;
const height = videoRef.current.videoHeight;
// iOS Safari occasionally reports 0x0 for videoWidth/videoHeight
// Poll with requestAnimationFrame until dimensions become available (or timeout).
if (width > 0 && height > 0) {
applyVideoDimensions(width, height);
return;
}
let attempts = 0;
const maxAttempts = 120; // ~2 seconds at 60fps
const tryGetDims = () => {
if (!videoRef.current) return;
const w = videoRef.current.videoWidth;
const h = videoRef.current.videoHeight;
if (w > 0 && h > 0) {
applyVideoDimensions(w, h);
return;
}
if (attempts < maxAttempts) {
attempts += 1;
requestAnimationFrame(tryGetDims);
}
};
requestAnimationFrame(tryGetDims);
}, [videoRef, applyVideoDimensions]);
useEffect(() => {
if (!videoRef.current) {
return;
}
2024-04-01 17:20:27 +03:00
if (USE_NATIVE_HLS && videoRef.current.canPlayType(HLS_MIME_TYPE)) {
return;
} else if (Hls.isSupported()) {
setUseHlsCompat(true);
}
}, [videoRef]);
useEffect(() => {
if (!videoRef.current) {
return;
}
setLoadedMetadata(false);
const currentPlaybackRate = videoRef.current.playbackRate;
if (!useHlsCompat) {
videoRef.current.src = currentSource.playlist;
videoRef.current.load();
return;
}
// Base HLS configuration
const hlsConfig: Partial<HlsConfig> = {
maxBufferLength: 10,
maxBufferSize: 20 * 1000 * 1000,
startPosition: currentSource.startPosition,
};
hlsRef.current = new Hls(hlsConfig);
hlsRef.current.attachMedia(videoRef.current);
hlsRef.current.loadSource(currentSource.playlist);
videoRef.current.playbackRate = currentPlaybackRate;
return () => {
// we must destroy the hlsRef every time the source changes
// so that we can create a new HLS instance with startPosition
// set at the optimal point in time
if (hlsRef.current) {
hlsRef.current.destroy();
}
};
}, [videoRef, hlsRef, useHlsCompat, currentSource]);
// state handling
const onPlayPause = useCallback(
(play: boolean) => {
if (!videoRef.current) {
return;
}
if (play) {
videoRef.current.play();
} else {
videoRef.current.pause();
}
},
[videoRef],
);
// controls
const [tallCamera, setTallCamera] = useState(false);
const [isPlaying, setIsPlaying] = useState(true);
const [muted, setMuted] = useUserPersistence("hlsPlayerMuted", true);
const [volume, setVolume] = useOverlayState("playerVolume", 1.0);
const [defaultPlaybackRate] = useUserPersistence("playbackRate", 1);
const [playbackRate, setPlaybackRate] = useOverlayState(
"playbackRate",
defaultPlaybackRate ?? 1,
);
const [mobileCtrlTimeout, setMobileCtrlTimeout] = useState<NodeJS.Timeout>();
const [controls, setControls] = useState(isMobile);
const [controlsOpen, setControlsOpen] = useState(false);
const [zoomScale, setZoomScale] = useState(1.0);
const [videoDimensions, setVideoDimensions] = useState<{
width: number;
height: number;
}>({ width: 0, height: 0 });
useEffect(() => {
if (!isDesktop) {
return;
}
const callback = (e: MouseEvent) => {
if (!videoRef.current) {
return;
}
const rect = videoRef.current.getBoundingClientRect();
if (
e.clientX > rect.left &&
e.clientX < rect.right &&
e.clientY > rect.top &&
e.clientY < rect.bottom
) {
setControls(true);
} else {
setControls(controlsOpen);
}
};
window.addEventListener("mousemove", callback);
return () => {
window.removeEventListener("mousemove", callback);
};
}, [videoRef, controlsOpen]);
const getVideoTime = useCallback(() => {
const currentTime = videoRef.current?.currentTime;
if (!currentTime) {
return undefined;
}
return currentTime + inpointOffset;
}, [videoRef, inpointOffset]);
return (
<TransformWrapper
minScale={1.0}
wheel={{ smoothStep: 0.005 }}
onZoom={(zoom) => setZoomScale(zoom.state.scale)}
disabled={!frigateControls}
>
{frigateControls && (
<VideoControls
className={cn(
"absolute left-1/2 z-50 -translate-x-1/2",
tallCamera ? "bottom-12" : "bottom-5",
)}
video={videoRef.current}
isPlaying={isPlaying}
show={visible && (controls || controlsOpen)}
muted={muted}
volume={volume}
features={{
volume: true,
seek: true,
playbackRate: true,
plusUpload: isAdmin && config?.plus?.enabled == true,
fullscreen: supportsFullscreen,
}}
setControlsOpen={setControlsOpen}
setMuted={(muted) => setMuted(muted)}
playbackRate={playbackRate ?? 1}
hotKeys={hotKeys}
onPlayPause={onPlayPause}
onSeek={(diff) => {
const currentTime = videoRef.current?.currentTime;
if (!videoRef.current || !currentTime) {
return;
}
videoRef.current.currentTime = Math.max(0, currentTime + diff);
}}
onSetPlaybackRate={(rate) => {
setPlaybackRate(rate, true);
if (videoRef.current) {
videoRef.current.playbackRate = rate;
}
}}
onUploadFrame={async () => {
const frameTime = getVideoTime();
if (frameTime && onUploadFrame) {
const resp = await onUploadFrame(frameTime);
if (resp && resp.status == 200) {
feat: add i18n (translation/localization) (#16877) * Translation module init * Add more i18n keys * fix: fix string wrong * refactor: use namespace translation file * chore: add more translation key * fix: fix some page name error * refactor: change Trans tag for t function * chore: fix some key not work * chore: fix SearchFilterDialog i18n key error Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * chore: fix en i18n file filter missing some keys * chore: add some i18n keys * chore: add more i18n keys again * feat: add search page i18n * feat: add explore model i18n keys * Update web/src/components/menu/GeneralSettings.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Update web/src/components/menu/GeneralSettings.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Update web/src/components/menu/GeneralSettings.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * feat: add more live i18n keys * feat: add more search setting i18n keys * fix: remove some comment * fix: fix some setting page url error * Update web/src/views/settings/SearchSettingsView.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * fix: add system missing keys * fix: update password update i18n keys * chore: remove outdate translation.json file * fix: fix exploreSettings error * chore: add object setting i18n keys * Update web/src/views/recording/RecordingView.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Update web/public/locales/en/components/filter.json Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Update web/src/components/overlay/ExportDialog.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * feat: add more i18n keys * fix: fix motionDetectionTuner html node * feat: add more page i18n keys * fix: cameraStream i18n keys error * feat: add Player i18n keys * feat: add more toast i18n keys * feat: change explore setting name * feat: add more document title i18n keys * feat: add more search i18n keys * fix: fix accessDenied i18n keys error * chore: add objectType i18n * chore: add inputWithTags i18n * chore: add SearchFilterDialog i18n * Update web/src/views/settings/ObjectSettingsView.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Update web/src/views/settings/ObjectSettingsView.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Update web/src/views/settings/ObjectSettingsView.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Update web/src/views/settings/ObjectSettingsView.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Update web/src/views/settings/ObjectSettingsView.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * chore: add some missing i18n keys * chore: remove most import { t } from "i18next"; --------- Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
2025-03-16 18:36:20 +03:00
toast.success(t("toast.success.submittedFrigatePlus"), {
position: "top-center",
});
} else {
feat: add i18n (translation/localization) (#16877) * Translation module init * Add more i18n keys * fix: fix string wrong * refactor: use namespace translation file * chore: add more translation key * fix: fix some page name error * refactor: change Trans tag for t function * chore: fix some key not work * chore: fix SearchFilterDialog i18n key error Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * chore: fix en i18n file filter missing some keys * chore: add some i18n keys * chore: add more i18n keys again * feat: add search page i18n * feat: add explore model i18n keys * Update web/src/components/menu/GeneralSettings.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Update web/src/components/menu/GeneralSettings.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Update web/src/components/menu/GeneralSettings.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * feat: add more live i18n keys * feat: add more search setting i18n keys * fix: remove some comment * fix: fix some setting page url error * Update web/src/views/settings/SearchSettingsView.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * fix: add system missing keys * fix: update password update i18n keys * chore: remove outdate translation.json file * fix: fix exploreSettings error * chore: add object setting i18n keys * Update web/src/views/recording/RecordingView.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Update web/public/locales/en/components/filter.json Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Update web/src/components/overlay/ExportDialog.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * feat: add more i18n keys * fix: fix motionDetectionTuner html node * feat: add more page i18n keys * fix: cameraStream i18n keys error * feat: add Player i18n keys * feat: add more toast i18n keys * feat: change explore setting name * feat: add more document title i18n keys * feat: add more search i18n keys * fix: fix accessDenied i18n keys error * chore: add objectType i18n * chore: add inputWithTags i18n * chore: add SearchFilterDialog i18n * Update web/src/views/settings/ObjectSettingsView.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Update web/src/views/settings/ObjectSettingsView.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Update web/src/views/settings/ObjectSettingsView.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Update web/src/views/settings/ObjectSettingsView.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Update web/src/views/settings/ObjectSettingsView.tsx Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * chore: add some missing i18n keys * chore: remove most import { t } from "i18next"; --------- Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
2025-03-16 18:36:20 +03:00
toast.success(t("toast.error.submitFrigatePlusFailed"), {
position: "top-center",
});
}
}
}}
fullscreen={fullscreen}
toggleFullscreen={toggleFullscreen}
containerRef={containerRef}
/>
)}
<TransformComponent
wrapperStyle={{
display: visible ? undefined : "none",
width: "100%",
height: "100%",
}}
wrapperProps={{
onClick: isDesktop ? undefined : () => setControls(!controls),
}}
contentStyle={{
width: "100%",
height: isMobile ? "100%" : undefined,
}}
>
<div className="relative size-full">
{transformedOverlay}
{isDetailMode &&
camera &&
currentTime &&
loadedMetadata &&
videoDimensions.width > 0 &&
videoDimensions.height > 0 && (
<div
className={cn(
"absolute inset-0 z-50",
isDesktop
? "size-full"
: "mx-auto flex items-center justify-center portrait:max-h-[50dvh]",
)}
style={{
aspectRatio: `${videoDimensions.width} / ${videoDimensions.height}`,
}}
>
<ObjectTrackOverlay
key={`overlay-${currentTime}`}
camera={camera}
showBoundingBoxes={!isPlaying}
currentTime={currentTime}
videoWidth={videoDimensions.width}
videoHeight={videoDimensions.height}
className="absolute inset-0 z-10"
onSeekToTime={(timestamp, play) => {
if (onSeekToTime) {
onSeekToTime(timestamp, play);
}
}}
/>
</div>
)}
<video
ref={videoRef}
className={`size-full rounded-lg bg-black md:rounded-2xl ${loadedMetadata ? "" : "invisible"} cursor-pointer`}
preload="auto"
autoPlay
controls={!frigateControls}
playsInline
muted={muted}
onClick={
isDesktop
? () => {
if (zoomScale == 1.0) onPlayPause(!isPlaying);
}
: undefined
}
onVolumeChange={() => {
setVolume(videoRef.current?.volume ?? 1.0, true);
if (!frigateControls) {
setMuted(videoRef.current?.muted);
}
}}
onPlay={() => {
setIsPlaying(true);
if (isMobile) {
setControls(true);
setMobileCtrlTimeout(
setTimeout(() => setControls(false), 4000),
);
}
}}
onPlaying={onPlaying}
onPause={() => {
setIsPlaying(false);
clearTimeout(bufferTimeout);
if (isMobile && mobileCtrlTimeout) {
clearTimeout(mobileCtrlTimeout);
}
}}
onWaiting={() => {
if (onError != undefined) {
if (videoRef.current?.paused) {
return;
}
setBufferTimeout(
setTimeout(() => {
if (
document.visibilityState === "visible" &&
videoRef.current
) {
onError("stalled");
}
}, 3000),
);
}
}}
onProgress={() => {
if (onError != undefined) {
if (videoRef.current?.paused) {
return;
}
if (bufferTimeout) {
clearTimeout(bufferTimeout);
setBufferTimeout(undefined);
}
}
}}
onTimeUpdate={() => {
if (!onTimeUpdate) {
return;
}
const frameTime = getVideoTime();
if (frameTime) {
onTimeUpdate(frameTime);
}
}}
onLoadedData={() => {
onPlayerLoaded?.();
handleLoadedMetadata();
if (videoRef.current) {
if (playbackRate) {
videoRef.current.playbackRate = playbackRate;
}
if (volume) {
videoRef.current.volume = volume;
}
}
}}
onEnded={() => {
if (onClipEnded) {
onClipEnded(getVideoTime() ?? 0);
}
}}
onError={(e) => {
if (
!hlsRef.current &&
// @ts-expect-error code does exist
unsupportedErrorCodes.includes(e.target.error.code) &&
videoRef.current
) {
setLoadedMetadata(false);
setUseHlsCompat(true);
} else {
toast.error(
// @ts-expect-error code does exist
`Failed to play recordings (error ${e.target.error.code}): ${e.target.error.message}`,
{
position: "top-center",
},
);
}
}}
/>
</div>
</TransformComponent>
</TransformWrapper>
);
}