[{"data":1,"prerenderedAt":1631},["ShallowReactive",2],{"blog-\u002Fblog\u002F2023\u002F12\u002Fdashboard-0-10-0":3,"blog-all-for-related":1630},{"id":4,"title":5,"authors":6,"body":8,"cta":1592,"date":1593,"description":1594,"extension":1595,"image":1596,"lastUpdated":1592,"meta":1597,"navigation":146,"path":1618,"seo":1619,"sitemap":1620,"stem":1621,"subtitle":1622,"tags":1623,"tldr":1592,"video":1592,"__hash__":1629},"blog\u002Fblog\u002F2023\u002F12\u002Fdashboard-0-10-0.md","Building a Custom Video Player in Dashboard 2.0",[7],"joe-pavitt",{"type":9,"value":10,"toc":1578},"minimark",[11,25,44,47,50,62,80,85,88,99,304,307,362,367,373,414,421,425,429,432,566,569,613,620,627,631,634,639,660,926,936,942,946,970,1292,1296,1303,1509,1512,1515,1531,1535,1542,1551,1559,1575],[12,13,14,15,19,20,24],"p",{},"Dashboard 2.0 just got ",[16,17,18],"em",{},"a lot"," more powerful with our new updates to the ",[21,22,23],"code",{},"ui-template"," node. New features added to the node include:",[26,27,28,32,39],"ul",{},[29,30,31],"li",{},"Support for a full Vue component  to be defined using the VueJS Options API.",[29,33,34,35,38],{},"Running of raw JavaScript within ",[21,36,37],{},"\u003Cscript \u002F>"," tags",[29,40,41,42,38],{},"Loading of external dependencies through ",[21,43,37],{},[12,45,46],{},"In this article we're going to deepdive into an example of how you can use this new functionality to build a custom video player.",[12,48,49],{},"We're going to aim for 3 key features:",[51,52,53,56,59],"ol",{},[29,54,55],{},"Emit events into Node-RED when a user plays\u002Fpauses the video",[29,57,58],{},"Allow for the video to be played\u002Fpaused from within Node-RED",[29,60,61],{},"Allow the user to seek to a specific point in the video from within Node-RED",[63,64,66,67,71,72,75,76,79],"div",{"style":65},"background-color: #fff4b9; border:1px solid #ffc400; color: #a27110; padding: 12px; border-radius: 6px; font-style: italic;","Reminder: all new releases of Dashboard are now under the ",[21,68,70],{"style":69},"background-color: transparent;","@flowfuse"," namespace, so you'll need to update to use ",[21,73,74],{"style":69},"@flowfuse\u002Fnode-red-dashboard",", and not ",[21,77,78],{"style":69},"@flowforge",".",[81,82,84],"h2",{"id":83},"building-a-vue-component","Building a Vue Component",[12,86,87],{},"With Dashboard 2.0, we switched over our underlying front-end framework to VueJS. We're aware that not everyone coming into Dashboard 2.0 will be familiar with VueJS.",[12,89,90,91,98],{},"We have a more detailed guide ",[92,93,97],"a",{"href":94,"rel":95},"https:\u002F\u002Fdashboard.flowfuse.com\u002Fnodes\u002Fwidgets\u002Fui-template.html#building-full-vue-components",[96],"nofollow","here",", but we'll also give a quick overview of the elements from Vue \"component\" that we'll use here:",[100,101,106],"pre",{"className":102,"code":103,"language":104,"meta":105,"style":105},"language-html shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","\u003Ctemplate>\n    \u003C!-- Our HTML content will go here -->\n\u003C\u002Ftemplate>\n\n\u003Cscript>\nexport default {\n  name: 'MyComponent',\n  methods: {\n    \u002F\u002F JS methods we want to use across our component will go here\n  },\n  mounted () {\n    \u002F\u002F Code we want to run when our component is loaded will go here\n  },\n  unmounted () {\n    \u002F\u002F Code we want to run when our component is unloaded will go here\n  }\n}\n\u003C\u002Fscript>\n\n\u003Cstyle>\n    \u002F* We can define custom CSS here too *\u002F\n\u003C\u002Fstyle>\n","html","",[21,107,108,124,131,141,148,158,171,193,203,209,215,226,232,237,247,253,259,265,274,279,289,295],{"__ignoreMap":105},[109,110,113,117,121],"span",{"class":111,"line":112},"line",1,[109,114,116],{"class":115},"sMK4o","\u003C",[109,118,120],{"class":119},"swJcz","template",[109,122,123],{"class":115},">\n",[109,125,127],{"class":111,"line":126},2,[109,128,130],{"class":129},"sHwdD","    \u003C!-- Our HTML content will go here -->\n",[109,132,134,137,139],{"class":111,"line":133},3,[109,135,136],{"class":115},"\u003C\u002F",[109,138,120],{"class":119},[109,140,123],{"class":115},[109,142,144],{"class":111,"line":143},4,[109,145,147],{"emptyLinePlaceholder":146},true,"\n",[109,149,151,153,156],{"class":111,"line":150},5,[109,152,116],{"class":115},[109,154,155],{"class":119},"script",[109,157,123],{"class":115},[109,159,161,165,168],{"class":111,"line":160},6,[109,162,164],{"class":163},"s7zQu","export",[109,166,167],{"class":163}," default",[109,169,170],{"class":115}," {\n",[109,172,174,177,180,183,187,190],{"class":111,"line":173},7,[109,175,176],{"class":119},"  name",[109,178,179],{"class":115},":",[109,181,182],{"class":115}," '",[109,184,186],{"class":185},"sfazB","MyComponent",[109,188,189],{"class":115},"'",[109,191,192],{"class":115},",\n",[109,194,196,199,201],{"class":111,"line":195},8,[109,197,198],{"class":119},"  methods",[109,200,179],{"class":115},[109,202,170],{"class":115},[109,204,206],{"class":111,"line":205},9,[109,207,208],{"class":129},"    \u002F\u002F JS methods we want to use across our component will go here\n",[109,210,212],{"class":111,"line":211},10,[109,213,214],{"class":115},"  },\n",[109,216,218,221,224],{"class":111,"line":217},11,[109,219,220],{"class":119},"  mounted",[109,222,223],{"class":115}," ()",[109,225,170],{"class":115},[109,227,229],{"class":111,"line":228},12,[109,230,231],{"class":129},"    \u002F\u002F Code we want to run when our component is loaded will go here\n",[109,233,235],{"class":111,"line":234},13,[109,236,214],{"class":115},[109,238,240,243,245],{"class":111,"line":239},14,[109,241,242],{"class":119},"  unmounted",[109,244,223],{"class":115},[109,246,170],{"class":115},[109,248,250],{"class":111,"line":249},15,[109,251,252],{"class":129},"    \u002F\u002F Code we want to run when our component is unloaded will go here\n",[109,254,256],{"class":111,"line":255},16,[109,257,258],{"class":115},"  }\n",[109,260,262],{"class":111,"line":261},17,[109,263,264],{"class":115},"}\n",[109,266,268,270,272],{"class":111,"line":267},18,[109,269,136],{"class":115},[109,271,155],{"class":119},[109,273,123],{"class":115},[109,275,277],{"class":111,"line":276},19,[109,278,147],{"emptyLinePlaceholder":146},[109,280,282,284,287],{"class":111,"line":281},20,[109,283,116],{"class":115},[109,285,286],{"class":119},"style",[109,288,123],{"class":115},[109,290,292],{"class":111,"line":291},21,[109,293,294],{"class":129},"    \u002F* We can define custom CSS here too *\u002F\n",[109,296,298,300,302],{"class":111,"line":297},22,[109,299,136],{"class":115},[109,301,286],{"class":119},[109,303,123],{"class":115},[12,305,306],{},"Some quick gotchas to note:",[26,308,309,315,321,327,333,350],{},[29,310,311,314],{},[21,312,313],{},"\u003Cdiv>{{ msg }}\u003C\u002Fdiv>"," - is an example of how you render variables into the HTML.",[29,316,317,320],{},[21,318,319],{},"\u003Cdiv v-if=\"myVar\">\u003C\u002Fdiv>"," - lets you conditionally show\u002Fhide content based on a variable.",[29,322,323,326],{},[21,324,325],{},"\u003Cdiv v-for=\"item in items\">\u003C\u002Fdiv>"," - lets you loop over an array of items and render them into the HTML.",[29,328,329,332],{},[21,330,331],{},"\u003Cdiv @click=\"myMethod\">\u003C\u002Fdiv>"," - lets you bind a method to an event, in this case, when the user clicks on the div.",[29,334,335,338,339,341,342,345,346,349],{},[21,336,337],{},"\u003Cdiv :class=\"{ 'my-class': isActive }\">\u003C\u002Fdiv>"," - ",[21,340,179],{}," is a way to define a \"bound\" property. In this case, the class ",[21,343,344],{},"my-class"," will be applied when ",[21,347,348],{},"isActive"," is true.",[29,351,352,355,356,358,359,79],{},[21,353,354],{},"console.log(this.myVar)"," - when you're writing code inside the ",[21,357,37],{}," tags, you can access Component variables and methods using ",[21,360,361],{},"this",[363,364,366],"h3",{"id":365},"built-in-extras","Built-in Extras",[12,368,369,370,372],{},"In addition to building a component from scratch, we'll also utilize some built-in features of ",[21,371,23],{}," too. These will be:",[26,374,375,401],{},[29,376,377,381],{},[378,379,380],"strong",{},"Variables:",[26,382,383,389,395],{},[29,384,385,388],{},[21,386,387],{},"id"," - The unique ID for this node in Node-RED",[29,390,391,394],{},[21,392,393],{},"msg"," - The message that was most recently received into the node",[29,396,397,400],{},[21,398,399],{},"$socket"," - The underlying SocketIO connection to Node-RED. Use this to listen to any incoming events, and send new ones back.",[29,402,403,406],{},[378,404,405],{},"Functions:",[26,407,408],{},[29,409,410,413],{},[21,411,412],{},"send(payload)"," - Send a message back to Node-RED",[12,415,416,417,79],{},"As above, we have more detailed documentation on these features ",[92,418,97],{"href":419,"rel":420},"https:\u002F\u002Fdashboard.flowfuse.com\u002Fnodes\u002Fwidgets\u002Fui-template.html#built-in-functionality",[96],[81,422,424],{"id":423},"building-the-video-player","Building the Video Player",[363,426,428],{"id":427},"defining-the-content-html","Defining the Content (HTML)",[12,430,431],{},"We're going to start by adding a basic HTML video player:",[100,433,435],{"className":102,"code":434,"language":104,"meta":105,"style":105},"\u003Ctemplate>\n    \u003Cvideo ref=\"my-video\" style=\"width: 100%\" controls @play=\"onPlay\" @pause=\"onPause\">\n        \u003Csource src=\"http:\u002F\u002Fcommondatastorage.googleapis.com\u002Fgtv-videos-bucket\u002Fsample\u002FBigBuckBunny.mp4\" type=\"video\u002Fmp4\">\n        Your browser does not support the video tag.\n    \u003C\u002Fvideo>\n\u003C\u002Ftemplate>\n",[21,436,437,445,509,543,549,558],{"__ignoreMap":105},[109,438,439,441,443],{"class":111,"line":112},[109,440,116],{"class":115},[109,442,120],{"class":119},[109,444,123],{"class":115},[109,446,447,450,453,457,460,463,466,468,471,473,475,478,480,483,486,488,490,493,495,498,500,502,505,507],{"class":111,"line":126},[109,448,449],{"class":115},"    \u003C",[109,451,452],{"class":119},"video",[109,454,456],{"class":455},"spNyl"," ref",[109,458,459],{"class":115},"=",[109,461,462],{"class":115},"\"",[109,464,465],{"class":185},"my-video",[109,467,462],{"class":115},[109,469,470],{"class":455}," style",[109,472,459],{"class":115},[109,474,462],{"class":115},[109,476,477],{"class":185},"width: 100%",[109,479,462],{"class":115},[109,481,482],{"class":455}," controls",[109,484,485],{"class":455}," @play",[109,487,459],{"class":115},[109,489,462],{"class":115},[109,491,492],{"class":185},"onPlay",[109,494,462],{"class":115},[109,496,497],{"class":455}," @pause",[109,499,459],{"class":115},[109,501,462],{"class":115},[109,503,504],{"class":185},"onPause",[109,506,462],{"class":115},[109,508,123],{"class":115},[109,510,511,514,517,520,522,524,527,529,532,534,536,539,541],{"class":111,"line":133},[109,512,513],{"class":115},"        \u003C",[109,515,516],{"class":119},"source",[109,518,519],{"class":455}," src",[109,521,459],{"class":115},[109,523,462],{"class":115},[109,525,526],{"class":185},"http:\u002F\u002Fcommondatastorage.googleapis.com\u002Fgtv-videos-bucket\u002Fsample\u002FBigBuckBunny.mp4",[109,528,462],{"class":115},[109,530,531],{"class":455}," type",[109,533,459],{"class":115},[109,535,462],{"class":115},[109,537,538],{"class":185},"video\u002Fmp4",[109,540,462],{"class":115},[109,542,123],{"class":115},[109,544,545],{"class":111,"line":143},[109,546,548],{"class":547},"sTEyZ","        Your browser does not support the video tag.\n",[109,550,551,554,556],{"class":111,"line":150},[109,552,553],{"class":115},"    \u003C\u002F",[109,555,452],{"class":119},[109,557,123],{"class":115},[109,559,560,562,564],{"class":111,"line":160},[109,561,136],{"class":115},[109,563,120],{"class":119},[109,565,123],{"class":115},[12,567,568],{},"A few things of importance to note here:",[26,570,571,585,591,604],{},[29,572,573,576,577,580,581,584],{},[21,574,575],{},"ref"," is Vue's replacement for ",[21,578,579],{},"document.getElementById()",". This is copied to each instance of the component, meaning we can call ",[21,582,583],{},"this.$refs['my-video']"," to access the video element, and this doesn't break when duplicating the widget multiple times in Dashboard.",[29,586,587,590],{},[21,588,589],{},"style=\"\""," is required here to ensure the video fills the group\u002Fwrapper that it is contained within.",[29,592,593,596,597,600,601,603],{},[21,594,595],{},"@play="," is Vue's way of binding onto the standard ",[21,598,599],{},"onplay"," event listener available on HTML video players. We'll define the ",[21,602,492],{}," method in the next section.",[29,605,606,609,610,612],{},[21,607,608],{},"@pause="," is our event listener for when the video is paused by the user. As with ",[21,611,492],{},", we'll define this shortly.",[12,614,615,616,619],{},"With ",[16,617,618],{},"just"," the above defined, we end up with a standard video player rendered:",[12,621,622],{},[623,624],"img",{"alt":625,"src":626},"HTML5 Video Player rendered in Dashboard","\u002Fblog\u002F2023\u002F12\u002Fimages\u002Fdashboard-video-1.png",[363,628,630],{"id":629},"defining-the-behaviors-vuejs","Defining the Behaviors (VueJS)",[12,632,633],{},"Now we begin to build our Vue component. Referring back to our earlier set of features, we'll tackle these one at a time.",[635,636,638],"h4",{"id":637},"_1-emitting-events-to-node-red-on-playpause","1. Emitting Events to Node-RED on Play\u002FPause",[12,640,641,642,645,646,648,649,651,652,655,656,659],{},"We can use ",[21,643,644],{},"methods"," to define our ",[21,647,492],{}," and ",[21,650,504],{}," functions that are called ",[21,653,654],{},"@play","\u002F",[21,657,658],{},"@pause"," respectively.",[100,661,663],{"className":102,"code":662,"language":104,"meta":105,"style":105},"\u003Cscript>\nexport default {\n  name: 'MyVideoPlayer',\n  methods: {\n    capture (eventType) {\n        \u002F\u002F let's define our own function that can be called onPlay\u002FonPause\n        \u002F\u002F this prevents duplicated code across the two methods\n\n        \u002F\u002F get the Video's DOM element\n        const video = this.$refs['my-video']\n\n        \u002F\u002F send a msg to Node-RED using built-in \"send\" fcn\n        this.send({\n            \u002F\u002F specify which action is taking place\n            event: eventType,\n            \u002F\u002F use Vue's $refs to get the video's currentTime\n            time: video.currentTime\n        })\n    },\n    onPlay () {\n        this.capture('play')\n    },\n    onPause () {\n        this.capture('pause')\n    }\n  }\n}\n\u003C\u002Fscript>\n",[21,664,665,673,681,696,704,721,726,731,735,740,769,773,778,793,798,810,815,829,837,842,851,869,873,883,901,907,912,917],{"__ignoreMap":105},[109,666,667,669,671],{"class":111,"line":112},[109,668,116],{"class":115},[109,670,155],{"class":119},[109,672,123],{"class":115},[109,674,675,677,679],{"class":111,"line":126},[109,676,164],{"class":163},[109,678,167],{"class":163},[109,680,170],{"class":115},[109,682,683,685,687,689,692,694],{"class":111,"line":133},[109,684,176],{"class":119},[109,686,179],{"class":115},[109,688,182],{"class":115},[109,690,691],{"class":185},"MyVideoPlayer",[109,693,189],{"class":115},[109,695,192],{"class":115},[109,697,698,700,702],{"class":111,"line":143},[109,699,198],{"class":119},[109,701,179],{"class":115},[109,703,170],{"class":115},[109,705,706,709,712,716,719],{"class":111,"line":150},[109,707,708],{"class":119},"    capture",[109,710,711],{"class":115}," (",[109,713,715],{"class":714},"sHdIc","eventType",[109,717,718],{"class":115},")",[109,720,170],{"class":115},[109,722,723],{"class":111,"line":160},[109,724,725],{"class":129},"        \u002F\u002F let's define our own function that can be called onPlay\u002FonPause\n",[109,727,728],{"class":111,"line":173},[109,729,730],{"class":129},"        \u002F\u002F this prevents duplicated code across the two methods\n",[109,732,733],{"class":111,"line":195},[109,734,147],{"emptyLinePlaceholder":146},[109,736,737],{"class":111,"line":205},[109,738,739],{"class":129},"        \u002F\u002F get the Video's DOM element\n",[109,741,742,745,748,751,754,757,760,762,764,766],{"class":111,"line":211},[109,743,744],{"class":455},"        const",[109,746,747],{"class":547}," video",[109,749,750],{"class":115}," =",[109,752,753],{"class":115}," this.",[109,755,756],{"class":547},"$refs",[109,758,759],{"class":119},"[",[109,761,189],{"class":115},[109,763,465],{"class":185},[109,765,189],{"class":115},[109,767,768],{"class":119},"]\n",[109,770,771],{"class":111,"line":217},[109,772,147],{"emptyLinePlaceholder":146},[109,774,775],{"class":111,"line":228},[109,776,777],{"class":129},"        \u002F\u002F send a msg to Node-RED using built-in \"send\" fcn\n",[109,779,780,783,787,790],{"class":111,"line":234},[109,781,782],{"class":115},"        this.",[109,784,786],{"class":785},"s2Zo4","send",[109,788,789],{"class":119},"(",[109,791,792],{"class":115},"{\n",[109,794,795],{"class":111,"line":239},[109,796,797],{"class":129},"            \u002F\u002F specify which action is taking place\n",[109,799,800,803,805,808],{"class":111,"line":249},[109,801,802],{"class":119},"            event",[109,804,179],{"class":115},[109,806,807],{"class":547}," eventType",[109,809,192],{"class":115},[109,811,812],{"class":111,"line":255},[109,813,814],{"class":129},"            \u002F\u002F use Vue's $refs to get the video's currentTime\n",[109,816,817,820,822,824,826],{"class":111,"line":261},[109,818,819],{"class":119},"            time",[109,821,179],{"class":115},[109,823,747],{"class":547},[109,825,79],{"class":115},[109,827,828],{"class":547},"currentTime\n",[109,830,831,834],{"class":111,"line":267},[109,832,833],{"class":115},"        }",[109,835,836],{"class":119},")\n",[109,838,839],{"class":111,"line":276},[109,840,841],{"class":115},"    },\n",[109,843,844,847,849],{"class":111,"line":281},[109,845,846],{"class":119},"    onPlay",[109,848,223],{"class":115},[109,850,170],{"class":115},[109,852,853,855,858,860,862,865,867],{"class":111,"line":291},[109,854,782],{"class":115},[109,856,857],{"class":785},"capture",[109,859,789],{"class":119},[109,861,189],{"class":115},[109,863,864],{"class":185},"play",[109,866,189],{"class":115},[109,868,836],{"class":119},[109,870,871],{"class":111,"line":297},[109,872,841],{"class":115},[109,874,876,879,881],{"class":111,"line":875},23,[109,877,878],{"class":119},"    onPause",[109,880,223],{"class":115},[109,882,170],{"class":115},[109,884,886,888,890,892,894,897,899],{"class":111,"line":885},24,[109,887,782],{"class":115},[109,889,857],{"class":785},[109,891,789],{"class":119},[109,893,189],{"class":115},[109,895,896],{"class":185},"pause",[109,898,189],{"class":115},[109,900,836],{"class":119},[109,902,904],{"class":111,"line":903},25,[109,905,906],{"class":115},"    }\n",[109,908,910],{"class":111,"line":909},26,[109,911,258],{"class":115},[109,913,915],{"class":111,"line":914},27,[109,916,264],{"class":115},[109,918,920,922,924],{"class":111,"line":919},28,[109,921,136],{"class":115},[109,923,155],{"class":119},[109,925,123],{"class":115},[12,927,928,929,931,932,935],{},"With this functionality in place, we can wire the ",[21,930,23],{}," node to a ",[21,933,934],{},"debug"," node, and see the following when we play\u002Fpause the video:",[12,937,938],{},[623,939],{"alt":940,"src":941},"Example debug output when our custom build video player is played\u002Fpaused","\u002Fblog\u002F2023\u002F12\u002Fimages\u002Fdashboard-video-2.png",[635,943,945],{"id":944},"_2-remote-control-of-playpause-from-node-red","2. Remote control of play\u002Fpause from Node-RED",[12,947,948,949,951,952,954,955,958,959,648,962,965,966,969],{},"We can use the built-in ",[21,950,399],{}," variable to listen for incoming events from Node-RED. When Dashboard 2.0's nodes receive a ",[21,953,393],{}," inside Node-RED, they send a ",[21,956,957],{},"msg-input:\u003Cnode-id>"," event to the Dashboard client. We can listen for this event and then call the ",[21,960,961],{},"play()",[21,963,964],{},"pause()"," methods on the video element, depending on any properties of that message, in this case, the ",[21,967,968],{},"msg.payload.event"," value.",[100,971,973],{"className":102,"code":972,"language":104,"meta":105,"style":105},"\u003Cscript>\nexport default {\n  name: 'MyVideoPlayer',\n  methods: {\n    \u002F\u002F ...\n  },\n  mounted () {\n    \u002F\u002F listen for incoming msg's from Node-RED\n    \u002F\u002F note our topic is \"msg-input\" + the node's unique ID\n    this.$socket.on('msg-input:' + this.id, (msg) => {\n        \u002F\u002F get the Video's DOM element\n        const video = this.$refs['my-video']\n\n        \u002F\u002F if the event is \"play\", call the video's play() method\n        if (msg.payload?.event === 'play') {\n            video.play()\n        }\n\n        \u002F\u002F if the event is \"pause\", call the video's pause() method\n        if (msg.payload?.event === 'pause') {\n            video.pause()\n        }\n    })\n  },\n  unmounted () {\n    \u002F\u002F make sure we remove our listeners when the widget is destroyed\n    this.$socket.off(`msg-input:${this.id}`)\n  }\n}\n\u003C\u002Fscript>\n",[21,974,975,983,991,1005,1013,1018,1022,1030,1035,1040,1082,1086,1108,1112,1117,1151,1163,1168,1172,1177,1205,1215,1219,1226,1230,1238,1243,1274,1278,1283],{"__ignoreMap":105},[109,976,977,979,981],{"class":111,"line":112},[109,978,116],{"class":115},[109,980,155],{"class":119},[109,982,123],{"class":115},[109,984,985,987,989],{"class":111,"line":126},[109,986,164],{"class":163},[109,988,167],{"class":163},[109,990,170],{"class":115},[109,992,993,995,997,999,1001,1003],{"class":111,"line":133},[109,994,176],{"class":119},[109,996,179],{"class":115},[109,998,182],{"class":115},[109,1000,691],{"class":185},[109,1002,189],{"class":115},[109,1004,192],{"class":115},[109,1006,1007,1009,1011],{"class":111,"line":143},[109,1008,198],{"class":119},[109,1010,179],{"class":115},[109,1012,170],{"class":115},[109,1014,1015],{"class":111,"line":150},[109,1016,1017],{"class":129},"    \u002F\u002F ...\n",[109,1019,1020],{"class":111,"line":160},[109,1021,214],{"class":115},[109,1023,1024,1026,1028],{"class":111,"line":173},[109,1025,220],{"class":119},[109,1027,223],{"class":115},[109,1029,170],{"class":115},[109,1031,1032],{"class":111,"line":195},[109,1033,1034],{"class":129},"    \u002F\u002F listen for incoming msg's from Node-RED\n",[109,1036,1037],{"class":111,"line":205},[109,1038,1039],{"class":129},"    \u002F\u002F note our topic is \"msg-input\" + the node's unique ID\n",[109,1041,1042,1045,1047,1049,1052,1054,1056,1059,1061,1064,1066,1068,1071,1073,1075,1077,1080],{"class":111,"line":211},[109,1043,1044],{"class":115},"    this.",[109,1046,399],{"class":547},[109,1048,79],{"class":115},[109,1050,1051],{"class":785},"on",[109,1053,789],{"class":119},[109,1055,189],{"class":115},[109,1057,1058],{"class":185},"msg-input:",[109,1060,189],{"class":115},[109,1062,1063],{"class":115}," +",[109,1065,753],{"class":115},[109,1067,387],{"class":547},[109,1069,1070],{"class":115},",",[109,1072,711],{"class":115},[109,1074,393],{"class":714},[109,1076,718],{"class":115},[109,1078,1079],{"class":455}," =>",[109,1081,170],{"class":115},[109,1083,1084],{"class":111,"line":217},[109,1085,739],{"class":129},[109,1087,1088,1090,1092,1094,1096,1098,1100,1102,1104,1106],{"class":111,"line":228},[109,1089,744],{"class":455},[109,1091,747],{"class":547},[109,1093,750],{"class":115},[109,1095,753],{"class":115},[109,1097,756],{"class":547},[109,1099,759],{"class":119},[109,1101,189],{"class":115},[109,1103,465],{"class":185},[109,1105,189],{"class":115},[109,1107,768],{"class":119},[109,1109,1110],{"class":111,"line":234},[109,1111,147],{"emptyLinePlaceholder":146},[109,1113,1114],{"class":111,"line":239},[109,1115,1116],{"class":129},"        \u002F\u002F if the event is \"play\", call the video's play() method\n",[109,1118,1119,1122,1124,1126,1128,1131,1134,1137,1140,1142,1144,1146,1149],{"class":111,"line":249},[109,1120,1121],{"class":163},"        if",[109,1123,711],{"class":119},[109,1125,393],{"class":547},[109,1127,79],{"class":115},[109,1129,1130],{"class":547},"payload",[109,1132,1133],{"class":115},"?.",[109,1135,1136],{"class":547},"event",[109,1138,1139],{"class":115}," ===",[109,1141,182],{"class":115},[109,1143,864],{"class":185},[109,1145,189],{"class":115},[109,1147,1148],{"class":119},") ",[109,1150,792],{"class":115},[109,1152,1153,1156,1158,1160],{"class":111,"line":255},[109,1154,1155],{"class":547},"            video",[109,1157,79],{"class":115},[109,1159,864],{"class":785},[109,1161,1162],{"class":119},"()\n",[109,1164,1165],{"class":111,"line":261},[109,1166,1167],{"class":115},"        }\n",[109,1169,1170],{"class":111,"line":267},[109,1171,147],{"emptyLinePlaceholder":146},[109,1173,1174],{"class":111,"line":276},[109,1175,1176],{"class":129},"        \u002F\u002F if the event is \"pause\", call the video's pause() method\n",[109,1178,1179,1181,1183,1185,1187,1189,1191,1193,1195,1197,1199,1201,1203],{"class":111,"line":281},[109,1180,1121],{"class":163},[109,1182,711],{"class":119},[109,1184,393],{"class":547},[109,1186,79],{"class":115},[109,1188,1130],{"class":547},[109,1190,1133],{"class":115},[109,1192,1136],{"class":547},[109,1194,1139],{"class":115},[109,1196,182],{"class":115},[109,1198,896],{"class":185},[109,1200,189],{"class":115},[109,1202,1148],{"class":119},[109,1204,792],{"class":115},[109,1206,1207,1209,1211,1213],{"class":111,"line":291},[109,1208,1155],{"class":547},[109,1210,79],{"class":115},[109,1212,896],{"class":785},[109,1214,1162],{"class":119},[109,1216,1217],{"class":111,"line":297},[109,1218,1167],{"class":115},[109,1220,1221,1224],{"class":111,"line":875},[109,1222,1223],{"class":115},"    }",[109,1225,836],{"class":119},[109,1227,1228],{"class":111,"line":885},[109,1229,214],{"class":115},[109,1231,1232,1234,1236],{"class":111,"line":903},[109,1233,242],{"class":119},[109,1235,223],{"class":115},[109,1237,170],{"class":115},[109,1239,1240],{"class":111,"line":909},[109,1241,1242],{"class":129},"    \u002F\u002F make sure we remove our listeners when the widget is destroyed\n",[109,1244,1245,1247,1249,1251,1254,1256,1259,1261,1264,1267,1269,1272],{"class":111,"line":914},[109,1246,1044],{"class":115},[109,1248,399],{"class":547},[109,1250,79],{"class":115},[109,1252,1253],{"class":785},"off",[109,1255,789],{"class":119},[109,1257,1258],{"class":115},"`",[109,1260,1058],{"class":185},[109,1262,1263],{"class":115},"${",[109,1265,1266],{"class":115},"this.",[109,1268,387],{"class":547},[109,1270,1271],{"class":115},"}`",[109,1273,836],{"class":119},[109,1275,1276],{"class":111,"line":919},[109,1277,258],{"class":115},[109,1279,1281],{"class":111,"line":1280},29,[109,1282,264],{"class":115},[109,1284,1286,1288,1290],{"class":111,"line":1285},30,[109,1287,136],{"class":115},[109,1289,155],{"class":119},[109,1291,123],{"class":115},[635,1293,1295],{"id":1294},"_3-seeking-to-a-specific-point-in-the-video-from-within-node-red","3. Seeking to a specific point in the video from within Node-RED",[12,1297,1298,1299,1302],{},"With the ",[21,1300,1301],{},"on('msg-input')"," listener in place, we can now extend our handler to handle seeking to a specific point in the video.",[100,1304,1306],{"className":102,"code":1305,"language":104,"meta":105,"style":105},"\u003Cscript>\nexport default {\n  name: 'MyVideoPlayer',\n  methods: {\n    \u002F\u002F ...\n  },\n  mounted () {\n    \u002F\u002F ...\n    this.$socket.on('msg-input:' + this.id, (msg) => {\n        \u002F\u002F ... other handlers\n\n        \u002F\u002F if the event is \"seek\", call the video's currentTime() method\n        if (msg.payload?.event === 'seek') {\n            video.currentTime = msg.payload.currentTime\n        }\n    })\n  },\n  unmounted () {\n    \u002F\u002F ...\n  }\n}\n\u003C\u002Fscript>\n",[21,1307,1308,1316,1324,1338,1346,1350,1354,1362,1366,1402,1407,1411,1416,1445,1467,1471,1477,1481,1489,1493,1497,1501],{"__ignoreMap":105},[109,1309,1310,1312,1314],{"class":111,"line":112},[109,1311,116],{"class":115},[109,1313,155],{"class":119},[109,1315,123],{"class":115},[109,1317,1318,1320,1322],{"class":111,"line":126},[109,1319,164],{"class":163},[109,1321,167],{"class":163},[109,1323,170],{"class":115},[109,1325,1326,1328,1330,1332,1334,1336],{"class":111,"line":133},[109,1327,176],{"class":119},[109,1329,179],{"class":115},[109,1331,182],{"class":115},[109,1333,691],{"class":185},[109,1335,189],{"class":115},[109,1337,192],{"class":115},[109,1339,1340,1342,1344],{"class":111,"line":143},[109,1341,198],{"class":119},[109,1343,179],{"class":115},[109,1345,170],{"class":115},[109,1347,1348],{"class":111,"line":150},[109,1349,1017],{"class":129},[109,1351,1352],{"class":111,"line":160},[109,1353,214],{"class":115},[109,1355,1356,1358,1360],{"class":111,"line":173},[109,1357,220],{"class":119},[109,1359,223],{"class":115},[109,1361,170],{"class":115},[109,1363,1364],{"class":111,"line":195},[109,1365,1017],{"class":129},[109,1367,1368,1370,1372,1374,1376,1378,1380,1382,1384,1386,1388,1390,1392,1394,1396,1398,1400],{"class":111,"line":205},[109,1369,1044],{"class":115},[109,1371,399],{"class":547},[109,1373,79],{"class":115},[109,1375,1051],{"class":785},[109,1377,789],{"class":119},[109,1379,189],{"class":115},[109,1381,1058],{"class":185},[109,1383,189],{"class":115},[109,1385,1063],{"class":115},[109,1387,753],{"class":115},[109,1389,387],{"class":547},[109,1391,1070],{"class":115},[109,1393,711],{"class":115},[109,1395,393],{"class":714},[109,1397,718],{"class":115},[109,1399,1079],{"class":455},[109,1401,170],{"class":115},[109,1403,1404],{"class":111,"line":211},[109,1405,1406],{"class":129},"        \u002F\u002F ... other handlers\n",[109,1408,1409],{"class":111,"line":217},[109,1410,147],{"emptyLinePlaceholder":146},[109,1412,1413],{"class":111,"line":228},[109,1414,1415],{"class":129},"        \u002F\u002F if the event is \"seek\", call the video's currentTime() method\n",[109,1417,1418,1420,1422,1424,1426,1428,1430,1432,1434,1436,1439,1441,1443],{"class":111,"line":234},[109,1419,1121],{"class":163},[109,1421,711],{"class":119},[109,1423,393],{"class":547},[109,1425,79],{"class":115},[109,1427,1130],{"class":547},[109,1429,1133],{"class":115},[109,1431,1136],{"class":547},[109,1433,1139],{"class":115},[109,1435,182],{"class":115},[109,1437,1438],{"class":185},"seek",[109,1440,189],{"class":115},[109,1442,1148],{"class":119},[109,1444,792],{"class":115},[109,1446,1447,1449,1451,1454,1456,1459,1461,1463,1465],{"class":111,"line":239},[109,1448,1155],{"class":547},[109,1450,79],{"class":115},[109,1452,1453],{"class":547},"currentTime",[109,1455,750],{"class":115},[109,1457,1458],{"class":547}," msg",[109,1460,79],{"class":115},[109,1462,1130],{"class":547},[109,1464,79],{"class":115},[109,1466,828],{"class":547},[109,1468,1469],{"class":111,"line":249},[109,1470,1167],{"class":115},[109,1472,1473,1475],{"class":111,"line":255},[109,1474,1223],{"class":115},[109,1476,836],{"class":119},[109,1478,1479],{"class":111,"line":261},[109,1480,214],{"class":115},[109,1482,1483,1485,1487],{"class":111,"line":267},[109,1484,242],{"class":119},[109,1486,223],{"class":115},[109,1488,170],{"class":115},[109,1490,1491],{"class":111,"line":276},[109,1492,1017],{"class":129},[109,1494,1495],{"class":111,"line":281},[109,1496,258],{"class":115},[109,1498,1499],{"class":111,"line":291},[109,1500,264],{"class":115},[109,1502,1503,1505,1507],{"class":111,"line":297},[109,1504,136],{"class":115},[109,1506,155],{"class":119},[109,1508,123],{"class":115},[12,1510,1511],{},"and with that, we now have a Dashboard 2.0 widget to display a video, that can be controlled from Node-RED, and logs details of user activity back into Node-RED.",[12,1513,1514],{},"Other features available with the UI Template are detailed in the online documentation, and include:",[26,1516,1517,1524],{},[29,1518,1519],{},[92,1520,1523],{"href":1521,"rel":1522},"https:\u002F\u002Fdashboard.flowfuse.com\u002Fnodes\u002Fwidgets\u002Fui-template.html#loading-external-dependencies",[96],"Loading External Dependencies",[29,1525,1526],{},[92,1527,1530],{"href":1528,"rel":1529},"https:\u002F\u002Fdashboard.flowfuse.com\u002Fnodes\u002Fwidgets\u002Fui-template.html#writing-raw-javascript",[96],"Running raw JavaScript",[81,1532,1534],{"id":1533},"follow-our-progress","Follow our Progress",[12,1536,1537,1538,1541],{},"You can also read the more comprehensive release notes for ",[21,1539,1540],{},"v0.10.0"," release here:",[26,1543,1544],{},[29,1545,1546],{},[92,1547,1550],{"href":1548,"rel":1549},"https:\u002F\u002Fgithub.com\u002FFlowFuse\u002Fnode-red-dashboard\u002Freleases\u002Ftag\u002Fv0.10.0",[96],"0.10.0 Release Notes",[12,1552,1553,1554,79],{},"As always, thanks for reading and your interest in Dashboard 2.0. If you have any feature requests, bugs\u002Fcomplaints or general feedback, please do reach out, and raise issues on our relevant ",[92,1555,1558],{"href":1556,"rel":1557},"https:\u002F\u002Fgithub.com\u002FFlowFuse\u002Fnode-red-dashboard",[96],"GitHub repository",[26,1560,1561,1568],{},[29,1562,1563],{},[92,1564,1567],{"href":1565,"rel":1566},"https:\u002F\u002Fgithub.com\u002Forgs\u002FFlowFuse\u002Fprojects\u002F15\u002Fviews\u002F1",[96],"Dashboard 2.0 Activity Tracker",[29,1569,1570],{},[92,1571,1574],{"href":1572,"rel":1573},"https:\u002F\u002Fgithub.com\u002Forgs\u002FFlowFuse\u002Fprojects\u002F15\u002Fviews\u002F4",[96],"Dashboard 2.0 Planning Board",[286,1576,1577],{},"html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}",{"title":105,"searchDepth":143,"depth":143,"links":1579},[1580,1583,1591],{"id":83,"depth":126,"text":84,"children":1581},[1582],{"id":365,"depth":133,"text":366},{"id":423,"depth":126,"text":424,"children":1584},[1585,1586],{"id":427,"depth":133,"text":428},{"id":629,"depth":133,"text":630,"children":1587},[1588,1589,1590],{"id":637,"depth":143,"text":638},{"id":944,"depth":143,"text":945},{"id":1294,"depth":143,"text":1295},{"id":1533,"depth":126,"text":1534},null,"2023-12-07","Delve into the possibilities of Dashboard 2.0's new UI Templates with a comprehensive tutorial on building a custom video player.","md","\u002Fblog\u002F2023\u002F12\u002Fimages\u002Ftile-dashboard-0-10-0.png",{"excerpt":1598},{"type":9,"value":1599},[1600,1606],[12,1601,14,1602,19,1604,24],{},[16,1603,18],{},[21,1605,23],{},[26,1607,1608,1610,1614],{},[29,1609,31],{},[29,1611,34,1612,38],{},[21,1613,37],{},[29,1615,41,1616,38],{},[21,1617,37],{},"\u002Fblog\u002F2023\u002F12\u002Fdashboard-0-10-0",{"title":5,"description":1594},{"loc":1618},"blog\u002F2023\u002F12\u002Fdashboard-0-10-0","We've just released the latest version of Dashboard 2.0, with a fully featured UI Templates node which now allows for full definition of a Vue component, external JS dependencies and CSS.",[1624,1625,1626,1627,1628],"posts","releases","community","changelog","dashboard","mj-0XLaMKN-QAFI0H8BxX0JvsFJD1z85Mmm997wJAq0",[],1786031137955]