Miscellaneous Fixes (#21024)

* fix wording in reference config

* spacing tweaks

* make live view settings drawer scrollable

* clarify audio transcription docs

* change audio transcription icon to activity indicator when transcription is in progress

the backend doesn't implement any kind of queueing for speech event transcription

* tracking details tweaks

- Add attribute box overlay and area
- Add score
- Throttle swr revalidation during video component rerendering

* add mse codecs to console debug on errors

* add camera name
This commit is contained in:
Josh Hawkins 2025-11-24 07:34:56 -06:00 committed by GitHub
parent 2d8b6c8301
commit aa8b423b68
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 592 additions and 390 deletions

View File

@ -144,4 +144,10 @@ In order to use transcription and translation for past events, you must enable a
The transcribed/translated speech will appear in the description box in the Tracked Object Details pane. If Semantic Search is enabled, embeddings are generated for the transcription text and are fully searchable using the description search type. The transcribed/translated speech will appear in the description box in the Tracked Object Details pane. If Semantic Search is enabled, embeddings are generated for the transcription text and are fully searchable using the description search type.
Recorded `speech` events will always use a `whisper` model, regardless of the `model_size` config setting. Without a GPU, generating transcriptions for longer `speech` events may take a fair amount of time, so be patient. :::note
Only one `speech` event may be transcribed at a time. Frigate does not automatically transcribe `speech` events or implement a queue for long-running transcription model inference.
:::
Recorded `speech` events will always use a `whisper` model, regardless of the `model_size` config setting. Without a supported Nvidia GPU, generating transcriptions for longer `speech` events may take a fair amount of time, so be patient.

View File

@ -700,11 +700,11 @@ genai:
# Optional: Configuration for audio transcription # Optional: Configuration for audio transcription
# NOTE: only the enabled option can be overridden at the camera level # NOTE: only the enabled option can be overridden at the camera level
audio_transcription: audio_transcription:
# Optional: Enable license plate recognition (default: shown below) # Optional: Enable live and speech event audio transcription (default: shown below)
enabled: False enabled: False
# Optional: The device to run the models on (default: shown below) # Optional: The device to run the models on for live transcription. (default: shown below)
device: CPU device: CPU
# Optional: Set the model size used for transcription. (default: shown below) # Optional: Set the model size used for live transcription. (default: shown below)
model_size: small model_size: small
# Optional: Set the language used for transcription translation. (default: shown below) # Optional: Set the language used for transcription translation. (default: shown below)
# List of language codes: https://github.com/openai/whisper/blob/main/whisper/tokenizer.py#L10 # List of language codes: https://github.com/openai/whisper/blob/main/whisper/tokenizer.py#L10

View File

@ -542,6 +542,7 @@ def transcribe_audio(request: Request, body: AudioTranscriptionBody):
status_code=409, # 409 Conflict status_code=409, # 409 Conflict
) )
else: else:
logger.debug(f"Failed to transcribe audio, response: {response}")
return JSONResponse( return JSONResponse(
content={ content={
"success": False, "success": False,

View File

@ -23,6 +23,7 @@ from frigate.const import (
NOTIFICATION_TEST, NOTIFICATION_TEST,
REQUEST_REGION_GRID, REQUEST_REGION_GRID,
UPDATE_AUDIO_ACTIVITY, UPDATE_AUDIO_ACTIVITY,
UPDATE_AUDIO_TRANSCRIPTION_STATE,
UPDATE_BIRDSEYE_LAYOUT, UPDATE_BIRDSEYE_LAYOUT,
UPDATE_CAMERA_ACTIVITY, UPDATE_CAMERA_ACTIVITY,
UPDATE_EMBEDDINGS_REINDEX_PROGRESS, UPDATE_EMBEDDINGS_REINDEX_PROGRESS,
@ -61,6 +62,7 @@ class Dispatcher:
self.model_state: dict[str, ModelStatusTypesEnum] = {} self.model_state: dict[str, ModelStatusTypesEnum] = {}
self.embeddings_reindex: dict[str, Any] = {} self.embeddings_reindex: dict[str, Any] = {}
self.birdseye_layout: dict[str, Any] = {} self.birdseye_layout: dict[str, Any] = {}
self.audio_transcription_state: str = "idle"
self._camera_settings_handlers: dict[str, Callable] = { self._camera_settings_handlers: dict[str, Callable] = {
"audio": self._on_audio_command, "audio": self._on_audio_command,
"audio_transcription": self._on_audio_transcription_command, "audio_transcription": self._on_audio_transcription_command,
@ -178,6 +180,19 @@ class Dispatcher:
def handle_model_state() -> None: def handle_model_state() -> None:
self.publish("model_state", json.dumps(self.model_state.copy())) self.publish("model_state", json.dumps(self.model_state.copy()))
def handle_update_audio_transcription_state() -> None:
if payload:
self.audio_transcription_state = payload
self.publish(
"audio_transcription_state",
json.dumps(self.audio_transcription_state),
)
def handle_audio_transcription_state() -> None:
self.publish(
"audio_transcription_state", json.dumps(self.audio_transcription_state)
)
def handle_update_embeddings_reindex_progress() -> None: def handle_update_embeddings_reindex_progress() -> None:
self.embeddings_reindex = payload self.embeddings_reindex = payload
self.publish( self.publish(
@ -264,10 +279,12 @@ class Dispatcher:
UPDATE_MODEL_STATE: handle_update_model_state, UPDATE_MODEL_STATE: handle_update_model_state,
UPDATE_EMBEDDINGS_REINDEX_PROGRESS: handle_update_embeddings_reindex_progress, UPDATE_EMBEDDINGS_REINDEX_PROGRESS: handle_update_embeddings_reindex_progress,
UPDATE_BIRDSEYE_LAYOUT: handle_update_birdseye_layout, UPDATE_BIRDSEYE_LAYOUT: handle_update_birdseye_layout,
UPDATE_AUDIO_TRANSCRIPTION_STATE: handle_update_audio_transcription_state,
NOTIFICATION_TEST: handle_notification_test, NOTIFICATION_TEST: handle_notification_test,
"restart": handle_restart, "restart": handle_restart,
"embeddingsReindexProgress": handle_embeddings_reindex_progress, "embeddingsReindexProgress": handle_embeddings_reindex_progress,
"modelState": handle_model_state, "modelState": handle_model_state,
"audioTranscriptionState": handle_audio_transcription_state,
"birdseyeLayout": handle_birdseye_layout, "birdseyeLayout": handle_birdseye_layout,
"onConnect": handle_on_connect, "onConnect": handle_on_connect,
} }

View File

@ -113,6 +113,7 @@ CLEAR_ONGOING_REVIEW_SEGMENTS = "clear_ongoing_review_segments"
UPDATE_CAMERA_ACTIVITY = "update_camera_activity" UPDATE_CAMERA_ACTIVITY = "update_camera_activity"
UPDATE_AUDIO_ACTIVITY = "update_audio_activity" UPDATE_AUDIO_ACTIVITY = "update_audio_activity"
EXPIRE_AUDIO_ACTIVITY = "expire_audio_activity" EXPIRE_AUDIO_ACTIVITY = "expire_audio_activity"
UPDATE_AUDIO_TRANSCRIPTION_STATE = "update_audio_transcription_state"
UPDATE_EVENT_DESCRIPTION = "update_event_description" UPDATE_EVENT_DESCRIPTION = "update_event_description"
UPDATE_REVIEW_DESCRIPTION = "update_review_description" UPDATE_REVIEW_DESCRIPTION = "update_review_description"
UPDATE_MODEL_STATE = "update_model_state" UPDATE_MODEL_STATE = "update_model_state"

View File

@ -13,6 +13,7 @@ from frigate.config import FrigateConfig
from frigate.const import ( from frigate.const import (
CACHE_DIR, CACHE_DIR,
MODEL_CACHE_DIR, MODEL_CACHE_DIR,
UPDATE_AUDIO_TRANSCRIPTION_STATE,
UPDATE_EVENT_DESCRIPTION, UPDATE_EVENT_DESCRIPTION,
) )
from frigate.data_processing.types import PostProcessDataEnum from frigate.data_processing.types import PostProcessDataEnum
@ -190,6 +191,8 @@ class AudioTranscriptionPostProcessor(PostProcessorApi):
self.transcription_running = False self.transcription_running = False
self.transcription_thread = None self.transcription_thread = None
self.requestor.send_data(UPDATE_AUDIO_TRANSCRIPTION_STATE, "idle")
def handle_request(self, topic: str, request_data: dict[str, any]) -> str | None: def handle_request(self, topic: str, request_data: dict[str, any]) -> str | None:
if topic == "transcribe_audio": if topic == "transcribe_audio":
event = request_data["event"] event = request_data["event"]
@ -203,6 +206,8 @@ class AudioTranscriptionPostProcessor(PostProcessorApi):
# Mark as running and start the thread # Mark as running and start the thread
self.transcription_running = True self.transcription_running = True
self.requestor.send_data(UPDATE_AUDIO_TRANSCRIPTION_STATE, "processing")
self.transcription_thread = threading.Thread( self.transcription_thread = threading.Thread(
target=self._transcription_wrapper, args=(event,), daemon=True target=self._transcription_wrapper, args=(event,), daemon=True
) )

View File

@ -109,6 +109,7 @@ class TimelineProcessor(threading.Thread):
event_data["region"], event_data["region"],
), ),
"attribute": "", "attribute": "",
"score": event_data["score"],
}, },
} }

View File

@ -461,6 +461,40 @@ export function useEmbeddingsReindexProgress(
return { payload: data }; return { payload: data };
} }
export function useAudioTranscriptionProcessState(
revalidateOnFocus: boolean = true,
): { payload: string } {
const {
value: { payload },
send: sendCommand,
} = useWs("audio_transcription_state", "audioTranscriptionState");
const data = useDeepMemo(
payload ? (JSON.parse(payload as string) as string) : "idle",
);
useEffect(() => {
let listener = undefined;
if (revalidateOnFocus) {
sendCommand("audioTranscriptionState");
listener = () => {
if (document.visibilityState == "visible") {
sendCommand("audioTranscriptionState");
}
};
addEventListener("visibilitychange", listener);
}
return () => {
if (listener) {
removeEventListener("visibilitychange", listener);
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [revalidateOnFocus]);
return { payload: data || "idle" };
}
export function useBirdseyeLayout(revalidateOnFocus: boolean = true): { export function useBirdseyeLayout(revalidateOnFocus: boolean = true): {
payload: string; payload: string;
} { } {

View File

@ -42,6 +42,7 @@ type ObjectData = {
pathPoints: PathPoint[]; pathPoints: PathPoint[];
currentZones: string[]; currentZones: string[];
currentBox?: number[]; currentBox?: number[];
currentAttributeBox?: number[];
}; };
export default function ObjectTrackOverlay({ export default function ObjectTrackOverlay({
@ -105,6 +106,12 @@ export default function ObjectTrackOverlay({
selectedObjectIds.length > 0 selectedObjectIds.length > 0
? ["event_ids", { ids: selectedObjectIds.join(",") }] ? ["event_ids", { ids: selectedObjectIds.join(",") }]
: null, : null,
null,
{
revalidateOnFocus: false,
revalidateOnReconnect: false,
dedupingInterval: 30000,
},
); );
// Fetch timeline data for each object ID using fixed number of hooks // Fetch timeline data for each object ID using fixed number of hooks
@ -112,7 +119,12 @@ export default function ObjectTrackOverlay({
selectedObjectIds.length > 0 selectedObjectIds.length > 0
? `timeline?source_id=${selectedObjectIds.join(",")}&limit=1000` ? `timeline?source_id=${selectedObjectIds.join(",")}&limit=1000`
: null, : null,
{ revalidateOnFocus: false }, null,
{
revalidateOnFocus: false,
revalidateOnReconnect: false,
dedupingInterval: 30000,
},
); );
const getZonesFriendlyNames = (zones: string[], config: FrigateConfig) => { const getZonesFriendlyNames = (zones: string[], config: FrigateConfig) => {
@ -270,6 +282,7 @@ export default function ObjectTrackOverlay({
); );
const currentBox = nearbyTimelineEvent?.data?.box; const currentBox = nearbyTimelineEvent?.data?.box;
const currentAttributeBox = nearbyTimelineEvent?.data?.attribute_box;
return { return {
objectId, objectId,
@ -278,6 +291,7 @@ export default function ObjectTrackOverlay({
pathPoints: combinedPoints, pathPoints: combinedPoints,
currentZones, currentZones,
currentBox, currentBox,
currentAttributeBox,
}; };
}) })
.filter((obj: ObjectData) => obj.pathPoints.length > 0); // Only include objects with path data .filter((obj: ObjectData) => obj.pathPoints.length > 0); // Only include objects with path data
@ -482,6 +496,20 @@ export default function ObjectTrackOverlay({
/> />
</g> </g>
)} )}
{objData.currentAttributeBox && showBoundingBoxes && (
<g>
<rect
x={objData.currentAttributeBox[0] * videoWidth}
y={objData.currentAttributeBox[1] * videoHeight}
width={objData.currentAttributeBox[2] * videoWidth}
height={objData.currentAttributeBox[3] * videoHeight}
fill="none"
stroke={objData.color}
strokeWidth={boxStroke}
opacity="0.9"
/>
</g>
)}
</g> </g>
); );
})} })}

