@ -1,53 +1,60 @@
const { join } = require ( 'path' ) ;
const isDev = require ( 'electron-is-dev' ) ;
const readline = require ( 'readline' ) ;
const stringToStream = require ( 'string-to-stream' ) ;
const execa = require ( 'execa' ) ;
import { join } from 'path' ;
import readline from 'readline' ;
import stringToStream from 'string-to-stream' ;
import { BufferEncodingOption , execa , ExecaChildProcess , Options as ExecaOptions } from 'execa' ;
import assert from 'assert' ;
import { Readable } from 'stream' ;
// eslint-disable-next-line import/no-extraneous-dependencies
import { app } from 'electron' ;
const { platform , arch , isWindows , isMac , isLinux } = require ( './util' ) ;
import { platform , arch , isWindows , isMac , isLinux } from './util.js' ;
import { CaptureFormat , Html5ifyMode , Waveform } from '../../types.js' ;
import isDev from './isDev.js' ;
const runningFfmpegs = new Set () ;
const runningFfmpegs = new Set <ExecaChildProcess < Buffer > > () ;
// setInterval(() => console.log(runningFfmpegs.size), 1000);
let customFfPath ;
let customFfPath : string | undefined ;
// Note that this does not work on MAS because of sandbox restrictions
function setCustomFfPath ( path ) {
export function setCustomFfPath ( path : string | undefined ) {
customFfPath = path ;
}
function getFfCommandLine ( cmd , args ) {
export function getFfCommandLine ( cmd : string , args : readonly string [ ] ) {
return ` ${ cmd } ${ args . map ( ( arg ) = > ( /[^\w-]/ . test ( arg ) ? ` ' ${ arg } ' ` : arg ) ) . join ( ' ' ) } ` ;
}
function getFfPath ( cmd ) {
function getFfPath ( cmd : string ) {
const exeName = isWindows ? ` ${ cmd } .exe ` : cmd ;
if ( customFfPath ) return join ( customFfPath , exeName ) ;
if ( isDev ) {
const components = [ 'ffmpeg' , ` ${ platform } - ${ arch } ` ] ;
if ( isWindows || isLinux ) components . push ( 'lib' ) ;
components . push ( exeName ) ;
return join ( . . . components ) ;
if ( app . isPackaged ) {
return join ( process . resourcesPath , exeName ) ;
}
return join ( process . resourcesPath , exeName ) ;
// local dev
const components = [ 'ffmpeg' , ` ${ platform } - ${ arch } ` ] ;
if ( isWindows || isLinux ) components . push ( 'lib' ) ;
components . push ( exeName ) ;
return join ( . . . components ) ;
}
const getFfprobePath = ( ) = > getFfPath ( 'ffprobe' ) ;
const getFfmpegPath = ( ) = > getFfPath ( 'ffmpeg' ) ;
export const getFfmpegPath = ( ) = > getFfPath ( 'ffmpeg' ) ;
function abortFfmpegs() {
export function abortFfmpegs() {
console . log ( 'Aborting' , runningFfmpegs . size , 'ffmpeg process(es)' ) ;
runningFfmpegs . forEach ( ( process ) = > {
process . kill ( 'SIGTERM' , { forceKillAfterTimeout : 10000 } ) ;
} ) ;
}
function handleProgress ( process , durationIn , onProgress , customMatcher = ( ) = > undefined ) {
function handleProgress ( process : { stderr : Readable | null } , durationIn : number | undefined , onProgress : ( a : number ) = > void , customMatcher : ( a : string ) = > void = ( ) = > undefined ) {
if ( ! onProgress ) return ;
if ( process . stderr == null ) return ;
onProgress ( 0 ) ;
const rl = readline . createInterface ( { input : process.stderr } ) ;
@ -61,20 +68,19 @@ function handleProgress(process, durationIn, onProgress, customMatcher = () => u
// eslint-disable-next-line unicorn/better-regex
if ( ! match ) match = line . match ( /(?:size|Lsize)=\s*[^\s]+\s+time=\s*([^\s]+)\s+/ ) ;
if ( ! match ) {
// @ts-expect-error todo
customMatcher ( line ) ;
return ;
}
const timeStr = match [ 1 ] ;
// console.log(timeStr);
const match2 = timeStr . match ( /^(\d+):(\d+):(\d+)\.(\d+)$/ ) ;
const match2 = timeStr ! . match ( /^(\d+):(\d+):(\d+)\.(\d+)$/ ) ;
if ( ! match2 ) throw new Error ( ` Invalid time from ffmpeg progress ${ timeStr } ` ) ;
const h = parseInt ( match2 [ 1 ] , 10 ) ;
const m = parseInt ( match2 [ 2 ] , 10 ) ;
const s = parseInt ( match2 [ 3 ] , 10 ) ;
const cs = parseInt ( match2 [ 4 ] , 10 ) ;
const h = parseInt ( match2 [ 1 ] ! , 10 ) ;
const m = parseInt ( match2 [ 2 ] ! , 10 ) ;
const s = parseInt ( match2 [ 3 ] ! , 10 ) ;
const cs = parseInt ( match2 [ 4 ] ! , 10 ) ;
const time = ( ( ( h * 60 ) + m ) * 60 + s ) + cs / 100 ;
// console.log(time);
@ -93,18 +99,22 @@ function handleProgress(process, durationIn, onProgress, customMatcher = () => u
} ) ;
}
// @ts-expect-error todo
function getExecaOptions ( { env , . . . customExecaOptions } = { } ) {
const execaOptions = { . . . customExecaOptions , env : { . . . env } } ;
function getExecaOptions ( { env , . . . customExecaOptions } : Omit < ExecaOptions < BufferEncodingOption > , 'buffer' > = { } ) {
const execaOptions : Omit < ExecaOptions < BufferEncodingOption > , 'buffer' > = { . . . customExecaOptions , encoding : 'buffer' } ;
// https://github.com/mifi/lossless-cut/issues/1143#issuecomment-1500883489
if ( isLinux && ! isDev && ! customFfPath ) execaOptions . env . LD_LIBRARY_PATH = process . resourcesPath ;
if ( isLinux && ! isDev && ! customFfPath ) {
return {
. . . execaOptions ,
env : { . . . env , LD_LIBRARY_PATH : process.resourcesPath } ,
} ;
}
return execaOptions ;
}
// todo collect warnings from ffmpeg output and show them after export? example: https://github.com/mifi/lossless-cut/issues/1469
function runFfmpegProcess ( args , customExecaOptions , { logCli = true } = { } ) {
function runFfmpegProcess ( args : readonly string [ ] , customExecaOptions? : Omit < ExecaOptions < BufferEncodingOption > , 'encoding' > , additionalOptions ? : { logCli? : boolean } ) {
const ffmpegPath = getFfmpegPath ( ) ;
if ( logCli) console . log ( getFfCommandLine ( 'ffmpeg' , args ) ) ;
if ( additionalOptions? . logCli) console . log ( getFfCommandLine ( 'ffmpeg' , args ) ) ;
const process = execa ( ffmpegPath , args , getExecaOptions ( customExecaOptions ) ) ;
@ -121,23 +131,29 @@ function runFfmpegProcess(args, customExecaOptions, { logCli = true } = {}) {
return process ;
}
async function runFfmpegConcat ( { ffmpegArgs , concatTxt , totalDuration , onProgress } ) {
export async function runFfmpegConcat ( { ffmpegArgs , concatTxt , totalDuration , onProgress } : {
ffmpegArgs : string [ ] , concatTxt : string , totalDuration : number , onProgress : ( a : number ) = > void
} ) {
const process = runFfmpegProcess ( ffmpegArgs ) ;
handleProgress ( process , totalDuration , onProgress ) ;
assert ( process . stdin != null ) ;
stringToStream ( concatTxt ) . pipe ( process . stdin ) ;
return process ;
}
async function runFfmpegWithProgress ( { ffmpegArgs , duration , onProgress } ) {
export async function runFfmpegWithProgress ( { ffmpegArgs , duration , onProgress } : {
ffmpegArgs : string [ ] , duration : number | undefined , onProgress : ( a : number ) = > void ,
} ) {
const process = runFfmpegProcess ( ffmpegArgs ) ;
assert ( process . stderr != null ) ;
handleProgress ( process , duration , onProgress ) ;
return process ;
}
async function runFfprobe ( args , { timeout = isDev ? 10000 : 30000 } = { } ) {
export async function runFfprobe ( args : readonly string [ ] , { timeout = isDev ? 10000 : 30000 } = { } ) {
const ffprobePath = getFfprobePath ( ) ;
console . log ( getFfCommandLine ( 'ffprobe' , args ) ) ;
const ps = execa ( ffprobePath , args , getExecaOptions ( ) ) ;
@ -152,13 +168,14 @@ async function runFfprobe(args, { timeout = isDev ? 10000 : 30000 } = {}) {
}
}
/** @type {(a: any) => Promise<import('../types').Waveform>} */
async function renderWaveformPng ( { filePath , start , duration , color , streamIndex } ) {
export async function renderWaveformPng ( { filePath , start , duration , color , streamIndex } : {
filePath : string , start : number , duration : number , color : string , streamIndex : number ,
} ) : Promise < Waveform > {
const args1 = [
'-hide_banner' ,
'-i' , filePath ,
'-ss' , start ,
'-t' , duration ,
'-ss' , String ( start ) ,
'-t' , String ( duration ) ,
'-c' , 'copy' ,
'-vn' ,
'-map' , ` 0: ${ streamIndex } ` ,
@ -179,16 +196,18 @@ async function renderWaveformPng({ filePath, start, duration, color, streamIndex
console . log ( getFfCommandLine ( 'ffmpeg1' , args1 ) ) ;
console . log ( '|' , getFfCommandLine ( 'ffmpeg2' , args2 ) ) ;
let ps1 ;
let ps2 ;
let ps1 : ExecaChildProcess < Buffer > | undefined ;
let ps2 : ExecaChildProcess < Buffer > | undefined ;
try {
ps1 = runFfmpegProcess ( args1 , { encoding : null , buffer : false } , { logCli : false } ) ;
ps2 = runFfmpegProcess ( args2 , { encoding : null } , { logCli : false } ) ;
ps1 = runFfmpegProcess ( args1 , { buffer : false } , { logCli : false } ) ;
ps2 = runFfmpegProcess ( args2 , undefined , { logCli : false } ) ;
assert ( ps1 . stdout != null ) ;
assert ( ps2 . stdin != null ) ;
ps1 . stdout . pipe ( ps2 . stdin ) ;
const timer = setTimeout ( ( ) = > {
ps1 . kill ( ) ;
ps2 . kill ( ) ;
ps1 ? . kill ( ) ;
ps2 ? . kill ( ) ;
console . warn ( 'ffmpeg timed out' ) ;
} , 10000 ) ;
@ -209,14 +228,14 @@ async function renderWaveformPng({ filePath, start, duration, color, streamIndex
}
}
const getInputSeekArgs = ( { filePath , from , to } ) = > [
const getInputSeekArgs = ( { filePath , from , to } : { filePath : string , from ? : number | undefined , to? : number | undefined } ) = > [
. . . ( from != null ? [ '-ss' , from . toFixed ( 5 ) ] : [ ] ) ,
'-i' , filePath ,
. . . ( to != null ? [ '-t' , ( to - from ) . toFixed ( 5 ) ] : [ ] ) ,
. . . ( from != null && to != null ? [ '-t' , ( to - from ) . toFixed ( 5 ) ] : [ ] ) ,
] ;
function mapTimesToSegments ( times ) {
const segments = [ ] ;
export function mapTimesToSegments ( times : number [ ] ) {
const segments : { start : number , end : number | undefined } [ ] = [ ] ;
for ( let i = 0 ; i < times . length ; i += 1 ) {
const start = times [ i ] ;
const end = times [ i + 1 ] ;
@ -225,32 +244,35 @@ function mapTimesToSegments(times) {
return segments ;
}
const getSegmentOffset = ( from ) = > ( from != null ? from : 0 ) ;
const getSegmentOffset = ( from ? : number ) = > ( from != null ? from : 0 ) ;
function adjustSegmentsWithOffset ( { segments , from } ) {
function adjustSegmentsWithOffset ( { segments , from } : { segments : { start : number , end : number | undefined } [ ] , from ? : number | undefined } ) {
const offset = getSegmentOffset ( from ) ;
return segments . map ( ( { start , end } ) = > ( { start : start + offset , end : end != null ? end + offset : end } ) ) ;
}
// https://stackoverflow.com/questions/35675529/using-ffmpeg-how-to-do-a-scene-change-detection-with-timecode
async function detectSceneChanges ( { filePath , minChange , onProgress , from , to } ) {
export async function detectSceneChanges ( { filePath , minChange , onProgress , from , to } : {
filePath : string , minChange : number | string , onProgress : ( p : number ) = > void , from : number , to : number ,
} ) {
const args = [
'-hide_banner' ,
. . . getInputSeekArgs ( { filePath , from , to } ) ,
'-filter_complex' , ` select='gt(scene, ${ minChange } )',metadata=print:file=- ` ,
'-f' , 'null' , '-' ,
] ;
const process = runFfmpegProcess ( args , { encoding: null , buffer: false } ) ;
const process = runFfmpegProcess ( args , { buffer: false } ) ;
const times = [ 0 ] ;
handleProgress ( process , to - from , onProgress ) ;
assert ( process . stdout != null ) ;
const rl = readline . createInterface ( { input : process.stdout } ) ;
rl . on ( 'line' , ( line ) = > {
// eslint-disable-next-line unicorn/better-regex
const match = line . match ( /^frame:\d+\s+pts:\d+\s+pts_time:([\d.]+)/ ) ;
if ( ! match ) return ;
const time = parseFloat ( match [ 1 ] ) ;
const time = parseFloat ( match [ 1 ] ! ) ;
// @ts-expect-error todo
if ( Number . isNaN ( time ) || time <= times . at ( - 1 ) ) return ;
times . push ( time ) ;
@ -263,89 +285,134 @@ async function detectSceneChanges({ filePath, minChange, onProgress, from, to })
return adjustSegmentsWithOffset ( { segments , from } ) ;
}
async function detectIntervals ( { filePath , customArgs , onProgress , from , to , matchLineTokens } ) {
async function detectIntervals ( { filePath , customArgs , onProgress , from , to , matchLineTokens } : {
filePath : string , customArgs : string [ ] , onProgress : ( p : number ) = > void , from : number , to : number , matchLineTokens : ( line : string ) = > { start? : string | number | undefined , end? : string | number | undefined } ,
} ) {
const args = [
'-hide_banner' ,
. . . getInputSeekArgs ( { filePath , from , to } ) ,
. . . customArgs ,
'-f' , 'null' , '-' ,
] ;
const process = runFfmpegProcess ( args , { encoding : null , buffer : false } ) ;
const segments = [ ] ;
function customMatcher ( line ) {
const { start : startStr , end : endStr } = matchLineTokens ( line ) ;
const start = parseFloat ( startStr ) ;
const end = parseFloat ( endStr ) ;
if ( start == null || end == null || Number . isNaN ( start ) || Number . isNaN ( end ) ) return ;
segments . push ( { start , end } ) ;
const process = runFfmpegProcess ( args , { buffer : false } ) ;
const segments : { start : number , end : number } [ ] = [ ] ;
function customMatcher ( line : string ) {
const { start : startRaw , end : endRaw } = matchLineTokens ( line ) ;
if ( typeof startRaw === 'number' && typeof endRaw === 'number' ) {
if ( startRaw == null || endRaw == null ) return ;
if ( Number . isNaN ( startRaw ) || Number . isNaN ( endRaw ) ) return ;
segments . push ( { start : startRaw , end : endRaw } ) ;
} else if ( typeof startRaw === 'string' && typeof endRaw === 'string' ) {
if ( startRaw == null || endRaw == null ) return ;
const start = parseFloat ( startRaw ) ;
const end = parseFloat ( endRaw ) ;
if ( Number . isNaN ( start ) || Number . isNaN ( end ) ) return ;
segments . push ( { start , end } ) ;
} else {
throw new TypeError ( 'Invalid line match' ) ;
}
}
// @ts-expect-error todo
handleProgress ( process , to - from , onProgress , customMatcher ) ;
await process ;
return adjustSegmentsWithOffset ( { segments , from } ) ;
}
const mapFilterOptions = ( options ) = > Object . entries ( options ) . map ( ( [ key , value ] ) = > ` ${ key } = ${ value } ` ) . join ( ':' ) ;
const mapFilterOptions = ( options : Record < string , string > ) = > Object . entries ( options ) . map ( ( [ key , value ] ) = > ` ${ key } = ${ value } ` ) . join ( ':' ) ;
async function blackDetect ( { filePath , filterOptions , onProgress , from , to } ) {
function matchLineTokens ( line ) {
// eslint-disable-next-line unicorn/better-regex
const match = line . match ( /^[blackdetect\s*@\s*0x[0-9a-f]+] black_start:([\d\\.]+) black_end:([\d\\.]+) black_duration:[\d\\.]+/ ) ;
if ( ! match ) return { } ;
return {
start : parseFloat ( match [ 1 ] ) ,
end : parseFloat ( match [ 2 ] ) ,
} ;
}
export async function blackDetect ( { filePath , filterOptions , onProgress , from , to } : { filePath : string , filterOptions : Record < string , string > , onProgress : ( p : number ) = > void , from : number , to : number } ) {
const customArgs = [ '-vf' , ` blackdetect= ${ mapFilterOptions ( filterOptions ) } ` , '-an' ] ;
return detectIntervals ( { filePath , onProgress , from , to , matchLineTokens , customArgs } ) ;
return detectIntervals ( {
filePath ,
onProgress ,
from ,
to ,
matchLineTokens : ( line ) = > {
// eslint-disable-next-line unicorn/better-regex
const match = line . match ( /^[blackdetect\s*@\s*0x[0-9a-f]+] black_start:([\d\\.]+) black_end:([\d\\.]+) black_duration:[\d\\.]+/ ) ;
if ( ! match ) {
return {
start : undefined ,
end : undefined ,
} ;
}
return {
start : match [ 1 ] ,
end : match [ 2 ] ,
} ;
} ,
customArgs ,
} ) ;
}
async function silenceDetect ( { filePath , filterOptions , onProgress , from , to } ) {
function matchLineTokens ( line ) {
// eslint-disable-next-line unicorn/better-regex
const match = line . match ( /^[silencedetect\s*@\s*0x[0-9a-f]+] silence_end: ([\d\\.]+)[|\s]+silence_duration: ([\d\\.]+)/ ) ;
if ( ! match ) return { } ;
const end = parseFloat ( match [ 1 ] ) ;
const silenceDuration = parseFloat ( match [ 2 ] ) ;
if ( Number . isNaN ( end ) || Number . isNaN ( silenceDuration ) ) return { } ;
const start = end - silenceDuration ;
if ( start < 0 || end <= 0 || start >= end ) return { } ;
return {
start ,
end ,
} ;
}
const customArgs = [ '-af' , ` silencedetect= ${ mapFilterOptions ( filterOptions ) } ` , '-vn' ] ;
return detectIntervals ( { filePath , onProgress , from , to , matchLineTokens , customArgs } ) ;
export async function silenceDetect ( { filePath , filterOptions , onProgress , from , to } : {
filePath : string , filterOptions : Record < string , string > , onProgress : ( p : number ) = > void , from : number , to : number ,
} ) {
return detectIntervals ( {
filePath ,
onProgress ,
from ,
to ,
matchLineTokens : ( line ) = > {
// eslint-disable-next-line unicorn/better-regex
const match = line . match ( /^[silencedetect\s*@\s*0x[0-9a-f]+] silence_end: ([\d\\.]+)[|\s]+silence_duration: ([\d\\.]+)/ ) ;
if ( ! match ) {
return {
start : undefined ,
end : undefined ,
} ;
}
const end = parseFloat ( match [ 1 ] ! ) ;
const silenceDuration = parseFloat ( match [ 2 ] ! ) ;
if ( Number . isNaN ( end ) || Number . isNaN ( silenceDuration ) ) {
return {
start : undefined ,
end : undefined ,
} ;
}
const start = end - silenceDuration ;
if ( start < 0 || end <= 0 || start >= end ) {
return {
start : undefined ,
end : undefined ,
} ;
}
return {
start ,
end ,
} ;
} ,
customArgs : [ '-af' , ` silencedetect= ${ mapFilterOptions ( filterOptions ) } ` , '-vn' ] ,
} ) ;
}
function getFffmpegJpegQuality ( quality ) {
function getFffmpegJpegQuality ( quality : number ) {
// Normal range for JPEG is 2-31 with 31 being the worst quality.
const qMin = 2 ;
const qMax = 31 ;
return Math . min ( Math . max ( qMin , quality , Math . round ( ( 1 - quality ) * ( qMax - qMin ) + qMin ) ) , qMax ) ;
}
function getQualityOpts ( { captureFormat , quality } ) {
if ( captureFormat === 'jpeg' ) return [ '-q:v' , getFffmpegJpegQuality ( quality ) ] ;
if ( captureFormat === 'webp' ) return [ '-q:v' , Math . max ( 0 , Math . min ( 100 , Math . round ( quality * 100 ) ) ) ] ;
function getQualityOpts ( { captureFormat , quality } : { captureFormat : CaptureFormat , quality : number } ) {
if ( captureFormat === 'jpeg' ) return [ '-q:v' , String ( getFffmpegJpegQuality ( quality ) ) ] ;
if ( captureFormat === 'webp' ) return [ '-q:v' , String( Math. max ( 0 , Math . min ( 100 , Math . round ( quality * 100 ) ) ) ) ] ;
return [ ] ;
}
function getCodecOpts ( captureFormat ) {
function getCodecOpts ( captureFormat : CaptureFormat ) {
if ( captureFormat === 'webp' ) return [ '-c:v' , 'libwebp' ] ; // else we get only a single file for webp https://github.com/mifi/lossless-cut/issues/1693
return [ ] ;
}
async function captureFrames ( { from , to , videoPath , outPathTemplate , quality , filter , framePts , onProgress , captureFormat } ) {
export async function captureFrames ( { from , to , videoPath , outPathTemplate , quality , filter , framePts , onProgress , captureFormat } : {
from : number , to : number , videoPath : string , outPathTemplate : string , quality : number , filter? : string | undefined , framePts? : boolean | undefined , onProgress : ( p : number ) = > void , captureFormat : CaptureFormat ,
} ) {
const args = [
'-ss' , from ,
'-ss' , String ( from ) ,
'-i' , videoPath ,
'-t' , Math. max ( 0 , to - from ) ,
'-t' , String( Math. max ( 0 , to - from ) ) ,
. . . getQualityOpts ( { captureFormat , quality } ) ,
. . . ( filter != null ? [ '-vf' , filter ] : [ ] ) ,
// https://superuser.com/questions/1336285/use-ffmpeg-for-thumbnail-selections
@ -356,39 +423,43 @@ async function captureFrames({ from, to, videoPath, outPathTemplate, quality, fi
'-y' , outPathTemplate ,
] ;
const process = runFfmpegProcess ( args , { encoding: null , buffer: false } ) ;
const process = runFfmpegProcess ( args , { buffer: false } ) ;
handleProgress ( process , to - from , onProgress ) ;
await process ;
}
async function captureFrame ( { timestamp , videoPath , outPath , quality } ) {
export async function captureFrame ( { timestamp , videoPath , outPath , quality } : {
timestamp : number , videoPath : string , outPath : string , quality : number ,
} ) {
const ffmpegQuality = getFffmpegJpegQuality ( quality ) ;
await runFfmpegProcess ( [
'-ss' , timestamp ,
'-ss' , String ( timestamp ) ,
'-i' , videoPath ,
'-vframes' , '1' ,
'-q:v' , ffmpegQuality ,
'-q:v' , String ( ffmpegQuality ) ,
'-y' , outPath ,
] ) ;
}
async function readFormatData ( filePath ) {
async function readFormatData ( filePath : string ) {
console . log ( 'readFormatData' , filePath ) ;
const { stdout } = await runFfprobe ( [
'-of' , 'json' , '-show_format' , '-i' , filePath , '-hide_banner' ,
] ) ;
return JSON . parse ( stdout ) . format ;
return JSON . parse ( stdout as unknown as string ) . format ;
}
async function getDuration ( filePath ) {
export async function getDuration ( filePath : string ) {
return parseFloat ( ( await readFormatData ( filePath ) ) . duration ) ;
}
async function html5ify ( { outPath , filePath : filePathArg , speed , hasAudio , hasVideo , onProgress } ) {
export async function html5ify ( { outPath , filePath : filePathArg , speed , hasAudio , hasVideo , onProgress } : {
outPath : string , filePath : string , speed : Html5ifyMode , hasAudio : boolean , hasVideo : boolean , onProgress : ( p : number ) = > void ,
} ) {
let audio ;
if ( hasAudio ) {
if ( speed === 'slowest' ) audio = 'hq' ;
@ -492,14 +563,14 @@ async function html5ify({ outPath, filePath: filePathArg, speed, hasAudio, hasVi
if ( duration ) handleProgress ( process , duration , onProgress ) ;
const { stdout } = await process ;
console . log ( stdout );
console . log ( stdout .toString ( 'utf8' ) );
}
function readOneJpegFrame ( { path , seekTo , videoStreamIndex } ) {
export function readOneJpegFrame ( { path , seekTo , videoStreamIndex } : { path : string , seekTo : number , videoStreamIndex : number } ) {
const args = [
'-hide_banner' , '-loglevel' , 'error' ,
'-ss' , seekTo ,
'-ss' , String ( seekTo ) ,
'-noautorotate' ,
@ -516,17 +587,19 @@ function readOneJpegFrame({ path, seekTo, videoStreamIndex }) {
// console.log(args);
return runFfmpegProcess ( args , { encoding : 'buffer' } , { logCli : true } ) ;
return runFfmpegProcess ( args , undefined , { logCli : true } ) ;
}
const enableLog = false ;
const encode = true ;
function createMediaSourceProcess ( { path , videoStreamIndex , audioStreamIndex , seekTo , size , fps } ) {
export function createMediaSourceProcess ( { path , videoStreamIndex , audioStreamIndex , seekTo , size , fps } : {
path : string , videoStreamIndex? : number | undefined , audioStreamIndex? : number | undefined , seekTo : number , size? : number | undefined , fps? : number | undefined ,
} ) {
function getVideoFilters() {
if ( videoStreamIndex == null ) return [ ] ;
const filters = [ ] ;
const filters : string [ ] = [ ] ;
if ( fps != null ) filters . push ( ` fps= ${ fps } ` ) ;
if ( size != null ) filters . push ( ` scale= ${ size } : ${ size } :flags=lanczos:force_original_aspect_ratio=decrease ` ) ;
if ( filters . length === 0 ) return [ ] ;
@ -547,7 +620,7 @@ function createMediaSourceProcess({ path, videoStreamIndex, audioStreamIndex, se
'-vsync' , 'passthrough' ,
'-ss' , seekTo ,
'-ss' , String ( seekTo ) ,
'-noautorotate' ,
@ -582,27 +655,5 @@ function createMediaSourceProcess({ path, videoStreamIndex, audioStreamIndex, se
return execa ( getFfmpegPath ( ) , args , { encoding : null , buffer : false , stderr : enableLog ? 'inherit' : 'pipe' } ) ;
}
// Don't pass complex objects over the bridge
const runFfmpeg = async ( . . . args ) = > runFfmpegProcess ( . . . args ) ;
module .exports = {
setCustomFfPath ,
abortFfmpegs ,
getFfmpegPath ,
runFfprobe ,
runFfmpeg ,
runFfmpegConcat ,
runFfmpegWithProgress ,
renderWaveformPng ,
mapTimesToSegments ,
detectSceneChanges ,
captureFrames ,
captureFrame ,
getFfCommandLine ,
html5ify ,
getDuration ,
readOneJpegFrame ,
blackDetect ,
silenceDetect ,
createMediaSourceProcess ,
} ;
// Don't pass complex objects over the bridge (process)
export const runFfmpeg = async ( . . . args : Parameters < typeof runFfmpegProcess > ) = > runFfmpegProcess ( . . . args ) ;