mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-12-17 02:26:43 +03:00
Compare commits
4 Commits
9872f9cac1
...
533092635b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
533092635b | ||
|
|
28b0ad782a | ||
|
|
644c7fa6b4 | ||
|
|
793906bb68 |
@ -837,7 +837,19 @@ async def recording_clip(
|
|||||||
dependencies=[Depends(require_camera_access)],
|
dependencies=[Depends(require_camera_access)],
|
||||||
description="Returns an HLS playlist for the specified timestamp-range on the specified camera. Append /master.m3u8 or /index.m3u8 for HLS playback.",
|
description="Returns an HLS playlist for the specified timestamp-range on the specified camera. Append /master.m3u8 or /index.m3u8 for HLS playback.",
|
||||||
)
|
)
|
||||||
async def vod_ts(camera_name: str, start_ts: float, end_ts: float):
|
async def vod_ts(
|
||||||
|
camera_name: str,
|
||||||
|
start_ts: float,
|
||||||
|
end_ts: float,
|
||||||
|
force_discontinuity: bool = False,
|
||||||
|
):
|
||||||
|
logger.debug(
|
||||||
|
"VOD: Generating VOD for %s from %s to %s with force_discontinuity=%s",
|
||||||
|
camera_name,
|
||||||
|
start_ts,
|
||||||
|
end_ts,
|
||||||
|
force_discontinuity,
|
||||||
|
)
|
||||||
recordings = (
|
recordings = (
|
||||||
Recordings.select(
|
Recordings.select(
|
||||||
Recordings.path,
|
Recordings.path,
|
||||||
@ -862,6 +874,14 @@ async def vod_ts(camera_name: str, start_ts: float, end_ts: float):
|
|||||||
|
|
||||||
recording: Recordings
|
recording: Recordings
|
||||||
for recording in recordings:
|
for recording in recordings:
|
||||||
|
logger.debug(
|
||||||
|
"VOD: processing recording: %s start=%s end=%s duration=%s",
|
||||||
|
recording.path,
|
||||||
|
recording.start_time,
|
||||||
|
recording.end_time,
|
||||||
|
recording.duration,
|
||||||
|
)
|
||||||
|
|
||||||
clip = {"type": "source", "path": recording.path}
|
clip = {"type": "source", "path": recording.path}
|
||||||
duration = int(recording.duration * 1000)
|
duration = int(recording.duration * 1000)
|
||||||
|
|
||||||
@ -870,6 +890,11 @@ async def vod_ts(camera_name: str, start_ts: float, end_ts: float):
|
|||||||
inpoint = int((start_ts - recording.start_time) * 1000)
|
inpoint = int((start_ts - recording.start_time) * 1000)
|
||||||
clip["clipFrom"] = inpoint
|
clip["clipFrom"] = inpoint
|
||||||
duration -= inpoint
|
duration -= inpoint
|
||||||
|
logger.debug(
|
||||||
|
"VOD: applied clipFrom %sms to %s",
|
||||||
|
inpoint,
|
||||||
|
recording.path,
|
||||||
|
)
|
||||||
|
|
||||||
# adjust end if recording.end_time is after end_ts
|
# adjust end if recording.end_time is after end_ts
|
||||||
if recording.end_time > end_ts:
|
if recording.end_time > end_ts:
|
||||||
@ -877,12 +902,23 @@ async def vod_ts(camera_name: str, start_ts: float, end_ts: float):
|
|||||||
|
|
||||||
if duration < min_duration_ms:
|
if duration < min_duration_ms:
|
||||||
# skip if the clip has no valid duration (too short to contain frames)
|
# skip if the clip has no valid duration (too short to contain frames)
|
||||||
|
logger.debug(
|
||||||
|
"VOD: skipping recording %s - resulting duration %sms too short",
|
||||||
|
recording.path,
|
||||||
|
duration,
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if min_duration_ms <= duration < max_duration_ms:
|
if min_duration_ms <= duration < max_duration_ms:
|
||||||
clip["keyFrameDurations"] = [duration]
|
clip["keyFrameDurations"] = [duration]
|
||||||
clips.append(clip)
|
clips.append(clip)
|
||||||
durations.append(duration)
|
durations.append(duration)
|
||||||
|
logger.debug(
|
||||||
|
"VOD: added clip %s duration_ms=%s clipFrom=%s",
|
||||||
|
recording.path,
|
||||||
|
duration,
|
||||||
|
clip.get("clipFrom"),
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logger.warning(f"Recording clip is missing or empty: {recording.path}")
|
logger.warning(f"Recording clip is missing or empty: {recording.path}")
|
||||||
|
|
||||||
@ -902,7 +938,7 @@ async def vod_ts(camera_name: str, start_ts: float, end_ts: float):
|
|||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
content={
|
content={
|
||||||
"cache": hour_ago.timestamp() > start_ts,
|
"cache": hour_ago.timestamp() > start_ts,
|
||||||
"discontinuity": False,
|
"discontinuity": force_discontinuity,
|
||||||
"consistentSequenceMediaInfo": True,
|
"consistentSequenceMediaInfo": True,
|
||||||
"durations": durations,
|
"durations": durations,
|
||||||
"segment_duration": max(durations),
|
"segment_duration": max(durations),
|
||||||
@ -986,6 +1022,19 @@ async def vod_event(
|
|||||||
return vod_response
|
return vod_response
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/vod/clip/{camera_name}/start/{start_ts}/end/{end_ts}",
|
||||||
|
dependencies=[Depends(require_camera_access)],
|
||||||
|
description="Returns an HLS playlist for a timestamp range with HLS discontinuity enabled. Append /master.m3u8 or /index.m3u8 for HLS playback.",
|
||||||
|
)
|
||||||
|
async def vod_clip(
|
||||||
|
camera_name: str,
|
||||||
|
start_ts: float,
|
||||||
|
end_ts: float,
|
||||||
|
):
|
||||||
|
return await vod_ts(camera_name, start_ts, end_ts, force_discontinuity=True)
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/events/{event_id}/snapshot.jpg",
|
"/events/{event_id}/snapshot.jpg",
|
||||||
description="Returns a snapshot image for the specified object id. NOTE: The query params only take affect while the event is in-progress. Once the event has ended the snapshot configuration is used.",
|
description="Returns a snapshot image for the specified object id. NOTE: The query params only take affect while the event is in-progress. Once the event has ended the snapshot configuration is used.",
|
||||||
|
|||||||
@ -83,6 +83,7 @@
|
|||||||
"aria": "Select Recent Classifications"
|
"aria": "Select Recent Classifications"
|
||||||
},
|
},
|
||||||
"categories": "Classes",
|
"categories": "Classes",
|
||||||
|
"none": "None",
|
||||||
"createCategory": {
|
"createCategory": {
|
||||||
"new": "Create New Class"
|
"new": "Create New Class"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -131,7 +131,9 @@ export default function ClassificationSelectionDialog({
|
|||||||
className="flex cursor-pointer gap-2 smart-capitalize"
|
className="flex cursor-pointer gap-2 smart-capitalize"
|
||||||
onClick={() => onCategorizeImage(category)}
|
onClick={() => onCategorizeImage(category)}
|
||||||
>
|
>
|
||||||
{category.replaceAll("_", " ")}
|
{category === "none"
|
||||||
|
? t("none")
|
||||||
|
: category.replaceAll("_", " ")}
|
||||||
</SelectorItem>
|
</SelectorItem>
|
||||||
))}
|
))}
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|||||||
@ -446,7 +446,7 @@ export function TrackingDetails({
|
|||||||
(event.end_time ?? Date.now() / 1000) + annotationOffset / 1000;
|
(event.end_time ?? Date.now() / 1000) + annotationOffset / 1000;
|
||||||
const startTime = eventStartRecord - REVIEW_PADDING;
|
const startTime = eventStartRecord - REVIEW_PADDING;
|
||||||
const endTime = eventEndRecord + REVIEW_PADDING;
|
const endTime = eventEndRecord + REVIEW_PADDING;
|
||||||
const playlist = `${baseUrl}vod/${event.camera}/start/${startTime}/end/${endTime}/index.m3u8`;
|
const playlist = `${baseUrl}vod/clip/${event.camera}/start/${startTime}/end/${endTime}/index.m3u8`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
playlist,
|
playlist,
|
||||||
@ -559,7 +559,6 @@ export function TrackingDetails({
|
|||||||
isDetailMode={true}
|
isDetailMode={true}
|
||||||
camera={event.camera}
|
camera={event.camera}
|
||||||
currentTimeOverride={currentTime}
|
currentTimeOverride={currentTime}
|
||||||
enableGapControllerRecovery={true}
|
|
||||||
/>
|
/>
|
||||||
{isVideoLoading && (
|
{isVideoLoading && (
|
||||||
<ActivityIndicator className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
|
<ActivityIndicator className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
|
||||||
|
|||||||
@ -180,7 +180,9 @@ export function ClassFilterContent({
|
|||||||
{allClasses.map((item) => (
|
{allClasses.map((item) => (
|
||||||
<FilterSwitch
|
<FilterSwitch
|
||||||
key={item}
|
key={item}
|
||||||
label={item.replaceAll("_", " ")}
|
label={
|
||||||
|
item === "none" ? t("none") : item.replaceAll("_", " ")
|
||||||
|
}
|
||||||
isChecked={classes?.includes(item) ?? false}
|
isChecked={classes?.includes(item) ?? false}
|
||||||
onCheckedChange={(isChecked) => {
|
onCheckedChange={(isChecked) => {
|
||||||
if (isChecked) {
|
if (isChecked) {
|
||||||
|
|||||||
@ -57,7 +57,6 @@ type HlsVideoPlayerProps = {
|
|||||||
isDetailMode?: boolean;
|
isDetailMode?: boolean;
|
||||||
camera?: string;
|
camera?: string;
|
||||||
currentTimeOverride?: number;
|
currentTimeOverride?: number;
|
||||||
enableGapControllerRecovery?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function HlsVideoPlayer({
|
export default function HlsVideoPlayer({
|
||||||
@ -82,7 +81,6 @@ export default function HlsVideoPlayer({
|
|||||||
isDetailMode = false,
|
isDetailMode = false,
|
||||||
camera,
|
camera,
|
||||||
currentTimeOverride,
|
currentTimeOverride,
|
||||||
enableGapControllerRecovery = false,
|
|
||||||
}: HlsVideoPlayerProps) {
|
}: HlsVideoPlayerProps) {
|
||||||
const { t } = useTranslation("components/player");
|
const { t } = useTranslation("components/player");
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
@ -173,21 +171,12 @@ export default function HlsVideoPlayer({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Base HLS configuration
|
// Base HLS configuration
|
||||||
const baseConfig: Partial<HlsConfig> = {
|
const hlsConfig: Partial<HlsConfig> = {
|
||||||
maxBufferLength: 10,
|
maxBufferLength: 10,
|
||||||
maxBufferSize: 20 * 1000 * 1000,
|
maxBufferSize: 20 * 1000 * 1000,
|
||||||
startPosition: currentSource.startPosition,
|
startPosition: currentSource.startPosition,
|
||||||
};
|
};
|
||||||
|
|
||||||
const hlsConfig = { ...baseConfig };
|
|
||||||
|
|
||||||
if (enableGapControllerRecovery) {
|
|
||||||
hlsConfig.highBufferWatchdogPeriod = 1; // Check for stalls every 1 second (default: 3)
|
|
||||||
hlsConfig.nudgeOffset = 0.2; // Nudge playhead forward 0.2s when stalled (default: 0.1)
|
|
||||||
hlsConfig.nudgeMaxRetry = 5; // Try up to 5 nudges before giving up (default: 3)
|
|
||||||
hlsConfig.maxBufferHole = 0.5; // Tolerate up to 0.5s gaps between fragments (default: 0.1)
|
|
||||||
}
|
|
||||||
|
|
||||||
hlsRef.current = new Hls(hlsConfig);
|
hlsRef.current = new Hls(hlsConfig);
|
||||||
hlsRef.current.attachMedia(videoRef.current);
|
hlsRef.current.attachMedia(videoRef.current);
|
||||||
hlsRef.current.loadSource(currentSource.playlist);
|
hlsRef.current.loadSource(currentSource.playlist);
|
||||||
@ -201,13 +190,7 @@ export default function HlsVideoPlayer({
|
|||||||
hlsRef.current.destroy();
|
hlsRef.current.destroy();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [
|
}, [videoRef, hlsRef, useHlsCompat, currentSource]);
|
||||||
videoRef,
|
|
||||||
hlsRef,
|
|
||||||
useHlsCompat,
|
|
||||||
currentSource,
|
|
||||||
enableGapControllerRecovery,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// state handling
|
// state handling
|
||||||
|
|
||||||
|
|||||||
@ -371,7 +371,12 @@ export default function FaceLibrary() {
|
|||||||
{selectedFaces?.length > 0 ? (
|
{selectedFaces?.length > 0 ? (
|
||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2">
|
||||||
<div className="mx-1 flex w-48 items-center justify-center text-sm text-muted-foreground">
|
<div className="mx-1 flex w-48 items-center justify-center text-sm text-muted-foreground">
|
||||||
<div className="p-1">{`${selectedFaces.length} selected`}</div>
|
<div className="p-1">
|
||||||
|
{t("selected", {
|
||||||
|
ns: "views/event",
|
||||||
|
count: selectedFaces.length,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
<div className="p-1">{"|"}</div>
|
<div className="p-1">{"|"}</div>
|
||||||
<div
|
<div
|
||||||
className="cursor-pointer p-2 text-primary hover:rounded-lg hover:bg-secondary"
|
className="cursor-pointer p-2 text-primary hover:rounded-lg hover:bg-secondary"
|
||||||
|
|||||||
@ -37,6 +37,7 @@ import EnrichmentsSettingsView from "@/views/settings/EnrichmentsSettingsView";
|
|||||||
import UiSettingsView from "@/views/settings/UiSettingsView";
|
import UiSettingsView from "@/views/settings/UiSettingsView";
|
||||||
import FrigatePlusSettingsView from "@/views/settings/FrigatePlusSettingsView";
|
import FrigatePlusSettingsView from "@/views/settings/FrigatePlusSettingsView";
|
||||||
import { useSearchEffect } from "@/hooks/use-overlay-state";
|
import { useSearchEffect } from "@/hooks/use-overlay-state";
|
||||||
|
import { usePersistence } from "@/hooks/use-persistence";
|
||||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||||
import { useInitialCameraState } from "@/api/ws";
|
import { useInitialCameraState } from "@/api/ws";
|
||||||
import { useIsAdmin } from "@/hooks/use-is-admin";
|
import { useIsAdmin } from "@/hooks/use-is-admin";
|
||||||
@ -207,7 +208,21 @@ export default function Settings() {
|
|||||||
.sort((aConf, bConf) => aConf.ui.order - bConf.ui.order);
|
.sort((aConf, bConf) => aConf.ui.order - bConf.ui.order);
|
||||||
}, [config]);
|
}, [config]);
|
||||||
|
|
||||||
const [selectedCamera, setSelectedCamera] = useState<string>("");
|
const [persistedCamera, setPersistedCamera] = usePersistence(
|
||||||
|
"selectedCamera",
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
const [selectedCamera, setSelectedCamera] = useState(persistedCamera);
|
||||||
|
useEffect(() => {
|
||||||
|
if (persistedCamera) {
|
||||||
|
setSelectedCamera(persistedCamera);
|
||||||
|
}
|
||||||
|
}, [persistedCamera]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedCamera) {
|
||||||
|
setPersistedCamera(selectedCamera);
|
||||||
|
}
|
||||||
|
}, [selectedCamera, setPersistedCamera]);
|
||||||
|
|
||||||
const { payload: allCameraStates } = useInitialCameraState(
|
const { payload: allCameraStates } = useInitialCameraState(
|
||||||
cameras.length > 0 ? cameras[0].name : "",
|
cameras.length > 0 ? cameras[0].name : "",
|
||||||
|
|||||||
@ -422,7 +422,12 @@ export default function ModelTrainingView({ model }: ModelTrainingViewProps) {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex w-48 items-center justify-center text-sm text-muted-foreground">
|
<div className="flex w-48 items-center justify-center text-sm text-muted-foreground">
|
||||||
<div className="p-1">{`${selectedImages.length} selected`}</div>
|
<div className="p-1">
|
||||||
|
{t("selected", {
|
||||||
|
ns: "views/event",
|
||||||
|
count: selectedImages.length,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
<div className="p-1">{"|"}</div>
|
<div className="p-1">{"|"}</div>
|
||||||
<div
|
<div
|
||||||
className="cursor-pointer p-2 text-primary hover:rounded-lg hover:bg-secondary"
|
className="cursor-pointer p-2 text-primary hover:rounded-lg hover:bg-secondary"
|
||||||
@ -682,7 +687,7 @@ function LibrarySelector({
|
|||||||
className="flex-grow cursor-pointer capitalize"
|
className="flex-grow cursor-pointer capitalize"
|
||||||
onClick={() => setPageToggle(id)}
|
onClick={() => setPageToggle(id)}
|
||||||
>
|
>
|
||||||
{id.replaceAll("_", " ")}
|
{id === "none" ? t("none") : id.replaceAll("_", " ")}
|
||||||
<span className="ml-2 text-muted-foreground">
|
<span className="ml-2 text-muted-foreground">
|
||||||
({dataset?.[id].length})
|
({dataset?.[id].length})
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user