Media source extension video player for live streaming mp4

I don't want to change your style, necessarily. It's just that having a linter setup makes it easier for others to contribute code to your repo. They can make their experiments work. Then they can lint their code to match bart's preferred style . And then make the pull request so that their contributions don't affect the overall style and make it seem like they changed every line of code. For example, in my mp4frag node, you can run npm run lint to cleanup the mp4frag.js file. (still haven't found a good linter setup for the mp4frag.html file). But, this is not important right now. Although it may have sped up development a little.

1 Like

Indeed it may not be a priority but if we want to go to the end of the monitoring I think we should not remove the sound.
Having a "loudspeaker" to turn on or off somewhere would be nice: hearing a broken window or the sound of strangers talking in your garden. An extreme case: a spray paint on the camera: there is still the sound! That would reassure the Anguished who leave their house.
Off topic: for example: we could have a node which measures decibels, and above a threshold, output a payload to an alarm in Node Red, or make detection more efficient (avoid false positives?).

1 Like

All of those use cases don’t really require synchronised audio. So it could be handled via a separate parallel flow.

1 Like

No worries about removing sounds. The debate was about having it muted in the ui because of browsers restricting autoplay with sound. The audio track will not be removed, unless you pass the -an flag to ffmpeg.

I like the idea of audio as another means of detecting the baddies. I had the thought myself years ago but never acted on it because my cams have no audio. I think moe at shinobi implemented a decibel library to do this.

2 Likes

Some feedback about your node. Hopefully you don't interpret this as criticism, because I'm VERY impressed that you learned developing such a heavy node in such a short time!!!!!!!!

  • Most Node-RED nodes have a similar looking readme page (which sections like 'install', 'usage', 'node properties'...). It might be useful to do it that way.

  • About:
    image
    All nodes that I know have a "Name" field. This an optional field where you can type whatever you like, use even duplicate names or leave it empty.

    You need a unique name (since it used as xxx.m3u8 file name) in your http endpoint. If I were you, I would use the node id for that purpose (I have explained below how you can use it). It is generated automatically and it is unique. You can read and use it simply as node.id...

  • About the context dropdown:

    image

    Normally we use a TypedInput if you need to specify where data should be put/get. It offers all kind of possibilities, but in your case you only should offer 'flow' and 'global':

    image

    This is only informational, but I don't think you need it (see below).

  • It is not clear to my why you store your mp4frag instance on global/flow memory. I don't think this is necessary, and be aware that users can setup a persistent context store e.g. file based (as you can read here). You don't want your instance to survive a Node-RED restart, so you don't need this setup ...

    I think you can simplify it like this:

    1. Store the mp4frag instance inside your node (and pass the node id to it):
      var node = this;
      ...
      node.mp4frag = new Mp4Frag({ hlsBase: node.id, hlsListSize, hlsListInit: true });
      
    2. The node id will now become m3u8 filename (I assume)
    3. You send a message to my node containing this filename.
    4. My UI node sends a http request to access your m3u8 file
    5. So in the http endpoint you can see the m3u8 filename within therequest (see below the example), which means you know now the node id.
    6. In your http endpoint you can now get the node instance corresponding to that node id:
      var mp4fragNode = RED.nodes.getNode(nodeIdFromRequest);
      // Now you can get access to the mp4frag instance that you have set in step 1
      var mp4frag = mp4fragNode.mp4frag;
      ...
      
      So now you http endpoint (which is shared and used for all your nodes in the flow), now knows exactly from which of your nodes I want to access the m3u8 file ...
  • But I'm still not sure whether your "non-UI" node should setup the UI routes. Seems something that my UI node should do... WOULD BE NICE IF ANYBODY COULD GIVE US SOME ADVICE HERE!!

  • When an error occurs you show the error description in the node status label:

    this.status({ fill: 'red', shape: 'dot', text: err.toString() });
    

    Be aware that the length of the status label is limited. Don't know if that is an issue here? And be aware that there is a Status-node that can used by people to detect status changes on your node, and then trigger actions in their flow. I don't use it myself, but I 'think' they prefer labels like "error" or other fixed texts that they can interpret. Now they don't know which status labels will indicate an error. At least that is how I think they use it ...

  • About the 'dynamic' routes. I think this can be greatly simplified, but perhaps I'm not understanding the whole picture correctly! I would do it like this:

     // By default the UI path in the settings.js file will be in comment:
     //     //ui: { path: "ui" },
     // But as soon as the user has specified a custom UI path there, we will need to use that path:
     //     ui: { path: "mypath" },
     var uiPath = ((RED.settings.ui || {}).path) || 'ui';
     
     // Create the complete server-side path
     uiPath = '/' + uiPath + '/ui_mp4frag/:resource';
    
     // Replace a sequence of multiple slashes (e.g. // or ///) by a single one
     uiPath = uiPath.replace(/\/+/g, '/');
     
     // Make all the files from this node public available (i.e. m3u8 file), for the client-side dashboard widget.
     RED.httpNode.get(uiPath, function(req, res) {
         // As explained above, get the node id from the request
        var nodeId = ...
        
         var mp4fragNode = RED.nodes.getNode(nodeIdFromRequest);
         var mp4frag = mp4fragNode.mp4frag;
         
         if(req.params.resource.endsWith(".m3u8")) { 
             ...
         }
        else if(req.params.resource.endsWith(".m3u8.txt")) { 
             ...
         }
        else if(req.params.resource.endsWith(".mp4")) { 
             ...
         }
        else if(req.params.resource.endsWith(".m4s")) { 
             ...
         }     
         else {
             res.status(404).json('Unknown mp4 player resource requested');
         }
     });
    
    • The uiPath lines is stuff that I always add because users can customize the ui path via their settings.js file. And I had some troubles in the past, e.g. with Node-RED setups on cloud environments. Those lines have solved all problems for me...
    • The path should always contain something unique for your node, in this case e.g. ui_mp4frag
    • I use :resource which means that ExpressJs will route all requests to your endpoint, and the req.params.resource will automatically contain the content of that dynamic part of the url.
    • At the start of the endpoint you get access to the mp4frag of the specified node (id).
    • Based on the content of the req.params.resource, you trigger the required actions.
    • Perhaps it is easier if you use 4 regular expressions, to get access to the information immediately...

Keep up the good/hard work!!!

Thanks for the many tidbits of code and the in-depth critique.

Definitely will make a better readme. Was focusing more on making it viable, admittedly quick and dirty.

Originally I played with using the id, but i was unsure if it would use unsafe url characters. Plus, I keep the hlsBase name limited to letters and underscores. Trying to keep the paths as simple as possible to extract the segment number from the http request and then to grab it from mp4frag's buffer. Although, I may be able to use the id in the path and then keep the resource to use a simple generic name as playlist.m3u8, init-playlist.mp4, playlist123.m4s, etc. I was trying not to strain the backend express too much with complicated route patterns. This may be the solution.

Originally, this was my way to make the internal mp4frag instance accessible from the outside. I then used the built-in http in node and a function node to deliver the m3u8 content. Since learning how to add routes, that was a leftover feature that will actually be used in a future feature not related to video streaming. It will contain buffered past video of however many seconds as a source to record from when motion detection happens. Sometimes it is good to record a few seconds of video prior to the triggering event. Also, you will see in the "close" event that the context is removed and never persists.

It seems that the core "http in" node adds routes and it is not listed as a "ui" node. Also, it removes the route when closed. I tried to imitate its behavior. My code is more of a backend server that makes content for your ui video player. I would consider the video player and its contents to be ui. The video source and its routes is strictly backend and should be separate and not be tightly coupled to the front end. This will allow for other video players to consume the content.

Ok, I looked a little closer at how you add routes. You add it before an instance of your node is created. I add my routes after an instance of my node is created so that I have access to the unique values set by the user. Because my mp4frag thing will be a source for video beyond a node-red client ui, I have to keep my routes user configurable and not based on a randomly generated ids, but of course, I may wrong.

Thanks again.

Morning Kevin,
Damn had forgotten about that. When you have a look at the node id, you will see that there is always a dot in the middle:

image

When I use the node id in the url, that works fine (cross-browser) except for the dot. Because the dot is the only symbol that is not a number or a character. But I always solve that by replacing it by an underscore, both in the endpoint and in the location where I call the endpoint ...

Ah ok, that is indeed true. But keep in mind that the node.mp4frag can also be used everywhere. And it keeps your config screen simple and less confusing for users.

I have been reading the code of many nodes in the past, and allmost all of them do it like that. Indeed I would simply define the endpoint, and try to get the node id:

RED.httpNode.get(uiPath, function(req, res) {
     // As explained above, get the node id from the request
    var nodeId = ...

   // The dot in the node id has been replaced by an underscore, so change it back...
   nodeId = nodeId.replace("_", ".");

   var mp4fragNode = RED.nodes.getNode(nodeId);

   if (!mp4fragNode) {
      res.status(404).json('Unexisting node (with id " + nodeId + ").  Possible not deployed yet..."');
      return;
   }
   ...

You won't find the node, until it has been deployed. Suppose I user changes the info on the config screen of your node, your endpoint will keep using the old info (from the last deploy). As soon as he deploys his new info, this endpoint will start using the new info. Seems like a very Node-RED compliant mechanism to me.

Yes I fully understand that my UI node won't be the only consumer of your streams. I'm pretty sure that the e.g. the uibuilder users will start consuming your streams in the near future...

But that is one of the main reasons why I find it so awkward that your node uses RED.httpNode.get``in contradiction to the other non-ui nodes that use RED.httpAdmin.get`. Now it really feels that your node has been developed only to support my UI node. But here ends my knowledge, so hopefully somebody else can join the discussion.

But indeed when you don't want the generated node id in the path, you need a unique name on the config screen. But I would use an extra field - beside the optional "Name" field. But again it would be nice to get other opinions about this.

I have no idea what the advantage of that is. Again hopefully somebody else can explain here.

Would be nice if these questions were solved before you continue, just to make sure you don't have to redesign your node later on. Because your node is going to become a popular node, that I can tell you :shushing_face:

About popular nodes. We have to keep in mind that also novice users will like to use your nodes to show their camera streams on their dashboard. But they most probably will quit frustrated during the ffmpeg installation. Some time ago I had lots of troubles getting ffmpeg installed on my Raspberry (i.e. compilation errors...), and it was not clear to me how to install it with h264 or not, and other decisions I had to make.

So minimal there should be a clear description on your readme, of the steps they have to execute. Or a link to a tutorial ..

Due to the installation issues, I had registered this issue to have ARM support for the pre-build binaries. At the time being I had the idea to add that npm library as a dependency for my ffmpeg node, or create a separate node-red-contrib-ffmpeg-installer node. What is your opinion about that? P.S. I haven't followed that node-ffmpeg-installer project meanwhile, so don't know if the binaries keep up with the recent ffmpeg versions, or whether the Raspberry 4 architecture is supported, or ... But I hope you will get the idea.

Because we have to be aware that not all Node-RED users are highly technical skilled, and we should try to create nodes for all types of users. Which might not be easy due to the ffmpeg dependency...

Bart, Kevin, the endpoints should be quite simple. The httpadmin ones are protected by the admin auth mechanism, and are for people who should have access to the editor. The others are protected by the http auth and are for users of the flow. So a node that needs to get something from the back end during config would use the admin route, but once deployed and in use it would expose an http one for users to hit.

2 Likes

Thanks for following this discussion Dave! So then I have been confusing Kevin, and it is fine that he defines the endpoint. That case is closed then...

Do you know what the advantage is of removing the routes, like the http-in node? Seems quite some overhead to me, since the :resource parameter in the url allows Kevin to use 1 endpoint for all his nodes and routes. But most probably I have missed something...

Because you shouldn’t leave them exposed when you close down. Potential security hole. Ties up resources etc. Memory leak etc

2 Likes

Does this mean that other nodes can easily access the internal mp4frag instance if I attach it to this node? If that is the case, then they would probably need the randomly generated id as opposed to a simply named identifier to access it from flow or global context?

I definitely like the idea of using just 1 endpoint. That would make things simpler for me. Right now, I add 4 per node instance, really only needs 3 but I added the .m3u8.txt for debugging to make it easier to view the playlist in your browser by delivering it as plain text. I could probably reduce it to 1 per node instance if I use the unique name in the path instead of the file, such as /unique_name/:resource with all of the resource files simple being named playlist.m3u8, init-playlist.mp4, etc. So the public routes matched would be /front_porch_main/playlist.m3u8 or /back_door_sub/playlist.m3u8. But then I wonder if the additional if/else blocks and additional getter code overhead vs the overhead of having specific routes already matched to the content and having the mp4frag value encapsulated in the route handler. This would be very hard to test the performance gains or losses based on the routing structure.

I still am still planning on doing that once I have the node a little more built out. As a node-red novice, I found its layout and interface to not be very intuitive. I am also surprised that there seems to be no helpful information on the settings screen of a node. Hunting for help text while being a newbie was a bit tricky. I wanted to put more details on my settings screen but didn't want to break with the norm of being unhelpful. Trust me, the show help button is not very obvious to a new user. Is there a way to trigger the help screen to open by clicking on a little i symbol next to a settings option? Now that would really be helpful for new users. Also, I will add back the Name option to comply with the norm.

The ffmpeg thing will be tricky. Luckily for me on a pi 4, I ended up with newer version after updating on the command line. In the past, and in some of my travis/appveyor tests, I think I use an npm module that brings ffmpeg with it. Problem is that it also brings with many other ffmpegs for other systems too. On a small system, that may be a waste of precious space. Will have to look into it further. I was planning on building a wrapper around my ffmpeg lib that monitors its progress and can automagically respawn it. Sometimes a video feed source may freeze, but ffmpeg will stay open as it patiently waits for more video content to arrive. I monitor the progress with a timer and respawn the process x number of times until finally emitting a 'fail' event which can ultimately alert you that your video feed is down hard.

Yes indeed.

Well by using the id it is rather simple to get the corresponding node instance. But I understand thzt you would like to allow the user to enter a unique name. Since I don't want to be your party-stopper I think you can achieve that, since you can loop the nodes (as I have done here in the past. Something like this:

RED.nodes.eachNode(function(node) {
   If (node.uniqueName && node.uniqueName === someName) {
  // node with unique id found, so access node.mp4frag
}
})

Although it might (again) result in a lot of cpu usage, since you do such a large amoung of http requests ...

Perhaps you can create simply a global Javascript object mp4fragments, where you store - as soon is created - a reference to your node. So instead of only keeping the uniqueNames here, store the link uniqueName vs node reference. Then you only need to search in that object...

Yes. Perhaps @afelix has some experience with that...

Don't know that...

:+1:

Why not draw from this tutorial :

the creation of this contrib-ffmpeg-installer node?

As a silent follower of this topic, I would suggest to ignore the potential ffmpeg installation difficulties for now and focus on the core functionalities. :slightly_smiling_face:

I also remember giving up on some NVR software installation attempt due to how confusing it seemed but then later on found out that the current Raspbian/Raspberry OS versions come with a suitable ffmpeg version built in.

Yes that seems to make sense. But the problem was that I still need to install ffmpeg on my system (to test Kevin's node), and I don't have the time or energy to go through the same mess as last time ... So I thought: if I have to install it anyway, why not trying to automate the process (for most know platforms) via pre-build stuff. Of course if possible with minimal efforts ...

When I simply do this:

npm install --save @ffmpeg-installer/ffmpeg

Then less than a minute later, ffmpeg is up-and-running.
No headache... How cool is that...

However I think it is a rather old version (?), and I don't know whether it is compiled using the correct settings (?)

I forgot we're not taking about Raspberry Pi's here. I can imagine it could be painful especially to compile a custom version on Windows... :roll_eyes:

But if the prebuilt one installed from npm works, that is truly handy!

Right now I am trying to beat up node-red to see what it can handle on a pi 4. The current experiment is to test my suspicious routing configuration and tweak it. I have to put it under load to see if there can be a more efficient way to handle the constant http requests for the hls fragments. Ultimately, sockets should win the fight vs http, but that is a future implementation.

So far, the limitation that I see is on the client side hls.js library. Trying to stream 14 mostly 1080p videos seems too much for the browser to handle, while the node-red server side seems to run just fine at a fluctuating node-red cpu load of 10% to 30% based on htop. This was strictly stream copying the main channel from the ip cams and delivering via http requests. No jpegs stuff in this experiment.

Realistically, we should not try to stream multiple 1080p cams at the same time to a browser, but of course, we will. Personally, I would use the sub stream for regular viewing when there is a montage of cams, and only use the main stream when viewing a single cam or using as a source for recording. FYI, you can view some debug info on chrome @ chrome://media-internals/.

Also, I was able to get good audio on 2 of my 3 cams that support it. In the past versions of ffmpeg, the audio seemed to not work very good. I read that the aac encoder is no longer experimental, but am not 100% sure on that. 1 of my cams has audio already encoded as aac, so I am able to stream copy it. The other 2 cams audio is pcm_alaw, which I encode to aac. One of those encoded audio is a little low quality and I am seeing how ffmpeg can filter its crackly sound if possible.

2 Likes

Wow ... :+1:

That is pretty low, not?

Main stream: does this main high frame rate and high resolution?

That was just for the node-red process, excluding all the other processes. It's not that bad. We have to make some benchmarks.

Main stream is usually higher resolution and sub stream is lower res. They can each have their own independent frame rate. The higher resolution definitely puts a strain on the browser for decoding and playback.

I was playing with my old repo ffmpeg-streamer to see if it would work on the pi. If it used the default system ffmpeg, then it worked good. If I triggered it to use the ffmpeg using the npm lib ffbinaries, it does not work good. It is an older version of ffmpeg that had some tls issue and could no longer read https files after some websites (including github that was hosting my hls files) did some tls 1.2 update a couple years back. I can't remember the exact details, but not all of the pre-combiled binaries may be fully compliant. Lots of testing needed.

1 Like