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