@calipsa/video-utils
Version:
Video utilities
96 lines (78 loc) • 2.47 kB
text/typescript
import ffmpeg, {
FfprobeData,
FfmpegCommandOptions,
FfmpegCommand,
} from 'fluent-ffmpeg'
import {
StreamWithFrames,
} from '@calipsa/stream-utils'
import getFrameAt from '../getFrameAt'
import getFrameIndices from './getFrameIndices'
interface Options {
path: string,
numFrames: number,
videoMetadata: FfprobeData,
ffmpegCommandOptions?: FfmpegCommandOptions,
configureFfmpeg?: (ffmpegCommand: FfmpegCommand) => void,
}
export default async ({
path,
numFrames,
videoMetadata,
ffmpegCommandOptions,
configureFfmpeg,
}: Options) => {
const videoStreamMetaData = videoMetadata.streams.find(item => item.codec_type === 'video')
if (!videoStreamMetaData) {
throw new Error('No video streams found')
}
const {
nb_frames: numInputFramesStr,
start_time: startTime = 0,
duration: durationStr,
} = videoStreamMetaData
const duration = durationStr === undefined ? undefined : +durationStr
if (!duration || !Number.isFinite(duration)) {
throw new TypeError(`ffprobe returned invalid duration: ${durationStr}`)
}
if (numFrames === 1) {
const frame = await getFrameAt({
path,
time: startTime + duration / 2,
ffmpegCommandOptions,
configureFfmpeg,
})
return [frame]
}
const numInputFrames = numInputFramesStr === undefined ? undefined : +numInputFramesStr
if (!numInputFrames || !Number.isFinite(numInputFrames)) {
throw new TypeError(`ffprobe returned invalid nb_frames: ${numInputFramesStr}`)
}
const frameIndices = getFrameIndices(numInputFrames, numFrames)
const filterSelect = frameIndices.map(i => `eq(n\\,${i})`).join('+')
const outStream = new StreamWithFrames()
const vcodec = 'mjpeg'
return new Promise<Buffer[]>((resolve, reject) => {
const proc = ffmpeg(path, ffmpegCommandOptions)
.output(outStream, {
end: true,
})
.noAudio()
// .setStartTime(startTime)
.format('image2pipe') // -f
.videoCodec(vcodec) // -vcodec
.size('640x480')
// .fpsOutput(numFrames / duration)
// .videoFilter(`fps=${numFrames / duration}`)
// .outputOption('-filter:v', `fps=${numFrames / duration}`)
.videoFilter(`select='${filterSelect}'`)
.outputOption('-vsync', 'vfr') // make the above work
.outputOption('-q:v', '2')
.on('error', reject)
.on('end', () => {
resolve(outStream.buffers)
})
configureFfmpeg?.(proc)
proc.run()
})
}