diff --git a/web/src/components/player/HlsVideoPlayer.tsx b/web/src/components/player/HlsVideoPlayer.tsx index 54bc8c8c9..55c175496 100644 --- a/web/src/components/player/HlsVideoPlayer.tsx +++ b/web/src/components/player/HlsVideoPlayer.tsx @@ -1,8 +1,15 @@ -import { MutableRefObject, useEffect, useRef, useState } from "react"; +import { + MutableRefObject, + useCallback, + useEffect, + useRef, + useState, +} from "react"; import Hls from "hls.js"; import { isAndroid, isDesktop, isMobile } from "react-device-detect"; import { TransformComponent, TransformWrapper } from "react-zoom-pan-pinch"; import VideoControls from "./VideoControls"; +import { VideoResolutionType } from "@/types/live"; // Android native hls does not seek correctly const USE_NATIVE_HLS = !isAndroid; @@ -21,6 +28,7 @@ type HlsVideoPlayerProps = { onPlayerLoaded?: () => void; onTimeUpdate?: (time: number) => void; onPlaying?: () => void; + setLiveResolution?: React.Dispatch>; }; export default function HlsVideoPlayer({ videoRef, @@ -31,6 +39,7 @@ export default function HlsVideoPlayer({ onPlayerLoaded, onTimeUpdate, onPlaying, + setLiveResolution, }: HlsVideoPlayerProps) { // playback @@ -38,6 +47,18 @@ export default function HlsVideoPlayer({ const [useHlsCompat, setUseHlsCompat] = useState(false); const [loadedMetadata, setLoadedMetadata] = useState(false); + const handleLoadedMetadata = useCallback(() => { + if (videoRef.current) { + setLoadedMetadata(true); + if (setLiveResolution) { + setLiveResolution({ + width: videoRef.current.videoWidth, + height: videoRef.current.videoHeight, + }); + } + } + }, [videoRef, setLiveResolution]); + useEffect(() => { if (!videoRef.current) { return; @@ -193,7 +214,7 @@ export default function HlsVideoPlayer({ : undefined } onLoadedData={onPlayerLoaded} - onLoadedMetadata={() => setLoadedMetadata(true)} + onLoadedMetadata={handleLoadedMetadata} onEnded={onClipEnded} onError={(e) => { if ( diff --git a/web/src/components/player/LivePlayer.tsx b/web/src/components/player/LivePlayer.tsx index d42ecb3a0..094e3a5cb 100644 --- a/web/src/components/player/LivePlayer.tsx +++ b/web/src/components/player/LivePlayer.tsx @@ -8,7 +8,7 @@ import JSMpegPlayer from "./JSMpegPlayer"; import { MdCircle } from "react-icons/md"; import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; import { useCameraActivity } from "@/hooks/use-camera-activity"; -import { LivePlayerMode } from "@/types/live"; +import { LivePlayerMode, VideoResolutionType } from "@/types/live"; import useCameraLiveMode from "@/hooks/use-camera-live-mode"; import { getIconForLabel } from "@/utils/iconUtil"; import Chip from "../indicators/Chip"; @@ -27,6 +27,7 @@ type LivePlayerProps = { iOSCompatFullScreen?: boolean; pip?: boolean; onClick?: () => void; + setLiveResolution: React.Dispatch>; }; export default function LivePlayer({ @@ -41,6 +42,7 @@ export default function LivePlayer({ iOSCompatFullScreen = false, pip, onClick, + setLiveResolution, }: LivePlayerProps) { const [cameraHovered, setCameraHovered] = useState(false); @@ -123,6 +125,7 @@ export default function LivePlayer({ audioEnabled={playAudio} onPlaying={() => setLiveReady(true)} pip={pip} + setLiveResolution={setLiveResolution} /> ); } else { @@ -142,6 +145,12 @@ export default function LivePlayer({ height={cameraConfig.detect.height} /> ); + if (setLiveResolution) { + setLiveResolution({ + width: cameraConfig.detect.width, + height: cameraConfig.detect.height, + }); + } } else { player = ; } diff --git a/web/src/components/player/MsePlayer.tsx b/web/src/components/player/MsePlayer.tsx index f25557d16..29403f3f3 100644 --- a/web/src/components/player/MsePlayer.tsx +++ b/web/src/components/player/MsePlayer.tsx @@ -1,5 +1,13 @@ import { baseUrl } from "@/api/baseUrl"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { VideoResolutionType } from "@/types/live"; +import { + SetStateAction, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; type MSEPlayerProps = { camera: string; @@ -8,6 +16,7 @@ type MSEPlayerProps = { audioEnabled?: boolean; pip?: boolean; onPlaying?: () => void; + setLiveResolution?: React.Dispatch>; }; function MSEPlayer({ @@ -17,6 +26,7 @@ function MSEPlayer({ audioEnabled = false, pip = false, onPlaying, + setLiveResolution, }: MSEPlayerProps) { let connectTS: number = 0; @@ -50,6 +60,15 @@ function MSEPlayer({ return `${baseUrl.replace(/^http/, "ws")}live/mse/api/ws?src=${camera}`; }, [camera]); + const handleLoadedMetadata = useCallback(() => { + if (videoRef.current && setLiveResolution) { + setLiveResolution({ + width: videoRef.current.videoWidth, + height: videoRef.current.videoHeight, + }); + } + }, [setLiveResolution]); + const play = () => { const currentVideo = videoRef.current; @@ -286,6 +305,7 @@ function MSEPlayer({ playsInline preload="auto" onLoadedData={onPlaying} + onLoadedMetadata={handleLoadedMetadata} muted={!audioEnabled} /> ); diff --git a/web/src/components/player/dynamic/DynamicVideoPlayer.tsx b/web/src/components/player/dynamic/DynamicVideoPlayer.tsx index 15216b749..a8e6d5434 100644 --- a/web/src/components/player/dynamic/DynamicVideoPlayer.tsx +++ b/web/src/components/player/dynamic/DynamicVideoPlayer.tsx @@ -9,6 +9,7 @@ import { DynamicVideoController } from "./DynamicVideoController"; import HlsVideoPlayer from "../HlsVideoPlayer"; import { TimeRange } from "@/types/timeline"; import ActivityIndicator from "@/components/indicators/activity-indicator"; +import { VideoResolutionType } from "@/types/live"; /** * Dynamically switches between video playback and scrubbing preview player. @@ -24,6 +25,7 @@ type DynamicVideoPlayerProps = { onControllerReady: (controller: DynamicVideoController) => void; onTimestampUpdate?: (timestamp: number) => void; onClipEnded?: () => void; + setLiveResolution: React.Dispatch>; }; export default function DynamicVideoPlayer({ className, @@ -36,6 +38,7 @@ export default function DynamicVideoPlayer({ onControllerReady, onTimestampUpdate, onClipEnded, + setLiveResolution, }: DynamicVideoPlayerProps) { const apiHost = useApiHost(); const { data: config } = useSWR("config"); @@ -182,6 +185,7 @@ export default function DynamicVideoPlayer({ setIsLoading(false); setNoRecording(false); }} + setLiveResolution={setLiveResolution} /> ({ + width: 0, + height: 0, + }); + const getCameraAspect = useCallback( (cam: string) => { if (!config) { @@ -211,9 +217,13 @@ export function RecordingView({ return undefined; } - return camera.detect.width / camera.detect.height; + if (liveResolution.width && liveResolution.height) { + return liveResolution.width / liveResolution.height; + } else { + return camera.detect.width / camera.detect.height; + } }, - [config], + [config, liveResolution], ); const mainCameraAspect = useMemo(() => { @@ -397,6 +407,7 @@ export function RecordingView({ mainControllerRef.current = controller; }} isScrubbing={scrubbing || exportMode == "timeline"} + setLiveResolution={setLiveResolution} /> {isDesktop && ( diff --git a/web/src/views/live/LiveCameraView.tsx b/web/src/views/live/LiveCameraView.tsx index eaf2a63bd..1c0c34084 100644 --- a/web/src/views/live/LiveCameraView.tsx +++ b/web/src/views/live/LiveCameraView.tsx @@ -20,6 +20,7 @@ import { TooltipProvider } from "@/components/ui/tooltip"; import { useResizeObserver } from "@/hooks/resize-observer"; import useKeyboardListener from "@/hooks/use-keyboard-listener"; import { CameraConfig } from "@/types/frigateConfig"; +import { VideoResolutionType } from "@/types/live"; import { CameraPtzInfo } from "@/types/ptz"; import { RecordingStartingPoint } from "@/types/record"; import React, { @@ -149,8 +150,18 @@ export default function LiveCameraView({ camera }: LiveCameraViewProps) { const [fullscreen, setFullscreen] = useState(false); const [pip, setPip] = useState(false); + const [liveResolution, setLiveResolution] = useState({ + width: 0, + height: 0, + }); + const growClassName = useMemo(() => { - const aspect = camera.detect.width / camera.detect.height; + let aspect; + if (liveResolution.width && liveResolution.height) { + aspect = liveResolution.width / liveResolution.height; + } else { + aspect = camera.detect.width / camera.detect.height; + } if (isMobile) { if (isPortrait) { @@ -173,7 +184,7 @@ export default function LiveCameraView({ camera }: LiveCameraViewProps) { } else { return "absolute top-2 bottom-2 left-[50%] -translate-x-[50%]"; } - }, [camera, fullscreen, isPortrait]); + }, [camera, fullscreen, isPortrait, liveResolution]); const preferredLiveMode = useMemo(() => { if (isSafari || mic) { @@ -188,8 +199,12 @@ export default function LiveCameraView({ camera }: LiveCameraViewProps) { }, [windowWidth, windowHeight]); const cameraAspectRatio = useMemo(() => { - return camera.detect.width / camera.detect.height; - }, [camera]); + if (liveResolution.width && liveResolution.height) { + return liveResolution.width / liveResolution.height; + } else { + return camera.detect.width / camera.detect.height; + } + }, [camera, liveResolution]); const aspectRatio = useMemo(() => { if (isMobile || fullscreen) { @@ -347,6 +362,7 @@ export default function LiveCameraView({ camera }: LiveCameraViewProps) { iOSCompatFullScreen={isIOS} preferredLiveMode={preferredLiveMode} pip={pip} + setLiveResolution={setLiveResolution} /> {camera.onvif.host != "" && (