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
[{"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 + \".\" + mm + \".\" + 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}]