]>
Commit | Line | Data |
---|---|---|
c729caf6 C |
1 | import { Job } from 'bull' |
2 | import ffmpeg, { FfmpegCommand } from 'fluent-ffmpeg' | |
3 | import { execPromise } from '@server/helpers/core-utils' | |
4 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | |
5 | import { CONFIG } from '@server/initializers/config' | |
6 | import { FFMPEG_NICE } from '@server/initializers/constants' | |
7 | import { EncoderOptions } from '@shared/models' | |
8 | ||
9 | const lTags = loggerTagsFactory('ffmpeg') | |
10 | ||
11 | type StreamType = 'audio' | 'video' | |
12 | ||
13 | function getFFmpeg (input: string, type: 'live' | 'vod') { | |
14 | // We set cwd explicitly because ffmpeg appears to create temporary files when trancoding which fails in read-only file systems | |
15 | const command = ffmpeg(input, { | |
16 | niceness: type === 'live' ? FFMPEG_NICE.LIVE : FFMPEG_NICE.VOD, | |
17 | cwd: CONFIG.STORAGE.TMP_DIR | |
18 | }) | |
19 | ||
20 | const threads = type === 'live' | |
21 | ? CONFIG.LIVE.TRANSCODING.THREADS | |
22 | : CONFIG.TRANSCODING.THREADS | |
23 | ||
24 | if (threads > 0) { | |
25 | // If we don't set any threads ffmpeg will chose automatically | |
26 | command.outputOption('-threads ' + threads) | |
27 | } | |
28 | ||
29 | return command | |
30 | } | |
31 | ||
32 | function getFFmpegVersion () { | |
33 | return new Promise<string>((res, rej) => { | |
34 | (ffmpeg() as any)._getFfmpegPath((err, ffmpegPath) => { | |
35 | if (err) return rej(err) | |
36 | if (!ffmpegPath) return rej(new Error('Could not find ffmpeg path')) | |
37 | ||
38 | return execPromise(`${ffmpegPath} -version`) | |
39 | .then(stdout => { | |
40 | const parsed = stdout.match(/ffmpeg version .?(\d+\.\d+(\.\d+)?)/) | |
41 | if (!parsed || !parsed[1]) return rej(new Error(`Could not find ffmpeg version in ${stdout}`)) | |
42 | ||
43 | // Fix ffmpeg version that does not include patch version (4.4 for example) | |
44 | let version = parsed[1] | |
45 | if (version.match(/^\d+\.\d+$/)) { | |
46 | version += '.0' | |
47 | } | |
48 | ||
49 | return res(version) | |
50 | }) | |
51 | .catch(err => rej(err)) | |
52 | }) | |
53 | }) | |
54 | } | |
55 | ||
56 | async function runCommand (options: { | |
57 | command: FfmpegCommand | |
58 | silent?: boolean // false by default | |
59 | job?: Job | |
60 | }) { | |
61 | const { command, silent = false, job } = options | |
62 | ||
63 | return new Promise<void>((res, rej) => { | |
64 | let shellCommand: string | |
65 | ||
66 | command.on('start', cmdline => { shellCommand = cmdline }) | |
67 | ||
68 | command.on('error', (err, stdout, stderr) => { | |
69 | if (silent !== true) logger.error('Error in ffmpeg.', { stdout, stderr, shellCommand, ...lTags() }) | |
70 | ||
71 | rej(err) | |
72 | }) | |
73 | ||
74 | command.on('end', (stdout, stderr) => { | |
75 | logger.debug('FFmpeg command ended.', { stdout, stderr, shellCommand, ...lTags() }) | |
76 | ||
77 | res() | |
78 | }) | |
79 | ||
80 | if (job) { | |
81 | command.on('progress', progress => { | |
82 | if (!progress.percent) return | |
83 | ||
84 | job.progress(Math.round(progress.percent)) | |
85 | .catch(err => logger.warn('Cannot set ffmpeg job progress.', { err, ...lTags() })) | |
86 | }) | |
87 | } | |
88 | ||
89 | command.run() | |
90 | }) | |
91 | } | |
92 | ||
93 | function buildStreamSuffix (base: string, streamNum?: number) { | |
94 | if (streamNum !== undefined) { | |
95 | return `${base}:${streamNum}` | |
96 | } | |
97 | ||
98 | return base | |
99 | } | |
100 | ||
101 | function getScaleFilter (options: EncoderOptions): string { | |
102 | if (options.scaleFilter) return options.scaleFilter.name | |
103 | ||
104 | return 'scale' | |
105 | } | |
106 | ||
107 | export { | |
108 | getFFmpeg, | |
109 | getFFmpegVersion, | |
110 | runCommand, | |
111 | StreamType, | |
112 | buildStreamSuffix, | |
113 | getScaleFilter | |
114 | } |