* Serialize snapshot commands and bump amcrest package to 1.2.4 Attempting to send a snapshot command when a previous one hasn't finished will result in warnings and/or errors. This can happen when the camera picture is clicked on in the frontend, resulting in the thread that updates the thumbnail in the background every 10 seconds to sometimes collide with the thread that updates the large picture in the foreground quickly. An automation that calls the camera.snapshot service in yet another thread can make the situation worse. Fix by adding a thread lock to serialize snapshot commands. Also bump the amcrest package to 1.2.4 which fixes error handling in the command method and improves performance by reusing requests sessions. * Update amcrest package to 1.2.4
97 lines
3.5 KiB
Python
97 lines
3.5 KiB
Python
"""Support for Amcrest IP cameras."""
|
|
import logging
|
|
import threading
|
|
|
|
from requests import RequestException
|
|
from urllib3.exceptions import ReadTimeoutError
|
|
|
|
from homeassistant.components.amcrest import (
|
|
DATA_AMCREST, STREAM_SOURCE_LIST, TIMEOUT)
|
|
from homeassistant.components.camera import Camera
|
|
from homeassistant.components.ffmpeg import DATA_FFMPEG
|
|
from homeassistant.const import CONF_NAME
|
|
from homeassistant.helpers.aiohttp_client import (
|
|
async_get_clientsession, async_aiohttp_proxy_web,
|
|
async_aiohttp_proxy_stream)
|
|
|
|
DEPENDENCIES = ['amcrest', 'ffmpeg']
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
async def async_setup_platform(hass, config, async_add_entities,
|
|
discovery_info=None):
|
|
"""Set up an Amcrest IP Camera."""
|
|
if discovery_info is None:
|
|
return
|
|
|
|
device_name = discovery_info[CONF_NAME]
|
|
amcrest = hass.data[DATA_AMCREST][device_name]
|
|
|
|
async_add_entities([AmcrestCam(hass, amcrest)], True)
|
|
|
|
return True
|
|
|
|
|
|
class AmcrestCam(Camera):
|
|
"""An implementation of an Amcrest IP camera."""
|
|
|
|
def __init__(self, hass, amcrest):
|
|
"""Initialize an Amcrest camera."""
|
|
super(AmcrestCam, self).__init__()
|
|
self._name = amcrest.name
|
|
self._camera = amcrest.device
|
|
self._base_url = self._camera.get_base_url()
|
|
self._ffmpeg = hass.data[DATA_FFMPEG]
|
|
self._ffmpeg_arguments = amcrest.ffmpeg_arguments
|
|
self._stream_source = amcrest.stream_source
|
|
self._resolution = amcrest.resolution
|
|
self._token = self._auth = amcrest.authentication
|
|
self._snapshot_lock = threading.Lock()
|
|
|
|
def camera_image(self):
|
|
"""Return a still image response from the camera."""
|
|
with self._snapshot_lock:
|
|
try:
|
|
# Send the request to snap a picture and return raw jpg data
|
|
return self._camera.snapshot(channel=self._resolution).data
|
|
except (RequestException, ReadTimeoutError, ValueError) as error:
|
|
_LOGGER.error(
|
|
'Could not get camera image due to error %s', error)
|
|
return None
|
|
|
|
async def handle_async_mjpeg_stream(self, request):
|
|
"""Return an MJPEG stream."""
|
|
# The snapshot implementation is handled by the parent class
|
|
if self._stream_source == STREAM_SOURCE_LIST['snapshot']:
|
|
return await super().handle_async_mjpeg_stream(request)
|
|
|
|
if self._stream_source == STREAM_SOURCE_LIST['mjpeg']:
|
|
# stream an MJPEG image stream directly from the camera
|
|
websession = async_get_clientsession(self.hass)
|
|
streaming_url = self._camera.mjpeg_url(typeno=self._resolution)
|
|
stream_coro = websession.get(
|
|
streaming_url, auth=self._token, timeout=TIMEOUT)
|
|
|
|
return await async_aiohttp_proxy_web(
|
|
self.hass, request, stream_coro)
|
|
|
|
# streaming via ffmpeg
|
|
from haffmpeg import CameraMjpeg
|
|
|
|
streaming_url = self._camera.rtsp_url(typeno=self._resolution)
|
|
stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop)
|
|
await stream.open_camera(
|
|
streaming_url, extra_cmd=self._ffmpeg_arguments)
|
|
|
|
try:
|
|
return await async_aiohttp_proxy_stream(
|
|
self.hass, request, stream,
|
|
self._ffmpeg.ffmpeg_stream_content_type)
|
|
finally:
|
|
await stream.close()
|
|
|
|
@property
|
|
def name(self):
|
|
"""Return the name of this camera."""
|
|
return self._name
|