|
9 | 9 | from requests.exceptions import ChunkedEncodingError |
10 | 10 | import voluptuous as vol |
11 | 11 |
|
12 | | -from homeassistant.components.camera import Camera |
| 12 | +from homeassistant.components.camera import Camera, CameraEntityFeature |
| 13 | +from homeassistant.components.stream import Stream |
13 | 14 | from homeassistant.const import CONF_FILE_PATH, CONF_FILENAME |
14 | | -from homeassistant.core import HomeAssistant |
| 15 | +from homeassistant.core import HomeAssistant, callback |
15 | 16 | from homeassistant.exceptions import HomeAssistantError, ServiceValidationError |
16 | 17 | from homeassistant.helpers import config_validation as cv, entity_platform |
17 | 18 | from homeassistant.helpers.device_registry import DeviceInfo |
@@ -76,6 +77,7 @@ def __init__(self, coordinator: BlinkUpdateCoordinator, name, camera) -> None: |
76 | 77 | super().__init__(coordinator) |
77 | 78 | Camera.__init__(self) |
78 | 79 | self._camera = camera |
| 80 | + self._livestream = None |
79 | 81 | self._attr_unique_id = f"{camera.serial}-camera" |
80 | 82 | self._attr_device_info = DeviceInfo( |
81 | 83 | identifiers={(DOMAIN, camera.serial)}, |
@@ -200,3 +202,68 @@ async def save_video(self, filename) -> None: |
200 | 202 | translation_domain=DOMAIN, |
201 | 203 | translation_key="cant_write", |
202 | 204 | ) from err |
| 205 | + |
| 206 | + _attr_supported_features = CameraEntityFeature.STREAM |
| 207 | + |
| 208 | + async def async_create_stream(self) -> Stream | None: |
| 209 | + """Create a stream.""" |
| 210 | + stream = await super().async_create_stream() |
| 211 | + if stream is None: |
| 212 | + _LOGGER.error("Unable to create stream for %s", self._camera.name) |
| 213 | + return None |
| 214 | + |
| 215 | + stream.pyav_options["f"] = "mpegts" |
| 216 | + stream.pyav_options["err_detect"] = "ignore_err" |
| 217 | + return stream |
| 218 | + |
| 219 | + async def stream_source(self) -> str | None: |
| 220 | + """Return the source of the stream.""" |
| 221 | + if not self.is_streaming: |
| 222 | + livestream = await self._camera.init_livestream() |
| 223 | + if await livestream.start(): |
| 224 | + _LOGGER.debug("%s started serving", self._camera.name) |
| 225 | + self._livestream = livestream |
| 226 | + else: |
| 227 | + _LOGGER.error("Unable to start stream for %s", self._camera.name) |
| 228 | + return None |
| 229 | + |
| 230 | + name = f"livestream-{self._camera.serial}" |
| 231 | + task = self.hass.async_create_task(target=livestream.feed(), name=name) |
| 232 | + if task: |
| 233 | + _LOGGER.debug("%s started streaming", self._camera.name) |
| 234 | + task.add_done_callback(self.async_stream_done_callback) |
| 235 | + else: |
| 236 | + _LOGGER.error("Unable to create stream task for %s", self._camera.name) |
| 237 | + return None |
| 238 | + |
| 239 | + if not self._livestream: |
| 240 | + _LOGGER.error("No livestream available for %s", self._camera.name) |
| 241 | + return None |
| 242 | + |
| 243 | + _LOGGER.debug("Stream URL for %s: %s", self._camera.name, self._livestream.url) |
| 244 | + return self._livestream.url |
| 245 | + |
| 246 | + @callback |
| 247 | + def async_stream_done_callback(self, task: Any) -> None: |
| 248 | + """Handle the completion of the stream task.""" |
| 249 | + self.stream = None |
| 250 | + |
| 251 | + if self._livestream: |
| 252 | + self._livestream.stop() |
| 253 | + self._livestream = None |
| 254 | + self.async_write_ha_state() |
| 255 | + _LOGGER.debug("%s finished streaming", self._camera.name) |
| 256 | + else: |
| 257 | + _LOGGER.debug( |
| 258 | + "%s finished streaming, but no stream was active", self._camera.name |
| 259 | + ) |
| 260 | + |
| 261 | + @property |
| 262 | + def is_streaming(self) -> bool: |
| 263 | + """Return True if the camera is streaming.""" |
| 264 | + if self._livestream and self._livestream.is_serving: |
| 265 | + _LOGGER.debug("%s is streaming", self._camera.name) |
| 266 | + return True |
| 267 | + |
| 268 | + _LOGGER.debug("%s is NOT streaming", self._camera.name) |
| 269 | + return False |
0 commit comments