Skip to content

Instantly share code, notes, and snippets.

@braunku
Last active July 20, 2024 07:27
Show Gist options
  • Save braunku/127b038961f873d1babeecaf5578959e to your computer and use it in GitHub Desktop.
Save braunku/127b038961f873d1babeecaf5578959e to your computer and use it in GitHub Desktop.
RTSP Grab Frame

This example flow shows how to use ffmpeg to grab a frame from an IP Camera's RTSP stream and also display it on a dashboard. It is based on an earlier example from nygma2004 that uses avconv (however I prefer ffmpeg).

Edit flow save directory (or mkdir /home/pi/node-red-static)

You may need to install ffmpeg, on rPi for example; sudo apt-get install ffmpeg

Video Tutorial: https://youtu.be/etYGczFJJW4

[{"id":"c0eaa1e3.932c2","type":"inject","z":"540efea0.5e223","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"2","crontab":"","once":true,"onceDelay":"","topic":"","payload":"","payloadType":"date","x":110,"y":140,"wires":[["ed28c584.34a4a8"]]},{"id":"31b99d4b.895742","type":"exec","z":"540efea0.5e223","command":"ffmpeg -y -i rtsp://192.168.4.93:554/1/h264major -vframes 1 -f image2pipe -vcodec png -","addpay":false,"append":"","useSpawn":"false","timer":"","oldrc":false,"name":"Grab a frame->stdout","x":546.4999923706055,"y":98.25,"wires":[["2d96cf3d.0dffe"],[],[]]},{"id":"2f7c77bd.2eeae8","type":"function","z":"540efea0.5e223","name":"Statistics","func":"var now = new Date();\nvar stat = context.get(\"stat\");\nif (stat===undefined) {\n // Initialize the object in case NR restart\n stat = { \"count\": 0, \"success\": 0, \"rate\": 0.0, \"last\": now};\n}\nif (msg.topic===\"reset\") {\n // Reset message was received: reset statistics\n stat = { \"count\": 0, \"success\": 0, \"rate\": 0.0, \"last\": now};\n} else {\n // Update statistics\n stat.count++;\n if (msg.payload.code===0) {\n stat.success++;\n } \n stat.rate=stat.success/stat.count;\n stat.last=now;\n}\n\n// Create formatted time\nvar yyyy = now.getFullYear();\nvar mm = now.getMonth() < 9 ? \"0\" + (now.getMonth() + 1) : (now.getMonth() + 1); // getMonth() is zero-based\nvar dd = now.getDate() < 10 ? \"0\" + now.getDate() : now.getDate();\nvar hh = now.getHours() < 10 ? \"0\" + now.getHours() : now.getHours();\nvar mmm = now.getMinutes() < 10 ? \"0\" + now.getMinutes() : now.getMinutes();\nvar ss = now.getSeconds() < 10 ? \"0\" + now.getSeconds() : now.getSeconds();\n\nmsg.formattedtime = dd + \".\" + mmm + \".\" + yyyy + \" \" + hh + \":\" + mmm + \":\" + ss;\nmsg.success = stat.success;\nmsg.rate = Math.floor(stat.rate*100);\n\nnode.status({fill:\"blue\",shape:\"ring\",text:\"Frames: \"+msg.success+\" | \"+msg.rate+\"% | Last update: \"+dd + \".\" + mm + \".\" + yyyy + \" \" + hh + \":\" + mmm + \":\" + ss});\n\n\n// Saving data in the context\ncontext.set(\"stat\",stat);\n\nreturn msg;\n\n\n","outputs":1,"noerr":0,"x":727.9999847412109,"y":290.25,"wires":[["122d531a.410bcd","47913bc9.420b84","5f987014.e8cba"]]},{"id":"b1f8f75e.c82c88","type":"inject","z":"540efea0.5e223","name":"Reset stat","props":[{"p":"payload","v":"","vt":"date"},{"p":"topic","v":"reset","vt":"string"}],"repeat":"","crontab":"","once":false,"topic":"reset","payload":"","payloadType":"date","x":120,"y":280,"wires":[["99bfd7f8.6e8f78"]]},{"id":"122d531a.410bcd","type":"ui_text","z":"540efea0.5e223","group":"e16e06ca.f38438","order":1,"width":0,"height":0,"name":"Last time","label":"Last grab","format":"{{msg.formattedtime}}","layout":"row-spread","x":929.0000686645508,"y":210.64999198913574,"wires":[]},{"id":"47913bc9.420b84","type":"ui_text","z":"540efea0.5e223","group":"e16e06ca.f38438","order":2,"width":0,"height":0,"name":"Frame count","label":"Frames grabbed","format":"{{msg.success}}","layout":"row-spread","x":938.899974822998,"y":246.4499807357788,"wires":[]},{"id":"5f987014.e8cba","type":"ui_text","z":"540efea0.5e223","group":"e16e06ca.f38438","order":3,"width":0,"height":0,"name":"Success rate","label":"Success rate","format":"{{msg.rate}} %","layout":"row-spread","x":939.8999481201172,"y":284.4499740600586,"wires":[]},{"id":"3bccc648.4f364a","type":"ui_button","z":"540efea0.5e223","name":"Refresh","group":"675036dd.603328","order":3,"width":0,"height":0,"passthru":false,"label":"Refresh image","color":"","bgcolor":"","icon":"","payload":"","payloadType":"str","topic":"","x":120,"y":340,"wires":[["459f7922.af9ee8"]]},{"id":"14f4f2e2.2bf82d","type":"comment","z":"540efea0.5e223","name":"Frame grabber","info":"This section of the flow is responsible for \ngrabbing a single out of the RTSP feed of the IP\nCamera. It uses avconv to do that which is part\nof the libav-tools for raspberry pi.\n\nThe trigger can be an inject, or a UI button.\nThe statistic node keeps a track of the number of\ngrabbed frames and the success rate (when the\nvideo conversion/grabbing was successful). The \nStatistic node also has a reset input which can \nbe used to periodically reset the stats (e.g.\ndaily, weekly).\n\nI directed the second output of the Exec node to\na file, as the output of the avconv is usually \nquite long and if there are errors you don't\nsee the entire output in the debug window, so in\nthat case just open to output and see what the issue\nis.","x":108.62501525878906,"y":44.00000762939453,"wires":[]},{"id":"ed28c584.34a4a8","type":"change","z":"540efea0.5e223","name":"Set filename","rules":[{"t":"set","p":"payload","pt":"msg","to":"/home/wago/node-red-static/grab.jpg","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":298,"y":153,"wires":[["9ce138d8.f118c8","31b99d4b.895742"]]},{"id":"6eba2f2f.5e0f","type":"template","z":"540efea0.5e223","name":"","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"<img width=\"320px\" height=\"200px\" src=\"data:image/jpg;base64,{{{payload}}}\">","output":"str","x":739.9999961853027,"y":157.25000190734863,"wires":[["dd3172bd.6a821"]]},{"id":"dd3172bd.6a821","type":"ui_template","z":"540efea0.5e223","group":"675036dd.603328","name":"","order":1,"width":"6","height":"5","format":"<div ng-bind-html=\"msg.payload\"></div>","storeOutMessages":true,"fwdInMessages":true,"templateScope":"local","x":926,"y":170.75000095367432,"wires":[[]]},{"id":"2d96cf3d.0dffe","type":"base64","z":"540efea0.5e223","name":"","action":"str","property":"payload","x":760,"y":80,"wires":[["6eba2f2f.5e0f"]]},{"id":"9ce138d8.f118c8","type":"exec","z":"540efea0.5e223","command":"ffmpeg -y -i rtsp://192.168.4.93:554/1/h264major -vframes 1 -qscale:v 2","addpay":true,"append":"","useSpawn":"false","timer":"","oldrc":false,"name":"Grab a frame -> jpg","x":537.9999847412109,"y":215.25,"wires":[[],[],["2f7c77bd.2eeae8"]]},{"id":"a5e46029.81ba4","type":"watch","z":"540efea0.5e223","name":"","files":"/home/wago/node-red-static/grab.jpg","recursive":"","x":650,"y":400,"wires":[["7081ec93.98cfa4"]]},{"id":"7081ec93.98cfa4","type":"ui_text","z":"540efea0.5e223","group":"675036dd.603328","order":2,"width":0,"height":0,"name":"File Size","label":"grab.jpg","format":"{{msg.size}} kb","layout":"row-spread","x":940,"y":400,"wires":[]},{"id":"459f7922.af9ee8","type":"function","z":"540efea0.5e223","name":"Frame grab","func":"var now = new Date();\n// Create formatted time\nvar yyyy = now.getFullYear();\nvar mm = now.getMonth() < 9 ? \"0\" + (now.getMonth() + 1) : (now.getMonth() + 1); // getMonth() is zero-based\nvar dd = now.getDate() < 10 ? \"0\" + now.getDate() : now.getDate();\nvar hh = now.getHours() < 10 ? \"0\" + now.getHours() : now.getHours();\nvar mmm = now.getMinutes() < 10 ? \"0\" + now.getMinutes() : now.getMinutes();\nvar ss = now.getSeconds() < 10 ? \"0\" + now.getSeconds() : now.getSeconds();\n\n// Last update: \"+dd + \".\" + mm + \".\" + yyyy + \" \" + hh + \":\" + mmm + \":\" + ss});\n\n// file path with / at the end\nvar path = \"/home/wago/node-red-static/\"; // This is the path\nvar filename = \"frame_\"+yyyy+mm+dd+\"-\"+hh+mm+ss+\".jpg\"; // file name\nmsg.payload = path + filename; // pass the full path to payload for the exec node to add to the end of the command\nmsg.file = filename; // To be used later to store the information in the DB\nmsg.path = path; // Same as above\nmsg.wwwpath = \"/\"; // Same as above\nmsg.topic = \"store\"; // Flag to store this image in the DB\nmsg.type = \"timelapse\"; // Image type e.g. Front camera, etc.\nmsg.epoch = now.getTime(); // Current timestamp\nmsg.formatteddate = dd + \".\" + mm + \".\" + yyyy + \" \" + hh + \":\" + mmm + \":\" + ss; // Formatted timestamp to be used later\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":290,"y":340,"wires":[["9ce138d8.f118c8","54d35cc4.46d474"]]},{"id":"54d35cc4.46d474","type":"ui_text_input","z":"540efea0.5e223","name":"","label":"Snapshot","tooltip":"","group":"675036dd.603328","order":4,"width":0,"height":0,"passthru":true,"mode":"text","delay":300,"topic":"","x":930,"y":360,"wires":[[]]},{"id":"31689e41.17fd82","type":"ui_button","z":"540efea0.5e223","name":"Reset","group":"e16e06ca.f38438","order":4,"width":0,"height":0,"passthru":false,"label":"Reset","tooltip":"","color":"","bgcolor":"","icon":"","payload":"","payloadType":"str","topic":"","x":110,"y":240,"wires":[["99bfd7f8.6e8f78"]]},{"id":"99bfd7f8.6e8f78","type":"function","z":"540efea0.5e223","name":"Reset","func":"msg.topic = 'reset';\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":490,"y":300,"wires":[["2f7c77bd.2eeae8"]]},{"id":"e16e06ca.f38438","type":"ui_group","name":"Frame Statistics","tab":"7af2d9c8.0a9148","order":2,"disp":true,"width":"6"},{"id":"675036dd.603328","type":"ui_group","name":"Frame Grab","tab":"7af2d9c8.0a9148","order":1,"disp":true,"width":"8","collapse":false},{"id":"7af2d9c8.0a9148","type":"ui_tab","name":"RTSP","icon":"dashboard","order":13,"disabled":false,"hidden":false}]
@daxliniere
Copy link

Hey Kurt! Thanks so much for sharing this. I have tried for a week to get Csongor's (nygma2004) flow working, but had zero luck. Yours works perfectly with my RPi 4!
One of the things I really like about Csongor's flow is that it can attach 1 (or 3) images to an email and also that it can write to an SQL database.
I tried substituting your working ffmpeg exec node into Csongor's flow, but it is still completely broken. Would you have any ideas on how these features could be adapted into your flow?

Any help would be greatly appreciated.
All the best!
Dax.

@daxliniere
Copy link

Ahhh! Actually, I just managed to get it working! I had accidentally used the "Grab a frame->stdout" exec node instead of the "Grab a frame -> jpg" node. I have that all working now!

Do you know of a way to get the video stream working with ffmpeg?

All the best,
Dax.

@heinzr
Copy link

heinzr commented Mar 2, 2021

Thanks Kurt, this helped me a lot!
There is small error in the 'Frame grab' function when setting the 'filename' var: it should be 'hh+mmm+ss', not 'hh+mm+ss'.
The msg.formatteddate is correct.
Best wishes, Heinz

@daxliniere
Copy link

Thanks Heinz. Yes, this occurs about 8 times, I think. Good catch!
Did you manage to get the video stream working, Heinz?

@braunku
Copy link
Author

braunku commented Mar 2, 2021

Thanks, fixed!

@heinzr
Copy link

heinzr commented Mar 2, 2021

Thanks Heinz. Yes, this occurs about 8 times, I think. Good catch!
Did you manage to get the video stream working, Heinz?

Dax, I have not tried that yet, mainly because I am pleasantly surprised by the richness of the camera API (Amcrest). I am not a big fan of doing a ton of video processing on the Pi, so if I can get away with storing the video on the camera SD card, and just copying the files that are interesting to a the home server, I may just do that, and rely on snapshots.

@jarcher12
Copy link

jarcher12 commented Apr 6, 2021

Got it working, i'm a dummy forgot the "quotes" on the rtsp stream. cost me a couple of hours... anyway good work thank you

@jarcher12
Copy link

jarcher12 commented Apr 7, 2021

Hey Kurt! Thanks so much for sharing this. I have tried for a week to get Csongor's (nygma2004) flow working, but had zero luck. Yours works perfectly with my RPi 4!
One of the things I really like about Csongor's flow is that it can attach 1 (or 3) images to an email and also that it can write to an SQL database.
I tried substituting your working ffmpeg exec node into Csongor's flow, but it is still completely broken. Would you have any ideas on how these features could be adapted into your flow?

Any help would be greatly appreciated.
All the best!
Dax.

i send mine to telegram pretty easy:
[{"id":"2601d0fb.257c28","type":"exec","z":"2298ed33.a2ea92","command":"ffmpeg -y -i \"rtsp://192.168.0.15:554/user=admin&password=ieusdip01&channel=6&stream=1.sdp?real_stream--rtp-caching=100\" -vframes 1 -qscale:v 2","addpay":true,"append":"","useSpawn":"false","timer":"","oldrc":false,"name":"Grab a frame -> jpg","x":360,"y":1440,"wires":[[],[],["86ef242c.1a10f8"]]},{"id":"86ef242c.1a10f8","type":"function","z":"2298ed33.a2ea92","name":"Image message","func":"msg.payload = {chatId: \"6240612345\", type:\"photo\", content:\"/home/pi/node-red-static/garagegrab.jpg\", caption:\"Garage door\"};\nreturn msg;\n","outputs":1,"noerr":0,"initialize":"","finalize":"","x":702.0000152587891,"y":1384.75,"wires":[["df0b0c3d.382bd8"]]},{"id":"df0b0c3d.382bd8","type":"telegram sender","z":"2298ed33.a2ea92","name":"Mojopi telegram","bot":"64c2383b.754598","x":972.0000152587891,"y":1384.75,"wires":[[]]},{"id":"64c2383b.754598","type":"telegram bot","botname":"yhjfmpBot","usernames":"","chatids":"","baseapiurl":"","updatemode":"polling","pollinterval":"300","usesocks":false,"sockshost":"","socksport":"6667","socksusername":"anonymous","sockspassword":"","bothost":"","localbotport":"8443","publicbotport":"8443","privatekey":"","certificate":"","useselfsignedcertificate":false,"sslterminated":false,"verboselogging":false}]

@heinzr
Copy link

heinzr commented Apr 7, 2021

The missing "quotes" on the RTSP part of the ffmpeg string cost me quite a bit of time (necessary if there are special characters like '&', which cause bash to break up the string).
I ended up capturing video using ffmpeg in copy mode - everything else put too much load on the RPi 4.

@Gun-neR
Copy link

Gun-neR commented Jun 21, 2021

"quotes"... I had no idea... thanks @heinzr for not only the 2nd mention of this but the reason behind them, which guided me as to where to put these "quotes" (around my camera URL within the Grab a frame commands).
I finally got this to work with my Amcrest camera, now to move onto the next task.
Really loads down my RPi3 if I leave if running (interval) for more then a few seconds... time to upgrade.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment