mpoulson / Indigo-Ring

Plugin for Ring Doorbell in the Indigo Home Automation System
http://forums.indigodomo.com/viewforum.php?f=221
1 stars 2 forks source link

Suggested feature: automatically grab recorded video file associated with a Ring event #5

Closed ZachBenz closed 6 years ago

ZachBenz commented 6 years ago

Note: this has to do with video recorded and saved by Ring in the cloud based on a triggered event (e.g. motion, doorbell press, or manual live view ideo trigger), and requires that the user have a cloud subscription. This is different than triggering the viewing of live video in real time, which would be a wholly separate endeavor, likely involving setting up SIP sessions directly with the Ring device (or somehow triggering a manual live view and then grabbing that video from e.g. the recordingUrl)

Using the python library @mpoulson pointed out on the Indigo support forms (https://github.com/tchellomello/python-ring-doorbell), it's fairly straightforward to grab a recorded video:

from ring_doorbell import Ring

myRing = Ring('yourRingAccountEmailAddress@somewhere.com', 'yourRingAccountPassword')

# Better code would search the list for the device based on e.g. Ring doorbellId, vs.
# the brittle method presented that expects the device to be at a certain position in the list
# note that .stickup_cams includes floodlights
ringDevice = list(myRing.doorbells + myRing.stickup_cams)[indexToCorrectDevice] 

# This is only getting 'motion' type events - would want to generalize this for other event types
ringDevice.recording_download(
    ringDevice.history(limit=100, kind='motion')[0]['id'],
                     filename='/somePath/ringVideo.mp4',
                     override=True)

A number of things need to be figured out for this to be useful:

ZachBenz commented 6 years ago

A thought on recordingUrl vs. the ringDevice.recording_download method: if you think about how the Ring app works, it allows you to immediately see video associated with an event. But, looking at the list of events in the app, if you haven't pulled up the video in response to the notification, you actually can't pull the video up from the list without waiting for a minute or two, presumably for the video to get archived in your cloud account following the live event.

This leads me to wonder if the recordingUrl is essentially a proxy for a live stream, staged temporarily on AWS but not yet permanently parked in the cloud account of the user. That is, recordingUrl, with its randomly generated authentication credentials and one hour expiry, allows the app to rapidly pull up the 'live' event video. Minutes later, the backend copies over and archives the video on the cloud, which allows you to download it via the API (as exposed by the ringDevice.recording_download method). That said, there must be some kind of live SIP connection at some point to allow for the two way audio... so maybe recordingUrl has some other purpose...

All to say for this feature request either the method exposed by ringDevice.recording_download or the recordingUrl could work (assuming you hit up the recordingUrl within the hour window). And it's possible that the video is available via the recordingUrl is available sooner than the other method (even immediately perhaps?).

ZachBenz commented 6 years ago

Ignore my last comment - it's completely wrong. :-)

recordingUrl and ringDevice.recording_download in tchellomello's library are exactly the same - both are using the dings/recordingID/recordings Ring API. What happens, is this API returns you a URL that is only good for one hour, with randomly generated authentication credentials. That is, after requesting the URL, you have no more than an hour to download the video, otherwise you need to request a new URL from the API.

Using this method you may need to wait until the video becomes available (?) after an event to download it, which could take at least a minute or two.

True live/immediate viewing would need to go the SIP connection route.

ZachBenz commented 6 years ago

To implement this suggested feature, Ring.py's GetReordingUrl (or a new method associated with it) could be modified to work similarly to tchellomellos' ringDevice.recording_download:

    def recording_download(self, recording_id, filename=None, override=False):
        """Save a recording in MP4 format to a file or return raw."""
        if not self.has_subscription:
            _LOGGER.warning("Your Ring account does not have" +
                            " an active subscription.")
            return False

        url = API_URI + URL_RECORDING.format(recording_id)
        try:
            req = self._ring.query(url, raw=True)
            if req and req.status_code == 200:

                if filename:
                    if os.path.isfile(filename) and not override:
                        _LOGGER.error("%s", FILE_EXISTS.format(filename))
                        return False

                    with open(filename, 'wb') as recording:
                        recording.write(req.content)
                        return True
                else:
                    return req.content
        except IOError as error:
            _LOGGER.error("%s", error)
            raise

That is, optionally download the file after getting the URL (block of code from if filename on).

The main issue is figuring what, if any, delay there needs to be between the event firing and the video actually being available to download.

ZachBenz commented 6 years ago

Just played around a bit, and looks like the recordingUrl currently retrieved by the Indigo plugin may not match up to the last event, probably because the url isn't 'ready' yet when the event is processed. I think the recordingUrl might be better retrieved on the fly when needed (e.g. add an Indigo action to the plugin to download video for the last event for a device, which would then do what is laid out in recording_download above - that is, retrieve the URL just before downloading, checking to see that video is really ready, or perhaps waiting some user defined timeout before attempting the download to ensure it is ready.

ZachBenz commented 6 years ago

Ok, I created a downloadVideo action for the plugin; creating a pull request now...

ZachBenz commented 6 years ago

See pull request #6

ZachBenz commented 6 years ago

I just tested out the v0.1.27 release with the downloadVideo action. I can't figure out why, but it always downloads a JSON file, not the mp4 content. The file is consistently a ~384 byte JSON file, consisting of the URL that the video download is supposed to come from - e.g. {"url":"https://ring-transcoded-videos.s3.amazonaws.com/"}. Inspecting the code, including Ring.py, I can't see a reason for the issue. When first writing the downloadVideo action, I had experienced this problem when I was improperly getting the response.content during initial development, but I see no such problem with the code as released. I'm totally baffled. Reverting to my forked plugin code (which is essentially exactly the same), everything works again. I don't get it... must be something simple.

ZachBenz commented 6 years ago

Just for a quick sanity check, I pulled up the URL that is appearing in the JSON download... and it loads in a browser just fine, pulling up the video.

ZachBenz commented 6 years ago

Tried commenting out the call to self.Ring.downloadVideo in v0.1.27's plugin.py, and replaced it with copy-pasted code from my forked version of the plugin (as copied below; also had to import requests in plugin.py), and still getting the JSON back, not a video.

#Perform the work
        #self.Ring.downloadVideo(dev, filename, eventId)

        #DEBUG_ZOB__vvvvvvv
        try:
            if filename:
                # Copied headers from Ring.py temporarily until this code is moved in there
                theHeaders = {'content-type': 'application/json', 'User-Agent': 'ring/3.6.4 (iPhone; iOS 10.2; Scale/2.00)'}

                url = self.Ring.GetRecordingUrl(eventId)
                response = requests.get(url, headers=theHeaders, verify=False)
                if response and response.status_code == 200:
                    with open(filename, 'wb') as recording:
                        recording.write(response.content)
                        indigo.server.log(u"Downloaded video of event for \"%s\" to %s" % (dev.name, filename))
                        return
                elif response:
                    indigo.server.log(u"Failed to download for \"%s\", response status code was %s" % (dev.name, response.status_code), isError=True)
                else:
                    indigo.server.log(u"Failed to download for \"%s\", no response for url %s" % (dev.name, url), isError=True)                     
            else:
                indigo.server.log(u"Missing filename setting in action settings for video download of event for \"%s\"" % (dev.name), isError=True)
                return
        except IOError as error:
            indigo.server.log(u"%s" % (error), isError=True)
ZachBenz commented 6 years ago

So perhaps it is the changes to GetRecordingUrl in v0.1.27 that is causing the problem? I noted that the url being constructed to make the API request for the recordingUrl has changed from:

url = '%sdings/%s/recording?api_version=8&auth_token=%s' % (self.baseUrl, recordingId, self.sessionID)

to:

url = '%sdings/%s/recording?api_version=9&disable_redirect=true&auth_token=%s' % (self.baseUrl, recordingId, self.sessionID)

Also, the associated request header has changed from:

request.add_header('User-Agent', 'AppleCoreMedia/1.0.0.14C92 (iPhone; U; CPU OS 10_2 like Mac OS X; en_us)')

to:

request.add_header('User-Agent', 'ring/4.1.8 (iPhone; iOS 11.2.5; Scale/2.00)')

ZachBenz commented 6 years ago

As a minor aside, Ring.py's downloadVideo doesn't handle when GetRecordingUrl returns 'No Subscription' as the url

ZachBenz commented 6 years ago

If I revert the url construction in Ring.py's GetRecordingUrl to:

url = '%sdings/%s/recording?api_version=8&auth_token=%s' % (self.baseUrl, recordingId, self.sessionID)

The actual video file downloads again!

ZachBenz commented 6 years ago

Ok, the new url construction will also work if you change disable_redirect=true to false; that is, this url construction works in GetRecordingUrl:

url = '%sdings/%s/recording?api_version=9&disable_redirect=false&auth_token=%s' % (self.baseUrl, recordingId, self.sessionID)

When the redirect disabled, it appears the video itself never gets loaded, and you just get the JSON with the URL. Allowing redirect causes the video content to be retrieved.

mpoulson commented 6 years ago

Fixed in 0.1.29