Cctv recording and playback (work in progress)

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

Hi @kevinGodell,

Finally I had enough technical background about this, via our mp4frag cheat sheet. Which allowed me to have recording running in a real short amount of time. You are my hero for today :star_struck:

Of course I only have a basic recording setup, which need to become more advanced in the near future. But at least I now have a starting point.

Thanks for all your hard labour!!!

2 Likes

Nice, it works fine! Both recording and viewing works smooth but I do not think I understood the necessary settings

Can we elaborate a bit about the httpStaticRoot and httpStatic?

In my settings.js I have since long only:

    httpStatic: '/var/www/html/pics/',

To this I have mounted an USB stick to avoid heavy writing of images and other files to my SD card. Now I added a subfolder 'recordings' to the /var/www/html/pics folder. I then configured the "recording" subflow to this:

When I start the streaming, recording begins and I find those files in a structure of subfolders below the 'recordings' subfolder.

The following url is sent to the ui_mp4frag node:

"/recordings/58b77ee9a2e29092/2023/02/24/14h.57m.30s.m3u8"

Viewing also starts and I can use the controls, all is working fine

But I cannot see previous recordings, there is no video file selection feature, should that be possible? Is there something wrong with my http settings in the settings.js that prevents that?

Best regards, Walter

I think at some point, node-red allowed more finer control of the static hosting. It didn't work for me on my old v1 node-red, but just fine on v3. From what I understand, the path is referring to the file path on disk, while the root is how the url shows up when hosting and serving it via http. This can be set to whatever you need to make it work on your system, as you have.

In my settings.js, i have the following settings (it could be more than 1 in the array):

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

httpStaticRoot: '/static/',

This has my files being served via http at mydomain/static/recordings/thevideo. The root allows you to change the http url path so that it does not have to be the same as the file path, maybe.

I matched that setting in my function node to use those paths set in the settings.js

That is not something built-in anywhere. I was starting to tinker with a template node to play back the recordings, but never got very far. I was waiting for somebody else to make it :grin:.

I will share my small effort and maybe you can expand on it. It makes use of the listing.txt file almost as a master playlist. The template fetches the list and loads the urls to the video sources. I think you can take this and make it better. Or you could make some flow that reads the listing file and then send an individual m3u8 playlist to ui_mp4rag or any other mp4/hlsjs nodes. Or maybe fetch the listing and use it to populate some html select element playlist picker and that will send the playlist to the ui node. I am not really sure how it should be. Nobody was using this, so there was no feedback that I could act upon. Also, there is always that lack of free time and now I am elbow deep in nginx trying to create the ultimate performance configuration for the cctv stuff.

