* use ffmpeg to probe rtsp urls instead of cv2

cv2 is faster (no subprocess launch) and will continue to be used for recording segments

* tweak faq

* change unsaved color to orange

avoids confusion with validation errors (red)

* don't use any variant of orange as a profile color

avoids confusion with unsaved changes

* more unsaved color tweaks
This commit is contained in:
Josh Hawkins 2026-04-22 10:19:30 -05:00 committed by GitHub
parent 20705a3e97
commit ad9092d0da
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 51 additions and 50 deletions

View File

@ -111,26 +111,16 @@ TCP ensures that all data packets arrive in the correct order. This is crucial f
You can still configure Frigate to use UDP by using ffmpeg input args or the preset `preset-rtsp-udp`. See the [ffmpeg presets](/configuration/ffmpeg_presets) documentation. You can still configure Frigate to use UDP by using ffmpeg input args or the preset `preset-rtsp-udp`. See the [ffmpeg presets](/configuration/ffmpeg_presets) documentation.
### Frigate hangs on startup with a "probing detect stream" message in the logs ### Frigate is slow to start up with a "probing detect stream" message in the logs
On startup, Frigate probes each camera's detect stream with OpenCV to auto-detect its resolution. OpenCV's FFmpeg backend may attempt RTSP over UDP during this probe regardless of the `-rtsp_transport tcp` in your `input_args` or preset. For cameras that do not respond to UDP (common on some Reolink models and others behind firewalls that block UDP), the probe can hang indefinitely and block Frigate from finishing startup, or it can return zeroed-out dimensions that show up as width `0` and height `0` in Camera Probe Info under System Metrics. When `detect.width` and `detect.height` are not set, Frigate probes each camera's detect stream on startup (and when saving the config) to auto-detect its resolution. For RTSP streams Frigate probes with ffprobe and automatically retries over TCP if UDP doesn't respond, with a 5 second timeout per attempt. A camera that cannot be reached over either transport will add up to ~10 seconds to startup before Frigate falls through with default dimensions, which may show up as width `0` and height `0` in Camera Probe Info under System Metrics.
There are two ways to avoid this: To skip the probe entirely and make startup instant, set `detect.width` and `detect.height` explicitly in your camera config:
1. Set `detect.width` and `detect.height` explicitly in your camera config. When both are set, Frigate skips the auto-detect probe entirely: ```yaml
cameras:
```yaml my_camera:
cameras: detect:
my_camera: width: 1280
detect: height: 720
width: 1280 ```
height: 720
```
2. Force OpenCV's FFmpeg backend to use TCP for RTSP by setting the environment variable on your Frigate container:
```
OPENCV_FFMPEG_CAPTURE_OPTIONS=rtsp_transport;tcp
```
This is a process-wide setting and applies to all cameras. If you have any cameras that require `preset-rtsp-udp`, use option 1 instead.

View File

