Home Assistant custom component for real-time viewing of almost any camera stream using WebRTC, MSE and other technologies, powered by the go2rtc streaming server.
About this fork
This is a hardened fork of AlexxIT/WebRTC, maintained by @fuzzybear62 and validated on a multi-camera fleet running over lossy multi-hop Wi-Fi repeater paths and remote tunnels.
The fork keeps 100% of the upstream card/config surface and adds two things: a seamless, reversible MSE→WebRTC upgrade path that never permanently harms a working stream to chase a better one, and a set of new user-facing features upstream doesn't have — dual grid/fullscreen streams, overlay indicators, browser-side ICE, tap actions, diagnostic sensors, and more. Highlights is the one-screen tour; What this fork adds is the deep dive.
Maintained as a personal project for my own multi-camera fleet. Issues and fixes are welcome, but support is best-effort and not guaranteed — see SUPPORT.
- Highlights
- What this fork adds
- go2rtc
- Installation
- Configuration
- Custom card
- Fork card options
- Dual stream: grid vs fullscreen (
url_fullscreen) - Diagnostic sensors
- Upstream issues & PRs addressed
- Templates
- Two-way audio
- Snapshots to Telegram
- Cast or share stream
- Stream to camera
- FAQ
- Debug
- Debug logging (troubleshooting stream loss)
- Known work cameras
The one-screen tour of what this fork gives you over plain upstream. Two categories: the reliability
engine (automatic — a plain type: custom:webrtc-camera card gets all of it, no options) and the new
user-facing features (opt-in card options). Each links to its full section.
Reliability & resilience — automatic, zero config:
- Seamless, reversible MSE→WebRTC upgrade. A reliable picture shows immediately (MSE); low-latency WebRTC is tried in the background and swapped in only after it proves itself, never producing a black frame or flicker. → details
- Stays up on bad networks. Congested streams are nursed instead of killed, frozen-but-fed pictures are nudged back to live, weak links are protected from the upgrade's own load, and cameras are serialized so a wall of tiles cooperates instead of competing for one uplink. → details
- Upstream bugs fixed & PRs folded in — crashes, iOS 1 fps, HA-compat breaks, auth/ICE, media_player. → list
New user-facing features — not in upstream (or fixed here):
| Feature | What it gives you |
|---|---|
Dual stream url_fullscreen |
A light substream in the dashboard grid, automatically swapped to a full-res stream only while fullscreen — big bandwidth/CPU win on camera walls and mobile. |
| Diagnostic sensors | Two sensor entities showing how many cameras are streaming and whether the background upgrade path is healthy — usable in dashboards and automations. |
tap_action |
Run any Home Assistant action (navigate, call-service, more-info, URL…) when you tap the video. |
live_indicator |
A liveness dot that shows at a glance whether the tile is actually receiving live video. |
network_indicator |
A network-state dot exposing the fork's own link/health assessment for the tile. |
Browser-side ice_servers |
Configure your own STUN/TURN in the card, and use Home Assistant's own ICE servers (incl. rotating Nabu Casa TURN). Two default public STUN servers so one blocked provider isn't fatal. |
unmute_in_fullscreen |
Automatically unmute while fullscreen and restore the muted state on exit (iOS + desktop). |
digital_ptz persistence & spinner options |
Remembered digital pan/tilt/zoom, plus a configurable loading spinner. |
media_player fixes |
Working play_media via go2rtc, volume_entity, and fire-dom-event shortcuts. |
Tuning knobs for the reliability engine are in Fork card options. Reading the logs is covered by DIAGNOSTICS.md.
First, a bit of background (no code required). A browser can show a camera in two main ways:
- MSE (Media Source Extensions) — video streamed over a normal WebSocket. It is robust: it works through firewalls, reverse proxies and remote tunnels, and it degrades gracefully. Its downside is higher latency (typically 1–3 s behind live).
- WebRTC — the same peer-to-peer tech used by video calls. It is low-latency (sub-second) but fragile: it needs UDP and a successful "handshake" that flaky Wi-Fi, TURN relays or restrictive networks often break.
So the ideal is: always show something reliable, but quietly upgrade to the fast path whenever the network actually allows it — and never make the picture worse in the attempt.
Upstream picks whichever technology connects first, and historically a failed WebRTC attempt could disturb an already-working MSE picture. On a clean LAN you never notice. On degraded paths (Wi-Fi repeaters, TURN relays, remote tunnels, briefly-blocked UDP) it causes black tiles and flicker.
This fork reworks that upgrade into an explicit, reversible process. In plain terms:
- MSE comes up first and stays on screen. You get a reliable picture immediately, and the card never tears it down just to experiment with something faster.
- WebRTC is tried invisibly, in the background. A throwaway second connection (internally called a shadow) attempts WebRTC off-screen. Your visible MSE picture is never touched while this happens.
- The switch only happens once WebRTC has proven itself. The background WebRTC feed must play
without a single gap for
rtc_swap_prove_ms(default 30 s) before it is allowed to replace the visible stream. If it stutters before then, it is silently thrown away — no black frame, no flicker, you stay on MSE. On a good network the switch is seamless and you simply get lower latency. - It keeps trying, forever, safely. If a camera only landed on MSE because WebRTC happened to be
unavailable at that moment (UDP briefly blocked, TURN down, a momentary ICE timeout), a background
re-probe loop retries later with increasing spacing (30 s → 1 min → … → 10 min) and upgrades the
moment the network recovers — again without ever disturbing the live picture. Turn it off with
rtc_reprobe: false.
The one rule behind all of it: never permanently harm a working stream in order to chase a better one. A camera that can only reach MSE stays on MSE, cleanly, indefinitely.
Everything above is automatic — a plain config (type: custom:webrtc-camera, url: …) gets it all
with no extra options. The knobs in Fork card options only let you tune the
behaviour; you never need them to benefit from it.
Unlike upstream, this fork also creates two diagnostic sensors so you can watch, at a glance, how many cameras are streaming and whether the background upgrade path is working — see Diagnostic sensors.
The reversible upgrade above is about reaching the fast path. The second half of this fork is about staying up — keeping a picture on screen through hours-long sessions and through genuinely bad networks (a phone on 4G, several cameras sharing one saturated uplink, a Wi-Fi repeater that drops packets). All of this is automatic and needs no configuration — the options in Fork card options only tune it; a plain card gets all of it.
- A congested MSE stream is nursed, not killed. Upstream tears the WebSocket down after a fixed no-data timeout and reconnects — which, on an already-saturated link, just adds load and can spiral into a reconnect storm across every camera on the page. This fork instead measures the link and, while it is congested, extends the patience window and rides out short blackouts (a grid brown-out, a repeater hiccup) rather than reconnecting. A stream that is merely slow is left alone; only a stream that is genuinely dead is reaped.
- A frozen-but-fed picture is nudged back to live instead of being torn down. If the socket is still delivering bytes but the picture has stalled at the buffer edge, the card seeks it to the live edge — a non-destructive recovery that avoids a full reconnect.
- Weak mobile links are protected from the upgrade itself. On a narrow link, the background WebRTC probe (and its keep-warm MSE) is additive uplink load that can starve the very MSE feed you are watching. If the fork sees a camera repeatedly reach for WebRTC and fall back, it latches that camera to MSE-only for a while, stopping the probing that was doing the harm. It re-tests later; a healthy link never triggers the latch.
- Cameras cooperate instead of competing. WebRTC probes across all cameras on the page are serialized through a shared gate, so a wall of tiles doesn't fire a dozen simultaneous negotiations into one uplink. And if any camera's probe reveals the path itself is blacked out, RTC is briefly suppressed grid-wide so the others don't repeat the same doomed attempt.
The net effect: on a clean LAN you get seamless sub-second WebRTC; on a pathological link you get a stubborn, self-throttling MSE picture that stays up instead of flickering — from the same card, with no per-network tuning. The full list of upstream bugs and PRs this fork folds in is in Upstream issues & PRs addressed.
The user-facing features that complement this engine — dual stream, overlay indicators, browser-side ICE, tap actions and more — are summarized in Highlights; the tuning knobs are in Fork card options.
This component uses the go2rtc application as a streaming server:
- lowest possible streaming latency for many supported protocols
- streaming from RTSP, RTMP, HTTP (FLV/MJPEG/JPEG), HomeKit cameras, USB cameras and other sources
- streaming to RTSP, WebRTC, MSE/MP4 or MJPEG
- support popular codec H264/H265, AAC, PCMU/PCMA, OPUS
- on-the-fly transcoding for unsupported codecs via FFmpeg
- autoselect streaming technology based on stream codecs, browser capabilities, network configuration
Read more in the go2rtc docs!
You can install go2rtc in several ways:
- Basic users - this component will automatically download and run the latest version of go2rtc, you don't need to do anything yourself.
- Advanced users - install the go2rtc or Frigate 12+ add-on.
- Hackers - install go2rtc as binary or Docker on any server in LAN.
You can change the go2rtc settings by adding the go2rtc.yaml file to your Hass configuration folder.
Defining a low-res substream + a full-res stream (for url_fullscreen).
Give each camera two named streams in go2rtc.yaml — a light one for the grid and the full-resolution
one for fullscreen. Many cameras publish both a main and a sub RTSP profile natively; otherwise let
go2rtc downscale with FFmpeg:
streams:
# Full-resolution stream — used only when the card is fullscreen
camera: rtsp://user:pass@192.168.1.123:554/Streaming/Channels/101
# Low-res substream — shown in the dashboard grid
camera_sub:
- rtsp://user:pass@192.168.1.123:554/Streaming/Channels/102 # native sub-profile if the camera has one
# or transcode down from the main stream instead of a native sub-profile:
# - "ffmpeg:camera#video=h264#width=640#hardware"Then point the card at both: url: camera_sub and url_fullscreen: camera (see
the card feature).
Important. go2rtc runs its own web interface on port 1984 without a password. There you can see a
list of active camera streams. Anyone on your LAN can access them without a password. You can disable
this in the go2rtc config.
Important
A true drop-in replacement — by design. This fork deliberately keeps the same webrtc
integration domain, the same webrtc-camera card, and the same file paths as
AlexxIT/WebRTC. That is a design choice, not an oversight: it
means you migrate without re-creating anything. Your existing config entry, entity IDs, camera
definitions, dashboard cards and automations keep working untouched — swapping the code underneath
them does not force you to redefine your setup. (A renamed fork with its own domain would orphan
every entity and every card that references webrtc, forcing a full rebuild — exactly what this
avoids.)
The one consequence: because the domain is shared, HACS treats this and the upstream repo as the
same installation slot, and won't track both at once. So if you already have AlexxIT/WebRTC
installed via HACS, remove it there first (HACS > the WebRTC Camera entry > ⋮ > Remove), then add
this fork. That removal is HACS-only — it just changes which source HACS pulls updates from; it
does not delete your config entry, entities or dashboards, and they all come back after you
install the fork below and restart HA. No need to hand-delete custom_components/webrtc either — the
install overwrites it in place.
Method 1 (HACS custom repository). HACS > Integrations > ⋮ > Custom repositories > add
https://github.com/fuzzybear62/webrtc as an Integration > install WebRTC Camera > restart
Home Assistant.
Method 2 (manual). Copy the custom_components/webrtc folder from this repo into your
/config/custom_components folder and restart Home Assistant.
Additional steps if you are using the UI in YAML mode: add card to resources
The custom_card will be automatically registered with the Home Assistant UI, except when you are
managing the UI in YAML mode. In that case add this to your UI resources for the
custom:webrtc-camera card to work:
url: /webrtc/webrtc-camera.js
type: module- Refresh your browser
Settings > Devices & Services > Add Integration > WebRTC Camera
If the integration is not in the list, you need to clear the browser cache.
The component adds two services (webrtc.create_link, webrtc.dash_cast), one lovelace custom
card, and — specific to this fork — two diagnostic sensor entities (see
Diagnostic sensors). It does not create camera entities; use your existing Hass
cameras or go2rtc stream names as the card source.
As a url you can use:
- any protocol supported by go2rtc (
rtsp,rtmp,http,onvif,dvrip,homekit,roborock, etc.) - stream
namefrom the go2rtc config Jinja2template (should render supported protocol or streamname)
As an entity you can use almost any camera from Hass.
As a poster you can use:
http-link (should be publicly available link)- camera
entityfrom Hass - stream
namefrom the go2rtc config Jinja2template (should render cameraentityor streamname)
Minimal
type: 'custom:webrtc-camera'
url: 'rtsp://rtsp:12345678@192.168.1.123:554/av_stream/ch0'or
type: 'custom:webrtc-camera'
url: 'camera1' # stream name from go2rtc.yamlor
type: 'custom:webrtc-camera'
entity: camera.generic_stream # change to your camera entity_idor
type: 'custom:webrtc-camera'
streams:
- url: go2rtc_stream_hd
name: HD # name is optional
mode: webrtc # mode is optional
media: video # media is optional
- url: go2rtc_stream_sd
name: SD
mode: mse
media: audioPS. You can change the active stream by clicking on the mode label. Or by clicking on the stream
name with enabled ui: true.
Full
All settings are optional! Only required setting - url or entity or streams.
type: 'custom:webrtc-camera'
url: 'rtsp://rtsp:12345678@192.168.1.123:554/av_stream/ch0'
entity: camera.generic_stream
mode: webrtc,mse,hls,mjpeg # stream technology, default: webrtc,mse,hls,mjpeg
media: video,audio # select only video or audio track, default both
server: http://192.168.1.123:1984/ # custom go2rtc server address, default empty
ui: true # custom video controls, default false
digital_ptz: # digital zoom and pan via mouse/touch, defaults:
mouse_drag_pan: true
mouse_wheel_zoom: true
mouse_double_click_zoom: true
touch_drag_pan: true
touch_pinch_zoom: true
touch_tap_drag_zoom: true
persist: true # zoom factor and viewport position survive page reloads
# digital_ptz: false # to disable all mouse/touch digital zoom and pan
title: My super camera # optional card title
poster: https://home-assistant.io/images/cast/splash.png # still image when stream is loading
muted: true # initial mute toggle state, default is false (unmuted)
intersection: 0.75 # auto stop stream when less than 75% of video element is in the screen, 50% by default
background: true # keep the stream running when off-screen / tab hidden (this fork defaults to true)
shortcuts: # custom shortcuts, default none
- name: Record
icon: mdi:record-circle-outline
service: switch.toggle
service_data:
entity_id: switch.camera_recordA shortcut may also url: (opens a link), more_info: (opens the more-info dialog of the given
entity id), or use the special service: fire-dom-event (fork #940) to dispatch a Lovelace
ll-custom DOM event — useful e.g. to close the current browser_mod popup. The whole shortcut item
becomes the event detail, so your browser_mod: / custom keys pass through:
shortcuts:
- name: Close
icon: mdi:close
service: fire-dom-event
browser_mod:
command: close_popupPan, tilt, zoom controls: PTZ config examples.
Paused by default
type: custom:webrtc-camera
poster: dahua1-snap # stream name from go2rtc.yaml (http-snapshot)
streams:
- url: '' # empty url, so only poster will be shown
- url: dahua1 # stream name from go2rtc.yaml (rtsp-stream)Video aspect ratio issue
style: "video {aspect-ratio: 16/9; object-fit: fill;}"Video rotation
- On client (free CPU):
style: 'video {transform: rotate(90deg); aspect-ratio: 1}'
- On server - FFmpeg transcoding (high CPU cost)
Hide UI elements
style: '.mode {display: none}' # hide mode label
style: '.fullscreen {display: none}' # hide fullscreen button
style: '.screenshot {display: none}' # hide screenshot button
style: '.pictureinpicture {display: none}' # hide PIP buttonReposition UI elements
# shortcuts to the top-right, stacked vertically
style: ".shortcuts {left: unset; top: 25px; right: 5px; display: flex; flex-direction: column}"
# PTZ to the left
style: ".ptz {right: unset; left: 10px}"
# mode label to the bottom
style: '.header {bottom: 6px} .mode {position: absolute; bottom: 0px}'
# header line to the bottom
style: '.header {top: unset; bottom: 6px}'These options are specific to this fork and tune the reversible MSE→WebRTC upgrade described in What this fork adds. You do not need any of them — the defaults are chosen to behave well on bad networks. They exist only for fine-tuning or diagnostics.
| Option | Type | Default | In one line |
|---|---|---|---|
rtc_swap_prove_ms |
number | 30000 (ms) |
How long the background WebRTC feed must run flawlessly before it replaces the visible stream. |
firstframe_timeout |
number | 120000 (ms) |
How long to wait for the very first video frame before declaring an attempt dead. |
network_strict |
boolean | false |
How aggressively to drop a connection on a low-level socket error. |
rtc_reprobe |
boolean | true |
Whether to keep retrying WebRTC in the background after a camera settled on MSE. |
rtc_reprobe_base |
number | 30000 (ms) |
First wait between those background retries. |
rtc_reprobe_max |
number | 600000 (ms, 10 min) |
Longest wait between those background retries. |
pause_delay |
number | 5000 (ms) |
Grace period before pausing a scrolled-away camera (only with background: false). |
type: 'custom:webrtc-camera'
url: 'rtsp://rtsp:12345678@192.168.1.123:554/av_stream/ch0'
# Fork tuning — every line is optional
rtc_swap_prove_ms: 30000 # prove WebRTC for 30s before switching to it
firstframe_timeout: 120000 # give an attempt 2 min to produce a first frame
network_strict: false # keep the working stream, recover in the background
rtc_reprobe: true # keep trying to upgrade an MSE-only cameraPer-stream overrides: rtc_swap_prove_ms, firstframe_timeout and network_strict can also be set
inside an individual streams: entry, where they override the card-level value for that one stream.
rtc_swap_prove_ms — the "prove it first" timer.
When WebRTC is being tried in the background, this is how long it must play without a single glitch
before the card trusts it enough to put it on screen in place of MSE. It is your safety-vs-speed dial:
- Raise it (e.g.
60000) if you have flaky cameras that connect via WebRTC but then stutter — a longer proof window means the card only switches to WebRTC that is genuinely stable. - Lower it (e.g.
10000) on a fast, reliable LAN where you want the low-latency picture sooner.
firstframe_timeout — the "is anything coming?" timer.
Any connection attempt (the visible one or a background WebRTC probe) is given this long to produce
its first decoded video frame. If nothing arrives, the attempt is considered dead and cleaned up. Note
that a connection can look "connected" at the network level while never actually delivering video — this
timer is what catches that case. Lower it to give up on dead or misconfigured sources faster; the
default (2 min) is deliberately generous so slow-to-start cameras are not killed prematurely.
network_strict — how twitchy to be about socket errors.
This controls one narrow thing: what to do when the underlying WebSocket reports a low-level error.
-
false(default, recommended): relaxed. A transient socket error is logged and ignored; if the connection has really died the browser will report it a moment later through the normal channel, and the fork's own 5-second no-data watchdog catches any silent freeze regardless. This is the safer choice because it won't tear down an otherwise-healthy picture over a momentary blip. -
true: fail-fast. The connection is dropped the instant any socket error is seen. This does not make recovery more reliable — the mechanisms above already recover on their own — it just reacts a few milliseconds sooner. Its real use is diagnostics: it surfaces flaky links immediately instead of letting the watchdog absorb them. For everyday viewing, leave itfalse.On a background WebRTC probe this setting has practically no effect: a probe is disposable and is reaped by
firstframe_timeout/the re-probe loop anyway, and — by design — it can never disturb the live picture.
rtc_reprobe (+ rtc_reprobe_base / rtc_reprobe_max) — the background retry loop.
When a camera ends up on MSE because WebRTC wasn't available at that moment, this loop keeps quietly
retrying WebRTC so the camera can be upgraded later when the network improves. Retries start spaced by
rtc_reprobe_base and back off up to rtc_reprobe_max, so a permanently WebRTC-incapable network
settles into one cheap probe every 10 minutes rather than hammering. Set rtc_reprobe: false to switch
the loop off entirely (the camera then keeps whatever it first connected with).
pause_delay — don't tear down on a quick scroll.
Only relevant if you set background: false (which stops a camera when it scrolls off-screen or the tab
is hidden). This is the grace period before that teardown actually fires, so flicking past a camera
doesn't needlessly kill and restart its stream.
The defaults are a good all-round compromise. If you want to squeeze the best behaviour out of a specific camera, the guiding idea is that each knob should lean in opposite directions depending on how good the path is:
- Wired / strong LAN — the path is reliable, so you can afford to be fast and confident: switch to the low-latency WebRTC picture sooner, and give up on genuinely dead sources sooner.
- Distant camera behind several Wi-Fi repeater hops — the path is jittery and lossy, so you must be patient and conservative: only switch to WebRTC once it has proven rock-solid, never kill a slow-starting attempt prematurely, and above all never tear down the working MSE picture over a blip.
| Option | Wired / strong LAN | Weak path (e.g. 3× Wi-Fi repeater hops) | Why |
|---|---|---|---|
rtc_swap_prove_ms (def. 30000) |
15000 (down to 10000) | 60000 (up to 90000) | The anti-flicker filter. On a good path WebRTC that starts will hold, so a shorter proof gets you to low latency sooner. On a weak path WebRTC can run 20–30 s and then stall — a long proof window lets only a genuinely stable feed take over, killing the swap→stall→revert churn. |
firstframe_timeout (def. 120000) |
60000 | 120000 (default — do not lower) | On a good path the first frame arrives fast, so you can drop a dead source quicker. On a lossy path the first keyframe can genuinely take a while; lowering this would reap attempts that would have succeeded. |
network_strict (def. false) |
false | false | On a flaky link, transient socket errors are expected and self-heal, so true would needlessly tear down a healthy picture. On wired, true only saves a few milliseconds. Leave it false either way — it is a diagnostics toggle, not a reliability one. |
rtc_reprobe / _base / _max |
defaults | rtc_reprobe_base: 60000 |
A good path usually reaches WebRTC and the loop stops on its own. On a known-marginal path, probing a little less often trims wasted background work. |
Ready-to-paste starting points (add to the rest of your card config):
# Wired / strong LAN — bias toward speed & low latency
rtc_swap_prove_ms: 15000 # a stable wired WebRTC: switch over sooner
firstframe_timeout: 60000 # first frame is fast here: drop dead sources sooner
network_strict: false# Distant camera, ~3 Wi-Fi repeater hops — bias toward stability & zero churn
rtc_swap_prove_ms: 60000 # only adopt WebRTC after 60s of flawless play
firstframe_timeout: 120000 # lossy link: give the first keyframe time (don't lower)
network_strict: false # a transient socket error must NOT drop the working MSE
rtc_reprobe_base: 60000 # optional: probe a marginal path a little less oftenA note on committing (releasing MSE). There is a third, deliberately non-exposed timer,
RTC_COMMIT_MS (180 s of flawless WebRTC), after which the card commits: it releases the background
MSE stream and closes its connection. This is intentional and self-protecting:
- On a wired camera it will be reached — after ~3 stable minutes the card commits to WebRTC and frees the resources. Correct.
- On a weak path it will almost never be reached (staying 180 s without a single gap is unlikely), so the camera stays reversible indefinitely — if WebRTC stalls it snaps back to the warm MSE with no black frame. Exactly what you want there.
Verifying it in operation. These are reasoned starting points, not per-camera measurements — set them, then watch two signals:
- The
shadow_probessensor. On the weak camera it should tick0↔1while the picture stays on MSE without visible flicker. If you see the tile flick to black and back (a swap that immediately reverts), raisertc_swap_prove_msfurther. - The browser console (with the card loaded, look for
[VideoRTC:…] RTC phase …lines). The time it takes to reach thepromotedphase is a path-quality canary: fast (~2–4 s) means WebRTC will adopt cleanly; slow (>10 s) means the long proof window is correctly doing its job filtering a marginal path. On a wired camera you should also see the phase reachcommittedafter ~3 minutes, and that camera'sproxied_connectionscontribution drop as the MSE connection closes.
Two small, opt-in UI features this fork adds to the card (both are false/absent by default).
Set live_indicator: true to show a small dot in the top-right controls that is green while frames
are actually being presented and red when the video has silently frozen. It is driven by
requestVideoFrameCallback on the current driver's <video> (so it also catches freezes that emit no
waiting event) plus a 500 ms watchdog, and it is re-bound automatically on every stream/driver swap.
It is purely visual — it does not change the stream or the upgrade logic.
type: custom:webrtc-camera
url: mycamera
live_indicator: trueSet tap_action to run a standard Home Assistant action when you tap the video. It uses the usual
Lovelace action shape (more-info, navigate, url, toggle, perform-action / call-service,
fire-dom-event, none).
The tap is gated so it never steals a digital-PTZ gesture: it fires only on a clean single-finger tap that doesn't move. A pinch (two fingers), a pan/drag (movement > ~10 px), and taps on the control overlay (shortcuts, PTZ, buttons) are all ignored — so pinch-zoom and pan keep working exactly as before.
⚠️ This card is stream-based, not entity-based. In the go2rtc spirit,url: mystreampoints at a go2rtc stream, which is not a Home Assistant entity. So a bareaction: more-infohas no entity to open and does nothing. You have two options:
1. Name the entity explicitly (works even though the card itself has no entity):
tap_action:
action: more-info
entity: switch.cancello_interno # required — the card provides no entity of its own2. Use an action that needs no entity — usually the more natural choice for a stream-only card:
# Open a gate straight from the video
tap_action:
action: perform-action
perform_action: switch.turn_on
target:
entity_id: switch.cancello_interno# Jump to another dashboard view
tap_action:
action: navigate
navigation_path: /lovelace/camerasThe action executor also accepts the older call-service name and the service + service_data
shape (the same shape used by shortcuts:), so this is equivalent to the perform-action example above:
tap_action:
action: call-service
service: switch.turn_on
service_data:
entity_id: switch.cancello_internoIf you configure the card with entity: camera.foo instead of url:, then a bare action: more-info
works because the card does have an entity to fall back to (resolution order:
tap_action.entity → card entity: → the current stream's entity).
A high-resolution stream that is perfect fullscreen is wasteful — and on a weak link outright harmful — when the camera is just one small tile in a dashboard grid. This fork lets you point the card at two streams: a light substream for normal (grid / thumbnail) viewing, and a high-res stream that is swapped in only while the card is fullscreen, then swapped back out on exit.
type: 'custom:webrtc-camera'
url: 'camera_sub' # low-res substream — shown in the grid / normal view
url_fullscreen: 'camera' # full-res stream — used only while fullscreen
ui: true # needed for the fullscreen buttonurl_fullscreen takes the same kind of value as url — a go2rtc stream name (recommended; define
both streams in go2rtc.yaml, see go2rtc) or a direct source URL.
Why this matters
- Bandwidth & CPU where it counts. A 9-camera wall pulls nine light substreams; only the tile you open fullscreen ever pulls the heavy high-res feed. On mobile / 4G this is the difference between a fluid grid and a stalled one.
- It reuses the full resilience stack. The fullscreen swap is a clean cold-restart of the main driver onto the high-res stream, so the reversible MSE→WebRTC upgrade, the adaptive watchdog and the congestion logic all apply to it exactly as they do to the normal stream — you are not dropping to a dumber player for fullscreen.
- Fully automatic revert. Leaving fullscreen restores the substream on its own; nothing to wire up.
Caveats
- iOS. On iOS/iPadOS the fullscreen control is bound by WebKit to the raw
<video>element (webkitEnterFullscreen), which the card cannot restart underneath — so iOS keeps showing the substream in fullscreen. The swap works on the desktop and Android PWA path, where fullscreen is taken on the card container. If your primary viewer is an iPhone, size the substream for the quality you want fullscreen (or just use a singleurl). - One extra negotiation. Entering and leaving fullscreen each trigger one clean reconnect (a brief reconnect, not a black grid), because the two streams are separate go2rtc sources.
- Omit
url_fullscreenand the card behaves exactly as before — single stream, no swap.
With ui: true the card draws its control overlay (fullscreen, screenshot, picture-in-picture,
play/pause, volume). These fork options tune that overlay:
| Option | Type | Default | What it does |
|---|---|---|---|
unmute_in_fullscreen |
boolean | false |
(#953) Unmute the stream while it is fullscreen, then restore the previous muted state when fullscreen ends. Works on iOS (webkitendfullscreen) and desktop (fullscreenchange). |
network_indicator |
boolean | false |
Draw a small network-state dot on the tile that mirrors the driver's own live link assessment: it tracks the measured bandwidth band, turns red when the camera has been latched to MSE-only on a weak link, and fades out when no signal has arrived recently. A fork-only, at-a-glance view of what the resilience stack is doing per camera. |
spinner |
boolean | true |
(#924) Set false to remove the loading spinner entirely. |
spinner_delay |
number | 0 (ms) |
(#924) Delay before the spinner appears on a waiting event, so a brief stall doesn't flash it. |
type: custom:webrtc-camera
url: mycamera
ui: true
unmute_in_fullscreen: true
spinner_delay: 800 # only show the spinner if a stall lasts > 0.8s
# spinner: false # …or drop it altogetherThe play/pause control (#913) is a toggle: it stays visible under ui: true and mirrors the
element state, so you can freeze the live picture and resume it (resuming re-seeks to the live edge).
A local still image can be used as the poster (#949) — any HA-served path works, since a poster
that starts with / (or contains ://) is passed straight to the <video> element:
poster: /local/mycamera.png # file in <config>/www/mycamera.pngBy default the card offers the browser two independent public STUN servers — Google
(stun:stun.l.google.com:19302) and Cloudflare (stun:stun.cloudflare.com:3478) (fork #915). STUN is
enough to discover your public address and set up a direct browser↔go2rtc WebRTC path on most
networks; using two providers means a blocked, filtered or down one (some ISPs/countries block Google)
doesn't stop discovery. This default applies to every camera. STUN is not enough when the home
end is behind CGNAT or a symmetric NAT with no reachable public endpoint — there the browser needs a
TURN relay, which you must supply yourself.
ice_servers (fork #952) lets you supply your own STUN/TURN list, which fully replaces the default on
the browser's RTCPeerConnection (applied to every internal driver, main and shadow — it survives the
MSE↔WebRTC driver swaps). It accepts the standard RTCIceServer shape — a bare string, a list of
strings, or a list of objects with urls / username / credential:
type: custom:webrtc-camera
url: mycamera
ice_servers:
- urls: stun:stun.cloudflare.com:3478
- urls: turn:turn.example.com:3478
username: myuser
credential: mysecretPrivacy opt-out (zero third parties). Setting an explicit empty list removes all default STUN servers, so the browser contacts no external provider. WebRTC then still works on the LAN (host candidates), while remote access simply falls back to the reliable MSE path. It costs nothing and is purely for those who don't want any third-party STUN:
ice_servers: [] # no STUN/TURN at all — no Google, no CloudflareNote: this is only meaningful as an explicit
ice_servers: []. Omittingice_serverskeeps the automatic behaviour below; a malformed non-empty list is treated as a typo and also keeps the default (it is not silently wiped) — and that case is logged atwarning(ice-config, visible in Settings → System → Logs without enabling debug), so a fat-fingered STUN/TURN URL is easy to spot. The resolved ICE source in effect is also mirrored to the log atdebug(ice/ice-ha, card sub-logger).
Home Assistant's own ICE servers (automatic, incl. Nabu Casa TURN — fork #923). When you do not
set a per-card ice_servers, the card automatically reuses Home Assistant's own ICE list — anything
you put under the core webrtc: config plus any cloud-provided TURN (Nabu Casa / Homeway) — fetched
once via HA's native web_rtc/ice_servers command. This matters because Nabu Casa's TURN credentials are
short-lived and rotating, so they cannot be pasted into ice_servers by hand — this is the only way a
CGNAT / symmetric-NAT home gets a working relay, and Nabu Casa subscribers get it for free with zero
config. It fails soft: on an HA too old to expose the command, the card just keeps the built-in STUN
default.
Precedence, most specific wins:
- per-card
ice_servers(#952) — fully replaces everything, including the[]privacy opt-out; - HA native ICE (#923) — the
webrtc:config + Nabu Casa/Homeway TURN, used automatically when (1) is unset; - built-in 2×STUN default (#915) — Google + Cloudflare, used when HA exposes no ICE servers.
All of the above is the browser side only — it does not touch how go2rtc (the home peer) gathers its own candidates. A per-card
ice_serversentry is something you run and pay for yourself; the HA native path (2) is the free automatic one. If you only have public-IP or port-forwarded access, STUN is all you need; a TURN relay is required only for CGNAT/symmetric-NAT homes.
This fork registers two number sensors under a WebRTC Camera device (upstream creates none). Every camera tile talks to Home Assistant through a small connection on the go2rtc proxy; these two sensors simply count those connections, split by purpose. They are useful for confirming the background upgrade path is actually running, and for spotting leaks (a count that never returns to zero when all cameras are closed).
| Sensor | What it counts | Think of it as |
|---|---|---|
sensor.proxied_connections — Proxied Connections |
Visible camera streams currently open (the "main" connections that carry MSE video). | How many camera tiles are streaming right now. |
sensor.shadow_probes — Shadow Probes |
Background WebRTC attempts in flight (the throwaway "shadow" connections — signalling only, no picture). | How many cameras are quietly trying to upgrade to WebRTC. |
How to read them:
- A camera showing MSE and trying to upgrade will make
shadow_probestick up to 1 while it probes, then back to 0 — repeatedly, spaced further apart over time (the re-probe backoff). Seeing it oscillate is normal and healthy, not a bug: it means the fork is doing its job. - When a camera successfully upgrades to WebRTC, its probe finishes and
shadow_probesdrops back. proxied_connectionsroughly tracks the number of tiles on screen. Note that a camera which fully commits to WebRTC eventually closes its proxy connection (the video then flows peer-to-peer, off the proxy), so a committed-WebRTC tile can read as 0 here even though you can see it — that is expected.
Upstream AlexxIT/WebRTC has not merged a PR or triaged an issue in roughly nine months. This fork carries fixes for the outstanding reports that matter in production. The table below summarizes each upstream report and how this fork resolves it.
Status legend: fixed (root-caused and resolved here) · mitigated (symptom removed; root cause is outside the card, e.g. a codec limit) · already immune (fork's modern code never had the bug).
Searching for a symptom? In plain language, this fork resolves: a camera that freezes or constantly buffers on a multi-camera dashboard or in Safari (
InvalidStateError); a stream that plays in slow motion — roughly one frame every few seconds — on iPhone or iPad after updating to iOS 26.1; a black tile after the stream switches technology (MSE↔WebRTC); themedia_playerplatform failing to load after a Home Assistant update (cannot import name 'SUPPORT_PLAY_MEDIA','LovelaceData' object is not subscriptable); a camera whose reconnects get the client IP-banned by Home Assistant (repeated401); WebRTC that never connects behind a blocked STUN/TURN provider; and the"webrtc-camera" has already been used with this registryconsole error. If any of these is what you're seeing, the exact fix is in the table below.
| Upstream | Symptom | In this fork |
|---|---|---|
| #886, #901, #933, PR #938 | InvalidStateError / constant buffering & freezing on Safari and multi-camera dashboards |
fixed — the whole MSE updateend block (including the unguarded sb.buffered access upstream leaves exposed) is wrapped so a teardown race can't throw. |
| #910, #884 | video plays in slow motion / stutters at ~1 frame every 3 s on iPhone & iPad (iOS 26.1) after upstream 3.6→3.6.1 | fixed — removed the inherited live-sync playbackRate catch-up whose 0.1× floor pins the picture near the live edge on iOS WebKit; MSE plays at 1×, buffer trim kept. |
| #871 | Black screen when WebRTC can't re-init after an MSE switch | mitigated — root cause is a real codec limit (H265/AAC can't traverse WebRTC); the fork removes the symptom by reverting to the warm MSE stream instead of leaving a frozen overlay. |
| Upstream | Symptom | In this fork |
|---|---|---|
| #897 | cannot import name 'SUPPORT_PLAY_MEDIA' breaks the media_player platform on HA 2025.11 |
already immune — uses the modern MediaPlayerEntityFeature.PLAY_MEDIA enum, no SUPPORT_* imports. |
| #926, #927, #930 | 'LovelaceData' object is not subscriptable on HA 2026.2 |
already immune — dual-path resource lookup handles both the old dict and the new LovelaceData object. |
| Upstream | What it adds / fixes | In this fork |
|---|---|---|
| PR #956 | Reject invalid/expired stream signatures with 403, not 401, so HA's login-ban middleware can't IP-ban a reconnecting client | fixed — matters more here than upstream: the aggressive shadow + re-probe reconnect pattern makes a 401-triggered self-ban far likelier. |
| PR #961 | Guard ws_poster against a None image (TypeError) |
fixed. |
| PR #923 | Use the ICE servers Home Assistant already configures (incl. rotating Nabu Casa TURN) | fixed, reimplemented as pure JS via HA's native web_rtc/ice_servers WS command — no fragile internal Python import. The only way to use rotating Nabu Casa TURN credentials. See ice_servers. |
| #952 | Browser-side configurable ice_servers (your own STUN/TURN) |
fixed — see ice_servers. |
| #915 | Default STUN unreachable for some users | fixed — the built-in default now offers two independent public STUN servers (Google + Cloudflare) so a single blocked provider isn't fatal. |
| Upstream | Feature | In this fork |
|---|---|---|
| PR #668 | tap_action on the video |
integrated (and corrected to actually dispatch the action from a standalone card) — see Card UI additions. Note the stream-based caveat there. |
| PR #922 | live_indicator liveness dot |
integrated (visual dot only) — see Card UI additions. |
| PR #951 | Fall back to muted autoplay only on NotAllowedError |
integrated — other play errors are logged, not silently muted. |
| PR #942 | Fix media_player.play_media via go2rtc /api/ffmpeg |
integrated — removes the misleading "can't find consumer" on codec mismatch. |
| PR #945 | media_player volume_entity |
integrated. |
| PR #940 | fire-dom-event from shortcuts |
integrated. |
| #932 | "webrtc-camera" has already been used on double load |
fixed — the card's customElements.define is now guarded (this was a fork-side bug too). |
| PR #916 | Update go2rtc to 1.9.13 | superseded — fork ships go2rtc 1.9.14. |
A few niche upstream PRs (#890 per-stream icons, #954, #622) are deferred or declined as niche or fragile. Because these are upstream reports on a repository this fork does not own, "fixed" means fixed here — the fork cannot close them on GitHub.
- Card options
shortcuts,styleandptzsupport JavaScript templates - In
shortcutsandstyleyou can usestatesrelated templates - In
ptzyou can usestreamName/streamIDrelated templates (useful for a card with multiple templates)
shortcuts:
- name: Barn Light
icon: ${ states['light.yeelight_lamp'].state === 'on' ? 'mdi:outdoor-lamp':'mdi:lamp' }
service: light.toggle
service_data:
entity_id: light.yeelight_lampptz:
service: notify.persistent_notification
data_left:
message: Left for ${ this.streamName } clicked
data_right:
message: Right for ${ this.streamID } clicked- Only for supported sources in go2rtc
- Only for Hass with HTTPS access, this limitation is from the browsers
- Only for WebRTC mode
- HTTPS is also important for the Hass Mobile App!
Add microphone to the media param. You can use two streams: one with mic, second without:
type: 'custom:webrtc-camera'
streams:
- url: go2rtc_stream
- url: go2rtc_stream
mode: webrtc
media: video,audio,microphonePS. For the Hass Mobile App ensure that you can use the microphone with the built-in Assist.
The component supports streaming to Google Cast
Chromecast devices (including Android TV and Google Smart Screen) via the webrtc.dash_cast service, and
creating a temporary or permanent link to a stream via the webrtc.create_link service — without sharing
access to your Home Assistant. Read more in the upstream
wiki.
go2rtc can play audio files (ex. music or TTS) and live streams (ex. radio) on cameras with two way audio support. You need to:
- Check if your camera has a supported two way audio source
- Setup the camera stream in the go2rtc.yaml config
- Check the audio codec that your camera supports
- Create virtual Media Players for your cameras in
configuration.yaml:
media_player:
- platform: webrtc
name: Dahua Camera
stream: dahua
audio: pcmu/48000
- platform: webrtc
name: Tapo Camera
stream: tapo
audio: pcmaQ. External access with WebRTC doesn't work A. Read more. On this fork, external/tunnel paths that cannot complete a WebRTC handshake will simply stay on MSE — this is by design and not a bug.
Q. My camera never switches to WebRTC / shadow_probes keeps ticking up and down
A. That oscillation is normal — it's the background re-probe loop keeping an MSE-only camera under
observation (see Diagnostic sensors). The camera isn't switching because the path
can't hold WebRTC gaplessly for rtc_swap_prove_ms, so each probe is discarded and the reliable MSE
picture is kept — the safety behaviour working as intended. If you'd rather it stop probing entirely,
set rtc_reprobe: false.
Q. Audio doesn't work A. Check what audio codec your camera outputs and what technology you use to watch. Different technologies support different codecs.
Add to your configuration.yaml:
logger:
default: warning
logs:
custom_components.webrtc: debugClient-side, open the browser console and confirm the card banner ([WebRTC Camera] vXX.X.X). After
updating the card, do a hard reload (Cmd/Ctrl+Shift+R) so the browser/PWA service worker picks up the
new resource version.
Tip
This section is the quick reference. For the full instrumentation manual — every field of the
metrics line decoded, the phase/band/cong/path vocabulary, the backend handshake log, and
worked "how to read a collapse" examples from real sessions — see DIAGNOSTICS.md.
The browser console is the natural place to watch a stream — but the Home Assistant mobile app has no console. If a camera keeps dropping only on a phone (e.g. on mobile data / 5G) you are blind. The card fixes that by mirroring every stream's lifecycle to the HA log, which you can read from the app under Settings → System → Logs — no console required.
There is no debug card option and no on/off switch: the mirroring is always on, and what you see is
decided entirely by the native HA logger level for the sub-logger custom_components.webrtc.card.
Events are emitted at rationalized levels — recoverable anomalies at warning (visible by default),
routine lifecycle at debug (filtered out unless you ask for it). This is the same discipline the browser
console follows (console.warn/console.error show by default; console.debug needs "Verbose").
What it logs (logger custom_components.webrtc.card, one line per event, prefixed with the camera url).
Events that signal a real anomaly are emitted at warning (shown by default); routine lifecycle and
telemetry are at debug (filtered out until you lower the level).
Anomalies — warning level, visible by default:
| Event | Meaning |
|---|---|
connection-closed |
The stream dropped. The reason is included: ws-close (server/network closed the socket), no-data-watchdog (the picture froze — silence with the socket still open, the classic weak-link symptom), or ws-error. |
driver-error |
go2rtc reported an error (e.g. no route to host, i/o timeout). Recoverable — the retry loop handles it. |
rtc-revert |
A promoted WebRTC feed stalled and the card fell back to the warm MSE stream. Detail carries the reason. Frequent reverts on one camera = a WebRTC path that connects but can't sustain. |
rtc-suppressed |
WebRTC probing was suppressed because the path itself is blacked out — either this camera aborted on a sustained bad band, or another camera tripped the grid-wide suppression. The tile stays on MSE until the window elapses. |
mse-playback (frozen) |
The MSE picture stalled at the buffer edge (frozen-but-fed). Emitted at warning only for the frozen state; the paired recovery is mse-rideout. |
mse-rideout |
Non-destructive recovery: a frozen-but-fed picture was seeked back to the live edge instead of reconnecting. |
ice-config |
Your ice_servers config had no valid entries and was ignored (falling back to the defaults). |
Lifecycle & telemetry — debug level, needs the override below:
| Event | Meaning |
|---|---|
stream-up |
The stream (re)connected, with the transport (mse / webrtc / …). |
mode |
A mode transition, e.g. mse -> rtc (background WebRTC upgrade succeeded) or rtc -> mse (RTC reverted). The only app-visible signal that a camera reached — or fell back from — WebRTC: stream-up reports only the initial transport, while the shadow swap and direct-RTC upgrades happen later. |
retry |
A reconnect was scheduled, with the attempt number and back-off delay. |
stream-stable |
The stream cleared its probation window and is considered healthy. |
rtc-flap |
A camera reached WebRTC and reverted repeatedly; the fork is about to latch it to MSE-only (the weak-link protection). |
rtc-retest |
A suppression / MSE-only window elapsed and full WebRTC mode is being re-tested. |
ws-bursty / ws-reap |
Byte-aware watchdog milestones on a narrow link: the self-measured inter-chunk gap grew (ws-bursty armed) or a warm socket was extended/reaped (ws-reap). Pure diagnostics for storm analysis. |
metrics |
Periodic link sample — the measured bandwidth band and congestion estimate the resilience stack is acting on. |
mse-playback (non-frozen) |
Routine MSE playback-health transitions (e.g. back to playing). |
ice / ice-ha |
Which ICE servers were used (ice) and how many Home Assistant supplied (ice-ha). |
page-hidden / page-visible |
The tab/app went to the background or came back. Key for mobile: if losses line up with page-hidden, the app is being backgrounded (or handing off 5G↔Wi-Fi), not a camera fault. |
auto-pause / auto-resume |
Only with background: false — the card tore down / restarted a scrolled-away camera. |
Repeated identical events are throttled: the first logs immediately, further ones in a 10-second
window are counted and flushed once as (repeated N× in 10s), so a flapping camera can't flood the log.
(The ws-bursty / ws-reap telemetry pair is exempt from the name-throttle so an arm and its reap, seconds
apart, both land.)
The card's events log under their own sub-logger, custom_components.webrtc.card, separate from the
integration's backend logger custom_components.webrtc. The two warning events (connection-closed,
driver-error) show up in Settings → System → Logs out of the box. The debug events (retry,
stream-up, page-hidden / page-visible, auto-pause / auto-resume) are filtered out at the default
level — to see them, lower the level for only this sub-logger in configuration.yaml and restart:
logger:
logs:
custom_components.webrtc.card: debug # ONLY the card events — backend stays quietNote: modern Home Assistant no longer keeps a persistent
home-assistant.logfile on disk (it was dropped to cut SD-card write wear) — read the log live in the app under Settings → System → Logs, not from a file over SSH. Scope the override tocustom_components.webrtc.card: raising the parentcustom_components.webrtcto debug instead un-mutes the backend proxy's own per-stream handshake and benchmark lines, which flood the log on a multi-camera fleet.
Recipe for the 5G stream-loss case: the warning events already land in Settings → System → Logs
with no configuration. If you also want the surrounding lifecycle, add the
custom_components.webrtc.card: debug logger block above and restart, reproduce the loss on the phone,
then watch the log. A run of connection-closed: no-data-watchdog → retry → stream-up is the network
dropping and the card recovering; the same run immediately after a page-hidden points at the app
backgrounding instead.
On desktop and Android Chrome you also have the browser console, which carries far more detail than the HA mirror — the driver's per-connection state machine, WebRTC phase transitions and the congestion stack. (The HA-app WebView has no console; that is exactly why the mirror above exists. Use the console when you can — it is the richer surface — and the HA log when you can't.)
Instrumentation is split across two prefixes, so you can filter the console on either:
[WebRTC Camera] …— the card: version banner, cold-start vs shadow decisions, shadow-swap / direct-RTC upgrades, hard resets, auto-pause/resume, auth failures.[VideoRTC:<clientId>] …— the driver, one instance per connection (<clientId>is a short id; during a shadow upgrade you will see two ids — the visible MSE driver and the background probe — which is how you tell the shadow apart from the live stream). Carries:Mode: MSE/WebRTC/MJPEG/HLS/MP4, RTC phase transitions (RTC phase X -> Y),RTC promoted … MSE kept warm/RTC stable … committing/reverting to warm MSE, theNo-data watchdog fired (…ms silent, phase=…, cong=…)line,MSE ride-out reseek …,ICE Error/SDP Error, and the shared probe-gate / MSE-reap grid latch messages (RTC probe gate SUPPRESSED grid-wide …,MSE-reap grid latch ENGAGED …).
Console level maps to severity the same way as the HA mirror: console.warn / console.error (real
anomalies, auth failures) show at the default level; the routine lifecycle uses console.debug, which most
browsers hide until you enable Verbose (Chrome DevTools → Console → Levels ▾ → Verbose). So:
- First confirm the banner
[WebRTC Camera] v14.16.0(info). If the version is old after an update, the service worker served a stale bundle — hard-reload (Cmd/Ctrl+Shift+R). See the cache note in Debug. - To watch a mode/upgrade play out, enable Verbose and filter on
VideoRTC(driver) orWebRTC Camera(card). A healthy upgrade reads:Mode: MSE→RTC promoted … MSE kept warm→RTC stable … committing; a rejected one endsRTC Rejected (Priority < MSE) — staying on MSEorreverting to warm MSE.
There is no debug card option on the console side either — the levels are the only switch, exactly as in the browser's own convention. Nothing needs to be enabled in the card config.
If a camera reached over cascaded Wi-Fi repeaters intermittently becomes unreachable from Home
Assistant — go2rtc logs driver-error: … dial tcp <ip>:554: connect: no route to host — while the
vendor app keeps working and only a camera reboot recovers it, the cause is almost always
ARP/broadcast black-holing across the repeater chain, not the card or go2rtc: Home Assistant's ARP
resolution for the camera fails (the neighbor entry goes INCOMPLETE), so the direct LAN dial to :554
dies even though the camera is up.
A small companion Home Assistant add-on works around it by pinning each camera's IP → MAC as a
permanent ARP entry on the host, so HA never needs to re-resolve it via the (black-holed) broadcast:
→ Static ARP for cameras — add the repository URL under Settings → Apps → App Store → ⋮ → Repositories, then install.
Scope: this is a Home Assistant OS add-on built for aarch64, developed and validated on a Raspberry Pi 5 (it applies to other aarch64 HAOS hosts too). It relies on the Supervisor add-on model (
host_network+NET_ADMIN), so it does not apply to Home Assistant Container/Core installs or other architectures without changes. It is a Layer-2 workaround for a flaky repeater path — the robust structural fix remains a wired or mesh backhaul.
| Brand | Models | Comment |
|---|---|---|
| ActiveCam | AC-D2121IR3 | |
| ActiveCam | AC-D7121IR1W | support sound |
| Android | IP Webcam Pro | support sound, rtsp://192.168.1.123:8080/h264_ulaw.sdp |
| C-tronics | CTIPC-690C | support sound, main : rtsp://username:password@192.168.1.xx:554/11 or onvif://username:password@192.168.1.xx:8080?subtype=MainStreamProfileToken |
| Dahua | DH-IPC-HDPW1431FP-AS-0280B, VTO2211G-P | support sound |
| Dahua | VTO2202F-P-S2 | read more |
| D-Link | DCS-5222LB1, DCS-8300LHV2 | ✅ verified with this fork |
| EZVIZ | C3S | rtsp://admin:pass@192.168.1.123:554/h264/ch01/main/av_stream and /h264/ch01/sub/av_stream |
| EZVIZ | C3W, C3WN, C6CN, C6T | rtsp://admin:pass@192.168.1.123:554/h264_stream |
| EZVIZ | C8C | rtsp://admin:pass@192.168.1.123:554/channel80 |
| Foscam | C1 | rtsp://user:pass@192.168.1.123:554/videoMain |
| Foscam | C2M, R2M | rtsp://user:pass@192.168.1.123:88/videoMain |
| GW Security | GW5088IP | rtsp://192.168.1.123:554/mpeg4cif?username=admin&password=123456 |
| GW Security | GW5078IP | rtsp://192.168.1.123:554/stream0?username=admin&password=123456 |
| GW Security | GW5071IP | Not working yet, something similar to rtsp://admin:123456@192.168.0.207:554/live/main or rtsp://192.168.0.207:554/live/main?username=admin&password=123456 |
| Hikvision | DS-2CD2T47G1-L, DS-2CD1321-I, DS-2CD2143G0-IS | rtsp://user:pass@192.168.1.123:554/ISAPI/Streaming/Channels/102 |
| Hikvision | IPC-HDW3849H-AS-PV, IPC-EW5531-AS | wired to nvr DHI-NVR2108HS-8P-I using custom component |
| Imou | IPC-F42-B2E3 (Bullet 2C 4MP) | rtsp://admin:password@192.168.1.123:554/cam/realmonitor?channel=1&subtype=0 |
| QNAP | QUSBCam2 | rtsp://username:password@192.168.1.123:554/channel1 docs |
| Raspberry Pi | PiCam | read more |
| Reolink | RLC-410, RLC-410W, RLC-510WA, E1 Pro, E1 Zoom, 4505MP | RLC-510WA support sound, E1 Zoom support sound, PTZ and zoom |
| Reolink | E1 | rtsp://admin:password@192.168.1.123:554/h264Preview_01_main |
| Sonoff | GK-200MP2-B | support sound and PTZ, rtsp://rtsp:12345678@192.168.1.123:554/av_stream/ch0 and /av_stream/ch1 |
| SriHome | SH035 | rtsp://192.168.xxx.xxx:8554/profile0 and /profile1 and /profile2 |
| Topvico | rtsp://192.168.1.123:8554/stream0 or rtsp://192.168.1.123:554/ch0_0.264 |
|
| TP-Link | Tapo C100/C200/C210/C220/C310; C110/C210/C320WS ✅ verified with this fork | rtsp://user:pass@192.168.1.123:554/stream1 and /stream2 |
| TVT/Secutech | NVR-0808B2-8P | rtsp://user:pass@192.168.1.123:554/chID=1&streamType=main and chID=2&streamType=main |
| TVT/Secutech | IPC5-DF28SN | rtsp://user:pass@192.168.1.123:554/profile1 and /profile2 |
| Unifi | G4 Dome, G4 doorbell, G3 Bullet, G3 Flex | Copy the rtsps link from the camera's settings in Unifi Protect, but change the link to rtsp:// (it defaults to rtsps://), change the port to 7447, and remove any query params |
| Wyze | Cam v2/v3, Cam Pan v1/v2 | support sound |
| Xiaomi | Dafang | with hack, rtsp://192.168.1.123:8554/unicast Video: H264, size: 1920x1080, bitrate: 1000, format: VBR, frame rate: 10 Audio: PCMU, rate in: 8000, rate out: 44100 |
| Yale | SV-4CFDVR-2, SV-DAFX | rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0 |
| Yale | SV-DPFX | rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=1 |
| Yi | Hi3518e Chipset | with hack |
| Yi | MStar Infinity Chipset | with hack |