Compare commits

..

5 Commits

Author SHA1 Message Date
Josh Hawkins
2c893aa125 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
2025-11-24 06:45:09 -06:00
Josh Hawkins
42fdadecd9 clarify audio transcription docs 2025-11-23 12:35:35 -06:00
Josh Hawkins
bf4f63e50e make live view settings drawer scrollable 2025-11-23 12:25:23 -06:00
Josh Hawkins
9242997079 spacing tweaks 2025-11-23 12:25:00 -06:00
Josh Hawkins
c5e08c7bcf fix wording in reference config 2025-11-23 12:22:27 -06:00
14 changed files with 97 additions and 210 deletions

View File

@ -15,7 +15,7 @@ ARG AMDGPU
RUN apt update -qq && \ RUN apt update -qq && \
apt install -y wget gpg && \ apt install -y wget gpg && \
wget -O rocm.deb https://repo.radeon.com/amdgpu-install/7.1/ubuntu/jammy/amdgpu-install_7.1.70100-1_all.deb && \ wget -O rocm.deb https://repo.radeon.com/amdgpu-install/7.0.2/ubuntu/jammy/amdgpu-install_7.0.2.70002-1_all.deb && \
apt install -y ./rocm.deb && \ apt install -y ./rocm.deb && \
apt update && \ apt update && \
apt install -qq -y rocm apt install -qq -y rocm

View File

@ -1 +1 @@
onnxruntime-migraphx @ https://github.com/NickM-27/frigate-onnxruntime-rocm/releases/download/v7.1.0/onnxruntime_migraphx-1.23.1-cp311-cp311-linux_x86_64.whl onnxruntime-migraphx @ https://github.com/NickM-27/frigate-onnxruntime-rocm/releases/download/v7.0.2/onnxruntime_migraphx-1.23.1-cp311-cp311-linux_x86_64.whl

View File

