Compare commits

..

6 Commits

Author SHA1 Message Date
Josh Hawkins
d43473867b show negotiated mse codecs in console on error 2025-11-24 09:04:00 -06:00
Josh Hawkins
1950f367f0 add i18n key 2025-11-24 08:46:27 -06:00
Josh Hawkins
4850c04c0c move scrollbar to edge on platform aware dialog drawers 2025-11-24 08:46:16 -06:00
Josh Hawkins
7c3e093fcc catch failed image embedding in triggers 2025-11-24 08:07:23 -06:00
Nicolas Mowen
2a9c028f55
Update ROCm to 7.1.0 (#21032)
Some checks are pending
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
* Update ROCm to 7.1.0

* Change to be consistent
2025-11-24 07:45:00 -06:00
Josh Hawkins
aa8b423b68
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
2025-11-24 06:34:56 -07:00
14 changed files with 210 additions and 97 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.0.2/ubuntu/jammy/amdgpu-install_7.0.2.70002-1_all.deb && \ wget -O rocm.deb https://repo.radeon.com/amdgpu-install/7.1/ubuntu/jammy/amdgpu-install_7.1.70100-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.0.2/onnxruntime_migraphx-1.23.1-cp311-cp311-linux_x86_64.whl 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

View File

@ -2,7 +2,7 @@ variable "AMDGPU" {
default = "gfx900" default = "gfx900"
} }
variable "ROCM" { variable "ROCM" {
default = "7.0.2" default = "7.1"
} }
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: 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: 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.
- 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 embedding is None: if not embedding:
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 embedding is None: if not embedding:
return JSONResponse( return JSONResponse(
content={ content={
"success": False, "success": False,

View File

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

View File

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

View File

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

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

@ -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

@ -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 px-4"> <DrawerContent className="max-h-[75dvh] overflow-hidden">
{content} <div className={contentClassName}>{content}</div>
</DrawerContent> </DrawerContent>
</Drawer> </Drawer>
); );

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,8 +94,19 @@ 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],
); );
@ -299,6 +311,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

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

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];