[{"id":"a73b2f112ad47063","type":"http in","z":"e9f5eca971f24054","name":"","url":"/hlsjs/recordings/:i/:y/:m/:d","method":"get","upload":false,"swaggerDoc":"","x":230,"y":600,"wires":[["2944f4b7ec1f333c"]]},{"id":"2944f4b7ec1f333c","type":"template","z":"e9f5eca971f24054","name":"video","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"<!doctype html>\n<html>\n    <head>\n        <title>{{req.params.i}} {{req.params.y}} {{req.params.m}} {{req.params.d}}</title>\n        <style>\n            html {\n                height: 100%;\n                overflow: hidden;\n            }\n            body {\n                height: 100%;\n                margin: 0;\n            }\n            video {\n                width: 100%;\n                height: 100%;\n                object-fit: fill;\n            }\n        </style>\n        <style>\n    html {\n      height: 100vh;\n      overflow: hidden;\n    }\n    body {\n      height: 100vh;\n      margin: 0;\n    }\n    video {\n      width: 100vw;\n      height: 100vh;\n      object-fit: fill;\n      z-index: 0;\n    }\n\n    button#prev {\n      opacity: 0.4;\n      font-size: xxx-large;\n      position: absolute;\n      top: 50vh;\n      left: 1vw;\n      z-index: 1;\n    }\n\n    button#next {\n      opacity: 0.4;\n      font-size: xxx-large;\n      position: absolute;\n      top: 50vh;\n      right: 1vw;\n      z-index: 1;\n    }\n\n  </style>\n  <style>\n    a {\n      text-decoration: none;\n      display: inline-block;\n      padding: 8px 16px;\n    }\n\n    a:hover {\n      background-color: #ddd;\n      color: black;\n    }\n\n    .previous {\n      opacity: 0.3;\n      background-color: #f1f1f1;\n      color: black;\n      position: absolute;\n      top: 50vh;\n      left: 1vw;\n      z-index: 1;\n      font-size: x-large;\n    }\n\n    .next {\n      opacity: 0.3;\n      background-color: #f1f1f1;\n      color: black;\n      position: absolute;\n      top: 50vh;\n      right: 1vw;\n      z-index: 1;\n      font-size: x-large;\n    }\n\n    .round {\n      border-radius: 50%;\n    }\n  </style>\n        <script src=\"//cdn.jsdelivr.net/npm/hls.js@latest\"></script>\n    </head>\n    <body>\n<div id=\"container\">\n<video id=\"hls\" controls muted playsinline></video>\n<a href=\"#\" id=\"prev\" class=\"previous round\">&#8249;</a>\n<a href=\"#\" id=\"next\" class=\"next round\">&#8250;</a>\n</div>\n<script>\nconst dir = '/static/recordings/{{req.params.i}}/{{req.params.y}}/{{req.params.m}}/{{req.params.d}}';\nfetch(`${dir}/listing.txt`)\n  .then(res => {\n    return res.text()\n  }).then(text => {\n  console.log(text);\n  const arr = text.split('\\n').filter(n => n);\n  console.log(arr);\n  const length = arr.length;\n  const minIndex = 0;\n  const maxIndex = length - 1;\n  let currentIndex = maxIndex;\n  const video = document.getElementById('hls');\n  const prev = document.getElementById('prev');\n  const next = document.getElementById('next');\n  const videoSrc = arr[currentIndex];\n  const hlsOptions = { startPosition: 0, liveDurationInfinity: false, progressive: false, maxMaxBufferLength: 16 * 60, maxBufferSize: 300 * 1000 * 1000, backBufferLength: 16 * 60 };  \n  let hls;\n    \n  const loadHls = () => {\n    if (Hls.isSupported()) {\n      hls = new Hls(hlsOptions);\n      hls.attachMedia(video);\n      hls.loadSource(`${dir}/${arr[currentIndex]}`);\n      hls.on(Hls.Events.MANIFEST_PARSED, async (event, data) => {\n        console.log('manifest loaded, found ' + data.levels.length + ' quality level');\n        try {\n          await video.play();\n          video.playbackRate = 5.0;\n        } catch(e) {\n          console.log(e);\n          // noop\n        }\n      });\n    } else if (video.canPlayType('application/vnd.apple.mpegurl')) {\n      video.src = `${dir}/${arr[currentIndex]}`;\n      video.play();\n    }\n  }\n\n  next.addEventListener('click', () => {\n    ++currentIndex;\n    if (currentIndex > maxIndex) {\n      currentIndex = maxIndex;\n      return;\n    }\n    hls && hls.destroy();\n    loadHls();\n  });\n\n  prev.addEventListener('click', () => {\n    currentIndex--;\n    if (currentIndex < minIndex) {\n      currentIndex = minIndex;\n      return;\n    }\n    hls && hls.destroy();\n    loadHls();\n  });\n\n  loadHls();\n});\n</script>\n</body>\n</html>","output":"str","x":470,"y":600,"wires":[["78503259e2433081"]]},{"id":"78503259e2433081","type":"http response","z":"e9f5eca971f24054","name":"","statusCode":"","headers":{},"x":670,"y":600,"wires":[]}]

This allows me to view the recordings for a single camera for a single day by going to the url mywebsite/hlsjs/recordings/front_driveway_main/2023/02/24

Be warned, most of the values are hard coded and it will not work without modification.

1 Like

Great, thanks, understood!

When you set this setting, httpStaticRoot: '/static/', did you also physically create that folder 'static' in the root or is it just, well, something virtual?

Interesting with the listing.txt, I will for sure dive into, cool
Best regards, Walter

1 Like

Not ment to be perfect,,,,but technology is so far working really great!!!

2 Likes

Playing a bit further, I found that the logic in NR would be simpler with just one common file listing the recordings. So just experimented a bit, maintaining my own recordings.txt with all recordings with the complete path. SO for every new recording, it's appended to the file and a watch node that updates the dropdown. Still not the target to make it winning a beauty contest, just verifying the logic

3 Likes

I think that is only for the url. I do not have a static folder in the file system. It just makes it easier to differentiate the http path, which could help when using a reverse proxy and you can tweak the caching, amongst other things.

Yes, many ways it can be done. Originally, I had wanted to make a select element that I could pick the camera, then the day, then the individual recording. Never got around to doing it. Playing back old recordings never became a priority for me since I rarely ever have to look back. Not too much happening near my house.

p.s. When playing back recordings, you can try changing the playback speed so it can play in fast motion to quickly get through the recordings if they are long. video.playbackRate = 5.0; That would have to be manually programmed in the ui template video controls, but should work. It would also be nice if you could have it automatically load the next recording after the current recording finishes. You could probably add some event listener to detect when the video has ended. The possibilities are many. It could be complicated, but I know you can do it.

2 Likes

Great stuff, great ideas & concepts, really like it! The quality of the recordings and live viewing is really exceptional. Using the slider, video moves so smooth and direct, no delays what I can see. Now, I only have recorded the red bull video so I have no idea if regular ip cameras provides the same fine quality but I assume they do

You have provided such a good toolbox, I would say everything is now there to create wonderful ui's for video management of ip cameras (is just a pitty my current house is stuffed with usb devices, need to change to another house somehow)

Great work, thanks!!
Best regards, Walter

2 Likes

Just tried to check if I could add some extra to the hls.js config in the ui_mp4frag but it seems there are no such event available in hls.js api what I could find. Catching an event in the "video controls" ui template will not work (I think) since there is no msg sent from the ui_mp4frag node when the playlist is either stopped, started or finished.

Does the ui_mp4frag node know when the playlist is playing - paused - finished? If so, could the node not send messages when that happen?

1 Like

The node is unaware of what is happening on the client side. It simply receives the playlist and sends it to the client side where there is an attempt to play it.

If you wanted to handle the client side event on the video element, you can add the event listener to detect ended and send a message back using scope.send. If you added this little piece of code on the controls, you could detect the event and send a message and somehow load the next video from your list. Of course, this would make it change for all viewers since the dashboard is designed to be single user.

const video = document.getElementById(msg.videoId);

            if (video) {

                video.addEventListener('ended', () => {
                    scope.send('it ended');
                });

This successfully sent a message to my debug panel.

It would need more work so that to only detects the ended event when you want it to, maybe with some checkbox control. Just a proof of concept at this point...

1 Like

Works great, really useful. Understood, it is events from the HTMLMediaElement you have to watch. Many useful and interesting:

1 Like

In this situation, it might be better to set the onended property with the callback function vs adding the event listener. This will help to ensure that you have only added the callback a single time. And instead of removing the event listener, you can just delete the onended property (or maybe set to null or call removeAttribute, can't remember right now at the top of my head).

Well, both works fine, why is one better than the other?
Best regards, Walter

            if (video) {

                video.onended = function() {myEnd()};

                function myEnd() {
                    scope.send('the end');
                }

                video.addEventListener('ended', () => {
                scope.send('it ended');
                });

                video.addEventListener('playing', () => {
                scope.send('it plays');
                });

                video.addEventListener('pause', () => {
                scope.send('it paused');
                });

Assigning it as a property only allows 1 handler, whereas adding the event listener will allow more than 1 handler. If you are only need a single callback, which is most likely the case for this particular situation, then using the property might be simpler and you won't accidentally add too many event listeners. Other than that, I don't know if one is better or just a matter of preference.

1 Like

Is anybody here using nginx along with the cctv stuff? I have been deep diving nginx over the last month and found some interesting techniques to offload some burden from the node-red instance, in particular when playing back recordings and using nginx's slice module. Hopefully I can get a small change to node-red core to allow this to work. If anybody is interested in nginx integration with the cctv things, then maybe we can start a new thread for that. Let me know.

1 Like