ffmpeg-conversion.test.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. /**
  2. * @fileoverview Tests for FFmpeg video format conversion functionality
  3. * @author GrabZilla Development Team
  4. * @version 2.1.0
  5. * @since 2024-01-01
  6. */
  7. import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
  8. import path from 'path';
  9. import fs from 'fs';
  10. // Mock the ffmpeg converter module
  11. const mockFFmpegConverter = {
  12. isAvailable: vi.fn(),
  13. convertVideo: vi.fn(),
  14. cancelConversion: vi.fn(),
  15. cancelAllConversions: vi.fn(),
  16. getActiveConversions: vi.fn(),
  17. getVideoDuration: vi.fn()
  18. };
  19. // Mock child_process
  20. vi.mock('child_process', () => ({
  21. spawn: vi.fn()
  22. }));
  23. // Mock fs with proper default export
  24. vi.mock('fs', async (importOriginal) => {
  25. const actual = await importOriginal();
  26. return {
  27. ...actual,
  28. default: {
  29. existsSync: vi.fn(),
  30. statSync: vi.fn(),
  31. unlinkSync: vi.fn()
  32. },
  33. existsSync: vi.fn(),
  34. statSync: vi.fn(),
  35. unlinkSync: vi.fn()
  36. };
  37. });
  38. describe('FFmpeg Format Conversion', () => {
  39. beforeEach(() => {
  40. vi.clearAllMocks();
  41. });
  42. afterEach(() => {
  43. vi.restoreAllMocks();
  44. });
  45. describe('Format Support', () => {
  46. it('should support H.264 format conversion', () => {
  47. const formats = ['H264', 'ProRes', 'DNxHR', 'Audio only'];
  48. expect(formats).toContain('H264');
  49. });
  50. it('should support ProRes format conversion', () => {
  51. const formats = ['H264', 'ProRes', 'DNxHR', 'Audio only'];
  52. expect(formats).toContain('ProRes');
  53. });
  54. it('should support DNxHR format conversion', () => {
  55. const formats = ['H264', 'ProRes', 'DNxHR', 'Audio only'];
  56. expect(formats).toContain('DNxHR');
  57. });
  58. it('should support audio-only extraction', () => {
  59. const formats = ['H264', 'ProRes', 'DNxHR', 'Audio only'];
  60. expect(formats).toContain('Audio only');
  61. });
  62. });
  63. describe('Encoding Parameters', () => {
  64. it('should use appropriate H.264 CRF values for different qualities', () => {
  65. const crfMap = {
  66. '4K': '18',
  67. '1440p': '20',
  68. '1080p': '23',
  69. '720p': '25',
  70. '480p': '28'
  71. };
  72. Object.entries(crfMap).forEach(([quality, expectedCrf]) => {
  73. expect(expectedCrf).toMatch(/^\d+$/);
  74. expect(parseInt(expectedCrf)).toBeGreaterThan(0);
  75. expect(parseInt(expectedCrf)).toBeLessThan(52);
  76. });
  77. });
  78. it('should use appropriate ProRes profiles for different qualities', () => {
  79. const profileMap = {
  80. '4K': '3',
  81. '1440p': '2',
  82. '1080p': '2',
  83. '720p': '1',
  84. '480p': '0'
  85. };
  86. Object.entries(profileMap).forEach(([quality, profile]) => {
  87. expect(profile).toMatch(/^[0-3]$/);
  88. });
  89. });
  90. it('should use appropriate DNxHR profiles for different qualities', () => {
  91. const profileMap = {
  92. '4K': 'dnxhr_hqx',
  93. '1440p': 'dnxhr_hq',
  94. '1080p': 'dnxhr_sq',
  95. '720p': 'dnxhr_lb',
  96. '480p': 'dnxhr_lb'
  97. };
  98. Object.entries(profileMap).forEach(([quality, profile]) => {
  99. expect(profile).toMatch(/^dnxhr_/);
  100. });
  101. });
  102. });
  103. describe('File Extensions', () => {
  104. it('should use correct file extensions for each format', () => {
  105. const extensionMap = {
  106. 'H264': 'mp4',
  107. 'ProRes': 'mov',
  108. 'DNxHR': 'mov',
  109. 'Audio only': 'm4a'
  110. };
  111. Object.entries(extensionMap).forEach(([format, extension]) => {
  112. expect(extension).toMatch(/^[a-z0-9]+$/);
  113. });
  114. });
  115. });
  116. describe('Progress Tracking', () => {
  117. it('should parse FFmpeg progress output correctly', () => {
  118. const progressLine = 'frame= 123 fps= 25 q=28.0 size= 1024kB time=00:00:05.00 bitrate=1677.7kbits/s speed=1.02x';
  119. // Mock progress parsing
  120. const timeMatch = progressLine.match(/time=(\d{2}):(\d{2}):(\d{2}\.\d{2})/);
  121. expect(timeMatch).toBeTruthy();
  122. if (timeMatch) {
  123. const hours = parseInt(timeMatch[1]);
  124. const minutes = parseInt(timeMatch[2]);
  125. const seconds = parseFloat(timeMatch[3]);
  126. const totalSeconds = hours * 3600 + minutes * 60 + seconds;
  127. expect(totalSeconds).toBe(5);
  128. }
  129. });
  130. it('should calculate progress percentage correctly', () => {
  131. const processedTime = 30; // 30 seconds processed
  132. const totalDuration = 120; // 2 minutes total
  133. const expectedProgress = Math.round((processedTime / totalDuration) * 100);
  134. expect(expectedProgress).toBe(25);
  135. });
  136. it('should handle progress updates during conversion', () => {
  137. const progressCallback = vi.fn();
  138. const progressData = {
  139. conversionId: 1,
  140. progress: 50,
  141. timeProcessed: 60,
  142. speed: 1.5,
  143. size: 2048
  144. };
  145. progressCallback(progressData);
  146. expect(progressCallback).toHaveBeenCalledWith(progressData);
  147. });
  148. });
  149. describe('Error Handling', () => {
  150. it('should handle missing input file error', () => {
  151. fs.existsSync.mockReturnValue(false);
  152. const options = {
  153. inputPath: '/nonexistent/file.mp4',
  154. outputPath: '/output/file.mp4',
  155. format: 'H264',
  156. quality: '1080p'
  157. };
  158. expect(() => {
  159. if (!fs.existsSync(options.inputPath)) {
  160. throw new Error(`Input file not found: ${options.inputPath}`);
  161. }
  162. }).toThrow('Input file not found');
  163. });
  164. it('should handle missing FFmpeg binary error', () => {
  165. mockFFmpegConverter.isAvailable.mockReturnValue(false);
  166. expect(() => {
  167. if (!mockFFmpegConverter.isAvailable()) {
  168. throw new Error('FFmpeg binary not found');
  169. }
  170. }).toThrow('FFmpeg binary not found');
  171. });
  172. it('should handle conversion process errors', () => {
  173. const errorMessage = 'Invalid data found when processing input';
  174. expect(() => {
  175. throw new Error(errorMessage);
  176. }).toThrow('Invalid data found when processing input');
  177. });
  178. });
  179. describe('Conversion Management', () => {
  180. it('should track active conversions', () => {
  181. const activeConversions = [
  182. { conversionId: 1, pid: 12345 },
  183. { conversionId: 2, pid: 12346 }
  184. ];
  185. mockFFmpegConverter.getActiveConversions.mockReturnValue(activeConversions);
  186. const result = mockFFmpegConverter.getActiveConversions();
  187. expect(result).toHaveLength(2);
  188. expect(result[0]).toHaveProperty('conversionId');
  189. expect(result[0]).toHaveProperty('pid');
  190. });
  191. it('should cancel specific conversion', () => {
  192. mockFFmpegConverter.cancelConversion.mockReturnValue(true);
  193. const result = mockFFmpegConverter.cancelConversion(1);
  194. expect(result).toBe(true);
  195. expect(mockFFmpegConverter.cancelConversion).toHaveBeenCalledWith(1);
  196. });
  197. it('should cancel all conversions', () => {
  198. mockFFmpegConverter.cancelAllConversions.mockReturnValue(2);
  199. const result = mockFFmpegConverter.cancelAllConversions();
  200. expect(result).toBe(2);
  201. expect(mockFFmpegConverter.cancelAllConversions).toHaveBeenCalled();
  202. });
  203. });
  204. describe('Integration with Download Process', () => {
  205. it('should integrate conversion into download workflow', async () => {
  206. const downloadOptions = {
  207. url: 'https://youtube.com/watch?v=test',
  208. quality: '1080p',
  209. format: 'H264',
  210. savePath: '/downloads',
  211. cookieFile: null
  212. };
  213. // Mock successful download
  214. const downloadResult = {
  215. success: true,
  216. filename: 'test_video.mp4',
  217. filePath: '/downloads/test_video.mp4'
  218. };
  219. // Mock successful conversion
  220. mockFFmpegConverter.convertVideo.mockResolvedValue({
  221. success: true,
  222. outputPath: '/downloads/test_video_h264.mp4',
  223. fileSize: 1024000
  224. });
  225. // Simulate the conversion requirement check
  226. const requiresConversion = downloadOptions.format !== 'None';
  227. expect(requiresConversion).toBe(true);
  228. if (requiresConversion) {
  229. const conversionResult = await mockFFmpegConverter.convertVideo({
  230. inputPath: downloadResult.filePath,
  231. outputPath: '/downloads/test_video_h264.mp4',
  232. format: downloadOptions.format,
  233. quality: downloadOptions.quality
  234. });
  235. expect(conversionResult.success).toBe(true);
  236. expect(mockFFmpegConverter.convertVideo).toHaveBeenCalled();
  237. }
  238. });
  239. it('should handle conversion progress in download workflow', () => {
  240. const progressUpdates = [];
  241. const mockProgressCallback = (data) => {
  242. progressUpdates.push(data);
  243. };
  244. // Simulate download progress (0-70%)
  245. mockProgressCallback({ stage: 'download', progress: 35, status: 'downloading' });
  246. // Simulate conversion progress (70-100%)
  247. mockProgressCallback({ stage: 'conversion', progress: 85, status: 'converting' });
  248. // Simulate completion
  249. mockProgressCallback({ stage: 'complete', progress: 100, status: 'completed' });
  250. expect(progressUpdates).toHaveLength(3);
  251. expect(progressUpdates[0].stage).toBe('download');
  252. expect(progressUpdates[1].stage).toBe('conversion');
  253. expect(progressUpdates[2].stage).toBe('complete');
  254. });
  255. });
  256. describe('Quality Settings', () => {
  257. it('should apply quality-specific encoding parameters', () => {
  258. const qualitySettings = {
  259. '4K': { crf: '18', profile: '3' },
  260. '1080p': { crf: '23', profile: '2' },
  261. '720p': { crf: '25', profile: '1' }
  262. };
  263. Object.entries(qualitySettings).forEach(([quality, settings]) => {
  264. expect(parseInt(settings.crf)).toBeGreaterThan(0);
  265. expect(parseInt(settings.profile)).toBeGreaterThanOrEqual(0);
  266. });
  267. });
  268. });
  269. });