Cctv recording and playback (work in progress)

I have been experimenting with a different way of recording video from my rtsp ip cams lately. The new technique I have settled on is using a single file byte-range HLS playlist. When the video is recorded and written to disc, there will be a single mp4 file along with it's companion HLS playlist.

So far, video playback has been working great using hls.js on modern browsers and native HLS on mac/ios safari browsers. The benefits, as I see them, is that the recording can be played in the browser while being written to disc and allows for easy scrubbing along the video timeline due to the nature of the byte-range requests.

I was previously just recording as mp4 files using FFmpeg's segment muxer, but found that there was no good way to play the video before the segment was complete. Unfortunately, I could not find a way to combine FFmpeg's segment muxer and the byte-range HLS muxer to do what I wanted, so I had to involve the function node to programmably write the HLS playlist.

For my particular setup with a raspberry pi 4, I am using an externally powered usb connected case holding a 6TB WD Purple meant for 24/7 video recording. I have configured node-red's settings.js file to include:

httpStaticRoot: '/static/',
httpStatic: [
        { path: '/run/media/system/surveillance1/cctv/recordings/', root: '/recordings/' }
    ],

The recordings are placed into the folder /run/media/system/surveillance1/cctv/recordings/ and served via http at /static/recordings/.

The files created are automatically named based on the current timestamp every 15 minutes and placed into folders created for the year, month, day.

The HLS playlist will look a bit like this:

#EXTM3U
#EXT-X-VERSION:7
#EXT-X-PLAYLIST-TYPE:EVENT
#EXT-X-TARGETDURATION:1
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-MAP:URI="02h.15m.00s.mp4",BYTERANGE="1242@0"
#EXTINF:1.0002555555555555
#EXT-X-BYTERANGE:707851@1242
02h.15m.00s.mp4
#EXTINF:0.9962444444444445
#EXT-X-BYTERANGE:679209@709093
02h.15m.00s.mp4

flow for a 24/7 video recording setup: (Do not use this without configuring the recording node)

