Downloading Video From the Arlo (No Longer Works)

Note: This post is from 2018. I no longer use the Arlo and have been told that what I have here no longer works. This is here for my personal history (and to remind myself how whiny (and unfair) I can be when frustrated).

The Prowler

I came home the other day to find some things from my garage outside on the driveway. The garage door was closed but the bolt wasn't in place, so I guess I forgot to secure it and someone went in while I was at work. I have two Arlo security cameras, one pointing at my front lawn and one at my back (I just took the ones I had set up to catch intruders and pointed them outside so they aren't really set up correctly) and they are supposed to send me notifications when motion is detected, but I didn't recall seeing any, but then I looked at my phone and I had missed one earlier in the day, and when I looked at it there was a video showing someone standing outside my back door, but that was all, but I have a Continuous Video Recording (CVR) subscription so I decided to poke around, that's when I found out how horrible Arlo's CVR really is.

The Problem with Arlo

The first problem with the Arlo's CVR system is that it's not always reliable. I've had cameras randomly stop recording videos on the server and the only "fix" that the help desk was able to give me was to deauthorize and reauthorize them (and in some cases completely remove them from the base-station and re-add them). This seems to work, but whenever you deauthorize a camera the server dumps all your recordings, so if you think you want to save something you'll have to do that first or it won't be there when the camera gets re-authorized. But then if you try to save something you'll find that the Arlo doesn't provide a way to download the video. The work-around you'll find on the forums is to do a screen capture while it plays - a horrible system, especially since the flash interface is pretty flaky and doesn't always work on my PC.

Then there's a secondary problem in that it's recording twenty-four hours a day - and they don't have a fast-forward button. They do have a timeline that you can click on to jump around, not only does this mean you might be jumping past events you want, but they don't really have a 'streaming' interface in the sense that Netflix or some other streaming service does where they're downloading small chunks so you don't have to wait a long time for the play to resume, instead the Arlo seems to be downloading one minute at a time so you have these really long waits, and the precision of the timeline isn't very good so you'll probably have to search a little bit just to find what you want.

There's also another problem, which is that the user-interface's timeline isn't accurate, so even if you know the time of an event, you can't use the time in the interface to find it. In my case the bit that was captured on my phone showing the trespasser gave me a time, and there was a little blue dot on the timeline showing that there was an event at that time, but there was nothing in the video. It turned out that the video for the camera I had out back was shifted fifty-five minutes into the future, which was even more confusing because the time for the camera I had in front wasn't shifted (that I know of) so when I looked at the video for the front around that time I was able to see the prowler arriving.

But even once I found the places in the CVR that showed the prowler (I didn't have it pointing at my garage so I didn't see him actually doing anything other than probing (knocking and then trying to open the doors)) I still had the problem of how to get the video before Netgear decided to break again and I lost it all. This is where python came in.

The Python Solution

It turns out that even though netgear hasn't published an official API for the Arlo, some people (like this guy) have reverse-engineered the API and there are several python-based libraries on github that help you talk to the arlo. This one, by jeffreydwalter was the one that I saw that helped with downloading the CVR videos so it's the one that I went with. It's on pypi, but the commit that added CVR support was made a month ago and I wasn't sure when the pypi code was uploaded so I decided to install it from the master branch, since I was going to use the example code to start my code anyway - by this I mean I had to pull the repository anyway, so I used the file to install it (in developer mode in case it changes).

The following code is basically what you can find in the example that comes with the repository called re-written to have a command-line interface and match my coding style more.

The Imports

# python standard library
from pathlib import Path
import argparse
import datetime
import os
import re
import sys
# from pypi
import dateparser
import requests
from Arlo import Arlo
from dotenv import load_dotenv

Getting the User Name and Password

In order to use the API you need to have a Netgear account and you have to pass it to the server. To prevent putting the credentials in the code, I'll use python-dotenv. In order to use it I created a file named config.arlo with the information so I can load it.

class Configuration:
    """Loads the credentials and other info for the Arlo

     config_path (str): path to the configuration for the arlo
     verbose (bool): emit more messages
    def __init__(self, config_path="~/.config/arlo/config.arlo",
        self.verbose = verbose
        self._config_path = None
        self.config_path = config_path
        self._username = None
        self._password = None
        self._front_camera = None
        self._back_camera = None
        self._kitchen_camra = None

    def config_path(self):
        """the path to the configuration"""
        return self._config_path

Load the credentials

dotenv works by adding the values in the configuration file into the environment dictionary. Most of the convenience of it comes if you create a .env file at the base of your repository, in which case it will automatically find it for you, but even though I'm not doing that it still saves me from parsing the file myself, so I'll use it. Since the other properties (username and password) rely on the values already being in the dictionary I'm calling load_dotenv in the config_path setter so that it's there before they get used.

def config_path(self, path):
    """Path to the credentials file

     loads the configuration into the environment

     path(str): path to the file
    self._config_path = Path(path).expanduser()
    if self.verbose:
        print("Loaded config from {}".format(self._config_path))