View File

@ -92,6 +92,7 @@ import { DialogPortal } from "@radix-ui/react-dialog";
import { useDetailStream } from "@/context/detail-stream-context"; import { useDetailStream } from "@/context/detail-stream-context";
import { PiSlidersHorizontalBold } from "react-icons/pi"; import { PiSlidersHorizontalBold } from "react-icons/pi";
import { HiSparkles } from "react-icons/hi"; import { HiSparkles } from "react-icons/hi";
import { useAudioTranscriptionProcessState } from "@/api/ws";
const SEARCH_TABS = ["snapshot", "tracking_details"] as const; const SEARCH_TABS = ["snapshot", "tracking_details"] as const;
export type SearchTab = (typeof SEARCH_TABS)[number]; export type SearchTab = (typeof SEARCH_TABS)[number];
@ -1076,6 +1077,11 @@ function ObjectDetailsTab({
}); });
}, [search, t]); }, [search, t]);
// audio transcription processing state
const { payload: audioTranscriptionProcessState } =
useAudioTranscriptionProcessState();
// frigate+ submission // frigate+ submission
type SubmissionState = "reviewing" | "uploading" | "submitted"; type SubmissionState = "reviewing" | "uploading" | "submitted";
@ -1431,10 +1437,20 @@ function ObjectDetailsTab({
<TooltipTrigger asChild> <TooltipTrigger asChild>
<button <button
aria-label={t("itemMenu.audioTranscription.label")} aria-label={t("itemMenu.audioTranscription.label")}
className="text-primary/40 hover:text-primary/80" className={cn(
"text-primary/40",
audioTranscriptionProcessState === "processing"
? "cursor-not-allowed"
: "hover:text-primary/80",
)}
onClick={onTranscribe} onClick={onTranscribe}
disabled={audioTranscriptionProcessState === "processing"}
> >
{audioTranscriptionProcessState === "processing" ? (
<ActivityIndicator className="size-4" />
) : (
<FaMicrophone className="size-4" /> <FaMicrophone className="size-4" />
)}
</button> </button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>

View File

@ -75,12 +75,15 @@ export function TrackingDetails({
setIsVideoLoading(true); setIsVideoLoading(true);
}, [event.id]); }, [event.id]);
const { data: eventSequence } = useSWR<TrackingDetailsSequence[]>([ const { data: eventSequence } = useSWR<TrackingDetailsSequence[]>(
"timeline", ["timeline", { source_id: event.id }],
null,
{ {
source_id: event.id, revalidateOnFocus: false,
revalidateOnReconnect: false,
dedupingInterval: 30000,
}, },
]); );
const { data: config } = useSWR<FrigateConfig>("config"); const { data: config } = useSWR<FrigateConfig>("config");
@ -104,6 +107,12 @@ export function TrackingDetails({
}, },
] ]
: null, : null,
null,
{
revalidateOnFocus: false,
revalidateOnReconnect: false,
dedupingInterval: 30000,
},
); );
// Convert a timeline timestamp to actual video player time, accounting for // Convert a timeline timestamp to actual video player time, accounting for
@ -714,53 +723,6 @@ export function TrackingDetails({
)} )}
<div className="space-y-2"> <div className="space-y-2">
{eventSequence.map((item, idx) => { {eventSequence.map((item, idx) => {
const isActive =
Math.abs(
(effectiveTime ?? 0) - (item.timestamp ?? 0),
) <= 0.5;
const formattedEventTimestamp = config
? formatUnixTimestampToDateTime(item.timestamp ?? 0, {
timezone: config.ui.timezone,
date_format:
config.ui.time_format == "24hour"
? t(
"time.formattedTimestampHourMinuteSecond.24hour",
{ ns: "common" },
)
: t(
"time.formattedTimestampHourMinuteSecond.12hour",
{ ns: "common" },
),
time_style: "medium",
date_style: "medium",
})
: "";
const ratio =
Array.isArray(item.data.box) &&
item.data.box.length >= 4
? (
aspectRatio *
(item.data.box[2] / item.data.box[3])
).toFixed(2)
: "N/A";
const areaPx =
Array.isArray(item.data.box) &&
item.data.box.length >= 4
? Math.round(
(config.cameras[event.camera]?.detect?.width ??
0) *
(config.cameras[event.camera]?.detect
?.height ?? 0) *
(item.data.box[2] * item.data.box[3]),
)
: undefined;
const areaPct =
Array.isArray(item.data.box) &&
item.data.box.length >= 4
? (item.data.box[2] * item.data.box[3]).toFixed(4)
: undefined;
return ( return (
<div <div
key={`${item.timestamp}-${item.source_id ?? ""}-${idx}`} key={`${item.timestamp}-${item.source_id ?? ""}-${idx}`}
@ -770,11 +732,7 @@ export function TrackingDetails({
> >
<LifecycleIconRow <LifecycleIconRow
item={item} item={item}
isActive={isActive} event={event}
formattedEventTimestamp={formattedEventTimestamp}
ratio={ratio}
areaPx={areaPx}
areaPct={areaPct}
onClick={() => handleLifecycleClick(item)} onClick={() => handleLifecycleClick(item)}
setSelectedZone={setSelectedZone} setSelectedZone={setSelectedZone}
getZoneColor={getZoneColor} getZoneColor={getZoneColor}
@ -798,11 +756,7 @@ export function TrackingDetails({
type LifecycleIconRowProps = { type LifecycleIconRowProps = {
item: TrackingDetailsSequence; item: TrackingDetailsSequence;
isActive?: boolean; event: Event;
formattedEventTimestamp: string;
ratio: string;
areaPx?: number;
areaPct?: string;
onClick: () => void; onClick: () => void;
setSelectedZone: (z: string) => void; setSelectedZone: (z: string) => void;
getZoneColor: (zoneName: string) => number[] | undefined; getZoneColor: (zoneName: string) => number[] | undefined;
@ -812,11 +766,7 @@ type LifecycleIconRowProps = {
function LifecycleIconRow({ function LifecycleIconRow({
item, item,
isActive, event,
formattedEventTimestamp,
ratio,
areaPx,
areaPct,
onClick, onClick,
setSelectedZone, setSelectedZone,
getZoneColor, getZoneColor,
@ -826,9 +776,101 @@ function LifecycleIconRow({
const { t } = useTranslation(["views/explore", "components/player"]); const { t } = useTranslation(["views/explore", "components/player"]);
const { data: config } = useSWR<FrigateConfig>("config"); const { data: config } = useSWR<FrigateConfig>("config");
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const navigate = useNavigate(); const navigate = useNavigate();
const aspectRatio = useMemo(() => {
if (!config) {
return 16 / 9;
}
return (
config.cameras[event.camera].detect.width /
config.cameras[event.camera].detect.height
);
}, [config, event]);
const isActive = useMemo(
() => Math.abs((effectiveTime ?? 0) - (item.timestamp ?? 0)) <= 0.5,
[effectiveTime, item.timestamp],
);
const formattedEventTimestamp = useMemo(
() =>
config
? formatUnixTimestampToDateTime(item.timestamp ?? 0, {
timezone: config.ui.timezone,
date_format:
config.ui.time_format == "24hour"
? t("time.formattedTimestampHourMinuteSecond.24hour", {
ns: "common",
})
: t("time.formattedTimestampHourMinuteSecond.12hour", {
ns: "common",
}),
time_style: "medium",
date_style: "medium",
})
: "",
[config, item.timestamp, t],
);
const ratio = useMemo(
() =>
Array.isArray(item.data.box) && item.data.box.length >= 4
? (aspectRatio * (item.data.box[2] / item.data.box[3])).toFixed(2)
: "N/A",
[aspectRatio, item.data.box],
);
const areaPx = useMemo(
() =>
Array.isArray(item.data.box) && item.data.box.length >= 4
? Math.round(
(config?.cameras[event.camera]?.detect?.width ?? 0) *
(config?.cameras[event.camera]?.detect?.height ?? 0) *
(item.data.box[2] * item.data.box[3]),
)
: undefined,
[config, event.camera, item.data.box],
);
const attributeAreaPx = useMemo(
() =>
Array.isArray(item.data.attribute_box) &&
item.data.attribute_box.length >= 4
? Math.round(
(config?.cameras[event.camera]?.detect?.width ?? 0) *
(config?.cameras[event.camera]?.detect?.height ?? 0) *
(item.data.attribute_box[2] * item.data.attribute_box[3]),
)
: undefined,
[config, event.camera, item.data.attribute_box],
);
const attributeAreaPct = useMemo(
() =>
Array.isArray(item.data.attribute_box) &&
item.data.attribute_box.length >= 4
? (item.data.attribute_box[2] * item.data.attribute_box[3]).toFixed(4)
: undefined,
[item.data.attribute_box],
);
const areaPct = useMemo(
() =>
Array.isArray(item.data.box) && item.data.box.length >= 4
? (item.data.box[2] * item.data.box[3]).toFixed(4)
: undefined,
[item.data.box],
);
const score = useMemo(() => {
if (item.data.score !== undefined) {
return (item.data.score * 100).toFixed(0) + "%";
}
return "N/A";
}, [item.data.score]);
return ( return (
<div <div
role="button" role="button"
@ -856,16 +898,28 @@ function LifecycleIconRow({
<div className="text-md flex items-start break-words text-left"> <div className="text-md flex items-start break-words text-left">
{getLifecycleItemDescription(item)} {getLifecycleItemDescription(item)}
</div> </div>
<div className="mt-1 flex flex-wrap items-center gap-2 text-xs text-secondary-foreground md:gap-5"> <div className="my-2 ml-2 flex flex-col flex-wrap items-start gap-1.5 text-xs text-secondary-foreground">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1.5">
<span className="text-primary-variant">
{t("trackingDetails.lifecycleItemDesc.header.score")}
</span>
<span className="font-medium text-primary">{score}</span>
</div>
<div className="flex items-center gap-1.5">
<span className="text-primary-variant"> <span className="text-primary-variant">
{t("trackingDetails.lifecycleItemDesc.header.ratio")} {t("trackingDetails.lifecycleItemDesc.header.ratio")}
</span> </span>
<span className="font-medium text-primary">{ratio}</span> <span className="font-medium text-primary">{ratio}</span>
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1.5">
<span className="text-primary-variant"> <span className="text-primary-variant">
{t("trackingDetails.lifecycleItemDesc.header.area")} {t("trackingDetails.lifecycleItemDesc.header.area")}{" "}
{attributeAreaPx !== undefined &&
attributeAreaPct !== undefined && (
<span className="text-primary-variant">
({getTranslatedLabel(item.data.label)})
</span>
)}
</span> </span>
{areaPx !== undefined && areaPct !== undefined ? ( {areaPx !== undefined && areaPct !== undefined ? (
<span className="font-medium text-primary"> <span className="font-medium text-primary">
@ -876,9 +930,25 @@ function LifecycleIconRow({
<span>N/A</span> <span>N/A</span>
)} )}
</div> </div>
{attributeAreaPx !== undefined &&
attributeAreaPct !== undefined && (
<div className="flex items-center gap-1.5">
<span className="text-primary-variant">
{t("trackingDetails.lifecycleItemDesc.header.area")} (
{getTranslatedLabel(item.data.attribute)})
</span>
<span className="font-medium text-primary">
{t("information.pixels", {
ns: "common",
area: attributeAreaPx,
})}{" "}
· {attributeAreaPct}%
</span>
</div>
)}
{item.data?.zones && item.data.zones.length > 0 && ( {item.data?.zones && item.data.zones.length > 0 && (
<div className="flex flex-wrap items-center gap-2"> <div className="mt-1 flex flex-wrap items-center gap-2">
{item.data.zones.map((zone, zidx) => { {item.data.zones.map((zone, zidx) => {
const color = getZoneColor(zone)?.join(",") ?? "0,0,0"; const color = getZoneColor(zone)?.join(",") ?? "0,0,0";
return ( return (

View File

@ -82,6 +82,7 @@ function MSEPlayer({
[key: string]: (msg: { value: string; type: string }) => void; [key: string]: (msg: { value: string; type: string }) => void;
}>({}); }>({});
const msRef = useRef<MediaSource | null>(null); const msRef = useRef<MediaSource | null>(null);
const mseCodecRef = useRef<string | null>(null);
const wsURL = useMemo(() => { const wsURL = useMemo(() => {
return `${baseUrl.replace(/^http/, "ws")}live/mse/api/ws?src=${camera}`; return `${baseUrl.replace(/^http/, "ws")}live/mse/api/ws?src=${camera}`;
@ -93,6 +94,10 @@ function MSEPlayer({
console.error( console.error(
`${camera} - MSE error '${error}': ${description} See the documentation: https://docs.frigate.video/configuration/live/#live-player-error-messages`, `${camera} - MSE error '${error}': ${description} See the documentation: https://docs.frigate.video/configuration/live/#live-player-error-messages`,
); );
if (mseCodecRef.current) {
// eslint-disable-next-line no-console
console.error(`${camera} - MSE codec in use: ${mseCodecRef.current}`);
}
onError?.(error); onError?.(error);
}, },
[camera, onError], [camera, onError],
@ -299,6 +304,9 @@ function MSEPlayer({
onmessageRef.current["mse"] = (msg) => { onmessageRef.current["mse"] = (msg) => {
if (msg.type !== "mse") return; if (msg.type !== "mse") return;
// Store the codec value for error logging
mseCodecRef.current = msg.value;
let sb: SourceBuffer | undefined; let sb: SourceBuffer | undefined;
try { try {
sb = msRef.current?.addSourceBuffer(msg.value); sb = msRef.current?.addSourceBuffer(msg.value);

View File

@ -16,6 +16,7 @@ export type TrackingDetailsSequence = {
data: { data: {
camera: string; camera: string;
label: string; label: string;
score: number;
sub_label: string; sub_label: string;
box?: [number, number, number, number]; box?: [number, number, number, number];
region: [number, number, number, number]; region: [number, number, number, number];

View File

@ -1376,8 +1376,9 @@ function FrigateCameraFeatures({
title={t("cameraSettings.title", { camera })} title={t("cameraSettings.title", { camera })}
/> />
</DrawerTrigger> </DrawerTrigger>
<DrawerContent className="rounded-2xl px-2 py-4"> <DrawerContent className="max-h-[75dvh] overflow-hidden rounded-2xl">
<div className="mt-2 flex flex-col gap-2"> <div className="scrollbar-container mt-2 flex h-auto flex-col gap-2 overflow-y-auto px-2 py-4">
<>
{isAdmin && ( {isAdmin && (
<> <>
<FilterSwitch <FilterSwitch
@ -1425,7 +1426,9 @@ function FrigateCameraFeatures({
disabled={audioState == "OFF"} disabled={audioState == "OFF"}
isChecked={transcriptionState == "ON"} isChecked={transcriptionState == "ON"}
onCheckedChange={() => onCheckedChange={() =>
sendTranscription(transcriptionState == "ON" ? "OFF" : "ON") sendTranscription(
transcriptionState == "ON" ? "OFF" : "ON",
)
} }
/> />
)} )}
@ -1440,7 +1443,6 @@ function FrigateCameraFeatures({
)} )}
</> </>
)} )}
</div>
<div className="mt-3 flex flex-col gap-5"> <div className="mt-3 flex flex-col gap-5">
{!isRestreamed && ( {!isRestreamed && (
@ -1482,7 +1484,8 @@ function FrigateCameraFeatures({
</div> </div>
</div> </div>
)} )}
{isRestreamed && Object.values(camera.live.streams).length > 0 && ( {isRestreamed &&
Object.values(camera.live.streams).length > 0 && (
<div className="mt-1 p-2"> <div className="mt-1 p-2">
<div className="mb-1 text-sm">{t("stream.title")}</div> <div className="mb-1 text-sm">{t("stream.title")}</div>
<Select <Select
@ -1526,7 +1529,9 @@ function FrigateCameraFeatures({
</div> </div>
)} )}
{preferredLiveMode != "jsmpeg" && !debug && isRestreamed && ( {preferredLiveMode != "jsmpeg" &&
!debug &&
isRestreamed && (
<div className="mt-1 flex flex-row items-center gap-1 text-sm text-muted-foreground"> <div className="mt-1 flex flex-row items-center gap-1 text-sm text-muted-foreground">
{supportsAudioOutput ? ( {supportsAudioOutput ? (
<> <>
@ -1555,7 +1560,9 @@ function FrigateCameraFeatures({
rel="noopener noreferrer" rel="noopener noreferrer"
className="inline" className="inline"
> >
{t("readTheDocumentation", { ns: "common" })} {t("readTheDocumentation", {
ns: "common",
})}
<LuExternalLink className="ml-2 inline-flex size-3" /> <LuExternalLink className="ml-2 inline-flex size-3" />
</Link> </Link>
</div> </div>
@ -1599,7 +1606,9 @@ function FrigateCameraFeatures({
rel="noopener noreferrer" rel="noopener noreferrer"
className="inline" className="inline"
> >
{t("readTheDocumentation", { ns: "common" })} {t("readTheDocumentation", {
ns: "common",
})}
<LuExternalLink className="ml-2 inline-flex size-3" /> <LuExternalLink className="ml-2 inline-flex size-3" />
</Link> </Link>
</div> </div>
@ -1613,7 +1622,9 @@ function FrigateCameraFeatures({
<div className="mt-2 flex flex-col items-center gap-3"> <div className="mt-2 flex flex-col items-center gap-3">
<div className="flex flex-row items-center gap-2"> <div className="flex flex-row items-center gap-2">
<IoIosWarning className="mr-1 size-8 text-danger" /> <IoIosWarning className="mr-1 size-8 text-danger" />
<p className="text-sm">{t("stream.lowBandwidth.tips")}</p> <p className="text-sm">
{t("stream.lowBandwidth.tips")}
</p>
</div> </div>
<Button <Button
className={`flex items-center gap-2.5 rounded-lg`} className={`flex items-center gap-2.5 rounded-lg`}
@ -1651,7 +1662,8 @@ function FrigateCameraFeatures({
onClick={handleEventButtonClick} onClick={handleEventButtonClick}
className={cn( className={cn(
"h-auto w-full whitespace-normal", "h-auto w-full whitespace-normal",
isRecording && "animate-pulse bg-red-500 hover:bg-red-600", isRecording &&
"animate-pulse bg-red-500 hover:bg-red-600",
)} )}
disabled={debug} disabled={debug}
> >
@ -1700,6 +1712,8 @@ function FrigateCameraFeatures({
/> />
</div> </div>
</div> </div>
</>
</div>
</DrawerContent> </DrawerContent>
</Drawer> </Drawer>
); );

View File

@ -784,7 +784,7 @@ export default function AuthenticationView({
return ( return (
<div className="flex size-full flex-col"> <div className="flex size-full flex-col">
<Toaster position="top-center" closeButton={true} /> <Toaster position="top-center" closeButton={true} />
<div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto pb-2 md:order-none md:mr-3 md:mt-0"> <div className="scrollbar-container order-last mb-2 mt-2 flex h-full w-full flex-col overflow-y-auto pb-2 md:order-none md:mr-3 md:mt-0">
{section === "users" && UsersSection} {section === "users" && UsersSection}
{section === "roles" && RolesSection} {section === "roles" && RolesSection}
{!section && ( {!section && (

View File

@ -65,7 +65,7 @@ export default function CameraManagementView({
closeButton closeButton
/> />
<div className="flex size-full flex-col md:flex-row"> <div className="flex size-full flex-col md:flex-row">
<div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto pb-2 md:order-none"> <div className="scrollbar-container order-last mb-2 mt-2 flex h-full w-full flex-col overflow-y-auto pb-2 md:order-none">
{viewMode === "settings" ? ( {viewMode === "settings" ? (
<> <>
<Heading as="h4" className="mb-2"> <Heading as="h4" className="mb-2">

View File

@ -298,7 +298,7 @@ export default function CameraReviewSettingsView({
<> <>
<div className="flex size-full flex-col md:flex-row"> <div className="flex size-full flex-col md:flex-row">
<Toaster position="top-center" closeButton={true} /> <Toaster position="top-center" closeButton={true} />
<div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto pb-2 md:order-none"> <div className="scrollbar-container order-last mb-2 mt-2 flex h-full w-full flex-col overflow-y-auto pb-2 md:order-none">
<Heading as="h4" className="mb-2"> <Heading as="h4" className="mb-2">
{t("cameraReview.title")} {t("cameraReview.title")}
</Heading> </Heading>

View File

@ -244,7 +244,7 @@ export default function EnrichmentsSettingsView({
return ( return (
<div className="flex size-full flex-col md:flex-row"> <div className="flex size-full flex-col md:flex-row">
<Toaster position="top-center" closeButton={true} /> <Toaster position="top-center" closeButton={true} />
<div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto pb-2 md:order-none"> <div className="scrollbar-container order-last mb-2 mt-2 flex h-full w-full flex-col overflow-y-auto pb-2 md:order-none">
<Heading as="h4" className="mb-2"> <Heading as="h4" className="mb-2">
{t("enrichments.title")} {t("enrichments.title")}
</Heading> </Heading>

View File

@ -211,7 +211,7 @@ export default function FrigatePlusSettingsView({
<> <>
<div className="flex size-full flex-col md:flex-row"> <div className="flex size-full flex-col md:flex-row">
<Toaster position="top-center" closeButton={true} /> <Toaster position="top-center" closeButton={true} />
<div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto pb-2 md:order-none"> <div className="scrollbar-container order-last mb-2 mt-2 flex h-full w-full flex-col overflow-y-auto pb-2 md:order-none">
<Heading as="h4" className="mb-2"> <Heading as="h4" className="mb-2">
{t("frigatePlus.title")} {t("frigatePlus.title")}
</Heading> </Heading>

View File

@ -434,7 +434,7 @@ export default function MasksAndZonesView({
{cameraConfig && editingPolygons && ( {cameraConfig && editingPolygons && (
<div className="flex size-full flex-col md:flex-row"> <div className="flex size-full flex-col md:flex-row">
<Toaster position="top-center" closeButton={true} /> <Toaster position="top-center" closeButton={true} />
<div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto rounded-lg border-[1px] border-secondary-foreground bg-background_alt p-2 md:order-none md:mr-3 md:mt-0 md:w-3/12"> <div className="scrollbar-container order-last mb-2 mt-2 flex h-full w-full flex-col overflow-y-auto rounded-lg border-[1px] border-secondary-foreground bg-background_alt p-2 md:order-none md:mr-3 md:mt-0 md:w-3/12">
{editPane == "zone" && ( {editPane == "zone" && (
<ZoneEditPane <ZoneEditPane
polygons={editingPolygons} polygons={editingPolygons}

View File

@ -191,7 +191,7 @@ export default function MotionTunerView({
return ( return (
<div className="flex size-full flex-col md:flex-row"> <div className="flex size-full flex-col md:flex-row">
<Toaster position="top-center" closeButton={true} /> <Toaster position="top-center" closeButton={true} />
<div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto rounded-lg border-[1px] border-secondary-foreground bg-background_alt p-2 md:order-none md:mr-3 md:mt-0 md:w-3/12"> <div className="scrollbar-container order-last mb-2 mt-2 flex h-full w-full flex-col overflow-y-auto rounded-lg border-[1px] border-secondary-foreground bg-background_alt p-2 md:order-none md:mr-3 md:mt-0 md:w-3/12">
<Heading as="h4" className="mb-2"> <Heading as="h4" className="mb-2">
{t("motionDetectionTuner.title")} {t("motionDetectionTuner.title")}
</Heading> </Heading>

View File

@ -331,7 +331,7 @@ export default function NotificationView({
if (!("Notification" in window) || !window.isSecureContext) { if (!("Notification" in window) || !window.isSecureContext) {
return ( return (
<div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto pb-2 md:order-none"> <div className="scrollbar-container order-last mb-2 mt-2 flex h-full w-full flex-col overflow-y-auto pb-2 md:order-none">
<div className="grid w-full grid-cols-1 gap-4 md:grid-cols-2"> <div className="grid w-full grid-cols-1 gap-4 md:grid-cols-2">
<div className="col-span-1"> <div className="col-span-1">
<Heading as="h4" className="mb-2"> <Heading as="h4" className="mb-2">
@ -385,7 +385,7 @@ export default function NotificationView({
<> <>
<div className="flex size-full flex-col md:flex-row"> <div className="flex size-full flex-col md:flex-row">
<Toaster position="top-center" closeButton={true} /> <Toaster position="top-center" closeButton={true} />
<div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto px-2 md:order-none"> <div className="scrollbar-container order-last mb-2 mt-2 flex h-full w-full flex-col overflow-y-auto px-2 md:order-none">
<div <div
className={cn( className={cn(
isAdmin && "grid w-full grid-cols-1 gap-4 md:grid-cols-2", isAdmin && "grid w-full grid-cols-1 gap-4 md:grid-cols-2",

View File

@ -164,7 +164,7 @@ export default function ObjectSettingsView({
return ( return (
<div className="mt-1 flex size-full flex-col pb-2 md:flex-row"> <div className="mt-1 flex size-full flex-col pb-2 md:flex-row">
<Toaster position="top-center" closeButton={true} /> <Toaster position="top-center" closeButton={true} />
<div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto rounded-lg border-[1px] border-secondary-foreground bg-background_alt p-2 md:order-none md:mb-0 md:mr-2 md:mt-0 md:w-3/12"> <div className="scrollbar-container order-last mb-2 mt-2 flex h-full w-full flex-col overflow-y-auto rounded-lg border-[1px] border-secondary-foreground bg-background_alt p-2 md:order-none md:mb-0 md:mr-2 md:mt-0 md:w-3/12">
<Heading as="h4" className="mb-2"> <Heading as="h4" className="mb-2">
{t("debug.title")} {t("debug.title")}
</Heading> </Heading>
@ -434,7 +434,7 @@ function ObjectList({ cameraConfig, objects }: ObjectListProps) {
{t("debug.objectShapeFilterDrawing.area")} {t("debug.objectShapeFilterDrawing.area")}
</p> </p>
{obj.area ? ( {obj.area ? (
<> <div className="text-end">
<div className="text-xs"> <div className="text-xs">
px: {obj.area.toString()} px: {obj.area.toString()}
</div> </div>
@ -448,7 +448,7 @@ function ObjectList({ cameraConfig, objects }: ObjectListProps) {
.toFixed(4) .toFixed(4)
.toString()} .toString()}
</div> </div>
</> </div>
) : ( ) : (
"-" "-"
)} )}

View File

@ -440,7 +440,7 @@ export default function TriggerView({
return ( return (
<div className="flex size-full flex-col md:flex-row"> <div className="flex size-full flex-col md:flex-row">
<Toaster position="top-center" closeButton={true} /> <Toaster position="top-center" closeButton={true} />
<div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto pb-2 md:order-none md:mr-3 md:mt-0"> <div className="scrollbar-container order-last mb-2 mt-2 flex h-full w-full flex-col overflow-y-auto pb-2 md:order-none md:mr-3 md:mt-0">
{!isSemanticSearchEnabled ? ( {!isSemanticSearchEnabled ? (
<div className="mb-5 flex flex-row items-center justify-between gap-2"> <div className="mb-5 flex flex-row items-center justify-between gap-2">
<div className="flex flex-col items-start"> <div className="flex flex-col items-start">

View File

@ -108,7 +108,7 @@ export default function UiSettingsView() {
<> <>
<div className="flex size-full flex-col md:flex-row"> <div className="flex size-full flex-col md:flex-row">
<Toaster position="top-center" closeButton={true} /> <Toaster position="top-center" closeButton={true} />
<div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto pb-2 md:order-none"> <div className="scrollbar-container order-last mb-2 mt-2 flex h-full w-full flex-col overflow-y-auto pb-2 md:order-none">
<Heading as="h4" className="mb-2"> <Heading as="h4" className="mb-2">
{t("general.title")} {t("general.title")}
</Heading> </Heading>