[{"id":"f3874cf4a963908d","type":"subflow","name":"recording","info":"","category":"","in":[{"x":80,"y":160,"wires":[{"id":"d0615cb16ccf58fb"}]}],"out":[{"x":320,"y":120,"wires":[{"id":"d0615cb16ccf58fb","port":0}]}],"env":[{"name":"STATIC_PATH","type":"str","value":"/run/media/system/surveillance1/cctv/recordings"},{"name":"STATIC_ROOT","type":"str","value":"/static/recordings"}],"meta":{},"color":"#DDAA99","status":{"x":320,"y":200,"wires":[{"id":"d0615cb16ccf58fb","port":1}]}},{"id":"d0615cb16ccf58fb","type":"function","z":"f3874cf4a963908d","name":"recording","func":"const { payload, payload: { length }, duration, topic } = msg;\n\nconst [ , id, , type ] = topic.split('/');\n\nconst recording = context.get('recording');\n\nif (type === 'seg' || type === 'pre') {\n\n    const { mp4WriteStream = {}, m3u8WriteStream = {}, mp4Name, bytesWritten, segments, playlist } = recording;\n\n    if (mp4WriteStream.writable && m3u8WriteStream.writable) {\n\n        // write live (seg) or previous (pre) segments to existing writeStreams\n\n        mp4WriteStream.write(payload);\n\n        m3u8WriteStream.write(`#EXTINF:${duration}\\n#EXT-X-BYTERANGE:${length}@${bytesWritten}\\n${mp4Name}\\n`);\n\n        recording.bytesWritten += length;\n\n        recording.segments++;\n\n        // only send playlist after the first segment is written so that hls.js does not throw Hls.Events.ERROR, Hls.ErrorTypes.NETWORK_ERROR, Hls.ErrorDetails.LEVEL_EMPTY_ERROR\n\n        if (segments === 0) {\n\n            return { payload: playlist };\n\n        }\n\n    } else {\n\n        node.error(`mp4WriteStream and m3u8WriteStream must be writable`);\n\n    }\n\n    return;\n\n}\n\nif (type === 'init') {\n\n    // create new or end existing write streams based on buffer length\n\n    if (length) {\n\n        const date = new Date();\n\n        const year = date.getFullYear();\n\n        const month = (date.getMonth() + 1).toString().padStart(2, '0');\n\n        const day = date.getDate().toString().padStart(2, '0');\n\n        const hours = date.getHours().toString().padStart(2, '0');\n\n        const minutes = date.getMinutes().toString().padStart(2, '0');\n\n        const seconds = date.getSeconds().toString().padStart(2, '0');\n\n        // create name based on timestamp\n\n        const baseName = `${hours}h.${minutes}m.${seconds}s`; // 00h.00m.00s\n\n        const mp4Name = `${baseName}.mp4`; // 00h.00m.00s.mp4\n\n        const m3u8Name = `${baseName}.m3u8`; // 00h.00m.00s.m3u8\n\n        const baseRoot = `${env.get('STATIC_ROOT')}/${id}`;\n\n        const basePath = `${env.get('STATIC_PATH')}/${id}`;\n\n        // create dir base on timestamp\n\n        const fullDir = `${basePath}/${year}/${month}/${day}`; // basePath/yyyy/mm/dd\n\n        const { mkdirSync, createWriteStream, writeFile } = fs;\n\n        const newDir = mkdirSync(fullDir, { recursive: true });\n\n        newDir && writeFile(`${basePath}/listing.txt`, `${year}/${month}/${day}\\n`, { encoding: 'utf8', flag: 'a' }, (err) => { err && node.error(err) });\n\n        writeFile(`${fullDir}/listing.txt`, `${m3u8Name}\\n`, { encoding: 'utf8', flag: 'a' }, (err) => { err && node.error(err) });\n\n        // create new write streams for mp4 and playlist\n       \n        const mp4WriteStream = createWriteStream(`${fullDir}/${mp4Name}`)\n            .on('error', (err) => {\n                node.error(err);\n            });\n\n        const m3u8WriteStream = createWriteStream(`${fullDir}/${m3u8Name}`)\n            .on('error', (err) => {\n                node.error(err);\n            });\n\n        mp4WriteStream.write(payload);\n\n        m3u8WriteStream.write(`#EXTM3U\\n#EXT-X-VERSION:7\\n#EXT-X-PLAYLIST-TYPE:EVENT\\n#EXT-X-TARGETDURATION:${Math.round(duration) || 1}\\n#EXT-X-MEDIA-SEQUENCE:0\\n#EXT-X-MAP:URI=\"${mp4Name}\",BYTERANGE=\"${length}@0\"\\n`);\n\n        recording.mp4WriteStream = mp4WriteStream;\n\n        recording.m3u8WriteStream = m3u8WriteStream;\n\n        recording.mp4Name = mp4Name;\n\n        recording.bytesWritten = length;\n\n        recording.segments = 0;\n\n        // save playlist\n\n        recording.playlist = `${baseRoot}/${year}/${month}/${day}/${m3u8Name}`;\n\n        // send status\n\n        return [ null, { payload: { fill: 'green', shape: \"dot\", text: `${id}/${year}/${month}/${day}/${baseName}` } }];\n\n    } else {\n\n        // end existing writeStreams\n\n        const { mp4WriteStream = {}, m3u8WriteStream = {} } = recording;\n\n        if (mp4WriteStream.writable) {\n\n            mp4WriteStream.end();\n\n        }\n\n        if (m3u8WriteStream.writable) {\n\n            m3u8WriteStream.end('#EXT-X-ENDLIST\\n');\n\n        }\n\n        return;\n\n    }\n\n}","outputs":2,"noerr":0,"initialize":"// Code added here will be run once\n// whenever the node is deployed.\n\ncontext.set('recording', {});","finalize":"// Code added here will be run when the\n// node is being stopped or re-deployed.\n\nconst { mp4WriteStream = {}, m3u8WriteStream = {} } = context.get('recording');\n\nif (mp4WriteStream.writable) {\n\n    mp4WriteStream.end();\n\n}\n\nif (m3u8WriteStream.writable) {\n\n    m3u8WriteStream.end('#EXT-X-ENDLIST\\n');\n\n}\n\ncontext.set('recording', undefined);","libs":[{"var":"fs","module":"fs"}],"x":200,"y":160,"wires":[[],[]]},{"id":"32d61e0f.0490b2","type":"subflow","name":"stderr","info":"","category":"","in":[{"x":60,"y":80,"wires":[{"id":"211d46bf.a79c7a"}]}],"out":[],"env":[],"color":"#DDAA99","status":{"x":280,"y":120,"wires":[{"id":"211d46bf.a79c7a","port":1}]}},{"id":"211d46bf.a79c7a","type":"function","z":"32d61e0f.0490b2","name":"stderr","func":"// convert payload for Buffer to String and split by newlines\n\nconst stderr = msg.payload.toString().split('\\n');\n\nnode.send([ { stderr }, { payload: { fill: 'red', text: `${new Date().toString()}` } } ]);","outputs":2,"noerr":0,"initialize":"","finalize":"","libs":[],"x":170,"y":80,"wires":[["c6e5408.f19c4c"],[]]},{"id":"c6e5408.f19c4c","type":"debug","z":"32d61e0f.0490b2","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"stderr","targetType":"msg","statusVal":"","statusType":"auto","x":330,"y":40,"wires":[]},{"id":"9b59c1a2222210b8","type":"inject","z":"0528fb314ac39109","name":"start","props":[{"p":"action","v":"{\"command\":\"start\"}","vt":"json"}],"repeat":"","crontab":"","once":false,"onceDelay":"5","topic":"","x":130,"y":220,"wires":[["2281776c992410dd"]]},{"id":"2281776c992410dd","type":"ffmpeg-spawn","z":"0528fb314ac39109","name":"","outputs":3,"cmdPath":"","cmdArgs":"[\"-loglevel\",\"+level+fatal\",\"-nostats\",\"-re\",\"-f\",\"lavfi\",\"-i\",\"testsrc=size=qcif:rate=5[out0];anoisesrc=c=brown:r=44100:a=0.5[out1]\",\"-g\",\"5\",\"-c:a\",\"aac\",\"-c:v\",\"libx264\",\"-profile:v\",\"high\",\"-pix_fmt\",\"yuv420p\",\"-level\",\"5.0\",\"-f\",\"mp4\",\"-movflags\",\"+faststart+frag_keyframe+empty_moov+default_base_moof\",\"-metadata\",\"title=test source\",\"pipe:1\"]","cmdOutputs":2,"killSignal":"SIGTERM","x":300,"y":260,"wires":[["58b77ee9a2e29092"],["58b77ee9a2e29092"],["a8741b7c88d88102"]]},{"id":"b4a8c0bc44499078","type":"inject","z":"0528fb314ac39109","name":"restart","props":[{"p":"action","v":"{\"command\":\"restart\"}","vt":"json"}],"repeat":"","crontab":"","once":false,"onceDelay":"1","topic":"","x":130,"y":260,"wires":[["2281776c992410dd"]]},{"id":"7dde9f15d2aa5933","type":"inject","z":"0528fb314ac39109","name":"stop","props":[{"p":"action","v":"{\"command\":\"stop\"}","vt":"json"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":130,"y":300,"wires":[["2281776c992410dd"]]},{"id":"58b77ee9a2e29092","type":"mp4frag","z":"0528fb314ac39109","name":"","outputs":2,"hlsPlaylistSize":4,"hlsPlaylistExtra":0,"basePath":"id","repeated":"false","timeLimit":"-1","preBuffer":"0","autoStart":"true","statusLocation":"displayed","x":550,"y":220,"wires":[[],["571c8af49dfc40ae"]]},{"id":"a8741b7c88d88102","type":"subflow:32d61e0f.0490b2","z":"0528fb314ac39109","name":"","x":470,"y":300,"wires":[]},{"id":"ef6008f4d7425c22","type":"inject","z":"0528fb314ac39109","name":"write restart","props":[{"p":"action","v":"{\"command\":\"restart\",\"subject\":\"write\",\"preBuffer\":1,\"timeLimit\":-1,\"repeated\":false}","vt":"json"}],"repeat":"","crontab":"*/15 0-23 * * *","once":false,"onceDelay":"5","topic":"","x":310,"y":140,"wires":[["58b77ee9a2e29092"]]},{"id":"e23111864612f2c5","type":"inject","z":"0528fb314ac39109","name":"write stop","props":[{"p":"action","v":"{\"command\":\"stop\",\"subject\":\"write\"}","vt":"json"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":320,"y":180,"wires":[["58b77ee9a2e29092"]]},{"id":"571c8af49dfc40ae","type":"subflow:f3874cf4a963908d","z":"0528fb314ac39109","name":"","x":780,"y":260,"wires":[["e2da3c3cee315665"]]},{"id":"e2da3c3cee315665","type":"ui_mp4frag","z":"0528fb314ac39109","name":"","group":"9a076144e0b8c687","order":1,"width":"20","height":"14","readyPoster":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAyAAAAJYCAMAAACtqHJCAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDowMzgwMTE3NDA3MjA2ODExODIyQUIzREQ3RTA0MDAyNCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpGRkU5MkM4RDJCNTkxMUVCQjJCOTg0MEQ0QThCQ0YyQyIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpGRkU5MkM4QzJCNTkxMUVCQjJCOTg0MEQ0QThCQ0YyQyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M2IChNYWNpbnRvc2gpIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6Rjc3RjExNzQwNzIwNjgxMTgyMkFDQTkzQjdGQkY1MzAiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MDM4MDExNzQwNzIwNjgxMTgyMkFCM0REN0UwNDAwMjQiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz7USCkmAAAAM1BMVEUAplH///9Cs3HR6dqc0rDp9O1rwIy43sUmrWJ9xpn0+vbF5NBYun+q2LuNzKTd7+P///8HAxLBAAAAEXRSTlP/////////////////////ACWtmWIAAA6fSURBVHja7d1rY6JIGoBRI6gIXub//9qZ7o4KRdVbZezuJLvnfNpJektujyAg2fwDFG0sAhAICAQEAgIBgYBAQCAgEBAIIBAQCAgEBAICAYGAQEAgIBAQCCAQEAgIBAQCAgGBgEBAICAQQCAgEBAICAQEAgIBgYBAQCAgEEAgIBAQCAgEBAICAYGAQEAggEBAICAQEAgIBAQCAgGBgEBAIIBAQCAgEBAICAQEAgIBgYBAAIGAQEAgIBAQCAgEBAICAYGAQACBgEBAICAQEAgIBAQCAgGBgEAsAhAICAQEAgIBgYBAQCAgEBAIIBAQCAgEBAICAYGAQEAgIBAQCCAQEAgIBAQCAgGBgEBAICAQQCAgEBAICAQEAgIBgYBAQCAgEEAgIBAQCAgEBAICAYGAQEAggEBAICAQEAgIBAQCAgGBgEBAIIBAQCAgEBAICAQEAgIBgYBAAIGAQEAgIBAQCAgEBAICAYGAQACBgEBAICAQEAgIBAQCAgGBgEAsAhAICAQEAgIBgYBAQCAgEBAIIBAQCAgEBAICAYGAQEAgIBAQCCAQEAgIBAQCAgGBgEBAICAQQCAgEBAICAQEAgIBgYBAQCAgEEAgIBAQCAgEBAICAYGAQEAggEBAICAQEAgIBAQCAgGBgEBAIIBAQCAgEBAICAQEAgIBgYBAAIGAQEAgIBAQCAgEBAICAYGAQIAkEAAAAAAAAAAAAAAAAAAAAAAAgD/v0u/efjj0228/L33/c1be+t56/d92+rWi33aZnw2b7fv/6n7+/PZfN12/n/2/0t++b0H3PMbZT6dsIskI1/64+lUXzsQP558/OdxeaL5Rv/9su57WTLPrMdcjJb9cT+TwPtO7IZrR5ctXlmNxwsprp7IwCN2229mPjr9+Mm4qgfxY88e2FTt0yc+P9UB+TMKlKZDLvL35LPyYh7vpMURmWrt9bcz7ZF6TkLfFibwNcqnN6Nt13xpIacLKa6eyMAjd1vZp9e7SNQTy37tfy4odrqtfXFoC+e+Vh4ZAzvONYtn9Y7aG2RRt422wMOb7OLvVdrgtBbLL/P9rL18LpDRhwdqJFwahaf2Wfrgv3IZA7oVEK/aQ+c22KZDkpbtwHma7wmm1BRxnW0l+Wi+VMTO/WEzVaiILO5C4gVogpQkL1k68MIg/bq7fS673n7QEcksrWLH78pbfuN3EgSze00+LbXNK33mv5Vcat/GYm+K87POBFHYg8cvXAilNWLB24oVB6Lj+vDfeF21TILvq5j1lf3Vq3G6GWiBDptf7z4Yk+3MwJ3085k9d+26utAMpvfy5JZDihAVrJ14YNJ3G6lbbxrYxkPe1VF6xw/xD43AonRuKNtswkFNuU7omm9Awf7MvvNJYGTOdl+1hcbyYTmRpBxK/fCWQ0oSFaydcGISG1ee9/eP4Nh/Ir386nJefQsrb8HG+L3gc1F1L282v8ftx9u/CQI6LDWJaHjqeV6fmVsNdduudWnbM9byc5zuJZCKLO5D45eOjyeKEhWsnXBjEdunu91LaLrdJS/3iv8sr9pyc8h3TV8yu4sfR/lDZavrlCdPlO+11ORFTblq364/p2TEz8/KWOXDrFgdju00lkOTlK4GUJixcO+HCINal756zpRcHsjy0La/Y9ETZtfAhJB1/9u/CreZ2Tn95ZicJ/zpPIB1uKp0r6tKzRYV5mdaj7uvns7MvXwmkNGHx2okWBrFz+nlvKh36p6vgvgVv4xWbJjgVjoDT8Q+PfxduNb8mY0wuDi/n67TYQtLh1qfy8mMW5+W6HrW8A4lfvhJIacLitRMtDJ48zzt7e6kE0uU++JYD2a43/KZALpWt5n0bPS5LX56e6xfHGKXzTX1lzOK87FajBjuQ4ssfGgIpTVi8dqKFQWyfXhLPXFD70oG8vx9Op2QrH+ejTdmLccVASmO2BxLsQF4JpDhhlbUTLAxi2+QD3Ha2//0WgRxvqzspfZofDY2LI6NqIKUxmwOJdiCvBFKcsMraCRYGFePynN9+9p/fIpD+9q92y9LnW/1pOXI1kNKYzYFEO5BXAilOWGXtBAuDiuvyI1s/Wz/fIpDD7f1wWpY+zK4V9MvrANVr3qUxWwPZRjuQuM84kOKEVdZOsDCoSLbXc/HM/hcN5Hrr+5y8zvWxDU3Lj7Sl00j76piNgRzCN+kXAilOWGXtBAuDxtNYl9VZ3u8RyHhb831yZqd/HHePy7fbdLhucXk8GrMLvmj0GHUb303+QiDFCasFUl4YVByXa3M3W/DfIZDt/Tf7ZLN8nOk5JleOk+GOb/nfZ8ZsC+SQBtd2HWRfDaQ8YbVAyguDitNy8yhf/PuagRzvH3CH9C7I3W3GzskngsVw2/NbOkHlMZsCqexA4h1Yy21nuZmtBFJeGNQszvOeyjf1fVYg8ZX0/rExjsk/Ot82vGty5Xgb3G4ej9kUSGUHUtqB1W76jGe2FkhxYVCzmzdxnNfykUDWXzb8aCBt92IdVnd1j+nB46V0SjT/5eFozJZAdrXvsxZ2YKdNvBzjma0FUlwY1Ezzld6X73r9y4E03s3bPbauQ3qP0bjYYvsokPHSNGZLIG+177OGO7AwkGBma4EUFwY15/mSPMyX32cG0vp9kPFxW+vqbO1U2GBzW+h5aBnzmUDOTwUSfLf/0DKz1UCmtnpZWZxl7HLfEvy7gTz1jcJhNk3HwgXx1UWJ7CuNp4YxnwlkfCaQy6YhkGhmq4GUFgY1+/k73pg7Hv7MQCrfSd/PDglP6Vv3UHhDrzy0IRjzmUCKJ4rCI7wokGhmq4EMTXs3NqU34Z/LfQi+N/E5gVSuns3vmxhWX0S9PvWAg64+5lOBdM8cYnUNgUQzWw2ktDCoGldfztxtvkogtedinecHGqvHJPTJnih7nvXYvWXPiObGfCqQ0oM9831O9UCima0HUlgYVF0f39+8ZFfW370Osuij9mTFbj5Wl57GOmWfb1D8zuuhOuZzgZyfCaT0dK3Gma0HUlgYVB0eZzb67CnHzwqk4dm843zs8+oUzZh7Qs5quO3yY3Uw5nOBjA3XQfbTcqut33aWn9l6IIWFQdXsxrdD8M27vxzIruXp7sNiS+xXn44fT7IdgkCWt/xHY7YFMh6eud29fyvfvdA+sw2B5BcGVbMzhtfFQv3cW02iLTo5bLguZ+S8mrXllePVcIvnSERjtgXSn8KP6enLj43PxQpntiGQ/MKg6vQ4BB+zx7VfN5D8Z6bHIfb9IVx9ONxhfnInGrPl+yA/PgJfo4/p6cufG5+LFc5sQyD5hUHdfZ0sd+LfIJDzco2vR+hyL1YM5FIbsymQS/aZ4OWXb33sTzizDYHkFwbNp7F2tytR128TSP652G8vBRKN2RLI/HnRY0sgrd9JD2dWIH/QbckPl/x9QV83kF1+mzm9Ekg0ZvNjfw7BFbn05feNgYQzK5A/6H7G8Jy/v+fLBjLkN5nZZvl8IOGYTz84rvt9gcQzK5A/6LaPP4YPN/iCgZzeyjc4fjSQcMzmQO5v99vfFkg8swL5C6ex+uvyCOXLB3IsbDPTC4GEY7YHEnxM/2Ag8cwK5A+67bwPyWfcz314dUMgfWGbuT4ZyPyh7eGYhXnZNSytlwOJZzZeOwJ5ze3pisky/tw/f9AQSOG8zuw0Vlsg80t14Zhp3N38y7PLUbvix/QPBhLPbLx2BPKaLn+A8ql/QKclkNUfulhd6W4K5DjfmMIxk3m5lZXbtC/FGwM/GEg8s/HaEchrDvmbUD/zT7A1BbLaF63OrrYE0i8OVsIxb/OyG9YfNEp3kGx/UyDxzMZrRyCv6eOvf/7dP+IZBLK8mzHzl2BWH43DQHJTG4+5nJflA0lKd5D0HwpkNWWVmd1Wz3gL5OOOmS8mbD7rz0C3B7L+Y5TH9H3zmUB+7uIqY3blRZBu96fS/HwskMqExWtHIK9ZLt3hqUCO1UD20VdMXwmkXw2V/rnKpwLpZ2/LpTH39SsRq5voj78lkMqEbes7EIF8XP5rPi2BnDfVQJKPOKULaM8HclgfrKXz8EQg03xai2NO1X1olx4CTY2BdOFyrExY7WmRAnnJNfve3hDIYdMQyHB9a3jix/OBXNcH+ekF7PZApsWSKI457EoHi6tASufrSsdi13A5Vias9rRIgbxkyi7VaiC746YlkM3QRXv+DwcyrgdLL7q0BnKfk+qY26T2cV881XbIf0xP/+HikkVxOVYmLF47AvmNp7H6xkC6fl89bHk8w22cv1mH3yNqDuSUOVpLTx01BTKbk5Yxz4s36aF8Lnq/PClc/IfTbLdaWo61CYvXjkC+vkv/64jg0G+///vJ7eEWvpsHAAAAAAAAAAAAAAAAAAAAAAAAwP+Xf4AigYBAQCAgEBAICAQEAgIBgYBAAIGAQEAgIBAQCAgEBAICAYGAQACBgEBAICAQEAgIBAQCAgGBAAIBgYBAQCAgEBAICAQEAgIBgQACAYGAQEAgIBAQCAgEBAICAQQCAgGBgEBAICAQEAgIBAQCAgEEAgIBgYBAQCAgEBAICAQEAggEBAICAYGAQEAgIBAQCAgEBAIIBAQCAgGBgEBAICAQEAgIBBAICAQEAgIBgYBAQCAgEBAICAQQCAgEBAICAYGAQEAgIBAQCAgEEAgIBAQCAgGBgEBAICAQEAggEBAICAQEAgIBgYBAQCAgEBAIIBAQCAgEBAICAYGAQEAgIBBAICAQEAgIBAQCAgGBgEBAICAQQCAgEBAICAQEAgIBgYBAQCCAQEAgIBAQCAgEBAICAYGAQEAggEBAICAQEAgIBAQCAgGBgEAAgYBAQCAgEBAICAQEAgIBgYBAAIGAQEAgIBAQCAgEBAICAYGAQACBgEBAICAQEAgIBAQCAgGBAAIBgYBAQCAgEBAICAQEAgIBgQACAYGAQEAgIBAQCAgEBAICAQQCAgGBgEBAICAQEAgIBAQCAgEEAgIBgYBAQCAgEBAICAQEAggEBAICAYGAQEAgIBAQCAgEBAIk/gX38R0SGa0LJgAAAABJRU5ErkJggg==","errorPoster":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAyAAAAJYCAMAAACtqHJCAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDowMzgwMTE3NDA3MjA2ODExODIyQUIzREQ3RTA0MDAyNCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpGRkU5MkM5MTJCNTkxMUVCQjJCOTg0MEQ0QThCQ0YyQyIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpGRkU5MkM5MDJCNTkxMUVCQjJCOTg0MEQ0QThCQ0YyQyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M2IChNYWNpbnRvc2gpIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6Rjc3RjExNzQwNzIwNjgxMTgyMkFDQTkzQjdGQkY1MzAiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MDM4MDExNzQwNzIwNjgxMTgyMkFCM0REN0UwNDAwMjQiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz72S0NpAAAAM1BMVEXtHCT////vT1T609T1oaP4u7zydHf96ur0k5XuOT7+9fX5x8jzhIfxY2b3rrD739////825EprAAAAEXRSTlP/////////////////////ACWtmWIAAA5DSURBVHja7d3rYqJIAoBRI3gBUeb9n3anu2Okirphujtm9pxfu82kIlifIKDZ/QNk7WwCEAgIBAQCAgGBgEBAICAQEAggEBAICAQEAgIBgYBAQCAgEBAIIBAQCAgEBAICAYGAQEAgIBBAICAQEAgIBAQCAgGBgEBAICAQQCAgEBAICAQEAgIBgYBAQCCAQEAgIBAQCAgEBAICAYGAQEAggEBAICAQEAgIBAQCAgGBgEAAgYBAQCAgEBAICAQEAgIBgYBAAIGAQEAgIBAQCAgEBAICAYGAQGwCEAgIBAQCAgGBgEBAICAQEAggEBAICAQEAgIBgYBAQCAgEBAIIBAQCAgEBAICAYGAQEAgIBBAICAQEAgIBAQCAgGBgEBAICAQQCAgEBAICAQEAgIBgYBAQCCAQEAgIBAQCAgEBAICAYGAQEAggEBAICAQEAgIBAQCAgGBgEAAgYBAQCAgEBAICAQEAgIBgYBAAIGAQEAgIBAQCAgEBAICAYGAQGwCEAgIBAQCAgGBgEBAICAQEAggEBAICAQEAgIBgYBAQCAgEBAIIBAQCAgEBAICAYGAQEAgIBBAICAQEAgIBAQCAgGBgEBAICAQQCAgEBAICAQEAgIBgYBAQCCAQEAgIBAQCAgEBAICAYGAQEAggEBAICAQEAgIBAQCAgGBgEAAgYBAQCAgEBAICAQEAgIBgYBAgCgQAAAAAAAAAAAAAAAAAAAAAACAP+/cn95+OPT7b78uff9zVd763kb+T7v+ep7fTol/G3a7/fv/7H4uuP+/u64/Pn4qXvg+gR7P3Lj450vy2YuGmPtptagrrsUPt5//crj/ouWkfv+3/frBJqbTesz1SNHC9YMc3lf6NJRWdLGtig+s/qhzG7lhfUm7b9HFP02//mXc1QL58cxPbYEMXbRgqgfy4zGcmwI5L6fFch1+rsTd5TFE4sF2x9qYHw9zjkLeZx/kfZDz7slAfgx/LPzYvHzU+Y1c+0my7k/2dfX62LUE8u+LUUsgw7xacm4J5N/fPDQEclsmG4b/WK+hPA+Xe7vMmO/jnFavEvtcIKfEz28N5PHAKo+6sJEb1pe0y/oV/fCY+Q2B3AspBnJILNo3BRL96q64Eot94WU1C6ZFMukHe66MmVgQPKrVg8zsQLYGcl+JylFsYSPXjn/Jv9tcb6x5/XyVAnlvq/QcHPMzv23eVAIJXtOvwdy8xLuEOf+bxn15zPy6HNOBZHYgmwN5f2DlR13ayA3rS9q0fh86PuZ9UyCnaiCX5LJr27wZh1ogQ6LXj38bou5vhTXpy2P+1LXv5nI7kM2BvD87xYXFjVz+SRpOY3WrqbFvDeTX9CkEMizf0g+H7NNTmLbFQK6pWT5Hc3tYvthnftNYGTNel/0hOJSJH2RuB7I9kHFXfdTFjVxfXzKG1fvQ4+K4Ox3Ir/92uAXvQgpTeFruCx5HdXNu3vwavx8X/10xkCl41i/hseNtfW4uHu58Wu/UkmOu1+W23ElEDzK7AymsTfGBFRcWN3J9fck5xYci58SxeiqQx5NwqgRyi05pjfGvTAbyOKgeKoH04fnLcBcwhw/ikpqh+/Xb9OSYiXV5Sxy4dcHB2Gn3dCDRAysuLG7k+vqS08UvJsuZVA4kOM4vTOH4TNmcef2Kx1/8d8VA7hc4wlNOUfnzckrEw63PeaXHzK5LYmsd6+ezq4GED6y4sLiR6+tLzi1+H3rJPiXxBP54EvblQOIGL8G5n3wgh8d/Vwzk18MY+/DUZrhi1yCXeLj1ubz0mNl1Sexv8zuQDYH0pUCChcWNXF9fms/zztmd+iqQbvEkNASyX0/8pkDOlUDe5+gUph6en+uDY6Xc+aa+MmZ2XdZHmYUdyIZAzqW3ecHC4kYu/iRFx3hb5XfNLxrI+87hco1m+bgc7ZK85JYNJDdmeyCFHYhAvpN99D50n39z96KBTPc5HD3rl2XqY3AAUg0kN2ZzIKUdiEC+lTE8J37Mnw590UD6+391ClNfzvprOHI1kNyYzYGUdiAC+Vbm8GxPn782/KKBHO47h0uY+rC4iNGHF0Wq17xzY7YGsi+eR90eSF9d+FQg3qQ3iKbrLXUF9qUDme+B36LfMz8m9yV8r507q3OsjtkYyKG0AxHItzyNdV6f5f0egYz3DProlFP/eBMyhvuBeLguuApdGrPL3ou8HHVfnoAC+U6mcGOdUrfovnIg+48lx+hpf5yCmqI7RqLhprf08sSYbYEc4uA+ex3kWF24KZA+8wyQP0t6nx3Bdv4OgUwf7zeH+C7I033NbtE7gmC4/e0tfkD5MZsCqexANgQS7NqKCzcF0pUDJnGd7f1EzTV4U/oagZSvpPePyThG/9HtPg/m6OavfeXu7/yYTYFUdiDtgUzB2abiwi2BTE5ibXFaNjEVrjg3BZL4NO6zgbTdi3V4HBPO0Smn+0Q4x7ccFj88XByzJZBT7Qh/X/1cZrRru9YXtgcS/yQVl+Wm7YPjrS8NpPFu3u7xZB/i24THYMb2pRk6npvGbAmkOv/qgSR3bcWFTYH4vNQzbstNeSjckvF3A2n9PMj4uN929d7zkpmwqQlzG1rG3BLI7TcFcmjZ720PxAFWo9TZwukrA9n0icJh8ZimzAXx1UWJ5G8arw1jbglk/D2BnEuP+rx7NhCfBWl1XL7gle5Z+ppAKp9JPy6OCa/xS/eQeUGvfIlBYcwtgWTn4KZAxnPTgeHWQEaBtFrekjGER9yvEEjlW02WN5EMq0/IzulvXsjsq7r6mJsC6X7PIVa3a1i4/RCrM/UbLe4LL97U9yWB1L4X67Y8Alp9TUIf7YmS6zV1b8nbbVJjbgrkbf973qRfGhY+8Sb9Yuq3mR+fKz2Hm+5rroMEfdS+WbFbjtXFp7Gu6emQ+wjqoTrmtkBuz14HiRzrC58IxIX0RofHWZ7iXa9/P5CG7+Ydl2PfVuerxsRX96yH24dvqwtjbgtk/NSFwuMljLu4sP06SPyT1CxuyAvP8n5tIKeWb3cfgpnYr94dP75idyjN0OCe/9KYbYGMh99zu3v/lv90TrRwy5X0vnaajcDiTOYcTt2vvdWkZUpdg4vk6++JnNbf3JMaLviKg9KYbYH01+Ib4fZ7scbSd5MECzfdizW6lL7F9XEEPoavtq8fyDn5NvZx7PDx/VB9cbjD8kxXacyWz4P8OB8wl96mtwdS+mK6cOGmQG6uhWzysfmGt9LnJl4xkFs4/dcjdKlflg3kXBuzKZBz5Yt1/uTX/my63d3nQTadxjrdr5DN3yeQ9Fc2v30qkNKYLYGcFpdPxs8F4jPpL+E+I4ZztN1eP5BTejJfPxNIaczmr/05rC9PPhHIsRTI8elAjgLZ5ONMZnRw8fqBDOm5vJiW2wMpjrn5i+M6gXx/9z3uFH23wesHcn3L3+D4bCDFMZsD+dgP7QXynzmN1c/RAcrLBzJlJvPlE4EUx2wPpPBGWCDfzP2g4hC/xf3aL69umFJ9ZjLPGwNZfjd6cczMupwattbfCaTpy6sFstH92xXjp/Rr//xBw5TKnHBaNN4WyPLKWXHMOO5u+VnWcNQu+zb9TwSy6c8fCGSjLnN88qV/QKdlSs3xBa/Vle6mQKZl6sUxo3W5l5WawOfsHU9/IpAtf0BHIFsdMvegfuWfYGuaUqt90ersaksgfXAUVRzzvi6nYf1GI3dDx/5vBLLhT7AJZLM+80G4r/kjnoVAwrsZoz+Lk3xrXAwk9XDLY4brEn4/SO6Gjr5pbZIfLW4PpOmPeArkSVPmYwJf82eg2wOZVperV1/5tCWQn6++lTG7+iboopODp78RSNOfgRbIk8JnbNgWyFQN5Nj2ic/tgfSroeK/3bkpkH6xv8iNecyvZvYm+ulvBFLayAL5rMynfFoCue2qgUTvcXIX0LYHclgfrMUrsSGQy/KxZse85HYg60D6zNv07YF0u9rC4kau/CRVc/qlvSGQw64hkGFu+daZ7YHM64P8+AJ2eyCXYFNkxxxOuYPFVSC583XtgQQ7r+LC4kau/CRVl8SUbwnkNO1aAtkNXfZmqc8EMq4Hi68HtAbysSbVMffRRByP2ZNTh/Tb9PZAipeahuiYOL+Raz/JltNYfWsgXX9sedLff8O4fLEufo6oOZBr4mgtPnXUFMhiTVrGvAW70CF/9vYYnhTeHshHm+fawuJGrv8kX+/c/zo4OfT77/+Ccv92i95GBgAAAAAAAAAAAAAAAAAAAAAAAOD/yj9AlkBAICAQEAgIBAQCAgGBgEBAIIBAQCAgEBAICAQEAgIBgYBAQCCAQEAgIBAQCAgEBAICAYGAQACBgEBAICAQEAgIBAQCAgGBgEAAgYBAQCAgEBAICAQEAgIBgQACAYGAQEAgIBAQCAgEBAICAYEAAgGBgEBAICAQEAgIBAQCAgEEAgIBgYBAQCAgEBAICAQEAgIBBAICAYGAQEAgIBAQCAgEBAIIBAQCAgGBgEBAICAQEAgIBAQCCAQEAgIBgYBAQCAgEBAICAQEAggEBAICAYGAQEAgIBAQCAgEEAgIBAQCAgGBgEBAICAQEAgIBBAICAQEAgIBgYBAQCAgEBAIIBAQCAgEBAICAYGAQEAgIBAQCCAQEAgIBAQCAgGBgEBAICAQQCAgEBAICAQEAgIBgYBAQCAgEEAgIBAQCAgEBAICAYGAQEAggEBAICAQEAgIBAQCAgGBgEBAIIBAQCAgEBAICAQEAgIBgYBAQCCAQEAgIBAQCAgEBAICAYGAQACBgEBAICAQEAgIBAQCAgGBgEAAgYBAQCAgEBAICAQEAgIBgQACAYGAQEAgIBAQCAgEBAICAYEAAgGBgEBAICAQEAgIBAQCAgEEAgIBgYBAQCAgEBAICAQEAgIBIv8Djt0c3tPuuskAAAAASUVORK5CYII=","hlsJsConfig":"{\"startPosition\":0,\"liveDurationInfinity\":false,\"progressive\":false}","autoplay":"true","unload":"false","threshold":0.1,"controls":"true","muted":"true","players":["hls.js","hls","socket.io","mp4"],"x":950,"y":220,"wires":[["9ec4f3ad80a3ec74"]]},{"id":"9ec4f3ad80a3ec74","type":"ui_template","z":"0528fb314ac39109","group":"9a076144e0b8c687","name":"video controls","order":15,"width":"20","height":"1","format":"<div layout-fill layout=\"row\">\n\n    <div layout-fill>\n        <md-button layout-fill class=\"md-raised\" ng-click=\"toggleControls()\">\n            <md-icon class=\"material-icons\">smart_button</md-icon>\n        </md-button>\n    </div>\n\n    <div layout-fill>\n        <md-button layout-fill class=\"md-raised\" ng-click=\"playPause()\">\n            <md-icon class=\"material-icons\">play_arrow</md-icon>\n        </md-button>\n    </div>\n\n    <div layout-fill>\n        <md-button layout-fill class=\"md-raised\" ng-click=\"forwardFive()\">\n            <md-icon class=\"material-icons\">forward_5</md-icon>\n        </md-button>\n    </div>\n\n    <div layout-fill>\n        <md-button layout-fill class=\"md-raised\" ng-click=\"replayFive()\">\n            <md-icon class=\"material-icons\">replay_5</md-icon>\n        </md-button>\n    </div>\n\n    <div layout-fill>\n        <md-button layout-fill class=\"md-raised\" ng-click=\"decreaseRate()\">\n            <md-icon class=\"material-icons\">slow_motion_video</md-icon>\n        </md-button>\n    </div>\n\n    <div layout-fill>\n        <md-button layout-fill class=\"md-raised\" ng-click=\"increaseRate()\">\n            <md-icon class=\"material-icons\">speed</md-icon>\n        </md-button>\n    </div>\n\n    <div layout-fill>\n        <md-button layout-fill class=\"md-raised\" ng-click=\"toggleMuted()\">\n            <md-icon class=\"material-icons\">volume_mute</md-icon>\n        </md-button>\n    </div>\n\n    <div layout-fill>\n        <md-button layout-fill class=\"md-raised\" ng-click=\"videoFullscreen()\">\n            <md-icon class=\"material-icons\">fullscreen</md-icon>\n        </md-button>\n    </div>\n\n</div>\n\n<script>\n    ((scope) => {\n\n    const unwatchMsg = scope.$watch('msg', (msg) => {\n\n        if (msg && msg.videoId) {\n\n            const video = document.getElementById(msg.videoId);\n            \n            if (video) {\n\n                const rates = [ 0.1, 0.2, 0.5, 1, 2, 5, 10];\n\n                let rateIndex = 3;\n\n                const requestFullscreen = video.webkitRequestFullscreen || video.webkitEnterFullScreen || video.mozRequestFullScreen || video.requestFullscreen;\n\n                scope.toggleControls = () => {\n\n                    video.controls = !video.controls;\n\n                }\n\n                scope.playPause = () => {\n\n                    video.paused ? video.play() : video.pause();\n\n                }\n\n                scope.toggleMuted = () => {\n\n                    video.muted = !video.muted;\n\n                }\n\n                scope.videoFullscreen = () => {\n\n                    requestFullscreen.call(video);\n\n                }\n\n                scope.videoDisplay = () => {\n\n                    video.style.display = video.style.display === 'none' ? 'initial' : 'none';\n\n                }\n\n                scope.increaseRate = () => {\n\n                    rateIndex++;\n\n                    if (rateIndex > 6) {\n\n                        rateIndex = 6;\n\n                    }\n\n                    video.playbackRate = rates[rateIndex];\n\n                }\n\n                scope.decreaseRate = () => {\n                \n                    rateIndex--;\n                \n                    if (rateIndex < 0) {\n\n                        rateIndex = 0;\n                    \n                    }\n                \n                    video.playbackRate = rates[rateIndex];\n                \n                }\n\n                scope.forwardFive = () => {\n                \n                    video.currentTime += 5;\n                \n                }\n\n                scope.replayFive = () => {\n                \n                    video.currentTime -= 5;\n                \n                }\n\n                unwatchMsg();\n\n            }\n\n        }\n\n    });\n\n})(scope);\n</script>","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":1140,"y":260,"wires":[[]]},{"id":"9a076144e0b8c687","type":"ui_group","name":"shared","tab":"e671ec4ca9656cab","order":1,"disp":true,"width":"20","collapse":false,"className":""},{"id":"e671ec4ca9656cab","type":"ui_tab","name":"shared","icon":"dashboard","disabled":false,"hidden":false}]

This requires 3 of my unpublished experimental nodes that are available from github by using the commands:

npm install kevinGodell/node-red-contrib-ffmpeg-spawn
npm install kevinGodell/node-red-contrib-mp4frag
npm install kevinGodell/node-red-contrib-ui-mp4frag

For those of you already using these nodes, you will have to update them because of some small changes I made recently to be compatible with this flow.

*note: FFmpeg is not included with these nodes.

6 Likes

Kevin,
So nice to hear from you again! I feared that you had vaporized in our community :wink:

Since I have very few free time lately, I have asked a collegue of mine (who is using your nodes at home) if he can test this.

Hopefully some other community members can find some time to test your new developments :pray:

Thanks for your hard work!!!!!!!!!
Bart

2 Likes

Kevin,

I've following settings in NR:

httpStatic: '/mntlarge/',
httpStaticRoot: '/mntlarge/',

The static location is een mounted SSD drive . When running your flow I see the recordings in

image

but I don't see any output on ui-mp4frag

My settings in the recording are

the msg.payload is correct
payload: "/mntlarge/Camera/Ftp/test/58b77ee9a2e29092/2022/11/14/11h.11m.07s.m3u8"

The staticRoot is a new setting in NR, I presume. I've used the httpStatic for quite a while, cause all my recordings of my cams are stored in subfolders under the mntlarge dir.

EDIT: even when i change the msg.payload to the correct full path location, nothing plays in the ui - when I trigger it directly in the brower path, the file is correctly downloaded thus NR can access it

Maybe I don't have hls.js support? How can this be checked? Any idea?

Many thanks

I can give a better answer later, as I am about to run out the door in a couple minutes.

For comparison, I have static path set as /run/media/system/surveillance1/cctv/recordings and static root set as /static/recordings, which makes my playlist available at http://xxxxxxxxxxxx:1880/static/recordings/front_corner_main/2022/11/14/05h.30m.00s.m3u8.

I think maybe your static path setting might need to be changed.

Can you place a debug node after the recording node to see what the playlist is in the output?

Also, have you updated the existing nodes? I fixed something that helped the hls.js lib load if you change the standard paths of node-red.

I updated the 3 nodes en restarted Nr

Have already changed the recording nodes multiple time to match to path

the output of the recording is
https://XXXX:XX/Camera/Ftp/test/58b77ee9a2e29092/2022/11/14/13h.53m.53s.m3u8

Path is correct because when entering it directly in the browser the m3u8 is downloaded

What does my node ui_mp4frag takes as input? Does it use the httpStatic settings or does it just use the path output of the msg.payload of the recording node?

PS It works now!!! Now going to test it :slight_smile:

1 Like

It should use the url playlist output from the recording node, but that will only work if the path and root are correct. The STATIC_PATH is to tell it where to save the files, and the STATIC_ROOT is to tell it where you are serving the files for http. The recording node builds the playlist url based on your settings.

Perhaps some confusion is because I might be using a different version of node-red than you. The settings seemed new to me from my old version to the current. There is a little description in the settings.js file:

All static routes will be appended to httpStaticRoot
e.g. if httpStatic = "/home/nol/docs" and  httpStaticRoot = "/static/"
     then "/home/nol/docs" will be served at "/static/"
e.g. if httpStatic = [{path: '/home/nol/pics/', root: "/img/"}]
     and httpStaticRoot = "/static/"
     then "/home/nol/pics/" will be served at "/static/img/"

I hope to hear good news and maybe we can figure out what you did to fix things.

1 Like

When starting the recording only the first frame is showed on the dashboard but the clip is not playing the rest of the clip.

In the network google console I see the output coming
#EXTM3U
#EXT-X-VERSION:7
#EXT-X-PLAYLIST-TYPE:EVENT
#EXT-X-TARGETDURATION:1
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-MAP:URI="11h.28m.49s.mp4",BYTERANGE="1298@0"
#EXTINF:0.3333333333333333
#EXT-X-BYTERANGE:36683@1298
11h.28m.49s.mp4
#EXTINF:0.3333333333333333
#EXT-X-BYTERANGE:53243@37981
11h.28m.49s.mp4
#EXTINF:0.3333333333333333
#EXT-X-BYTERANGE:66925@91224
11h.28m.49s.mp4
#EXTINF:0.3333333333333333
#EXT-X-BYTERANGE:70516@158149
11h.28m.49s.mp4
#EXTINF:0.3333333333333333
#EXT-X-BYTERANGE:71510@228665
11h.28m.49s.mp4
#EXTINF:0.3333333333333333
#EXT-X-BYTERANGE:69806@300175
11h.28m.49s.mp4
#EXTINF:0.3333333333333333
#EXT-X-BYTERANGE:81588@369981
11h.28m.49s.mp4
#EXTINF:0.3333333333333333
#EXT-X-BYTERANGE:61233@451569
11h.28m.49s.mp4
#EXTINF:0.3333333333333333
#EXT-X-BYTERANGE:66393@512802
11h.28m.49s.mp4
#EXTINF:0.3333333333333333
#EXT-X-BYTERANGE:69045@579195
11h.28m.49s.mp4
#EXTINF:0.3333333333333333
#EXT-X-BYTERANGE:70404@648240
11h.28m.49s.mp4
#EXTINF:0.3333333333333333
#EXT-X-BYTERANGE:69785@718644
11h.28m.49s.mp4
#EXTINF:0.3333333333333333
#EXT-X-BYTERANGE:80054@788429
11h.28m.49s.mp4
#EXTINF:0.3333333333333333
#EXT-X-BYTERANGE:60290@868483
11h.28m.49s.mp4
#EXTINF:0.3333333333333333
#EXT-X-BYTERANGE:65677@928773
11h.28m.49s.mp4
#EXTINF:0.3333333333333333
#EXT-X-BYTERANGE:68476@994450
11h.28m.49s.mp4
#EXTINF:0.3333333333333333
#EXT-X-BYTERANGE:70253@1062926
11h.28m.49s.mp4
#EXTINF:0.3333333333333333
#EXT-X-BYTERANGE:70398@1133179
11h.28m.49s.mp4
#EXTINF:0.3333333333333333
#EXT-X-BYTERANGE:80010@1203577
11h.28m.49s.mp4
#EXTINF:0.3333333333333333
#EXT-X-BYTERANGE:64172@1283587
11h.28m.49s.mp4
#EXTINF:0.3333333333333333
#EXT-X-BYTERANGE:67849@1347759
11h.28m.49s.mp4
#EXTINF:0.3333333333333333
#EXT-X-BYTERANGE:69276@1415608
11h.28m.49s.mp4
#EXTINF:0.3333333333333333
#EXT-X-BYTERANGE:71042@1484884
11h.28m.49s.mp4
#EXTINF:0.3333333333333333
#EXT-X-BYTERANGE:69426@1555926
11h.28m.49s.mp4
#EXTINF:0.3333333333333333
#EXT-X-BYTERANGE:78355@1625352
11h.28m.49s.mp4
#EXTINF:0.3333333333333333
#EXT-X-BYTERANGE:69512@1703707
11h.28m.49s.mp4
#EXTINF:0.3333333333333333
#EXT-X-BYTERANGE:72166@1773219
11h.28m.49s.mp4
#EXTINF:0.3333333333333333
#EXT-X-BYTERANGE:74212@1845385
11h.28m.49s.mp4

What could be the cause the player isn't playing the clip but only shows the first frame?

When killing the ffmpeg spawn, the recording stays 'playing' the clip. Should it not also kill the recording?

The only error I see in the console is
Cannot GET /ui/loading.html

When reloading the page I see a couple of frames playing and then the clip stops playing

Are you using another version of de the NR dashboard than 3.2.0?

Is there any way you can share the complete flow with me? Of course, hide any private info. Also, are you taking advantage of the SECRET option in the ffmpeg node, which allows you to hide your ip camera source path which may have passwords in the url? That secret will not be shared when exporting the flow.


When in your browser, are you able to load the generate listing.txt file?

Currently, i do not have the recording node not output a blank playlist to clear the old, since we are not live streaming. I figured that since you are viewing a recording, there would be no need to make it empty. Also, when you do stop ffmpeg, there should be a final line written to the playlist

#EXTINF:1
#EXT-X-BYTERANGE:554736@28446428
05h.14m.03s.mp4
#EXT-X-ENDLIST

That gives the clue to hls.js or native hls in safari that the event is no longer live and can be treated as a vod, or something like that.

Screen Shot 2022-11-15 at 5.30.47 AM

Just had a thought as I was commuting. I remember trying to do this on my old node red instance and every time I tried to load the files in the browser, it would be empty. It seemed like the static hosting did not like my files or I had it configured wrong. I had abandoned that and started my work on the new pi and it just simply worked. I wonder if there was any file type restriction in the static hosting of the older versions of nodered?

Kevin,

With the arguments you showed in your reply the clip is working now for a part. So my setup could't clearly work with the ffmpeg parameters of the flows provided.

I copied your arguments, except the stimeout because then I couldn't start the ffmeg spawn, it's showing error [fatal] Error splitting the argument list: Option not found)

Problem is we're really depending how ffmpeg was installed. Will try to update ffmpeg so the stimeout is a valid parameter

The ffmpeg spawn is closed after a big minute with following ffmpeg parameters
image

"-loglevel",
"+level+fatal",
"-nostats",
"-rtsp_transport",
"tcp",
"-i",
"SECRET",
"-c:a",
"copy",
"-c:v",
"copy",
"-f",
"mp4",
"-movflags",
"+frag_keyframe+empty_moov+default_base_moof",
"-metadata",
"title=zij",
"pipe:1",
"-progress",
"pipe:3"

The listing shows indeed all the recordings made.

In case you need to troubleshoot the front end hls.js trying to play the video, you can add a little to the hls.js options section on the ui-mp4frag node's settings.

{
    "debug": true,
    "startPosition": 0,
    "liveDurationInfinity": false,
    "progressive": false
}

debug true will cause hls.js to post much info to the browsers console window that looks like:

startPosition 0 is not a required setting. If there is a live hls playlist, then this tells hls.js where to start playing. If you leave this out, then the video will play near live position. If 0, then it will play from the oldest video position.

1 Like