Add support for width and height to ffmpeg based camera snapshots (#53837)

This commit is contained in:
J. Nick Koston 2021-08-10 20:31:11 -05:00 committed by GitHub
parent d0b11568cc
commit 4d40d95848
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 109 additions and 60 deletions

View file

@ -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

View file

@ -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:

View file

@ -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)
) )

View file

@ -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

View file

@ -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()

View file

@ -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

View file

@ -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

View file

@ -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")
]