I'm working with ffmpeg to process an incoming MPEGTS stream from remote cameras, and deliver it to multiple clients using my app.
Technically, I'm using ffmpeg to convert the incoming stream to an MJPEG output, and piping the data chunks (from the ffmpeg process stdout) to a writeable stream on the client http response.
However, I'm facing a problem- not all data chunks represent a full 'whole' frame. thus, displaying them in a row in the browser, results in a flickering video, with half-complete frames, on a random basis. I know this because when printing each chunk length, results most of the time in a big value (X), but every now and then I get 2 consecutive chunks with length (2/5X) followed by (3/5X) for example.
So the question - is there a way to force the ffmpeg process to output only whole frames? if not, is there a way for me to check each data chunk 'manually' and look for headers/metadata/flags to indicate frame start/end?
my ffmpeg command for outputting MJPEG is:
ffmpeg -i - -c:v mjpeg -f mjpeg -
explained:
"-i -" : (input) is the stdin of the process (and not a static file)
"-c:v mjpeg" : using the mjpeg codec
"-f mjpeg" : output will be in the mjpeg format
"-" : output not specified (file or url) - will be the process stdout
Edit: here are some console.log prints to visualize the problem:
%%% FFMPEG Info %%%
frame= 832 fps= 39 q=24.8 q=29.0 size= 49399kB time=00:00:27.76 bitrate=14577.1kbits/s speed=1.29x
data.length: 60376
data.length: 60411
data.length: 60465
data.length: 32768
data.length: 27688
data.length: 32768
data.length: 27689
data.length: 60495
data.length: 60510
data.length: 60457
data.length: 59811
data.length: 59953
data.length: 59889
data.length: 59856
data.length: 59936
data.length: 60049
data.length: 60091
data.length: 60012
%%% FFMPEG Info %%%
frame= 848 fps= 38 q=24.8 q=29.0 size= 50340kB time=00:00:28.29 bitrate=14574.4kbits/s speed=1.28x
data.length: 60025
data.length: 60064
data.length: 60122
data.length: 60202
data.length: 60113
data.length: 60211
data.length: 60201
data.length: 60195
data.length: 60116
data.length: 60167
data.length: 60273
data.length: 60222
data.length: 60223
data.length: 60267
data.length: 60329
%%% FFMPEG Info %%%
frame= 863 fps= 38 q=24.8 q=29.0 size= 51221kB time=00:00:28.79 bitrate=14571.9kbits/s speed=1.27x
As you can see, a whole frame is about ~60k (my indication is a clean video stream i'm viewing on the browser), but every now and then the output consists of 2 consecutive chunks that add up to ~60k. when delivered to the browser, these are 'half frames'.
Following the comments here and on StackExchange, it seems that the MJPEG stream outputted from the ffmpeg process should consist of whole frames. listening to the ffmpeg ChildProcess stdout yields data chunks of varying size- which means they dont always represent a whole frame (full JPEG) image.
So instead of just pushing them to the consumer (currently a web browser showing the video stream) I wrote a bit of code to handle 'half-chunks' in memory and append them together until the frame is complete.
This seems to solve the problem, as I'm getting no flickering in the video.
const _SOI = Buffer.from([0xff, 0xd8]);
const _EOI = Buffer.from([0xff, 0xd9]);
private size: number = 0;
private chunks: any[] = [];
private jpegInst: any = null;
private pushWholeMjpegFrame(chunk: any): void {
const chunkLength = chunk.length;
let pos = 0;
while (true) {
if (this.size) {
const eoi = chunk.indexOf(_EOI);
if (eoi === -1) {
this.chunks.push(chunk);
this.size += chunkLength;
break;
} else {
pos = eoi + 2;
const sliced = chunk.slice(0, pos);
this.chunks.push(sliced);
this.size += sliced.length;
this.jpegInst = Buffer.concat(this.chunks, this.size);
this.chunks = [];
this.size = 0;
this.sendJpeg();
if (pos === chunkLength) {
break;
}
}
} else {
const soi = chunk.indexOf(_SOI, pos);
if (soi === -1) {
break;
} else {
pos = soi + 500;
}
const eoi = chunk.indexOf(_EOI, pos);
if (eoi === -1) {
const sliced = chunk.slice(soi);
this.chunks = [sliced];
this.size = sliced.length;
break;
} else {
pos = eoi + 2;
this.jpegInst = chunk.slice(soi, pos);
this.sendJpeg();
if (pos === chunkLength) {
break;
}
}
}
}
}
I would love to get some more educated input on my solution if it can be improved and optimized, as well as some more knowledge as to the origin of the problem and perhaps a way to get the desired behavior out-of-the-box with ffmpeg, so feel free to keep this question alive with more answers and comments.