Migrating ui template from Dashboard-1 to Dashboard-2

Hello, I just started looking into my old very well working ui template to try to migrate the functionality to Dashboard-2

The template is capable and used to stream & show video (like rtsp and http) as well as starting actions like taking snapshots

The issue is what needs to be changed? A straight forward copying of code does not work, it was not expected either

I assume it is those parts where I'm using the scope in the javascripts and the ng-click in the html that needs to be replaced somehow. Any suggestions is highly appreciated. If I manage to show rtsp streams in the dashboard-2 in this way would be, well, maybe not a breakthrough but at least interesting

So basically finding typical replacements for:

(function(scope) {
    scope.$watch('msg', function(msg) {.........

and in the html part

ng-click="send({payload:action('snapshot?mainview_url'), topic:'Snapshot'})"

The complete dashboard-1 code below

<script src="//cdn.jsdelivr.net/npm/hls.js@latest"></script>

<script type="text/javascript">
hls_m = undefined;

(function(scope) {
    scope.$watch('msg', function(msg) {
        if(msg.payload.indexOf('goback')>-1) {
            window.history.back();
        }
        if(msg.prev.indexOf('recordings')>-1||msg.prev.indexOf('.mp4')>-1||msg.prev.indexOf('motions')>-1||msg.prev.indexOf('youtube')>-1) {
            stopVideos(msg.prev);
        }
        if (msg.payload.indexOf('snapshots')>-1||msg.payload.indexOf('recordings')>-1||msg.payload.indexOf('motions')>-1) {
//            alert('here1');
            let all = $( "iframe" );
            let ip = location.host.split(':')[0];
            all.attr("src", "http://"+ip+msg.url);
            if(hls_m!=undefined){hls_m.destroy();}
            $("#def_"+scope.$id).hide();
            $("#pic_"+scope.$id).hide();
            $("#str_"+scope.$id).hide();
            $("#vid_"+scope.$id).hide();
            $("#hls_"+scope.$id).hide();
            $("#saf_"+scope.$id).hide();
            $("#cap_"+scope.$id).show();
        }
        if (msg.payload.indexOf('med')<0 && msg.payload.indexOf('default')>-1) {
//            alert('here2');
            if(hls_m!=undefined){hls_m.destroy();}
            let df = $("[title~='Default']");
            let ip = location.host.split(':')[0];
            df.attr("src", "http://"+ip+msg.url);
            $("#def_"+scope.$id).show();
            $("#pic_"+scope.$id).hide();
            $("#str_"+scope.$id).hide();
            $("#vid_"+scope.$id).hide();
            $("#hls_"+scope.$id).hide();
            $("#saf_"+scope.$id).hide();
            $("#cap_"+scope.$id).hide();
        }
        if (msg.payload.indexOf('med')>-1 && msg.payload.indexOf('default')<0) {
//            alert('here3');
            if(hls_m!=undefined){hls_m.destroy();}
            $("#def_"+scope.$id).hide();
            $("#pic_"+scope.$id).hide();
            $("#str_"+scope.$id).hide();
            $("#vid_"+scope.$id).hide();
            $("#hls_"+scope.$id).hide();
            $("#saf_"+scope.$id).hide();
            $("#cap_"+scope.$id).hide();
            if(msg.url.indexOf('.mp4')>-1){
                let med = $("[title~='Main']");
                med.attr("src", msg.url);
                $("#vid_"+scope.$id).show();
            }else{
                $("#pic_"+scope.$id).show();
            }
        }
        if (msg.payload.indexOf('cam')>-1 && msg.url.indexOf('.m3u8')<0 && msg.url.indexOf('youtube')<0) {
//            alert('here4');
            if(hls_m!=undefined){hls_m.destroy();}
            $("#def_"+scope.$id).hide();
            $("#pic_"+scope.$id).hide();
            $("#str_"+scope.$id).show();
            $("#vid_"+scope.$id).hide();
            $("#hls_"+scope.$id).hide();
            $("#saf_"+scope.$id).hide();
            $("#cap_"+scope.$id).hide();
        }    

        if (msg.url.indexOf('.m3u8')>-1) {
//            alert('here5');
            if(hls_m!=undefined){hls_m.destroy();}
            $("#def_"+scope.$id).hide();
            $("#pic_"+scope.$id).hide();
            $("#str_"+scope.$id).hide();
            $("#vid_"+scope.$id).hide();
            $("#hls_"+scope.$id).hide();
            $("#saf_"+scope.$id).hide();
            $("#cap_"+scope.$id).hide();
            if(isSafari()){
                $("#saf_"+scope.$id).show();
            }
            if(!isSafari()){
//                alert('here6');
                $("#hls_"+scope.$id).show();
                hls_m = new Hls();
                if (Hls.isSupported()) {
                    let z = $("#hls_"+scope.$id)[0];
                    // bind them together
                    hls_m.attachMedia(z);
                    hls_m.on(Hls.Events.MEDIA_ATTACHED, function () {
                      hls_m.loadSource(msg.url);
                      hls_m.on(Hls.Events.MANIFEST_PARSED, function () {
                        z.play();  
                      });
                    });
                }
            }
        }
        if (msg.url.indexOf('youtube')>-1) {
//            alert('here7');
            let all = $( "iframe" );
            all.attr("src", msg.url);
            if(hls_m!=undefined){hls_m.destroy();}
            $("#def_"+scope.$id).hide();
            $("#pic_"+scope.$id).hide();
            $("#str_"+scope.$id).hide();
            $("#vid_"+scope.$id).hide();
            $("#hls_"+scope.$id).hide();
            $("#saf_"+scope.$id).hide();
            $("#cap_"+scope.$id).show();
        }
    });
})(scope);

function isSafari() {
    if (/apple/i.test(navigator.vendor)) {
        //alert("Safari");
        return true;
    }else{
        return false;
    } 
}

function stopVideos(prev) {
    if(prev.indexOf('recordings')>-1 || prev.indexOf('youtube')>-1) {
//        alert('rec or youtube found');
        $("iframe").each(function() { 
            var src= $(this).attr('src');
            if(prev.indexOf('youtube')>-1) {
                $(this).attr('src','');  
            }else{
                $(this).attr('src',src);  
            }
        });
    }
    if(prev.indexOf('.mp4')>-1) {
//        alert('mp4 found');
        $("[title~='Main']").trigger('pause');
    }
};

this.scope.action = function(event) { return event; }

this.scope.send({payload:'do_init'});

</script>

<div id="{{'def_'+$id}}">
<center>
    <table>
        <tr>
            <td style="text-align: center; transform:rotate({{msg.rotate}})">
            <img class="main_monitor" title="Default">
            </td>
        </tr>
    </table>
</center>
</div>

<div id="{{'pic_'+$id}}">
<center>
    <table>
        <tr>
            <td style="text-align: center; transform:rotate({{msg.rotate}})">
            <img src={{msg.url}}?{{msg._msgid}} class="main_monitor" title="Mediafile">
            </td>
        </tr>
    </table>
</center>
</div>

<div id="{{'str_'+$id}}">
<center>
    <table>
        <tr>
            <td style="text-align: center; transform:rotate({{msg.rotate}})">
            <img src={{msg.url}}?{{msg._msgid}} class="main_monitor" ng-click="send({payload:action('snapshot?mainview_url'), topic:'Snapshot'})" title="Click for snapshot">
            </td>
        </tr>
    </table>
</center>
</div>

<div id="{{'vid_'+$id}}">
<center>
    <table>
        <tr>
    		<td style="text-align: center; transform:rotate({{msg.rotate}})">
    		<video autoplay controls class="main_monitor" title="Main display" type="video/mp4"></video>
    		</td>
        </tr>
    </table>
</center>
</div>

<div>
<center>
    <table>
        <tr>
    		<td style="text-align: center; transform:rotate({{msg.rotate}})">
    		<video autoplay muted id="{{'hls_'+$id}}" class="main_monitor" ng-click="send({payload:action('snapshot?mainview_url'), topic:'Snapshot'})" title="Playing m3u8, click for snapshot" type="video/mp4"></video>
    		</td>
        </tr>
    </table>
</center>
</div>

<div id="{{'saf_'+$id}}">
<center>
    <table>
        <tr>
    		<td style="text-align: center; transform:rotate({{msg.rotate}})">
    		<video autoplay muted src={{msg.url}}  class="main_monitor" ng-click="send({payload:action('snapshot?mainview_url'), topic:'Snapshot'})" title="Playing m3u8, click for snapshot" type="video/mp4"></video>
    		</td>
        </tr>
    </table>
</center>
</div>

<div id="{{'cap_'+$id}}" class="container">
<center>
    <table>
        <tr>
            <td style="text-align: center">
            <iframe class="responsive-iframe" height="400"></iframe>    
            </td>
        </tr>
    </table>
</center>
</div>

Start your adventure armed with this knowledge:

Dashboard 2 uses Vue 3 Options API and Vuetify component framework
You don't need all that ugly "xxx"+scope.$id stuff and there is no jQuery. Instead of applying ids to your components you use ref instead. Vue 'ref' Attribute

On top of what I wrote here: Subflow - UI Group - #11 by Steve-Mcl , most things have an immediate alternative. e.g. md-button becomes v-btn and the docs for v-btn are here: Button component — Vuetify

Lastly, the docs for dashboard 2 are pretty decent: Template ui-template | Node-RED Dashboard 2.0

Here is a very quick, very dirty, partially working (and needs lots of work to make it better) demo to get you moving...

chrome_xk5dt9SJGZ

[{"id":"88d4cf6b97770beb","type":"ui-template","z":"a77bbd614562e1ee","group":"00328f3e4d88b179","page":"","ui":"","name":"mediaplayer","order":1,"width":0,"height":0,"head":"","format":"<template>\n    <div class=\"nrdb-widget--media-player\">\n        <div v-if=\"loaded\">\n            <div v-if=\"def_\" class=\"centered\">\n                <v-img :src=\"def_src\" class=\"main_monitor\" title=\"Default\"></v-img>\n            </div>\n\n            <div v-if=\"pic_\" class=\"centered\">\n                <v-img :src=\"pic_src\" class=\"main_monitor\" title=\"Mediafile\"></v-img>\n            </div>\n\n            <div v-if=\"str_\" class=\"centered\">\n                <v-img :src=\"str_src\" class=\"main_monitor\" title=\"Click for snapshot\" @click=\"send({payload:'snapshot?mainview_url', topic:'Snapshot'})\">\n                </v-img>\n            </div>\n\n            <div v-show=\"vid_\" class=\"centered\">\n                <video\n                    height=\"400\" width=\"100%\"\n                    ref=\"vid_\" autoplay controls :src=\"vid_src\" class=\"main_monitor\" title=\"Main display\"\n                    type=\"video/mp4\"\n                />\n            </div>\n\n            <div v-show=\"hls_\" class=\"centered\">\n                <video\n                    ref=\"hls_\" autoplay muted class=\"main_monitor\"\n                    title=\"Playing m3u8, click for snapshot\" type=\"video/mp4\"\n                    @click=\"send({payload:'snapshot?mainview_url', topic:'Snapshot'})\"\n                />\n            </div>\n\n            <div v-show=\"saf_\" class=\"centered\">\n                <video\n                    ref=\"saf_\" autoplay muted :src=\"saf_src || ''\" class=\"main_monitor\"\n                    title=\"Playing m3u8, click for snapshot\" type=\"video/mp4\"\n                    @click=\"send({payload:'snapshot?mainview_url', topic:'Snapshot'})\"\n                />\n            </div>\n\n            <div v-show=\"cap_\" class=\"centered\">\n                <iframe ref=\"cap_\" height=\"400\" width=\"100%\" src=\"\" title=\"YouTube video player\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\" referrerpolicy=\"strict-origin-when-cross-origin\" allowfullscreen></iframe>\n            </div>\n        </div>\n        <div v-else>\n            <v-progress-circular indeterminate />\n        </div>\n    </div>\n</template>\n\n\n<script>\nexport default {\n    name: 'MediaPlayer',\n    data () {\n        return {\n            loaded: false,\n            hls_m: null,\n\n            // simple booleans that reactively show/hide components\n            def_: false,\n            pic_: false,\n            str_: false,\n            vid_: false,\n            hls_: false,\n            saf_: false,\n            cap_: true,\n\n            // the source for each type of media - these are bound to the :src attributes in the template\n            def_src: '',\n            vid_src: '',\n            hls_src: '',\n            saf_src: '',\n            cap_src: ''\n        }\n    },\n    computed: {\n        str_src () {\n            return this.str_ ? (this.msg?.url + '?' + this.msg?._msgid || '') : ''\n        },\n        pic_src () {\n            return this.pic_ ? (this.msg?.url + '?' + this.msg?._msgid || '') : ''\n        }\n    },\n    mounted () {\n        console.log('mounted')\n        // code here when the component is first loaded\n        const interval = setInterval(() => {\n            if (window.Hls) {\n                console.log('Hls has loaded')\n                clearInterval(interval)\n                this.loaded = true\n                this.init()\n            }\n        }, 100)\n    },\n    methods: {\n        init () {\n            console.log('init')\n            this.$socket.on('msg-input:' + this.id, (msg) => {\n                // do stuff with msg. runs only when new messages are received\n                this.onMsg(msg)\n            })\n            this.send({ payload: 'do_init' })\n            this.loaded = true\n        },\n        isSafari () {\n            if (/apple/i.test(navigator.vendor)) {\n                // alert(\"Safari\");\n                return true\n            } else {\n                return false\n            }\n        },\n        stopVideos (prev) {\n            // TODO: not sure what this is supposed to be doing\n        //     if(prev.indexOf('recordings')>-1 || prev.indexOf('youtube')>-1) {\n        //         $(\"iframe\").each(function() {\n        //             var src= $(this).attr('src');\n        //             if(prev.indexOf('youtube')>-1) {\n        //                 $(this).attr('src','');\n        //             }else{\n        //                 $(this).attr('src',src);\n        //             }\n        //         });\n        //     }\n        //     if(prev.indexOf('.mp4')>-1) {\n        //         $(\"[title~='Main']\").trigger('pause');\n        //     }\n        },\n        show (which, src) {\n            console.log('showing', which, src)\n            this.def_src = which !== 'def_' ? '' : (src || '')\n            this.vid_src = which !== 'vid_' ? '' : (src || '')\n            this.hls_src = which !== 'hls_' ? '' : (src || '')\n            this.saf_src = which !== 'saf_' ? '' : (src || '')\n            this.cap_src = which !== 'cap_' ? '' : (src || '')\n            this.def_ = which === 'def_'\n            this.pic_ = which === 'pic_'\n            this.str_ = which === 'str_'\n            this.vid_ = which === 'vid_'\n            this.hls_ = which === 'hls_'\n            this.saf_ = which === 'saf_'\n            this.cap_ = which === 'cap_'\n        },\n        onMsg (msg) {\n            console.log('msg', msg)\n            if (msg.topic === 'stop') {\n                if (this.hls_m) { this.hls_m.destroy() }\n                this.show('')\n            }\n            if (msg.payload?.indexOf('goback') > -1) {\n                window.history.back()\n            }\n            if (msg.prev?.indexOf('recordings') > -1 || msg.prev?.indexOf('.mp4') > -1 || msg.prev?.indexOf('motions') > -1 || msg.prev?.indexOf('youtube') > -1) {\n                this.stopVideos(msg.prev)\n            }\n            if (msg.payload?.indexOf('snapshots') > -1 || msg.payload?.indexOf('recordings') > -1 || msg.payload?.indexOf('motions') > -1) {\n                const ip = location.host.split(':')[0]\n                if (this.hls_m) { this.hls_m.destroy() }\n                this.show('cap_', 'http://' + ip + msg.url)\n            }\n            if (msg.payload?.indexOf('med') < 0 && msg.payload?.indexOf('default') > -1) {\n                if (this.hls_m) { this.hls_m.destroy() }\n                const ip = location.host.split(':')[0]\n                this.show('def_', 'http://' + ip + msg.url)\n            }\n            if (msg.payload?.indexOf('med') > -1 && msg.payload?.indexOf('default') < 0) {\n                if (this.hls_m) { this.hls_m.destroy() }\n\n                if (msg.url?.indexOf('.mp4') > -1) {\n                    this.show('vid_', msg.url)\n                } else {\n                    this.show('pic_')\n                }\n            }\n            if (msg.payload?.indexOf('cam') > -1 && msg.url?.indexOf('.m3u8') < 0 && msg.url?.indexOf('youtube') < 0) {\n                if (this.hls_m) { this.hls_m.destroy() }\n                this.show('str_')\n            } else if (msg.url?.indexOf('.m3u8') > -1) {\n                if (this.hls_m) { this.hls_m.destroy() }\n                if (this.isSafari()) {\n                    this.show('saf_')\n                } else {\n                    this.show('hls_')\n                    this.hls_m = new window.Hls()\n                    if (window.Hls?.isSupported()) {\n                        const z = this.$refs.hls_\n                        // bind them together\n                        this.hls_m.attachMedia(z)\n                        this.hls_m.on(window.Hls.Events.MEDIA_ATTACHED, function () {\n                            this.hls_m.loadSource(msg.url)\n                            this.hls_m.on(window.Hls.Events.MANIFEST_PARSED, function () {\n                                z.play()\n                            })\n                        })\n                    }\n                }\n            } else if (msg.url?.indexOf('youtube') > -1) {\n                if (this.hls_m) { this.hls_m.destroy() }\n                this.$refs.cap_.src = msg.url\n                this.show('cap_')\n            } else if (msg.url?.endsWith('.mp4')) {\n                if (this.hls_m) { this.hls_m.destroy() }\n                this.show('vid_', msg.url)\n            }\n        }\n    }\n}\n</script>\n\n<script type=\"text/javascript\" src=\"https://cdn.jsdelivr.net/npm/hls.js@latest\"></script>\n","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":1510,"y":380,"wires":[["be79f6a038a78723"]]},{"id":"b9d9e258c694ba99","type":"inject","z":"a77bbd614562e1ee","name":"tears-of-steel m3u8 (needs work)","props":[{"p":"url","v":"https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.ism/.m3u8","vt":"str"},{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"str","x":1230,"y":220,"wires":[["88d4cf6b97770beb"]]},{"id":"bb7d17187016273a","type":"inject","z":"a77bbd614562e1ee","name":"youtube for cat lovers","props":[{"p":"url","v":"https://www.youtube.com/embed/oZFAcp-Qfbs?si=dVMgWw6fuTof_om-","vt":"str"},{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"cam-1","payloadType":"str","x":1200,"y":300,"wires":[["88d4cf6b97770beb"]]},{"id":"5df6be2507f19659","type":"inject","z":"a77bbd614562e1ee","name":"youtube for dog lovers","props":[{"p":"url","v":"https://www.youtube.com/embed/c2OTHeCKsBE?si=M4Bq3U0gJTe5uwIr","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":1200,"y":340,"wires":[["88d4cf6b97770beb"]]},{"id":"6d27f6b22732560e","type":"inject","z":"a77bbd614562e1ee","name":"stop","props":[{"p":"url","v":"","vt":"str"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"stop","x":1150,"y":380,"wires":[["88d4cf6b97770beb"]]},{"id":"be79f6a038a78723","type":"debug","z":"a77bbd614562e1ee","name":"debug 2566","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1510,"y":440,"wires":[]},{"id":"e11ce29786db928d","type":"inject","z":"a77bbd614562e1ee","name":"BigBuckBunny.mp4","props":[{"p":"url","v":"http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4","vt":"str"},{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"str","x":1190,"y":260,"wires":[["88d4cf6b97770beb"]]},{"id":"00328f3e4d88b179","type":"ui-group","name":"mediaplayer","page":"b6154633432f57b1","width":"6","height":"1","order":1,"showTitle":false,"className":"","visible":"true","disabled":"false","groupType":"default"},{"id":"b6154633432f57b1","type":"ui-page","name":"mediaplayer","ui":"72c1e5a9ec204878","path":"/mediaplayer","icon":"home","layout":"grid","theme":"0d92c765bfad87e6","breakpoints":[{"name":"Default","px":"0","cols":"3"},{"name":"Tablet","px":"576","cols":"6"},{"name":"Small Desktop","px":"768","cols":"9"},{"name":"Desktop","px":"1024","cols":"12"}],"order":1,"className":"","visible":"true","disabled":"false"},{"id":"72c1e5a9ec204878","type":"ui-base","name":"My Dashboard","path":"/dashboard","appIcon":"","includeClientData":true,"acceptsClientConfig":["ui-notification","ui-control","ui-chart"],"showPathInSidebar":false,"showPageTitle":true,"navigationStyle":"default","titleBarStyle":"default"},{"id":"0d92c765bfad87e6","type":"ui-theme","name":"Basic Blue Theme","colors":{"surface":"#4d58ff","primary":"#0094ce","bgPage":"#eeeeee","groupBg":"#ffffff","groupOutline":"#cccccc"},"sizes":{"pagePadding":"12px","groupGap":"12px","groupBorderRadius":"4px","widgetGap":"12px","density":"default"}}]
1 Like

Fantastic, so great!! I will dive into, motivation was triggered
Thank you!!

@Steve-Mcl One issue, If you run one of the YouTube videos and then try to stop it, while the video stops, the audio continues even it you run something else

  1. select YouTube for cat lovers
  2. in dashboard click on the play button in the video
  3. in NR press the stop option
    If you go back to the video the audio is still playing.

May i direct you to this...

:wink:

I might pick it up and improve it I think but you guys have got this :wink:

2 Likes

Been experimenting and it works fine when playing mp4 as well as http but when I try to play a hls stream it does not start playing

Below is the specific code where I inserted a number of debug messages. It reaches the following:
show hls
hls is supported
z is set
binding worked

but it never reaches url loaded

Here I'm stucked, any hints would be great

The url is "/mp4frag/7424e65996a40660/hls.m3u8" and plays fine in the db-1 version both in Chrome and Safari

            if (msg.payload?.indexOf('cam') > -1 && msg.url?.indexOf('.m3u8') < 0 && msg.url?.indexOf('youtube') < 0) {
                if (this.hls_m) { this.hls_m.destroy() }
                this.show('str_')
            } else if (msg.url?.indexOf('.m3u8') > -1) {
                if (this.hls_m) { this.hls_m.destroy() }
                if (this.isSafari()) {
                    this.show('saf_')
                } else {
                    this.send({ payload: 'show hls' })
                    this.show('hls_')
                    this.hls_m = new window.Hls()
                    if (window.Hls?.isSupported()) {
                        this.send('hls is supported')
                        const z = this.$refs.hls_
                        this.send('z is set')
                        // bind them together
                        this.hls_m.attachMedia(z)
                        this.send('binding worked')
                        this.hls_m.on(window.Hls.Events.MEDIA_ATTACHED, function () {
                            this.hls_m.loadSource(msg.url)
                            this.send('url loaded')
                            this.hls_m.on(window.Hls.Events.MANIFEST_PARSED, function () {
                                this.send('hls now playing')
                                z.play()
                            })
                        })
                    }
                }

Here is working m3u8

chrome_Ppks6QJc5m

It still needs work :wink:

Like cleaning up, error handling, sanity checking etc.

[{"id":"88d4cf6b97770beb","type":"ui-template","z":"a77bbd614562e1ee","group":"00328f3e4d88b179","page":"","ui":"","name":"mediaplayer","order":1,"width":0,"height":0,"head":"","format":"<template>\n    <div class=\"nrdb-widget--media-player\">\n        <div v-if=\"loaded && !loading\">\n            <div v-if=\"error_\" class=\"centered\">\n                {{ error_msg || 'Error loading media' }}\n            </div>\n\n            <div v-if=\"def_\" class=\"centered\">\n                <v-img :src=\"def_src\" class=\"main_monitor\" title=\"Default\"></v-img>\n            </div>\n\n            <div v-if=\"pic_\" class=\"centered\">\n                <v-img :src=\"pic_src\" class=\"main_monitor\" title=\"Mediafile\"></v-img>\n            </div>\n\n            <div v-if=\"str_\" class=\"centered\">\n                <v-img :src=\"str_src\" class=\"main_monitor\" title=\"Click for snapshot\" @click=\"send({payload:'snapshot?mainview_url', topic:'Snapshot'})\">\n                </v-img>\n            </div>\n\n            <div v-show=\"vid_\" class=\"centered\">\n                <video\n                    height=\"400\" width=\"100%\"\n                    ref=\"vid_\" autoplay controls :src=\"vid_src\" class=\"main_monitor\" title=\"Main display\"\n                    type=\"video/mp4\"\n                />\n            </div>\n\n            <div v-show=\"hls_\" class=\"centered\">\n                <video\n                    ref=\"hls_\" autoplay muted class=\"main_monitor\"\n                    height=\"400\" width=\"100%\"\n                    title=\"Playing m3u8, click for snapshot\" type=\"video/mp4\"\n                    @click=\"send({payload:'snapshot?mainview_url', topic:'Snapshot'})\"\n                />\n            </div>\n\n            <div v-show=\"saf_\" class=\"centered\">\n                <video\n                    ref=\"saf_\" autoplay muted :src=\"saf_src || ''\" class=\"main_monitor\"\n                    height=\"400\" width=\"100%\"\n                    title=\"Playing m3u8, click for snapshot\" type=\"video/mp4\"\n                    @click=\"send({payload:'snapshot?mainview_url', topic:'Snapshot'})\"\n                />\n            </div>\n\n            <div v-show=\"cap_\" class=\"centered\">\n                <iframe ref=\"cap_\" height=\"400\" width=\"100%\" src=\"\" title=\"YouTube video player\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\" referrerpolicy=\"strict-origin-when-cross-origin\" allowfullscreen></iframe>\n            </div>\n        </div>\n        <div v-else>\n            <v-progress-circular indeterminate />\n        </div>\n    </div>\n</template>\n\n\n<script>\nexport default {\n    name: 'MediaPlayer',\n    data () {\n        return {\n            loaded: false,\n            loading: false,\n            unsupported: false,\n            hls_m: null,\n\n            // simple booleans that reactively show/hide components\n            def_: false,\n            pic_: false,\n            str_: false,\n            vid_: false,\n            hls_: false,\n            saf_: false,\n            cap_: true,\n\n            // the source for each type of media - these are bound to the :src attributes in the template\n            def_src: '',\n            vid_src: '',\n            hls_src: '',\n            saf_src: '',\n            cap_src: ''\n        }\n    },\n    computed: {\n        str_src () {\n            return this.str_ ? (this.msg?.url + '?' + this.msg?._msgid || '') : ''\n        },\n        pic_src () {\n            return this.pic_ ? (this.msg?.url + '?' + this.msg?._msgid || '') : ''\n        }\n    },\n    mounted () {\n        console.log('mounted')\n        // code here when the component is first loaded\n        const interval = setInterval(() => {\n            if (window.Hls) {\n                console.log('Hls has loaded')\n                clearInterval(interval)\n                this.loaded = true\n                this.init()\n            }\n        }, 100)\n    },\n    methods: {\n        init () {\n            console.log('init')\n            this.$socket.on('msg-input:' + this.id, (msg) => {\n                // do stuff with msg. runs only when new messages are received\n                this.onMsg(msg)\n            })\n            this.send({ payload: 'do_init' })\n            this.loaded = true\n        },\n        isSafari () {\n            if (/apple/i.test(navigator.vendor)) {\n                // alert(\"Safari\");\n                return true\n            } else {\n                return false\n            }\n        },\n        stopVideos (prev) {\n            // TODO: not sure what this is supposed to be doing\n        //     if(prev.indexOf('recordings')>-1 || prev.indexOf('youtube')>-1) {\n        //         $(\"iframe\").each(function() {\n        //             var src= $(this).attr('src');\n        //             if(prev.indexOf('youtube')>-1) {\n        //                 $(this).attr('src','');\n        //             }else{\n        //                 $(this).attr('src',src);\n        //             }\n        //         });\n        //     }\n        //     if(prev.indexOf('.mp4')>-1) {\n        //         $(\"[title~='Main']\").trigger('pause');\n        //     }\n        },\n        show (which, src) {\n            console.log('showing', which, src)\n            this.loading = false\n            this.def_src = which !== 'def_' ? '' : (src || '')\n            this.vid_src = which !== 'vid_' ? '' : (src || '')\n            this.hls_src = which !== 'hls_' ? '' : (src || '')\n            this.saf_src = which !== 'saf_' ? '' : (src || '')\n            this.cap_src = which !== 'cap_' ? '' : (src || '')\n            this.error_msg = which !== 'error_' ? '' : (src || '')\n            this.def_ = which === 'def_'\n            this.pic_ = which === 'pic_'\n            this.str_ = which === 'str_'\n            this.vid_ = which === 'vid_'\n            this.hls_ = which === 'hls_'\n            this.saf_ = which === 'saf_'\n            this.cap_ = which === 'cap_'\n            this.error_ = which === 'error_'\n        },\n        stopHls() {\n            this.$refs.hls_?.stop && this.$refs.hls_.stop()\n            if (this.hls_m) {\n                try {\n                    this.hls_m.destroy()\n                } catch (err) {}\n                this.hls_m = null\n            }\n        },\n        onMsg (msg) {\n            console.log('msg', msg)\n            if (msg.topic === 'stop') {\n                this.stopHls()\n                this.show('')\n            }\n            if (msg.payload?.indexOf('goback') > -1) {\n                window.history.back()\n            }\n            if (msg.prev?.indexOf('recordings') > -1 || msg.prev?.indexOf('.mp4') > -1 || msg.prev?.indexOf('motions') > -1 || msg.prev?.indexOf('youtube') > -1) {\n                this.stopVideos(msg.prev)\n            }\n            if (msg.payload?.indexOf('snapshots') > -1 || msg.payload?.indexOf('recordings') > -1 || msg.payload?.indexOf('motions') > -1) {\n                const ip = location.host.split(':')[0]\n                this.stopHls()\n                this.show('cap_', 'http://' + ip + msg.url)\n            }\n            if (msg.payload?.indexOf('med') < 0 && msg.payload?.indexOf('default') > -1) {\n                this.stopHls()\n                const ip = location.host.split(':')[0]\n                this.show('def_', 'http://' + ip + msg.url)\n            }\n            if (msg.payload?.indexOf('med') > -1 && msg.payload?.indexOf('default') < 0) {\n                this.stopHls()\n\n                if (msg.url?.indexOf('.mp4') > -1) {\n                    this.show('vid_', msg.url)\n                } else {\n                    this.show('pic_')\n                }\n            }\n            if (msg.payload?.indexOf('cam') > -1 && msg.url?.indexOf('.m3u8') < 0 && msg.url?.indexOf('youtube') < 0) {\n                this.stopHls()\n                this.show('str_')\n            } else if (msg.url?.indexOf('.m3u8') > -1) {\n                this.stopHls()\n                if (this.isSafari()) {\n                    this.show('saf_')\n                } else {\n                    this.send({ payload: 'show hls' })\n                    this.show('hls_')\n                    if (window.Hls?.isSupported()) {\n                        try {\n                            const video = this.$refs.hls_\n                            const hsl = new window.Hls()\n                            this.hls_m = hsl\n                            console.log('hls is supported')\n                            this.loading = true\n                            \n                            hsl.on(Hls.Events.MEDIA_ATTACHED, function () {\n                                console.log('video and hls.js are now bound together !');\n                            });\n                            hsl.on(Hls.Events.MANIFEST_PARSED, function (event, data) {\n                                console.log(\n                                    'manifest loaded, found ' + data.levels.length + ' quality level',\n                                );\n                            });\n                            hsl.loadSource(msg.url);\n                            hsl.attachMedia(video);\n                            this.loading = false\n                        } catch (err) {\n                            this.show('error_')\n                            node.send( { error: { message: err.message } } )\n                        }\n                    } else {\n                        this.show('error_', 'Hls Error: ' + err.message)\n                        node.send( { error: { message: 'Hls Not supported' } } )\n                    }\n                }\n            } else if (msg.url?.indexOf('youtube') > -1) {\n                this.stopHls()\n                this.$refs.cap_.src = msg.url\n                this.show('cap_')\n            } else if (msg.url?.endsWith('.mp4')) {\n                this.stopHls()\n                this.show('vid_', msg.url)\n            }\n        }\n    }\n}\n</script>\n\n<script type=\"text/javascript\" src=\"https://cdn.jsdelivr.net/npm/hls.js@latest\"></script>\n","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":1450,"y":340,"wires":[["be79f6a038a78723"]]},{"id":"bb7d17187016273a","type":"inject","z":"a77bbd614562e1ee","name":"youtube for cat lovers","props":[{"p":"url","v":"https://www.youtube.com/embed/oZFAcp-Qfbs?si=dVMgWw6fuTof_om-","vt":"str"},{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"cam-1","payloadType":"str","x":1200,"y":300,"wires":[["88d4cf6b97770beb"]]},{"id":"5df6be2507f19659","type":"inject","z":"a77bbd614562e1ee","name":"youtube for dog lovers","props":[{"p":"url","v":"https://www.youtube.com/embed/c2OTHeCKsBE?si=M4Bq3U0gJTe5uwIr","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":1200,"y":340,"wires":[["88d4cf6b97770beb"]]},{"id":"6d27f6b22732560e","type":"inject","z":"a77bbd614562e1ee","name":"stop","props":[{"p":"url","v":"","vt":"str"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"stop","x":1150,"y":380,"wires":[["88d4cf6b97770beb"]]},{"id":"be79f6a038a78723","type":"debug","z":"a77bbd614562e1ee","name":"debug 2566","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1450,"y":380,"wires":[]},{"id":"e11ce29786db928d","type":"inject","z":"a77bbd614562e1ee","name":"BigBuckBunny.mp4","props":[{"p":"url","v":"http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4","vt":"str"},{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"str","x":1190,"y":260,"wires":[["88d4cf6b97770beb"]]},{"id":"91525ca5a6fd15cc","type":"inject","z":"a77bbd614562e1ee","name":"longtailvideo m3u8","props":[{"p":"url","v":"http://playertest.longtailvideo.com/adaptive/wowzaid3/playlist.m3u8","vt":"str"},{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"str","x":1190,"y":180,"wires":[["88d4cf6b97770beb"]]},{"id":"780b18b4cac8b3b2","type":"inject","z":"a77bbd614562e1ee","name":"another m3u8","props":[{"p":"url","v":"https://bitdash-a.akamaihd.net/content/MI201109210084_1/m3u8s/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.m3u8","vt":"str"},{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"str","x":1170,"y":220,"wires":[["88d4cf6b97770beb"]]},{"id":"00328f3e4d88b179","type":"ui-group","name":"mediaplayer","page":"b6154633432f57b1","width":"6","height":"1","order":1,"showTitle":false,"className":"","visible":"true","disabled":"false","groupType":"default"},{"id":"b6154633432f57b1","type":"ui-page","name":"mediaplayer","ui":"72c1e5a9ec204878","path":"/mediaplayer","icon":"home","layout":"grid","theme":"0d92c765bfad87e6","breakpoints":[{"name":"Default","px":"0","cols":"3"},{"name":"Tablet","px":"576","cols":"6"},{"name":"Small Desktop","px":"768","cols":"9"},{"name":"Desktop","px":"1024","cols":"12"}],"order":1,"className":"","visible":"true","disabled":"false"},{"id":"72c1e5a9ec204878","type":"ui-base","name":"My Dashboard","path":"/dashboard","appIcon":"","includeClientData":true,"acceptsClientConfig":["ui-notification","ui-control","ui-chart"],"showPathInSidebar":false,"showPageTitle":true,"navigationStyle":"default","titleBarStyle":"default"},{"id":"0d92c765bfad87e6","type":"ui-theme","name":"Basic Blue Theme","colors":{"surface":"#4d58ff","primary":"#0094ce","bgPage":"#eeeeee","groupBg":"#ffffff","groupOutline":"#cccccc"},"sizes":{"pagePadding":"12px","groupGap":"12px","groupBorderRadius":"4px","widgetGap":"12px","density":"default"}}]
1 Like

@Steve-Mcl

This is great, working smooth and fine in Chrome. In Safari on mac the hls does not play but the others are working. Tried figure out why hls did not, should be straight forward since hls support is built into Safari, hmm, strange. Could it be something with vue??

Can't help you there I'm afraid. I don't do Apple. It'll likely be down to how the src attribute is bound. Try using a $ref & setting src in js rather than binding to the attribute

:wink: Thanks for your great help with all the other parts!! It's working very well

This did the trick. Now hls plays natively in Safari (just adding the msg.url)

                    this.show('saf_', msg.url)

2 Likes