fluent-ffmpeg / node-fluent-ffmpeg

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

Take screenshot but not save in image #1144

Closed Jourdelune closed 2 years ago

Jourdelune commented 2 years ago

Hello, I would like to take a picture of my video but without saving it in an image and instead retrieve the image data stored in a variable. Is this possible?

mandaputtra commented 2 years ago

No, I look at the implementation there are no options to save it as buffer.

Jourdelune commented 2 years ago

And do you know any alternative that can do it in js? I'm looking and I can't find it

mandaputtra commented 2 years ago

What actually do you want to achieve? I can profide some example.

You want to compress the image? Or else?

Jourdelune commented 2 years ago

I make a small application to play music via files, I want to get the thumbnail of a mp4 file to be able to display it but since there can be a lot of music, I will not save the image in a png file, get the buffer, create a url and send it to the html then delete the image and this for each music I would have to recover the buffer directly

mandaputtra commented 2 years ago

Okay then it is should be like this. I just scribbled here please read the comments, basically fs.readFileSync will create the buffer and you can sent it to the user.

const tmp = require('tmp'); // install this with npm first
const ffmpeg = require('ffmpeg')
const http = require('http');
const fs = require('fs');
const path = require('path')

http.createServer(function(req, res) {
   // create temp directory
   const tmpobj = tmp.dirSync();
// process the screenshot
  ffmpeg('/path/to/video.avi')
   .screenshots({
    timestamps: [30.5, '50%', '01:10.123'],
    filename: 'thumbnail.png',
    folder: tmpobj.name, // path to tmp folder
    size: '320x240'
  });

   // create buffer
   const buf = fs.readFileSync('./package.json');
   const bufString = buf.toString('hex');
   // delete your file
     tmpobj.removeCallback();
    return bufString
}).listen(8080);
Jourdelune commented 2 years ago

Hi, thanks for your answer and really thank you for taking the time to develop this script! On the other hand I did not understand why read package.json "const buf = fs.readFileSync('./package.json');"

mandaputtra commented 2 years ago

lol I just copy paste the usage, it should be fs.readfilesync(path.join(tmpobj.name, 'thumbnail.png'))

On Sat, 4 Sep 2021, 21:33 Jourdelune, @.***> wrote:

Hi, thanks for your answer and really thank you for taking the time to develop this script! On the other hand I did not understand why read package.json "const buf = fs.readFileSync('./package.json');"

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/fluent-ffmpeg/node-fluent-ffmpeg/issues/1144#issuecomment-912984203, or unsubscribe https://github.com/notifications/unsubscribe-auth/AFSC6X7BR4FFPDYXU2UMZLLUAIU4DANCNFSM5DME2K4Q .

Jourdelune commented 2 years ago

Okay, I have do that

const tmpobj = tmp.dirSync();
ffmpeg(`${testFolder}${file}`)
.takeScreenshots({
    timestamps: ['50%'],
    filename: 'thumbnail.png',
    folder: tmpobj.name,
    size: '320x240'
});
const buf = fs.readFileSync(path.join(tmpobj.name,'thumbnail.png'))
const bufString = buf.toString('hex');
tmpobj.removeCallback();

But I have this error

"Uncaught (in promise) Error: ENOENT: no such file or directory, open '/tmp/tmp-19974-ueHIfWHyyFNj/thumbnail.png'", source: fs.js (476)

however the file with the image exists (I checked), you have an idea of the reason (maybe it looks for this file in the current directory) ?

Jourdelune commented 2 years ago

Finally I have make that and that work, thanks for help me^^

const tmpobj = tmp.dirSync();
                ffmpeg(`${testFolder}${file}`).takeScreenshots({
                  timestamps: ['50%'],
                  filename: 'thumbnail.png',
                  folder: tmpobj.name,
                  size: '320x240'
                }).on('end', async function() {
                  const buf = await fs.readFileSync(path.join(tmpobj.name,'thumbnail.png'), {encoding: 'base64'})
                  const format = "image/png";
                  addHtml('liste-track', `
                  <tr class="track-choice">
                    <td class="track-name" style="max-width: 200px;"><img src="data:image/gif;base64,${buf}" alt="" width="40px" height="40px" class="logo-align"/><i class="fas fa-play lg play-in-survol" onclick="PlayMusic(this, '${testFolder}${file}')"></i>${metadata.common.title}</td>
                    <td class="artist-name">${metadata.common.artist}</td>
                    <td class="album">${tag.tags.album==="undefined"? tag.tags.album:"None"}</td>
                    <td class="timer-table">${duration}</td>
                  </tr>
                  `)
                  fs.rmdirSync(tmpobj.name, { recursive: true });
                })
              }
