MJPEG with proxy latency increases over time

Viewed 18

Previously I had ESP32CAM streaming its video as an MJPEG, this only allowed one viewer, therefore I used an MJPEG proxy on my computer and clients would be fed that stream instead of going directly to the ESP32.

I would have two cameras running at all times and max 2 viewers per stream. I was watching both and there would be another user watching per camera, so three separate clients (me, user1, user2).

Over time the latency would increase to 5+ seconds. The only way to resolve this issue was to completely restart the ESP32 and sometimes restart the node server proxying the requests.

I have since moved to RaspberryPi's as I wanted a little more computation power to stream the image and can't get them to go out of sync as of yet. But, I am not able to test with lots of different users/clients.

One of my thoughts was that the proxy was waiting for responses from all clients before sending the next frame. For E.g. user1 and user2 are viewing the stream, user2 has a really slow connection and is therefore loading frames very slowly, therefore the proxy would be delaying sending frames to user1 as well. But when throttling with chrome tools, this does not seem to be the issue as user1's stream remains to be fine, and user2's is slow/delayed.

The stream needs to be as minimal latency as possible.

This is the code I have used for the MJPEG proxy. Variation of mjpeg-proxy npm package

I am wondering if anyone can spot what the issue with the delay may be, or, how to try to reproduce this scenario. I understand I could use something like Gatling to performance test the site, but I am unsure if I can throttle connections/view mjpegs with Gatling.

#initialiseSourceRequest(req, res) {
        this.mjpegRequest = http.request(this.mjpegOptions, mjpegResponse => {
            // console.log('request');
            this.globalMjpegResponse = mjpegResponse;
            this.boundary = extractBoundary(mjpegResponse.headers['content-type']);

            let lastByte1 = null;
            let lastByte2 = null;

            mjpegResponse.on('data', chunk => {
                // Fix CRLF issue on iOS 6+: boundary should be preceded by CRLF.
                let buff = Buffer.from(chunk);
                if (lastByte1 != null && lastByte2 != null) {
                    let oldheader = '--' + this.boundary;

                    let p = buff.indexOf(oldheader);

                    if (p == 0 && !(lastByte2 == 0x0d && lastByte1 == 0x0a) || p > 1 && !(chunk[p - 2] == 0x0d && chunk[p - 1] == 0x0a)) {
                        let b1 = chunk.slice(0, p);
                        let b2 = Buffer.from('\r\n--' + this.boundary);
                        let b3 = chunk.slice(p + oldheader.length);
                        chunk = Buffer.concat([b1, b2, b3]);
                    }
                }

                lastByte1 = chunk[chunk.length - 1];
                lastByte2 = chunk[chunk.length - 2];

                for (const [clientId, res] of Object.entries(this.audienceResponses)) {
                    if (!MjpegProxy.canAccessStream(clientId)) {
                        this.audienceResponses[clientId].destroy();
                        delete this.audienceResponses[clientId]
                        continue;
                    }

                    // First time we push data... lets start at a boundary
                    if (clientId in this.newAudienceResponses) {
                        let p = buff.indexOf('--' + this.boundary);
                        if (p >= 0) {
                            res.write(chunk.slice(p));
                            delete this.newAudienceResponses[clientId]
                        }
                    } else {
                        res.write(chunk);
                    }
                }
            });
            mjpegResponse.on('end', () => {
                console.log("...end");
                for (const [_, res] of Object.entries(this.audienceResponses)) {
                    res.end();
                }
                this.emit("streamstop", "[MjpegProxy] 0 Users, Stopping stream " + this.mjpegUrl);

            });
            mjpegResponse.on('close', () => {
                // console.log("...close");
            });
        });

        this.mjpegRequest.on('error', e => {
            console.error('problem with request: ', e);
            this.emit("error", { msg: e, url: this.mjpegUrl });
        });

        this.mjpegRequest.on('timeout', () => {
            console.log("timeout")
        })

        this.mjpegRequest.on('close', () => {
            console.log("close")
        })

        this.mjpegRequest.end();
    }

TLDR

MJPEG stream latency gets worse over time, needs to be minimal latency.

0 Answers
Related