]>
Commit | Line | Data |
---|---|---|
3545e72c | 1 | import { MutexInterface } from 'async-mutex' |
5a921e7b | 2 | import { Job } from 'bullmq' |
c729caf6 C |
3 | import { FfmpegCommand } from 'fluent-ffmpeg' |
4 | import { readFile, writeFile } from 'fs-extra' | |
5 | import { dirname } from 'path' | |
3545e72c | 6 | import { VIDEO_TRANSCODING_FPS } from '@server/initializers/constants' |
c729caf6 C |
7 | import { pick } from '@shared/core-utils' |
8 | import { AvailableEncoders, VideoResolution } from '@shared/models' | |
9 | import { logger, loggerTagsFactory } from '../logger' | |
10 | import { getFFmpeg, runCommand } from './ffmpeg-commons' | |
11 | import { presetCopy, presetOnlyAudio, presetVOD } from './ffmpeg-presets' | |
84cae54e | 12 | import { computeFPS, ffprobePromise, getVideoStreamDimensionsInfo, getVideoStreamFPS } from './ffprobe-utils' |
c729caf6 C |
13 | |
14 | const lTags = loggerTagsFactory('ffmpeg') | |
15 | ||
16 | // --------------------------------------------------------------------------- | |
17 | ||
18 | type TranscodeVODOptionsType = 'hls' | 'hls-from-ts' | 'quick-transcode' | 'video' | 'merge-audio' | 'only-audio' | |
19 | ||
20 | interface BaseTranscodeVODOptions { | |
21 | type: TranscodeVODOptionsType | |
22 | ||
23 | inputPath: string | |
24 | outputPath: string | |
25 | ||
3545e72c C |
26 | // Will be released after the ffmpeg started |
27 | // To prevent a bug where the input file does not exist anymore when running ffmpeg | |
28 | inputFileMutexReleaser: MutexInterface.Releaser | |
29 | ||
c729caf6 C |
30 | availableEncoders: AvailableEncoders |
31 | profile: string | |
32 | ||
33 | resolution: number | |
34 | ||
c729caf6 C |
35 | job?: Job |
36 | } | |
37 | ||
38 | interface HLSTranscodeOptions extends BaseTranscodeVODOptions { | |
39 | type: 'hls' | |
40 | copyCodecs: boolean | |
41 | hlsPlaylist: { | |
42 | videoFilename: string | |
43 | } | |
44 | } | |
45 | ||
46 | interface HLSFromTSTranscodeOptions extends BaseTranscodeVODOptions { | |
47 | type: 'hls-from-ts' | |
48 | ||
49 | isAAC: boolean | |
50 | ||
51 | hlsPlaylist: { | |
52 | videoFilename: string | |
53 | } | |
54 | } | |
55 | ||
56 | interface QuickTranscodeOptions extends BaseTranscodeVODOptions { | |
57 | type: 'quick-transcode' | |
58 | } | |
59 | ||
60 | interface VideoTranscodeOptions extends BaseTranscodeVODOptions { | |
61 | type: 'video' | |
62 | } | |
63 | ||
64 | interface MergeAudioTranscodeOptions extends BaseTranscodeVODOptions { | |
65 | type: 'merge-audio' | |
66 | audioPath: string | |
67 | } | |
68 | ||
69 | interface OnlyAudioTranscodeOptions extends BaseTranscodeVODOptions { | |
70 | type: 'only-audio' | |
71 | } | |
72 | ||
73 | type TranscodeVODOptions = | |
74 | HLSTranscodeOptions | |
75 | | HLSFromTSTranscodeOptions | |
76 | | VideoTranscodeOptions | |
77 | | MergeAudioTranscodeOptions | |
78 | | OnlyAudioTranscodeOptions | |
79 | | QuickTranscodeOptions | |
80 | ||
81 | // --------------------------------------------------------------------------- | |
82 | ||
83 | const builders: { | |
84 | [ type in TranscodeVODOptionsType ]: (c: FfmpegCommand, o?: TranscodeVODOptions) => Promise<FfmpegCommand> | FfmpegCommand | |
85 | } = { | |
86 | 'quick-transcode': buildQuickTranscodeCommand, | |
87 | 'hls': buildHLSVODCommand, | |
88 | 'hls-from-ts': buildHLSVODFromTSCommand, | |
89 | 'merge-audio': buildAudioMergeCommand, | |
90 | 'only-audio': buildOnlyAudioCommand, | |
91 | 'video': buildVODCommand | |
92 | } | |
93 | ||
94 | async function transcodeVOD (options: TranscodeVODOptions) { | |
95 | logger.debug('Will run transcode.', { options, ...lTags() }) | |
96 | ||
97 | let command = getFFmpeg(options.inputPath, 'vod') | |
98 | .output(options.outputPath) | |
99 | ||
100 | command = await builders[options.type](command, options) | |
101 | ||
3545e72c C |
102 | command.on('start', () => { |
103 | setTimeout(() => { | |
104 | options.inputFileMutexReleaser() | |
105 | }, 1000) | |
106 | }) | |
107 | ||
c729caf6 C |
108 | await runCommand({ command, job: options.job }) |
109 | ||
110 | await fixHLSPlaylistIfNeeded(options) | |
111 | } | |
112 | ||
113 | // --------------------------------------------------------------------------- | |
114 | ||
115 | export { | |
116 | transcodeVOD, | |
117 | ||
118 | buildVODCommand, | |
119 | ||
120 | TranscodeVODOptions, | |
121 | TranscodeVODOptionsType | |
122 | } | |
123 | ||
124 | // --------------------------------------------------------------------------- | |
125 | ||
126 | async function buildVODCommand (command: FfmpegCommand, options: TranscodeVODOptions) { | |
84cae54e C |
127 | const probe = await ffprobePromise(options.inputPath) |
128 | ||
129 | let fps = await getVideoStreamFPS(options.inputPath, probe) | |
c729caf6 C |
130 | fps = computeFPS(fps, options.resolution) |
131 | ||
132 | let scaleFilterValue: string | |
133 | ||
134 | if (options.resolution !== undefined) { | |
84cae54e C |
135 | const videoStreamInfo = await getVideoStreamDimensionsInfo(options.inputPath, probe) |
136 | ||
137 | scaleFilterValue = videoStreamInfo?.isPortraitMode === true | |
c729caf6 C |
138 | ? `w=${options.resolution}:h=-2` |
139 | : `w=-2:h=${options.resolution}` | |
140 | } | |
141 | ||
142 | command = await presetVOD({ | |
143 | ...pick(options, [ 'resolution', 'availableEncoders', 'profile' ]), | |
144 | ||
145 | command, | |
146 | input: options.inputPath, | |
147 | canCopyAudio: true, | |
148 | canCopyVideo: true, | |
149 | fps, | |
150 | scaleFilterValue | |
151 | }) | |
152 | ||
153 | return command | |
154 | } | |
155 | ||
156 | function buildQuickTranscodeCommand (command: FfmpegCommand) { | |
157 | command = presetCopy(command) | |
158 | ||
159 | command = command.outputOption('-map_metadata -1') // strip all metadata | |
160 | .outputOption('-movflags faststart') | |
161 | ||
162 | return command | |
163 | } | |
164 | ||
165 | // --------------------------------------------------------------------------- | |
166 | // Audio transcoding | |
167 | // --------------------------------------------------------------------------- | |
168 | ||
169 | async function buildAudioMergeCommand (command: FfmpegCommand, options: MergeAudioTranscodeOptions) { | |
170 | command = command.loop(undefined) | |
171 | ||
172 | const scaleFilterValue = getMergeAudioScaleFilterValue() | |
173 | command = await presetVOD({ | |
174 | ...pick(options, [ 'resolution', 'availableEncoders', 'profile' ]), | |
175 | ||
176 | command, | |
177 | input: options.audioPath, | |
178 | canCopyAudio: true, | |
179 | canCopyVideo: true, | |
180 | fps: VIDEO_TRANSCODING_FPS.AUDIO_MERGE, | |
181 | scaleFilterValue | |
182 | }) | |
183 | ||
184 | command.outputOption('-preset:v veryfast') | |
185 | ||
186 | command = command.input(options.audioPath) | |
187 | .outputOption('-tune stillimage') | |
188 | .outputOption('-shortest') | |
189 | ||
190 | return command | |
191 | } | |
192 | ||
193 | function buildOnlyAudioCommand (command: FfmpegCommand, _options: OnlyAudioTranscodeOptions) { | |
194 | command = presetOnlyAudio(command) | |
195 | ||
196 | return command | |
197 | } | |
198 | ||
199 | // --------------------------------------------------------------------------- | |
200 | // HLS transcoding | |
201 | // --------------------------------------------------------------------------- | |
202 | ||
203 | async function buildHLSVODCommand (command: FfmpegCommand, options: HLSTranscodeOptions) { | |
204 | const videoPath = getHLSVideoPath(options) | |
205 | ||
206 | if (options.copyCodecs) command = presetCopy(command) | |
207 | else if (options.resolution === VideoResolution.H_NOVIDEO) command = presetOnlyAudio(command) | |
208 | else command = await buildVODCommand(command, options) | |
209 | ||
210 | addCommonHLSVODCommandOptions(command, videoPath) | |
211 | ||
212 | return command | |
213 | } | |
214 | ||
215 | function buildHLSVODFromTSCommand (command: FfmpegCommand, options: HLSFromTSTranscodeOptions) { | |
216 | const videoPath = getHLSVideoPath(options) | |
217 | ||
218 | command.outputOption('-c copy') | |
219 | ||
220 | if (options.isAAC) { | |
221 | // Required for example when copying an AAC stream from an MPEG-TS | |
222 | // Since it's a bitstream filter, we don't need to reencode the audio | |
223 | command.outputOption('-bsf:a aac_adtstoasc') | |
224 | } | |
225 | ||
226 | addCommonHLSVODCommandOptions(command, videoPath) | |
227 | ||
228 | return command | |
229 | } | |
230 | ||
231 | function addCommonHLSVODCommandOptions (command: FfmpegCommand, outputPath: string) { | |
232 | return command.outputOption('-hls_time 4') | |
233 | .outputOption('-hls_list_size 0') | |
234 | .outputOption('-hls_playlist_type vod') | |
235 | .outputOption('-hls_segment_filename ' + outputPath) | |
236 | .outputOption('-hls_segment_type fmp4') | |
237 | .outputOption('-f hls') | |
238 | .outputOption('-hls_flags single_file') | |
239 | } | |
240 | ||
241 | async function fixHLSPlaylistIfNeeded (options: TranscodeVODOptions) { | |
242 | if (options.type !== 'hls' && options.type !== 'hls-from-ts') return | |
243 | ||
244 | const fileContent = await readFile(options.outputPath) | |
245 | ||
246 | const videoFileName = options.hlsPlaylist.videoFilename | |
247 | const videoFilePath = getHLSVideoPath(options) | |
248 | ||
249 | // Fix wrong mapping with some ffmpeg versions | |
250 | const newContent = fileContent.toString() | |
251 | .replace(`#EXT-X-MAP:URI="${videoFilePath}",`, `#EXT-X-MAP:URI="${videoFileName}",`) | |
252 | ||
253 | await writeFile(options.outputPath, newContent) | |
254 | } | |
255 | ||
256 | // --------------------------------------------------------------------------- | |
257 | // Helpers | |
258 | // --------------------------------------------------------------------------- | |
259 | ||
260 | function getHLSVideoPath (options: HLSTranscodeOptions | HLSFromTSTranscodeOptions) { | |
261 | return `${dirname(options.outputPath)}/${options.hlsPlaylist.videoFilename}` | |
262 | } | |
263 | ||
264 | // Avoid "height not divisible by 2" error | |
265 | function getMergeAudioScaleFilterValue () { | |
266 | return 'trunc(iw/2)*2:trunc(ih/2)*2' | |
267 | } |