diff options
author | Chocobozzz <me@florianbigard.com> | 2023-04-21 14:55:10 +0200 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2023-05-09 08:57:34 +0200 |
commit | 0c9668f77901e7540e2c7045eb0f2974a4842a69 (patch) | |
tree | 226d3dd1565b0bb56588897af3b8530e6216e96b /server/helpers/ffmpeg/ffmpeg-vod.ts | |
parent | 6bcb854cdea8688a32240bc5719c7d139806e00b (diff) | |
download | PeerTube-0c9668f77901e7540e2c7045eb0f2974a4842a69.tar.gz PeerTube-0c9668f77901e7540e2c7045eb0f2974a4842a69.tar.zst PeerTube-0c9668f77901e7540e2c7045eb0f2974a4842a69.zip |
Implement remote runner jobs in server
Move ffmpeg functions to @shared
Diffstat (limited to 'server/helpers/ffmpeg/ffmpeg-vod.ts')
-rw-r--r-- | server/helpers/ffmpeg/ffmpeg-vod.ts | 267 |
1 files changed, 0 insertions, 267 deletions
diff --git a/server/helpers/ffmpeg/ffmpeg-vod.ts b/server/helpers/ffmpeg/ffmpeg-vod.ts deleted file mode 100644 index d84703eb9..000000000 --- a/server/helpers/ffmpeg/ffmpeg-vod.ts +++ /dev/null | |||
@@ -1,267 +0,0 @@ | |||
1 | import { MutexInterface } from 'async-mutex' | ||
2 | import { Job } from 'bullmq' | ||
3 | import { FfmpegCommand } from 'fluent-ffmpeg' | ||
4 | import { readFile, writeFile } from 'fs-extra' | ||
5 | import { dirname } from 'path' | ||
6 | import { VIDEO_TRANSCODING_FPS } from '@server/initializers/constants' | ||
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' | ||
12 | import { computeFPS, ffprobePromise, getVideoStreamDimensionsInfo, getVideoStreamFPS } from './ffprobe-utils' | ||
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 | |||
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 | |||
30 | availableEncoders: AvailableEncoders | ||
31 | profile: string | ||
32 | |||
33 | resolution: number | ||
34 | |||
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 | |||
102 | command.on('start', () => { | ||
103 | setTimeout(() => { | ||
104 | options.inputFileMutexReleaser() | ||
105 | }, 1000) | ||
106 | }) | ||
107 | |||
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) { | ||
127 | const probe = await ffprobePromise(options.inputPath) | ||
128 | |||
129 | let fps = await getVideoStreamFPS(options.inputPath, probe) | ||
130 | fps = computeFPS(fps, options.resolution) | ||
131 | |||
132 | let scaleFilterValue: string | ||
133 | |||
134 | if (options.resolution !== undefined) { | ||
135 | const videoStreamInfo = await getVideoStreamDimensionsInfo(options.inputPath, probe) | ||
136 | |||
137 | scaleFilterValue = videoStreamInfo?.isPortraitMode === true | ||
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 | } | ||