@ -2,7 +2,7 @@ variable "AMDGPU" {
default = "gfx900" default = "gfx900"
} }
variable "ROCM" { variable "ROCM" {
default = "7.1" default = "7.0.2"
} }
variable "HSA_OVERRIDE_GFX_VERSION" { variable "HSA_OVERRIDE_GFX_VERSION" {
default = "" default = ""

View File

@ -232,7 +232,7 @@ When your browser runs into problems playing back your camera streams, it will l
- **mse-decode** - **mse-decode**
- What it means: The browser reported a decoding error while trying to play the stream, which usually is a result of a codec incompatibility or corrupted frames. - What it means: The browser reported a decoding error while trying to play the stream, which usually is a result of a codec incompatibility or corrupted frames.
- What to try: Check the browser console for the supported and negotiated codecs. Ensure your camera/restream is using H.264 video and AAC audio (these are the most compatible). If your camera uses a non-standard audio codec, configure `go2rtc` to transcode the stream to AAC. Try another browser (some browsers have stricter MSE/codec support) and, for iPhone, ensure you're on iOS 17.1 or newer. - What to try: Ensure your camera/restream is using H.264 video and AAC audio (these are the most compatible). If your camera uses a non-standard audio codec, configure `go2rtc` to transcode the stream to AAC. Try another browser (some browsers have stricter MSE/codec support) and, for iPhone, ensure you're on iOS 17.1 or newer.
- Possible console messages from the player code: - Possible console messages from the player code:

View File

@ -1753,7 +1753,7 @@ def create_trigger_embedding(
body.data, (base64.b64encode(thumbnail).decode("ASCII")) body.data, (base64.b64encode(thumbnail).decode("ASCII"))
) )
if not embedding: if embedding is None:
return JSONResponse( return JSONResponse(
content={ content={
"success": False, "success": False,
@ -1888,7 +1888,7 @@ def update_trigger_embedding(
body.data, (base64.b64encode(thumbnail).decode("ASCII")) body.data, (base64.b64encode(thumbnail).decode("ASCII"))
) )
if not embedding: if embedding is None:
return JSONResponse( return JSONResponse(
content={ content={
"success": False, "success": False,

View File

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

View File

@ -61,8 +61,7 @@
"header": { "header": {
"zones": "Zones", "zones": "Zones",
"ratio": "Ratio", "ratio": "Ratio",
"area": "Area", "area": "Area"
"score": "Score"
} }
}, },
"annotationSettings": { "annotationSettings": {

View File

@ -314,10 +314,11 @@ function GeneralFilterButton({
<PlatformAwareDialog <PlatformAwareDialog
trigger={trigger} trigger={trigger}
content={content} content={content}
contentClassName={cn( contentClassName={
"scrollbar-container h-auto overflow-y-auto", isDesktop
isDesktop ? "max-h-[80dvh]" : "px-4", ? "scrollbar-container h-auto max-h-[80dvh] overflow-y-auto"
)} : "max-h-[75dvh] overflow-hidden p-4"
}
open={open} open={open}
onOpenChange={(open) => { onOpenChange={(open) => {
if (!open) { if (!open) {
@ -509,10 +510,11 @@ function SortTypeButton({
<PlatformAwareDialog <PlatformAwareDialog
trigger={trigger} trigger={trigger}
content={content} content={content}
contentClassName={cn( contentClassName={
"scrollbar-container h-auto overflow-y-auto", isDesktop
isDesktop ? "max-h-[80dvh]" : "px-4", ? "scrollbar-container h-auto max-h-[80dvh] overflow-y-auto"
)} : "max-h-[75dvh] overflow-hidden p-4"
}
open={open} open={open}
onOpenChange={(open) => { onOpenChange={(open) => {
if (!open) { if (!open) {

View File

@ -42,7 +42,6 @@ type ObjectData = {
pathPoints: PathPoint[]; pathPoints: PathPoint[];
currentZones: string[]; currentZones: string[];
currentBox?: number[]; currentBox?: number[];
currentAttributeBox?: number[];
}; };
export default function ObjectTrackOverlay({ export default function ObjectTrackOverlay({
@ -106,12 +105,6 @@ 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
@ -119,12 +112,7 @@ 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,
null, { revalidateOnFocus: false },
{
revalidateOnFocus: false,
revalidateOnReconnect: false,
dedupingInterval: 30000,
},
); );
const getZonesFriendlyNames = (zones: string[], config: FrigateConfig) => { const getZonesFriendlyNames = (zones: string[], config: FrigateConfig) => {
@ -282,7 +270,6 @@ export default function ObjectTrackOverlay({
); );
const currentBox = nearbyTimelineEvent?.data?.box; const currentBox = nearbyTimelineEvent?.data?.box;
const currentAttributeBox = nearbyTimelineEvent?.data?.attribute_box;
return { return {
objectId, objectId,
@ -291,7 +278,6 @@ 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
@ -496,20 +482,6 @@ 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

@ -75,15 +75,12 @@ export function TrackingDetails({
setIsVideoLoading(true); setIsVideoLoading(true);
}, [event.id]); }, [event.id]);
const { data: eventSequence } = useSWR<TrackingDetailsSequence[]>( const { data: eventSequence } = useSWR<TrackingDetailsSequence[]>([
["timeline", { source_id: event.id }], "timeline",
null,
{ {
revalidateOnFocus: false, source_id: event.id,
revalidateOnReconnect: false,
dedupingInterval: 30000,
}, },
); ]);
const { data: config } = useSWR<FrigateConfig>("config"); const { data: config } = useSWR<FrigateConfig>("config");
@ -107,12 +104,6 @@ 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
@ -723,6 +714,53 @@ 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}`}
@ -732,7 +770,11 @@ export function TrackingDetails({
> >
<LifecycleIconRow <LifecycleIconRow
item={item} item={item}
event={event} isActive={isActive}
formattedEventTimestamp={formattedEventTimestamp}
ratio={ratio}
areaPx={areaPx}
areaPct={areaPct}
onClick={() => handleLifecycleClick(item)} onClick={() => handleLifecycleClick(item)}
setSelectedZone={setSelectedZone} setSelectedZone={setSelectedZone}
getZoneColor={getZoneColor} getZoneColor={getZoneColor}
@ -756,7 +798,11 @@ export function TrackingDetails({
type LifecycleIconRowProps = { type LifecycleIconRowProps = {
item: TrackingDetailsSequence; item: TrackingDetailsSequence;
event: Event; isActive?: boolean;
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;
@ -766,7 +812,11 @@ type LifecycleIconRowProps = {
function LifecycleIconRow({ function LifecycleIconRow({
item, item,
event, isActive,
formattedEventTimestamp,
ratio,
areaPx,
areaPct,
onClick, onClick,
setSelectedZone, setSelectedZone,
getZoneColor, getZoneColor,
@ -776,101 +826,9 @@ 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"
@ -898,28 +856,16 @@ 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="my-2 ml-2 flex flex-col flex-wrap items-start gap-1.5 text-xs text-secondary-foreground"> <div className="mt-1 flex flex-wrap items-center gap-2 text-xs text-secondary-foreground md:gap-5">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1">
<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.5"> <div className="flex items-center gap-1">
<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">
@ -930,25 +876,9 @@ 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="mt-1 flex flex-wrap items-center gap-2"> <div className="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

@ -44,8 +44,8 @@ export default function PlatformAwareDialog({
return ( return (
<Drawer open={open} onOpenChange={onOpenChange}> <Drawer open={open} onOpenChange={onOpenChange}>
<DrawerTrigger asChild>{trigger}</DrawerTrigger> <DrawerTrigger asChild>{trigger}</DrawerTrigger>
<DrawerContent className="max-h-[75dvh] overflow-hidden"> <DrawerContent className="max-h-[75dvh] overflow-hidden px-4">
<div className={contentClassName}>{content}</div> {content}
</DrawerContent> </DrawerContent>
</Drawer> </Drawer>
); );

View File

@ -82,7 +82,6 @@ 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}`;
@ -94,19 +93,8 @@ 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} - Browser negotiated codecs: ${mseCodecRef.current}`,
);
// eslint-disable-next-line no-console
console.error(`${camera} - Supported codecs: ${CODECS.join(", ")}`);
}
onError?.(error); onError?.(error);
}, },
// we know that these deps are correct
// eslint-disable-next-line react-hooks/exhaustive-deps
[camera, onError], [camera, onError],
); );
@ -311,9 +299,6 @@ 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

@ -136,10 +136,11 @@ export default function ExploreSettings({
<PlatformAwareDialog <PlatformAwareDialog
trigger={trigger} trigger={trigger}
content={content} content={content}
contentClassName={cn( contentClassName={
"scrollbar-container h-auto overflow-y-auto", isDesktop
isDesktop ? "max-h-[80dvh]" : "px-4", ? "scrollbar-container h-auto max-h-[80dvh] overflow-y-auto"
)} : "max-h-[75dvh] overflow-hidden p-4"
}
open={open} open={open}
onOpenChange={(open) => { onOpenChange={(open) => {
setOpen(open); setOpen(open);

View File

@ -16,7 +16,6 @@ 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];