mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-08-23 23:10:17 +05:30
Add src/main/VideoOperations.js with pure argument-builder functions
(buildConvertArgs, buildCompressArgs, buildTrimArgs, buildFramesArgs,
buildGifArgs) and a single executeOperation entry point that spawns
ffmpeg via dependency-injected execFileFn, mirroring AudioOperations.js.
Wire ipcMain.handle('process-video-operation', ...) in main.js using
getFFmpegPath() and sanitizeErrorMessage(). Update preload.js's
ALLOWED_SEND_CHANNELS: remove 6 stale video-* channel names, add
process-video-operation.
Amit Haridas
156 lines
5.0 KiB
JavaScript
156 lines
5.0 KiB
JavaScript
const fs = require('fs');
|
|
const path = require('path');
|
|
const VideoOperations = require('../../src/main/VideoOperations');
|
|
|
|
describe('VideoOperations argument builders', () => {
|
|
test('buildConvertArgs builds correct ffmpeg args', () => {
|
|
const args = VideoOperations.buildConvertArgs({ inputPath: '/a.mov', outputPath: '/b.mp4' });
|
|
expect(args).toEqual(['-i', '/a.mov', '-y', '/b.mp4']);
|
|
});
|
|
|
|
test('buildCompressArgs builds correct compress args with given crf', () => {
|
|
const args = VideoOperations.buildCompressArgs({
|
|
inputPath: '/a.mp4',
|
|
outputPath: '/b.mp4',
|
|
crf: 23,
|
|
});
|
|
expect(args).toEqual(['-i', '/a.mp4', '-vcodec', 'libx264', '-crf', '23', '-y', '/b.mp4']);
|
|
});
|
|
|
|
test('buildCompressArgs defaults crf to 28', () => {
|
|
const args = VideoOperations.buildCompressArgs({ inputPath: '/a.mp4', outputPath: '/b.mp4' });
|
|
expect(args).toEqual(['-i', '/a.mp4', '-vcodec', 'libx264', '-crf', '28', '-y', '/b.mp4']);
|
|
});
|
|
|
|
test('buildCompressArgs rejects out-of-range crf', () => {
|
|
expect(() =>
|
|
VideoOperations.buildCompressArgs({ inputPath: '/a.mp4', outputPath: '/b.mp4', crf: 52 })
|
|
).toThrow();
|
|
});
|
|
|
|
test('buildCompressArgs rejects non-integer crf', () => {
|
|
expect(() =>
|
|
VideoOperations.buildCompressArgs({ inputPath: '/a.mp4', outputPath: '/b.mp4', crf: 12.5 })
|
|
).toThrow();
|
|
});
|
|
|
|
test('buildTrimArgs builds correct trim args', () => {
|
|
const args = VideoOperations.buildTrimArgs({
|
|
inputPath: '/a.mp4',
|
|
outputPath: '/b.mp4',
|
|
startTime: 5,
|
|
duration: 10,
|
|
});
|
|
expect(args).toEqual(['-i', '/a.mp4', '-ss', '5', '-t', '10', '-y', '/b.mp4']);
|
|
});
|
|
|
|
test('buildTrimArgs rejects non-finite startTime', () => {
|
|
expect(() =>
|
|
VideoOperations.buildTrimArgs({
|
|
inputPath: '/a.mp4',
|
|
outputPath: '/b.mp4',
|
|
startTime: NaN,
|
|
duration: 10,
|
|
})
|
|
).toThrow('Invalid trim range');
|
|
});
|
|
|
|
test('buildFramesArgs builds correct frame extraction args with given fps', () => {
|
|
const args = VideoOperations.buildFramesArgs({
|
|
inputPath: '/a.mp4',
|
|
outputDir: '/out',
|
|
fps: 2,
|
|
});
|
|
expect(args).toEqual(['-i', '/a.mp4', '-vf', 'fps=2', path.join('/out', 'frame-%04d.png')]);
|
|
});
|
|
|
|
test('buildFramesArgs defaults fps to 1', () => {
|
|
const args = VideoOperations.buildFramesArgs({ inputPath: '/a.mp4', outputDir: '/out' });
|
|
expect(args).toEqual(['-i', '/a.mp4', '-vf', 'fps=1', path.join('/out', 'frame-%04d.png')]);
|
|
});
|
|
|
|
test('buildFramesArgs rejects non-positive fps', () => {
|
|
expect(() =>
|
|
VideoOperations.buildFramesArgs({ inputPath: '/a.mp4', outputDir: '/out', fps: 0 })
|
|
).toThrow();
|
|
});
|
|
|
|
test('buildFramesArgs rejects non-finite fps', () => {
|
|
expect(() =>
|
|
VideoOperations.buildFramesArgs({ inputPath: '/a.mp4', outputDir: '/out', fps: Infinity })
|
|
).toThrow();
|
|
});
|
|
|
|
test('buildGifArgs builds correct gif args with defaults', () => {
|
|
const args = VideoOperations.buildGifArgs({ inputPath: '/a.mp4', outputPath: '/b.gif' });
|
|
expect(args).toEqual([
|
|
'-i',
|
|
'/a.mp4',
|
|
'-vf',
|
|
'fps=10,scale=480:-1:flags=lanczos',
|
|
'-y',
|
|
'/b.gif',
|
|
]);
|
|
});
|
|
|
|
test('buildGifArgs builds correct gif args with given fps and width', () => {
|
|
const args = VideoOperations.buildGifArgs({
|
|
inputPath: '/a.mp4',
|
|
outputPath: '/b.gif',
|
|
fps: 15,
|
|
width: 320,
|
|
});
|
|
expect(args).toEqual([
|
|
'-i',
|
|
'/a.mp4',
|
|
'-vf',
|
|
'fps=15,scale=320:-1:flags=lanczos',
|
|
'-y',
|
|
'/b.gif',
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('VideoOperations.executeOperation', () => {
|
|
test('convert calls execFileFn with ffmpeg path and args, resolves success', async () => {
|
|
const execFileFn = (cmd, args, opts, cb) => cb(null, '', '');
|
|
const result = await VideoOperations.executeOperation(
|
|
'convert',
|
|
{ inputPath: '/a.mov', outputPath: '/b.mp4' },
|
|
{ ffmpegPath: '/usr/bin/ffmpeg', execFileFn }
|
|
);
|
|
expect(result.success).toBe(true);
|
|
expect(result.outputPath).toBe('/b.mp4');
|
|
});
|
|
|
|
test('frames creates the output directory before spawning ffmpeg and resolves success', async () => {
|
|
const mkdirSpy = jest.spyOn(fs, 'mkdirSync').mockImplementation(() => {});
|
|
const execFileFn = jest.fn((cmd, args, opts, cb) => cb(null, '', ''));
|
|
|
|
const result = await VideoOperations.executeOperation(
|
|
'frames',
|
|
{ inputPath: '/a.mp4', outputDir: '/out', fps: 1 },
|
|
{ ffmpegPath: '/usr/bin/ffmpeg', execFileFn }
|
|
);
|
|
|
|
expect(mkdirSpy).toHaveBeenCalledWith('/out', { recursive: true });
|
|
expect(mkdirSpy.mock.invocationCallOrder[0]).toBeLessThan(
|
|
execFileFn.mock.invocationCallOrder[0]
|
|
);
|
|
expect(result.success).toBe(true);
|
|
expect(result.outputDir).toBe('/out');
|
|
|
|
mkdirSpy.mockRestore();
|
|
});
|
|
|
|
test('unknown operation rejects', async () => {
|
|
await expect(
|
|
VideoOperations.executeOperation(
|
|
'bogus',
|
|
{},
|
|
{ ffmpegPath: '/usr/bin/ffmpeg', execFileFn: () => {} }
|
|
)
|
|
).rejects.toThrow();
|
|
});
|
|
});
|