Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add camera proxy #12006

Merged
merged 4 commits into from Mar 9, 2018
Merged

Add camera proxy #12006

merged 4 commits into from Mar 9, 2018

Conversation

PhracturedBlue
Copy link
Contributor

Description:

As per discussion in #11577, this is an implementation of a camera proxy. Its primary purpose is to reduce the bandwidth utilization of streaming images to the frontend.

Note that this does not implement dynamic sizing as requested by the frontend as that part of #11577 was more controversial. I may try to implement that capability in a separate patch in the future.

Pull request in home-assistant.github.io with documentation (if applicable): home-assistant/home-assistant.github.io#<home-assistant.github.io PR number goes here>

Example entry for configuration.yaml (if applicable):

camera:
  - platform: foscam
    ip: xx.xx.xx.xx
  - platform: proxy
    entity_id: camera.proxy
    max_stream_width: 360
    max_image_width: 480
    stream_quality: 60
    image_refresh_rate: 10

Checklist:

  • The code change is tested and works locally.

If user exposed functionality or configuration variables are added/changed:

If the code communicates with devices, web services, or third-party tools:

  • Local tests with tox run successfully. Your PR cannot be merged unless tests pass
  • New dependencies have been added to the REQUIREMENTS variable ([example][ex-requir]).
  • New dependencies are only imported inside functions that use them ([example][ex-import]).
  • New dependencies have been added to requirements_all.txt by running script/gen_requirements_all.py.
  • New files were added to .coveragerc.

If the code does not interact with devices:

  • Local tests with tox run successfully. Your PR cannot be merged unless tests pass
  • Tests have been added to verify that the new code works.

@pvizeli
Copy link
Member

pvizeli commented Jan 30, 2018

Maybe render is a better name?

@PhracturedBlue
Copy link
Contributor Author

I don't really think that give the proper connotation. How about 'transform' or 'munge'? While the current code can only do resizing/quality adjustments, I was planning to add stuff like flipping and rotating for the cases where the camera doesn't support those features.

Copy link
Contributor

@dgomes dgomes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi! Good work submit this component 👍

I'm not an official reviewer, but an interested party in this component :)

Just giving you some feedback to get your PR approved ASAP:

You should check voluptuous documentation, default parameters would greatly improve this component.

# Read in HTTP headers:
stream = req.content
# multipart/x-mixed-replace; boundary=--frameboundary
__mimetype, options = cgi.parse_header(req.headers['content-type'])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the double underscore ?

Double underscore is used to mangle the attribute names of a class to avoid conflicts of attribute names between classes. That see this as the case.

Just leave let it be a "_" (don't care)

__mimetype, options = cgi.parse_header(req.headers['content-type'])
boundary = options.get('boundary').encode('utf-8')
if not boundary:
raise Exception("Can't find content-type")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would advise a _LOGGER.error() message before the raise. So it can become easier to debug

old_size = len(image)
if old_width <= new_width:
if opts.quality is None:
_LOGGER.debug("Image is smaller than requested width")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

according to your code, it can also be the same size...

newimage = imgbuf.getvalue()
if not opts.force_resize and len(newimage) >= old_size:
_LOGGER.debug("Using original image(%d bytes) "
"because resized image (%d bytes) is larger",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or equal...

self._proxied_camera = config.get(CONF_ENTITY_ID)
self._name = config.get(CONF_NAME)
if not self._name:
self._name = "{} - {}".format(DEFAULT_BASENAME,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes no sense... just pass along this string through the default parameter of the vol.Optional() method.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not believe that voluptuous can evaluate a default parameter based on another parameter. The point here is that the camera name needs to include the original proxied name unless the user overrode it

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK

Still, some room for improvement:

self._name = config.get(CONF_NAME) or "{} - {}".format(DEFAULT_BASENAME, self._proxied_camera)

4 lines can be resumed in this short one as long as config.get(CONF_NAME) is None by default

self.force_resize = force_resize

def __bool__(self):
"""Bool evaution rules."""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

evaluation

self._image_refresh_rate = config.get(CONF_IMAGE_REFRESH_RATE)
self._cache_images = (
True
if (config.get(CONF_IMAGE_REFRESH_RATE)
Copy link
Contributor

@dgomes dgomes Feb 1, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why does CONF_IMAGE_REFRESH_RATE trigger cache ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the frontend makes a request at an arbitrary rate so you must cache the results when the refresh rate is specified (strictly speaking only when slower than the frontend rate, but there is no way to know what that is)

Copy link
Member

@balloob balloob Feb 28, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multilines if statements are unreadable. Also, in this case you just want True or False, so just convert the if-condition to a bool.

@PhracturedBlue
Copy link
Contributor Author

As far as further default values in voluptous, I don't see any places where it will help. The only defualt parameter that would make any sense would be image quality, but I want to distinguish between a user specifying a value vs not, and using a default value would mean I just need a different set of logic to get the same result.

"""Set up the Proxy camera platform."""
async_add_devices([ProxyCamera(hass, config)])

return True
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Platforms have no return value



@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've switched to async/await syntax so you can drop @asyncio.coroutine and make it async def async_setup_platform(…)



@asyncio.coroutine
def _read_frame(req):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already have extract_image_from_jpeg

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That function is synchronous. I don't know how to feed async data to it. Is there another way to feed asynchronous data to that function? Also, I don't think this function is suitable for my needs. It reads data by chunks, and if I understand it correctly, it will throw away unused data. I want to capture every image in an mjpeg into an image without dropping any.

if not opts:
return image

quality = opts.quality if opts.quality else DEFAULT_QUALITY
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

quality = opts.quality or DEFAULT_QUALITY


def __bool__(self):
"""Bool evalution rules."""
return True if self.max_width or self.quality else False
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return bool(self.max_width or self.quality)

return newimage


class ImageOpts():
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can optionally use the attrs package which is installed as part of Home Assistant core

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless you are going to mandate it, I'd rather not. As far as I can tell, it won't reduce any code for my use case, and would make the code harder to read.

@balloob balloob added this to the 0.65 milestone Mar 9, 2018
@balloob balloob merged commit c5228cb into home-assistant:dev Mar 9, 2018
balloob pushed a commit that referenced this pull request Mar 9, 2018
* Add camera proxy

* Fix additional tox linting issues

* Trivial cleanup

* update to new async/await methods rather than decorators.  Other minor fixes from code review
@balloob balloob mentioned this pull request Mar 9, 2018
@home-assistant home-assistant locked and limited conversation to collaborators Jul 26, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants