Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 28 additions & 3 deletions custom_components/xmeye/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import voluptuous as vol
from homeassistant.components.camera import Camera, CameraEntityFeature
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_platform
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.dispatcher import async_dispatcher_connect
Expand Down Expand Up @@ -363,6 +363,9 @@ def __init__(self, coordinator: XMEyeCoordinator, channel: int) -> None:
# registered stream name; None otherwise. Used to clean up on
# entity removal and to switch stream_source() to the go2rtc URL.
self._go2rtc_stream_name: str | None = None
# Privacy nulls stream_source(), which drops the WebRTC provider;
# tracked so we can ask HA to re-evaluate when it is lifted.
self._was_private = channel in coordinator.private_channels

# Locks prevent concurrent probes if HA calls us before the background task finishes.
self._snapshot_lock = asyncio.Lock()
Expand Down Expand Up @@ -410,6 +413,16 @@ async def async_will_remove_from_hass(self) -> None:
if self._coordinator.h265_channels.pop(self._channel, None) is not None:
_refresh_h265_issues(self.hass, self._coordinator)

@callback
def _handle_update(self) -> None:
# Leaving privacy gives the camera a stream source again. HA does not
# re-evaluate WebRTC providers for that on its own, so nudge it.
private = self._channel in self._coordinator.private_channels
if private != self._was_private:
self._was_private = private
self.hass.async_create_task(self.async_refresh_providers())
super()._handle_update()

async def _probe_urls(self) -> None:
"""Probe snapshot and RTSP URLs concurrently at startup, then
arm go2rtc transcoding if the chosen stream is H.265-only."""
Expand All @@ -420,6 +433,14 @@ async def _probe_urls(self) -> None:
self._find_stream_url(),
)
await self._maybe_register_go2rtc()
# HA decided whether to attach a WebRTC provider back in
# async_internal_added_to_hass, before any of the above had run, and
# it only re-evaluates on its own when a provider registers or the
# STREAM feature bit flips. Ask it to look again now that there is a
# real stream source — otherwise the camera stays HLS-only and
# browsers that cannot decode H.265 fall back to the still-image
# proxy at roughly one frame per second.
await self.async_refresh_providers()

async def _maybe_register_go2rtc(self) -> None:
"""Arm go2rtc transcoding if the chosen stream turned out to be H.265."""
Expand Down Expand Up @@ -650,6 +671,12 @@ def _default_stream_url(self) -> str:
async def stream_source(self) -> str | None:
"""Return the DVR's RTSP URL for the live view.

Never gated on ``coordinator.connected``: RTSP does not go through
the DVRIP alarm socket, and HA asks for the stream source while
deciding whether to attach a WebRTC provider — which happens before
the coordinator's background connection loop has logged in. Returning
None there would leave the camera HLS-only for the rest of its life.

Always the camera's own URL — never a go2rtc loopback URL. Chaining
go2rtc through its own RTSP server collapses the codec negotiation
(an RTSP consumer takes the first matching video track and stops
Expand All @@ -662,8 +689,6 @@ async def stream_source(self) -> str | None:
two producers here means the state is already correct by the time it
looks — and self-heals if anything ever replaced it.
"""
if not self._coordinator.connected:
return None
if self._channel in self._coordinator.private_channels:
return None
url = self._stream_url or self._default_stream_url()
Expand Down
2 changes: 1 addition & 1 deletion custom_components/xmeye/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@
"issue_tracker": "https://github.com/equake/hass-xmeye/issues",
"quality_scale": "silver",
"requirements": [],
"version": "0.8.0"
"version": "0.8.1"
}
41 changes: 41 additions & 0 deletions docs/STREAM_INVESTIGATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,3 +244,44 @@ mexe no encoder do usuário**. Passou a haver dois repair issues por device (nã
Ambos explicam o trade-off: H.264 ocupa ~40–100 % mais disco por minuto que H.265 com
qualidade equivalente (perto do dobro em cenas com muito movimento; ~15–25 % em baixa
resolução ou cenas quase estáticas).

---

## 9. Follow-up (v0.8.1) — o provider WebRTC nunca era anexado

Com a v0.8.0 o registro no go2rtc passou a acontecer corretamente (config confirmada em
campo, os três canais H.265 com os dois producers cada), mas **nenhum cliente consumia via
go2rtc**: Companion abria em H.265 direto e Chrome caía no still-image proxy (~1 fps), os
dois com alguns segundos de espera — assinatura clássica do caminho HLS.

Causa: `Camera.async_refresh_providers()` só roda em `async_internal_added_to_hass()`, e
quando um provider se registra/desregistra, ou quando o bit `CameraEntityFeature.STREAM`
muda. Nesse primeiro momento:

- `XMEyeCoordinator.async_setup()` dispara `_connection_loop()` como background task e
retorna imediatamente, então `coordinator.connected` ainda é `False`;
- `stream_source()` tinha o guard `if not self._coordinator.connected: return None`;
- `async_get_supported_provider()` recebe `None` e devolve `None` → `_webrtc_provider`
fica `None` → `camera_capabilities` = `{HLS}` para o resto da vida da entidade.

Ironia: o `after_dependencies: [go2rtc]` adicionado na v0.8.0 piorou isso. Antes, se a
integração `go2rtc` subisse depois das câmeras, o `async_register_webrtc_provider()` dela
disparava `_async_refresh_providers(hass)` e dava uma segunda chance de anexar o provider.

Correções:

1. Removido o guard `connected` de `stream_source()` — o RTSP não passa pelo socket DVRIP
de alarme (era a pendência da §7).
2. `await self.async_refresh_providers()` no fim de `_probe_urls()`, quando já se conhece
a URL e o codec reais.
3. `_handle_update()` sobrescrito para re-avaliar providers quando o modo privacidade é
ligado/desligado — privacidade zera `stream_source()`, e o HA não reavalia sozinho.

Como verificar rapidamente, no console do browser com o frontend do HA aberto:

```js
await document.querySelector("home-assistant").hass
.callWS({type: "camera/capabilities", entity_id: "camera.<sua_camera>"})
```

Antes: `{frontend_stream_types: ["hls"]}`. Depois: `["hls", "webrtc"]`.
Loading