Add support for width and height to ffmpeg based camera snapshots (#53837)
This commit is contained in:
parent
d0b11568cc
commit
4d40d95848
8 changed files with 109 additions and 60 deletions
|
@ -1,7 +1,6 @@
|
||||||
"""Support for Canary camera."""
|
"""Support for Canary camera."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import Final
|
from typing import Final
|
||||||
|
|
||||||
|
@ -9,9 +8,9 @@ from aiohttp.web import Request, StreamResponse
|
||||||
from canary.api import Device, Location
|
from canary.api import Device, Location
|
||||||
from canary.live_stream_api import LiveStreamSession
|
from canary.live_stream_api import LiveStreamSession
|
||||||
from haffmpeg.camera import CameraMjpeg
|
from haffmpeg.camera import CameraMjpeg
|
||||||
from haffmpeg.tools import IMAGE_JPEG, ImageFrame
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.components import ffmpeg
|
||||||
from homeassistant.components.camera import (
|
from homeassistant.components.camera import (
|
||||||
PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA,
|
PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA,
|
||||||
Camera,
|
Camera,
|
||||||
|
@ -131,16 +130,13 @@ class CanaryCamera(CoordinatorEntity, Camera):
|
||||||
live_stream_url = await self.hass.async_add_executor_job(
|
live_stream_url = await self.hass.async_add_executor_job(
|
||||||
getattr, self._live_stream_session, "live_stream_url"
|
getattr, self._live_stream_session, "live_stream_url"
|
||||||
)
|
)
|
||||||
|
return await ffmpeg.async_get_image(
|
||||||
ffmpeg = ImageFrame(self._ffmpeg.binary)
|
self.hass,
|
||||||
image: bytes | None = await asyncio.shield(
|
live_stream_url,
|
||||||
ffmpeg.get_image(
|
extra_cmd=self._ffmpeg_arguments,
|
||||||
live_stream_url,
|
width=width,
|
||||||
output_format=IMAGE_JPEG,
|
height=height,
|
||||||
extra_cmd=self._ffmpeg_arguments,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
return image
|
|
||||||
|
|
||||||
async def handle_async_mjpeg_stream(
|
async def handle_async_mjpeg_stream(
|
||||||
self, request: Request
|
self, request: Request
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
"""Support ezviz camera devices."""
|
"""Support ezviz camera devices."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from haffmpeg.tools import IMAGE_JPEG, ImageFrame
|
|
||||||
from pyezviz.exceptions import HTTPError, InvalidHost, PyEzvizError
|
from pyezviz.exceptions import HTTPError, InvalidHost, PyEzvizError
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.components import ffmpeg
|
||||||
from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera
|
from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera
|
||||||
from homeassistant.components.ffmpeg import DATA_FFMPEG
|
from homeassistant.components.ffmpeg import DATA_FFMPEG
|
||||||
from homeassistant.config_entries import (
|
from homeassistant.config_entries import (
|
||||||
|
@ -329,12 +328,11 @@ class EzvizCamera(CoordinatorEntity, Camera):
|
||||||
self, width: int | None = None, height: int | None = None
|
self, width: int | None = None, height: int | None = None
|
||||||
) -> bytes | None:
|
) -> bytes | None:
|
||||||
"""Return a frame from the camera stream."""
|
"""Return a frame from the camera stream."""
|
||||||
ffmpeg = ImageFrame(self._ffmpeg.binary)
|
if self._rtsp_stream is None:
|
||||||
|
return None
|
||||||
image = await asyncio.shield(
|
return await ffmpeg.async_get_image(
|
||||||
ffmpeg.get_image(self._rtsp_stream, output_format=IMAGE_JPEG)
|
self.hass, self._rtsp_stream, width=width, height=height
|
||||||
)
|
)
|
||||||
return image
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_info(self) -> DeviceInfo:
|
def device_info(self) -> DeviceInfo:
|
||||||
|
|
|
@ -20,6 +20,7 @@ from homeassistant.helpers.dispatcher import (
|
||||||
async_dispatcher_send,
|
async_dispatcher_send,
|
||||||
)
|
)
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
|
from homeassistant.loader import bind_hass
|
||||||
|
|
||||||
DOMAIN = "ffmpeg"
|
DOMAIN = "ffmpeg"
|
||||||
|
|
||||||
|
@ -89,15 +90,26 @@ async def async_setup(hass, config):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@bind_hass
|
||||||
async def async_get_image(
|
async def async_get_image(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
input_source: str,
|
input_source: str,
|
||||||
output_format: str = IMAGE_JPEG,
|
output_format: str = IMAGE_JPEG,
|
||||||
extra_cmd: str | None = None,
|
extra_cmd: str | None = None,
|
||||||
|
width: int | None = None,
|
||||||
|
height: int | None = None,
|
||||||
) -> bytes | None:
|
) -> bytes | None:
|
||||||
"""Get an image from a frame of an RTSP stream."""
|
"""Get an image from a frame of an RTSP stream."""
|
||||||
manager = hass.data[DATA_FFMPEG]
|
manager = hass.data[DATA_FFMPEG]
|
||||||
ffmpeg = ImageFrame(manager.binary)
|
ffmpeg = ImageFrame(manager.binary)
|
||||||
|
|
||||||
|
if width and height and (extra_cmd is None or "-s" not in extra_cmd):
|
||||||
|
size_cmd = f"-s {width}x{height}"
|
||||||
|
if extra_cmd is None:
|
||||||
|
extra_cmd = size_cmd
|
||||||
|
else:
|
||||||
|
extra_cmd += " " + size_cmd
|
||||||
|
|
||||||
image = await asyncio.shield(
|
image = await asyncio.shield(
|
||||||
ffmpeg.get_image(input_source, output_format=output_format, extra_cmd=extra_cmd)
|
ffmpeg.get_image(input_source, output_format=output_format, extra_cmd=extra_cmd)
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,14 +1,12 @@
|
||||||
"""Support for ONVIF Cameras with FFmpeg as decoder."""
|
"""Support for ONVIF Cameras with FFmpeg as decoder."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
from haffmpeg.camera import CameraMjpeg
|
from haffmpeg.camera import CameraMjpeg
|
||||||
from haffmpeg.tools import IMAGE_JPEG, ImageFrame
|
|
||||||
from onvif.exceptions import ONVIFError
|
from onvif.exceptions import ONVIFError
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from yarl import URL
|
from yarl import URL
|
||||||
|
|
||||||
|
from homeassistant.components import ffmpeg
|
||||||
from homeassistant.components.camera import SUPPORT_STREAM, Camera
|
from homeassistant.components.camera import SUPPORT_STREAM, Camera
|
||||||
from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS, DATA_FFMPEG
|
from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS, DATA_FFMPEG
|
||||||
from homeassistant.const import HTTP_BASIC_AUTHENTICATION
|
from homeassistant.const import HTTP_BASIC_AUTHENTICATION
|
||||||
|
@ -141,15 +139,12 @@ class ONVIFCameraEntity(ONVIFBaseEntity, Camera):
|
||||||
)
|
)
|
||||||
|
|
||||||
if image is None:
|
if image is None:
|
||||||
ffmpeg = ImageFrame(self.hass.data[DATA_FFMPEG].binary)
|
return await ffmpeg.async_get_image(
|
||||||
image = await asyncio.shield(
|
self.hass,
|
||||||
ffmpeg.get_image(
|
self._stream_uri,
|
||||||
self._stream_uri,
|
extra_cmd=self.device.config_entry.options.get(CONF_EXTRA_ARGUMENTS),
|
||||||
output_format=IMAGE_JPEG,
|
width=width,
|
||||||
extra_cmd=self.device.config_entry.options.get(
|
height=height,
|
||||||
CONF_EXTRA_ARGUMENTS
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return image
|
return image
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
"""This component provides support to the Ring Door Bell camera."""
|
"""This component provides support to the Ring Door Bell camera."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from haffmpeg.camera import CameraMjpeg
|
from haffmpeg.camera import CameraMjpeg
|
||||||
from haffmpeg.tools import IMAGE_JPEG, ImageFrame
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
from homeassistant.components import ffmpeg
|
||||||
from homeassistant.components.camera import Camera
|
from homeassistant.components.camera import Camera
|
||||||
from homeassistant.components.ffmpeg import DATA_FFMPEG
|
from homeassistant.components.ffmpeg import DATA_FFMPEG
|
||||||
from homeassistant.const import ATTR_ATTRIBUTION
|
from homeassistant.const import ATTR_ATTRIBUTION
|
||||||
|
@ -44,12 +43,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
class RingCam(RingEntityMixin, Camera):
|
class RingCam(RingEntityMixin, Camera):
|
||||||
"""An implementation of a Ring Door Bell camera."""
|
"""An implementation of a Ring Door Bell camera."""
|
||||||
|
|
||||||
def __init__(self, config_entry_id, ffmpeg, device):
|
def __init__(self, config_entry_id, ffmpeg_manager, device):
|
||||||
"""Initialize a Ring Door Bell camera."""
|
"""Initialize a Ring Door Bell camera."""
|
||||||
super().__init__(config_entry_id, device)
|
super().__init__(config_entry_id, device)
|
||||||
|
|
||||||
self._name = self._device.name
|
self._name = self._device.name
|
||||||
self._ffmpeg = ffmpeg
|
self._ffmpeg_manager = ffmpeg_manager
|
||||||
self._last_event = None
|
self._last_event = None
|
||||||
self._last_video_id = None
|
self._last_video_id = None
|
||||||
self._video_url = None
|
self._video_url = None
|
||||||
|
@ -107,25 +106,19 @@ class RingCam(RingEntityMixin, Camera):
|
||||||
self, width: int | None = None, height: int | None = None
|
self, width: int | None = None, height: int | None = None
|
||||||
) -> bytes | None:
|
) -> bytes | None:
|
||||||
"""Return a still image response from the camera."""
|
"""Return a still image response from the camera."""
|
||||||
ffmpeg = ImageFrame(self._ffmpeg.binary)
|
|
||||||
|
|
||||||
if self._video_url is None:
|
if self._video_url is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
image = await asyncio.shield(
|
return await ffmpeg.async_get_image(
|
||||||
ffmpeg.get_image(
|
self.hass, self._video_url, width=width, height=height
|
||||||
self._video_url,
|
|
||||||
output_format=IMAGE_JPEG,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
return image
|
|
||||||
|
|
||||||
async def handle_async_mjpeg_stream(self, request):
|
async def handle_async_mjpeg_stream(self, request):
|
||||||
"""Generate an HTTP MJPEG stream from the camera."""
|
"""Generate an HTTP MJPEG stream from the camera."""
|
||||||
if self._video_url is None:
|
if self._video_url is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
stream = CameraMjpeg(self._ffmpeg.binary)
|
stream = CameraMjpeg(self._ffmpeg_manager.binary)
|
||||||
await stream.open_camera(self._video_url)
|
await stream.open_camera(self._video_url)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -134,7 +127,7 @@ class RingCam(RingEntityMixin, Camera):
|
||||||
self.hass,
|
self.hass,
|
||||||
request,
|
request,
|
||||||
stream_reader,
|
stream_reader,
|
||||||
self._ffmpeg.ffmpeg_stream_content_type,
|
self._ffmpeg_manager.ffmpeg_stream_content_type,
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
await stream.close()
|
await stream.close()
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
"""This component provides support for Xiaomi Cameras."""
|
"""This component provides support for Xiaomi Cameras."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
|
||||||
from ftplib import FTP, error_perm
|
from ftplib import FTP, error_perm
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from haffmpeg.camera import CameraMjpeg
|
from haffmpeg.camera import CameraMjpeg
|
||||||
from haffmpeg.tools import IMAGE_JPEG, ImageFrame
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.components import ffmpeg
|
||||||
from homeassistant.components.camera import PLATFORM_SCHEMA, Camera
|
from homeassistant.components.camera import PLATFORM_SCHEMA, Camera
|
||||||
from homeassistant.components.ffmpeg import DATA_FFMPEG
|
from homeassistant.components.ffmpeg import DATA_FFMPEG
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
|
@ -153,11 +152,12 @@ class XiaomiCamera(Camera):
|
||||||
|
|
||||||
url = await self.hass.async_add_executor_job(self.get_latest_video_url, host)
|
url = await self.hass.async_add_executor_job(self.get_latest_video_url, host)
|
||||||
if url != self._last_url:
|
if url != self._last_url:
|
||||||
ffmpeg = ImageFrame(self._manager.binary)
|
self._last_image = await ffmpeg.async_get_image(
|
||||||
self._last_image = await asyncio.shield(
|
self.hass,
|
||||||
ffmpeg.get_image(
|
url,
|
||||||
url, output_format=IMAGE_JPEG, extra_cmd=self._extra_arguments
|
extra_cmd=self._extra_arguments,
|
||||||
)
|
width=width,
|
||||||
|
height=height,
|
||||||
)
|
)
|
||||||
self._last_url = url
|
self._last_url = url
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
"""Support for Xiaomi Cameras (HiSilicon Hi3518e V200)."""
|
"""Support for Xiaomi Cameras (HiSilicon Hi3518e V200)."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from aioftp import Client, StatusCodeError
|
from aioftp import Client, StatusCodeError
|
||||||
from haffmpeg.camera import CameraMjpeg
|
from haffmpeg.camera import CameraMjpeg
|
||||||
from haffmpeg.tools import IMAGE_JPEG, ImageFrame
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.components import ffmpeg
|
||||||
from homeassistant.components.camera import PLATFORM_SCHEMA, Camera
|
from homeassistant.components.camera import PLATFORM_SCHEMA, Camera
|
||||||
from homeassistant.components.ffmpeg import DATA_FFMPEG
|
from homeassistant.components.ffmpeg import DATA_FFMPEG
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
|
@ -127,11 +126,12 @@ class YiCamera(Camera):
|
||||||
"""Return a still image response from the camera."""
|
"""Return a still image response from the camera."""
|
||||||
url = await self._get_latest_video_url()
|
url = await self._get_latest_video_url()
|
||||||
if url and url != self._last_url:
|
if url and url != self._last_url:
|
||||||
ffmpeg = ImageFrame(self._manager.binary)
|
self._last_image = await ffmpeg.async_get_image(
|
||||||
self._last_image = await asyncio.shield(
|
self.hass,
|
||||||
ffmpeg.get_image(
|
url,
|
||||||
url, output_format=IMAGE_JPEG, extra_cmd=self._extra_arguments
|
extra_cmd=self._extra_arguments,
|
||||||
),
|
width=width,
|
||||||
|
height=height,
|
||||||
)
|
)
|
||||||
self._last_url = url
|
self._last_url = url
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
"""The tests for Home Assistant ffmpeg."""
|
"""The tests for Home Assistant ffmpeg."""
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import AsyncMock, MagicMock, Mock, call, patch
|
||||||
|
|
||||||
import homeassistant.components.ffmpeg as ffmpeg
|
from homeassistant.components import ffmpeg
|
||||||
from homeassistant.components.ffmpeg import (
|
from homeassistant.components.ffmpeg import (
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
SERVICE_RESTART,
|
SERVICE_RESTART,
|
||||||
|
@ -181,3 +181,58 @@ async def test_setup_component_test_service_start_with_entity(hass):
|
||||||
|
|
||||||
assert ffmpeg_dev.called_start
|
assert ffmpeg_dev.called_start
|
||||||
assert ffmpeg_dev.called_entities == ["test.ffmpeg_device"]
|
assert ffmpeg_dev.called_entities == ["test.ffmpeg_device"]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_async_get_image_with_width_height(hass):
|
||||||
|
"""Test fetching an image with a specific width and height."""
|
||||||
|
with assert_setup_component(1):
|
||||||
|
await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}})
|
||||||
|
|
||||||
|
get_image_mock = AsyncMock()
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.ffmpeg.ImageFrame",
|
||||||
|
return_value=Mock(get_image=get_image_mock),
|
||||||
|
):
|
||||||
|
await ffmpeg.async_get_image(hass, "rtsp://fake", width=640, height=480)
|
||||||
|
|
||||||
|
assert get_image_mock.call_args_list == [
|
||||||
|
call("rtsp://fake", output_format="mjpeg", extra_cmd="-s 640x480")
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_async_get_image_with_extra_cmd_overlapping_width_height(hass):
|
||||||
|
"""Test fetching an image with and extra_cmd with width and height and a specific width and height."""
|
||||||
|
with assert_setup_component(1):
|
||||||
|
await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}})
|
||||||
|
|
||||||
|
get_image_mock = AsyncMock()
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.ffmpeg.ImageFrame",
|
||||||
|
return_value=Mock(get_image=get_image_mock),
|
||||||
|
):
|
||||||
|
await ffmpeg.async_get_image(
|
||||||
|
hass, "rtsp://fake", extra_cmd="-s 1024x768", width=640, height=480
|
||||||
|
)
|
||||||
|
|
||||||
|
assert get_image_mock.call_args_list == [
|
||||||
|
call("rtsp://fake", output_format="mjpeg", extra_cmd="-s 1024x768")
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_async_get_image_with_extra_cmd_width_height(hass):
|
||||||
|
"""Test fetching an image with and extra_cmd and a specific width and height."""
|
||||||
|
with assert_setup_component(1):
|
||||||
|
await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}})
|
||||||
|
|
||||||
|
get_image_mock = AsyncMock()
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.ffmpeg.ImageFrame",
|
||||||
|
return_value=Mock(get_image=get_image_mock),
|
||||||
|
):
|
||||||
|
await ffmpeg.async_get_image(
|
||||||
|
hass, "rtsp://fake", extra_cmd="-vf any", width=640, height=480
|
||||||
|
)
|
||||||
|
|
||||||
|
assert get_image_mock.call_args_list == [
|
||||||
|
call("rtsp://fake", output_format="mjpeg", extra_cmd="-vf any -s 640x480")
|
||||||
|
]
|
||||||
|
|
Loading…
Add table
Reference in a new issue