diff options
Diffstat (limited to 'server/helpers/ffmpeg-utils.ts')
-rw-r--r-- | server/helpers/ffmpeg-utils.ts | 228 |
1 files changed, 115 insertions, 113 deletions
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts index 00c32e99a..557fb5e3a 100644 --- a/server/helpers/ffmpeg-utils.ts +++ b/server/helpers/ffmpeg-utils.ts | |||
@@ -1,12 +1,78 @@ | |||
1 | import * as ffmpeg from 'fluent-ffmpeg' | 1 | import * as ffmpeg from 'fluent-ffmpeg' |
2 | import { dirname, join } from 'path' | 2 | import { dirname, join } from 'path' |
3 | import { getTargetBitrate, getMaxBitrate, VideoResolution } from '../../shared/models/videos' | 3 | import { getMaxBitrate, getTargetBitrate, VideoResolution } from '../../shared/models/videos' |
4 | import { FFMPEG_NICE, VIDEO_TRANSCODING_FPS } from '../initializers/constants' | 4 | import { FFMPEG_NICE, VIDEO_TRANSCODING_FPS } from '../initializers/constants' |
5 | import { processImage } from './image-utils' | 5 | import { processImage } from './image-utils' |
6 | import { logger } from './logger' | 6 | import { logger } from './logger' |
7 | import { checkFFmpegEncoders } from '../initializers/checker-before-init' | 7 | import { checkFFmpegEncoders } from '../initializers/checker-before-init' |
8 | import { readFile, remove, writeFile } from 'fs-extra' | 8 | import { readFile, remove, writeFile } from 'fs-extra' |
9 | import { CONFIG } from '../initializers/config' | 9 | import { CONFIG } from '../initializers/config' |
10 | import { VideoFileMetadata } from '@shared/models/videos/video-file-metadata' | ||
11 | |||
12 | /** | ||
13 | * A toolbox to play with audio | ||
14 | */ | ||
15 | namespace audio { | ||
16 | export const get = (videoPath: string) => { | ||
17 | // without position, ffprobe considers the last input only | ||
18 | // we make it consider the first input only | ||
19 | // if you pass a file path to pos, then ffprobe acts on that file directly | ||
20 | return new Promise<{ absolutePath: string, audioStream?: any }>((res, rej) => { | ||
21 | |||
22 | function parseFfprobe (err: any, data: ffmpeg.FfprobeData) { | ||
23 | if (err) return rej(err) | ||
24 | |||
25 | if ('streams' in data) { | ||
26 | const audioStream = data.streams.find(stream => stream['codec_type'] === 'audio') | ||
27 | if (audioStream) { | ||
28 | return res({ | ||
29 | absolutePath: data.format.filename, | ||
30 | audioStream | ||
31 | }) | ||
32 | } | ||
33 | } | ||
34 | |||
35 | return res({ absolutePath: data.format.filename }) | ||
36 | } | ||
37 | |||
38 | return ffmpeg.ffprobe(videoPath, parseFfprobe) | ||
39 | }) | ||
40 | } | ||
41 | |||
42 | export namespace bitrate { | ||
43 | const baseKbitrate = 384 | ||
44 | |||
45 | const toBits = (kbits: number) => kbits * 8000 | ||
46 | |||
47 | export const aac = (bitrate: number): number => { | ||
48 | switch (true) { | ||
49 | case bitrate > toBits(baseKbitrate): | ||
50 | return baseKbitrate | ||
51 | |||
52 | default: | ||
53 | return -1 // we interpret it as a signal to copy the audio stream as is | ||
54 | } | ||
55 | } | ||
56 | |||
57 | export const mp3 = (bitrate: number): number => { | ||
58 | /* | ||
59 | a 192kbit/sec mp3 doesn't hold as much information as a 192kbit/sec aac. | ||
60 | That's why, when using aac, we can go to lower kbit/sec. The equivalences | ||
61 | made here are not made to be accurate, especially with good mp3 encoders. | ||
62 | */ | ||
63 | switch (true) { | ||
64 | case bitrate <= toBits(192): | ||
65 | return 128 | ||
66 | |||
67 | case bitrate <= toBits(384): | ||
68 | return 256 | ||
69 | |||
70 | default: | ||
71 | return baseKbitrate | ||
72 | } | ||
73 | } | ||
74 | } | ||
75 | } | ||
10 | 76 | ||
11 | function computeResolutionsToTranscode (videoFileHeight: number) { | 77 | function computeResolutionsToTranscode (videoFileHeight: number) { |
12 | const resolutionsEnabled: number[] = [] | 78 | const resolutionsEnabled: number[] = [] |
@@ -24,7 +90,7 @@ function computeResolutionsToTranscode (videoFileHeight: number) { | |||
24 | ] | 90 | ] |
25 | 91 | ||
26 | for (const resolution of resolutions) { | 92 | for (const resolution of resolutions) { |
27 | if (configResolutions[ resolution + 'p' ] === true && videoFileHeight > resolution) { | 93 | if (configResolutions[resolution + 'p'] === true && videoFileHeight > resolution) { |
28 | resolutionsEnabled.push(resolution) | 94 | resolutionsEnabled.push(resolution) |
29 | } | 95 | } |
30 | } | 96 | } |
@@ -48,9 +114,9 @@ async function getVideoStreamCodec (path: string) { | |||
48 | const videoCodec = videoStream.codec_tag_string | 114 | const videoCodec = videoStream.codec_tag_string |
49 | 115 | ||
50 | const baseProfileMatrix = { | 116 | const baseProfileMatrix = { |
51 | 'High': '6400', | 117 | High: '6400', |
52 | 'Main': '4D40', | 118 | Main: '4D40', |
53 | 'Baseline': '42E0' | 119 | Baseline: '42E0' |
54 | } | 120 | } |
55 | 121 | ||
56 | let baseProfile = baseProfileMatrix[videoStream.profile] | 122 | let baseProfile = baseProfileMatrix[videoStream.profile] |
@@ -59,7 +125,8 @@ async function getVideoStreamCodec (path: string) { | |||
59 | baseProfile = baseProfileMatrix['High'] // Fallback | 125 | baseProfile = baseProfileMatrix['High'] // Fallback |
60 | } | 126 | } |
61 | 127 | ||
62 | const level = videoStream.level.toString(16) | 128 | let level = videoStream.level.toString(16) |
129 | if (level.length === 1) level = `0${level}` | ||
63 | 130 | ||
64 | return `${videoCodec}.${baseProfile}${level}` | 131 | return `${videoCodec}.${baseProfile}${level}` |
65 | } | 132 | } |
@@ -91,7 +158,7 @@ async function getVideoFileFPS (path: string) { | |||
91 | if (videoStream === null) return 0 | 158 | if (videoStream === null) return 0 |
92 | 159 | ||
93 | for (const key of [ 'avg_frame_rate', 'r_frame_rate' ]) { | 160 | for (const key of [ 'avg_frame_rate', 'r_frame_rate' ]) { |
94 | const valuesText: string = videoStream[ key ] | 161 | const valuesText: string = videoStream[key] |
95 | if (!valuesText) continue | 162 | if (!valuesText) continue |
96 | 163 | ||
97 | const [ frames, seconds ] = valuesText.split('/') | 164 | const [ frames, seconds ] = valuesText.split('/') |
@@ -104,24 +171,26 @@ async function getVideoFileFPS (path: string) { | |||
104 | return 0 | 171 | return 0 |
105 | } | 172 | } |
106 | 173 | ||
107 | async function getVideoFileBitrate (path: string) { | 174 | async function getMetadataFromFile <T> (path: string, cb = metadata => metadata) { |
108 | return new Promise<number>((res, rej) => { | 175 | return new Promise<T>((res, rej) => { |
109 | ffmpeg.ffprobe(path, (err, metadata) => { | 176 | ffmpeg.ffprobe(path, (err, metadata) => { |
110 | if (err) return rej(err) | 177 | if (err) return rej(err) |
111 | 178 | ||
112 | return res(metadata.format.bit_rate) | 179 | return res(cb(new VideoFileMetadata(metadata))) |
113 | }) | 180 | }) |
114 | }) | 181 | }) |
115 | } | 182 | } |
116 | 183 | ||
184 | async function getVideoFileBitrate (path: string) { | ||
185 | return getMetadataFromFile<number>(path, metadata => metadata.format.bit_rate) | ||
186 | } | ||
187 | |||
117 | function getDurationFromVideoFile (path: string) { | 188 | function getDurationFromVideoFile (path: string) { |
118 | return new Promise<number>((res, rej) => { | 189 | return getMetadataFromFile<number>(path, metadata => Math.floor(metadata.format.duration)) |
119 | ffmpeg.ffprobe(path, (err, metadata) => { | 190 | } |
120 | if (err) return rej(err) | ||
121 | 191 | ||
122 | return res(Math.floor(metadata.format.duration)) | 192 | function getVideoStreamFromFile (path: string) { |
123 | }) | 193 | return getMetadataFromFile<any>(path, metadata => metadata.streams.find(s => s.codec_type === 'video') || null) |
124 | }) | ||
125 | } | 194 | } |
126 | 195 | ||
127 | async function generateImageFromVideoFile (fromPath: string, folder: string, imageName: string, size: { width: number, height: number }) { | 196 | async function generateImageFromVideoFile (fromPath: string, folder: string, imageName: string, size: { width: number, height: number }) { |
@@ -191,7 +260,8 @@ interface OnlyAudioTranscodeOptions extends BaseTranscodeOptions { | |||
191 | type: 'only-audio' | 260 | type: 'only-audio' |
192 | } | 261 | } |
193 | 262 | ||
194 | type TranscodeOptions = HLSTranscodeOptions | 263 | type TranscodeOptions = |
264 | HLSTranscodeOptions | ||
195 | | VideoTranscodeOptions | 265 | | VideoTranscodeOptions |
196 | | MergeAudioTranscodeOptions | 266 | | MergeAudioTranscodeOptions |
197 | | OnlyAudioTranscodeOptions | 267 | | OnlyAudioTranscodeOptions |
@@ -204,13 +274,13 @@ function transcode (options: TranscodeOptions) { | |||
204 | .output(options.outputPath) | 274 | .output(options.outputPath) |
205 | 275 | ||
206 | if (options.type === 'quick-transcode') { | 276 | if (options.type === 'quick-transcode') { |
207 | command = await buildQuickTranscodeCommand(command) | 277 | command = buildQuickTranscodeCommand(command) |
208 | } else if (options.type === 'hls') { | 278 | } else if (options.type === 'hls') { |
209 | command = await buildHLSCommand(command, options) | 279 | command = await buildHLSCommand(command, options) |
210 | } else if (options.type === 'merge-audio') { | 280 | } else if (options.type === 'merge-audio') { |
211 | command = await buildAudioMergeCommand(command, options) | 281 | command = await buildAudioMergeCommand(command, options) |
212 | } else if (options.type === 'only-audio') { | 282 | } else if (options.type === 'only-audio') { |
213 | command = await buildOnlyAudioCommand(command, options) | 283 | command = buildOnlyAudioCommand(command, options) |
214 | } else { | 284 | } else { |
215 | command = await buildx264Command(command, options) | 285 | command = await buildx264Command(command, options) |
216 | } | 286 | } |
@@ -247,22 +317,27 @@ async function canDoQuickTranscode (path: string): Promise<boolean> { | |||
247 | 317 | ||
248 | // check video params | 318 | // check video params |
249 | if (videoStream == null) return false | 319 | if (videoStream == null) return false |
250 | if (videoStream[ 'codec_name' ] !== 'h264') return false | 320 | if (videoStream['codec_name'] !== 'h264') return false |
251 | if (videoStream[ 'pix_fmt' ] !== 'yuv420p') return false | 321 | if (videoStream['pix_fmt'] !== 'yuv420p') return false |
252 | if (fps < VIDEO_TRANSCODING_FPS.MIN || fps > VIDEO_TRANSCODING_FPS.MAX) return false | 322 | if (fps < VIDEO_TRANSCODING_FPS.MIN || fps > VIDEO_TRANSCODING_FPS.MAX) return false |
253 | if (bitRate > getMaxBitrate(resolution.videoFileResolution, fps, VIDEO_TRANSCODING_FPS)) return false | 323 | if (bitRate > getMaxBitrate(resolution.videoFileResolution, fps, VIDEO_TRANSCODING_FPS)) return false |
254 | 324 | ||
255 | // check audio params (if audio stream exists) | 325 | // check audio params (if audio stream exists) |
256 | if (parsedAudio.audioStream) { | 326 | if (parsedAudio.audioStream) { |
257 | if (parsedAudio.audioStream[ 'codec_name' ] !== 'aac') return false | 327 | if (parsedAudio.audioStream['codec_name'] !== 'aac') return false |
258 | 328 | ||
259 | const maxAudioBitrate = audio.bitrate[ 'aac' ](parsedAudio.audioStream[ 'bit_rate' ]) | 329 | const maxAudioBitrate = audio.bitrate['aac'](parsedAudio.audioStream['bit_rate']) |
260 | if (maxAudioBitrate !== -1 && parsedAudio.audioStream[ 'bit_rate' ] > maxAudioBitrate) return false | 330 | if (maxAudioBitrate !== -1 && parsedAudio.audioStream['bit_rate'] > maxAudioBitrate) return false |
261 | } | 331 | } |
262 | 332 | ||
263 | return true | 333 | return true |
264 | } | 334 | } |
265 | 335 | ||
336 | function getClosestFramerateStandard (fps: number, type: 'HD_STANDARD' | 'STANDARD'): number { | ||
337 | return VIDEO_TRANSCODING_FPS[type].slice(0) | ||
338 | .sort((a, b) => fps % a - fps % b)[0] | ||
339 | } | ||
340 | |||
266 | // --------------------------------------------------------------------------- | 341 | // --------------------------------------------------------------------------- |
267 | 342 | ||
268 | export { | 343 | export { |
@@ -270,6 +345,7 @@ export { | |||
270 | getAudioStreamCodec, | 345 | getAudioStreamCodec, |
271 | getVideoStreamSize, | 346 | getVideoStreamSize, |
272 | getVideoFileResolution, | 347 | getVideoFileResolution, |
348 | getMetadataFromFile, | ||
273 | getDurationFromVideoFile, | 349 | getDurationFromVideoFile, |
274 | generateImageFromVideoFile, | 350 | generateImageFromVideoFile, |
275 | TranscodeOptions, | 351 | TranscodeOptions, |
@@ -286,13 +362,14 @@ export { | |||
286 | 362 | ||
287 | async function buildx264Command (command: ffmpeg.FfmpegCommand, options: TranscodeOptions) { | 363 | async function buildx264Command (command: ffmpeg.FfmpegCommand, options: TranscodeOptions) { |
288 | let fps = await getVideoFileFPS(options.inputPath) | 364 | let fps = await getVideoFileFPS(options.inputPath) |
289 | // On small/medium resolutions, limit FPS | ||
290 | if ( | 365 | if ( |
366 | // On small/medium resolutions, limit FPS | ||
291 | options.resolution !== undefined && | 367 | options.resolution !== undefined && |
292 | options.resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN && | 368 | options.resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN && |
293 | fps > VIDEO_TRANSCODING_FPS.AVERAGE | 369 | fps > VIDEO_TRANSCODING_FPS.AVERAGE |
294 | ) { | 370 | ) { |
295 | fps = VIDEO_TRANSCODING_FPS.AVERAGE | 371 | // Get closest standard framerate by modulo: downsampling has to be done to a divisor of the nominal fps value |
372 | fps = getClosestFramerateStandard(fps, 'STANDARD') | ||
296 | } | 373 | } |
297 | 374 | ||
298 | command = await presetH264(command, options.inputPath, options.resolution, fps) | 375 | command = await presetH264(command, options.inputPath, options.resolution, fps) |
@@ -305,7 +382,7 @@ async function buildx264Command (command: ffmpeg.FfmpegCommand, options: Transco | |||
305 | 382 | ||
306 | if (fps) { | 383 | if (fps) { |
307 | // Hard FPS limits | 384 | // Hard FPS limits |
308 | if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = VIDEO_TRANSCODING_FPS.MAX | 385 | if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = getClosestFramerateStandard(fps, 'HD_STANDARD') |
309 | else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN | 386 | else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN |
310 | 387 | ||
311 | command = command.withFPS(fps) | 388 | command = command.withFPS(fps) |
@@ -327,14 +404,14 @@ async function buildAudioMergeCommand (command: ffmpeg.FfmpegCommand, options: M | |||
327 | return command | 404 | return command |
328 | } | 405 | } |
329 | 406 | ||
330 | async function buildOnlyAudioCommand (command: ffmpeg.FfmpegCommand, options: OnlyAudioTranscodeOptions) { | 407 | function buildOnlyAudioCommand (command: ffmpeg.FfmpegCommand, options: OnlyAudioTranscodeOptions) { |
331 | command = await presetOnlyAudio(command) | 408 | command = presetOnlyAudio(command) |
332 | 409 | ||
333 | return command | 410 | return command |
334 | } | 411 | } |
335 | 412 | ||
336 | async function buildQuickTranscodeCommand (command: ffmpeg.FfmpegCommand) { | 413 | function buildQuickTranscodeCommand (command: ffmpeg.FfmpegCommand) { |
337 | command = await presetCopy(command) | 414 | command = presetCopy(command) |
338 | 415 | ||
339 | command = command.outputOption('-map_metadata -1') // strip all metadata | 416 | command = command.outputOption('-map_metadata -1') // strip all metadata |
340 | .outputOption('-movflags faststart') | 417 | .outputOption('-movflags faststart') |
@@ -345,7 +422,8 @@ async function buildQuickTranscodeCommand (command: ffmpeg.FfmpegCommand) { | |||
345 | async function buildHLSCommand (command: ffmpeg.FfmpegCommand, options: HLSTranscodeOptions) { | 422 | async function buildHLSCommand (command: ffmpeg.FfmpegCommand, options: HLSTranscodeOptions) { |
346 | const videoPath = getHLSVideoPath(options) | 423 | const videoPath = getHLSVideoPath(options) |
347 | 424 | ||
348 | if (options.copyCodecs) command = await presetCopy(command) | 425 | if (options.copyCodecs) command = presetCopy(command) |
426 | else if (options.resolution === VideoResolution.H_NOVIDEO) command = presetOnlyAudio(command) | ||
349 | else command = await buildx264Command(command, options) | 427 | else command = await buildx264Command(command, options) |
350 | 428 | ||
351 | command = command.outputOption('-hls_time 4') | 429 | command = command.outputOption('-hls_time 4') |
@@ -378,17 +456,6 @@ async function fixHLSPlaylistIfNeeded (options: TranscodeOptions) { | |||
378 | await writeFile(options.outputPath, newContent) | 456 | await writeFile(options.outputPath, newContent) |
379 | } | 457 | } |
380 | 458 | ||
381 | function getVideoStreamFromFile (path: string) { | ||
382 | return new Promise<any>((res, rej) => { | ||
383 | ffmpeg.ffprobe(path, (err, metadata) => { | ||
384 | if (err) return rej(err) | ||
385 | |||
386 | const videoStream = metadata.streams.find(s => s.codec_type === 'video') | ||
387 | return res(videoStream || null) | ||
388 | }) | ||
389 | }) | ||
390 | } | ||
391 | |||
392 | /** | 459 | /** |
393 | * A slightly customised version of the 'veryfast' x264 preset | 460 | * A slightly customised version of the 'veryfast' x264 preset |
394 | * | 461 | * |
@@ -413,71 +480,6 @@ async function presetH264VeryFast (command: ffmpeg.FfmpegCommand, input: string, | |||
413 | } | 480 | } |
414 | 481 | ||
415 | /** | 482 | /** |
416 | * A toolbox to play with audio | ||
417 | */ | ||
418 | namespace audio { | ||
419 | export const get = (videoPath: string) => { | ||
420 | // without position, ffprobe considers the last input only | ||
421 | // we make it consider the first input only | ||
422 | // if you pass a file path to pos, then ffprobe acts on that file directly | ||
423 | return new Promise<{ absolutePath: string, audioStream?: any }>((res, rej) => { | ||
424 | |||
425 | function parseFfprobe (err: any, data: ffmpeg.FfprobeData) { | ||
426 | if (err) return rej(err) | ||
427 | |||
428 | if ('streams' in data) { | ||
429 | const audioStream = data.streams.find(stream => stream[ 'codec_type' ] === 'audio') | ||
430 | if (audioStream) { | ||
431 | return res({ | ||
432 | absolutePath: data.format.filename, | ||
433 | audioStream | ||
434 | }) | ||
435 | } | ||
436 | } | ||
437 | |||
438 | return res({ absolutePath: data.format.filename }) | ||
439 | } | ||
440 | |||
441 | return ffmpeg.ffprobe(videoPath, parseFfprobe) | ||
442 | }) | ||
443 | } | ||
444 | |||
445 | export namespace bitrate { | ||
446 | const baseKbitrate = 384 | ||
447 | |||
448 | const toBits = (kbits: number) => kbits * 8000 | ||
449 | |||
450 | export const aac = (bitrate: number): number => { | ||
451 | switch (true) { | ||
452 | case bitrate > toBits(baseKbitrate): | ||
453 | return baseKbitrate | ||
454 | |||
455 | default: | ||
456 | return -1 // we interpret it as a signal to copy the audio stream as is | ||
457 | } | ||
458 | } | ||
459 | |||
460 | export const mp3 = (bitrate: number): number => { | ||
461 | /* | ||
462 | a 192kbit/sec mp3 doesn't hold as much information as a 192kbit/sec aac. | ||
463 | That's why, when using aac, we can go to lower kbit/sec. The equivalences | ||
464 | made here are not made to be accurate, especially with good mp3 encoders. | ||
465 | */ | ||
466 | switch (true) { | ||
467 | case bitrate <= toBits(192): | ||
468 | return 128 | ||
469 | |||
470 | case bitrate <= toBits(384): | ||
471 | return 256 | ||
472 | |||
473 | default: | ||
474 | return baseKbitrate | ||
475 | } | ||
476 | } | ||
477 | } | ||
478 | } | ||
479 | |||
480 | /** | ||
481 | * Standard profile, with variable bitrate audio and faststart. | 483 | * Standard profile, with variable bitrate audio and faststart. |
482 | * | 484 | * |
483 | * As for the audio, quality '5' is the highest and ensures 96-112kbps/channel | 485 | * As for the audio, quality '5' is the highest and ensures 96-112kbps/channel |
@@ -507,10 +509,10 @@ async function presetH264 (command: ffmpeg.FfmpegCommand, input: string, resolut | |||
507 | // of course this is far from perfect, but it might save some space in the end | 509 | // of course this is far from perfect, but it might save some space in the end |
508 | localCommand = localCommand.audioCodec('aac') | 510 | localCommand = localCommand.audioCodec('aac') |
509 | 511 | ||
510 | const audioCodecName = parsedAudio.audioStream[ 'codec_name' ] | 512 | const audioCodecName = parsedAudio.audioStream['codec_name'] |
511 | 513 | ||
512 | if (audio.bitrate[ audioCodecName ]) { | 514 | if (audio.bitrate[audioCodecName]) { |
513 | const bitrate = audio.bitrate[ audioCodecName ](parsedAudio.audioStream[ 'bit_rate' ]) | 515 | const bitrate = audio.bitrate[audioCodecName](parsedAudio.audioStream['bit_rate']) |
514 | if (bitrate !== undefined && bitrate !== -1) localCommand = localCommand.audioBitrate(bitrate) | 516 | if (bitrate !== undefined && bitrate !== -1) localCommand = localCommand.audioBitrate(bitrate) |
515 | } | 517 | } |
516 | } | 518 | } |
@@ -531,14 +533,14 @@ async function presetH264 (command: ffmpeg.FfmpegCommand, input: string, resolut | |||
531 | return localCommand | 533 | return localCommand |
532 | } | 534 | } |
533 | 535 | ||
534 | async function presetCopy (command: ffmpeg.FfmpegCommand): Promise<ffmpeg.FfmpegCommand> { | 536 | function presetCopy (command: ffmpeg.FfmpegCommand): ffmpeg.FfmpegCommand { |
535 | return command | 537 | return command |
536 | .format('mp4') | 538 | .format('mp4') |
537 | .videoCodec('copy') | 539 | .videoCodec('copy') |
538 | .audioCodec('copy') | 540 | .audioCodec('copy') |
539 | } | 541 | } |
540 | 542 | ||
541 | async function presetOnlyAudio (command: ffmpeg.FfmpegCommand): Promise<ffmpeg.FfmpegCommand> { | 543 | function presetOnlyAudio (command: ffmpeg.FfmpegCommand): ffmpeg.FfmpegCommand { |
542 | return command | 544 | return command |
543 | .format('mp4') | 545 | .format('mp4') |
544 | .audioCodec('copy') | 546 | .audioCodec('copy') |