diff --git a/Makefile b/Makefile index fa692b681..04eee68d5 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ default_target: local COMMIT_HASH := $(shell git log -1 --pretty=format:"%h"|tail -1) -VERSION = 0.16.2 +VERSION = 0.16.3 IMAGE_REPO ?= ghcr.io/blakeblackshear/frigate GITHUB_REF_NAME ?= $(shell git rev-parse --abbrev-ref HEAD) BOARDS= #Initialized empty diff --git a/docker/main/requirements.txt b/docker/main/requirements.txt index 3ae420d07..f1ba7d9ad 100644 --- a/docker/main/requirements.txt +++ b/docker/main/requirements.txt @@ -1,2 +1 @@ scikit-build == 0.18.* -nvidia-pyindex diff --git a/frigate/api/auth.py b/frigate/api/auth.py index 9459c4ac8..95586e955 100644 --- a/frigate/api/auth.py +++ b/frigate/api/auth.py @@ -447,8 +447,14 @@ def create_user( return JSONResponse(content={"username": body.username}) -@router.delete("/users/{username}") -def delete_user(username: str): +@router.delete("/users/{username}", dependencies=[Depends(require_role(["admin"]))]) +def delete_user(request: Request, username: str): + # Prevent deletion of the built-in admin user + if username == "admin": + return JSONResponse( + content={"message": "Cannot delete admin user"}, status_code=403 + ) + User.delete_by_id(username) return JSONResponse(content={"success": True}) diff --git a/web/src/components/player/HlsVideoPlayer.tsx b/web/src/components/player/HlsVideoPlayer.tsx index 4b1bfe5ef..38896f842 100644 --- a/web/src/components/player/HlsVideoPlayer.tsx +++ b/web/src/components/player/HlsVideoPlayer.tsx @@ -6,7 +6,7 @@ import { useState, } from "react"; import Hls from "hls.js"; -import { isAndroid, isDesktop, isMobile } from "react-device-detect"; +import { isDesktop, isMobile } from "react-device-detect"; import { TransformComponent, TransformWrapper } from "react-zoom-pan-pinch"; import VideoControls from "./VideoControls"; import { VideoResolutionType } from "@/types/live"; @@ -21,7 +21,7 @@ import { ASPECT_VERTICAL_LAYOUT, RecordingPlayerError } from "@/types/record"; import { useTranslation } from "react-i18next"; // Android native hls does not seek correctly -const USE_NATIVE_HLS = !isAndroid; +const USE_NATIVE_HLS = false; const HLS_MIME_TYPE = "application/vnd.apple.mpegurl" as const; const unsupportedErrorCodes = [ MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED, diff --git a/web/src/components/player/dynamic/DynamicVideoController.ts b/web/src/components/player/dynamic/DynamicVideoController.ts index 0683481c6..b68191e16 100644 --- a/web/src/components/player/dynamic/DynamicVideoController.ts +++ b/web/src/components/player/dynamic/DynamicVideoController.ts @@ -2,7 +2,10 @@ import { Recording } from "@/types/record"; import { DynamicPlayback } from "@/types/playback"; import { PreviewController } from "../PreviewPlayer"; import { TimeRange, ObjectLifecycleSequence } from "@/types/timeline"; -import { calculateInpointOffset } from "@/utils/videoUtil"; +import { + calculateInpointOffset, + calculateSeekPosition, +} from "@/utils/videoUtil"; type PlayerMode = "playback" | "scrubbing"; @@ -68,38 +71,20 @@ export class DynamicVideoController { return; } - if ( - this.recordings.length == 0 || - time < this.recordings[0].start_time || - time > this.recordings[this.recordings.length - 1].end_time - ) { - this.setNoRecording(true); - return; - } - if (this.playerMode != "playback") { this.playerMode = "playback"; } - let seekSeconds = 0; - (this.recordings || []).every((segment) => { - // if the next segment is past the desired time, stop calculating - if (segment.start_time > time) { - return false; - } + const seekSeconds = calculateSeekPosition( + time, + this.recordings, + this.inpointOffset, + ); - if (segment.end_time < time) { - seekSeconds += segment.end_time - segment.start_time; - return true; - } - - seekSeconds += - segment.end_time - segment.start_time - (segment.end_time - time); - return true; - }); - - // adjust for HLS inpoint offset - seekSeconds -= this.inpointOffset; + if (seekSeconds === undefined) { + this.setNoRecording(true); + return; + } if (seekSeconds != 0) { this.playerController.currentTime = seekSeconds; diff --git a/web/src/components/player/dynamic/DynamicVideoPlayer.tsx b/web/src/components/player/dynamic/DynamicVideoPlayer.tsx index 836203ca7..5967fc6dc 100644 --- a/web/src/components/player/dynamic/DynamicVideoPlayer.tsx +++ b/web/src/components/player/dynamic/DynamicVideoPlayer.tsx @@ -13,7 +13,10 @@ import { VideoResolutionType } from "@/types/live"; import axios from "axios"; import { cn } from "@/lib/utils"; import { useTranslation } from "react-i18next"; -import { calculateInpointOffset } from "@/utils/videoUtil"; +import { + calculateInpointOffset, + calculateSeekPosition, +} from "@/utils/videoUtil"; import { isFirefox } from "react-device-detect"; /** @@ -99,10 +102,10 @@ export default function DynamicVideoPlayer({ const [isLoading, setIsLoading] = useState(false); const [isBuffering, setIsBuffering] = useState(false); const [loadingTimeout, setLoadingTimeout] = useState(); - const [source, setSource] = useState({ - playlist: `${apiHost}vod/${camera}/start/${timeRange.after}/end/${timeRange.before}/master.m3u8`, - startPosition: startTimestamp ? timeRange.after - startTimestamp : 0, - }); + + // Don't set source until recordings load - we need accurate startPosition + // to avoid hls.js clamping to video end when startPosition exceeds duration + const [source, setSource] = useState(undefined); // start at correct time @@ -174,7 +177,7 @@ export default function DynamicVideoPlayer({ ); useEffect(() => { - if (!controller || !recordings?.length) { + if (!recordings?.length) { if (recordings?.length == 0) { setNoRecording(true); } @@ -182,10 +185,6 @@ export default function DynamicVideoPlayer({ return; } - if (playerRef.current) { - playerRef.current.autoplay = !isScrubbing; - } - let startPosition = undefined; if (startTimestamp) { @@ -193,14 +192,12 @@ export default function DynamicVideoPlayer({ recordingParams.after, (recordings || [])[0], ); - const idealStartPosition = Math.max( - 0, - startTimestamp - timeRange.after - inpointOffset, - ); - if (idealStartPosition >= recordings[0].start_time - timeRange.after) { - startPosition = idealStartPosition; - } + startPosition = calculateSeekPosition( + startTimestamp, + recordings, + inpointOffset, + ); } setSource({ @@ -208,6 +205,18 @@ export default function DynamicVideoPlayer({ startPosition, }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [recordings]); + + useEffect(() => { + if (!controller || !recordings?.length) { + return; + } + + if (playerRef.current) { + playerRef.current.autoplay = !isScrubbing; + } + setLoadingTimeout(setTimeout(() => setIsLoading(true), 1000)); controller.newPlayback({ @@ -215,7 +224,7 @@ export default function DynamicVideoPlayer({ timeRange, }); - // we only want this to change when recordings update + // we only want this to change when controller or recordings update // eslint-disable-next-line react-hooks/exhaustive-deps }, [controller, recordings]); @@ -253,38 +262,40 @@ export default function DynamicVideoPlayer({ return ( <> - { - if (isScrubbing) { - playerRef.current?.pause(); - } + {source && ( + { + if (isScrubbing) { + playerRef.current?.pause(); + } - if (loadingTimeout) { - clearTimeout(loadingTimeout); - } + if (loadingTimeout) { + clearTimeout(loadingTimeout); + } - setNoRecording(false); - }} - setFullResolution={setFullResolution} - onUploadFrame={onUploadFrameToPlus} - toggleFullscreen={toggleFullscreen} - onError={(error) => { - if (error == "stalled" && !isScrubbing) { - setIsBuffering(true); - } - }} - /> + setNoRecording(false); + }} + setFullResolution={setFullResolution} + onUploadFrame={onUploadFrameToPlus} + toggleFullscreen={toggleFullscreen} + onError={(error) => { + if (error == "stalled" && !isScrubbing) { + setIsBuffering(true); + } + }} + /> + )} 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; +}