Four weeks ago I adopted a baby cockatiel. As a first-time bird owner, I'm often too anxious about the health condition of my birdie, as I've read that birds are prone to many household hazards and are notorious for hiding critical health problems too well when someone is nearby. So, besides watching its body temperature (with a thermal imager😎) and body weight every day, I want to watch it inside its cage whenever I want, so that I'm familiar with its normal behavior and can notice problems more easily.

So our goal is to set up cameras whose image can be viewed at any place that has internet access, preferably on smartphones, with as less latency as possible, with recording capability, and avoid retail solutions (I don't trust them).

The prerequisites of my solution are:

  • A home server which has a public IP address. The home server's function is restreaming. It collects video streams from network cameras, and push them to one or more viewers. Not having public IP means you can't initiate a connection to your home server. Though this can be addressed by setting up a VPN network with a rented server which has public IP, I will not cover that in this post. See my first blog about how to set up a VPN network.
  • One or more Raspberry Pis with camera modules attached. RPis act as network cameras. I don't like retail IP cameras because they are prone to cyber attack, often have stability issues, and may send your video to the company selling them. Plus, Raspberry Pi 3 Model B+ has 5GHz WiFi, which I've not seen in any WiFi cameras. But IP cameras should be okay in my solution, as long as you know the address of their video stream.
  • Viewing devices. Things I've tried: Google Chrome on Linux and Android, VLC on Linux and Android. Google Chrome is recommended.

Setting up Raspberry Pi

One thing I can't wait to share is a clean way to stream video from camera module on a Raspberry Pi, using raspivid and VLC. No (seems ugly) v4l2 driver and no compiling any unmaintained code.

Following content assumes you have done setting up the camera module and setting up the wired/wireless network on your Raspberry Pi.

TCP Video Server

We are going to use the method described in the next section instead of this, as the latter needs some hacking on the home server to consume TCP stream and eventually failed due to some weird "Connection Refused" problem (ffmpeg will terminate TCP connection prematurely when spawned by Shinobi, the restreaming software, while works normally when launched by other methods).

A recent update of raspivid enabled it to listen on TCP connection, through -l -o tcp://0.0.0.0:<port> and once the connection is established, dump video stream through this connection. But raspivid will quit on connection close, so it's better to automatically restart it with a service manager such as systemd. So I ended up having the following systemd configuration:

#/etc/systemd/system/networkcamera.service
[Unit]
Description=Camera H264 TCP Server
Wants=network-online.target
After=network-online.target

[Service]
Type=simple
User=<username>
ExecStart=/usr/bin/raspivid -t 0 -n -l -o tcp://0.0.0.0:34567
RestartSec=1
Restart=always
IgnoreSIGPIPE=false

(Note the last line, it makes raspivid quit as expected when TCP connection is closed. Learn more)

Update configuration and start the service using the following commands:

# systemctl daemon-reload
# systemctl start networkcamera.service

Then you can play this stream with ffmpeg on a Linux computer (VLC can't play this stream):

ffplay -i tcp://<raspi-address>:34567

RTSP Video Server

First, install VLC:

# apt update && apt install vlc

Then create a Bash script /home/<username>/networkcamera.sh:

#!/bin/bash
/usr/bin/raspivid -l -o - -t 0 -rot 180 -w 1296 -h 972 -a 1036 -fps 25 --awb fluorescent -b 0 -n -ISO 800 -ex night -g 125 -qp 30 -md 4 | cvlc stream:///dev/stdin --sout '#rtp{sdp=rtsp://:8554/stream,proto=udp,rtcp-mux}' :demux=h264

The raspivid arguments are recommended for V1.x camera module (OV5647 chip), adjust if needed.
Some interesting facts on the command used:

  • It seems better to use cvlc stream:///dev/stdin instead of cvlc -, though they both mean to read from stdin.
  • :demux=h264 is essential for VLC to recognize the stream sent by raspivid.
  • If mux=ts option is added to rtp module and try playing the RTSP stream with ffmpeg, ffmpeg will exit with error. But VLC Media Player will play the RTSP stream normally.
  • rtcp-mux is essential for ffmpeg, which is on the server side, to play the RTSP stream.
  • More about VLC streaming from the command line.

Modify /etc/systemd/system/networkcamera.service as follow:

[Unit]
Description=Camera RTSP Streamer
Wants=network-online.target
After=network-online.target

[Service]
Type=simple
User=<username>
ExecStart=/home/<username>/networkcamera.sh
Restart=no
IgnoreSIGPIPE=false

Reload and restart the service.

# systemctl daemon-reload
# systemctl start networkcamera.service

Install VLC Media Player on your computer and try playing network stream with address rtsp://<raspi-ip>:8554/stream. If it plays normally, you can proceed with setting up your home server.

Setting up Restreaming Server

We will use Shinobi as our restreaming software. Shinobi is basically a ffmpeg wrapper geared towards restreaming. Compared with Zoneminder, it features (seemingly) more modern streaming methods rather than MJPEG (which has several funny restrictions imposed by both client browser and server operating system) and doesn't have weird high CPU usage problems. Though Shinobi's web dashboard is frustrating and buggy, it provides handy APIs which we will use later.

Shinobi Installation

Go through Shinobi's installation described in its Documentation. The process is basically getting the Node.js script camera.js running after providing what it needs: Required Node.js libraries (via npm install), ffmpeg, two configuration files(conf.json and super.json) and a SQL server with database initialized.

This is my conf.json as an example:

{
  "port": 8010,
  "ip": "127.0.0.1",
  "utcOffset": "+0800",
  "videosDir": "/media/Shinobi/videos",
  "db": {
    "host": "127.0.0.1",
    "user": "<database_user>",
    "password": "<database_password>",
    "database": "ccio",
    "port": 3306
  },
  "mail": {
    "service": "gmail",
    "auth": {
      "user": "your_email@gmail.com",
      "pass": "your_password_or_app_specific_password"
    }
  },
  "cron": {
    "key": "<some_random_string>"
  },
  "pluginKeys": {
    "Motion": "<some_random_string_which_match_plugins/motion/conf.json>",
    "OpenCV": "<some_random_string_which_match_plugins/opency/conf.json>"
  },
  "productType":"Pro"
}

It is advised that you make conf.json and super.json accessible only by the user running Shinobi as they contain sensitive information such as database password:

# chown <username> conf.json super.json
# chmod 600 conf.json super.json

It is also advised that you change the default super user account by editing the super.json. The MD5 hash of your password can be obtained by running

md5sum -

Input your password and press Ctrl+D twice. The MD5 hash will be printed after your password.

Now even with camera.js running, you should not be able to access Shinobi web page (http://<home-server-ip>:8010/) using your laptop or phone. Because, as what conf.json says, Shinobi only accepts requests sent by 127.0.0.1, i.e. your home server.

Setting up HTTPS

This is necessary because if you use HTTP, which communicates via plain text, an unethical network node between you and your home server will be able to capture your video stream and streaming address. Then anyone who obtains this information can watch your camera as convenient as yourself. If you insist not using HTTPS, just delete the "ip" line and change "port" to 80 in your conf.json then skip this section.

I will not cover how to obtain SSL certificates required by running an HTTPS site as others have already got them for me. But I can compare two ways of obtaining them:

  • The self-signing method doesn't beat man-in-middle attacks and makes your browser complain if you haven't made your browser trust your randomly generated Certificate Authority yet. But it is as easy as running several OpenSSL commands.
  • Signing via CA method is the standard way and is also easy because there are free CAs such as Let's Encrypt, but it requires a domain name.

Though you can configure Shinobi to use SSL certificates (described here. Don't forget to delete the "ip" line and change "port" to 443 in your conf.json after doing so), I'm using Nginx to add the SSL shell to Shinobi's HTTP, because I'm hosting other sites on the same machine.

After installing Nginx, create configuration file /etc/nginx/sites-available/cctv:

server {
        listen *:80;
        server_name <your-domain-name>; # Delete this line if you don't have domain name for your Shinobi site.
        return 301 https://$host:443$request_uri;
}

server {
        listen *:443 ssl;
        ssl_certificate /etc/letsencrypt/live/<domain-name>/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/<domain-name>/privkey.pem;
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
        ssl_prefer_server_ciphers on;
        ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH';

        server_name <your-domain-name>; # Delete this line if you don't have domain name for your Shinobi site.
        sendfile on;
        location / {
                proxy_pass http://127.0.0.1:8010;
                proxy_set_header Upgrade $http_upgrade;
                proxy_set_header Connection "upgrade";
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header Host $http_host;
        }
}

Run commands to enable the configuration file and start Nginx:

# ln -s /etc/nginx/sites-available/cctv /etc/nginx/sites-enabled/
# systemctl start nginx

If all goes OK, you should be able to log in your freshly built security system at http://<your-domain-name-or-ip> or https://<your-domain-name-or-ip>.

Add Network Cameras

Log in as administrator. In Shinobi Dashboard, click the "+" sign (Add Monitor) on the upper left, fill blanks as follow:

  • Mode: Record or Watch Only
  • Input Type: H.264 / H.265 / H.265+
  • Automatic: No
  • Connection Type: RTSP
  • RTSP  Transport: UDP
  • Host: Your Raspberry Pi's IP
  • Port: 8554
  • Path: /stream
  • Stream Type: Poseidon
  • Connection Type: Both are okay
  • Video Encoder: copy
  • Audio Encoder: No Audio
  • Record File Type: MP4
  • Video Codec: copy
  • Audio Codec: No Audio
  • Double Quote Directory: Yes if your video path contains space
  • Recording Segment Interval: Video files will be segmented by this interval, mine is 15

Click "Save". You should be able to see images from your camera after a while. If not, try refreshing the web page.

As what I've said before, Shinobi's Dashboard is frustrating to use and buggy. So we are going to use its API to watch our video stream.

Generate API key and Get Video Stream Address

It's advised to create a sub-account, deprive it's all privileges except viewing certain monitors and generate an API key for it, in case of API key leakage. Open https://<your-domain-name-or-ip>/admin and login as administrator to add a sub-account.

Then open https://<your-domain-name-or-ip>/ again, log in as just created sub-account, click the E-mail address on the upper left, select "API" in the menu. Fill "Allowed IPs" as "0.0.0.0", leave privileges as "Yes" and click "Add". Your API key should show up as a weird string contains digits, uppercase letters, and lower case letters.

As described here, your stream address looks like this:

https://<your-domain-name-or-ip>/<API-key>/mp4/<group-ID>/<monitor-ID>/s.mp4

Group ID can be obtained in the Settings Dialogue from users who are created by the same administrator or administrator itself, while Monitor ID can be seen from the corresponding monitor's Settings Dialogue. Typically they are both 10-character strings if you chose to let Shinoi generate them for you.

View Your Video Stream On Your Device

Simply open the stream address with your Google Chrome browser, the video should show up after a few seconds, with a little under 10 seconds of latency. On your smartphone, you can even create a shortcut on your home screen for extra convenience. The stream address should also be able to be played by any video player which supports playing network stream. I just found no one could beat Google Chrome.