[beta testing] nodes for live streaming mp4

When outputting and saving the mp4 buffer directly, there will be some limitations. It has to be a multiple of the segments individual duration. Since they are consistently 3 seconds, it would have to be 3, 6, 9, 12, 15, etc. The preBuffer will be atleast 1 segment, which will be the most current segment. So, the preBuffer will give 3 seconds and then if the timeLimit is set to 10000, it will output 12 more seconds of segments, since it is designed to be generous and give >= the set amount. Should be a total of 15, but sometimes it is 12 depending on when it was started.

If you pass the mp4 segments thought another ffmpeg-spawn, then you can have more control of the duration and get closer to the desired time.

[{"id":"23160170.77800e","type":"subflow","name":"top","info":"","category":"","in":[{"x":100,"y":100,"wires":[{"id":"98a41609.d8efc8"}]}],"out":[{"x":620,"y":100,"wires":[{"id":"bc97fbbe.cb9318","port":1}]},{"x":620,"y":160,"wires":[{"id":"bc97fbbe.cb9318","port":2}]}],"env":[],"color":"#DDAA99","status":{"x":620,"y":40,"wires":[{"id":"bc97fbbe.cb9318","port":0}]}},{"id":"e4f2412d.1b609","type":"exec","z":"23160170.77800e","command":"top -b -d 5 -p","addpay":true,"append":"","useSpawn":"true","timer":"","oldrc":false,"name":"","x":329,"y":101,"wires":[["bc97fbbe.cb9318"],[],[]]},{"id":"bc97fbbe.cb9318","type":"function","z":"23160170.77800e","name":"","func":"const { payload } = msg;\n\nif (typeof payload === 'string') {\n    \n    //node.warn(payload);\n    \n    const lines = payload.toString().trim().split(/[\\n]+/);\n    \n    if (lines.length >= 2) {\n        \n        const labels = lines[lines.length - 2].trim().split(/[\\s]+/);\n\n        const values = lines[lines.length - 1].trim().split(/[\\s]+/);\n        \n        const data = {};\n        \n        for (let i = 0; i < labels.length; ++i) {\n            data[labels[i]] = values[i] || '0.0';\n        }\n        \n        const cpu = data['%CPU'];\n        \n        const mem = data['%MEM'];\n        \n        const text = `%CPU ${cpu}, %MEM ${mem}`;\n        \n        return node.send([{ payload: { fill: 'green', shape: 'dot', text } }, { payload: cpu }, { payload: mem } ]);\n\n    }\n\n}\n\nnode.send({ payload: {} });","outputs":3,"noerr":0,"initialize":"","finalize":"","x":495,"y":100,"wires":[[],[],[]],"l":false,"info":"# minimize top's output in batch mode\n\nrun top in interactive mode (dont use -b)\n\nopen column configuration menu (f) and toggle values\n\nquit (q) menu\n\nconfigure display using (l, t, m) to remove extra\n\nsave configuration (shift + w)\n\nquit (q) top\n\nrun top in batch mode (top -b) to see the changes"},{"id":"98a41609.d8efc8","type":"function","z":"23160170.77800e","name":"","func":"const { payload = {} } = msg;\n\nconst { status, pid } = payload;\n\nconst timeout = context.get('timeout');\n\nclearTimeout(timeout);\n\nif (status === 'spawn') {\n    \n    const timeout = setTimeout(() => node.send({ payload: pid }), 100);\n    \n    context.set('timeout', timeout);\n    \n} else if (status === 'close') {\n    \n    node.send({ payload: 'kill', kill: 'SIGTERM' });\n    \n}","outputs":1,"noerr":0,"initialize":"// Code added here will be run once\n// whenever the node is deployed.\n\ncontext.set('timeout', undefined);","finalize":"// Code added here will be run when the\n// node is being stopped or re-deployed.\n\nconst timeout = context.get('timeout');\n\nclearTimeout(timeout);\n\ncontext.set('timeout', undefined);","x":203,"y":101,"wires":[["e4f2412d.1b609"]],"l":false},{"id":"32d61e0f.0490b2","type":"subflow","name":"stderr","info":"","category":"","in":[{"x":60,"y":80,"wires":[{"id":"211d46bf.a79c7a"}]}],"out":[],"env":[],"color":"#DDAA99","status":{"x":320,"y":120,"wires":[{"id":"211d46bf.a79c7a","port":1}]}},{"id":"211d46bf.a79c7a","type":"function","z":"32d61e0f.0490b2","name":"stderr","func":"const stderr = msg.payload.toString().split('\\n');\n\nnode.send([{ stderr }, { payload: {fill: 'red', text: `${new Date().toString()}` } } ]);","outputs":2,"noerr":0,"initialize":"","finalize":"","x":190,"y":73,"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":370,"y":60,"wires":[]},{"id":"adcbab69.6989b8","type":"inject","z":"9bab7cb3.50207","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"true","payloadType":"bool","x":240,"y":220,"wires":[["f5c97ce9.c297e"]]},{"id":"f5c97ce9.c297e","type":"change","z":"9bab7cb3.50207","name":"Start recording","rules":[{"t":"set","p":"action","pt":"msg","to":"{\"subject\":\"write\",\"command\":\"start\",\"timeLimit\":20000,\"repeated\":false,\"preBuffer\":1}","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":430,"y":220,"wires":[["9efdfd05.fdf24"]]},{"id":"9efdfd05.fdf24","type":"mp4frag","z":"9bab7cb3.50207","name":"","hlsPlaylistSize":"5","hlsPlaylistExtra":"1","basePath":"id","repeated":"false","timeLimit":"-1","preBuffer":"1","autoStart":"false","x":810,"y":240,"wires":[["5f7995d9.b2d73c"],["5f7995d9.b2d73c","b73c092c.c6c6a8","eed0b3e5.4af53"]]},{"id":"5f7995d9.b2d73c","type":"debug","z":"9bab7cb3.50207","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1050,"y":140,"wires":[]},{"id":"b73c092c.c6c6a8","type":"function","z":"9bab7cb3.50207","name":"","func":"const { payload, action } = msg;\n\nif (Buffer.isBuffer(payload)) {\n    msg.filename = \"/home/pi/recording9-1.mp4\";\n    return msg;\n}\n\n","outputs":1,"noerr":0,"initialize":"","finalize":"","x":1060,"y":220,"wires":[["e508789a.412588"]]},{"id":"d5ae6fa2.1c2ac","type":"ffmpeg-spawn","z":"9bab7cb3.50207","name":"","outputs":2,"cmdPath":"ffmpeg","cmdArgs":"[]","cmdOutputs":1,"killSignal":"SIGTERM","x":590,"y":360,"wires":[["9efdfd05.fdf24"],["9efdfd05.fdf24"]]},{"id":"463998b8.7ed2a8","type":"change","z":"9bab7cb3.50207","name":"Stop recording","rules":[{"t":"set","p":"action","pt":"msg","to":"{\"subject\":\"write\",\"command\":\"stop\"}","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":430,"y":260,"wires":[["9efdfd05.fdf24"]]},{"id":"eed0b3e5.4af53","type":"ffmpeg-spawn","z":"9bab7cb3.50207","name":"","outputs":3,"cmdPath":"ffmpeg","cmdArgs":"[\"-f\",\"mp4\",\"-i\",\"pipe:0\",\"-f\",\"mp4\",\"-c\",\"copy\",\"-movflags\",\"+frag_keyframe+default_base_moof\",\"-t\",\"00:00:13\",\"pipe:1\"]","cmdOutputs":2,"killSignal":"SIGTERM","x":1080,"y":300,"wires":[["65199bc3.fd4614"],["574388cd.5a17c8"],["9b8326fc.2f1d58"]]},{"id":"e508789a.412588","type":"file","z":"9bab7cb3.50207","name":"","filename":"","appendNewline":false,"createDir":false,"overwriteFile":"false","encoding":"none","x":1270,"y":180,"wires":[[]]},{"id":"69076045.40a79","type":"inject","z":"9bab7cb3.50207","name":"RedBull","props":[{"p":"action","v":"{\"command\":\"start\",\"args\":[\"-loglevel\",\"error\",\"-nostats\",\"-f\",\"hls\",\"-http_multiple\",\"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\",\"-progress\",\"pipe:3\"]}","vt":"json"},{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":"5","topic":"","payload":"cam10","payloadType":"str","x":400,"y":340,"wires":[["d5ae6fa2.1c2ac"]]},{"id":"e8126ca9.8e679","type":"inject","z":"9bab7cb3.50207","name":"stop","props":[{"p":"action","v":"{\"command\":\"stop\"}","vt":"json"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":400,"y":380,"wires":[["d5ae6fa2.1c2ac"]]},{"id":"c40b6056.92fd6","type":"inject","z":"9bab7cb3.50207","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"true","payloadType":"bool","x":240,"y":260,"wires":[["463998b8.7ed2a8"]]},{"id":"574388cd.5a17c8","type":"function","z":"9bab7cb3.50207","name":"","func":"const { payload, action } = msg;\n\nif (Buffer.isBuffer(payload)) {\n    msg.filename = \"/home/pi/recording9-2.mp4\";\n    return msg;\n}\n\n","outputs":1,"noerr":0,"initialize":"","finalize":"","x":1280,"y":320,"wires":[["860a810b.3ac94"]]},{"id":"9b8326fc.2f1d58","type":"subflow:32d61e0f.0490b2","z":"9bab7cb3.50207","name":"","x":1270,"y":380,"wires":[]},{"id":"65199bc3.fd4614","type":"subflow:23160170.77800e","z":"9bab7cb3.50207","name":"","x":1270,"y":260,"wires":[[],[]]},{"id":"860a810b.3ac94","type":"file","z":"9bab7cb3.50207","name":"","filename":"","appendNewline":false,"createDir":false,"overwriteFile":"false","encoding":"none","x":1450,"y":320,"wires":[[]]}]

I will admit, at the time I made the timeLimit output, I had not yet learned how to read the individual durations of segments (previously estimated using timestamps). It might be an improvement if the timeLimit takes advantage of the calculated durations instead of estimates. Although the issue will still remain that it has to be generous because it is only capable of giving out complete segments, which may push the total amount of video slightly more than the settings.

1 Like