Skip to content

Instantly share code, notes, and snippets.

@braunku
Last active August 3, 2023 12:54
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • 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}]
@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