| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322 |
- /**
- * @fileoverview Tests for FFmpeg video format conversion functionality
- * @author GrabZilla Development Team
- * @version 2.1.0
- * @since 2024-01-01
- */
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
- import path from 'path';
- import fs from 'fs';
- // Mock the ffmpeg converter module
- const mockFFmpegConverter = {
- isAvailable: vi.fn(),
- convertVideo: vi.fn(),
- cancelConversion: vi.fn(),
- cancelAllConversions: vi.fn(),
- getActiveConversions: vi.fn(),
- getVideoDuration: vi.fn()
- };
- // Mock child_process
- vi.mock('child_process', () => ({
- spawn: vi.fn()
- }));
- // Mock fs with proper default export
- vi.mock('fs', async (importOriginal) => {
- const actual = await importOriginal();
- return {
- ...actual,
- default: {
- existsSync: vi.fn(),
- statSync: vi.fn(),
- unlinkSync: vi.fn()
- },
- existsSync: vi.fn(),
- statSync: vi.fn(),
- unlinkSync: vi.fn()
- };
- });
- describe('FFmpeg Format Conversion', () => {
- beforeEach(() => {
- vi.clearAllMocks();
- });
- afterEach(() => {
- vi.restoreAllMocks();
- });
- describe('Format Support', () => {
- it('should support H.264 format conversion', () => {
- const formats = ['H264', 'ProRes', 'DNxHR', 'Audio only'];
- expect(formats).toContain('H264');
- });
- it('should support ProRes format conversion', () => {
- const formats = ['H264', 'ProRes', 'DNxHR', 'Audio only'];
- expect(formats).toContain('ProRes');
- });
- it('should support DNxHR format conversion', () => {
- const formats = ['H264', 'ProRes', 'DNxHR', 'Audio only'];
- expect(formats).toContain('DNxHR');
- });
- it('should support audio-only extraction', () => {
- const formats = ['H264', 'ProRes', 'DNxHR', 'Audio only'];
- expect(formats).toContain('Audio only');
- });
- });
- describe('Encoding Parameters', () => {
- it('should use appropriate H.264 CRF values for different qualities', () => {
- const crfMap = {
- '4K': '18',
- '1440p': '20',
- '1080p': '23',
- '720p': '25',
- '480p': '28'
- };
- Object.entries(crfMap).forEach(([quality, expectedCrf]) => {
- expect(expectedCrf).toMatch(/^\d+$/);
- expect(parseInt(expectedCrf)).toBeGreaterThan(0);
- expect(parseInt(expectedCrf)).toBeLessThan(52);
- });
- });
- it('should use appropriate ProRes profiles for different qualities', () => {
- const profileMap = {
- '4K': '3',
- '1440p': '2',
- '1080p': '2',
- '720p': '1',
- '480p': '0'
- };
- Object.entries(profileMap).forEach(([quality, profile]) => {
- expect(profile).toMatch(/^[0-3]$/);
- });
- });
- it('should use appropriate DNxHR profiles for different qualities', () => {
- const profileMap = {
- '4K': 'dnxhr_hqx',
- '1440p': 'dnxhr_hq',
- '1080p': 'dnxhr_sq',
- '720p': 'dnxhr_lb',
- '480p': 'dnxhr_lb'
- };
- Object.entries(profileMap).forEach(([quality, profile]) => {
- expect(profile).toMatch(/^dnxhr_/);
- });
- });
- });
- describe('File Extensions', () => {
- it('should use correct file extensions for each format', () => {
- const extensionMap = {
- 'H264': 'mp4',
- 'ProRes': 'mov',
- 'DNxHR': 'mov',
- 'Audio only': 'm4a'
- };
- Object.entries(extensionMap).forEach(([format, extension]) => {
- expect(extension).toMatch(/^[a-z0-9]+$/);
- });
- });
- });
- describe('Progress Tracking', () => {
- it('should parse FFmpeg progress output correctly', () => {
- const progressLine = 'frame= 123 fps= 25 q=28.0 size= 1024kB time=00:00:05.00 bitrate=1677.7kbits/s speed=1.02x';
-
- // Mock progress parsing
- const timeMatch = progressLine.match(/time=(\d{2}):(\d{2}):(\d{2}\.\d{2})/);
- expect(timeMatch).toBeTruthy();
-
- if (timeMatch) {
- const hours = parseInt(timeMatch[1]);
- const minutes = parseInt(timeMatch[2]);
- const seconds = parseFloat(timeMatch[3]);
- const totalSeconds = hours * 3600 + minutes * 60 + seconds;
-
- expect(totalSeconds).toBe(5);
- }
- });
- it('should calculate progress percentage correctly', () => {
- const processedTime = 30; // 30 seconds processed
- const totalDuration = 120; // 2 minutes total
- const expectedProgress = Math.round((processedTime / totalDuration) * 100);
-
- expect(expectedProgress).toBe(25);
- });
- it('should handle progress updates during conversion', () => {
- const progressCallback = vi.fn();
- const progressData = {
- conversionId: 1,
- progress: 50,
- timeProcessed: 60,
- speed: 1.5,
- size: 2048
- };
- progressCallback(progressData);
- expect(progressCallback).toHaveBeenCalledWith(progressData);
- });
- });
- describe('Error Handling', () => {
- it('should handle missing input file error', () => {
- fs.existsSync.mockReturnValue(false);
-
- const options = {
- inputPath: '/nonexistent/file.mp4',
- outputPath: '/output/file.mp4',
- format: 'H264',
- quality: '1080p'
- };
- expect(() => {
- if (!fs.existsSync(options.inputPath)) {
- throw new Error(`Input file not found: ${options.inputPath}`);
- }
- }).toThrow('Input file not found');
- });
- it('should handle missing FFmpeg binary error', () => {
- mockFFmpegConverter.isAvailable.mockReturnValue(false);
-
- expect(() => {
- if (!mockFFmpegConverter.isAvailable()) {
- throw new Error('FFmpeg binary not found');
- }
- }).toThrow('FFmpeg binary not found');
- });
- it('should handle conversion process errors', () => {
- const errorMessage = 'Invalid data found when processing input';
-
- expect(() => {
- throw new Error(errorMessage);
- }).toThrow('Invalid data found when processing input');
- });
- });
- describe('Conversion Management', () => {
- it('should track active conversions', () => {
- const activeConversions = [
- { conversionId: 1, pid: 12345 },
- { conversionId: 2, pid: 12346 }
- ];
- mockFFmpegConverter.getActiveConversions.mockReturnValue(activeConversions);
-
- const result = mockFFmpegConverter.getActiveConversions();
- expect(result).toHaveLength(2);
- expect(result[0]).toHaveProperty('conversionId');
- expect(result[0]).toHaveProperty('pid');
- });
- it('should cancel specific conversion', () => {
- mockFFmpegConverter.cancelConversion.mockReturnValue(true);
-
- const result = mockFFmpegConverter.cancelConversion(1);
- expect(result).toBe(true);
- expect(mockFFmpegConverter.cancelConversion).toHaveBeenCalledWith(1);
- });
- it('should cancel all conversions', () => {
- mockFFmpegConverter.cancelAllConversions.mockReturnValue(2);
-
- const result = mockFFmpegConverter.cancelAllConversions();
- expect(result).toBe(2);
- expect(mockFFmpegConverter.cancelAllConversions).toHaveBeenCalled();
- });
- });
- describe('Integration with Download Process', () => {
- it('should integrate conversion into download workflow', async () => {
- const downloadOptions = {
- url: 'https://youtube.com/watch?v=test',
- quality: '1080p',
- format: 'H264',
- savePath: '/downloads',
- cookieFile: null
- };
- // Mock successful download
- const downloadResult = {
- success: true,
- filename: 'test_video.mp4',
- filePath: '/downloads/test_video.mp4'
- };
- // Mock successful conversion
- mockFFmpegConverter.convertVideo.mockResolvedValue({
- success: true,
- outputPath: '/downloads/test_video_h264.mp4',
- fileSize: 1024000
- });
- // Simulate the conversion requirement check
- const requiresConversion = downloadOptions.format !== 'None';
- expect(requiresConversion).toBe(true);
- if (requiresConversion) {
- const conversionResult = await mockFFmpegConverter.convertVideo({
- inputPath: downloadResult.filePath,
- outputPath: '/downloads/test_video_h264.mp4',
- format: downloadOptions.format,
- quality: downloadOptions.quality
- });
- expect(conversionResult.success).toBe(true);
- expect(mockFFmpegConverter.convertVideo).toHaveBeenCalled();
- }
- });
- it('should handle conversion progress in download workflow', () => {
- const progressUpdates = [];
- const mockProgressCallback = (data) => {
- progressUpdates.push(data);
- };
- // Simulate download progress (0-70%)
- mockProgressCallback({ stage: 'download', progress: 35, status: 'downloading' });
-
- // Simulate conversion progress (70-100%)
- mockProgressCallback({ stage: 'conversion', progress: 85, status: 'converting' });
-
- // Simulate completion
- mockProgressCallback({ stage: 'complete', progress: 100, status: 'completed' });
- expect(progressUpdates).toHaveLength(3);
- expect(progressUpdates[0].stage).toBe('download');
- expect(progressUpdates[1].stage).toBe('conversion');
- expect(progressUpdates[2].stage).toBe('complete');
- });
- });
- describe('Quality Settings', () => {
- it('should apply quality-specific encoding parameters', () => {
- const qualitySettings = {
- '4K': { crf: '18', profile: '3' },
- '1080p': { crf: '23', profile: '2' },
- '720p': { crf: '25', profile: '1' }
- };
- Object.entries(qualitySettings).forEach(([quality, settings]) => {
- expect(parseInt(settings.crf)).toBeGreaterThan(0);
- expect(parseInt(settings.profile)).toBeGreaterThanOrEqual(0);
- });
- });
- });
- });
|