# RTSP IP live camera in Dashboard 2.0

**URL:** <https://discourse.nodered.org/t/rtsp-ip-live-camera-in-dashboard-2-0/101423>\
**Category:** Share Your Projects\
**Tags:** dashboard-2\
**Created:** [8 July 2026 10:00 UTC](https://discourse.nodered.org/t/rtsp-ip-live-camera-in-dashboard-2-0/101423 "2026-07-08T10:00:35Z")\
**Posts on this page:** 4\
**Page:** 1

<div class="post-metadata">

**Author:** ![SuperNinja](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/superninja/32/5761_2.png) [@SuperNinja](https://discourse.nodered.org/u/SuperNinja)\
**Post date:** [8 July 2026 10:00 UTC](https://discourse.nodered.org/t/rtsp-ip-live-camera-in-dashboard-2-0/101423/1 "2026-07-08T10:00:35Z")

</div>

> [@Ui\_mp4frag for DB2 - how to continue](https://discourse.nodered.org/t/ui-mp4frag-for-db2-how-to-continue/90241):
>
> Hi folks, I have been implementing some pull requests for dashboard D2, but now I finally would like to continue migrating some old ui nodes. I have some questions about another discussion: To be able to migrate my own dashboard to D2, this node has the top priority for me. While digging through the code of the [old ui node](https://github.com/kevinGodell/node-red-ui-mp4frag), some questions popped up in my head. So hopefully our video expert @kevinGodell can help me with that. And others are of course welcome to share how they use this nod…

With the retirement of Dash1.0, I wanted to continue seeing the RTSP feed from my IP cameras in Dash2.0. While waiting for an "official" node from @kevigodell, I propose a Template node (DB2) which can stream my local RTSP IP cameras.

You need to install Kevin's nodes:

> **[@kevingodell/node-red-ffmpeg](https://flows.nodered.org/node/@kevingodell/node-red-ffmpeg)**
>
> Spawn a long-running ffmpeg process to handle video/image processing.

> **[@kevingodell/node-red-mp4frag](https://flows.nodered.org/node/@kevingodell/node-red-mp4frag)**
>
> Parse a fragmented mp4 from a buffer and send it to a downstream node or serve it via socket.io and http.

(or use the Palette).

 ![image](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/3/d/3d57562fbb08f2e1f83bec0d895203f2f1d2b833.png)

```auto
[
    {
        "id": "022949e53279a741",
        "type": "inject",
        "z": "fe2c7bc6.bd49c8",
        "name": "stop default",
        "props": [
            {
                "p": "action",
                "v": "{\"command\":\"stop\"}",
                "vt": "json"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "x": 5840,
        "y": 5050,
        "wires": [
            [
                "d4b659133c509c6b"
            ]
        ]
    },
    {
        "id": "561cfc07f7e0c0da",
        "type": "ui-button",
        "z": "fe2c7bc6.bd49c8",
        "group": "5496b19a4e084472",
        "name": "refresh noire",
        "label": "",
        "order": 1,
        "width": "1",
        "height": "1",
        "emulateClick": false,
        "tooltip": "",
        "color": "",
        "bgcolor": "",
        "className": "",
        "icon": "refresh",
        "iconPosition": "left",
        "payload": "",
        "payloadType": "str",
        "topic": "",
        "topicType": "str",
        "buttonColor": "",
        "textColor": "",
        "iconColor": "",
        "enableClick": true,
        "enablePointerdown": false,
        "pointerdownPayload": "",
        "pointerdownPayloadType": "str",
        "enablePointerup": false,
        "pointerupPayload": "",
        "pointerupPayloadType": "str",
        "x": 5820,
        "y": 5080,
        "wires": [
            [
                "3fedb0188197880b"
            ]
        ]
    },
    {
        "id": "e11dd7afe2f06243",
        "type": "inject",
        "z": "fe2c7bc6.bd49c8",
        "name": "restart",
        "props": [
            {
                "p": "payload"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 5820,
        "y": 5110,
        "wires": [
            [
                "3fedb0188197880b"
            ]
        ]
    },
    {
        "id": "3fedb0188197880b",
        "type": "change",
        "z": "fe2c7bc6.bd49c8",
        "name": "restart noire",
        "rules": [
            {
                "t": "set",
                "p": "action",
                "pt": "msg",
                "to": "{\"command\":\"restart\",\"args\":[\"-loglevel\",\"quiet\",\"-rtsp_transport\",\"tcp\",\"-i\",\"rtsp://192.168.1.12:554/12\",\"-c:a\",\"aac\",\"-c:v\",\"copy\",\"-f\",\"mp4\",\"-movflags\",\"+frag_every_frame+empty_moov+default_base_moof\",\"-min_frag_duration\",\"500000\",\"pipe:1\",\"-progress\",\"pipe:2\"]}",
                "tot": "json"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 5960,
        "y": 5110,
        "wires": [
            [
                "d4b659133c509c6b"
            ]
        ]
    },
    {
        "id": "d4b659133c509c6b",
        "type": "ffmpeg",
        "z": "fe2c7bc6.bd49c8",
        "name": "",
        "outputs": 3,
        "cmdPath": "ffmpeg",
        "cmdArgs": "[\"-version\"]",
        "cmdOutputs": 2,
        "killSignal": "SIGTERM",
        "x": 6080,
        "y": 5070,
        "wires": [
            [
                "85f4d4cff74d42d3"
            ],
            [
                "85f4d4cff74d42d3"
            ],
            [
                "6b4334e0a2e9017a"
            ]
        ]
    },
    {
        "id": "85f4d4cff74d42d3",
        "type": "mp4frag",
        "z": "fe2c7bc6.bd49c8",
        "name": "testdash2noire",
        "outputs": 2,
        "basePath": "testdash2noire",
        "serveHttp": "true",
        "serveIo": "true",
        "hlsPlaylistSize": "3",
        "hlsPlaylistExtra": "1",
        "bufferPool": 0,
        "autoStart": "false",
        "preBuffer": 1,
        "timeLimit": "2000",
        "repeated": "false",
        "statusData": "playlist",
        "x": 6260,
        "y": 5030,
        "wires": [
            [
                "13352f64ced5b279"
            ],
            []
        ]
    },
    {
        "id": "6b4334e0a2e9017a",
        "type": "function",
        "z": "fe2c7bc6.bd49c8",
        "name": "progress",
        "func": "const props = msg.payload.toString().split('\\n');\nconst progress = {};\nprops.forEach(item => {\n const [name, value] = item.split('=');\n if (name && value) {\n progress[name] = value;\n }\n});\n\nconst color = progress['progress'] === 'continue' ? 'green' : 'red';\nconst message = `fps: ${progress['fps']}, bitrate: ${progress['bitrate']}`;\nnode.status({ fill: color, shape: 'dot', text: message });\nreturn { payload: progress };",
        "outputs": 1,
        "timeout": "",
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 6260,
        "y": 5080,
        "wires": [
            [
                "6f546b3baf7c9379"
            ]
        ]
    },
    {
        "id": "6f546b3baf7c9379",
        "type": "change",
        "z": "fe2c7bc6.bd49c8",
        "name": "",
        "rules": [
            {
                "t": "change",
                "p": "payload.bitrate",
                "pt": "msg",
                "from": "kbits/s",
                "fromt": "str",
                "to": "",
                "tot": "str"
            },
            {
                "t": "change",
                "p": "payload.bitrate",
                "pt": "msg",
                "from": " ",
                "fromt": "str",
                "to": "",
                "tot": "str"
            },
            {
                "t": "set",
                "p": "payload.bitrate",
                "pt": "msg",
                "to": "$number(payload.bitrate)",
                "tot": "jsonata"
            },
            {
                "t": "set",
                "p": "payload.bitrate",
                "pt": "msg",
                "to": "$round(payload.bitrate/8,1)",
                "tot": "jsonata"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 6375,
        "y": 5080,
        "wires": [
            [
                "3f174e338ae9c1fa"
            ]
        ],
        "l": false
    },
    {
        "id": "3f174e338ae9c1fa",
        "type": "function",
        "z": "fe2c7bc6.bd49c8",
        "name": "adapt text //1",
        "func": "//1.corbug erreur au demarrage\n\n// 0. SÉCURITÉ : Si payload n'existe pas ou n'est pas un objet, on quitte pour éviter le crash au Ctrl+F5\nif (!msg || !msg.payload || typeof msg.payload !== 'object') {\n return null; // Stoppe le traitement de ce message fantôme\n}\n\n// 1. Récupérer et formater la date/heure\nconst timestamp = new Date();\nconst heureLocale = timestamp.toLocaleString('fr-FR', {\n timeZone: 'Europe/Paris',\n hour: '2-digit',\n minute: '2-digit',\n second: '2-digit',\n day: '2-digit',\n month: '2-digit'\n});\nconst [date, heure] = heureLocale.split(' ');\nconst datetime = `${heure}<br>${date}`;\n\n// 2. Gestion du LABEL (Partie gauche - Corrigé ici)\nconst labelText = msg.payload.bitrate !== undefined ? msg.payload.bitrate : 0;\nmsg.ui_update = {\n label: `<div style='font-size: 12px; line-height: 1.2; color: #EEE;'>${labelText} KB/s<br><span style='color: grey;'>${datetime}</span></div>`,\n fontSize: \"16px\"\n};\n\n// 3. Gestion du PAYLOAD (Partie droite)\nconst valeurBrute = msg.payload.fps !== undefined ? msg.payload.fps : 0;\nmsg.payload = `<div style=\"color: #0da3d5; font-weight: bold; font-size: 15px; text-align: right;\">\n ${valeurBrute} \n <span style=\\\"font-size: 10px; color: white;\\\">fps</span>\n</div>`;\n\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 6480,
        "y": 5080,
        "wires": [
            [
                "6c1db2e2510998ec"
            ]
        ]
    },
    {
        "id": "13352f64ced5b279",
        "type": "ui-template",
        "z": "fe2c7bc6.bd49c8",
        "group": "5496b19a4e084472",
        "page": "",
        "ui": "",
        "name": "noire socket.io //2",
        "order": 3,
        "width": "6",
        "height": "6",
        "head": "",
        "format": "\n\n<template>\n <div ref=\"containerEl\"\n style=\"width:100%; height:100%; background:#000; position:relative; overflow:hidden; border-radius:4px; display:flex; align-items:center; justify-content:center;\">\n <video ref=\"videoEl\" autoplay muted playsinline\n style=\"width:100%; display:block; max-height:100%;\"></video>\n\n \n <div class=\"ptz-custom-controls\">\n \n <button @click.stop=\"toggleMute\" class=\"ptz-btn\" :title=\"isMuted ? 'Activer le son' : 'Couper le son'\">\n <svg v-if=\"isMuted\" viewBox=\"0 0 24 24\" class=\"ptz-icon\"><path fill=\"currentColor\" d=\"M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.21.05-.42.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z\"/></svg>\n <svg v-else viewBox=\"0 0 24 24\" class=\"ptz-icon\"><path fill=\"currentColor\" d=\"M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z\"/></svg>\n </button>\n\n \n <button @click.stop=\"togglePiP\" class=\"ptz-btn\" title=\"Mode incrustation (PiP)\">\n <svg viewBox=\"0 0 24 24\" class=\"ptz-icon\"><path fill=\"currentColor\" d=\"M19 11h-8v6h8v-6zm4 8V5c0-1.1-.9-2-2-2H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2zm-2 0H3V5h18v14z\"/></svg>\n </button>\n\n \n <button @click.stop=\"toggleFullscreen\" class=\"ptz-btn\" title=\"Plein écran\">\n <svg viewBox=\"0 0 24 24\" class=\"ptz-icon\"><path fill=\"currentColor\" d=\"M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z\"/></svg>\n </button>\n </div>\n\n <div v-if=\"shouldShowStatus\" :style=\"statusStyle\">\n {{ status }}\n </div>\n </div>\n</template>\n\n<style>\n .ptz-custom-controls {\n position: absolute;\n bottom: 8px;\n right: 8px;\n display: flex;\n gap: 6px;\n z-index: 20;\n opacity: 0.2;\n transition: opacity 0.3s ease;\n }\n\n .ptz-custom-controls:hover {\n opacity: 1;\n }\n\n .ptz-btn {\n background: rgba(0, 0, 0, 0.75);\n border: 1px solid rgba(255, 255, 255, 0.2);\n color: #ffffff;\n border-radius: 6px;\n width: 32px;\n height: 32px;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n transition: background 0.2s, border-color 0.2s;\n }\n\n .ptz-btn:hover {\n background: rgba(13, 163, 213, 0.9);\n border-color: #0da3d5;\n }\n\n .ptz-icon {\n width: 18px;\n height: 18px;\n }\n</style>\n\n<script>\nexport default {\n name: 'UiMp4FragSocketIoFixe',\n\n data() {\n return {\n status: 'Initialisation...',\n isError: false,\n isStoppedVoluntarily: false,\n isPlayingNormally: false,\n isMuted: true,\n socketIoConfig: null\n }\n },\n\n computed: {\n shouldShowStatus() {\n return !this.isPlayingNormally;\n },\n\n statusStyle() {\n return {\n position: 'absolute', top: '8px', left: '8px',\n color: '#ffffff', fontSize: '11px',\n fontFamily: 'monospace', pointerEvents: 'none',\n zIndex: 10\n }\n }\n },\n\n watch: {\n msg: {\n handler(newMsg) {\n if (!newMsg) return;\n if (newMsg.status === 'reset' || newMsg.payload === '') {\n this.handleVoluntaryStop();\n } else if (newMsg.status === 'playlist' && newMsg.payload?.socketIo) {\n this.socketIoConfig = newMsg.payload.socketIo;\n this.isStoppedVoluntarily = false;\n this.initStream();\n }\n },\n immediate: true,\n deep: true\n }\n },\n\n mounted() {\n this.$nextTick(() => {\n this.startVisibilityWatcher();\n });\n },\n\n unmounted() {\n this.cleanup();\n if (this._onVisibilityChange) {\n document.removeEventListener('visibilitychange', this._onVisibilityChange);\n }\n if (this._intersectionObserver) {\n this._intersectionObserver.disconnect();\n }\n },\n\n methods: {\n // --- BOUTONS ---\n toggleMute() {\n const video = this.$refs.videoEl;\n if (!video) return;\n video.muted = !video.muted;\n this.isMuted = video.muted;\n },\n\n toggleFullscreen() {\n const container = this.$refs.containerEl;\n if (!container) return;\n if (!document.fullscreenElement && !document.webkitFullscreenElement) {\n (container.requestFullscreen || container.webkitRequestFullscreen).call(container);\n } else {\n (document.exitFullscreen || document.webkitExitFullscreen).call(document);\n }\n },\n\n togglePiP() {\n const video = this.$refs.videoEl;\n if (!video) return;\n if (document.pictureInPictureElement) document.exitPictureInPicture();\n else if (video.requestPictureInPicture) video.requestPictureInPicture();\n },\n\n // --- STREAM SOCKET.IO ---\n initStream() {\n if (this.isStoppedVoluntarily || !this.socketIoConfig) return;\n const video = this.$refs.videoEl;\n if (!video) return;\n\n if (window.io) {\n this.setupSocketPlayer(video);\n } else if (!document.getElementById('socketio-cdn-script')) {\n const script = document.createElement('script');\n script.id = 'socketio-cdn-script';\n script.src = 'https://cdn.socket.io/4.7.5/socket.io.min.js';\n script.onload = () => this.setupSocketPlayer(video);\n document.head.appendChild(script);\n }\n },\n\n setupSocketPlayer(video) {\n this.cleanup();\n this.status = 'Connexion...';\n this.isPlayingNormally = false;\n\n const { path, namespace, key } = this.socketIoConfig;\n\n const socket = window.io(window.location.origin + namespace, {\n path: path,\n transports: ['websocket'],\n forceNew: true,\n auth: { key: key }\n });\n this._socket = socket;\n\n let sourceBuffer = null;\n let lastTimestamp = -1;\n const queue = [];\n\n const appendNext = () => {\n if (sourceBuffer && !sourceBuffer.updating && queue.length > 0) {\n try { sourceBuffer.appendBuffer(queue.shift()); }\n catch(e) { console.warn('appendBuffer:', e); }\n }\n };\n\n socket.on('auth', (authorized) => {\n if (!authorized) return;\n socket.emit('mime');\n socket.emit('auth', { key: key });\n });\n\n socket.on('mime', (mimeCodecs) => {\n if (!this._mediaSource) return;\n const trySetup = () => {\n if (this._mediaSource.readyState === 'open') {\n try {\n sourceBuffer = this._mediaSource.addSourceBuffer(mimeCodecs);\n sourceBuffer.mode = 'sequence';\n sourceBuffer.addEventListener('updateend', appendNext);\n socket.emit('initialization');\n } catch(e) { console.error('SourceBuffer:', e); }\n } else {\n setTimeout(trySetup, 50);\n }\n };\n trySetup();\n });\n\n socket.on('initialization', (data) => {\n const buf = data instanceof ArrayBuffer ? data\n : data?.segment ? new Uint8Array(data.segment).buffer\n : new Uint8Array(data).buffer;\n queue.push(buf);\n appendNext();\n socket.emit('segment', { buffered: false, all: false });\n });\n\n socket.on('segment', (data) => {\n const seg = data instanceof ArrayBuffer ? data\n : data?.segment ? new Uint8Array(data.segment).buffer\n : null;\n\n if (data?.timestamp !== undefined) lastTimestamp = data.timestamp;\n\n if (seg) {\n queue.push(seg);\n appendNext();\n }\n\n if (!this.isPlayingNormally) {\n video.muted = this.isMuted;\n video.play().then(() => {\n this.isPlayingNormally = true;\n this.isError = false;\n }).catch(e => {});\n }\n\n socket.emit('segment', { timestamp: lastTimestamp });\n });\n\n socket.on('mp4frag_error', (err) => {\n this.isError = true;\n this.status = 'Erreur : ' + (err?.message || err);\n });\n\n socket.on('connect', () => { this.isError = false; });\n\n socket.on('connect_error', (err) => {\n this.isError = true;\n this.status = 'Erreur socket';\n });\n\n socket.on('disconnect', () => {\n if (!this.isStoppedVoluntarily) {\n this.isPlayingNormally = false;\n this.status = 'Reconnexion...';\n }\n });\n\n const mediaSource = new MediaSource();\n this._mediaSource = mediaSource;\n const objectUrl = URL.createObjectURL(mediaSource);\n video.src = objectUrl;\n mediaSource.addEventListener('sourceopen', () => URL.revokeObjectURL(objectUrl));\n },\n\n handleVoluntaryStop() {\n this.isStoppedVoluntarily = true;\n this.isPlayingNormally = false;\n this.isError = false;\n this.status = 'Flux arrêté';\n this.cleanup();\n },\n\n cleanup() {\n if (this._socket) {\n this._socket.disconnect();\n this._socket = null;\n }\n if (this._mediaSource && this._mediaSource.readyState === 'open') {\n try { this._mediaSource.endOfStream(); } catch(e) {}\n }\n this._mediaSource = null;\n const video = this.$refs.videoEl;\n if (video) {\n video.pause();\n video.src = '';\n video.load();\n }\n },\n\n // --- VISIBILITY WATCHER ---\n startVisibilityWatcher() {\n this._onVisibilityChange = () => {\n if (document.hidden) this._pauseForVisibility();\n else this._resumeForVisibility();\n };\n document.addEventListener('visibilitychange', this._onVisibilityChange);\n\n const video = this.$refs.videoEl;\n if (video) {\n this._intersectionObserver = new IntersectionObserver(\n (entries) => {\n if (entries[0].intersectionRatio < 0.5) this._pauseForVisibility();\n else this._resumeForVisibility();\n },\n { threshold: [0, 0.5, 1.0] }\n );\n this._intersectionObserver.observe(video);\n }\n },\n\n _pauseForVisibility() {\n if (this.isStoppedVoluntarily || this._pausedForVisibility) return;\n this._pausedForVisibility = true;\n const video = this.$refs.videoEl;\n if (video) video.pause();\n this.isPlayingNormally = false;\n this.status = 'Pause...';\n },\n\n _resumeForVisibility() {\n if (!this._pausedForVisibility || this.isStoppedVoluntarily) return;\n this._pausedForVisibility = false;\n const video = this.$refs.videoEl;\n if (!video) return;\n video.play().then(() => {\n this.isPlayingNormally = true;\n }).catch(e => console.log('Resume:', e));\n }\n }\n}\n</script>",
        "storeOutMessages": true,
        "passthru": false,
        "resendOnRefresh": true,
        "templateScope": "local",
        "className": "",
        "x": 6590,
        "y": 5050,
        "wires": [
            []
        ]
    },
    {
        "id": "6c1db2e2510998ec",
        "type": "ui-text",
        "z": "fe2c7bc6.bd49c8",
        "group": "5496b19a4e084472",
        "order": 4,
        "width": "3",
        "height": "1",
        "name": "noire fps",
        "label": "",
        "format": "{{msg.payload}}",
        "layout": "row-left",
        "style": false,
        "font": "Arial,Arial,Helvetica,sans-serif",
        "fontSize": "12",
        "color": "#ffffff",
        "wrapText": false,
        "className": "",
        "value": "payload",
        "valueType": "msg",
        "x": 6620,
        "y": 5080,
        "wires": []
    },
    {
        "id": "e862467bbd11be70",
        "type": "ui-button",
        "z": "fe2c7bc6.bd49c8",
        "group": "5496b19a4e084472",
        "name": "stop noire",
        "label": "",
        "order": 2,
        "width": "1",
        "height": "1",
        "emulateClick": false,
        "tooltip": "",
        "color": "",
        "bgcolor": "",
        "className": "",
        "icon": "stop",
        "iconPosition": "left",
        "payload": "",
        "payloadType": "str",
        "topic": "",
        "topicType": "str",
        "buttonColor": "",
        "textColor": "",
        "iconColor": "",
        "enableClick": true,
        "enablePointerdown": false,
        "pointerdownPayload": "",
        "pointerdownPayloadType": "str",
        "enablePointerup": false,
        "pointerupPayload": "",
        "pointerupPayloadType": "str",
        "x": 5810,
        "y": 5150,
        "wires": [
            [
                "1cc4a0af2f697617"
            ]
        ]
    },
    {
        "id": "1cc4a0af2f697617",
        "type": "change",
        "z": "fe2c7bc6.bd49c8",
        "name": "stop noire",
        "rules": [
            {
                "t": "set",
                "p": "action",
                "pt": "msg",
                "to": "{\"command\":\"stop\"}",
                "tot": "json"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 5970,
        "y": 5150,
        "wires": [
            [
                "d4b659133c509c6b"
            ]
        ]
    },
    {
        "id": "5496b19a4e084472",
        "type": "ui-group",
        "name": "noire",
        "page": "5e4ae4ec6d0fef71",
        "width": 6,
        "height": 1,
        "order": 1,
        "showTitle": true,
        "className": "",
        "visible": "true",
        "disabled": "false",
        "groupType": "default"
    },
    {
        "id": "5e4ae4ec6d0fef71",
        "type": "ui-page",
        "name": "test",
        "ui": "3dcb17d3fbb77cb1",
        "path": "/page8",
        "icon": "home",
        "layout": "grid",
        "theme": "2da75e915972c00d",
        "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": 8,
        "className": "",
        "visible": "true",
        "disabled": "false"
    },
    {
        "id": "3dcb17d3fbb77cb1",
        "type": "ui-base",
        "name": "Page flowfuse",
        "path": "/dashboard",
        "appIcon": "",
        "includeClientData": true,
        "acceptsClientConfig": [
            "ui-control",
            "ui-notification"
        ],
        "showPathInSidebar": false,
        "headerContent": "page",
        "navigationStyle": "temporary",
        "titleBarStyle": "default",
        "showReconnectNotification": true,
        "notificationDisplayTime": 5,
        "showDisconnectNotification": true,
        "allowInstall": true
    },
    {
        "id": "2da75e915972c00d",
        "type": "ui-theme",
        "name": "Theme black",
        "colors": {
            "surface": "#177278",
            "primary": "#0da3d5",
            "bgPage": "#111111",
            "groupBg": "#333333",
            "groupOutline": "#787878"
        },
        "sizes": {
            "density": "default",
            "pagePadding": "1px",
            "groupGap": "4px",
            "groupBorderRadius": "2px",
            "widgetGap": "3px"
        }
    },
    {
        "id": "3dce48901209b9e9",
        "type": "global-config",
        "env": [],
        "modules": {
            "@flowfuse/node-red-dashboard": "1.30.2",
            "@kevingodell/node-red-ffmpeg": "0.1.1-beta.3",
            "@kevingodell/node-red-mp4frag": "0.1.1-beta.2"
        }
    }
]

```

My camera is called "noire" and the dashboard is in the page: test and the group: noire. Personalize to your liking.

In " **Restart noire**" at the line: `"rtsp://192.168.1.12:554/12",`  
**replace what is in the quotes with the address of your IP camera.**

 ![garage](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/9/0/9036cb6df8c288d87817528ff3367b11854b75d7.jpeg)

The result:

- a stream from the camera with integrated buttons: mute, picture in picture and large screen.
- A stream start/restart and stop button.
- And optional flow rate indications.

As I am not a programmer, but I have ideas 😀, Obviously, I asked the AI ​​so there are surely imperfections in the Template, but I apologize in advance for not being able to resolve the bugs if there are any.

This Flow works well, I don't have many bugs. It consumes very few resources thanks to the performance of the original nodes from @kevingodell, on a Raspberry PI 3B+, and NR 4.1.11, Raspbian BUSTER, nodejs v18.20.2.

TO DO: upgrade to NR 5.0.1 and nodejs 24, with Raspberry Pi OS (64-bit) (3B compatible)

Enjoy

---

<div class="post-metadata">

**Author:** ![krambriw](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/krambriw/32/5429_2.png) [@krambriw](https://discourse.nodered.org/u/krambriw)\
**Post date:** [8 July 2026 18:38 UTC](https://discourse.nodered.org/t/rtsp-ip-live-camera-in-dashboard-2-0/101423/2 "2026-07-08T18:38:56Z")

</div>

Hello @SuperNinja, nice to see you back here again!

Maybe this posting may give some additional (or maybe the same) info:

> [@Migrating ui template from Dashboard-1 to Dashboard-2](https://discourse.nodered.org/t/migrating-ui-template-from-dashboard-1-to-dashboard-2/92891#:~:text=Migrating%20ui%20template%20from%20Dashboard%2D1%20to%20Dashboard%2D2):
>
> 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 som…

---

<div class="post-metadata">

**Author:** ![SuperNinja](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/superninja/32/5761_2.png) [@SuperNinja](https://discourse.nodered.org/u/SuperNinja)\
**Post date:** [9 July 2026 16:15 UTC](https://discourse.nodered.org/t/rtsp-ip-live-camera-in-dashboard-2-0/101423/3 "2026-07-09T16:15:23Z")

</div>

Hi @krambriw, yes, I still check the forum from time to time. 😉  
The switch to DB2 is proving more painful than I expected... but I'm managing reasonably well thanks to AI. My code probably looks awful to a pro programmer, but at least it works.  
Thanks for the link—I'll take a look.  
Cheers.

---

<div class="post-metadata">

**Author:** ![Nodi.Rubrum](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/nodi.rubrum/32/107482_2.png) [@Nodi.Rubrum](https://discourse.nodered.org/u/Nodi.Rubrum)\
**Post date:** [20 July 2026 18:05 UTC](https://discourse.nodered.org/t/rtsp-ip-live-camera-in-dashboard-2-0/101423/4 "2026-07-20T18:05:09Z")

</div>

Unofficial rule... as a programmer... never negatively critique code not yours... until yours if perfect... and since it never is perfect... well you get the point. 🙂 My code is never perfect! LOL
