[Error: #8192] 
Return type of app\model\model::jsonSerialize() should either be compatible with JsonSerializable::jsonSerialize(): mixed, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice
Building a Raspberry Pi Security Camera Network - Moonbench.xyz

Building a Raspberry Pi Security Camera Network

What's going on around your house when you're away?

I always felt uneasy knowing there's just a wooden door and thin windows protecting my personal space (and even more uneasy when I started to practice lock picking!)

For a while I considered buying a cloud-connected Wi-Fi security camera, but ultimately decided against it. Installing a camera in my house with software and network connections controlled by someone else felt like a bigger invasion of privacy; like entering a Panopticon.

It's not the camera I disliked, it was the ownership model. If I'm going to let a camera watch my life then it needs to run software I can audit, monitor, and trust (and it needs to keep the footage private.)

I have some webcams and Raspberry Pis sitting around. Let's use them to build security cameras and create a DIY home surveillance system.

Preparing the Raspberry Pis

We'll need some USB webcams and a Raspberry Pi (or any computer we can run 24/7.)

Photo of an HP 3100 Webcam next to a Raspberry Pi in a 3D printed case

I'm going to use the Raspian-lite Linux distribution for this project, but you can use your favorite flavor. I also did some basic setup to make the Pi easier to find on my network.

While we can plug the cameras directly into our Raspberry Pi, we might cause a brownout or see strange artifacts:

Corrupted image with artifacts of an image visible

To fix this, more often than not, we'll also need a powered USB hub to provide sufficient juice to all the cameras.

Sabrent powered USB hub on top of a Raspberry Pi

Checking the equipment

Once we have everything connected we can SSH into our Pi and verify that Linux sees the cameras. Let's list the video devices:

$ ls /dev/video*
/dev/video0    /dev/video1    /dev/video2

We can also list the resolutions available for each camera. First we need to find their address on the USB bus:

$ lsusb
Bus 001 Device 004: ID 046d:0805 Logitech, Inc. Webcam C300

We can use the Bus and Device values from the the result above to get more information about the video device. For example, if we want the height and width we can call:

$ lsusb -s 001:004 -v | egrep "Width|Height"
wWidth    640
wHeight   480
wWidth    1280
wHeight   1024

Photos on a schedule with LibAV and CRON

The first package I used to interface with a camera was LibAV. It provides some powerful low-level tools for working with media streams, and there's loads of documentation available. (Later we'll replace LibAV with Motion but it's still fun to learn.)

Installing LibAV is painless:

$ sudo apt-get install libav-tools

Capturing a photo

Once we have the package installed we can start taking pictures.

$ avconv -f video4linux2 -i /dev/video0 -vframes 1 -ss 1 test.jpg

The parameters we're using here are:

  • -f video4linux2 The input format
  • -i /dev/video0 The input device
  • -vframes 1 Capture one frame
  • -ss 1 Delay 1 second (to focus)
  • test.jpg The output filename

We should be able to FTP into our Pi, download the test.jpg file, and view the result.

Downtown San Francisco captured on an HP-3100 webcam.

My living room, captured on an ELP-USBFHD01M-L180 camera module.

Taking photos every 10 mins using CRON

Going one step further, our camera needs to take photos when we're not home. The easiest solution is to use the Unix utility Cron to schedule captures.

Since we'll be taking lots of pictures we should create a dedicated directory to keep them organized:

$ mkdir ~/camera

Now, open the crontab and add our modified command to the bottom:

$ crontab -e
*/10 * * * * avconv -f video4linux2 -i /dev/video0 -vframes 1 -ss 1 ~/camera/$(date +\%Y\%m\%d\%H\%M).jpg

Using */10 * * * * means we're scheduled to take a picture once every 10 minutes. The filename $(date +\%Y\%m\%d\%H\%M).jpg is dynamic, matching the current time and date.

Now we can wait a few hours and see what we catch:

Sequence of 16 images showing a city during sunset

Using multiple cameras

We can plug in multiple cameras but we should be careful. Using them all at the same time might overwhelm our little Pi. This could happen if we instruct Cron to capture from multiple cameras on the same schedule. It'll run the commands in parallel, but they might never finish.

To get around that, we can slow down and step through the cameras sequentially, giving them each the full bandwidth of the USB bus and the maximum processing power of the CPU.

Configuring two cameras

Instead of putting all the pictures into one directory, we should give each camera its own:

$ mkdir ~/camera/0
$ mkdir ~/camera/1

Now we can write a small script to step through the cameras, taking one picture at a time.

Create the file ~/camera/take_photos.sh and add a command for each camera:


avconv -f video4linux2 -i /dev/video0 -vframes 1 -ss 1 ~/camera/0/$(date +\%Y\%m\%d\%H\%M).jpg
avconv -f video4linux2 -i /dev/video1 -vframes 1 -ss 1 ~/camera/1/$(date +\%Y\%m\%d\%H\%M).jpg

Save and exit, then make sure the script is executable:

$ chmod +x ~/camera/take_photos.sh

Now, open the Crontab and schedule our new script:

$ crontab -e
*/10 * * * * ~/camera/take_photos.sh

With this setup, every 10 minutes Cron will execute our take_photos.sh script which will take a picture on /dev/video0 then on /dev/video1.

Sequence of 16 images showing a city during sunset
Sequence of 16 images showing an apartment during sunset

Generating time-lapses from stills

Having a folder of still images is interesting but it's tedious having to manually flip through them.

We could use LibAV to convert the folder of images into a time-lapse video, making it easier to see if anything moves. We'll just need to do a bit of prep work.

Rename the files

The converting process expects the filenames to be sequential integers but our filenames are non-sequential timestamps. We need to rename the files to help LibAV process them.

Lets make a temporary directory to hold the pictures and a renaming script:

$ mkdir ~/camera/timelapse

Now, make the script ~/camera/timelapse/rename.sh and add the following:


for i in *.jpg; do
    new=$(printf "%06d.jpg" "$a")
    mv -i -- "$i" "$new"
    let a=a+1

Save and exit. Then make sure our new script is executable:

$ chmod +x ~/camera/timelapse/rename.sh

With the renaming script ready we can copy our images into the new folder and rename them:

$ cp -r ~/camera/0/*.jpg ~/camera/timelapse
$ cd ~/camera/timelapse
$ ./rename.sh

This renames the files in the format: 000001.jpg, 000002.jpg, 000003.jpg...

Create the time-lapse

Finally, we can use LibAV to convert the still images into one time-lapse video:

$ avconv -framerate 16 -f image2 -i %06d.jpg -q 2 output.webm

Give it a minute to process and... voila!

A time-lapse video of my driveway.

A time-lapse capture of an indoor succulent.

Recording videos with LibAV

Security cameras usually capture more than just still images. Any camera worth its salt should also record video. We can use LibAV to record video in a similar way to photos, just with slightly different options.

$ avconv -f video4linux2 -s 800x600 -i /dev/video0 -t 20 -q 15 output.flv

  • -f video4linux2 The input format
  • -s 800x600 The resolution
  • -i /dev/video0 The input device
  • -t 20 Record for 20 seconds
  • -q 15 The recording quality (From 2-31 with 2 being the best)
  • output.flv The output filename

Converting video formats

The file format will be interpreted automatically from the output filename. I've seen the best encoding times (with reasonable quality) using the .flv format.

Despite being quick to encode, .flv videos can't be played natively in all web browsers. Thankfully, we can use LibAV to convert our video to the more accessible .webm format:

$ avconv -i output.flv output.webm

This will take a few minutes as it converts the video frame by frame. Once it's finished we should have a new video file.

This process is how I made these videos:

Fireworks in the fog on the 4th of July.

A calm afternoon with a slow breeze.

Motion Detection with Motion

Taking pictures and videos every 10 minutes produces cool time-lapses, but it might not be the best way to accomplish our goal: Observe what happens when nobody's home.

If an interesting event doesn't coincide with a scheduled capture, we'll miss it.

Detecting Movement

To capture events of interest we need to be able to detect them. One approach to doing that is to constantly monitor the camera and count the number of pixels that change between each frame. As it turns out, we can do this with the Motion package.

This converts our mindless time-lapse camera into an intelligent home security camera.

Using Motion

Install Motion:

$ sudo apt-get install motion

Unlike LibAV (where we passed our options in through the command-line), Motion reads its options in from a configuration file.

Make a motion.conf file and add the following options:

videodevice /dev/video0

threshold 1500
event_gap 15

output_pictures best
quality 100
locate_motion_mode on
locate_motion_style box
text_changes on
Details about these options (and more): Motion configuration documentation.

We can now run motion with:

$ motion

It'll activate our camera and start watching for movement between frames. If anything is detected we should see output in the terminal similar to:

[1:ml1] [NTC] [VID] motion_detected: Motion detected - starting event 1

Once the event ends we should also have an image produced:

[1:ml1] [NTC] [VID] motion_detected: Motion detected - starting event 1
[1:ml1] [NTC] [VID] event_newfile: File of type 1 saved to: ./01-20180531123343-11.jpg
[1:ml1] [NTC] [VID] motion_loop: End of Event 1
Photo showing a person walking across the frame, as detected by motion capture

Recording Videos of the Motion

We can modify motion.conf, adding one more option (ffpmeg_output_movies on) to record videos of the events. Our configuration file should now look like:

videodevice /dev/video0

threshold 1500
event_gap 15

ffmpeg_output_movies on

output_pictures best
quality 100
locate_motion_mode on
locate_motion_style box
text_changes on

Now as soon as we have a motion detection event we should begin producing an output video:

[1:ml1] [NTC] [VID] event_newfile: File of type 8 saved to: ./01-20180531123544.avi
[1:ml1] [NTC] [VID] motion_detected: Motion detected - starting event 1
[1:ml1] [NTC] [VID] event_newfile: File of type 1 saved to: ./01-20180531123558-06.jpg
[1:ml1] [NTC] [VID] motion_loop: End of Event 1
Video of a movement event with unknown individuals in a driveway.

Multiple cameras

We can run two or more cameras without invoking Motion multiple times. We simply create a camera.conf file for each camera and tell our motion.conf file to include them.

If, for example, we have two cameras, we need two camera.conf config files and one shared motion.conf config file.

camera0.conf    camera1.conf    motion.conf

In the camera configuration files, set the following options:

videodevice /dev/video0
camera_name FirstCam
camera_id 0
videodevice /dev/video1
camera_name SecondCam
camera_id 1

Then modify motion.conf, removing the old hardcoded video device and adding our two camera configurations:

threshold 1500
event_gap 15

ffmpeg_output_movies on

output_pictures best
quality 100
locate_motion_mode on
locate_motion_style box
text_changes on

camera camera0.conf
camera camera1.conf

Now running motion should activate and monitor both streams!

Note: I've been able to reliably run two USB webcams (using motion detection) on a single Raspberry Pi provided that their resolutions are low. You can monitor the load on your Pi while Motion is running by consulting top or htop.

Time-lapses with Motion

The use of Motion to detect events is powerful but the time-lapses we made with LibAV were useful too. Some events might be too gradual to trigger motion detection, but we might still be interested in monitoring their progression. It would be nice if we could have the best of both worlds.

Well, we can have motion detection and time-lapse videos without relying on LibAV and Cron. Motion can do both!

Enabling time-lapse output

Simply open a configuration file and add the time-lapse options:

timelapse_filename %Y%m%d-timelapse
ffmpeg_timelapse 60
ffmpeg_timelapse_mode daily

Now when we run motion we'll have our motion detection running plus we'll save a frame into a time-lapse video every 60 seconds (with a new file started daily.) We don't even need to run manual scripts to rename files or convert media!

Let the camera run for a day and check the result:

Time-lapse video of a sunrise, sunset, and foggy night in San Francisco.

Live Video

What if we hear something outside and want to use our cameras to investigate the cause?

Currently the only way to "view" a camera is to first hope that something triggers motion detection (or manually run LibAV), then download the video when it's finished recording, and then finally watch it. It's a pretty inconvenient process.

We could save a lot of time by viewing the live video stream directly. Thankfully, Motion also has the ability to show live streams.

Enable the stream

By default, Motion will only allow connections from localhost. It's a secure default that prevents strangers viewing your cameras. However, we want to open up the box to let us watch remotely.

We're going to carefully disable the "localhost-only" protection and replace it with a username and password authentication scheme. (We could also use the safer MD5 digest method.)

Edit the shared motion.conf configuration file to enable network streaming:

stream_localhost off
stream_auth_method 1
stream_authentication username:password

Make sure you change username and password to something strong and secret!

Also edit the camera's camera0.conf configuration to specify a port to stream on:

stream_port 8100

Now with Motion running we can see the live video in a web browser by visiting the Pi's IP address and the camera's port. For me they were

Screenshot of a browser showing the address bar and a video stream

Multiple cameras

The only change we need to make for additional cameras is to give them each a unique port.

If camera0.conf was on port 8100, for example, then camera1.conf can be on port 8101:

stream_port 8101

Saving some bandwidth and processing power

We can reduce the demand on the Raspberry Pi, and our network, by only streaming at full speed if something interesting is happening (aka, moving.)

We can edit the shared motion.conf configuration file and add:

stream_motion on

Now Motion will reduce the framerate to 1 frame-per-second when nothing is moving. As soon as something starts to move Motion will stream at the full framerate again, which will continue until it stops.

A remote control dashboard

While we're making our camera more accessible on the network, we can consider making the web control panel available too.

Enabling the control panel

The control panel has the same default localhost restriction that the camera streams had. And again, we're going to disable that to give ourselves remote access.

We'll edit the motion.conf config file and add:

webcontrol_localhost off
webcontrol_port 8080
webcontrol_authentication username:password

Make username and password something secure! Anyone with access can execute any command on the Pi.

Using the control panel

The built-in Motion control panel is lacking in style, but makes up for it in functionality.

The front page shows us live previews of our streaming cameras:

The Motion project web control page showing two cameras

The web control panel also lets us interact with the cameras while they're running. One of the most powerful tools is the ability to change configuration settings on the fly:

Screenshot of the Motion web control page showing the configuration menu

And we can also enable or disable motion detection at will.

Custom dashboards?

If you want to go even further you might build a dashboard that combines several Raspberry Pis with several cameras into a single dashboard!

I'm working on one for my deployment and hope to share it as a project soon:

Screenshot of experimental security camera surveillance monitor web interface

Protecting the hardware

If you think you're going to keep your system running for a while it's probably wise to protect all the components from their environment.

My housing for a Pi

I recycled what I had available into enclosures for this project; a bricked firewall and an external hard drive's enclosure:

Raspberry pi and Sabrent USB hub in a Cisco Meraki firewall case
Raspberry pi and Sabrent USB hub in a plastic hard drive enclosure

Camera covers

Likewise, if you're doing anything outdoors, you might want to protect your cameras.

Try as I might, I couldn't see downtown as clearly from the inside as I could from the deck. I wanted to be able to leave a camera out there without it being exposed to the elements.

I first built a watchtower out of cardboard (covered up in clear packaging tape) with two glass windows from an unused picture frame.

Cardboard tower with two cameras inside

It isn't perfect but it might qualify for an IP54 rating. ;)

Thanks for reading!

Wow, you made it to the end! I hope you learned a bit, or at least enjoyed the pictures.

Want to see more of these pictures and videos? Take a peek at my Raspberry Pi Webcam Gallery. It has some of the most interesting/artistic shots collected while working on this project.