Allow to set a desired update interval for camera_proxy_stream view (#13350)

* allow to set a desired update interval for camera_proxy_stream view

* lint

* refactor into a seperate method.
Keep the handle_async_mjpeg_stream method to be overridden by platforms
so they can keep proxying the direct streams from the camera

* change descriptions

* consolidate

* lint

* travis

* async/await and force min stream interval for fallback stream.

* guard clause. Let the method raise error on interval.

* is is not =

* what to except when you're excepting

* raise ValueError, remove unnecessary 500 response
This commit is contained in:
NovapaX 2018-05-01 20:49:33 +02:00 committed by Paulus Schoutsen
parent bf53cbe08d
commit 38560cda1c

View File

@ -53,6 +53,9 @@ ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}'
TOKEN_CHANGE_INTERVAL = timedelta(minutes=5) TOKEN_CHANGE_INTERVAL = timedelta(minutes=5)
_RND = SystemRandom() _RND = SystemRandom()
FALLBACK_STREAM_INTERVAL = 1 # seconds
MIN_STREAM_INTERVAL = 0.5 # seconds
CAMERA_SERVICE_SCHEMA = vol.Schema({ CAMERA_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
}) })
@ -252,19 +255,21 @@ class Camera(Entity):
""" """
return self.hass.async_add_job(self.camera_image) return self.hass.async_add_job(self.camera_image)
@asyncio.coroutine async def handle_async_still_stream(self, request, interval):
def handle_async_mjpeg_stream(self, request):
"""Generate an HTTP MJPEG stream from camera images. """Generate an HTTP MJPEG stream from camera images.
This method must be run in the event loop. This method must be run in the event loop.
""" """
response = web.StreamResponse() if interval < MIN_STREAM_INTERVAL:
raise ValueError("Stream interval must be be > {}"
.format(MIN_STREAM_INTERVAL))
response = web.StreamResponse()
response.content_type = ('multipart/x-mixed-replace; ' response.content_type = ('multipart/x-mixed-replace; '
'boundary=--frameboundary') 'boundary=--frameboundary')
yield from response.prepare(request) await response.prepare(request)
async def write(img_bytes): async def write_to_mjpeg_stream(img_bytes):
"""Write image to stream.""" """Write image to stream."""
await response.write(bytes( await response.write(bytes(
'--frameboundary\r\n' '--frameboundary\r\n'
@ -277,21 +282,21 @@ class Camera(Entity):
try: try:
while True: while True:
img_bytes = yield from self.async_camera_image() img_bytes = await self.async_camera_image()
if not img_bytes: if not img_bytes:
break break
if img_bytes and img_bytes != last_image: if img_bytes and img_bytes != last_image:
yield from write(img_bytes) await write_to_mjpeg_stream(img_bytes)
# Chrome seems to always ignore first picture, # Chrome seems to always ignore first picture,
# print it twice. # print it twice.
if last_image is None: if last_image is None:
yield from write(img_bytes) await write_to_mjpeg_stream(img_bytes)
last_image = img_bytes last_image = img_bytes
yield from asyncio.sleep(.5) await asyncio.sleep(interval)
except asyncio.CancelledError: except asyncio.CancelledError:
_LOGGER.debug("Stream closed by frontend.") _LOGGER.debug("Stream closed by frontend.")
@ -299,7 +304,17 @@ class Camera(Entity):
finally: finally:
if response is not None: if response is not None:
yield from response.write_eof() await response.write_eof()
async def handle_async_mjpeg_stream(self, request):
"""Serve an HTTP MJPEG stream from the camera.
This method can be overridden by camera plaforms to proxy
a direct stream from the camera.
This method must be run in the event loop.
"""
await self.handle_async_still_stream(request,
FALLBACK_STREAM_INTERVAL)
@property @property
def state(self): def state(self):
@ -411,7 +426,17 @@ class CameraMjpegStream(CameraView):
url = '/api/camera_proxy_stream/{entity_id}' url = '/api/camera_proxy_stream/{entity_id}'
name = 'api:camera:stream' name = 'api:camera:stream'
@asyncio.coroutine async def handle(self, request, camera):
def handle(self, request, camera): """Serve camera stream, possibly with interval."""
"""Serve camera image.""" interval = request.query.get('interval')
yield from camera.handle_async_mjpeg_stream(request) if interval is None:
await camera.handle_async_mjpeg_stream(request)
return
try:
# Compose camera stream from stills
interval = float(request.query.get('interval'))
await camera.handle_async_still_stream(request, interval)
return
except ValueError:
return web.Response(status=400)