From 2388d62755252ac4e35159880fe4b6c17c954789 Mon Sep 17 00:00:00 2001 From: PhracturedBlue Date: Wed, 14 Mar 2018 21:44:13 -0700 Subject: [PATCH] More robust MJPEG parser. Fixes #13138. (#13226) * More robust MJPEG parser. Fixes ##13138. * Reimplement image extraction from mjpeg without ascy generator to support python 3.5 --- homeassistant/components/camera/proxy.py | 52 ++++++++---------------- 1 file changed, 17 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/camera/proxy.py b/homeassistant/components/camera/proxy.py index 56b9db5c0ec..9f261b89bb2 100644 --- a/homeassistant/components/camera/proxy.py +++ b/homeassistant/components/camera/proxy.py @@ -56,34 +56,6 @@ async def async_setup_platform(hass, config, async_add_devices, async_add_devices([ProxyCamera(hass, config)]) -async def _read_frame(req): - """Read a single frame from an MJPEG stream.""" - # based on https://gist.github.com/russss/1143799 - import cgi - # Read in HTTP headers: - stream = req.content - # multipart/x-mixed-replace; boundary=--frameboundary - _mimetype, options = cgi.parse_header(req.headers['content-type']) - boundary = options.get('boundary').encode('utf-8') - if not boundary: - _LOGGER.error("Malformed MJPEG missing boundary") - raise Exception("Can't find content-type") - - line = await stream.readline() - # Seek ahead to the first chunk - while line.strip() != boundary: - line = await stream.readline() - # Read in chunk headers - while line.strip() != b'': - parts = line.split(b':') - if len(parts) > 1 and parts[0].lower() == b'content-length': - # Grab chunk length - length = int(parts[1].strip()) - line = await stream.readline() - image = await stream.read(length) - return image - - def _resize_image(image, opts): """Resize image.""" from PIL import Image @@ -227,9 +199,9 @@ class ProxyCamera(Camera): 'boundary=--frameboundary') await response.prepare(request) - def write(img_bytes): + async def write(img_bytes): """Write image to stream.""" - response.write(bytes( + await response.write(bytes( '--frameboundary\r\n' 'Content-Type: {}\r\n' 'Content-Length: {}\r\n\r\n'.format( @@ -240,13 +212,23 @@ class ProxyCamera(Camera): req = await stream_coro try: + # This would be nicer as an async generator + # But that would only be supported for python >=3.6 + data = b'' + stream = req.content while True: - image = await _read_frame(req) - if not image: + chunk = await stream.read(102400) + if not chunk: break - image = await self.hass.async_add_job( - _resize_image, image, self._stream_opts) - write(image) + data += chunk + jpg_start = data.find(b'\xff\xd8') + jpg_end = data.find(b'\xff\xd9') + if jpg_start != -1 and jpg_end != -1: + image = data[jpg_start:jpg_end + 2] + image = await self.hass.async_add_job( + _resize_image, image, self._stream_opts) + await write(image) + data = data[jpg_end + 2:] except asyncio.CancelledError: _LOGGER.debug("Stream closed by frontend.") req.close()