diff --git a/docker/main/Dockerfile b/docker/main/Dockerfile index 7a0351240..2a7d388bc 100644 --- a/docker/main/Dockerfile +++ b/docker/main/Dockerfile @@ -55,7 +55,7 @@ RUN --mount=type=tmpfs,target=/tmp --mount=type=tmpfs,target=/var/cache/apt \ FROM scratch AS go2rtc ARG TARGETARCH WORKDIR /rootfs/usr/local/go2rtc/bin -ADD --link --chmod=755 "https://github.com/AlexxIT/go2rtc/releases/download/v1.9.2/go2rtc_linux_${TARGETARCH}" go2rtc +ADD --link --chmod=755 "https://github.com/AlexxIT/go2rtc/releases/download/v1.9.9/go2rtc_linux_${TARGETARCH}" go2rtc FROM wget AS tempio ARG TARGETARCH diff --git a/docker/main/rootfs/usr/local/go2rtc/create_config.py b/docker/main/rootfs/usr/local/go2rtc/create_config.py index 4fe26775e..d7c21c7f7 100644 --- a/docker/main/rootfs/usr/local/go2rtc/create_config.py +++ b/docker/main/rootfs/usr/local/go2rtc/create_config.py @@ -69,10 +69,6 @@ elif go2rtc_config["log"].get("format") is None: if go2rtc_config.get("webrtc") is None: go2rtc_config["webrtc"] = {} -# go2rtc should listen on 8555 tcp & udp by default -if go2rtc_config["webrtc"].get("listen") is None: - go2rtc_config["webrtc"]["listen"] = ":8555" - if go2rtc_config["webrtc"].get("candidates") is None: default_candidates = [] # use internal candidate if it was discovered when running through the add-on @@ -84,33 +80,15 @@ if go2rtc_config["webrtc"].get("candidates") is None: go2rtc_config["webrtc"]["candidates"] = default_candidates -# This prevents WebRTC from attempting to establish a connection to the internal -# docker IPs which are not accessible from outside the container itself and just -# wastes time during negotiation. Note that this is only necessary because -# Frigate container doesn't run in host network mode. -if go2rtc_config["webrtc"].get("filter") is None: - go2rtc_config["webrtc"]["filter"] = {"candidates": []} -elif go2rtc_config["webrtc"]["filter"].get("candidates") is None: - go2rtc_config["webrtc"]["filter"]["candidates"] = [] +if go2rtc_config.get("rtsp", {}).get("username") is not None: + go2rtc_config["rtsp"]["username"] = go2rtc_config["rtsp"]["username"].format( + **FRIGATE_ENV_VARS + ) -# sets default RTSP response to be equivalent to ?video=h264,h265&audio=aac -# this means user does not need to specify audio codec when using restream -# as source for frigate and the integration supports HLS playback -if go2rtc_config.get("rtsp") is None: - go2rtc_config["rtsp"] = {"default_query": "mp4"} -else: - if go2rtc_config["rtsp"].get("default_query") is None: - go2rtc_config["rtsp"]["default_query"] = "mp4" - - if go2rtc_config["rtsp"].get("username") is not None: - go2rtc_config["rtsp"]["username"] = go2rtc_config["rtsp"]["username"].format( - **FRIGATE_ENV_VARS - ) - - if go2rtc_config["rtsp"].get("password") is not None: - go2rtc_config["rtsp"]["password"] = go2rtc_config["rtsp"]["password"].format( - **FRIGATE_ENV_VARS - ) +if go2rtc_config.get("rtsp", {}).get("password") is not None: + go2rtc_config["rtsp"]["password"] = go2rtc_config["rtsp"]["password"].format( + **FRIGATE_ENV_VARS + ) # ensure ffmpeg path is set correctly path = config.get("ffmpeg", {}).get("path", "default") diff --git a/docs/docs/configuration/advanced.md b/docs/docs/configuration/advanced.md index b037c0768..c889d2d26 100644 --- a/docs/docs/configuration/advanced.md +++ b/docs/docs/configuration/advanced.md @@ -186,7 +186,7 @@ To do this: ### Custom go2rtc version -Frigate currently includes go2rtc v1.9.2, there may be certain cases where you want to run a different version of go2rtc. +Frigate currently includes go2rtc v1.9.9, there may be certain cases where you want to run a different version of go2rtc. To do this: diff --git a/docs/docs/configuration/camera_specific.md b/docs/docs/configuration/camera_specific.md index fb4ce5714..0cd1efd47 100644 --- a/docs/docs/configuration/camera_specific.md +++ b/docs/docs/configuration/camera_specific.md @@ -219,7 +219,7 @@ go2rtc: - rtspx://192.168.1.1:7441/abcdefghijk ``` -[See the go2rtc docs for more information](https://github.com/AlexxIT/go2rtc/tree/v1.9.2#source-rtsp) +[See the go2rtc docs for more information](https://github.com/AlexxIT/go2rtc/tree/v1.9.9#source-rtsp) In the Unifi 2.0 update Unifi Protect Cameras had a change in audio sample rate which causes issues for ffmpeg. The input rate needs to be set for record if used directly with unifi protect. diff --git a/docs/docs/configuration/license_plate_recognition.md b/docs/docs/configuration/license_plate_recognition.md index 3fe1ee852..776f30cf9 100644 --- a/docs/docs/configuration/license_plate_recognition.md +++ b/docs/docs/configuration/license_plate_recognition.md @@ -3,16 +3,16 @@ id: license_plate_recognition title: License Plate Recognition (LPR) --- -Frigate can recognize license plates on vehicles and automatically add the detected characters or recognized name as a `sub_label` to objects that are of type `car`. A common use case may be to read the license plates of cars pulling into a driveway or cars passing by on a street. +Frigate can recognize license plates on vehicles and automatically add the detected characters to the `recognized_license_plate` field or a known name as a `sub_label` to objects that are of type `car`. A common use case may be to read the license plates of cars pulling into a driveway or cars passing by on a street. LPR works best when the license plate is clearly visible to the camera. For moving vehicles, Frigate continuously refines the recognition process, keeping the most confident result. However, LPR does not run on stationary vehicles. -When a plate is recognized, the detected characters or recognized name is: +When a plate is recognized, the recognized name is: -- Added as a `sub_label` to the `car` tracked object. +- Added to the `car` tracked object as a `sub_label` (if known) or the `recognized_license_plate` field (if unknown) - Viewable in the Review Item Details pane in Review and the Tracked Object Details pane in Explore. - Filterable through the More Filters menu in Explore. -- Published via the `frigate/events` MQTT topic as a `sub_label` for the tracked object. +- Published via the `frigate/events` MQTT topic as a `sub_label` (known) or `recognized_license_plate` (unknown) for the tracked object. ## Model Requirements @@ -71,6 +71,7 @@ Fine-tune the LPR feature using these optional parameters: - **`known_plates`**: List of strings or regular expressions that assign custom a `sub_label` to `car` objects when a recognized plate matches a known value. - These labels appear in the UI, filters, and notifications. + - Unknown plates are still saved but are added to the `recognized_license_plate` field rather than the `sub_label`. - **`match_distance`**: Allows for minor variations (missing/incorrect characters) when matching a detected plate to a known plate. - For example, setting `match_distance: 1` allows a plate `ABCDE` to match `ABCBE` or `ABCD`. - This parameter will _not_ operate on known plates that are defined as regular expressions. You should define the full string of your plate in `known_plates` in order to use `match_distance`. diff --git a/docs/docs/configuration/reference.md b/docs/docs/configuration/reference.md index 37884259a..9a880aade 100644 --- a/docs/docs/configuration/reference.md +++ b/docs/docs/configuration/reference.md @@ -591,7 +591,7 @@ genai: person: "My special person prompt." # Optional: Restream configuration -# Uses https://github.com/AlexxIT/go2rtc (v1.9.2) +# Uses https://github.com/AlexxIT/go2rtc (v1.9.9) # NOTE: The default go2rtc API port (1984) must be used, # changing this port for the integrated go2rtc instance is not supported. go2rtc: diff --git a/docs/docs/configuration/restream.md b/docs/docs/configuration/restream.md index 0db4ded80..288dc8fd9 100644 --- a/docs/docs/configuration/restream.md +++ b/docs/docs/configuration/restream.md @@ -7,7 +7,7 @@ title: Restream Frigate can restream your video feed as an RTSP feed for other applications such as Home Assistant to utilize it at `rtsp://:8554/`. Port 8554 must be open. [This allows you to use a video feed for detection in Frigate and Home Assistant live view at the same time without having to make two separate connections to the camera](#reduce-connections-to-camera). The video feed is copied from the original video feed directly to avoid re-encoding. This feed does not include any annotation by Frigate. -Frigate uses [go2rtc](https://github.com/AlexxIT/go2rtc/tree/v1.9.2) to provide its restream and MSE/WebRTC capabilities. The go2rtc config is hosted at the `go2rtc` in the config, see [go2rtc docs](https://github.com/AlexxIT/go2rtc/tree/v1.9.2#configuration) for more advanced configurations and features. +Frigate uses [go2rtc](https://github.com/AlexxIT/go2rtc/tree/v1.9.9) to provide its restream and MSE/WebRTC capabilities. The go2rtc config is hosted at the `go2rtc` in the config, see [go2rtc docs](https://github.com/AlexxIT/go2rtc/tree/v1.9.9#configuration) for more advanced configurations and features. :::note @@ -134,7 +134,7 @@ cameras: ## Handling Complex Passwords -go2rtc expects URL-encoded passwords in the config, [urlencoder.org](https://urlencoder.org) can be used for this purpose. +go2rtc expects URL-encoded passwords in the config, [urlencoder.org](https://urlencoder.org) can be used for this purpose. For example: @@ -156,7 +156,7 @@ See [this comment(https://github.com/AlexxIT/go2rtc/issues/1217#issuecomment-224 ## Advanced Restream Configurations -The [exec](https://github.com/AlexxIT/go2rtc/tree/v1.9.2#source-exec) source in go2rtc can be used for custom ffmpeg commands. An example is below: +The [exec](https://github.com/AlexxIT/go2rtc/tree/v1.9.9#source-exec) source in go2rtc can be used for custom ffmpeg commands. An example is below: NOTE: The output will need to be passed with two curly braces `{{output}}` diff --git a/docs/docs/guides/configuring_go2rtc.md b/docs/docs/guides/configuring_go2rtc.md index 1a61fd0c5..3b8a8af1f 100644 --- a/docs/docs/guides/configuring_go2rtc.md +++ b/docs/docs/guides/configuring_go2rtc.md @@ -13,7 +13,7 @@ Use of the bundled go2rtc is optional. You can still configure FFmpeg to connect # Setup a go2rtc stream -First, you will want to configure go2rtc to connect to your camera stream by adding the stream you want to use for live view in your Frigate config file. Avoid changing any other parts of your config at this step. Note that go2rtc supports [many different stream types](https://github.com/AlexxIT/go2rtc/tree/v1.9.2#module-streams), not just rtsp. +First, you will want to configure go2rtc to connect to your camera stream by adding the stream you want to use for live view in your Frigate config file. Avoid changing any other parts of your config at this step. Note that go2rtc supports [many different stream types](https://github.com/AlexxIT/go2rtc/tree/v1.9.9#module-streams), not just rtsp. :::tip @@ -32,69 +32,74 @@ go2rtc: After adding this to the config, restart Frigate and try to watch the live stream for a single camera by clicking on it from the dashboard. It should look much clearer and more fluent than the original jsmpeg stream. - ### What if my video doesn't play? - Check Logs: - - Access the go2rtc logs in the Frigate UI under Logs in the sidebar. - - If go2rtc is having difficulty connecting to your camera, you should see some error messages in the log. + + - Access the go2rtc logs in the Frigate UI under Logs in the sidebar. + - If go2rtc is having difficulty connecting to your camera, you should see some error messages in the log. - Check go2rtc Web Interface: if you don't see any errors in the logs, try viewing the camera through go2rtc's web interface. - - Navigate to port 1984 in your browser to access go2rtc's web interface. - - If using Frigate through Home Assistant, enable the web interface at port 1984. - - If using Docker, forward port 1984 before accessing the web interface. - - Click `stream` for the specific camera to see if the camera's stream is being received. + + - Navigate to port 1984 in your browser to access go2rtc's web interface. + - If using Frigate through Home Assistant, enable the web interface at port 1984. + - If using Docker, forward port 1984 before accessing the web interface. + - Click `stream` for the specific camera to see if the camera's stream is being received. - Check Video Codec: - - If the camera stream works in go2rtc but not in your browser, the video codec might be unsupported. - - If using H265, switch to H264. Refer to [video codec compatibility](https://github.com/AlexxIT/go2rtc/tree/v1.9.2#codecs-madness) in go2rtc documentation. - - If unable to switch from H265 to H264, or if the stream format is different (e.g., MJPEG), re-encode the video using [FFmpeg parameters](https://github.com/AlexxIT/go2rtc/tree/v1.9.2#source-ffmpeg). It supports rotating and resizing video feeds and hardware acceleration. Keep in mind that transcoding video from one format to another is a resource intensive task and you may be better off using the built-in jsmpeg view. - ```yaml - go2rtc: - streams: - back: - - rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2 - - "ffmpeg:back#video=h264#hardware" - ``` -- Switch to FFmpeg if needed: - - Some camera streams may need to use the ffmpeg module in go2rtc. This has the downside of slower startup times, but has compatibility with more stream types. - ```yaml - go2rtc: - streams: - back: - - ffmpeg:rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2 - ``` + - If the camera stream works in go2rtc but not in your browser, the video codec might be unsupported. + - If using H265, switch to H264. Refer to [video codec compatibility](https://github.com/AlexxIT/go2rtc/tree/v1.9.9#codecs-madness) in go2rtc documentation. + - If unable to switch from H265 to H264, or if the stream format is different (e.g., MJPEG), re-encode the video using [FFmpeg parameters](https://github.com/AlexxIT/go2rtc/tree/v1.9.9#source-ffmpeg). It supports rotating and resizing video feeds and hardware acceleration. Keep in mind that transcoding video from one format to another is a resource intensive task and you may be better off using the built-in jsmpeg view. + ```yaml + go2rtc: + streams: + back: + - rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2 + - "ffmpeg:back#video=h264#hardware" + ``` - - If you can see the video but do not have audio, this is most likely because your camera's audio stream codec is not AAC. - - If possible, update your camera's audio settings to AAC in your camera's firmware. - - If your cameras do not support AAC audio, you will need to tell go2rtc to re-encode the audio to AAC on demand if you want audio. This will use additional CPU and add some latency. To add AAC audio on demand, you can update your go2rtc config as follows: - ```yaml - go2rtc: - streams: - back: - - rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2 - - "ffmpeg:back#audio=aac" - ``` +- Switch to FFmpeg if needed: - If you need to convert **both** the audio and video streams, you can use the following: + - Some camera streams may need to use the ffmpeg module in go2rtc. This has the downside of slower startup times, but has compatibility with more stream types. - ```yaml - go2rtc: - streams: - back: - - rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2 - - "ffmpeg:back#video=h264#audio=aac#hardware" - ``` + ```yaml + go2rtc: + streams: + back: + - ffmpeg:rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2 + ``` - When using the ffmpeg module, you would add AAC audio like this: + - If you can see the video but do not have audio, this is most likely because your camera's audio stream codec is not AAC. + - If possible, update your camera's audio settings to AAC in your camera's firmware. + - If your cameras do not support AAC audio, you will need to tell go2rtc to re-encode the audio to AAC on demand if you want audio. This will use additional CPU and add some latency. To add AAC audio on demand, you can update your go2rtc config as follows: - ```yaml - go2rtc: - streams: - back: - - "ffmpeg:rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2#video=copy#audio=copy#audio=aac#hardware" - ``` + ```yaml + go2rtc: + streams: + back: + - rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2 + - "ffmpeg:back#audio=aac" + ``` + + If you need to convert **both** the audio and video streams, you can use the following: + + ```yaml + go2rtc: + streams: + back: + - rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2 + - "ffmpeg:back#video=h264#audio=aac#hardware" + ``` + + When using the ffmpeg module, you would add AAC audio like this: + + ```yaml + go2rtc: + streams: + back: + - "ffmpeg:rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2#video=copy#audio=copy#audio=aac#hardware" + ``` :::warning diff --git a/docs/docs/integrations/mqtt.md b/docs/docs/integrations/mqtt.md index fc8888e40..abbc12974 100644 --- a/docs/docs/integrations/mqtt.md +++ b/docs/docs/integrations/mqtt.md @@ -54,7 +54,9 @@ Message published for each changed tracked object. The first message is publishe }, // attributes with top score that have been identified on the object at any point "current_attributes": [], // detailed data about the current attributes in this frame "current_estimated_speed": 0.71, // current estimated speed (mph or kph) for objects moving through zones with speed estimation enabled - "velocity_angle": 180 // direction of travel relative to the frame for objects moving through zones with speed estimation enabled + "velocity_angle": 180, // direction of travel relative to the frame for objects moving through zones with speed estimation enabled + "recognized_license_plate": "ABC12345", // a recognized license plate for car objects + "recognized_license_plate_score": 0.933451 }, "after": { "id": "1607123955.475377-mxklsc", @@ -93,7 +95,9 @@ Message published for each changed tracked object. The first message is publishe } ], "current_estimated_speed": 0.77, // current estimated speed (mph or kph) for objects moving through zones with speed estimation enabled - "velocity_angle": 180 // direction of travel relative to the frame for objects moving through zones with speed estimation enabled + "velocity_angle": 180, // direction of travel relative to the frame for objects moving through zones with speed estimation enabled + "recognized_license_plate": "ABC12345", // a recognized license plate for car objects + "recognized_license_plate_score": 0.933451 } } ``` diff --git a/docs/sidebars.ts b/docs/sidebars.ts index 0c25e4eb7..bffb54349 100644 --- a/docs/sidebars.ts +++ b/docs/sidebars.ts @@ -1,106 +1,106 @@ -import type { SidebarsConfig, } from '@docusaurus/plugin-content-docs'; -import { PropSidebarItemLink } from '@docusaurus/plugin-content-docs'; -import frigateHttpApiSidebar from './docs/integrations/api/sidebar'; +import type { SidebarsConfig } from "@docusaurus/plugin-content-docs"; +import { PropSidebarItemLink } from "@docusaurus/plugin-content-docs"; +import frigateHttpApiSidebar from "./docs/integrations/api/sidebar"; const sidebars: SidebarsConfig = { docs: { Frigate: [ - 'frigate/index', - 'frigate/hardware', - 'frigate/installation', - 'frigate/camera_setup', - 'frigate/video_pipeline', - 'frigate/glossary', + "frigate/index", + "frigate/hardware", + "frigate/installation", + "frigate/camera_setup", + "frigate/video_pipeline", + "frigate/glossary", ], Guides: [ - 'guides/getting_started', - 'guides/configuring_go2rtc', - 'guides/ha_notifications', - 'guides/ha_network_storage', - 'guides/reverse_proxy', + "guides/getting_started", + "guides/configuring_go2rtc", + "guides/ha_notifications", + "guides/ha_network_storage", + "guides/reverse_proxy", ], Configuration: { - 'Configuration Files': [ - 'configuration/index', - 'configuration/reference', + "Configuration Files": [ + "configuration/index", + "configuration/reference", { - type: 'link', - label: 'Go2RTC Configuration Reference', - href: 'https://github.com/AlexxIT/go2rtc/tree/v1.9.2#configuration', + type: "link", + label: "Go2RTC Configuration Reference", + href: "https://github.com/AlexxIT/go2rtc/tree/v1.9.9#configuration", } as PropSidebarItemLink, ], Detectors: [ - 'configuration/object_detectors', - 'configuration/audio_detectors', + "configuration/object_detectors", + "configuration/audio_detectors", ], Classifiers: [ - 'configuration/semantic_search', - 'configuration/genai', - 'configuration/face_recognition', - 'configuration/license_plate_recognition', + "configuration/semantic_search", + "configuration/genai", + "configuration/face_recognition", + "configuration/license_plate_recognition", ], Cameras: [ - 'configuration/cameras', - 'configuration/review', - 'configuration/record', - 'configuration/snapshots', - 'configuration/motion_detection', - 'configuration/birdseye', - 'configuration/live', - 'configuration/restream', - 'configuration/autotracking', - 'configuration/camera_specific', + "configuration/cameras", + "configuration/review", + "configuration/record", + "configuration/snapshots", + "configuration/motion_detection", + "configuration/birdseye", + "configuration/live", + "configuration/restream", + "configuration/autotracking", + "configuration/camera_specific", ], Objects: [ - 'configuration/object_filters', - 'configuration/masks', - 'configuration/zones', - 'configuration/objects', - 'configuration/stationary_objects', + "configuration/object_filters", + "configuration/masks", + "configuration/zones", + "configuration/objects", + "configuration/stationary_objects", ], - 'Extra Configuration': [ - 'configuration/authentication', - 'configuration/notifications', - 'configuration/hardware_acceleration', - 'configuration/ffmpeg_presets', + "Extra Configuration": [ + "configuration/authentication", + "configuration/notifications", + "configuration/hardware_acceleration", + "configuration/ffmpeg_presets", "configuration/pwa", - 'configuration/tls', - 'configuration/advanced', + "configuration/tls", + "configuration/advanced", ], }, Integrations: [ - 'integrations/plus', - 'integrations/home-assistant', + "integrations/plus", + "integrations/home-assistant", // This is the HTTP API generated by OpenAPI { - type: 'category', - label: 'HTTP API', + type: "category", + label: "HTTP API", link: { - type: 'generated-index', - title: 'Frigate HTTP API', - description: 'HTTP API', - slug: '/integrations/api/frigate-http-api', + type: "generated-index", + title: "Frigate HTTP API", + description: "HTTP API", + slug: "/integrations/api/frigate-http-api", }, items: frigateHttpApiSidebar, }, - 'integrations/mqtt', - 'configuration/metrics', - 'integrations/third_party_extensions', + "integrations/mqtt", + "configuration/metrics", + "integrations/third_party_extensions", ], - 'Frigate+': [ - 'plus/index', - 'plus/first_model', - 'plus/improving_model', - 'plus/faq', + "Frigate+": [ + "plus/index", + "plus/first_model", + "plus/improving_model", + "plus/faq", ], Troubleshooting: [ - 'troubleshooting/faqs', - 'troubleshooting/recordings', - 'troubleshooting/edgetpu', + "troubleshooting/faqs", + "troubleshooting/recordings", + "troubleshooting/edgetpu", ], Development: [ - 'development/contributing', - 'development/contributing-boards', + "development/contributing", + "development/contributing-boards", ], }, }; diff --git a/frigate/api/app.py b/frigate/api/app.py index 5ce90130f..05013ed12 100644 --- a/frigate/api/app.py +++ b/frigate/api/app.py @@ -619,6 +619,41 @@ def get_sub_labels(split_joined: Optional[int] = None): return JSONResponse(content=sub_labels) +@router.get("/recognized_license_plates") +def get_recognized_license_plates(split_joined: Optional[int] = None): + try: + events = Event.select(Event.data).distinct() + except Exception: + return JSONResponse( + content=( + {"success": False, "message": "Failed to get recognized license plates"} + ), + status_code=404, + ) + + recognized_license_plates = [] + for e in events: + if e.data is not None and "recognized_license_plate" in e.data: + recognized_license_plates.append(e.data["recognized_license_plate"]) + + while None in recognized_license_plates: + recognized_license_plates.remove(None) + + if split_joined: + original_recognized_license_plates = recognized_license_plates.copy() + for recognized_license_plate in original_recognized_license_plates: + if recognized_license_plate and "," in recognized_license_plate: + recognized_license_plates.remove(recognized_license_plate) + parts = recognized_license_plate.split(",") + for part in parts: + if part.strip() not in recognized_license_plates: + recognized_license_plates.append(part.strip()) + + recognized_license_plates = list(set(recognized_license_plates)) + recognized_license_plates.sort() + return JSONResponse(content=recognized_license_plates) + + @router.get("/timeline") def timeline(camera: str = "all", limit: int = 100, source_id: Optional[str] = None): clauses = [] diff --git a/frigate/api/defs/query/events_query_parameters.py b/frigate/api/defs/query/events_query_parameters.py index 01c79abb0..d707ba8cc 100644 --- a/frigate/api/defs/query/events_query_parameters.py +++ b/frigate/api/defs/query/events_query_parameters.py @@ -27,6 +27,7 @@ class EventsQueryParams(BaseModel): max_score: Optional[float] = None min_speed: Optional[float] = None max_speed: Optional[float] = None + recognized_license_plate: Optional[str] = "all" is_submitted: Optional[int] = None min_length: Optional[float] = None max_length: Optional[float] = None @@ -55,6 +56,7 @@ class EventsSearchQueryParams(BaseModel): max_score: Optional[float] = None min_speed: Optional[float] = None max_speed: Optional[float] = None + recognized_license_plate: Optional[str] = "all" sort: Optional[str] = None diff --git a/frigate/api/event.py b/frigate/api/event.py index 91651313d..88a865318 100644 --- a/frigate/api/event.py +++ b/frigate/api/event.py @@ -101,6 +101,7 @@ def events(params: EventsQueryParams = Depends()): min_length = params.min_length max_length = params.max_length event_id = params.event_id + recognized_license_plate = params.recognized_license_plate sort = params.sort @@ -158,6 +159,45 @@ def events(params: EventsQueryParams = Depends()): sub_label_clause = reduce(operator.or_, sub_label_clauses) clauses.append((sub_label_clause)) + if recognized_license_plate != "all": + # use matching so joined recognized_license_plates are included + # for example a recognized license plate 'ABC123' would get events + # with recognized license plates 'ABC123' and 'ABC123, XYZ789' + recognized_license_plate_clauses = [] + filtered_recognized_license_plates = recognized_license_plate.split(",") + + if "None" in filtered_recognized_license_plates: + filtered_recognized_license_plates.remove("None") + recognized_license_plate_clauses.append( + (Event.data["recognized_license_plate"].is_null()) + ) + + for recognized_license_plate in filtered_recognized_license_plates: + # Exact matching plus list inclusion + recognized_license_plate_clauses.append( + ( + Event.data["recognized_license_plate"].cast("text") + == recognized_license_plate + ) + ) + recognized_license_plate_clauses.append( + ( + Event.data["recognized_license_plate"].cast("text") + % f"*{recognized_license_plate},*" + ) + ) + recognized_license_plate_clauses.append( + ( + Event.data["recognized_license_plate"].cast("text") + % f"*, {recognized_license_plate}*" + ) + ) + + recognized_license_plate_clause = reduce( + operator.or_, recognized_license_plate_clauses + ) + clauses.append((recognized_license_plate_clause)) + if zones != "all": # use matching so events with multiple zones # still match on a search where any zone matches @@ -340,6 +380,8 @@ def events_explore(limit: int = 10): "average_estimated_speed", "velocity_angle", "path_data", + "recognized_license_plate", + "recognized_license_plate_score", ] }, "event_count": label_counts[event.label], @@ -397,6 +439,7 @@ def events_search(request: Request, params: EventsSearchQueryParams = Depends()) has_clip = params.has_clip has_snapshot = params.has_snapshot is_submitted = params.is_submitted + recognized_license_plate = params.recognized_license_plate # for similarity search event_id = params.event_id @@ -466,6 +509,45 @@ def events_search(request: Request, params: EventsSearchQueryParams = Depends()) event_filters.append((reduce(operator.or_, zone_clauses))) + if recognized_license_plate != "all": + # use matching so joined recognized_license_plates are included + # for example an recognized_license_plate 'ABC123' would get events + # with recognized_license_plates 'ABC123' and 'ABC123, XYZ789' + recognized_license_plate_clauses = [] + filtered_recognized_license_plates = recognized_license_plate.split(",") + + if "None" in filtered_recognized_license_plates: + filtered_recognized_license_plates.remove("None") + recognized_license_plate_clauses.append( + (Event.data["recognized_license_plate"].is_null()) + ) + + for recognized_license_plate in filtered_recognized_license_plates: + # Exact matching plus list inclusion + recognized_license_plate_clauses.append( + ( + Event.data["recognized_license_plate"].cast("text") + == recognized_license_plate + ) + ) + recognized_license_plate_clauses.append( + ( + Event.data["recognized_license_plate"].cast("text") + % f"*{recognized_license_plate},*" + ) + ) + recognized_license_plate_clauses.append( + ( + Event.data["recognized_license_plate"].cast("text") + % f"*, {recognized_license_plate}*" + ) + ) + + recognized_license_plate_clause = reduce( + operator.or_, recognized_license_plate_clauses + ) + event_filters.append((recognized_license_plate_clause)) + if after: event_filters.append((Event.start_time > after)) @@ -627,6 +709,8 @@ def events_search(request: Request, params: EventsSearchQueryParams = Depends()) "average_estimated_speed", "velocity_angle", "path_data", + "recognized_license_plate", + "recognized_license_plate_score", ] } @@ -681,6 +765,7 @@ def events_summary(params: EventsSummaryQueryParams = Depends()): Event.camera, Event.label, Event.sub_label, + Event.data, fn.strftime( "%Y-%m-%d", fn.datetime( @@ -695,6 +780,7 @@ def events_summary(params: EventsSummaryQueryParams = Depends()): Event.camera, Event.label, Event.sub_label, + Event.data, (Event.start_time + seconds_offset).cast("int") / (3600 * 24), Event.zones, ) diff --git a/frigate/camera/state.py b/frigate/camera/state.py index dfd6744e2..0e02c6c14 100644 --- a/frigate/camera/state.py +++ b/frigate/camera/state.py @@ -137,12 +137,16 @@ class CameraState: # draw the bounding boxes on the frame box = obj["box"] text = ( - obj["label"] + obj["sub_label"][0] if ( - not obj.get("sub_label") - or not is_label_printable(obj["sub_label"][0]) + obj.get("sub_label") and is_label_printable(obj["sub_label"][0]) ) - else obj["sub_label"][0] + else obj.get("recognized_license_plate", [None])[0] + if ( + obj.get("recognized_license_plate") + and obj["recognized_license_plate"][0] + ) + else obj["label"] ) draw_box_with_label( frame_copy, diff --git a/frigate/comms/event_metadata_updater.py b/frigate/comms/event_metadata_updater.py index 3342182c3..c702208bc 100644 --- a/frigate/comms/event_metadata_updater.py +++ b/frigate/comms/event_metadata_updater.py @@ -14,6 +14,7 @@ class EventMetadataTypeEnum(str, Enum): manual_event_end = "manual_event_end" regenerate_description = "regenerate_description" sub_label = "sub_label" + recognized_license_plate = "recognized_license_plate" class EventMetadataPublisher(Publisher): diff --git a/frigate/config/camera/ffmpeg.py b/frigate/config/camera/ffmpeg.py index 0b1ec2331..04bbfac7b 100644 --- a/frigate/config/camera/ffmpeg.py +++ b/frigate/config/camera/ffmpeg.py @@ -21,7 +21,7 @@ __all__ = [ FFMPEG_GLOBAL_ARGS_DEFAULT = ["-hide_banner", "-loglevel", "warning", "-threads", "2"] FFMPEG_INPUT_ARGS_DEFAULT = "preset-rtsp-generic" -RECORD_FFMPEG_OUTPUT_ARGS_DEFAULT = "preset-record-generic" +RECORD_FFMPEG_OUTPUT_ARGS_DEFAULT = "preset-record-generic-audio-aac" DETECT_FFMPEG_OUTPUT_ARGS_DEFAULT = [ "-threads", "2", diff --git a/frigate/data_processing/common/license_plate/mixin.py b/frigate/data_processing/common/license_plate/mixin.py index c74949d9c..751a674f5 100644 --- a/frigate/data_processing/common/license_plate/mixin.py +++ b/frigate/data_processing/common/license_plate/mixin.py @@ -1054,13 +1054,20 @@ class LicensePlateProcessingMixin: for plate in plates ) ), - top_plate, + None, ) - # Send the result to the API + # If it's a known plate, publish to sub_label + if sub_label is not None: + self.sub_label_publisher.publish( + EventMetadataTypeEnum.sub_label, (id, sub_label, avg_confidence) + ) + self.sub_label_publisher.publish( - EventMetadataTypeEnum.sub_label, (id, sub_label, avg_confidence) + EventMetadataTypeEnum.recognized_license_plate, + (id, top_plate, avg_confidence), ) + self.detected_license_plates[id] = { "plate": top_plate, "char_confidences": top_char_confidences, diff --git a/frigate/events/maintainer.py b/frigate/events/maintainer.py index 5cfa7c716..7788c83e9 100644 --- a/frigate/events/maintainer.py +++ b/frigate/events/maintainer.py @@ -27,6 +27,8 @@ def should_update_db(prev_event: Event, current_event: Event) -> bool: or prev_event["average_estimated_speed"] != current_event["average_estimated_speed"] or prev_event["velocity_angle"] != current_event["velocity_angle"] + or prev_event["recognized_license_plate"] + != current_event["recognized_license_plate"] or prev_event["path_data"] != current_event["path_data"] ): return True @@ -226,6 +228,15 @@ class EventProcessor(threading.Thread): event[Event.sub_label] = event_data["sub_label"][0] event[Event.data]["sub_label_score"] = event_data["sub_label"][1] + # only overwrite the recognized_license_plate in the database if it's set + if event_data.get("recognized_license_plate") is not None: + event[Event.data]["recognized_license_plate"] = event_data[ + "recognized_license_plate" + ][0] + event[Event.data]["recognized_license_plate_score"] = event_data[ + "recognized_license_plate" + ][1] + ( Event.insert(event) .on_conflict( diff --git a/frigate/track/object_processing.py b/frigate/track/object_processing.py index ddac2b588..b18ad97fa 100644 --- a/frigate/track/object_processing.py +++ b/frigate/track/object_processing.py @@ -346,6 +346,44 @@ class TrackedObjectProcessor(threading.Thread): return True + def set_recognized_license_plate( + self, event_id: str, recognized_license_plate: str | None, score: float | None + ) -> None: + """Update recognized license plate for given event id.""" + tracked_obj: TrackedObject = None + + for state in self.camera_states.values(): + tracked_obj = state.tracked_objects.get(event_id) + + if tracked_obj is not None: + break + + try: + event: Event = Event.get(Event.id == event_id) + except DoesNotExist: + event = None + + if not tracked_obj and not event: + return + + if tracked_obj: + tracked_obj.obj_data["recognized_license_plate"] = ( + recognized_license_plate, + score, + ) + + if event: + data = event.data + data["recognized_license_plate"] = recognized_license_plate + if recognized_license_plate is None: + data["recognized_license_plate_score"] = None + elif score is not None: + data["recognized_license_plate_score"] = score + event.data = data + event.save() + + return True + def create_manual_event(self, payload: tuple) -> None: ( frame_time, @@ -507,6 +545,11 @@ class TrackedObjectProcessor(threading.Thread): if topic.endswith(EventMetadataTypeEnum.sub_label.value): (event_id, sub_label, score) = payload self.set_sub_label(event_id, sub_label, score) + if topic.endswith(EventMetadataTypeEnum.recognized_license_plate.value): + (event_id, recognized_license_plate, score) = payload + self.set_recognized_license_plate( + event_id, recognized_license_plate, score + ) elif topic.endswith(EventMetadataTypeEnum.manual_event_create.value): self.create_manual_event(payload) elif topic.endswith(EventMetadataTypeEnum.manual_event_end.value): diff --git a/frigate/track/tracked_object.py b/frigate/track/tracked_object.py index f1eb29328..7a4829c2a 100644 --- a/frigate/track/tracked_object.py +++ b/frigate/track/tracked_object.py @@ -153,6 +153,12 @@ class TrackedObject: "current_estimated_speed": self.current_estimated_speed, "velocity_angle": self.velocity_angle, "path_data": self.path_data, + "recognized_license_plate": obj_data.get( + "recognized_license_plate" + ), + "recognized_license_plate_score": obj_data.get( + "recognized_license_plate_score" + ), } thumb_update = True @@ -365,6 +371,7 @@ class TrackedObject: "average_estimated_speed": self.average_estimated_speed, "velocity_angle": self.velocity_angle, "path_data": self.path_data, + "recognized_license_plate": self.obj_data.get("recognized_license_plate"), } return event diff --git a/web/src/components/overlay/detail/SearchDetailDialog.tsx b/web/src/components/overlay/detail/SearchDetailDialog.tsx index 254697234..dd775f582 100644 --- a/web/src/components/overlay/detail/SearchDetailDialog.tsx +++ b/web/src/components/overlay/detail/SearchDetailDialog.tsx @@ -337,6 +337,23 @@ function ObjectDetailsTab({ } }, [search]); + const recognizedLicensePlateScore = useMemo(() => { + if (!search) { + return undefined; + } + + if ( + search.data.recognized_license_plate && + search.data?.recognized_license_plate_score + ) { + return Math.round( + (search.data?.recognized_license_plate_score ?? 0) * 100, + ); + } else { + return undefined; + } + }, [search]); + const averageEstimatedSpeed = useMemo(() => { if (!search || !search.data?.average_estimated_speed) { return undefined; @@ -547,6 +564,20 @@ function ObjectDetailsTab({ + {search?.data.recognized_license_plate && ( +
+
+ Recognized License Plate +
+
+
+ {search.data.recognized_license_plate}{" "} + {recognizedLicensePlateScore && + ` (${recognizedLicensePlateScore}%)`} +
+
+
+ )}
diff --git a/web/src/components/overlay/dialog/SearchFilterDialog.tsx b/web/src/components/overlay/dialog/SearchFilterDialog.tsx index 621572e57..509169602 100644 --- a/web/src/components/overlay/dialog/SearchFilterDialog.tsx +++ b/web/src/components/overlay/dialog/SearchFilterDialog.tsx @@ -34,6 +34,14 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { Trans, useTranslation } from "react-i18next"; +import { + Command, + CommandEmpty, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { LuCheck } from "react-icons/lu"; type SearchFilterDialogProps = { config?: FrigateConfig; @@ -78,7 +86,8 @@ export default function SearchFilterDialog({ (currentFilter.max_score ?? 1) < 1 || (currentFilter.max_speed ?? 150) < 150 || (currentFilter.zones?.length ?? 0) > 0 || - (currentFilter.sub_labels?.length ?? 0) > 0), + (currentFilter.sub_labels?.length ?? 0) > 0 || + (currentFilter.recognized_license_plate?.length ?? 0) > 0), [currentFilter], ); @@ -120,6 +129,15 @@ export default function SearchFilterDialog({ setCurrentFilter({ ...currentFilter, sub_labels: newSubLabels }) } /> + + setCurrentFilter({ + ...currentFilter, + recognized_license_plate: plate, + }) + } + /> @@ -843,3 +862,130 @@ export function SnapshotClipFilterContent({
); } + +type RecognizedLicensePlatesFilterContentProps = { + recognizedLicensePlates: string[] | undefined; + setRecognizedLicensePlates: ( + recognizedLicensePlates: string[] | undefined, + ) => void; +}; + +export function RecognizedLicensePlatesFilterContent({ + recognizedLicensePlates, + setRecognizedLicensePlates, +}: RecognizedLicensePlatesFilterContentProps) { + const { data: allRecognizedLicensePlates, error } = useSWR( + "recognized_license_plates", + { + revalidateOnFocus: false, + }, + ); + + const [selectedRecognizedLicensePlates, setSelectedRecognizedLicensePlates] = + useState(recognizedLicensePlates || []); + const [inputValue, setInputValue] = useState(""); + + useEffect(() => { + if (recognizedLicensePlates) { + setSelectedRecognizedLicensePlates(recognizedLicensePlates); + } else { + setSelectedRecognizedLicensePlates([]); + } + }, [recognizedLicensePlates]); + + const handleSelect = (recognizedLicensePlate: string) => { + const newSelected = selectedRecognizedLicensePlates.includes( + recognizedLicensePlate, + ) + ? selectedRecognizedLicensePlates.filter( + (id) => id !== recognizedLicensePlate, + ) // Deselect + : [...selectedRecognizedLicensePlates, recognizedLicensePlate]; // Select + + setSelectedRecognizedLicensePlates(newSelected); + if (newSelected.length === 0) { + setRecognizedLicensePlates(undefined); // Clear filter if no plates selected + } else { + setRecognizedLicensePlates(newSelected); + } + }; + + if (!allRecognizedLicensePlates || allRecognizedLicensePlates.length === 0) { + return null; + } + + const filteredRecognizedLicensePlates = + allRecognizedLicensePlates?.filter((id) => + id.toLowerCase().includes(inputValue.toLowerCase()), + ) || []; + + return ( +
+ +
Recognized License Plates
+ {error ? ( +

+ Failed to load recognized license plates. +

+ ) : !allRecognizedLicensePlates ? ( +

+ Loading recognized license plates... +

+ ) : ( + <> + + + + {filteredRecognizedLicensePlates.length === 0 && inputValue && ( + No license plates found. + )} + {filteredRecognizedLicensePlates.map((plate) => ( + handleSelect(plate)} + className="cursor-pointer" + > + + {plate} + + ))} + + + {selectedRecognizedLicensePlates.length > 0 && ( +
+ {selectedRecognizedLicensePlates.map((id) => ( + + {id} + + + ))} +
+ )} + + )} +

+ Select one or more plates from the list. +

+
+ ); +} diff --git a/web/src/pages/Explore.tsx b/web/src/pages/Explore.tsx index 0e884f8f1..061ab3fba 100644 --- a/web/src/pages/Explore.tsx +++ b/web/src/pages/Explore.tsx @@ -108,6 +108,8 @@ export default function Explore() { cameras: searchSearchParams["cameras"], labels: searchSearchParams["labels"], sub_labels: searchSearchParams["sub_labels"], + recognized_license_plate: + searchSearchParams["recognized_license_plate"], zones: searchSearchParams["zones"], before: searchSearchParams["before"], after: searchSearchParams["after"], @@ -143,6 +145,8 @@ export default function Explore() { cameras: searchSearchParams["cameras"], labels: searchSearchParams["labels"], sub_labels: searchSearchParams["sub_labels"], + recognized_license_plate: + searchSearchParams["recognized_license_plate"], zones: searchSearchParams["zones"], before: searchSearchParams["before"], after: searchSearchParams["after"], diff --git a/web/src/types/search.ts b/web/src/types/search.ts index cef7f6aff..5dca11973 100644 --- a/web/src/types/search.ts +++ b/web/src/types/search.ts @@ -58,6 +58,8 @@ export type SearchResult = { average_estimated_speed: number; velocity_angle: number; path_data: [number[], number][]; + recognized_license_plate?: string; + recognized_license_plate_score?: number; }; }; @@ -66,6 +68,7 @@ export type SearchFilter = { cameras?: string[]; labels?: string[]; sub_labels?: string[]; + recognized_license_plate?: string[]; zones?: string[]; before?: number; after?: number; @@ -89,6 +92,7 @@ export type SearchQueryParams = { cameras?: string[]; labels?: string[]; sub_labels?: string[]; + recognized_license_plate?: string[]; zones?: string[]; before?: string; after?: string; diff --git a/web/src/views/search/SearchView.tsx b/web/src/views/search/SearchView.tsx index 9211fa91a..96a7d7be6 100644 --- a/web/src/views/search/SearchView.tsx +++ b/web/src/views/search/SearchView.tsx @@ -123,6 +123,9 @@ export default function SearchView({ }, [config, searchFilter]); const { data: allSubLabels } = useSWR("sub_labels"); + const { data: allRecognizedLicensePlates } = useSWR( + "recognized_license_plates", + ); const allZones = useMemo(() => { if (!config) { @@ -162,12 +165,20 @@ export default function SearchView({ max_score: ["100"], min_speed: ["1"], max_speed: ["150"], + recognized_license_plate: allRecognizedLicensePlates, has_clip: ["yes", "no"], has_snapshot: ["yes", "no"], ...(config?.plus?.enabled && searchFilter?.has_snapshot && { is_submitted: ["yes", "no"] }), }), - [config, allLabels, allZones, allSubLabels, searchFilter], + [ + config, + allLabels, + allZones, + allSubLabels, + allRecognizedLicensePlates, + searchFilter, + ], ); // remove duplicate event ids