|
1 | | -import ffmpeg from 'fluent-ffmpeg'; |
2 | 1 | import fs from 'fs/promises'; |
3 | 2 |
|
4 | 3 | import { tmpdir } from 'os'; |
5 | 4 | import path from 'path'; |
| 5 | +import { spawn } from 'child_process'; |
6 | 6 |
|
7 | 7 | export const FFMPEG_CONSTANTS = { |
8 | 8 | OPUS: { |
@@ -44,15 +44,18 @@ export interface FFmpegConfig { |
44 | 44 | onError: (err: Error) => Promise<void>; |
45 | 45 | } |
46 | 46 |
|
| 47 | +let ffmpegPath = 'ffmpeg'; |
| 48 | +let ffprobePath = 'ffprobe'; |
| 49 | + |
47 | 50 | export const initializeFFmpeg = async (disable: boolean = false) => { |
48 | 51 | if (disable) return; |
49 | 52 |
|
50 | 53 | try { |
51 | 54 | const ffmpegInstaller = (await import('@ffmpeg-installer/ffmpeg')).default; |
52 | 55 | const ffprobeInstaller = (await import('@ffprobe-installer/ffprobe')).default; |
53 | 56 |
|
54 | | - ffmpeg.setFfmpegPath(ffmpegInstaller.path); |
55 | | - ffmpeg.setFfprobePath(ffprobeInstaller.path); |
| 57 | + ffmpegPath = ffmpegInstaller.path; |
| 58 | + ffprobePath = ffprobeInstaller.path; |
56 | 59 | } catch {} |
57 | 60 | }; |
58 | 61 |
|
@@ -152,45 +155,58 @@ export class MimeValidator { |
152 | 155 | export class FFmpegProcessor { |
153 | 156 | static async process(config: FFmpegConfig): Promise<void> { |
154 | 157 | return new Promise((resolve, reject) => { |
155 | | - const processor = ffmpeg(config.input).output(config.output); |
156 | | - |
157 | | - for (let i = 0; i < config.options.length; i++) { |
158 | | - const option = config.options[i]; |
159 | | - |
160 | | - if (option.startsWith('-') && i + 1 < config.options.length && !config.options[i + 1].startsWith('-')) { |
161 | | - processor.outputOptions(option, config.options[i + 1]); |
162 | | - i++; |
163 | | - } else { |
164 | | - processor.outputOptions(option); |
165 | | - } |
166 | | - } |
| 158 | + const args = ['-y', '-i', config.input, ...config.options, config.output]; |
| 159 | + const child = spawn(ffmpegPath, args, { stdio: 'ignore' }); |
167 | 160 |
|
168 | | - processor |
169 | | - .on('end', async () => { |
| 161 | + child.on('close', async (code) => { |
| 162 | + if (code === 0) { |
170 | 163 | try { |
171 | 164 | await config.onEnd(); |
172 | 165 | resolve(); |
173 | 166 | } catch (error) { |
174 | 167 | reject(error); |
175 | 168 | } |
176 | | - }) |
177 | | - .on('error', async (err: Error) => { |
| 169 | + } else { |
178 | 170 | try { |
| 171 | + const err = new Error(`FFmpeg exited with code ${code}`); |
179 | 172 | await config.onError(err); |
180 | | - } finally { |
181 | 173 | reject(err); |
| 174 | + } catch (error) { |
| 175 | + reject(error); |
182 | 176 | } |
183 | | - }) |
184 | | - .run(); |
| 177 | + } |
| 178 | + }); |
| 179 | + |
| 180 | + child.on('error', async (err: Error) => { |
| 181 | + try { |
| 182 | + await config.onError(err); |
| 183 | + } finally { |
| 184 | + reject(err); |
| 185 | + } |
| 186 | + }); |
185 | 187 | }); |
186 | 188 | } |
187 | 189 |
|
188 | 190 | static async getDuration(filePath: string): Promise<number> { |
189 | 191 | return new Promise((resolve, reject) => { |
190 | | - ffmpeg.ffprobe(filePath, (err: Error | null, metadata: ffmpeg.FfprobeData) => { |
191 | | - if (err) return reject(err); |
192 | | - resolve(metadata.format.duration || 0); |
| 192 | + const child = spawn(ffprobePath, [ |
| 193 | + '-v', 'error', |
| 194 | + '-show_entries', 'format=duration', |
| 195 | + '-of', 'default=noprint_wrappers=1:nokey=1', |
| 196 | + filePath |
| 197 | + ]); |
| 198 | + |
| 199 | + let output = ''; |
| 200 | + child.stdout.on('data', (data) => output += data.toString()); |
| 201 | + |
| 202 | + child.on('close', (code) => { |
| 203 | + if (code === 0) { |
| 204 | + resolve(parseFloat(output.trim()) || 0); |
| 205 | + } else { |
| 206 | + reject(new Error(`ffprobe exited with code ${code}`)); |
| 207 | + } |
193 | 208 | }); |
| 209 | + child.on('error', reject); |
194 | 210 | }); |
195 | 211 | } |
196 | 212 | } |
0 commit comments