laggingreflex commented 2 years ago

The original intent (to retreive a screenshot/thumbnail directly in a buffer) is actually possible:

const noop = () => {}
ffmpeg
  .input('D:/videos/input.mp4')
  .inputOptions(['-ss', 5])

  .output({
    writable: true,
    write: data => {

      data // = image buffer

    },
    on: noop, once: noop, pipe: noop, emit: noop, end: noop,
  })
  .outputOptions([
    '-vframes 1',
    '-f image2pipe'
  ])
  .run();

.output() can be a stream which will receive buffered data, so I've just mocked a writable stream here to get it.

DrSammyD commented 1 year ago

If you want to take a dependency on PNG here's the code I've been using to parse PNG's and emitting them

import ffmpeg from "fluent-ffmpeg"
import { Transform } from "stream"
const { PNG } = require('pngjs');

// magic string to detect end of png from stream
const pngMagicNumber = "IEND®B`‚".split("").map((val) => val.charCodeAt(0));
function parseFFMpegPng(ffmpeg: ffmpeg.FfmpegCommand) {
    let incompleteFrame = [] as number[];
    let frames = 1;

    return ffmpeg.output(
        new Transform({
            write(data: Buffer, _encoding, cb) {
                let buffers = [] as Buffer[];
                let found = [] as number[];
                ({ incompleteFrame, buffers } = data.reduce((last, current) => {
                    last.incompleteFrame.push(current);
                    if (found.length >= 8) {
                        found.shift();
                    }
                    found.push(current);
                    if (pngMagicNumber.every((val, index) => val === found[index])) {
                        last.buffers.push(Buffer.from(last.incompleteFrame));
                        found = [];
                        last.incompleteFrame = [];
                    }
                    return last;
                }, { incompleteFrame, buffers: [] as Buffer[] }));
                if (buffers.length) {
                    this.emit('png', buffers)
                }
                cb();
            },
        }).on('png', function (buffers: Buffer[]) {
            buffers.forEach((buffer) => {
                const dst = new PNG;
                const frame = frames++;
                dst.on('parsed', function (this: { width: number, height: number, data: Buffer }) {
                    const { width, height, data } = this;
                    ffmpeg.emit('parsed', { width, height, data, frame });
                }).parse(buffer);
            });
        }))
        .videoCodec('png')
        .addOutputOption('-vsync', '0')
        .format("image2pipe");
}

Then to use, just pass the function an ffmpeg command

const reader = parseFFMpegPng(ffmpeg(url, {
            stdoutLines: 0,
            preset: 'ultrafast',
            logger: {
                info: console.log,
                warn: console.warn,
                error: console.error,
                debug: console.info,
            }
        })))

To get the PNG data.

reader.on('parsed', (png: { width: number, height: number, data: Buffer, frame: number }) => {
    // Use PNGjs data here
})
tyutjohn commented 6 months ago

Maybe you need this

const readStream = fs.createReadStream('demo.mp4')
const frames: Buffer[] = []
let currentBuffer: Buffer[] = []

const PNG_HEADER = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])

ffmpeg().input(readStream)
.outputOptions(['-movflags frag_keyframe+empty_moov', '-f image2pipe', '-vcodec png'])
.output(
        new Transform({
          write(data: Buffer, _encoding, cb) {
            const currentIndex = data.indexOf(PNG_HEADER)
            const isPngHeaderStart = currentIndex !== -1
            if(isPngHeaderStart) {
              const first = data.subarray(0, currentIndex)
              const last = data.subarray(currentIndex)
              if (currentBuffer.length !== 0) {
                frames.push(Buffer.concat([...currentBuffer, first]))
              }
              currentBuffer = [last]
            } else {
              currentBuffer.push(data)
            }
            cb()
          },
        }), { end: true }
      )
.on('end', () => {
     frames.push(Buffer.concat(currentBuffer))
     currentBuffer = []
     // do something, frames is you need Buffers
     console.log(frames.length, 'frames')
     // clear Buffer
     frames.length = 0;
})
.run()