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

Nice!
Is there a way to grab my RTSP / RTMP streams from webcams ?

Have you tried? Simply changing to your cameras url

I changed it:

msg.url = "rtsp://login:password@192.168.0.59:554/h264Preview_01_sub";

this url did work fine with mp4frag for display of cams in Dashboard 1.

EDIT:
Another thing I noticed is that the player does only work if there is a webserver with this script online. So LAN will not work as long as I do not host a local server with that stuff by myself. is there a workaround ? :slight_smile:

<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>

Yes. Host the cdn.jsdelivr.net/npm/hls.js file your yourself.

One approach: download it to a file and serve it from httpStatic

Or just serve it with http-in -> file read (to read the downloaded file) -> http-response

The ui template example above supports hls (.m3u8) and .mp4 so the url you have provided will not work in that example. An example that works is:

http://rbmn-live.akamaized.net/hls/live/590964/BoRB-AT/master_1660.m3u8

If you feed your camera url through the mp4frag node, that will then produce .m3u8 format and that should then be possible to feed into the template (then as .m3u8). To test you could use what you have since before and feed the template with what you currently feed the ui_mp4frag with in the Dahboard 1 setup. To make it work you need to add a change node as a "helper" node, setting the msg.url and a cam nbr in the msg.payload

Check out and test if this flow works

[{"id":"6c9aaef2f8f4cf39","type":"ffmpeg","z":"c66150ba6c5c5715","name":"","outputs":2,"cmdPath":"ffmpeg","cmdArgs":"[\"-version\"]","cmdOutputs":1,"killSignal":"SIGTERM","x":840,"y":240,"wires":[["512079fd81675014"],["512079fd81675014"]]},{"id":"512079fd81675014","type":"mp4frag","z":"c66150ba6c5c5715","name":"","outputs":2,"basePath":"id","serveHttp":"true","serveIo":"true","hlsPlaylistSize":4,"hlsPlaylistExtra":0,"bufferPool":0,"autoStart":"true","preBuffer":1,"timeLimit":10000,"repeated":"false","statusData":"playlist","x":1070,"y":240,"wires":[["8cdd756e8c64f81a","1a249f31b27e436b"],[]]},{"id":"eb4bb64c.4552b8","type":"inject","z":"c66150ba6c5c5715","name":"RedBull","props":[{"p":"action","v":"{\"command\":\"start\",\"args\":[\"-loglevel\",\"error\",\"-nostats\",\"-f\",\"hls\",\"-http_multiple\",\"1\",\"-timeout\",\"1\",\"-re\",\"-i\",\"http://rbmn-live.akamaized.net/hls/live/590964/BoRB-AT/master_1660.m3u8\",\"-c:v\",\"copy\",\"-c:a\",\"aac\",\"-f\",\"mp4\",\"-movflags\",\"+frag_keyframe+empty_moov+default_base_moof\",\"pipe:1\"]}","vt":"json"},{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":"5","topic":"","payload":"cam10","payloadType":"str","x":700,"y":190,"wires":[["6c9aaef2f8f4cf39"]]},{"id":"e6391835.e71298","type":"inject","z":"c66150ba6c5c5715","name":"stop","props":[{"p":"action","v":"{\"command\":\"stop\"}","vt":"json"},{"p":"reset","v":"true","vt":"bool"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payloadType":"str","x":700,"y":290,"wires":[["6c9aaef2f8f4cf39"]]},{"id":"8cdd756e8c64f81a","type":"change","z":"c66150ba6c5c5715","name":"","rules":[{"t":"set","p":"url","pt":"msg","to":"payload.hlsPlaylist","tot":"msg"},{"t":"set","p":"payload","pt":"msg","to":"cam10","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":1300,"y":290,"wires":[["5552af0ad076c781"]]},{"id":"1a249f31b27e436b","type":"debug","z":"c66150ba6c5c5715","name":"debug 5","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1280,"y":170,"wires":[]},{"id":"5552af0ad076c781","type":"ui-template","z":"c66150ba6c5c5715","group":"b7971684f73a027c","page":"","ui":"","name":"mediaplayer","order":2,"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//            this.send(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_', msg.url)\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":1300,"y":350,"wires":[[]]},{"id":"b7971684f73a027c","type":"ui-group","name":"Group 5","page":"f85cf0eee9119058","width":"4","height":"1","order":2,"showTitle":false,"className":"","visible":"true","disabled":"false","groupType":"default"},{"id":"f85cf0eee9119058","type":"ui-page","name":"Hemma","ui":"72c1e5a9ec204878","path":"/home","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":2,"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"}}]
2 Likes

It works. Here is the flow to get it working on LAN. Still need to download and place the hls.js to filesystem:

[
    {
        "id": "abeec101acc2371f",
        "type": "http in",
        "z": "8612f3810195de21",
        "name": "hls mediaplayer",
        "url": "/hls",
        "method": "get",
        "upload": false,
        "swaggerDoc": "",
        "x": 340,
        "y": 620,
        "wires": [
            [
                "89bb8ebe48667252"
            ]
        ]
    },
    {
        "id": "89bb8ebe48667252",
        "type": "file in",
        "z": "8612f3810195de21",
        "name": "",
        "filename": "/home/pi/hlsPlayer/hls.js",
        "filenameType": "str",
        "format": "utf8",
        "chunk": false,
        "sendError": false,
        "encoding": "none",
        "allProps": false,
        "x": 610,
        "y": 620,
        "wires": [
            [
                "ba297de056c20044",
                "8cec66c84f732cfb"
            ]
        ]
    },
    {
        "id": "ba297de056c20044",
        "type": "http response",
        "z": "8612f3810195de21",
        "name": "",
        "statusCode": "200",
        "headers": {},
        "x": 880,
        "y": 620,
        "wires": []
    },
    {
        "id": "8cec66c84f732cfb",
        "type": "debug",
        "z": "8612f3810195de21",
        "name": "debug 2570",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "statusVal": "",
        "statusType": "auto",
        "x": 890,
        "y": 660,
        "wires": []
    }
]

Thx! I will check that out an report back.

It worked but somehow is the cam lags for 2-3 seconds very often while when using the same nodes expect change and mediaplayer (template) node are working like charm on DB 1.
The example flow of mediaplayer (template) node works smooth also on DB2.
I guess it is the reason may be the change node or so.... At least is this the node that does remain.

EDIT: I tested a bit more and there is no sound in my case (RTSP of Reolink cams). And the nice resizing of the stream (mp4frag - like) is not possible too. :frowning:

I do not have any RTSP camera so I can only try with some available sources I find on internet. Like red bull in the example

I added the ui_mp4frag node just run and compare the two dashboards in parallel. In my case, if both dashboards are visible when you start the stream, I think there is just a small lag between them, the ui_mp4frag is about .025 seconds behind. If you minimize any of them, the lag has increased when you bring them up again (no idea why, buffering maybe...)

BTW: I do not see that node in the palette any longer

There is of course not all the functionality in the ui_template example, it was a quick fix/attempt to make it possible to show hls and other on the Dashboard 2 but it was not ment to replace a node like the ui_mp4frag. For instance, the ui_template needs to be visible on screen when you start the stream. Audio is muted by default, think this is a browser security thing, otherwise autoplay of video does not start. You can right click to get the controls and then enable audio. I think it is possible to change in the template so you get those controls by default

Best regards, Walter

1 Like