Closed knpwrs closed 8 years ago
I think the browser/whatever http client you're using is closing the connexion early because your output is bad. And the reason for that is probably your usage of complexFilter : this method should only be called once, every additional call will overwrite previously set filters. Build your filtergraph first, then pass it to complexFilter all at once.
Same problem with the following:
router.get('/', function mixRoot(req, res) {
// Create ffmpeg command with inputs
let command = tracks.reduce(
(cmd, track) => cmd.input(track.name),
ffmpeg()
);
// Construct filter graph
const filterGraph = [];
tracks.forEach((track, i) => filterGraph.push({
filter: 'pan',
options: `stereo|c0=${track.l}*c0|c1=${track.r}*c0`,
outputs: `a${i}`,
}));
filterGraph.push({
filter: 'amix',
options: {inputs: tracks.length},
inputs: tracks.map((track, i) => `a${i}`),
});
// Finalize command
command = command.complexFilter(filterGraph).audioCodec('libmp3lame').format('mp3');
// Send to client
res.writeHead(200, {
'Content-Type': 'audio/mp3',
});
command.pipe(res, {end: true});
});
Also, why is the behavior different between pipe
and output
?
Also, is there a way to see the command this is generating? Like a string I can run in a terminal or something?
The output
method does not launch ffmpeg, it just adds an output to the command. Hence the pending response.
You can use the 'start' event to see what command is run.
About the error you get, is it coming from the 'error' event handler ? Can you post the values of the 2nd and 3rd arguments to this handler (ffmpeg stdout and stderr) ?
Ummmm.... It started working as soon as I added an error
handler. I still get the error, but data is being streamed to the client.
It would appear as though stderr
and stdout
are undefined
. The client is making two requests for some reason. The second request has a Range
header on it:
First request:
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding:gzip, deflate, sdch
Accept-Language:en-US,en;q=0.8
Cache-Control:no-cache
Connection:keep-alive
Cookie:connect.sid=s%3An1t31HZSge8zUVIeJCnFzknr.opmA2Jbi%2BQ4geTQQXefFaPBAcEREtNsDhAcxtcKMzDw
Host:localhost:3000
Pragma:no-cache
Upgrade-Insecure-Requests:1
User-Agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.86 Safari/537.36
Second request:
Accept:*/*
Accept-Encoding:identity;q=1, *;q=0
Accept-Language:en-US,en;q=0.8
Cache-Control:no-cache
Connection:keep-alive
Cookie:connect.sid=s%3An1t31HZSge8zUVIeJCnFzknr.opmA2Jbi%2BQ4geTQQXefFaPBAcEREtNsDhAcxtcKMzDw
Host:localhost:3000
Pragma:no-cache
Range:bytes=0-
Referer:http://localhost:3000/mix?song=This%20Is%20Amazing%20Grace%20[A]&tracks=[{%22name%22:%22Bass.wav%22,%22l%22:1,%22r%22:1},{%22name%22:%22Drums.wav%22,%22l%22:2,%22r%22:2}]
User-Agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.86 Safari/537.36
So I guess the closed stream is due to the browser ending the first request and making the second request. I can look into this more. Or maybe you know something about it?
Also, I'm kind of surprised that the command works at all, unless the string passed to the start
event isn't verbatim what is executed:
'ffmpeg -i /Users/kpowers/Workspace/Excel/mixatron/songs/This Is Amazing Grace [A]/Bass.wav -i /Users/kpowers/Workspace/Excel/mixatron/songs/This Is Amazing Grace [A]/Drums.wav -filter_complex pan=stereo|c0=1*c0|c1=1*c0[a0];pan=stereo|c0=2*c0|c1=2*c0[a1];[a0][a1]amix=inputs=2 -acodec libmp3lame -f mp3 pipe:1'
As you can see, there are spaces in the input paths but everything still works fine.
If you know anything about the dual request thing that would be great to know. Otherwise I can close this issue. Thanks for the help!
The string passed to 'start' is a concatenation with spaces of the argument list, so basically it wouldn't work if you just copied it to command line (launching processes from nodejs does not require quoting arguments, as they are passed as an array).
The double request thing is something I've seen before. Basically the implementation in most browsers starts downloading a few bytes, reads file headers (sometimes even only reads http response headers), then switches to ranged requests tailored to the actual stream contents.
The fact that your command works with an 'error' handler is not surprising. You should always have a 'error' handler, or else any error will be caught by nodejs main loop and stop your program. And the stdout/strerr arguments are probably undefined because the stream was closed too soon for them to contain something.
Any tips for handling the double request? I'm thinking just close the response when there isn't a Range
header.
In that case I'd probably also have to set a few other headers to ensure consistent behavior across all browsers.
Setting the response header Content-Disposition:attachment
seems to solve two problems for me: the double request and the browser will now force a download instead of trying to stream.
Thanks for your help!
Glad I could help.
Oh and don't forget : you should expect requests that close early. This is also what will happen if your user cancels the download. Make sure your program handles this correctly (maybe logging a warning and freeing anything that's necessary). Do not force early request termination as that probably will upset some browsers.
Two questions: will closing the connection automatically terminate FFMPEG? Also, is there a way to support ranges with FFMPEG?
Closing the connection will terminate ffmpeg, it will end with a "broken pipe" error.
There is no easy way to suport ranges with ffmpeg. Ranged requests are used by <video> tags to seek into the media, and <video> tags are primarily made to access already-encoded streams (i.e. files), everything else is considered live and won't be able to seek. Ranged requests are made with byte offsets, and you cannot know them beforehand when transcoding (except when using very specific codecs and settings, which are not supported by browsers...)
There are alternatives but they're quite cumbersome. Search for "chrome", "video tag", "html" or "http" in the issues for more info.
Again, all media APIs in browser were designed towards media hosting services that have already-encoded files available, or at least encode them in advance (eg. what youtube does). Live streaming is supported but the very useful use case of "I have a media stream in a random format, I don't want to store versions encoded for the web, let me live-transcode it" has been completely forgotten.
~derp I was so new to NodeJS back then~
You can't "catch" an asynchronous error. You must register an event handler to catch it.
Very old thread, but I have the exact same problem and adding an error handler does not fix the problem for me. I get "Output stream closed" after some information has been piped, the progress event is being fired about 3 times each request. From what I can see my browser (chrome) is only sending one request and not two.
let filename = 'C:\\Users\\Gustav\\code\\nextjs-blog\\MovieServer\\movieserver\\movies\\20051210-w50s.flv';
res.setHeader('Content-Type', 'video/flv');
fs.stat(filename, function (err, stats) {
var range = req.headers.range;
if (!range) {
console.log("Ingen range");
return res.status(416).end();
}
//Chunk logic here
var positions = range.replace(/bytes=/, "").split("-");
var start = parseInt(positions[0], 10);
var total = stats.size;
var end = positions[1] ? parseInt(positions[1], 10) : total - 1;
var chunksize = (end - start) + 1;
res.writeHead(206, {
'Transfer-Encoding': 'chunked',
"Content-Range": "bytes " + start + "-" + end + "/" + total,
"Accept-Ranges": "bytes",
"Content-Length": chunksize,
"Content-Type": 'video/flv'
});
// make sure you set the correct path to your video file storage
var proc = ffmpeg(filename)
.audioCodec('aac')
.videoCodec('libx264')
.addOption('-strict', 'experimental')
.addOption('-movflags', 'faststart')
.toFormat('flv')
.size('640x480').autopad()
// setup event handlers
.on('end', function() {
console.log('file has been converted succesfully');
})
.on('progress', function(progress) {
console.log('Processing: ' + progress.percent + '% done');
})
.on('error', function(err) {
console.log('an error happened: ' + err.message);
})
// save to stream
.pipe(res, {end:true});
});
Same here @GustavPS ! I have exactly the same situation as you and my code is pretty similar to yours
Same here @GustavPS ! I have exactly the same situation as you and my code is pretty similar to yours
I managed to make it work, here is my code. Maybe it can help you.
var proc = ffmpeg(filename, { presets: '../../../../lib/ffmpeg-presets'})
.preset(quality)
// Might be faster with only 1 thread? TODO: Test it
.inputOptions([
`-ss ${offset}`,
'-threads 3'
])
<Event_stuff_here>
proc.output(res,{ end:true }).run();
That preset contains:
ffmpeg
//.withVideoCodec('h264_nvenc')
.withVideoBitrate(8000)
.withAudioCodec('libmp3lame')
.withVideoCodec('h264_nvenc')
.outputOption([
'-map 0',
'-map -v',
'-map -a',
'-map 0:V',
'-map 0:m:language:eng?', // TODO: This should be an input parameter to be able to change language
'-deadline realtime',
'-lag-in-frames 0',
'-static-thresh 0',
'-frame-parallel 1',
'-crf 4',
'-movflags frag_keyframe+faststart',
'-pix_fmt yuv420p',
'-sn',
'-max_muxing_queue_size 9999'
])
.outputFormat('mp4')
};
My headers are:
res.writeHead(200, {
'Accept-Ranges': 'bytes',
'Connection':'keep-alive',
'Content-Range':'bytes '+start+'-'+end+'/*',
'Transfer-Encoding':'chunked',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': '*',
"Content-Disposition":"inline",
"Content-Transfer-Enconding":"binary",
'Content-Type': 'video/mp4'
});
Old thread, but i still had issues with this, even after trying all suggestions and solutions posted here and on other related issues from this repo (like #1067, and #366).
First for context, my code was like this:
const { start, duration } = req.query;
const { agendamentoId, fileName } = req.params;
const video_path = path.join(path.resolve(Agendamento.VIDEO_FOLDER), agendamentoId, fileName);
if (start == undefined && duration == undefined) {
return res.sendFile(video_path, start, duration);
}
let totalkbytes = 0;
const clip_command = Ffmpeg(video_path, { niceness: 5 }) //dá pioridade inferior (padrão é 0) ao processo.
.format("mp4") // rawvideo (extremamente grande!) ou h264.`ffmpeg -muxers` lista os formatos disponíveis
.outputOptions(["-movflags", "frag_keyframe+empty_moov"])
.withVideoCodec("copy") // evita reencoding do vídeo
.withAudioCodec("copy")
.seekInput(start)
.withDuration(duration)
.on("error", (err) => {
return res.status(400).json({ error: `Um erro interno ocorreu durante o corte do vídeo: ${err.message}` });
})
res.writeHead(200, {
Connection: "keep-alive",
"Content-Transfer-Encoding": "binary",
"Access-Control-Allow-Origin": "*",
"Content-Disposition": `attachment; filename=${agendamentoId}_editado.mp4`,
"Content-Type": "video/mp4",
});
clip_command.pipe(res, { end: true });
And after A LOT of research, i've decided to spwan a ffmpeg process myself and pipe it stdout to res
. And then got this error:
muxer does not support non seekable output
and then added -movflags frag_keyframe+empty_moov
to the args, like explained in ffmpeg-transmux-mpegts-to-mp4-gives-error-muxer-does-not-support-non-seekable, and finally solving my issue.
latter i've found out that returning a response inside on('error')
like this:
Ffmpeg(video_path, { niceness: 5 })
.format("mp4")
// .outputOptions(["-movflags", "frag_keyframe+empty_moov"]) //without these options ffmpeg errors with `muxer does not support non seekable output`
.withVideoCodec("copy")
.withAudioCodec("copy")
.seekInput(start)
.withDuration(duration)
.on("error", (err) => {
return res.status(400).json({ error: `Um erro interno ocorreu durante o corte do vídeo: ${err.message}` });
})
throwed another error:
Output stream closed
_http_outgoing.js:561
throw new ERR_HTTP_HEADERS_SENT('set');
^
Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
, which offuscated the muxer does not support non seekable output
one.
So the solution was to:
.on('error')
res
, add the options -movflags frag_keyframe+empty_moov
So my final working code got like this:
const { start, duration } = req.query;
const { agendamentoId, fileName } = req.params;
const video_path = path.join(path.resolve(Agendamento.VIDEO_FOLDER), agendamentoId, fileName);
if (start == undefined && duration == undefined) {
return res.sendFile(video_path, start, duration);
}
let totalkbytes = 0;
const clip_command = Ffmpeg(video_path, { niceness: 5 }) //dá pioridade inferior (padrão é 0) ao processo.
.format("mp4") // rawvideo (extremamente grande!) ou h264.`ffmpeg -muxers` lista os formatos disponíveis
.outputOptions(["-movflags", "frag_keyframe+empty_moov"])
.withVideoCodec("copy") // evita reencoding do vídeo
.withAudioCodec("copy")
.seekInput(start)
.withDuration(duration)
.on("start", (cmd) => console.log("Iniciando corte do vídeo " + `${cmd}`.gray))
.on("progress", (progress) => (totalkbytes = progress.targetSize))
.on("error", (err) => {
console.log(`Um erro ocorreu durante o corte do vídeo: ${err.message}`.red);
// return res.status(400).json({ error: `Um erro interno ocorreu durante o corte do vídeo: ${err.message}` });
})
.on("end", (stdout, stderr) =>
console.log(
"Corte do vídeo finalizado! " + `escreveu ${totalkbytes} Kbytes (${Math.round(totalkbytes * 0.001)} Mb)`.gray
)
);
// fonte do porquê de usar esses headers: https://github.com/fluent-ffmpeg/node-fluent-ffmpeg/issues/470
res.writeHead(200, {
Connection: "keep-alive",
"Content-Transfer-Encoding": "binary",
"Access-Control-Allow-Origin": "*",
"Content-Disposition": `attachment; filename=${agendamentoId}_editado.mp4`,
"Content-Type": "video/mp4",
});
clip_command.pipe(res, { end: true });
Old thread, but i still had issues with this, even after trying all suggestions and solutions posted here and on other related issues from this repo (like #1067, and #366).
First for context, my code was like this:
const { start, duration } = req.query; const { agendamentoId, fileName } = req.params; const video_path = path.join(path.resolve(Agendamento.VIDEO_FOLDER), agendamentoId, fileName); if (start == undefined && duration == undefined) { return res.sendFile(video_path, start, duration); } let totalkbytes = 0; const clip_command = Ffmpeg(video_path, { niceness: 5 }) //dá pioridade inferior (padrão é 0) ao processo. .format("mp4") // rawvideo (extremamente grande!) ou h264.`ffmpeg -muxers` lista os formatos disponíveis .outputOptions(["-movflags", "frag_keyframe+empty_moov"]) .withVideoCodec("copy") // evita reencoding do vídeo .withAudioCodec("copy") .seekInput(start) .withDuration(duration) .on("error", (err) => { return res.status(400).json({ error: `Um erro interno ocorreu durante o corte do vídeo: ${err.message}` }); }) res.writeHead(200, { Connection: "keep-alive", "Content-Transfer-Encoding": "binary", "Access-Control-Allow-Origin": "*", "Content-Disposition": `attachment; filename=${agendamentoId}_editado.mp4`, "Content-Type": "video/mp4", }); clip_command.pipe(res, { end: true });
And after A LOT of research, i've decided to spwan a ffmpeg process myself and pipe it stdout to
res
. And then got this error:muxer does not support non seekable output
and then added
-movflags frag_keyframe+empty_moov
to the args, like explained in ffmpeg-transmux-mpegts-to-mp4-gives-error-muxer-does-not-support-non-seekable, and finally solving my issue.latter i've found out that returning a response inside
on('error')
like this:Ffmpeg(video_path, { niceness: 5 }) .format("mp4") // .outputOptions(["-movflags", "frag_keyframe+empty_moov"]) //without these options ffmpeg errors with `muxer does not support non seekable output` .withVideoCodec("copy") .withAudioCodec("copy") .seekInput(start) .withDuration(duration) .on("error", (err) => { return res.status(400).json({ error: `Um erro interno ocorreu durante o corte do vídeo: ${err.message}` }); })
throwed another error:
Output stream closed _http_outgoing.js:561 throw new ERR_HTTP_HEADERS_SENT('set'); ^ Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
, which offuscated the
muxer does not support non seekable output
one.So the solution was to:
- don't set the response status or header inside
.on('error')
- when using mp4 and piping directly to
res
, add the options-movflags frag_keyframe+empty_moov
So my final working code got like this:
const { start, duration } = req.query; const { agendamentoId, fileName } = req.params; const video_path = path.join(path.resolve(Agendamento.VIDEO_FOLDER), agendamentoId, fileName); if (start == undefined && duration == undefined) { return res.sendFile(video_path, start, duration); } let totalkbytes = 0; const clip_command = Ffmpeg(video_path, { niceness: 5 }) //dá pioridade inferior (padrão é 0) ao processo. .format("mp4") // rawvideo (extremamente grande!) ou h264.`ffmpeg -muxers` lista os formatos disponíveis .outputOptions(["-movflags", "frag_keyframe+empty_moov"]) .withVideoCodec("copy") // evita reencoding do vídeo .withAudioCodec("copy") .seekInput(start) .withDuration(duration) .on("start", (cmd) => console.log("Iniciando corte do vídeo " + `${cmd}`.gray)) .on("progress", (progress) => (totalkbytes = progress.targetSize)) .on("error", (err) => { console.log(`Um erro ocorreu durante o corte do vídeo: ${err.message}`.red); // return res.status(400).json({ error: `Um erro interno ocorreu durante o corte do vídeo: ${err.message}` }); }) .on("end", (stdout, stderr) => console.log( "Corte do vídeo finalizado! " + `escreveu ${totalkbytes} Kbytes (${Math.round(totalkbytes * 0.001)} Mb)`.gray ) ); // fonte do porquê de usar esses headers: https://github.com/fluent-ffmpeg/node-fluent-ffmpeg/issues/470 res.writeHead(200, { Connection: "keep-alive", "Content-Transfer-Encoding": "binary", "Access-Control-Allow-Origin": "*", "Content-Disposition": `attachment; filename=${agendamentoId}_editado.mp4`, "Content-Type": "video/mp4", }); clip_command.pipe(res, { end: true });
Hi, how did you solve the seeking problem? Just letting the webpage create a new request to the server with a different start query each time?
Old thread, but i still had issues with this, even after trying all suggestions and solutions posted here and on other related issues from this repo (like #1067, and #366).
First for context, my code was like this:
const { start, duration } = req.query; const { agendamentoId, fileName } = req.params; const video_path = path.join(path.resolve(Agendamento.VIDEO_FOLDER), agendamentoId, fileName); if (start == undefined && duration == undefined) { return res.sendFile(video_path, start, duration); } let totalkbytes = 0; const clip_command = Ffmpeg(video_path, { niceness: 5 }) //dá pioridade inferior (padrão é 0) ao processo. .format("mp4") // rawvideo (extremamente grande!) ou h264.`ffmpeg -muxers` lista os formatos disponíveis .outputOptions(["-movflags", "frag_keyframe+empty_moov"]) .withVideoCodec("copy") // evita reencoding do vídeo .withAudioCodec("copy") .seekInput(start) .withDuration(duration) .on("error", (err) => { return res.status(400).json({ error: `Um erro interno ocorreu durante o corte do vídeo: ${err.message}` }); }) res.writeHead(200, { Connection: "keep-alive", "Content-Transfer-Encoding": "binary", "Access-Control-Allow-Origin": "*", "Content-Disposition": `attachment; filename=${agendamentoId}_editado.mp4`, "Content-Type": "video/mp4", }); clip_command.pipe(res, { end: true });
And after A LOT of research, i've decided to spwan a ffmpeg process myself and pipe it stdout to
res
. And then got this error:muxer does not support non seekable output
and then added
-movflags frag_keyframe+empty_moov
to the args, like explained in ffmpeg-transmux-mpegts-to-mp4-gives-error-muxer-does-not-support-non-seekable, and finally solving my issue.latter i've found out that returning a response inside
on('error')
like this:Ffmpeg(video_path, { niceness: 5 }) .format("mp4") // .outputOptions(["-movflags", "frag_keyframe+empty_moov"]) //without these options ffmpeg errors with `muxer does not support non seekable output` .withVideoCodec("copy") .withAudioCodec("copy") .seekInput(start) .withDuration(duration) .on("error", (err) => { return res.status(400).json({ error: `Um erro interno ocorreu durante o corte do vídeo: ${err.message}` }); })
throwed another error:
Output stream closed _http_outgoing.js:561 throw new ERR_HTTP_HEADERS_SENT('set'); ^ Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
, which offuscated the
muxer does not support non seekable output
one.So the solution was to:
- don't set the response status or header inside
.on('error')
- when using mp4 and piping directly to
res
, add the options-movflags frag_keyframe+empty_moov
So my final working code got like this:
const { start, duration } = req.query; const { agendamentoId, fileName } = req.params; const video_path = path.join(path.resolve(Agendamento.VIDEO_FOLDER), agendamentoId, fileName); if (start == undefined && duration == undefined) { return res.sendFile(video_path, start, duration); } let totalkbytes = 0; const clip_command = Ffmpeg(video_path, { niceness: 5 }) //dá pioridade inferior (padrão é 0) ao processo. .format("mp4") // rawvideo (extremamente grande!) ou h264.`ffmpeg -muxers` lista os formatos disponíveis .outputOptions(["-movflags", "frag_keyframe+empty_moov"]) .withVideoCodec("copy") // evita reencoding do vídeo .withAudioCodec("copy") .seekInput(start) .withDuration(duration) .on("start", (cmd) => console.log("Iniciando corte do vídeo " + `${cmd}`.gray)) .on("progress", (progress) => (totalkbytes = progress.targetSize)) .on("error", (err) => { console.log(`Um erro ocorreu durante o corte do vídeo: ${err.message}`.red); // return res.status(400).json({ error: `Um erro interno ocorreu durante o corte do vídeo: ${err.message}` }); }) .on("end", (stdout, stderr) => console.log( "Corte do vídeo finalizado! " + `escreveu ${totalkbytes} Kbytes (${Math.round(totalkbytes * 0.001)} Mb)`.gray ) ); // fonte do porquê de usar esses headers: https://github.com/fluent-ffmpeg/node-fluent-ffmpeg/issues/470 res.writeHead(200, { Connection: "keep-alive", "Content-Transfer-Encoding": "binary", "Access-Control-Allow-Origin": "*", "Content-Disposition": `attachment; filename=${agendamentoId}_editado.mp4`, "Content-Type": "video/mp4", }); clip_command.pipe(res, { end: true });
Hi, how did you solve the seeking problem? Just letting the webpage create a new request to the server with a different start query each time?
I did not, I've just avoid it.
My code treats a request for clip/trim a long video and download it.
I only wanted to provide a file to download, never to play it directly on the browser.
So why pipe ffmpeg directly to the response stream, instead of to a file?: To avoid disk IO and resources, since this new generated vídeo is a one time request that (probably) never repeats.
You can see that on my headers I use Content-Disposition: attachment
, because my intention is to make ffmpeg generate a new file, and pipe it to the client also as a file ( a downloadable file).
All the browsers treat it as a attachment file and just download it,
except for Safari that still gives the option to play it or download it, because Apple and they implement stuff like they want to. And when it tries to play the file, it will cause some error that I just ignore (since it was never meant to play it, just download).
So TL;DR: nah, block video play, just force video download.
Hi everybody, it looks so old thread but I have tried almost every solution but it didn't help. So, I have hls files 10 second/per file:
And here is my code:
The problem is when I try to change video range by my hls video client before current ones are pending I got this error:
@jgabriel98 @njoyard any solutions for this problem?
I'm experiencing an "Output stream closed" error when trying capture image form RTSP via FFmpeg in an Express app. Despite setting the 'Content-Disposition' header correctly, the error persists. What should do ? Is there any option to increase time for the http request.
I am trying to take several input files and mix them together with various pan settings. This is the command for
ffmpeg
I am trying to replicate:That command works. The inputs are single-channel
wav
files and the command produces a stereomp3
with funky panning. This is the code I am using withfluent-ffmpeg
:This is the error I'm getting:
When I change the final function call from
pipe
tooutput
I consistently get a request that is continually pending:I feel like I'm missing something obvious. Any ideas?