And The Rest

This is the rest of the methods for the Credentials class. Org-mode pushes it flush-left on export so it looks like it is missing whitespace, but it's there in the code.

def username(self):
    """The netgear login username

     Exception: no username found
    if self._username is None:
        self._username = os.getenv("username")
        if not self._username:
            raise Exception("Username not found")
    return self._username

def password(self):
    """The netgear login password

     Exception: no password found
    if self._password is None:
        self._password = os.getenv("password")
        if not self._password:
            raise Exception("Password not found")
    return self._password

def front_camera(self):
    """The index for the front camera

     index (int): the index for the front camera in the camera list

     Exception: something bad happened
    if self._front_camera is None:
            self._front_camera = int(os.getenv("front_camera"))
        except (TypeError, ValueError) as error:
            raise Exception("Invalid front camera")
    return self._front_camera

def back_camera(self):
    """the index for the back camera"""
    if self._back_camera is None:
            self._back_camera = int(os.getenv("back_camera"))
        except Exception:
            print("invalid back camera")
    return self._back_camera

def kitchen_camera(self):
    """index for the kitchen"""
    if self._kitchen_camera is None:
            self._kitchen_camera = int(os.getenv("kitchen_camera"))
        except Exception:
            print("invalid kitchen camera")
    return self._kitchen_camera

Output File Paths

Python 3.4 added a Path class to create an object-oriented version of what os.path does (along with some other stuff). You don't need it but I like it. My command-line interface actually doesn't support the sub-folder so it mostly just makes sure the folder is there. The Paths class here is to help set up the directories for the video output files.

class Paths:
    """Paths for the output

     root (str): path to the folder
     verbose (bool): whether to emit more messages
    def __init__(self, root=".", verbose=False):
        # warning: the 'root' setter uses self.verbose
        # so set it before setting self.root
        self.verbose = verbose
        self._root = None
        self.root = root

    def root(self):
        """path to the folder"""
        return self._root

    def root(self, path):
        """path to put the files in"""
        if self._root is None:
            self._root = Path(path)
            self._root.mkdir(parents=True, exist_ok=True)
            if self.verbose:
                print("Root Video Path: {}".format(self._root))
        return self._root

    def add_subfolder(self, sub):
        """adds the root to the sub

       creates the sub-folder if it doesn't exist

        sub(str): subfolder name

        path: path object for the sub-directory
        path = self.root.joinpath(sub)
        path.mkdir(parents=True, exist_ok=True)
        if self.verbose:
            print("subfolder: {}".format(path))
        return path


This is the class to actually do the downloading. It essentially does what the example does but I like it to be both smaller and more verbose so this matches my style more.

class Downloader:
    """Downloads the videos

     camera (int): index of the camera to grab the files for
     start (str): date and time for the start of the videos
     end (str): date and time for the end of the videos
     path: object with the path for folders to store
     configuration: object with configuration information about the arlo
     output_timestamp(str): how to timestamp the files saved
     verbose (bool): emit more messages
    date_format = "%Y%m%d"
    video_url = re.compile("^http.+(?P<camera>[A-Z0-9]{13})"

    def __init__(self, camera, start, end, path,
                 ): = camera
        self.start = start
        self.end = end
        self.path = path
        self.output_timestamp = output_timestamp
        self.verbose = verbose
        self.configuration = configuration
        self._start_time = None
        self._end_time = None
        self._start_date = None
        self._end_date = None
        self._arlo = None
        self._basestations = None
        self._cameras = None
        self._playlist = None

    def start_time(self):
        """Starting time for the videos

        time (datetime.datetime): the starting time of videos to pull
        if self._start_time is None:
            self._start_time = dateparser.parse(self.start)
            if self.verbose:
                print("Start Time: {}".format(self._start_time))
        return self._start_time

    def end_time(self):
        """ending time for the videos

        time (datetime.datetime): ending time of videos to pull
        if self._end_time is None:
            self._end_time = dateparser.parse(self.end)
            if self.verbose:
                print("End Time: {}".format(self._end_time))
        return self._end_time

    def start_date(self):
        """The start date for the playlist

        start-date (`string`): starting date for the recordings
        if self._start_date is None:
            self._start_date = self.start_time.strftime(self.date_format)
            if self.verbose:
                print("Start Date: {}".format(self._start_date))
        return self._start_date

    def end_date(self):
        """end-date for the playlist

        end-date (`str`): end-date for the recordings
        if self._end_date is None:
            self._end_date = self.end_time.strftime(self.date_format)
            if self.verbose:
                print("End Date: {}".format(self._end_date))
        return self._end_date

    def arlo(self):
        """The Arlo object

       Instantiating the Arlo object automatically calls Login(), which
       returns an oAuth token that gets cached. Subsequent successful calls
       to login will update the oAuth token

        Arlo: thing to talk to the arlo
        if self._arlo is None:
            self._arlo = Arlo(self.configuration.username,
            if self.verbose:
                print("Arlo created")
        return self._arlo

    def basestations(self):
        """This next part was in the original code but not used

       I'm leaving it in on the chance that it might be needed for the

       Gets the list of devices and filter on device type to only get
       the basestation.
       This will return an array which includes all of the basestation's
       associated metadata.

        list: list of basestations
        if self._basestations is None:
            self._basestations = self.arlo.GetDevices('basestation')
            if self.verbose:
                print("Base Stations retrieved")
        return self._basestations

    def cameras(self):
        """Get the camera.

         array: the camera's metadata
        if self._cameras is None:
            self._cameras = self.arlo.GetDevices('camera')
            if self.verbose:
                print("Cameras retrieved")
        return self._cameras

    def playlist(self):
        """the recordings within our date-range

        SystemExit: no playlist for the dates was found
        if self._playlist is None:
            if self.verbose:
                print("Getting Playlist")
            self._playlist = self.arlo.GetCvrPlaylist(
            self._playlist = self._playlist["playlist"]
            if not self._playlist:
                    ("No playlist found for camera"
                     " {} from {} through {}").format(
        return self._playlist

The Call

This does the actual downloading. It downloads some m3u8 files and then pulls the files that they refer to. m3u8 (MP3 URL with UTF-8 encoding) is a format to list URLs or paths that point to media and is said to be popular (by Wikipedia) in Dynamic Adaptive Streaming over HTTP (although I don't think the arlo uses DASH).

def __call__(self):
    """Downloads the videos"""
    print("Downloading CVR videos from {} to {}".format(
        self.start, self.end))
        # the playlist values is a list of lists
        for playlist in self.playlist.values():
            # each 'playlist' is a list of dictionaries
            for recordings in playlist:
                m3u8 = requests.get(recordings["url"]).text.split("\n")
                for location in m3u8:
                    match = self.video_url.match(location)
                    if match:
                        camera_id ="camera")
                        video_time = datetime.datetime.fromtimestamp(
                            int("timestamp")) // 1000)
                        if self.verbose:
                            print("Video Time: {}".format(video_time))
                        if self.start_time < video_time < self.end_time:
                            filename = (
                                + '-'
                                + video_time.strftime(
                                + '.mp4')

                            file_path = self.path.joinpath(filename)
                            if file_path.is_file():
                                    ("Video {} already exists, "
                                     "not downloading.").format(filename))
                                print('Downloading {}'.format(filename))
                                with'wb') as writer:
                                    # Get video as a chunked stream.
                                    # StreamRecording returns a generator.
                                    for chunk in self.arlo.StreamRecording(
                    elif self.verbose:
                        print("didn't match")
            print('Logged out')
    except Exception as e:
        print('Logged out')

A Command-Line Interface

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("camera", help="location of the camera to use",
                        choices=["front", "back", "kitchen"])
    parser.add_argument("start", help="Time of earliest video to grab")
    parser.add_argument("end", help="Time of latest video to grab")
    parser.add_argument("--sub-folder", help="sub-folder to put the videos in",
    parser.add_argument("--verbose", action="store_true",
                        help="Emit more messages")
    arguments = parser.parse_args()
    path = Paths(root=arguments.sub_folder, verbose=arguments.verbose)
    configuration = Configuration(verbose=arguments.verbose)
    camera = getattr(configuration, "{}_camera".format(
    download = Downloader(camera=camera,
if __name__ == "__main__":


For a consumer product this seems like a lot of work to download videos, but I'm glad someone went to the trouble to build this to make up for Netgear's horrible user interface. The Arlo seems like some decent (although probably overpriced) hardware matched to some pretty bad software and nearly non-existent customer support or documentation and a not particularly robust web-service. I don't know that I would recommend it to anyone who wants more than a web-camera.