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."""
from __future__ import annotations
import asyncio
from datetime import timedelta
from typing import Final
@ -9,9 +8,9 @@ from aiohttp.web import Request, StreamResponse
from canary.api import Device, Location
from canary.live_stream_api import LiveStreamSession
from haffmpeg.camera import CameraMjpeg
from haffmpeg.tools import IMAGE_JPEG, ImageFrame
import voluptuous as vol
from homeassistant.components import ffmpeg
from homeassistant.components.camera import (
PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA,
Camera,
@ -131,16 +130,13 @@ class CanaryCamera(CoordinatorEntity, Camera):
live_stream_url = await self.hass.async_add_executor_job(
getattr, self._live_stream_session, "live_stream_url"
)
ffmpeg = ImageFrame(self._ffmpeg.binary)
image: bytes | None = await asyncio.shield(
ffmpeg.get_image(
live_stream_url,
output_format=IMAGE_JPEG,
extra_cmd=self._ffmpeg_arguments,
)
return await ffmpeg.async_get_image(
self.hass,
live_stream_url,
extra_cmd=self._ffmpeg_arguments,
width=width,
height=height,
)
return image
async def handle_async_mjpeg_stream(
self, request: Request

View file

@ -1,13 +1,12 @@
"""Support ezviz camera devices."""
from __future__ import annotations
import asyncio
import logging
from haffmpeg.tools import IMAGE_JPEG, ImageFrame
from pyezviz.exceptions import HTTPError, InvalidHost, PyEzvizError
import voluptuous as vol
from homeassistant.components import ffmpeg
from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera
from homeassistant.components.ffmpeg import DATA_FFMPEG
from homeassistant.config_entries import (
@ -329,12 +328,11 @@ class EzvizCamera(CoordinatorEntity, Camera):
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return a frame from the camera stream."""
ffmpeg = ImageFrame(self._ffmpeg.binary)
image = await asyncio.shield(
ffmpeg.get_image(self._rtsp_stream, output_format=IMAGE_JPEG)
if self._rtsp_stream is None:
return None
return await ffmpeg.async_get_image(
self.hass, self._rtsp_stream, width=width, height=height
)
return image
@property
def device_info(self) -> DeviceInfo:

View file

@ -20,6 +20,7 @@ from homeassistant.helpers.dispatcher import (
async_dispatcher_send,
)
from homeassistant.helpers.entity import Entity
from homeassistant.loader import bind_hass
DOMAIN = "ffmpeg"
@ -89,15 +90,26 @@ async def async_setup(hass, config):
return True
@bind_hass
async def async_get_image(
hass: HomeAssistant,
input_source: str,
output_format: str = IMAGE_JPEG,
extra_cmd: str | None = None,
width: int | None = None,
height: int | None = None,
) -> bytes | None:
"""Get an image from a frame of an RTSP stream."""
manager = hass.data[DATA_FFMPEG]
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(
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."""
from __future__ import annotations
import asyncio
from haffmpeg.camera import CameraMjpeg
from haffmpeg.tools import IMAGE_JPEG, ImageFrame
from onvif.exceptions import ONVIFError
import voluptuous as vol
from yarl import URL
from homeassistant.components import ffmpeg
from homeassistant.components.camera import SUPPORT_STREAM, Camera
from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS, DATA_FFMPEG
from homeassistant.const import HTTP_BASIC_AUTHENTICATION
@ -141,15 +139,12 @@ class ONVIFCameraEntity(ONVIFBaseEntity, Camera):
)
if image is None:
ffmpeg = ImageFrame(self.hass.data[DATA_FFMPEG].binary)
image = await asyncio.shield(
ffmpeg.get_image(
self._stream_uri,
output_format=IMAGE_JPEG,
extra_cmd=self.device.config_entry.options.get(
CONF_EXTRA_ARGUMENTS
),
)
return await ffmpeg.async_get_image(
self.hass,
self._stream_uri,
extra_cmd=self.device.config_entry.options.get(CONF_EXTRA_ARGUMENTS),
width=width,
height=height,
)
return image

View file

@ -1,15 +1,14 @@
"""This component provides support to the Ring Door Bell camera."""
from __future__ import annotations
import asyncio
from datetime import timedelta
from itertools import chain
import logging
from haffmpeg.camera import CameraMjpeg
from haffmpeg.tools import IMAGE_JPEG, ImageFrame
import requests
from homeassistant.components import ffmpeg
from homeassistant.components.camera import Camera
from homeassistant.components.ffmpeg import DATA_FFMPEG
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):
"""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."""
super().__init__(config_entry_id, device)
self._name = self._device.name
self._ffmpeg = ffmpeg
self._ffmpeg_manager = ffmpeg_manager
self._last_event = None
self._last_video_id = None
self._video_url = None
@ -107,25 +106,19 @@ class RingCam(RingEntityMixin, Camera):
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return a still image response from the camera."""
ffmpeg = ImageFrame(self._ffmpeg.binary)
if self._video_url is None:
return
image = await asyncio.shield(
ffmpeg.get_image(
self._video_url,
output_format=IMAGE_JPEG,
)
return await ffmpeg.async_get_image(
self.hass, self._video_url, width=width, height=height
)
return image
async def handle_async_mjpeg_stream(self, request):
"""Generate an HTTP MJPEG stream from the camera."""
if self._video_url is None:
return
stream = CameraMjpeg(self._ffmpeg.binary)
stream = CameraMjpeg(self._ffmpeg_manager.binary)
await stream.open_camera(self._video_url)
try:
@ -134,7 +127,7 @@ class RingCam(RingEntityMixin, Camera):
self.hass,
request,
stream_reader,
self._ffmpeg.ffmpeg_stream_content_type,
self._ffmpeg_manager.ffmpeg_stream_content_type,
)
finally:
await stream.close()

View file

@ -1,14 +1,13 @@
"""This component provides support for Xiaomi Cameras."""
from __future__ import annotations
import asyncio
from ftplib import FTP, error_perm
import logging
from haffmpeg.camera import CameraMjpeg
from haffmpeg.tools import IMAGE_JPEG, ImageFrame
import voluptuous as vol
from homeassistant.components import ffmpeg
from homeassistant.components.camera import PLATFORM_SCHEMA, Camera
from homeassistant.components.ffmpeg import DATA_FFMPEG
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)
if url != self._last_url:
ffmpeg = ImageFrame(self._manager.binary)
self._last_image = await asyncio.shield(
ffmpeg.get_image(
url, output_format=IMAGE_JPEG, extra_cmd=self._extra_arguments
)
self._last_image = await ffmpeg.async_get_image(
self.hass,
url,
extra_cmd=self._extra_arguments,
width=width,
height=height,
)
self._last_url = url

View file

@ -1,14 +1,13 @@
"""Support for Xiaomi Cameras (HiSilicon Hi3518e V200)."""
from __future__ import annotations
import asyncio
import logging
from aioftp import Client, StatusCodeError
from haffmpeg.camera import CameraMjpeg
from haffmpeg.tools import IMAGE_JPEG, ImageFrame
import voluptuous as vol
from homeassistant.components import ffmpeg
from homeassistant.components.camera import PLATFORM_SCHEMA, Camera
from homeassistant.components.ffmpeg import DATA_FFMPEG
from homeassistant.const import (
@ -127,11 +126,12 @@ class YiCamera(Camera):
"""Return a still image response from the camera."""
url = await self._get_latest_video_url()
if url and url != self._last_url:
ffmpeg = ImageFrame(self._manager.binary)
self._last_image = await asyncio.shield(
ffmpeg.get_image(
url, output_format=IMAGE_JPEG, extra_cmd=self._extra_arguments
),
self._last_image = await ffmpeg.async_get_image(
self.hass,
url,
extra_cmd=self._extra_arguments,
width=width,
height=height,
)
self._last_url = url

View file

@ -1,7 +1,7 @@
"""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 (
DOMAIN,
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_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")
]