From 46c3ef8c6b9f770770af92d4c2b55d5d91bf0dcc Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Sat, 29 Jun 2024 07:18:40 -0600 Subject: [PATCH 1/3] Nginx config tweaks (#12174) * Change auth port and remove ipv6 * Add docs for nginx bind mount * Consolidate listen statements * Update port in docs * Fix typing --- .devcontainer/devcontainer.json | 4 +- .../etc/s6-overlay/s6-rc.d/certsync/run | 2 +- .../rootfs/usr/local/nginx/conf/nginx.conf | 3 -- .../usr/local/nginx/templates/listen.gotmpl | 7 ++- docs/docs/configuration/advanced.md | 50 ++++++++++++++++++- docs/docs/configuration/authentication.md | 2 +- docs/docs/configuration/reference.md | 2 +- docs/docs/configuration/tls.md | 6 +-- docs/docs/frigate/installation.md | 8 +-- docs/docs/guides/getting_started.md | 4 +- docs/docs/guides/reverse_proxy.md | 18 +++---- frigate/api/media.py | 2 +- frigate/config.py | 2 +- 13 files changed, 78 insertions(+), 32 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 223112a7d..425a282d4 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -10,9 +10,9 @@ "features": { "ghcr.io/devcontainers/features/common-utils:1": {} }, - "forwardPorts": [8080, 5000, 5001, 5173, 8554, 8555], + "forwardPorts": [8971, 5000, 5001, 5173, 8554, 8555], "portsAttributes": { - "8080": { + "8971": { "label": "External NGINX", "onAutoForward": "silent" }, diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync/run b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync/run index 521a07463..af3bc04de 100755 --- a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync/run +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync/run @@ -34,7 +34,7 @@ do ;; esac - liveprint=`echo | openssl s_client -showcerts -connect 127.0.0.1:8080 2>&1 | openssl x509 -fingerprint 2>&1 | grep -i fingerprint || echo 'failed'` + liveprint=`echo | openssl s_client -showcerts -connect 127.0.0.1:8971 2>&1 | openssl x509 -fingerprint 2>&1 | grep -i fingerprint || echo 'failed'` case "$liveprint" in *Fingerprint*) diff --git a/docker/main/rootfs/usr/local/nginx/conf/nginx.conf b/docker/main/rootfs/usr/local/nginx/conf/nginx.conf index 7bf7ef59e..186b7037c 100644 --- a/docker/main/rootfs/usr/local/nginx/conf/nginx.conf +++ b/docker/main/rootfs/usr/local/nginx/conf/nginx.conf @@ -59,9 +59,6 @@ http { include go2rtc_upstream.conf; server { - # intended for internal traffic, not protected by auth - listen [::]:5000 ipv6only=off; - include listen.conf; # vod settings diff --git a/docker/main/rootfs/usr/local/nginx/templates/listen.gotmpl b/docker/main/rootfs/usr/local/nginx/templates/listen.gotmpl index e301a884e..83f13b756 100644 --- a/docker/main/rootfs/usr/local/nginx/templates/listen.gotmpl +++ b/docker/main/rootfs/usr/local/nginx/templates/listen.gotmpl @@ -1,9 +1,12 @@ {{ if not .enabled }} # intended for external traffic, protected by auth -listen [::]:8080 ipv6only=off; +listen 8971; {{ else }} # intended for external traffic, protected by auth -listen [::]:8080 ipv6only=off ssl; +listen 8971 ssl; + +# intended for internal traffic, not protected by auth +listen 5000; ssl_certificate /etc/letsencrypt/live/frigate/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/frigate/privkey.pem; diff --git a/docs/docs/configuration/advanced.md b/docs/docs/configuration/advanced.md index 0e8e8d641..37a3b5ceb 100644 --- a/docs/docs/configuration/advanced.md +++ b/docs/docs/configuration/advanced.md @@ -106,7 +106,53 @@ Some labels have special handling and modifications can disable functionality. ::: -## Custom ffmpeg build +## Network Configuration + +Changes to Frigate's internal network configuration can be made by bind mounting nginx.conf into the container. For example: + +```yaml +services: + frigate: + container_name: frigate + ... + volumes: + ... + - /path/to/your/nginx.conf:/usr/local/nginx/conf/nginx.conf +``` + +### Enabling IPv6 + +IPv6 is disabled by default, to enable IPv6 listen.gotmpl needs to be bind mounted with IPv6 enabled. For example: + +``` +{{ if not .enabled }} +# intended for external traffic, protected by auth +listen 8971; +{{ else }} +# intended for external traffic, protected by auth +listen 8971 ssl; + +# intended for internal traffic, not protected by auth +listen 5000; +``` + +becomes + +``` +{{ if not .enabled }} +# intended for external traffic, protected by auth +listen [::]:8971 ipv6only=off; +{{ else }} +# intended for external traffic, protected by auth +listen [::]:8971 ipv6only=off ssl; + +# intended for internal traffic, not protected by auth +listen [::]:5000 ipv6only=off; +``` + +## Custom Dependencies + +### Custom ffmpeg build Included with Frigate is a build of ffmpeg that works for the vast majority of users. However, there exists some hardware setups which have incompatibilities with the included build. In this case, a docker volume mapping can be used to overwrite the included ffmpeg build with an ffmpeg build that works for your specific hardware setup. @@ -118,7 +164,7 @@ To do this: NOTE: The folder that is mapped from the host needs to be the folder that contains `/bin`. So if the full structure is `/home/appdata/frigate/custom-ffmpeg/bin/ffmpeg` then `/home/appdata/frigate/custom-ffmpeg` needs to be mapped to `/usr/lib/btbn-ffmpeg`. -## Custom go2rtc version +### Custom go2rtc version Frigate currently includes go2rtc v1.9.4, there may be certain cases where you want to run a different version of go2rtc. diff --git a/docs/docs/configuration/authentication.md b/docs/docs/configuration/authentication.md index 2728d1421..47d7e85a3 100644 --- a/docs/docs/configuration/authentication.md +++ b/docs/docs/configuration/authentication.md @@ -13,7 +13,7 @@ The following ports are available to access the Frigate web UI. | Port | Description | | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `8080` | Authenticated UI and API. Reverse proxies should use this port. | +| `8971` | Authenticated UI and API. Reverse proxies should use this port. | | `5000` | Internal unauthenticated UI and API access. Access to this port should be limited. Intended to be used within the docker network for services that integrate with Frigate and do not support authentication. | ## Onboarding diff --git a/docs/docs/configuration/reference.md b/docs/docs/configuration/reference.md index 90bdce8a9..8b51de148 100644 --- a/docs/docs/configuration/reference.md +++ b/docs/docs/configuration/reference.md @@ -65,7 +65,7 @@ database: # Optional: TLS configuration tls: - # Optional: Enable TLS for port 8080 (default: shown below) + # Optional: Enable TLS for port 8971 (default: shown below) enabled: True # Optional: Proxy configuration diff --git a/docs/docs/configuration/tls.md b/docs/docs/configuration/tls.md index 89e79410e..7b254c100 100644 --- a/docs/docs/configuration/tls.md +++ b/docs/docs/configuration/tls.md @@ -5,7 +5,7 @@ title: TLS # TLS -Frigate's integrated NGINX server supports TLS certificates. By default Frigate will generate a self signed certificate that will be used for port 8080. Frigate is designed to make it easy to use whatever tool you prefer to manage certificates. +Frigate's integrated NGINX server supports TLS certificates. By default Frigate will generate a self signed certificate that will be used for port 8971. Frigate is designed to make it easy to use whatever tool you prefer to manage certificates. Frigate is often running behind a reverse proxy that manages TLS certificates for multiple services. You will likely need to set your reverse proxy to allow self signed certificates or you can disable TLS in Frigate's config. However, if you are running on a dedicated device that's separate from your proxy or if you expose Frigate directly to the internet, you may want to configure TLS with valid certificates. @@ -44,13 +44,13 @@ frigate: Frigate automatically compares the fingerprint of the certificate at `/etc/letsencrypt/live/frigate/fullchain.pem` against the fingerprint of the TLS cert in NGINX every minute. If these differ, the NGINX config is reloaded to pick up the updated certificate. -If you issue Frigate valid certificates you will likely want to configure it to run on port 443 so you can access it without a port number like `https://your-frigate-domain.com` by mapping 8080 to 443. +If you issue Frigate valid certificates you will likely want to configure it to run on port 443 so you can access it without a port number like `https://your-frigate-domain.com` by mapping 8971 to 443. ```yaml frigate: ... ports: - - "443:8080" + - "443:8971" ... ``` diff --git a/docs/docs/frigate/installation.md b/docs/docs/frigate/installation.md index ff2bdf220..e80831edb 100644 --- a/docs/docs/frigate/installation.md +++ b/docs/docs/frigate/installation.md @@ -34,7 +34,7 @@ The following ports are used by Frigate and can be mapped via docker as required | Port | Description | | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `8080` | Authenticated UI and API access without TLS. Reverse proxies should use this port. | +| `8971` | Authenticated UI and API access without TLS. Reverse proxies should use this port. | | `5000` | Internal unauthenticated UI and API access. Access to this port should be limited. Intended to be used within the docker network for services that integrate with Frigate. | | `8554` | RTSP restreaming. By default, these streams are unauthenticated. Authentication can be configured in go2rtc section of config. | | `8555` | WebRTC connections for low latency live views. | @@ -171,7 +171,7 @@ services: tmpfs: size: 1000000000 ports: - - "8080:8080" + - "8971:8971" # - "5000:5000" # Internal unauthenticated access. Expose carefully. - "8554:8554" # RTSP feeds - "8555:8555/tcp" # WebRTC over tcp @@ -194,7 +194,7 @@ docker run -d \ -v /path/to/your/config:/config \ -v /etc/localtime:/etc/localtime:ro \ -e FRIGATE_RTSP_PASSWORD='password' \ - -p 8080:8080 \ + -p 8971:8971 \ -p 8554:8554 \ -p 8555:8555/tcp \ -p 8555:8555/udp \ @@ -370,7 +370,7 @@ docker run \ --network=bridge \ --privileged \ --workdir=/opt/frigate \ - -p 8080:8080 \ + -p 8971:8971 \ -p 8554:8554 \ -p 8555:8555 \ -p 8555:8555/udp \ diff --git a/docs/docs/guides/getting_started.md b/docs/docs/guides/getting_started.md index a828bfbdb..3b58a1d38 100644 --- a/docs/docs/guides/getting_started.md +++ b/docs/docs/guides/getting_started.md @@ -117,7 +117,7 @@ services: tmpfs: size: 1000000000 ports: - - "8080:8080" + - "8971:8971" - "8554:8554" # RTSP feeds ``` @@ -137,7 +137,7 @@ cameras: - detect ``` -Now you should be able to start Frigate by running `docker compose up -d` from within the folder containing `docker-compose.yml`. On startup, an admin user and password will be created and outputted in the logs. You can see this by running `docker logs frigate`. Frigate should now be accessible at `https://server_ip:8080` where you can login with the `admin` user and finish the configuration using the built-in configuration editor. +Now you should be able to start Frigate by running `docker compose up -d` from within the folder containing `docker-compose.yml`. On startup, an admin user and password will be created and outputted in the logs. You can see this by running `docker logs frigate`. Frigate should now be accessible at `https://server_ip:8971` where you can login with the `admin` user and finish the configuration using the built-in configuration editor. ## Configuring Frigate diff --git a/docs/docs/guides/reverse_proxy.md b/docs/docs/guides/reverse_proxy.md index b65ad515b..012d6b228 100644 --- a/docs/docs/guides/reverse_proxy.md +++ b/docs/docs/guides/reverse_proxy.md @@ -38,20 +38,20 @@ Here we access Frigate via https://cctv.mydomain.co.uk ServerName cctv.mydomain.co.uk ProxyPreserveHost On - ProxyPass "/" "http://frigatepi.local:8080/" - ProxyPassReverse "/" "http://frigatepi.local:8080/" + ProxyPass "/" "http://frigatepi.local:8971/" + ProxyPassReverse "/" "http://frigatepi.local:8971/" - ProxyPass /ws ws://frigatepi.local:8080/ws - ProxyPassReverse /ws ws://frigatepi.local:8080/ws + ProxyPass /ws ws://frigatepi.local:8971/ws + ProxyPassReverse /ws ws://frigatepi.local:8971/ws - ProxyPass /live/ ws://frigatepi.local:8080/live/ - ProxyPassReverse /live/ ws://frigatepi.local:8080/live/ + ProxyPass /live/ ws://frigatepi.local:8971/live/ + ProxyPassReverse /live/ ws://frigatepi.local:8971/live/ RewriteEngine on RewriteCond %{HTTP:Upgrade} =websocket [NC] - RewriteRule /(.*) ws://frigatepi.local:8080/$1 [P,L] + RewriteRule /(.*) ws://frigatepi.local:8971/$1 [P,L] RewriteCond %{HTTP:Upgrade} !=websocket [NC] - RewriteRule /(.*) http://frigatepi.local:8080/$1 [P,L] + RewriteRule /(.*) http://frigatepi.local:8971/$1 [P,L] ``` @@ -101,7 +101,7 @@ This is set in `$server` and `$port` this should match your ports you have expos server { set $forward_scheme http; set $server "192.168.100.2"; # FRIGATE SERVER LOCATION - set $port 8080; + set $port 8971; listen 80; listen 443 ssl http2; diff --git a/frigate/api/media.py b/frigate/api/media.py index 3f3cdb736..c0afb613e 100644 --- a/frigate/api/media.py +++ b/frigate/api/media.py @@ -637,7 +637,7 @@ def vod_event(id): # If the recordings are not found and the event started more than 5 minutes ago, set has_clip to false if ( event.start_time < datetime.now().timestamp() - 300 - and type(vod_response) == tuple + and type(vod_response) is tuple and len(vod_response) == 2 and vod_response[1] == 404 ): diff --git a/frigate/config.py b/frigate/config.py index 59ce58ea3..5331d311a 100644 --- a/frigate/config.py +++ b/frigate/config.py @@ -116,7 +116,7 @@ class UIConfig(FrigateBaseModel): class TlsConfig(FrigateBaseModel): - enabled: bool = Field(default=True, title="Enable TLS for port 8080") + enabled: bool = Field(default=True, title="Enable TLS for port 8971") class HeaderMappingConfig(FrigateBaseModel): From 48a87b16b841367801bfc25a6d11368e21c6cef9 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Sat, 29 Jun 2024 08:35:34 -0600 Subject: [PATCH 2/3] Fix yamnet model download (#12200) --- docker/main/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/main/Dockerfile b/docker/main/Dockerfile index 15fb3b030..8679c50a6 100644 --- a/docker/main/Dockerfile +++ b/docker/main/Dockerfile @@ -78,7 +78,7 @@ COPY --from=ov-converter /models/ssdlite_mobilenet_v2.bin openvino-model/ RUN wget -q https://github.com/openvinotoolkit/open_model_zoo/raw/master/data/dataset_classes/coco_91cl_bkgr.txt -O openvino-model/coco_91cl_bkgr.txt && \ sed -i 's/truck/car/g' openvino-model/coco_91cl_bkgr.txt # Get Audio Model and labels -RUN wget -qO cpu_audio_model.tflite https://tfhub.dev/google/lite-model/yamnet/classification/tflite/1?lite-format=tflite +RUN wget -qO cpu_audio_model.tflite https://www.kaggle.com/api/v1/models/google/yamnet/tfLite/classification-tflite/1/download COPY audio-labelmap.txt . From 53a2a865f112f622b56e842503dd101a8489e1de Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Sat, 29 Jun 2024 10:02:30 -0500 Subject: [PATCH 3/3] Live player fixes and improvements (#12202) * Live player fixes and improvements * remove comment * Simplify wording --- docs/docs/configuration/live.md | 14 +- web/src/components/camera/CameraImage.tsx | 19 ++- .../components/player/BirdseyeLivePlayer.tsx | 4 +- web/src/components/player/JSMpegPlayer.tsx | 85 ++++++++---- web/src/components/player/LivePlayer.tsx | 20 +-- web/src/components/player/MsePlayer.tsx | 123 +++++++++++++++--- web/src/pages/UIPlayground.tsx | 4 +- web/src/views/live/DraggableGridLayout.tsx | 45 ++++++- web/src/views/live/LiveDashboardView.tsx | 38 +++++- 9 files changed, 267 insertions(+), 85 deletions(-) diff --git a/docs/docs/configuration/live.md b/docs/docs/configuration/live.md index 42b7be83b..efd970a1d 100644 --- a/docs/docs/configuration/live.md +++ b/docs/docs/configuration/live.md @@ -7,13 +7,15 @@ Frigate intelligently displays your camera streams on the Live view dashboard. Y ## Live View technologies -Frigate intelligently uses three different streaming technologies to display your camera streams. The highest quality and fluency of the Live view requires the bundled `go2rtc` to be configured as shown in the [step by step guide](/guides/configuring_go2rtc). +Frigate intelligently uses three different streaming technologies to display your camera streams on the dashboard and the single camera view, switching between available modes based on network bandwidth, player errors, or required features like two-way talk. The highest quality and fluency of the Live view requires the bundled `go2rtc` to be configured as shown in the [step by step guide](/guides/configuring_go2rtc). -| Source | Latency | Frame Rate | Resolution | Audio | Requires go2rtc | Other Limitations | -| ------ | ------- | ------------------------------------- | -------------- | ---------------------------- | --------------- | ------------------------------------------------ | -| jsmpeg | low | same as `detect -> fps`, capped at 10 | same as detect | no | no | none | -| mse | low | native | native | yes (depends on audio codec) | yes | iPhone requires iOS 17.1+, Firefox is h.264 only | -| webrtc | lowest | native | native | yes (depends on audio codec) | yes | requires extra config, doesn't support h.265 | +The jsmpeg live view will use more browser and client GPU resources. Using go2rtc is highly recommended and will provide a superior experience. + +| Source | Latency | Frame Rate | Resolution | Audio | Requires go2rtc | Other Limitations | +| ------ | ------- | ------------------------------------- | ---------- | ---------------------------- | --------------- | ------------------------------------------------------------------------------------ | +| jsmpeg | low | same as `detect -> fps`, capped at 10 | 720p | no | no | resolution is configurable, but go2rtc is recommended if you want higher resolutions | +| mse | low | native | native | yes (depends on audio codec) | yes | iPhone requires iOS 17.1+, Firefox is h.264 only | +| webrtc | lowest | native | native | yes (depends on audio codec) | yes | requires extra config, doesn't support h.265 | ### Audio Support diff --git a/web/src/components/camera/CameraImage.tsx b/web/src/components/camera/CameraImage.tsx index 7994c19e1..92d0572c1 100644 --- a/web/src/components/camera/CameraImage.tsx +++ b/web/src/components/camera/CameraImage.tsx @@ -26,13 +26,12 @@ export default function CameraImage({ const { name } = config ? config.cameras[camera] : ""; const enabled = config ? config.cameras[camera].enabled : "True"; - const [isPortraitImage, setIsPortraitImage] = useState(false); const [{ width: containerWidth, height: containerHeight }] = useResizeObserver(containerRef); const requestHeight = useMemo(() => { - if (!config || containerHeight == 0) { + if (!config || containerHeight == 0 || !hasLoaded) { return 360; } @@ -40,7 +39,14 @@ export default function CameraImage({ config.cameras[camera].detect.height, Math.round(containerHeight * (isDesktop ? 1.1 : 1.25)), ); - }, [config, camera, containerHeight]); + }, [config, camera, containerHeight, hasLoaded]); + + const isPortraitImage = useMemo(() => { + if (imgRef.current && containerWidth && containerHeight && hasLoaded) { + const { naturalHeight, naturalWidth } = imgRef.current; + return naturalWidth / naturalHeight < containerWidth / containerHeight; + } + }, [containerWidth, containerHeight, hasLoaded]); useEffect(() => { if (!config || !imgRef.current) { @@ -61,13 +67,6 @@ export default function CameraImage({ onLoad={() => { setHasLoaded(true); - if (imgRef.current) { - const { naturalHeight, naturalWidth } = imgRef.current; - setIsPortraitImage( - naturalWidth / naturalHeight < containerWidth / containerHeight, - ); - } - if (onload) { onload(); } diff --git a/web/src/components/player/BirdseyeLivePlayer.tsx b/web/src/components/player/BirdseyeLivePlayer.tsx index 127933f09..235e14785 100644 --- a/web/src/components/player/BirdseyeLivePlayer.tsx +++ b/web/src/components/player/BirdseyeLivePlayer.tsx @@ -12,7 +12,7 @@ type LivePlayerProps = { birdseyeConfig: BirdseyeConfig; liveMode: LivePlayerMode; onClick?: () => void; - containerRef?: React.MutableRefObject; + containerRef: React.MutableRefObject; }; export default function BirdseyeLivePlayer({ @@ -54,6 +54,7 @@ export default function BirdseyeLivePlayer({ width={birdseyeConfig.width} height={birdseyeConfig.height} containerRef={containerRef} + playbackEnabled={true} /> ); } else { @@ -62,6 +63,7 @@ export default function BirdseyeLivePlayer({ return (
; + containerRef: React.MutableRefObject; + playbackEnabled: boolean; onPlaying?: () => void; }; @@ -20,18 +21,21 @@ export default function JSMpegPlayer({ height, className, containerRef, + playbackEnabled, onPlaying, }: JSMpegPlayerProps) { const url = `${baseUrl.replace(/^http/, "ws")}live/jsmpeg/${camera}`; - const playerRef = useRef(null); - const videoRef = useRef(null); + const videoRef = useRef(null); + const canvasRef = useRef(null); const internalContainerRef = useRef(null); const onPlayingRef = useRef(onPlaying); const [showCanvas, setShowCanvas] = useState(false); const selectedContainerRef = useMemo( - () => containerRef ?? internalContainerRef, - [containerRef, internalContainerRef], + () => (containerRef.current ? containerRef : internalContainerRef), + // we know that these deps are correct + // eslint-disable-next-line react-hooks/exhaustive-deps + [containerRef, containerRef.current, internalContainerRef], ); const [{ width: containerWidth, height: containerHeight }] = @@ -83,39 +87,64 @@ export default function JSMpegPlayer({ } }, [scaledHeight, aspectRatio]); - const uniqueId = useId(); - useEffect(() => { onPlayingRef.current = onPlaying; }, [onPlaying]); useEffect(() => { - if (!playerRef.current || videoRef.current) { + if (!selectedContainerRef?.current || !url) { return; } - videoRef.current = new JSMpeg.VideoElement( - playerRef.current, - url, - { canvas: `#${CSS.escape(uniqueId)}` }, - { - protocols: [], - audio: false, - videoBufferSize: 1024 * 1024 * 4, - onPlay: () => { - setShowCanvas(true); - onPlayingRef.current?.(); - }, - }, - ); - }, [url, uniqueId]); + const videoWrapper = videoRef.current; + const canvas = canvasRef.current; + let hasData = false; + let videoElement: JSMpeg.VideoElement | null = null; + + if (videoWrapper && playbackEnabled) { + // Delayed init to avoid issues with react strict mode + const initPlayer = setTimeout(() => { + videoElement = new JSMpeg.VideoElement( + videoWrapper, + url, + { canvas: canvas }, + { + protocols: [], + audio: false, + videoBufferSize: 1024 * 1024 * 4, + onVideoDecode: () => { + if (!hasData) { + hasData = true; + setShowCanvas(true); + onPlayingRef.current?.(); + } + }, + }, + ); + }, 0); + + return () => { + clearTimeout(initPlayer); + if (videoElement) { + try { + // this causes issues in react strict mode + // https://stackoverflow.com/questions/76822128/issue-with-cycjimmy-jsmpeg-player-in-react-18-cannot-read-properties-of-null-o + videoElement.destroy(); + // eslint-disable-next-line no-empty + } catch (e) {} + } + }; + } + // we know that these deps are correct + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [playbackEnabled, url]); return ( -
-
-
+
+
+
(null); // camera activity const { activeMotion, activeTracking, objects, offline } = @@ -73,20 +74,12 @@ export default function LivePlayer({ const [liveReady, setLiveReady] = useState(false); useEffect(() => { - if (!autoLive) { - return; - } - - if (!liveReady) { - if (cameraActive && liveMode == "jsmpeg") { - setLiveReady(true); - } - + if (!autoLive || !liveReady) { return; } if (!cameraActive) { - setTimeout(() => setLiveReady(false), 500); + setLiveReady(false); } // live mode won't change // eslint-disable-next-line react-hooks/exhaustive-deps @@ -181,7 +174,8 @@ export default function LivePlayer({ camera={cameraConfig.live.stream_name} width={cameraConfig.detect.width} height={cameraConfig.detect.height} - containerRef={containerRef} + playbackEnabled={cameraActive || !showStillWithoutActivity} + containerRef={containerRef ?? internalContainerRef} onPlaying={playerIsPlaying} /> ); @@ -194,7 +188,7 @@ export default function LivePlayer({ return (
(WebSocket.CLOSED); const [connectTS, setConnectTS] = useState(0); const [bufferTimeout, setBufferTimeout] = useState(); + const [errorCount, setErrorCount] = useState(0); const videoRef = useRef(null); const wsRef = useRef(null); @@ -117,12 +119,21 @@ function MSEPlayer({ }, [wsURL]); const onDisconnect = useCallback(() => { - if (wsRef.current && wsState == WebSocket.OPEN) { + if (bufferTimeout) { + clearTimeout(bufferTimeout); + setBufferTimeout(undefined); + } + + if ((isSafari || isIOS) && safariPlaying) { + setSafariPlaying(false); + } + + if (wsRef.current && wsState != WebSocket.CLOSED) { setWsState(WebSocket.CLOSED); wsRef.current.close(); wsRef.current = null; } - }, [wsState]); + }, [wsState, bufferTimeout, safariPlaying]); const onOpen = () => { setWsState(WebSocket.OPEN); @@ -162,6 +173,26 @@ function MSEPlayer({ reconnect(); }; + const sendWithTimeout = (value: object, timeout: number) => { + return new Promise((resolve, reject) => { + const timeoutId = setTimeout(() => { + reject(new Error("Timeout waiting for response")); + }, timeout); + + send(value); + + // Override the onmessageRef handler for mse type to resolve the promise on response + const originalHandler = onmessageRef.current["mse"]; + onmessageRef.current["mse"] = (msg) => { + if (msg.type === "mse") { + clearTimeout(timeoutId); + if (originalHandler) originalHandler(msg); + resolve(); + } + }; + }); + }; + const onMse = () => { if ("ManagedMediaSource" in window) { const MediaSource = window.ManagedMediaSource; @@ -169,10 +200,22 @@ function MSEPlayer({ msRef.current?.addEventListener( "sourceopen", () => { - send({ - type: "mse", - // @ts-expect-error for typing - value: codecs(MediaSource.isTypeSupported), + sendWithTimeout( + { + type: "mse", + // @ts-expect-error for typing + value: codecs(MediaSource.isTypeSupported), + }, + 3000, + ).catch(() => { + if (wsRef.current) { + onDisconnect(); + } + if (isIOS || isSafari) { + onError?.("mse-decode"); + } else { + onError?.("startup"); + } }); }, { once: true }, @@ -187,9 +230,21 @@ function MSEPlayer({ "sourceopen", () => { URL.revokeObjectURL(videoRef.current?.src || ""); - send({ - type: "mse", - value: codecs(MediaSource.isTypeSupported), + sendWithTimeout( + { + type: "mse", + value: codecs(MediaSource.isTypeSupported), + }, + 3000, + ).catch(() => { + if (wsRef.current) { + onDisconnect(); + } + if (isIOS || isSafari) { + onError?.("mse-decode"); + } else { + onError?.("startup"); + } }); }, { once: true }, @@ -260,10 +315,6 @@ function MSEPlayer({ return () => { onDisconnect(); - if (bufferTimeout) { - clearTimeout(bufferTimeout); - setBufferTimeout(undefined); - } }; // we know that these deps are correct // eslint-disable-next-line react-hooks/exhaustive-deps @@ -305,6 +356,23 @@ function MSEPlayer({ videoRef.current.requestPictureInPicture(); }, [pip, videoRef]); + // ensure we disconnect for slower connections + + useEffect(() => { + if (wsState === WebSocket.OPEN && !playbackEnabled) { + if (bufferTimeout) { + clearTimeout(bufferTimeout); + setBufferTimeout(undefined); + } + + setTimeout(() => { + if (!playbackEnabled) onDisconnect(); + }, 10000); + } + // we know that these deps are correct + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [playbackEnabled]); + return (