How to build a video surveillance system from scratch?

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.

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.

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, 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:

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.

[{"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.

1 Like