Jump to content

Attempting to deliver an hls stream using express, works in browser but wont play in HLS player - Javascript

Akonfan

I am trying to make an express server that takes rtsp(am using ffmpeg) converted to hls/m3u8 and streams it upon request(so it can later be viewed on a client).I am still learning and am not too familiar, so any and all help is appreciated - if you have any resources that could help me in this endeavor or if you have a better way of doing this I would appreciate it.

 

When going to localhost:PORT in a browser it shows the ts file information as I believe it should, but when using an hls.js demo player it gives the error - "Error while loading fragment A network error occurred: fragLoadError". Checking in the dev tools for the browser it shows GET requests to url localhost:PORT/cameras/cam1 as expected and the status comes back okay 200, but when it requests a ts file the url changes to localhost:PORT/cameras/123.ts and the status comes back Not Found 404.

 

HLS PLAYER OUTPUT:

Loading http://localhost:PORT/cameras/cam1

Loading manifest and attaching video element...

1 quality levels found

Manifest successfully loaded

Media element attached

Error while loading fragment http://localhost:4000/cameras/index133.ts

Error while loading fragment http://localhost:4000/cameras/index134.ts

Error while loading fragment http://localhost:4000/cameras/index135.ts

 

 

Here is my code, should start an express server on process.env.PORT, the /cameras routes are handles by cameras.js which should get an incoming request set the headers and read the index.m3u8 file which points to the location of ts files to stream video.

 

index.js

const express = require("express");
const exec = require("child_process").exec;
const fs = require("fs");
const app = express();
const cors = require("cors")
const cameras = require("./routes/cameras")
require("dotenv").config();

app.use(cors());
app.use("/cameras", cameras);

app.get("/", (req,res) =\> {
console.log("New Request Incoming...", new Date());
})

app.listen(process.env.PORT, ()=\> {
console.log("server online at port: ", process.env.PORT);
});
cameras.js
const express = require("express");
const router = express.Router();
const fs = require("fs");

const camOneStream = "./cameras/cam1/index.m3u8"

//Camera One Routes
router.get("/cam1", (req,res) => {

    console.log('Request Incoming for CAM1 from...', new Date());
    res.set('Access-Control-Allow-Origin', '*');
    res.set('Access-Control-Allow-Headers', 'Range');
    res.set('Access-Control-Expose-Headers', 'Content-Length, Content-Range');
    fs.readFile(camOneStream, (err, cont) => {
        if (err) {
            console.log(err);
            res.writeHead(500);
            res.end('Sorry, check with the site admin for error: ' + err.code + ' ..\n');
            res.end();
            
        }
        else {
            res.writeHead(200);
            res.end(cont, 'utf-8');
        }
    });
});
module.exports = router;


I was expecting the hls stream to be played on the hls player but the files could not be found resulting in fragment loading errors.

I have verified the hls files work. I have verified all the file paths. I included CORS headers manually. I removed firewall. I am not getting any errors on the server console. The hls files can be seen when going to the url in the browser, I believe all the .m3u8/.ts files are correct in pathing as they all can be played in vlc, CORS headers are included, so I am unsure of what I am doing wrong but I cannot get the url to play video in any hls player or vlc. Please Help.

Link to comment
Share on other sites

Link to post
Share on other sites

You haven't created an endpoint to serve the Transport Stream files, so you're getting a 404. The server does not know what to do with this "http://localhost:4000/cameras/index133.ts" URL. Your /cam1 endpoint will always return the index.m3u8 stream, as you've hard-coded it to do that. You will still need to configure an endpoint capable of serving TS files.

 

Is there a way to configure the HLS client to change the directory it is requesting the files from? I'd need to see the client-side code to help you with that. I am also assuming you have configured the server to take the RTSP stream and convert it to TS files on the fly.

 

You need to create an in your cameras.js file to serve the TS files. It would look something like this:

const outputDirectory = './cameras'


router.get('/stream/:segment.ts', (req, res) => {
	const segmentName = req.params.segment;
	const segmentFilepath = `${outputDirectory}/${segmentName}`;

	if (fs.existsSync(segmentFilepath)) {
		res.sendFile(segmentFilepath);
	}

	res.sendStatus(404);
});

This code shouldn't work. It will need some refactoring to work with your exact setup.

 

I would also be against manually coding a route for each camera. Look into how Route parameters work in the Express documentation.

Link to comment
Share on other sites

Link to post
Share on other sites

Thank you for your help, I appreciate all your suggestions, I will be looking into route params and will implement your code.

 

To my understanding the .m3u8 file should tell the client where to find each .ts file in the directory and they are fetched as the client progresses through the video so only the endpoint for the .m3u8 file is needed - is this not the case?

 

The url the HLS client shows when fetching the .ts file does not match the file directory of where the ts files are, "http://localhost:4000/cameras/index133.ts" VS. "http://localhost:4000/cameras/cam/index133.ts", could that be why the 404 not found error comes in?

 

I am using ffmpeg to convert the rtsp to hls/ts on the fly currently just in CLI but will put in server.

 

Again thank you.

Link to comment
Share on other sites

Link to post
Share on other sites

2 hours ago, Ominous said:

