Intro
I have recently moved into a house which contains a fish pond in the garden. Feeding them gets me outside in the fresh air and can be nice and relaxing to see them splashing around and enjoying life without the worry of COVID or Brexit. So obviously, one day I decided it would be a sensible idea to put a camera next to the pond so I could check on my fishies without leaving the safety of my home, or at least the inside part of my home.
A quick google would lead you to believe the best way to do this is to buy a dedicated camera designed for live streaming outdoors, such as this £900 beast. You simply plug it into your router, install the included software on your Windows PC, connect it to YouTube or Facebook and start streaming.
This approach has many issues.
- I have no issue spending many hours of time on my stupid projects, as long as they are as close to free as possible. £900 is quite a long way away from free.
- Even if I owned a Windows PC, I would not want to leave it running 24/7 for the sole (haha) purpose of broadcasting fish. Think of the polar bears! (and my electricity bill)
- Youtube and Facebook are evil and I don't trust them with a live feed of my garden. Instead, I will broadcast it publicly from my website using a cheap Chinese webcam that has multiple reported security breaches (and no I don't see any irony in that)
- I want to do something interesting enough to write a blog about
So, I set off to find the cheapest way possible to maintain a 24/7 live stream of my fish.
Choosing a camera
Really the only features I was looking for here were:
- WiFi connectivity - I don't want to run cables down my garden
- As low a price as possible
The cheapest I could find was this Eufy indoor cam. I got it in an Amazon warehouse deal for the eye-watering price of £24.99, but they sell new for not too much more than that.
The quality is good, it's easy to power and has survived several rainy days outside so far. However, a big issue I found is that it cannot work without a connection to Eufy's servers, which means that the stream goes down if they have server issues. This doesn't happen often, but it's frustrating as I see no reason for it. If I were to buy another camera, I would pick one without this limitation, even if that meant spending a little more.
Getting a useable stream URL
After a bit of hunting, I came across a NAS syncing option in the app settings. It turns out that turning this on exposes an RTSP feed from the camera.
RTSP is a pretty old protocol and no browsers support it unless you're running a browser old enough to support Adobe Flash player or ActiveX. The general suggestion was to use FFMPEG to decode the RTSP stream into a better-supported format, but unfortunately, the web assembly port of FFMPEG can't decode it yet. That means the only way I'm going to be able to stream this to browsers is to set up an intermediary process that will convert the RTSP stream into a format that browsers recognise.
If you just want a cheap and easy live stream setup, get a camera that supports RTMP Push as services like YouTube support that format without any encoding. Meaning you won't need to mess with a local server or port forwarding. However my fish deserve better than being trapped in the web of a big corporation!
To do the stream conversion I used Node Media Server. It's probably a little overkill as it wouldn't be difficult to simply use FFMPEG and an NGINX/Cabby server instead, but it was easier to set up and gives me a nice web console to monitor the stream setup.
const NodeMediaServer = require("node-media-server");
const config = {
rtmp: {
port: 1935,
chunk_size: 6000,
gop_cache: true,
ping: 60,
ping_timeout: 30,
},
http: {
port: 35928,
allow_origin: "*",
mediaroot: "./media",
},
relay: {
ffmpeg: "/opt/homebrew/bin/ffmpeg",
tasks: [
{
app: "live",
mode: "static",
edge: "rtsp://192.168.1.40/live0",
name: "fishcam",
rtsp_transport: "tcp"
},
],
},
trans: {
ffmpeg: "/opt/homebrew/bin/ffmpeg",
tasks: [
{
app: "live",
ac: "aac",
vc: "copy",
hls: true,
hlsFlags: "[hls_time=2:hls_list_size=100000:hls_flags=delete_segments]",
},
],
},
};
var nms = new NodeMediaServer(config);
nms.run();
The key things to look for here are:
mediaroot
is the directory where the video files will be saved
relay.tasks
is a list of sources. In my case I just have one camera, with the rtsp address shown inedge
. Mode is set tostatic
as I like to have a few hours of history in addition to the live stream, but you can set it topull
which will only start the encoding on demand.
-
hlsFlags
can be tweaked to change the buffer time (lower reduces latency but will lead to buffering on clients with slow networks) and history. HLS streams consist of a playlist file which includes a list of small .ts files, each containing a few seconds of the stream. With the above config, the server will keep 100,000 of these files before deleting the old ones. This uses roughly 3GB of diskspace and gives end users 4.5 hours of footage history.
Upon running
node index.js
I can see the NMS console at localhost:8000
and the new browser compatible HLS stream is available at localhost:8000/live/fishcam/index.m3u8
Now my website is hosted on Vercel, which although it supports server-side code by means of serverless functions, has a limitation of 10 seconds execution time per function on the free plan and doesn't support WebSockets. I could set up some hacky workaround to restart the stream encoder every 10 seconds, but as I already have a small home media server, I decided to just run the encoder from that and forward a port.
Now there are obvious security implications with exposing ports on my home network to the internet, so I set up my Next.js config to proxy the stream URL. This allows me to set my firewall to only allow connections from Vercel, keeping my network secure and hiding my public IP address from any evil hackz0rs. It also means I don't have to bother setting up an SSL certificate for the stream.
module.exports = {
...
async rewrites() {
return [
...
{
source: '/fishfeed/:path*',
destination: 'http://{MY PUBLIC IP}/live/fishcam/:path*'
},
...
]
}
}
For the client player, I used the video.js library. It has good documentation and supports many codecs on all major platforms.
import { useVideojs } from 'react-videojs-hook'
import { VideoJsPlayerOptions } from 'video.js'
import 'video.js/dist/video-js.css'
const Player = (props: VideoJsPlayerOptions) => {
const { vjsId, vjsRef, vjsClassName } = useVideojs(props)
return (
<div data-vjs-player>
<video ref={vjsRef} id={vjsId} className={vjsClassName}></video>
</div>
)
}
export default Player
const videoJsOptions = {
autoplay: false,
liveui: true,
controls: true,
fluid: true,
src: '/fishfeed/index.m3u8',
type: 'application/x-mpegURL',
bigPlayButtonCentered: true,
}
<Player {...videoJsOptions} />
Other little things
So there we have it. A few dozen lines of code, a £25 WiFi camera and a few hours of time that would have gone to total waste anyway and we have this.
You may notice the live comment feed and audio chat room on this page, but that's a story for another blog I think.
In the meantime, visit the page here and chill out with my fish! I've actually found the stream quite relaxing, the birds that live in my hedge provide a calming soundtrack which is only sometimes ruined by my neighbours. If you have any feedback, feel free to leave a comment.