Hi folks,
After a very heavy week at work I "really" wanted to do something fun this evening...
Had been experimenting a bit last week with "foreign objects" in SVG. Seems that it is also possible to use a html video
element inside your SVG. But unfortunately it is not equally supported by all browsers, and things like positioning can be very different between different browsers ...
So I thought: what if I simply use an SVG image
element, and I push periodically (e.g. 4 times per second) a (base64 encoded) image to it. That way you can 'simulate' a simple video player by using only standard SVG elements, so then I expect it to behave correctly on all browsers...
The steps:
-
Simply add an SVG
image
element below your camera Fontawesome icon:<image id="cam_living_video" x="395" y="130" width="320" height="180" xlink:href="" stroke="blue" stroke-width="2" visibility="hidden" style="outline: 6px solid white;"/> <text id="cam_living_icon" x="690" y="280" font-family="FontAwesome" fill="blue" stroke="white" stroke-width="2" font-size="35" text-anchor="middle" alignment-baseline="middle">fa-video-camera</text>
-
When the camera icon is clicked, a message is send to the server (with topic containing the id of the image element).
-
The flow will toggle the image stream: the first click you start sending images, the second time you stop sending images, and so on... You can do this by simply starting/stopping trigger nodes.
-
The flow will also toggle the visibility of the image element: when you are sending images the image element should be visible, and otherwise it should be invisible. That way your floorplan doesn't become cluttered with paused video footage of all your camera's...
-
The flow fetches the images (e.g. from your ip camera), and encodes them to base64.
-
The flow creates a data url (containing the base64 encoded image) and applies that url to the image element:
payload: { "command": "set_attribute", "selector": msg.image_selector, "attributeName": "xlink:href", "attributeValue": imageAsDataUrl }
This allows you to push images (via the websocket channel) to the dashboard, and show camera footage on top of your floorplan. This way you can very easily and quickly see what is going on in your house, by showing/hiding camera footage:
Here is the demo flow, in case you want to play with it:
[{"id":"58329d91.3fc564","type":"ui_svg_graphics","z":"42b7b639.325dd8","group":"f014eb03.a3c618","order":1,"width":"14","height":"10","svgString":"<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" preserveAspectRatio=\"none meet\" x=\"0\" y=\"0\" viewBox=\"0 0 900 710\" width=\"100%\" height=\"100%\">\n <rect id=\"svgEditorBackground\" x=\"0\" y=\"0\" width=\"900\" height=\"710\" style=\"fill:none;stroke:none;\" />\n <image width=\"889\" height=\"703\" id=\"background\" xlink:href=\"https://www.roomsketcher.com/wp-content/uploads/2016/10/1-Bedroom-Floor-Plans.jpg\" />\n <image id=\"cam_living_video\" x=\"395\" y=\"130\" width=\"320\" height=\"180\" xlink:href=\"\" stroke=\"blue\" stroke-width=\"2\" visibility=\"hidden\" style=\"outline: 6px solid white;\"/>\n <image id=\"cam_garage_video\" x=\"580\" y=\"480\" width=\"320\" height=\"180\" xlink:href=\"\" stroke=\"blue\" stroke-width=\"2\" visibility=\"hidden\" style=\"outline: 6px solid white;\"/>\n <text id=\"cam_living_icon\" x=\"690\" y=\"280\" font-family=\"FontAwesome\" fill=\"blue\" stroke=\"white\" stroke-width=\"2\" font-size=\"35\" text-anchor=\"middle\" alignment-baseline=\"middle\">fa-video-camera</text>\n <text id=\"cam_garage_icon\" x=\"760\" y=\"640\" font-family=\"FontAwesome\" fill=\"blue\" stroke=\"white\" stroke-width=\"2\" font-size=\"35\" text-anchor=\"middle\" alignment-baseline=\"middle\">fa-video-camera</text>\n</svg>","clickableShapes":[{"targetId":"#cam_living_icon","action":"click","payload":"toggle","payloadType":"str","topic":"#cam_living_video"},{"targetId":"#cam_garage_icon","action":"click","payload":"toggle","payloadType":"str","topic":"#cam_garage_video"}],"javascriptHandlers":[],"smilAnimations":[],"bindings":[],"showCoordinates":false,"autoFormatAfterEdit":false,"showBrowserErrors":false,"showBrowserEvents":false,"enableJsDebugging":false,"sendMsgWhenLoaded":false,"outputField":"","editorUrl":"http://drawsvg.org/drawsvg.html","directory":"","panning":"disabled","zooming":"disabled","panOnlyWhenZoomed":false,"doubleClickZoomEnabled":false,"mouseWheelZoomEnabled":false,"name":"","x":1340,"y":820,"wires":[["503a621e.d0b47c"]]},{"id":"d0dc288a.1c6398","type":"http request","z":"42b7b639.325dd8","name":"Get garage image","method":"GET","ret":"bin","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","authType":"","x":890,"y":700,"wires":[["394bde6d.062452"]]},{"id":"5b5d753e.e2344c","type":"function","z":"42b7b639.325dd8","name":"Garage image url","func":"debugger;\n\nvar count = context.get('garage_img_sequence_count');\n\nif (!count || count > 47) {\n count = 1;\n}\n\nvar countAsString = \"000000000\" + count;\ncountAsString = countAsString.slice(-3);\n\nmsg.url = `https://github.com/bartbutenaers/node-red-static-resources/blob/main/image_sequence/garage/ezgif-frame-${countAsString}.jpg?raw=true`\nmsg.image_selector = \"#cam_garage_video\";\n\ncontext.set('garage_img_sequence_count', count + 2);\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":670,"y":700,"wires":[["d0dc288a.1c6398"]]},{"id":"e5ef519e.37daa","type":"function","z":"42b7b639.325dd8","name":"Living image url","func":"debugger;\n\nvar count = context.get('living_img_sequence_count');\n\nif (!count || count > 47) {\n count = 1;\n}\n\nvar countAsString = \"000000000\" + count;\ncountAsString = countAsString.slice(-3);\n\nmsg.url = `https://github.com/bartbutenaers/node-red-static-resources/blob/main/image_sequence/living/ezgif-frame-${countAsString}.jpg?raw=true`\nmsg.image_selector = \"#cam_living_video\";\n\ncontext.set('living_img_sequence_count', count + 2);\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":660,"y":760,"wires":[["694228d5.81f3f8"]]},{"id":"694228d5.81f3f8","type":"http request","z":"42b7b639.325dd8","name":"Get image","method":"GET","ret":"bin","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","authType":"","x":870,"y":760,"wires":[["394bde6d.062452"]]},{"id":"394bde6d.062452","type":"function","z":"42b7b639.325dd8","name":"Update image in SVG","func":"// Convert the (base64 encoded) image as a data URL\nvar imageAsBuffer = new Buffer(msg.payload);\nvar imageAsBase64 = imageAsBuffer.toString('base64');\nvar imageAsDataUrl = \"data:image/jpg;base64,\" + imageAsBase64;\n\nconsole.log(msg.visibility)\n\n// Update the data URL of the image element in the SVG\nmsg = {\n payload: {\n \"command\": \"set_attribute\",\n \"selector\": msg.image_selector,\n \"attributeName\": \"xlink:href\",\n \"attributeValue\": imageAsDataUrl\n }\n}\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":1120,"y":760,"wires":[["58329d91.3fc564"]]},{"id":"ba5f4819.2d4d08","type":"trigger","z":"42b7b639.325dd8","name":"Repeat","op1":"1","op2":"0","op1type":"str","op2type":"str","duration":"-250","extend":false,"overrideDelay":false,"units":"ms","reset":"stop","bytopic":"all","topic":"topic","outputs":1,"x":480,"y":760,"wires":[["e5ef519e.37daa"]]},{"id":"40f2ce95.0a6b8","type":"trigger","z":"42b7b639.325dd8","name":"Repeat","op1":"1","op2":"0","op1type":"str","op2type":"str","duration":"-250","extend":false,"overrideDelay":false,"units":"ms","reset":"stop","bytopic":"all","topic":"topic","outputs":1,"x":480,"y":700,"wires":[["5b5d753e.e2344c"]]},{"id":"503a621e.d0b47c","type":"link out","z":"42b7b639.325dd8","name":"SVG output","links":["e4d546fb.caf698"],"x":1475,"y":820,"wires":[]},{"id":"e4d546fb.caf698","type":"link in","z":"42b7b639.325dd8","name":"","links":["503a621e.d0b47c"],"x":35,"y":760,"wires":[["bb362ae5.2ccc38"]]},{"id":"a795eddb.01c7f","type":"switch","z":"42b7b639.325dd8","name":"","property":"topic","propertyType":"msg","rules":[{"t":"eq","v":"#cam_garage_video","vt":"str"},{"t":"eq","v":"#cam_living_video","vt":"str"}],"checkall":"true","repair":false,"outputs":2,"x":330,"y":760,"wires":[["40f2ce95.0a6b8"],["ba5f4819.2d4d08"]],"outputLabels":["cam_garage_video","cam_living_video"]},{"id":"bb362ae5.2ccc38","type":"function","z":"42b7b639.325dd8","name":"Toggle start/stop","func":"msg.payload = context.get(msg.topic) || \"start\";\n\n// Toggle the start/stop of the image player\nif (msg.payload === \"start\") {\n context.set(msg.topic, \"stop\");\n}\nelse {\n context.set(msg.topic, \"start\");\n}\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":160,"y":760,"wires":[["a795eddb.01c7f","63a3fd45.302dc4"]]},{"id":"63a3fd45.302dc4","type":"function","z":"42b7b639.325dd8","name":"Show/hide image","func":"var visibility;\n\nif (msg.payload === \"start\") {\n visibility = \"visible\";\n}\nelse {\n visibility = \"hidden\";\n}\n\nmsg = {\n payload: {\n \"command\": \"set_style\",\n \"selector\": msg.topic,\n \"attributeName\": \"visibility\",\n \"attributeValue\": visibility \n }\n}\n \nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":370,"y":820,"wires":[["58329d91.3fc564"]]},{"id":"f014eb03.a3c618","type":"ui_group","name":"Floorplan test","tab":"80068970.6e2868","order":1,"disp":true,"width":"14","collapse":false},{"id":"80068970.6e2868","type":"ui_tab","name":"SVG","icon":"dashboard","disabled":false,"hidden":false}]
There are - most probably - lots of improvements possible, because I implemented this quickly tonight. But I think it offers lots of possibilities, and it is a good starting point for a 'constructive' discussion...
[EDIT] Don't confuse this with high quality camera streaming (which currently is being developed in another discussion). In this demo I push some low quality video at a very low frame rates. It are merely thumbnail images that give you an overview of what is going on in your property. The high quality (high speed) images will need to be displayed in a dedicated player (like e.g. in the player which @kevinGodell is developing at the moment)...
Enjoy it !!
Bart