From de9d47353ab8b64089a83936a79d570d5b80d770 Mon Sep 17 00:00:00 2001 From: Marc Hoersken Date: Mon, 14 Apr 2025 21:20:06 +0200 Subject: [PATCH 1/2] Add support for livestreaming of Blink cameras --- homeassistant/components/blink/camera.py | 71 +++++++++++++++++++- homeassistant/components/blink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 72 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/blink/camera.py b/homeassistant/components/blink/camera.py index 0ebef1627954f5..b1962120815ced 100644 --- a/homeassistant/components/blink/camera.py +++ b/homeassistant/components/blink/camera.py @@ -10,8 +10,9 @@ from blinkpy.camera import BlinkCamera as BlinkCameraAPI from requests.exceptions import ChunkedEncodingError -from homeassistant.components.camera import Camera -from homeassistant.core import HomeAssistant +from homeassistant.components.camera import Camera, CameraEntityFeature +from homeassistant.components.stream import Stream +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ( ConfigEntryAuthFailed, HomeAssistantError, @@ -58,6 +59,7 @@ def __init__( super().__init__(coordinator) Camera.__init__(self) self._camera = camera + self._livestream = None self._attr_unique_id = f"{camera.serial}-camera" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, camera.serial)}, @@ -200,3 +202,68 @@ async def save_video(self, filename) -> None: except UnauthorizedError as er: self.coordinator.config_entry.async_start_reauth(self.hass) raise ConfigEntryAuthFailed("Blink authorization failed") from er + + _attr_supported_features = CameraEntityFeature.STREAM + + async def async_create_stream(self) -> Stream | None: + """Create a stream with custom ffmpeg options.""" + stream = await super().async_create_stream() + if stream is None: + _LOGGER.error("Unable to create stream for %s", self._camera.name) + return None + + stream.pyav_options["f"] = "mpegts" + stream.pyav_options["err_detect"] = "ignore_err" + return stream + + async def stream_source(self) -> str | None: + """Return the source of the stream.""" + if not self.is_streaming: + livestream = await self._camera.init_livestream() + if await livestream.start(): + _LOGGER.debug("%s started serving", self._camera.name) + self._livestream = livestream + else: + _LOGGER.error("Unable to start stream for %s", self._camera.name) + return None + + name = f"livestream-{self._camera.serial}" + task = self.hass.async_create_task(target=livestream.feed(), name=name) + if task: + _LOGGER.debug("%s started streaming", self._camera.name) + task.add_done_callback(self.async_stream_done_callback) + else: + _LOGGER.error("Unable to create stream task for %s", self._camera.name) + return None + + if not self._livestream: + _LOGGER.error("No livestream available for %s", self._camera.name) + return None + + _LOGGER.debug("Stream URL for %s: %s", self._camera.name, self._livestream.url) + return self._livestream.url + + @callback + def async_stream_done_callback(self, task: Any) -> None: + """Handle the completion of the stream task.""" + self.stream = None + + if self._livestream: + self._livestream.stop() + self._livestream = None + self.async_write_ha_state() + _LOGGER.debug("%s finished streaming", self._camera.name) + else: + _LOGGER.debug( + "%s finished streaming, but no stream was active", self._camera.name + ) + + @property + def is_streaming(self) -> bool: + """Return True if the camera is streaming.""" + if self._livestream and self._livestream.is_serving: + _LOGGER.debug("%s is streaming", self._camera.name) + return True + + _LOGGER.debug("%s is NOT streaming", self._camera.name) + return False diff --git a/homeassistant/components/blink/manifest.json b/homeassistant/components/blink/manifest.json index 9dd3b5493c9585..694ffd38f06818 100644 --- a/homeassistant/components/blink/manifest.json +++ b/homeassistant/components/blink/manifest.json @@ -21,5 +21,5 @@ "integration_type": "hub", "iot_class": "cloud_polling", "loggers": ["blinkpy"], - "requirements": ["blinkpy==0.25.2"] + "requirements": ["blinkpy==0.25.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index c2a00e015ea300..9420b0d64226e3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -652,7 +652,7 @@ bleak==2.1.1 blebox-uniapi==2.5.0 # homeassistant.components.blink -blinkpy==0.25.2 +blinkpy==0.25.3 # homeassistant.components.bitcoin blockchain==1.4.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 08158046f1e65a..651fbdb13976d1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -589,7 +589,7 @@ bleak==2.1.1 blebox-uniapi==2.5.0 # homeassistant.components.blink -blinkpy==0.25.2 +blinkpy==0.25.3 # homeassistant.components.blue_current bluecurrent-api==1.3.2 From b52af380de5cb55bac46b3b8f86665d38d415ad2 Mon Sep 17 00:00:00 2001 From: Marc Hoersken Date: Wed, 11 Feb 2026 22:21:19 +0100 Subject: [PATCH 2/2] Potentially improve performance with separate I/O stream thread --- homeassistant/components/blink/camera.py | 23 +++++++++++++++++++- homeassistant/components/blink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 25 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/blink/camera.py b/homeassistant/components/blink/camera.py index b1962120815ced..1a7fe9fd87ac1c 100644 --- a/homeassistant/components/blink/camera.py +++ b/homeassistant/components/blink/camera.py @@ -2,8 +2,10 @@ from __future__ import annotations +import asyncio from collections.abc import Mapping import logging +import threading from typing import Any from blinkpy.auth import UnauthorizedError @@ -227,8 +229,27 @@ async def stream_source(self) -> str | None: _LOGGER.error("Unable to start stream for %s", self._camera.name) return None + def executor_job(livestream, ready): + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + loop.run_until_complete(livestream.auth()) + ready.set() + loop.run_until_complete( + asyncio.gather(livestream.recv(), livestream.send()) + ) + + ready = threading.Event() + + task = self.hass.async_add_executor_job(executor_job, livestream, ready) + if task: + task.add_done_callback(self.async_stream_done_callback) + + await self.hass.async_add_executor_job(ready.wait, 10) + name = f"livestream-{self._camera.serial}" - task = self.hass.async_create_task(target=livestream.feed(), name=name) + task = self.hass.async_create_background_task( + target=livestream.poll(), name=name, eager_start=False + ) if task: _LOGGER.debug("%s started streaming", self._camera.name) task.add_done_callback(self.async_stream_done_callback) diff --git a/homeassistant/components/blink/manifest.json b/homeassistant/components/blink/manifest.json index 694ffd38f06818..5a900c1802c0c8 100644 --- a/homeassistant/components/blink/manifest.json +++ b/homeassistant/components/blink/manifest.json @@ -21,5 +21,5 @@ "integration_type": "hub", "iot_class": "cloud_polling", "loggers": ["blinkpy"], - "requirements": ["blinkpy==0.25.3"] + "requirements": ["blinkpy==0.25.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9420b0d64226e3..b167a2e572fb4b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -652,7 +652,7 @@ bleak==2.1.1 blebox-uniapi==2.5.0 # homeassistant.components.blink -blinkpy==0.25.3 +blinkpy==0.25.5 # homeassistant.components.bitcoin blockchain==1.4.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 651fbdb13976d1..d85ef668765e26 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -589,7 +589,7 @@ bleak==2.1.1 blebox-uniapi==2.5.0 # homeassistant.components.blink -blinkpy==0.25.3 +blinkpy==0.25.5 # homeassistant.components.blue_current bluecurrent-api==1.3.2