Having fun with SVG - pushing images

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:

  1. 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>
    
  2. When the camera icon is clicked, a message is send to the server (with topic containing the id of the image element).

  3. 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.

  4. 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...

  5. The flow fetches the images (e.g. from your ip camera), and encodes them to base64.

  6. 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:

svg_video_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

6 Likes

P.S. For anybody wondering: this flow only sends images to camera's that are currently being visualized (to reduce network traffic)...

2 Likes

I wasn't aware that the Trigger node supported topic-dependent repeating.
Anyway that allowed me to simplify the flow a lot (see below).

And a NEW FEATURE has been added: when an alert is detected in the flow, the camera view will be opened automatically in the floorplan (with a red border around it). This reduces the amount of steps you need to execute in the dashboard, to analyze what is going on in your property:

And here is the flow to achieve this:

[{"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":true,"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":1720,"y":1020,"wires":[["503a621e.d0b47c"]]},{"id":"e5ef519e.37daa","type":"function","z":"42b7b639.325dd8","name":"Next image URL","func":"// Each SVG 'image' element gets its own counter (to store the number of the last displayed image from the image sequence)\nconst counterName = 'img_sequence_' + msg.topic;\n\nvar count = context.get(counterName);\n\n// When the last image from the image sequence has been read, start again with the first image of the sequence\nif (!count || count > 47) {\n    count = 1;\n}\n\n// Convert the image number to a 3-digit number with leading zeros (e.g. 3 to \"003\")\nvar countAsString = \"000000000\" + count;\ncountAsString = countAsString.slice(-3);\n\n// Each SVG 'image' element has its own image sequence on my \"node-red-static-resources\" Github repository\nswitch(msg.topic) {\n    case \"#cam_garage_video\":\n        msg.url = `https://github.com/bartbutenaers/node-red-static-resources/blob/main/image_sequence/garage/ezgif-frame-${countAsString}.jpg?raw=true`;\n        break;\n    case \"#cam_living_video\":\n        msg.url = `https://github.com/bartbutenaers/node-red-static-resources/blob/main/image_sequence/living/ezgif-frame-${countAsString}.jpg?raw=true`;\n        break;\n}\n\n// Make sure we fetch the next image, the next time that we arrive in this function node\ncontext.set(counterName, count + 2);\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":1100,"y":960,"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":1290,"y":960,"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\n// Update the data URL of the SVG 'image' element in the SVG\nmsg = {\n    payload: {\n        \"command\": \"set_attribute\",\n        \"selector\": msg.topic,\n        \"attributeName\": \"xlink:href\",\n        \"attributeValue\": imageAsDataUrl\n    }\n}\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":1500,"y":960,"wires":[["58329d91.3fc564"]]},{"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":"topic","topic":"topic","outputs":1,"x":920,"y":960,"wires":[["e5ef519e.37daa"]]},{"id":"503a621e.d0b47c","type":"link out","z":"42b7b639.325dd8","name":"SVG output","links":["e4d546fb.caf698"],"x":1855,"y":1020,"wires":[]},{"id":"e4d546fb.caf698","type":"link in","z":"42b7b639.325dd8","name":"","links":["503a621e.d0b47c"],"x":595,"y":1020,"wires":[["bb362ae5.2ccc38"]]},{"id":"bb362ae5.2ccc38","type":"function","z":"42b7b639.325dd8","name":"Toggle start/stop","func":"msg.payload = msg.alert || context.get(msg.topic) || \"start\";\n\n// Toggle the start/stop of the trigger node, to start/stop pushing images to the dashboard\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":740,"y":960,"wires":[["63a3fd45.302dc4","40f2ce95.0a6b8"]]},{"id":"63a3fd45.302dc4","type":"function","z":"42b7b639.325dd8","name":"Show/hide image","func":"var visibility;\ndebugger;\n\nif (msg.payload === \"start\") {\n    visibility = \"visible\";\n}\nelse {\n    visibility = \"hidden\";\n}\n\nvar newMsg = {\n    payload: [{\n        \"command\": \"set_style\",\n        \"selector\": msg.topic,\n        \"attributeName\": \"visibility\",\n        \"attributeValue\": visibility        \n    }]\n}\n\nif (msg.alert) {\n    switch (msg.alert) {\n        case \"start\":\n            newMsg.payload.push({\n                \"command\": \"set_style\",\n                \"selector\": msg.topic,\n                \"attributeName\": \"outline\",\n                \"attributeValue\": \"red solid 6px\"     \n            });\n            break;\n        case \"stop\":\n            newMsg.payload.push({\n                \"command\": \"set_style\",\n                \"selector\": msg.topic,\n                \"attributeName\": \"outline\",\n                \"attributeValue\": \"white solid 6px\"     \n            });\n            break;\n    }\n}\n\nreturn newMsg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":950,"y":1020,"wires":[["58329d91.3fc564"]]},{"id":"7decac8e.ca5fc4","type":"inject","z":"42b7b639.325dd8","name":"Alert living","props":[{"p":"alert","v":"start","vt":"str"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"#cam_living_video","x":540,"y":900,"wires":[["bb362ae5.2ccc38"]]},{"id":"c3041ad9.5fdef8","type":"inject","z":"42b7b639.325dd8","name":"Normal living","props":[{"p":"alert","v":"stop","vt":"str"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"#cam_living_video","x":550,"y":960,"wires":[["bb362ae5.2ccc38"]]},{"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}]

If anybody has seen other useful features (e.g. in "E-map" software tools), please share it here...

3 Likes

Haha love the garage door video!

Damn, now you have witnessed what is going on in my house.
This is a major violation of my most important house rule:
"what happens in my garage, stays in my garage..." :shushing_face:

2 Likes

Having cameras anywhere issue :smiley:
Just got my latest RED node reaching some limits


The text in TV in English is "MORE"

Deep sorry about being that much off topic

1 Like