@@ -0,0 +1,24 @@ | |||||
type ProcessEvent = { type: string, phase: string, command?: string }; | |||||
type ProcessEventCallback = (event: ProcessEvent) => void; | |||||
type ErrorEventCallback = (event: Error) => void; | |||||
export interface VideoClipEventEmitter extends NodeJS.EventEmitter { | |||||
process(): void; | |||||
on(eventType: 'process', callback: ProcessEventCallback): this; | |||||
on(eventType: 'error', callback: ErrorEventCallback): this; | |||||
on(eventType: 'end', callback: ErrorEventCallback): this; | |||||
} | |||||
export const FILE_TYPES: Record<string, string> = { | |||||
mkv: 'video/x-matroska', | |||||
webm: 'video/webm', | |||||
mp4: 'video/mp4', | |||||
}; | |||||
export interface CreateBaseClipperParams { | |||||
url: string; | |||||
start?: number | string; | |||||
end?: number | string; | |||||
} |
@@ -0,0 +1,16 @@ | |||||
export const convertSecondsToDuration = (s: number) => { | |||||
const milliseconds = (s - Math.floor(s)) * 1000; | |||||
const seconds = s % 60; | |||||
const minutes = Math.floor(s / 60) % 60; | |||||
const hours = Math.floor(s / 3600); | |||||
const sss = milliseconds.toString().padStart(3, '0'); | |||||
const ss = seconds.toString().padStart(2, '0'); | |||||
const mm = minutes.toString().padStart(2, '0'); | |||||
const hh = hours.toString().padStart(2, '0'); | |||||
return `${hh}:${mm}:${ss}.${sss}`; | |||||
}; | |||||
export const convertDurationToSeconds = (d: string) => { | |||||
const [hh, mm, ss] = d.split(':'); | |||||
return (Number(hh) * 3600) + (Number(mm) * 60) + Number(ss); | |||||
}; |
@@ -1,266 +1,27 @@ | |||||
import { spawnSync } from 'child_process'; | |||||
import { unlinkSync, readFileSync } from 'fs'; | |||||
import { EventEmitter } from 'events'; | |||||
import { CreateYouTubeClipperParams, YouTubeVideoClipEventEmitter } from './video-types/youtube'; | |||||
import { VideoClipEventEmitter } from './common'; | |||||
export enum VideoType { | export enum VideoType { | ||||
YOUTUBE = 'youtube', | YOUTUBE = 'youtube', | ||||
} | } | ||||
interface CreateBaseClipperParams { | |||||
url: string; | |||||
start?: number | string; | |||||
end?: number | string; | |||||
postprocessorExecutablePath?: string; | |||||
} | |||||
interface CreateYouTubeClipperParams extends CreateBaseClipperParams { | |||||
downloaderExecutablePath?: string; | |||||
} | |||||
export interface CreateVideoClipperParams extends CreateYouTubeClipperParams { | export interface CreateVideoClipperParams extends CreateYouTubeClipperParams { | ||||
type: VideoType, | type: VideoType, | ||||
} | } | ||||
const convertSecondsToDuration = (s: number) => { | |||||
const milliseconds = (s - Math.floor(s)) * 1000; | |||||
const seconds = s % 60; | |||||
const minutes = Math.floor(s / 60) % 60; | |||||
const hours = Math.floor(s / 3600); | |||||
const sss = milliseconds.toString().padStart(3, '0'); | |||||
const ss = seconds.toString().padStart(2, '0'); | |||||
const mm = minutes.toString().padStart(2, '0'); | |||||
const hh = hours.toString().padStart(2, '0'); | |||||
return `${hh}:${mm}:${ss}.${sss}`; | |||||
}; | |||||
const convertDurationToSeconds = (d: string) => { | |||||
const [hh, mm, ss] = d.split(':'); | |||||
return (Number(hh) * 3600) + (Number(mm) * 60) + Number(ss); | |||||
}; | |||||
const normalizeStartSeconds = (start?: number | string) => { | |||||
if (typeof start === 'undefined') { | |||||
return 0; | |||||
} | |||||
if (typeof start === 'string') { | |||||
return convertDurationToSeconds(start); | |||||
} | |||||
return start; | |||||
}; | |||||
const getCacheFilename = () => { | |||||
const time = new Date().getTime(); | |||||
return `output-${time}.mkv`; | |||||
}; | |||||
export interface VideoClipEventEmitter extends EventEmitter { | |||||
process(): void; | |||||
} | |||||
class YouTubeVideoClipEventEmitter extends EventEmitter implements VideoClipEventEmitter { | |||||
constructor(private readonly params: CreateYouTubeClipperParams) { | |||||
super(); | |||||
} | |||||
private constructDefaultDownloadArgs(cacheFilename: string) { | |||||
const startSeconds = normalizeStartSeconds(this.params.start); | |||||
const downloadArgs = [ | |||||
'--youtube-skip-dash-manifest', | |||||
'-f', 'bestvideo+bestaudio', | |||||
'--fragment-retries', 'infinite', | |||||
]; | |||||
if (typeof this.params.end !== 'undefined') { | |||||
downloadArgs.push('--download-sections'); | |||||
const endSeconds = ( | |||||
typeof this.params.end === 'string' | |||||
? convertDurationToSeconds(this.params.end) | |||||
: this.params.end | |||||
); | |||||
downloadArgs.push( | |||||
`*${convertSecondsToDuration(startSeconds)}-${convertSecondsToDuration(endSeconds)}`, | |||||
); | |||||
downloadArgs.push('-o'); | |||||
downloadArgs.push(cacheFilename); | |||||
downloadArgs.push('--force-keyframes-at-cuts'); | |||||
if (startSeconds > 0) { | |||||
downloadArgs.push(`${this.params.url}?t=${startSeconds}`); | |||||
} else { | |||||
downloadArgs.push(this.params.url); | |||||
} | |||||
} else if (startSeconds > 0) { | |||||
downloadArgs.push('--download-sections'); | |||||
downloadArgs.push(`*${convertSecondsToDuration(startSeconds)}-inf`); | |||||
downloadArgs.push('-o'); | |||||
downloadArgs.push(cacheFilename); | |||||
downloadArgs.push('--force-keyframes-at-cuts'); | |||||
downloadArgs.push(`${this.params.url}?t=${startSeconds}`); | |||||
} else { | |||||
downloadArgs.push('-o'); | |||||
downloadArgs.push(cacheFilename); | |||||
downloadArgs.push(this.params.url); | |||||
} | |||||
return downloadArgs; | |||||
} | |||||
private doDownloadProcess(downloadArgs: string[]) { | |||||
this.emit('process', { | |||||
type: 'download', | |||||
phase: 'start', | |||||
command: `${this.params.downloaderExecutablePath as string} ${downloadArgs.join(' ')}`, | |||||
}); | |||||
const downloaderProcess = spawnSync( | |||||
this.params.downloaderExecutablePath as string, | |||||
downloadArgs, | |||||
); | |||||
if (downloaderProcess.error) { | |||||
this.emit('error', new Error('Downloader error.', { cause: downloaderProcess.error })); | |||||
this.emit('end', null); | |||||
return null; | |||||
} | |||||
return downloaderProcess; | |||||
} | |||||
private processDefault() { | |||||
const cacheFilename = getCacheFilename(); // todo label this on the cache | |||||
const downloadArgs = this.constructDefaultDownloadArgs(cacheFilename); | |||||
const downloaderProcess = this.doDownloadProcess(downloadArgs); | |||||
if (!downloaderProcess) { | |||||
return; | |||||
} | |||||
this.emit('process', { | |||||
type: 'download', | |||||
phase: 'success', | |||||
}); | |||||
const output = readFileSync(cacheFilename); | |||||
unlinkSync(cacheFilename); | |||||
this.emit('success', { | |||||
type: 'video/webm', | |||||
output, | |||||
}); | |||||
this.emit('end'); | |||||
} | |||||
private processWithPostprocessing() { | |||||
const downloadArgs = [ | |||||
'--youtube-skip-dash-manifest', | |||||
'-f', 'bestvideo+bestaudio', | |||||
'-g', this.params.url, | |||||
]; | |||||
const downloaderProcess = this.doDownloadProcess(downloadArgs); | |||||
if (!downloaderProcess) { | |||||
return; | |||||
} | |||||
const [videoUrlStream, audioUrlStream] = downloaderProcess.stdout.toString('utf-8').split('\n'); | |||||
this.emit('process', { | |||||
type: 'download', | |||||
phase: 'success', | |||||
streamUrls: { | |||||
video: [videoUrlStream], | |||||
audio: [audioUrlStream], | |||||
}, | |||||
}); | |||||
const startSeconds = normalizeStartSeconds(this.params.start); | |||||
const ffmpegProcessVideoStreamArgs = [ | |||||
'-ss', (this.params.start ?? '00:00:00').toString(), | |||||
'-i', videoUrlStream, | |||||
]; | |||||
const ffmpegProcessAudioStreamArgs = [ | |||||
'-ss', (this.params.start ?? '00:00:00').toString(), | |||||
'-i', audioUrlStream, | |||||
]; | |||||
if (typeof this.params.end !== 'undefined') { | |||||
// -to flag is broken on video stream in ffmpeg..... | |||||
const endSeconds = ( | |||||
typeof this.params.end === 'string' | |||||
? convertDurationToSeconds(this.params.end) | |||||
: this.params.end | |||||
); | |||||
const difference = endSeconds - startSeconds; | |||||
const clipDuration = convertSecondsToDuration(difference); | |||||
ffmpegProcessVideoStreamArgs.push('-t'); | |||||
ffmpegProcessVideoStreamArgs.push(clipDuration); | |||||
ffmpegProcessAudioStreamArgs.push('-t'); | |||||
ffmpegProcessAudioStreamArgs.push(clipDuration); | |||||
} | |||||
const cacheFilename = getCacheFilename(); // todo label this on the cache | |||||
const ffmpegProcessArgs = [ | |||||
...ffmpegProcessVideoStreamArgs, | |||||
...ffmpegProcessAudioStreamArgs, | |||||
'-map', '0:v', | |||||
'-map', '1:a', | |||||
cacheFilename, | |||||
]; | |||||
this.emit('process', { | |||||
type: 'postprocess', | |||||
phase: 'start', | |||||
command: `${this.params.postprocessorExecutablePath as string} ${ffmpegProcessArgs.join(' ')}`, | |||||
}); | |||||
const ffmpegProcess = spawnSync( | |||||
this.params.postprocessorExecutablePath as string, | |||||
ffmpegProcessArgs, | |||||
); | |||||
if (ffmpegProcess.error) { | |||||
this.emit('error', new Error('ffmpeg error.', { cause: ffmpegProcess.error })); | |||||
this.emit('end'); | |||||
return; | |||||
} | |||||
this.emit('process', { | |||||
type: 'postprocess', | |||||
phase: 'success', | |||||
}); | |||||
const output = readFileSync(cacheFilename); | |||||
unlinkSync(cacheFilename); | |||||
this.emit('success', { | |||||
type: 'video/webm', | |||||
output, | |||||
}); | |||||
this.emit('end'); | |||||
} | |||||
process() { | |||||
if (!this.params.downloaderExecutablePath) { | |||||
this.emit('error', new Error('Downloader not found.')); | |||||
this.emit('end', null); | |||||
return; | |||||
} | |||||
if (this.params.postprocessorExecutablePath) { | |||||
this.processWithPostprocessing(); | |||||
return; | |||||
} | |||||
this.processDefault(); | |||||
} | |||||
} | |||||
export const createVideoClipper = (params: CreateVideoClipperParams) => { | |||||
export const createVideoClipper = (params: CreateVideoClipperParams): VideoClipEventEmitter => { | |||||
const { | const { | ||||
type: videoType, | type: videoType, | ||||
url, | url, | ||||
start, | start, | ||||
end, | end, | ||||
downloaderExecutablePath, | downloaderExecutablePath, | ||||
postprocessorExecutablePath, | |||||
} = params; | } = params; | ||||
switch (videoType as string) { | switch (videoType as string) { | ||||
case VideoType.YOUTUBE: | case VideoType.YOUTUBE: | ||||
return new YouTubeVideoClipEventEmitter({ | return new YouTubeVideoClipEventEmitter({ | ||||
downloaderExecutablePath, | downloaderExecutablePath, | ||||
postprocessorExecutablePath, | |||||
url, | url, | ||||
start, | start, | ||||
end, | end, | ||||
@@ -271,3 +32,5 @@ export const createVideoClipper = (params: CreateVideoClipperParams) => { | |||||
throw new TypeError(`Invalid video type: "${videoType}". Valid values are: ${JSON.stringify(Object.values(VideoType))}`); | throw new TypeError(`Invalid video type: "${videoType}". Valid values are: ${JSON.stringify(Object.values(VideoType))}`); | ||||
}; | }; | ||||
export { VideoClipEventEmitter }; |
@@ -0,0 +1,142 @@ | |||||
import { EventEmitter } from 'events'; | |||||
import { spawnSync } from 'child_process'; | |||||
import { readFileSync, unlinkSync } from 'fs'; | |||||
import { CreateBaseClipperParams, FILE_TYPES, VideoClipEventEmitter } from '../common'; | |||||
import { convertDurationToSeconds, convertSecondsToDuration } from '../duration'; | |||||
const normalizeStartSeconds = (start?: number | string) => { | |||||
if (typeof start === 'undefined') { | |||||
return 0; | |||||
} | |||||
if (typeof start === 'string') { | |||||
return convertDurationToSeconds(start); | |||||
} | |||||
return start; | |||||
}; | |||||
const FORMAT_DEFAULT_ARGS = [ | |||||
'--youtube-skip-dash-manifest', | |||||
'-f', 'bestvideo+bestaudio', | |||||
]; | |||||
const getFileExtension = (downloaderExecutablePath: string, url: string) => { | |||||
const result = spawnSync( | |||||
downloaderExecutablePath, | |||||
[ | |||||
...FORMAT_DEFAULT_ARGS, | |||||
'--print', 'filename', | |||||
'-o', '%(ext)s', | |||||
url, | |||||
], | |||||
); | |||||
if (result.error) { | |||||
throw result.error; | |||||
} | |||||
return result.stdout.toString('utf-8'); | |||||
}; | |||||
const constructDownloadSectionsRegex = (start?: number | string, end?: number | string) => { | |||||
const startSeconds = normalizeStartSeconds(start); | |||||
if (typeof end !== 'undefined') { | |||||
const endSeconds = ( | |||||
typeof end === 'string' | |||||
? convertDurationToSeconds(end) | |||||
: end | |||||
); | |||||
return `*${convertSecondsToDuration(startSeconds)}-${convertSecondsToDuration(endSeconds)}`; | |||||
} | |||||
if (startSeconds > 0) { | |||||
return `*${convertSecondsToDuration(startSeconds)}-inf`; | |||||
} | |||||
return null; | |||||
}; | |||||
const constructDefaultDownloadArgs = ( | |||||
outputFilename: string, | |||||
url: string, | |||||
start?: number | string, | |||||
end?: number | string, | |||||
) => { | |||||
const defaultDownloadArgs = [ | |||||
...FORMAT_DEFAULT_ARGS, | |||||
'-o', outputFilename, | |||||
]; | |||||
const downloadSectionsRegex = constructDownloadSectionsRegex(start, end); | |||||
if (typeof downloadSectionsRegex === 'string') { | |||||
return [ | |||||
...defaultDownloadArgs, | |||||
'--force-keyframes-at-cuts', | |||||
'--download-sections', downloadSectionsRegex, | |||||
url, | |||||
]; | |||||
} | |||||
return [ | |||||
...defaultDownloadArgs, | |||||
url, | |||||
]; | |||||
}; | |||||
export interface CreateYouTubeClipperParams extends CreateBaseClipperParams { | |||||
downloaderExecutablePath?: string; | |||||
} | |||||
export class YouTubeVideoClipEventEmitter extends EventEmitter implements VideoClipEventEmitter { | |||||
constructor(private readonly params: CreateYouTubeClipperParams) { | |||||
super(); | |||||
} | |||||
process() { | |||||
if (!this.params.downloaderExecutablePath) { | |||||
this.emit('error', new Error('Downloader not found.')); | |||||
this.emit('end'); | |||||
return; | |||||
} | |||||
const fileExtension = getFileExtension( | |||||
this.params.downloaderExecutablePath, | |||||
this.params.url, | |||||
); | |||||
const cacheFilename = `output.${fileExtension}`; // todo label this on the cache | |||||
const downloadArgs = constructDefaultDownloadArgs( | |||||
cacheFilename, | |||||
this.params.url, | |||||
this.params.start, | |||||
this.params.end, | |||||
); | |||||
this.emit('process', { | |||||
type: 'download', | |||||
phase: 'start', | |||||
command: `${this.params.downloaderExecutablePath} ${downloadArgs.join(' ')}`, | |||||
}); | |||||
const downloaderProcess = spawnSync( | |||||
this.params.downloaderExecutablePath, | |||||
downloadArgs, | |||||
); | |||||
if (downloaderProcess.error) { | |||||
this.emit('error', downloaderProcess.error); | |||||
this.emit('end'); | |||||
return; | |||||
} | |||||
this.emit('process', { | |||||
type: 'download', | |||||
phase: 'success', | |||||
}); | |||||
const output = readFileSync(cacheFilename); | |||||
unlinkSync(cacheFilename); | |||||
this.emit('success', { | |||||
type: FILE_TYPES[fileExtension], | |||||
output, | |||||
}); | |||||
this.emit('end'); | |||||
} | |||||
} |
@@ -1,113 +1,27 @@ | |||||
import { | import { | ||||
describe, it, expect, vi, beforeEach, Mock, afterEach, | |||||
describe, | |||||
it, | |||||
expect, | |||||
vi, | |||||
beforeEach, | |||||
Mock, | |||||
afterEach, | |||||
} from 'vitest'; | } from 'vitest'; | ||||
import { spawnSync } from 'child_process'; | import { spawnSync } from 'child_process'; | ||||
import { readFileSync, unlinkSync } from 'fs'; | import { readFileSync, unlinkSync } from 'fs'; | ||||
import * as webVideoClipCore from '../src'; | import * as webVideoClipCore from '../src'; | ||||
import { VideoClipEventEmitter } from '../src'; | |||||
vi.mock('child_process', () => ({ | |||||
spawnSync: vi.fn(), | |||||
})); | |||||
vi.mock('child_process'); | |||||
vi.mock('fs', () => ({ | |||||
readFileSync: vi.fn(), | |||||
unlinkSync: vi.fn(), | |||||
})); | |||||
vi.mock('fs'); | |||||
describe('createVideoClipper', () => { | describe('createVideoClipper', () => { | ||||
beforeEach(() => { | beforeEach(() => { | ||||
(readFileSync as Mock).mockReturnValueOnce(Buffer.from('')); | (readFileSync as Mock).mockReturnValueOnce(Buffer.from('')); | ||||
}); | }); | ||||
describe('with postprocessing', () => { | |||||
let clipper: VideoClipEventEmitter; | |||||
beforeEach(() => { | |||||
clipper = webVideoClipCore.createVideoClipper({ | |||||
downloaderExecutablePath: 'yt-dlp', | |||||
postprocessorExecutablePath: 'ffmpeg', | |||||
type: webVideoClipCore.VideoType.YOUTUBE, | |||||
url: 'https://www.youtube.com/watch?v=BaW_jenozKc', | |||||
start: 0, | |||||
end: 0, | |||||
}); | |||||
}); | |||||
afterEach(() => { | |||||
(spawnSync as Mock).mockReset(); | |||||
}); | |||||
it('calls the downloader function', () => new Promise((done) => { | |||||
(spawnSync as Mock) | |||||
.mockReturnValueOnce({ stdout: Buffer.from('') }) | |||||
.mockReturnValueOnce({}); | |||||
clipper.on('end', done); | |||||
clipper.process(); | |||||
expect(spawnSync).nthCalledWith(1, 'yt-dlp', expect.anything()); | |||||
})); | |||||
it('emits downloader errors', () => new Promise((done) => { | |||||
const causeError = new Error('generic downloader message'); | |||||
(spawnSync as Mock) | |||||
.mockReturnValueOnce({ error: causeError }) | |||||
.mockReturnValueOnce({}); | |||||
clipper.on('error', (err: Error) => { | |||||
expect(err.cause).toHaveProperty('message', causeError.message); | |||||
}); | |||||
clipper.on('end', done); | |||||
clipper.process(); | |||||
})); | |||||
it('calls the postprocess function', () => new Promise((done) => { | |||||
(spawnSync as Mock) | |||||
.mockReturnValueOnce({ stdout: Buffer.from('') }) | |||||
.mockReturnValueOnce({}); | |||||
clipper.on('end', done); | |||||
clipper.process(); | |||||
expect(spawnSync).nthCalledWith(2, 'ffmpeg', expect.anything()); | |||||
})); | |||||
it('emits postprocess errors', () => new Promise((done) => { | |||||
const causeError = new Error('generic postprocessor message'); | |||||
(spawnSync as Mock) | |||||
.mockReturnValueOnce({ stdout: Buffer.from('') }) | |||||
.mockReturnValueOnce({ error: causeError }); | |||||
clipper.on('error', (err: Error) => { | |||||
expect(err.cause).toHaveProperty('message', causeError.message); | |||||
}); | |||||
clipper.on('end', done); | |||||
clipper.process(); | |||||
})); | |||||
it('calls the buffer extract function', () => new Promise((done) => { | |||||
(spawnSync as Mock) | |||||
.mockReturnValueOnce({ stdout: Buffer.from('') }) | |||||
.mockReturnValueOnce({}); | |||||
clipper.on('end', done); | |||||
clipper.process(); | |||||
expect(readFileSync).toBeCalled(); | |||||
})); | |||||
it('calls the cleanup function', () => new Promise((done) => { | |||||
(spawnSync as Mock) | |||||
.mockReturnValueOnce({ stdout: Buffer.from('') }) | |||||
.mockReturnValueOnce({}); | |||||
clipper.on('end', done); | |||||
clipper.process(); | |||||
expect(unlinkSync).toBeCalled(); | |||||
})); | |||||
}); | |||||
describe('without postprocessing', () => { | describe('without postprocessing', () => { | ||||
let clipper: VideoClipEventEmitter; | |||||
let clipper: webVideoClipCore.VideoClipEventEmitter; | |||||
beforeEach(() => { | beforeEach(() => { | ||||
clipper = webVideoClipCore.createVideoClipper({ | clipper = webVideoClipCore.createVideoClipper({ | ||||
downloaderExecutablePath: 'yt-dlp', | downloaderExecutablePath: 'yt-dlp', | ||||
@@ -124,23 +38,24 @@ describe('createVideoClipper', () => { | |||||
it('calls the downloader function', () => new Promise((done) => { | it('calls the downloader function', () => new Promise((done) => { | ||||
(spawnSync as Mock) | (spawnSync as Mock) | ||||
.mockReturnValueOnce({ stdout: Buffer.from('') }) | |||||
.mockReturnValueOnce({}); | |||||
.mockReturnValueOnce({ stdout: Buffer.from('mkv') }) | |||||
.mockReturnValueOnce({ stdout: Buffer.from('') }); | |||||
clipper.on('end', done); | clipper.on('end', done); | ||||
clipper.process(); | clipper.process(); | ||||
expect(spawnSync).toBeCalledTimes(1); | |||||
expect(spawnSync).nthCalledWith(1, 'yt-dlp', expect.anything()); | |||||
expect(spawnSync).toBeCalledTimes(2); | |||||
expect(spawnSync).nthCalledWith(1, 'yt-dlp', expect.arrayContaining(['--print'])); | |||||
expect(spawnSync).nthCalledWith(2, 'yt-dlp', expect.anything()); | |||||
})); | })); | ||||
it('emits downloader errors', () => new Promise((done) => { | it('emits downloader errors', () => new Promise((done) => { | ||||
const causeError = new Error('generic downloader message'); | const causeError = new Error('generic downloader message'); | ||||
(spawnSync as Mock) | (spawnSync as Mock) | ||||
.mockReturnValueOnce({ error: causeError }) | |||||
.mockReturnValueOnce({}); | |||||
.mockReturnValueOnce({ stdout: Buffer.from('mkv') }) | |||||
.mockReturnValueOnce({ error: causeError }); | |||||
clipper.on('error', (err: Error) => { | clipper.on('error', (err: Error) => { | ||||
expect(err.cause).toHaveProperty('message', causeError.message); | |||||
expect(err).toHaveProperty('message', causeError.message); | |||||
}); | }); | ||||
clipper.on('end', done); | clipper.on('end', done); | ||||
clipper.process(); | clipper.process(); | ||||
@@ -148,8 +63,8 @@ describe('createVideoClipper', () => { | |||||
it('calls the buffer extract function', () => new Promise((done) => { | it('calls the buffer extract function', () => new Promise((done) => { | ||||
(spawnSync as Mock) | (spawnSync as Mock) | ||||
.mockReturnValueOnce({ stdout: Buffer.from('') }) | |||||
.mockReturnValueOnce({}); | |||||
.mockReturnValueOnce({ stdout: Buffer.from('mkv') }) | |||||
.mockReturnValueOnce({ stdout: Buffer.from('') }); | |||||
clipper.on('end', done); | clipper.on('end', done); | ||||
clipper.process(); | clipper.process(); | ||||
@@ -159,8 +74,8 @@ describe('createVideoClipper', () => { | |||||
it('calls the cleanup function', () => new Promise((done) => { | it('calls the cleanup function', () => new Promise((done) => { | ||||
(spawnSync as Mock) | (spawnSync as Mock) | ||||
.mockReturnValueOnce({ stdout: Buffer.from('') }) | |||||
.mockReturnValueOnce({}); | |||||
.mockReturnValueOnce({ stdout: Buffer.from('mkv') }) | |||||
.mockReturnValueOnce({ stdout: Buffer.from('') }); | |||||
clipper.on('end', done); | clipper.on('end', done); | ||||
clipper.process(); | clipper.process(); | ||||
@@ -14,8 +14,7 @@ | |||||
"noImplicitReturns": true, | "noImplicitReturns": true, | ||||
"noFallthroughCasesInSwitch": true, | "noFallthroughCasesInSwitch": true, | ||||
"moduleResolution": "node", | "moduleResolution": "node", | ||||
"jsx": "react", | |||||
"esModuleInterop": true, | "esModuleInterop": true, | ||||
"target": "es2018" | |||||
"target": "es2022" | |||||
} | } | ||||
} | } |
@@ -1,6 +1,6 @@ | |||||
{ | { | ||||
"exclude": ["node_modules"], | "exclude": ["node_modules"], | ||||
"include": ["src", "types"], | |||||
"include": ["src", "types", "test"], | |||||
"compilerOptions": { | "compilerOptions": { | ||||
"module": "ESNext", | "module": "ESNext", | ||||
"lib": ["ESNext"], | "lib": ["ESNext"], | ||||
@@ -14,8 +14,7 @@ | |||||
"noImplicitReturns": true, | "noImplicitReturns": true, | ||||
"noFallthroughCasesInSwitch": true, | "noFallthroughCasesInSwitch": true, | ||||
"moduleResolution": "node", | "moduleResolution": "node", | ||||
"jsx": "react", | |||||
"esModuleInterop": true, | "esModuleInterop": true, | ||||
"target": "es2018" | |||||
"target": "es2022" | |||||
} | } | ||||
} | } |