# How to build a video surveillance system from scratch?

**URL:** <https://discourse.nodered.org/t/how-to-build-a-video-surveillance-system-from-scratch/51077>\
**Category:** General\
**Created:** [15 September 2021 20:43 UTC](https://discourse.nodered.org/t/how-to-build-a-video-surveillance-system-from-scratch/51077 "2021-09-15T20:43:10Z")\
**Posts on this page:** 1\
**Showing post:** 32

<div class="post-metadata">

**Author:** ![kevinGodell](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/kevingodell/32/27040_2.png) [@kevinGodell](https://discourse.nodered.org/u/kevinGodell)\
**Post date:** [3 October 2021 14:16 UTC](https://discourse.nodered.org/t/how-to-build-a-video-surveillance-system-from-scratch/51077/32 "2021-10-03T14:16:51Z")

</div>

> [@BartButenaers](#):
>
> Do you have any idea what could cause this kind of behaviour?

Yes, the jpeg files are not complete. message !== jpeg, too. jpegs that are bigger than the pipe's buffer limit will be broken into chunks, same as the mp4 segments. You have to catch the pieces and re-assemble them.

> [@BartButenaers](#):
>
> It is a pitty that it is not possible to get a full resolution image this way

Nah, we don't have to settle for that. That is why **pipe2jpeg** was created, and years later **node-red-contrib-pipe2jpeg**. It can handle all sizes of jpegs. Just plug that node after the ffmpeg's jpeg buffer output.

> [@BartButenaers](#):
>
> And I was wondering: is it required to change the resolution in the web interface, or can I also pass the resolution in my ffmpeg rtsp command?

Both are acceptable, but you will have to experiment on what works best for your system.

Personally, I use both video streams coming from each of my cams, **main** and **sub**. I would likely choose the **sub** stream and use that for creating jpegs. For example, if you have an input with a resolution of 1080 and want to reduce it to 1/4 the size for a jpeg, then there be an extra cost to the rescaling.

For [scaling with ffmpeg](https://trac.ffmpeg.org/wiki/Scaling), we can add to the vf filter with the following examples.

dynamic scaling for 75 percent of the original input width and having the width and height divisible by 2 and keeping the aspect ratio:  
`-vf scale=trunc(iw*0.75/2)*2:-2`

fixed size scaling:  
`-vf scale=400:300`

try this flow to create a jpeg once per 30 seconds using mp4frag and a 2nd ffmpeg-spawn:

 ![Screen Shot 2021-10-03 at 9.02.13 AM](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/9/5/956f5e94638f9ecee494118cbd7192a91e28f8b9.png)

You can see in the screenshot that there have been 412 segments with durations approximately 1 second each. 412/30 ~ 14 jpegs created. This uses alot less cpu than having the 1st ffmpeg outputting a jpeg once per 30 seconds.

````auto
[{"id":"1a29fa75.ef7866","type":"subflow","name":"progress","info":"","category":"","in":[{"x":60,"y":100,"wires":[{"id":"1370adb2.08e592"}]}],"out":[{"x":340,"y":100,"wires":[{"id":"1370adb2.08e592","port":1}]},{"x":340,"y":160,"wires":[{"id":"1370adb2.08e592","port":2}]}],"env":[],"color":"#DDAA99","status":{"x":340,"y":40,"wires":[{"id":"1370adb2.08e592","port":0}]}},{"id":"1370adb2.08e592","type":"function","z":"1a29fa75.ef7866","name":"progress","func":"const props = msg.payload.toString().split('\\n');\n\nconst progress = {};\n\nprops.forEach(item => {\n \n const [name, value] = item.split('=');\n \n if (name && value) {\n \n progress[name] = value;\n\n }\n \n});\n\n//node.warn(props);\n\nconst fps = progress['fps'] || '0';\n\nconst bitrate = progress['bitrate'] || '0';\n\nconst kbps = bitrate.replace('kbits/s', '');\n\nconst color = progress['progress'] === 'continue' ? 'green' : 'red';\n\nconst text = `fps: ${fps}, kbps: ${kbps}`;\n\nnode.send([{ payload: { fill: color, shape: 'dot', text } }, { payload: fps }, { payload: kbps }]);","outputs":3,"noerr":0,"initialize":"","finalize":"","x":200,"y":100,"wires":[[],[],[]]},{"id":"32d61e0f.0490b2","type":"subflow","name":"stderr","info":"","category":"","in":[{"x":60,"y":80,"wires":[{"id":"211d46bf.a79c7a"}]}],"out":[],"env":[],"color":"#DDAA99","status":{"x":320,"y":120,"wires":[{"id":"211d46bf.a79c7a","port":1}]}},{"id":"211d46bf.a79c7a","type":"function","z":"32d61e0f.0490b2","name":"stderr","func":"const stderr = msg.payload.toString().split('\\n');\n\nnode.send([{ stderr }, { payload: {fill: 'red', text: `${new Date().toString()}` } }]);","outputs":2,"noerr":0,"initialize":"","finalize":"","x":190,"y":73,"wires":[["c6e5408.f19c4c"],[]]},{"id":"c6e5408.f19c4c","type":"debug","z":"32d61e0f.0490b2","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"stderr","targetType":"msg","statusVal":"","statusType":"auto","x":370,"y":60,"wires":[]},{"id":"b4ccc97e.e6a588","type":"inject","z":"4d9df903.2567d8","name":"start","props":[{"p":"action","v":"{\"command\":\"start\"}","vt":"json"}],"repeat":"","crontab":"","once":true,"onceDelay":"5","topic":"","payloadType":"str","x":150,"y":280,"wires":[["dc48904a.5770d"]]},{"id":"dc48904a.5770d","type":"ffmpeg-spawn","z":"4d9df903.2567d8","name":"","outputs":4,"cmdPath":"","cmdArgs":"[\"-loglevel\",\"+level+fatal\",\"-nostats\",\"-rtsp_transport\",\"tcp\",\"-i\",\"rtsp://...\",\"-f\",\"mp4\",\"-c:v\",\"copy\",\"-c:a\",\"copy\",\"-movflags\",\"+frag_keyframe+empty_moov+default_base_moof\",\"-metadata\",\"title=hello bart\",\"pipe:1\",\"-progress\",\"pipe:3\"]","cmdOutputs":3,"killSignal":"SIGTERM","x":380,"y":320,"wires":[["b4b70cb6.e18c6"],["b4b70cb6.e18c6"],["3e8cc973.e00ce6"],["38130165.18f05e"]],"info":"ffmpeg -loglevel quiet -rtsp_transport tcp -i rtsp://192.168.1.18:554/user=admin&password=pass&channel=2&stream=1.sdp -reset_timestamps 1 -an -c:v copy -f mp4 -movflags +frag_keyframe+empty_moov+default_base_moof pipe:1\n\n```\n[\n \"-loglevel\",\n \"error\",\n \"-nostats\",\n \"-rtsp_transport\",\n \"+tcp+http+udp+udp_multicast\",\n \"-rtsp_flags\",\n \"+prefer_tcp\",\n \"-i\",\n \"rtsp://192.168.1.18:554/user=admin&password=pass&channel=1&stream=0.sdp\",\n \"-reset_timestamps\",\n \"1\",\n \"-muxdelay\",\n \"0.1\",\n \"-an\",\n \"-c:v\",\n \"copy\",\n \"-f\",\n \"mp4\",\n \"-movflags\",\n \"+frag_every_frame+empty_moov+default_base_moof\",\n \"-min_frag_duration\",\n \"500000\",\n \"-metadata\",\n \"title=garage 1 main\",\n \"-reset_timestamps\",\n \"1\",\n \"-vsync\",\n \"1\",\n \"pipe:1\"\n]\n```\n\n```\n[\n \"-loglevel\",\n \"fatal\",\n \"-nostats\",\n \"-stimeout\",\n \"20000000\",\n \"-rtsp_transport\",\n \"tcp\",\n \"-i\",\n \"rtsp://admin:Purple@2026@192.168.1.174:554/cam/realmonitor?channel=1&subtype=0\",\n \"-reset_timestamps\",\n \"1\",\n \"-muxdelay\",\n \"0.1\",\n \"-c:a\",\n \"copy\",\n \"-c:v\",\n \"copy\",\n \"-f\",\n \"mp4\",\n \"-movflags\",\n \"+frag_every_frame+empty_moov+default_base_moof\",\n \"-min_frag_duration\",\n \"500000\",\n \"-metadata\",\n \"title=front corner main\",\n \"-reset_timestamps\",\n \"1\",\n \"-vsync\",\n \"1\",\n \"pipe:1\",\n \"-progress\",\n \"pipe:3\"\n]\n```\n\n/mnt/surveillance1/mp4frag/%Y/%m/%d/\n\n\"-segment_format_options\",\n \"movflags=+faststart\",\n \n movflags=+faststart:\n \n \"-c:v\",\n \"copy\",\n \"-c:a\",\n \"copy\",\n \"-strftime\",\n \"1\",\n \"-strftime_mkdir\",\n \"1\",\n \"-segment_time\",\n \"30\",\n \"-segment_atclocktime\",\n \"1\",\n \"-reset_timestamps\",\n \"1\",\n \"-f\",\n \"segment\",\n \"-segment_format\",\n \"hls\",\n \"-segment_format_options\",\n \"movflags=+faststart\",\n \"-metadata\",\n \"title=front corner main recording\",\n \"/mnt/surveillance1/mp4frag_%Y_%m_%d_file-%Y%m%d-%s.mp4\",\n \"-y\"\n \n \n \n +default_base_moof\n \n -----------------------\n \n [\n \"-y\",\n \"-use_wallclock_as_timestamps\",\n \"1\",\n \"-loglevel\",\n \"+level+fatal\",\n \"-nostats\",\n \"-stimeout\",\n \"20000000\",\n \"-rtsp_transport\",\n \"tcp\",\n \"-err_detect\",\n \"ignore_err\",\n \"-re\",\n \"-i\",\n \"rtsp://admin:Purple@2026@192.168.1.32:554/cam/realmonitor?channel=1&subtype=0\",\n \"-c:a\",\n \"copy\",\n \"-c:v\",\n \"copy\",\n \"-f\",\n \"mp4\",\n \"-movflags\",\n \"+frag_keyframe+default_base_moof\",\n \"-metadata\",\n \"title=front corner main live\",\n \"pipe:1\",\n \"-progress\",\n \"pipe:3\",\n \"-c:v\",\n \"copy\",\n \"-c:a\",\n \"copy\",\n \"-f\",\n \"ssegment\",\n \"-copytb\",\n \"1\",\n \"-reset_timestamps\",\n \"1\",\n \"-segment_format\",\n \"mp4\",\n \"-segment_atclocktime\",\n \"1\",\n \"-segment_time\",\n \"900\",\n \"-segment_list\",\n \"pipe:4\",\n \"-segment_list_type\",\n \"flat\",\n \"-segment_list_entry_prefix\",\n \"/media/pi/surveillance1/mp4frag/\",\n \"-segment_list_size\",\n \"1\",\n \"-segment_format_options\",\n \"movflags=+frag_keyframe+empty_moov+default_base_moof\",\n \"-metadata\",\n \"title=front corner main recording\",\n \"-strftime\",\n \"1\",\n \"/media/pi/surveillance1/mp4frag/front_corner_main~%Y~%m~%d~%Hh.%Mm.%Ss.mp4\",\n \"-f\",\n \"mp4\",\n \"-vn\",\n \"-c:a\",\n \"copy\",\n \"-movflags\",\n \"+frag_keyframe+empty_moov+default_base_moof\",\n \"-metadata\",\n \"title=front corner main live audio\",\n \"-muxdelay\",\n \"0.1\",\n \"-max_delay\",\n \"0.1\",\n \"-frag_duration\",\n \"2000000\",\n \"pipe:5\"\n]"},{"id":"b4b70cb6.e18c6","type":"mp4frag","z":"4d9df903.2567d8","name":"","hlsPlaylistSize":"10","hlsPlaylistExtra":"5","basePath":"id","repeated":"false","timeLimit":"3000","preBuffer":"1","autoStart":"true","x":680,"y":220,"wires":[[],["522f5cb.734b5a4"]]},{"id":"3e8cc973.e00ce6","type":"subflow:32d61e0f.0490b2","z":"4d9df903.2567d8","name":"","env":[],"x":610,"y":300,"wires":[]},{"id":"38130165.18f05e","type":"subflow:1a29fa75.ef7866","z":"4d9df903.2567d8","name":"progress","env":[],"x":620,"y":380,"wires":[["21b001fa.1ca22e"],["5065e4ea.9d9f4c"]]},{"id":"33b4cab7.2d9b16","type":"inject","z":"4d9df903.2567d8","name":"stop","props":[{"p":"action","v":"{\"command\":\"stop\"}","vt":"json"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payloadType":"str","x":150,"y":360,"wires":[["dc48904a.5770d"]]},{"id":"522f5cb.734b5a4","type":"ffmpeg-spawn","z":"4d9df903.2567d8","name":"","outputs":3,"cmdPath":"","cmdArgs":"[\"-loglevel\",\"+level+fatal\",\"-nostats\",\"-f\",\"mp4\",\"-i\",\"pipe:0\",\"-c\",\"mjpeg\",\"-f\",\"image2pipe\",\"-vframes\",\"1\",\"-vf\",\"scale=trunc(iw*0.75/2)*2:-2\",\"pipe:1\"]","cmdOutputs":2,"killSignal":"SIGTERM","x":980,"y":220,"wires":[[],["cd27717a.1831b"],["4811dbab.5def74"]],"info":"[ffmpeg drawtext](https://ffmpeg.org/ffmpeg-filters.html#drawtext)\n\n[drawtext tutorial](https://ottverse.com/ffmpeg-drawtext-filter-dynamic-overlays-timecode-scrolling-text-credits/)\n\n\n[\n \"-f\",\n \"mp4\",\n \"-i\",\n \"pipe:0\",\n \"-vf\",\n \"drawtext=text='video playback ready':x=(w-text_w)/2:y=(h-text_h)/2:fontsize=120:fontcolor=black:box=1:boxborderw=10:boxcolor=white@0.5\",\n \"-c\",\n \"mjpeg\",\n \"-f\",\n \"image2pipe\",\n \"-vframes\",\n \"1\",\n \"pipe:1\"\n]"},{"id":"d80db3bd.4ce42","type":"inject","z":"4d9df903.2567d8","name":"write start","props":[{"p":"action","v":"{\"command\":\"start\",\"subject\":\"write\",\"preBuffer\":1,\"timeLimit\":3000,\"repeated\":false}","vt":"json"}],"repeat":"30","crontab":"","once":false,"onceDelay":"30","topic":"","payloadType":"str","x":390,"y":160,"wires":[["b4b70cb6.e18c6"]]},{"id":"21b001fa.1ca22e","type":"ui_text","z":"4d9df903.2567d8","group":"","order":4,"width":"2","height":"1","name":"fps","label":"fps","format":"{{msg.payload}}","layout":"col-center","x":830,"y":340,"wires":[]},{"id":"5065e4ea.9d9f4c","type":"ui_text","z":"4d9df903.2567d8","group":"","order":3,"width":"2","height":"1","name":"kbps","label":"kbps","format":"{{msg.payload}}","layout":"col-center","x":830,"y":420,"wires":[]},{"id":"cd27717a.1831b","type":"pipe2jpeg","z":"4d9df903.2567d8","name":"","x":1220,"y":180,"wires":[["4b40a99d.a142d8"]]},{"id":"4811dbab.5def74","type":"subflow:32d61e0f.0490b2","z":"4d9df903.2567d8","name":"","env":[],"x":1210,"y":260,"wires":[]},{"id":"4b40a99d.a142d8","type":"ui_template","z":"4d9df903.2567d8","group":"","name":"","order":5,"width":"4","height":"3","format":"<img ng-src=\"{{src}}\" ng-on-load=\"onLoad()\"/>\n\n<script>\n\n((scope) => {\n\n scope.$watch('msg', (msg) => {\n\n if (msg && msg.payload instanceof ArrayBuffer) {\n\n const arrayBufferView = new Uint8Array(msg.payload);\n \n const blob = new window.Blob([arrayBufferView], { type: 'image/jpeg' });\n \n const urlCreator = window.URL || window.webkitURL;\n \n const objectURL = urlCreator.createObjectURL(blob);\n\n scope.src = objectURL;\n\n scope.onLoad = () => {\n\n urlCreator.revokeObjectURL(objectURL);\n\n }\n\n }\n\n });\n\n})(scope);\n</script>\n","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":true,"templateScope":"local","x":1440,"y":180,"wires":[[]]}]

````

p.s. I think i will build the interval output option into mp4frag to remove the need for the extra inject node. Not sure when that will happen.

---

_[View the full topic](https://discourse.nodered.org/t/how-to-build-a-video-surveillance-system-from-scratch/51077)._
