fluent-ffmpeg / node-fluent-ffmpeg

A fluent API to FFMPEG (http://www.ffmpeg.org)
MIT License
7.93k stars 880 forks source link

Having Trouble Streaming to HTTP Response: Output stream closed #470

Closed knpwrs closed 8 years ago

knpwrs commented 8 years ago

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:

ffmpeg -i 'Bass.wav' -i 'Drums.wav' -filter_complex [0:a]pan=stereo\|c0=2*c0\|c1=3*c0[a0]\;[1:a]pan=stereo\|c0=1*c0\|c1=4*c0[a1]\;[a0][a1]amix=inputs=2 -f mp3 -acodec libmp3lame pipe:1

That command works. The inputs are single-channel wav files and the command produces a stereo mp3 with funky panning. This is the code I am using with fluent-ffmpeg:

router.get('/', function mixRoot(req, res) {
  // Set inputs
  let command = tracks.reduce(
    (cmd, track) => cmd.input(track.name),
    ffmpeg() // 'ffmpeg-static'
  );
  // Set pan filters
  command = tracks.reduce(
    (cmd, track, i) => cmd.complexFilter({
      filter: 'pan',
      options: `stereo|c0=${track.l}*c0|c1=${track.r}*c0`,
      outputs: `a${i}`,
    }),
    command
  );
  // Mix filter
  command = command.complexFilter({
    filter: 'amix',
    options: {inputs: tracks.length},
    inputs: tracks.map((track, i) => `a${i}`),
  });
  // Output options
  command = command.audioCodec('libmp3lame').format('mp3');
  // Write headers
  res.writeHead(200, {
    'Content-Type': 'audio/mp3',
  });
  // Pipe to client
  command.pipe(res, {end: true});
});

This is the error I'm getting:

Error: Output stream closed
    at null._onTimeout (.../node_modules/fluent-ffmpeg/lib/processor.js:476:25)
    at Timer.listOnTimeout (timers.js:92:15)

When I change the final function call from pipe to output I consistently get a request that is continually pending:

image

I feel like I'm missing something obvious. Any ideas?

njoyard commented 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.

knpwrs commented 8 years ago

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?

knpwrs commented 8 years ago

Also, is there a way to see the command this is generating? Like a string I can run in a terminal or something?

njoyard commented 8 years ago

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) ?

knpwrs commented 8 years ago

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!

njoyard commented 8 years ago

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.

knpwrs commented 8 years ago

Any tips for handling the double request? I'm thinking just close the response when there isn't a Range header.

knpwrs commented 8 years ago

In that case I'd probably also have to set a few other headers to ensure consistent behavior across all browsers.

knpwrs commented 8 years ago

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!

njoyard commented 8 years ago

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.

knpwrs commented 8 years ago

Two questions: will closing the connection automatically terminate FFMPEG? Also, is there a way to support ranges with FFMPEG?

njoyard commented 8 years ago

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.

Codingale commented 8 years ago

~derp I was so new to NodeJS back then~

njoyard commented 8 years ago

You can't "catch" an asynchronous error. You must register an event handler to catch it.

GustavPS commented 4 years ago

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});
  });
guillaume-g commented 4 years ago

Same here @GustavPS ! I have exactly the same situation as you and my code is pretty similar to yours

GustavPS commented 4 years ago

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'
      });
jgabriel98 commented 3 years ago

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:


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 });
GustavPS commented 3 years ago

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?

jgabriel98 commented 3 years ago

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.

Some context first:

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.

manuchekhr32 commented 1 year ago

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: image

And here is my code: image

The problem is when I try to change video range by my hls video client before current ones are pending I got this error: image

@jgabriel98 @njoyard any solutions for this problem?

eumentis-pooja commented 5 months ago

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.