You haven't created an endpoint to serve the Transport Stream files, so you're getting a 404. The server does not know what to do with this "http://localhost:4000/cameras/index133.ts" URL. Your /cam1 endpoint will always return the index.m3u8 stream, as you've hard-coded it to do that. You will still need to configure an endpoint capable of serving TS files.

 

Is there a way to configure the HLS client to change the directory it is requesting the files from? I'd need to see the client-side code to help you with that. I am also assuming you have configured the server to take the RTSP stream and convert it to TS files on the fly.

 

You need to create an in your cameras.js file to serve the TS files. It would look something like this:

const outputDirectory = './cameras'


router.get('/stream/:segment.ts', (req, res) => {
	const segmentName = req.params.segment;
	const segmentFilepath = `${outputDirectory}/${segmentName}`;

	if (fs.existsSync(segmentFilepath)) {
		res.sendFile(segmentFilepath);
	}

	res.sendStatus(404);
});

This code shouldn't work. It will need some refactoring to work with your exact setup.

 

I would also be against manually coding a route for each camera. Look into how Route parameters work in the Express documentation.

Thank you for your help, I appreciate all your suggestions, I will be looking into route params and will implement your code.

 

To my understanding the .m3u8 file should tell the client where to find each .ts file in the directory and they are fetched as the client progresses through the video so only the endpoint for the .m3u8 file is needed - is this not the case?

 

The url the HLS client shows when fetching the .ts file does not match the file directory of where the ts files are, "http://localhost:4000/cameras/index133.ts" VS. "http://localhost:4000/cameras/cam/index133.ts", could that be why the 404 not found error comes in?

 

I am using ffmpeg to convert the rtsp to hls/ts on the fly currently just in CLI but will put in server.

 

Again thank you.

Link to comment
Share on other sites

Link to post
Share on other sites

44 minutes ago, Akonfan said:

To my understanding the .m3u8 file should tell the client where to find each .ts file in the directory and they are fetched as the client progresses through the video so only the endpoint for the .m3u8 file is needed - is this not the case?

The m3u8 file is a playlist file that tells the client what files exist and what it should request. Herein lies the problem. Your routes are a menu of what someone can request from a server. When a request hits the express server, it looks at the URL and tries to match it against registered routes. As an example, you've shown two routes:

  • /
  • /cam1

When the client requests "/cameras/index133.ts", it tries to match that to one of the routes above. There is no match, so the server can't handle the request. It does not know what you want. The default response then is an HTTP 404 status. This is most likely where the 404 is coming from, and I can't see why correcting the URL would fix that unless something is implicitly running in the background that would allow the server to respond with .ts files.

 

I think my solution is susceptible to directory traversal, so you would need to sanitise the user input from the query parameters (or someone could request files that you don't intend to be available). Instead of that, you could configure the express server to host your output folder as a static directory.

 

Do you intend to deploy your finish projected? Generally, static file serving like this is usually accomplished using a web server such as NGINX or Apache but it's not necessarily required. I also want to mention that someone has already created an HLS server module for Node if you want to take a look at that.

 

Link to comment
Share on other sites

Link to post
Share on other sites

6 hours ago, Ominous said:

The m3u8 file is a playlist file that tells the client what files exist and what it should request. Herein lies the problem. Your routes are a menu of what someone can request from a server. When a request hits the express server, it looks at the URL and tries to match it against registered routes. As an example, you've shown two routes:

  • /
  • /cam1

When the client requests "/cameras/index133.ts", it tries to match that to one of the routes above. There is no match, so the server can't handle the request. It does not know what you want. The default response then is an HTTP 404 status. This is most likely where the 404 is coming from, and I can't see why correcting the URL would fix that unless something is implicitly running in the background that would allow the server to respond with .ts files.

 

I think my solution is susceptible to directory traversal, so you would need to sanitise the user input from the query parameters (or someone could request files that you don't intend to be available). Instead of that, you could configure the express server to host your output folder as a static directory.

 

Do you intend to deploy your finish projected? Generally, static file serving like this is usually accomplished using a web server such as NGINX or Apache but it's not necessarily required. I also want to mention that someone has already created an HLS server module for Node if you want to take a look at that.

 

Thank you for your help I have refactored the code it is now not hard coded and when going to the specific url so multiple cameras work for this route(saved me a lot of work lol), I have gotten it to stream video in the player but that is by accepting any URL request that starts with "/cameras/:cameraID", if the url ends in "index.m3u8", the "req.params[0]" in the filePath reference the proper file path and the stream can be played. I am not sure why it works because it seems like its referencing the same index.m3u8 file but it works.

router.get("/:cameraId/*", (req,res) => {
    const cameraId = req.params.cameraId;
    console.log(`Request starting for camera: ${cameraId}`)

    let filePath = path.join(__dirname, '..', 'cameras', cameraId, req.params[0]);
    fs.readFile(filePath, (err, cont) => {
        if (err) {
            if (err.code == 'ENOENT') {
                fs.readFile('./404.html', (err, cont) => {
                    res.status(404).send(cont);
                });
            } else {
                res.status(500).send('Sorry, check with the site admin for error: ' + err.code + ' ..\n');
            }
        } else {
            res.send(cont);
        }
    });
});
Link to comment
Share on other sites

Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×