@ -877,15 +877,23 @@ async def get_video_properties(
cap.release() cap.release()
return valid, width, height, fourcc, duration return valid, width, height, fourcc, duration
# try cv2 first is_rtsp = url.startswith("rtsp://")
has_video, width, height, fourcc, duration = probe_with_cv2(url)
# fallback to ffprobe if needed if is_rtsp:
if not has_video or (get_duration and duration < 0): # skip cv2 for RTSP: its FFmpeg backend has a hardcoded ~30s internal
# timeout that cannot be shortened per-call, and ffprobe bounded by
# -rw_timeout handles RTSP probing reliably
has_video, width, height, fourcc, duration = await probe_with_ffprobe(url) has_video, width, height, fourcc, duration = await probe_with_ffprobe(url)
else:
# try cv2 first for local files, HTTP, RTMP
has_video, width, height, fourcc, duration = probe_with_cv2(url)
# fallback to ffprobe if needed
if not has_video or (get_duration and duration < 0):
has_video, width, height, fourcc, duration = await probe_with_ffprobe(url)
# last resort for RTSP: try TCP transport, since default UDP may be blocked # last resort for RTSP: try TCP transport, since default UDP may be blocked
if (not has_video or (get_duration and duration < 0)) and url.startswith("rtsp://"): if (not has_video or (get_duration and duration < 0)) and is_rtsp:
has_video, width, height, fourcc, duration = await probe_with_ffprobe( has_video, width, height, fourcc, duration = await probe_with_ffprobe(
url, rtsp_transport="tcp" url, rtsp_transport="tcp"
) )

View File

@ -218,7 +218,7 @@ export default function CameraReviewClassification({
<Label <Label
className={cn( className={cn(
"flex flex-row items-center text-base", "flex flex-row items-center text-base",
alertsZonesModified && "text-danger", alertsZonesModified && "text-unsaved",
)} )}
> >
<Trans ns="views/settings">cameraReview.review.alerts</Trans> <Trans ns="views/settings">cameraReview.review.alerts</Trans>
@ -286,7 +286,7 @@ export default function CameraReviewClassification({
<Label <Label
className={cn( className={cn(
"flex flex-row items-center text-base", "flex flex-row items-center text-base",
detectionsZonesModified && "text-danger", detectionsZonesModified && "text-unsaved",
)} )}
> >
<Trans ns="views/settings"> <Trans ns="views/settings">

View File

@ -1012,7 +1012,7 @@ export function ConfigSection({
> >
{hasChanges && ( {hasChanges && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-sm text-danger"> <span className="text-sm text-unsaved">
{t("unsavedChanges", { {t("unsavedChanges", {
ns: "views/settings", ns: "views/settings",
defaultValue: "You have unsaved changes", defaultValue: "You have unsaved changes",
@ -1299,7 +1299,7 @@ export function ConfigSection({
{hasChanges && ( {hasChanges && (
<Badge <Badge
variant="secondary" variant="secondary"
className="cursor-default bg-danger text-xs text-white hover:bg-danger" className="cursor-default bg-unsaved text-xs text-black hover:bg-unsaved"
> >
{t("button.modified", { {t("button.modified", {
ns: "common", ns: "common",

View File

@ -154,7 +154,7 @@ export function KnownPlatesField(props: FieldProps) {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<CardTitle <CardTitle
className={cn("text-sm", isModified && "text-danger")} className={cn("text-sm", isModified && "text-unsaved")}
> >
{title} {title}
</CardTitle> </CardTitle>

View File

@ -142,7 +142,7 @@ export function ReplaceRulesField(props: FieldProps) {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<CardTitle <CardTitle
className={cn("text-sm", isModified && "text-danger")} className={cn("text-sm", isModified && "text-unsaved")}
> >
{title} {title}
</CardTitle> </CardTitle>

View File

@ -497,7 +497,7 @@ export function FieldTemplate(props: FieldTemplateProps) {
htmlFor={id} htmlFor={id}
className={cn( className={cn(
"text-sm font-medium", "text-sm font-medium",
isModified && "text-danger", isModified && "text-unsaved",
hasFieldErrors && "text-destructive", hasFieldErrors && "text-destructive",
)} )}
> >
@ -516,7 +516,7 @@ export function FieldTemplate(props: FieldTemplateProps) {
return ( return (
<Label <Label
htmlFor={id} htmlFor={id}
className={cn("text-sm font-medium", isModified && "text-danger")} className={cn("text-sm font-medium", isModified && "text-unsaved")}
> >
{finalLabel} {finalLabel}
{required && <span className="ml-1 text-destructive">*</span>} {required && <span className="ml-1 text-destructive">*</span>}
@ -535,7 +535,7 @@ export function FieldTemplate(props: FieldTemplateProps) {
htmlFor={id} htmlFor={id}
className={cn( className={cn(
"text-sm font-medium", "text-sm font-medium",
isModified && "text-danger", isModified && "text-unsaved",
hasFieldErrors && "text-destructive", hasFieldErrors && "text-destructive",
)} )}
> >

View File

@ -467,7 +467,7 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
<CardTitle <CardTitle
className={cn( className={cn(
"flex items-center text-sm", "flex items-center text-sm",
hasModifiedDescendants && "text-danger", hasModifiedDescendants && "text-unsaved",
)} )}
> >
{inferredLabel} {inferredLabel}

View File

@ -1435,7 +1435,7 @@ export default function Settings() {
/> />
)} )}
{showUnsavedDot && ( {showUnsavedDot && (
<span className="inline-block size-2 rounded-full bg-danger" /> <span className="inline-block size-2 rounded-full bg-unsaved" />
)} )}
</div> </div>
)} )}
@ -1516,7 +1516,7 @@ export default function Settings() {
<div className="sticky bottom-0 z-50 mt-2 bg-background p-4"> <div className="sticky bottom-0 z-50 mt-2 bg-background p-4">
<div className="flex flex-col items-center gap-2"> <div className="flex flex-col items-center gap-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-sm text-danger"> <span className="text-sm text-unsaved">
{t("unsavedChanges", { {t("unsavedChanges", {
ns: "views/settings", ns: "views/settings",
defaultValue: "You have unsaved changes", defaultValue: "You have unsaved changes",

View File

@ -79,11 +79,11 @@ const PROFILE_COLORS: ProfileColor[] = [
bgMuted: "bg-green-400/20", bgMuted: "bg-green-400/20",
}, },
{ {
bg: "bg-amber-400", bg: "bg-fuchsia-500",
text: "text-amber-400", text: "text-fuchsia-500",
dot: "bg-amber-400", dot: "bg-fuchsia-500",
border: "border-amber-400", border: "border-fuchsia-500",
bgMuted: "bg-amber-400/20", bgMuted: "bg-fuchsia-500/20",
}, },
{ {
bg: "bg-slate-400", bg: "bg-slate-400",
@ -93,11 +93,11 @@ const PROFILE_COLORS: ProfileColor[] = [
bgMuted: "bg-slate-400/20", bgMuted: "bg-slate-400/20",
}, },
{ {
bg: "bg-orange-300", bg: "bg-stone-500",
text: "text-orange-300", text: "text-stone-500",
dot: "bg-orange-300", dot: "bg-stone-500",
border: "border-orange-300", border: "border-stone-500",
bgMuted: "bg-orange-300/20", bgMuted: "bg-stone-500/20",
}, },
{ {
bg: "bg-blue-300", bg: "bg-blue-300",

View File

@ -380,7 +380,9 @@ export default function Go2RtcStreamsSettingsView({
> >
{hasChanges && ( {hasChanges && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-sm text-danger">{t("unsavedChanges")}</span> <span className="text-sm text-unsaved">
{t("unsavedChanges")}
</span>
</div> </div>
)} )}
<div className="flex w-full items-center gap-2 md:w-auto"> <div className="flex w-full items-center gap-2 md:w-auto">

View File

@ -212,7 +212,7 @@ export function SingleSectionPage({
{sectionStatus.hasChanges && ( {sectionStatus.hasChanges && (
<Badge <Badge
variant="secondary" variant="secondary"
className="cursor-default bg-danger text-xs text-white hover:bg-danger" className="cursor-default bg-unsaved text-xs text-black hover:bg-unsaved"
> >
{t("button.modified", { {t("button.modified", {
ns: "common", ns: "common",
@ -250,7 +250,7 @@ export function SingleSectionPage({
{sectionStatus.hasChanges && ( {sectionStatus.hasChanges && (
<Badge <Badge
variant="secondary" variant="secondary"
className="cursor-default bg-danger text-xs text-white hover:bg-danger" className="cursor-default bg-unsaved text-xs text-black hover:bg-unsaved"
> >
{t("button.modified", { ns: "common", defaultValue: "Modified" })} {t("button.modified", { ns: "common", defaultValue: "Modified" })}
</Badge> </Badge>

View File

@ -65,6 +65,7 @@ module.exports = {
ring: "hsl(var(--ring))", ring: "hsl(var(--ring))",
danger: "#ef4444", danger: "#ef4444",
success: "#22c55e", success: "#22c55e",
unsaved: "#f59e0b",
background: "hsl(var(--background))", background: "hsl(var(--background))",
background_alt: "hsl(var(--background-alt))", background_alt: "hsl(var(--background-alt))",
foreground: "hsl(var(--foreground))", foreground: "hsl(var(--foreground))",