]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/lib/transcoding/transcoding.ts
Use bullmq job dependency
[github/Chocobozzz/PeerTube.git] / server / lib / transcoding / transcoding.ts
CommitLineData
5a921e7b 1import { Job } from 'bullmq'
3851e732 2import { copyFile, ensureDir, move, remove, stat } from 'fs-extra'
d7a25329 3import { basename, extname as extnameUtil, join } from 'path'
318b0bd0 4import { toEven } from '@server/helpers/core-utils'
7b6b445d 5import { retryTransactionWrapper } from '@server/helpers/database-utils'
053aed43 6import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
7b6b445d 7import { sequelizeTypescript } from '@server/initializers/database'
1bb4c9ab 8import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models'
0305db28 9import { VideoResolution, VideoStorage } from '../../../shared/models/videos'
c729caf6 10import {
4ec52d04 11 buildFileMetadata,
c729caf6 12 canDoQuickTranscode,
84cae54e 13 computeResolutionsToTranscode,
c729caf6 14 getVideoStreamDuration,
c729caf6
C
15 getVideoStreamFPS,
16 transcodeVOD,
17 TranscodeVODOptions,
18 TranscodeVODOptionsType
19} from '../../helpers/ffmpeg'
c07902b9 20import { CONFIG } from '../../initializers/config'
c07902b9
C
21import { VideoFileModel } from '../../models/video/video-file'
22import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist'
1bb4c9ab
C
23import { updatePlaylistAfterFileChange } from '../hls'
24import { generateHLSVideoFilename, generateWebTorrentVideoFilename, getHlsResolutionPlaylistFilename } from '../paths'
0305db28 25import { VideoPathManager } from '../video-path-manager'
c729caf6 26import { VideoTranscodingProfilesManager } from './default-transcoding-profiles'
098eb377 27
658a47ab 28/**
6b67897e
C
29 *
30 * Functions that run transcoding functions, update the database, cleanup files, create torrent files...
31 * Mainly called by the job queue
32 *
658a47ab 33 */
6b67897e
C
34
35// Optimize the original video file and replace it. The resolution is not changed.
84cae54e
C
36function optimizeOriginalVideofile (options: {
37 video: MVideoFullLight
38 inputVideoFile: MVideoFile
39 job: Job
40}) {
41 const { video, inputVideoFile, job } = options
42
2fbd5e25 43 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
098eb377 44 const newExtname = '.mp4'
9f1ddd24 45
ad5db104 46 return VideoPathManager.Instance.makeAvailableVideoFile(inputVideoFile.withVideoOrPlaylist(video), async videoInputPath => {
0305db28 47 const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
098eb377 48
c729caf6 49 const transcodeType: TranscodeVODOptionsType = await canDoQuickTranscode(videoInputPath)
0305db28
JB
50 ? 'quick-transcode'
51 : 'video'
5ba49f26 52
84cae54e 53 const resolution = buildOriginalFileResolution(inputVideoFile.resolution)
318b0bd0 54
c729caf6 55 const transcodeOptions: TranscodeVODOptions = {
0305db28 56 type: transcodeType,
9252a33d 57
0305db28
JB
58 inputPath: videoInputPath,
59 outputPath: videoTranscodedPath,
9252a33d 60
0305db28
JB
61 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
62 profile: CONFIG.TRANSCODING.PROFILE,
9252a33d 63
0305db28 64 resolution,
3b01f4c0 65
0305db28
JB
66 job
67 }
098eb377 68
0305db28 69 // Could be very long!
c729caf6 70 await transcodeVOD(transcodeOptions)
098eb377 71
1d1da336 72 // Important to do this before getVideoFilename() to take in account the new filename
84cae54e 73 inputVideoFile.resolution = resolution
1d1da336
C
74 inputVideoFile.extname = newExtname
75 inputVideoFile.filename = generateWebTorrentVideoFilename(resolution, newExtname)
76 inputVideoFile.storage = VideoStorage.FILE_SYSTEM
098eb377 77
1d1da336 78 const videoOutputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, inputVideoFile)
2fbd5e25 79
1d1da336
C
80 const { videoFile } = await onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath)
81 await remove(videoInputPath)
098eb377 82
1d1da336 83 return { transcodeType, videoFile }
0305db28 84 })
098eb377
C
85}
86
84cae54e
C
87// Transcode the original video file to a lower resolution compatible with WebTorrent
88function transcodeNewWebTorrentResolution (options: {
89 video: MVideoFullLight
90 resolution: VideoResolution
91 job: Job
92}) {
93 const { video, resolution, job } = options
94
2fbd5e25 95 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
84cae54e 96 const newExtname = '.mp4'
098eb377 97
ad5db104 98 return VideoPathManager.Instance.makeAvailableVideoFile(video.getMaxQualityFile().withVideoOrPlaylist(video), async videoInputPath => {
0305db28
JB
99 const newVideoFile = new VideoFileModel({
100 resolution,
84cae54e
C
101 extname: newExtname,
102 filename: generateWebTorrentVideoFilename(resolution, newExtname),
0305db28
JB
103 size: 0,
104 videoId: video.id
105 })
098eb377 106
0305db28
JB
107 const videoOutputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, newVideoFile)
108 const videoTranscodedPath = join(transcodeDirectory, newVideoFile.filename)
90a8bd30 109
0305db28
JB
110 const transcodeOptions = resolution === VideoResolution.H_NOVIDEO
111 ? {
112 type: 'only-audio' as 'only-audio',
098eb377 113
0305db28
JB
114 inputPath: videoInputPath,
115 outputPath: videoTranscodedPath,
9252a33d 116
0305db28
JB
117 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
118 profile: CONFIG.TRANSCODING.PROFILE,
9252a33d 119
0305db28 120 resolution,
9252a33d 121
0305db28
JB
122 job
123 }
124 : {
125 type: 'video' as 'video',
126 inputPath: videoInputPath,
127 outputPath: videoTranscodedPath,
3b01f4c0 128
0305db28
JB
129 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
130 profile: CONFIG.TRANSCODING.PROFILE,
9252a33d 131
0305db28 132 resolution,
9252a33d 133
0305db28
JB
134 job
135 }
3b01f4c0 136
c729caf6 137 await transcodeVOD(transcodeOptions)
098eb377 138
0305db28
JB
139 return onWebTorrentVideoFileTranscoding(video, newVideoFile, videoTranscodedPath, videoOutputPath)
140 })
536598cf
C
141}
142
6b67897e 143// Merge an image with an audio file to create a video
84cae54e
C
144function mergeAudioVideofile (options: {
145 video: MVideoFullLight
146 resolution: VideoResolution
147 job: Job
148}) {
149 const { video, resolution, job } = options
150
536598cf
C
151 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
152 const newExtname = '.mp4'
153
92e0f42e 154 const inputVideoFile = video.getMinQualityFile()
2fbd5e25 155
ad5db104 156 return VideoPathManager.Instance.makeAvailableVideoFile(inputVideoFile.withVideoOrPlaylist(video), async audioInputPath => {
0305db28 157 const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
098eb377 158
0305db28
JB
159 // If the user updates the video preview during transcoding
160 const previewPath = video.getPreview().getPath()
161 const tmpPreviewPath = join(CONFIG.STORAGE.TMP_DIR, basename(previewPath))
162 await copyFile(previewPath, tmpPreviewPath)
eba06469 163
0305db28
JB
164 const transcodeOptions = {
165 type: 'merge-audio' as 'merge-audio',
9252a33d 166
0305db28
JB
167 inputPath: tmpPreviewPath,
168 outputPath: videoTranscodedPath,
9252a33d 169
0305db28
JB
170 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
171 profile: CONFIG.TRANSCODING.PROFILE,
9252a33d 172
0305db28
JB
173 audioPath: audioInputPath,
174 resolution,
3b01f4c0 175
0305db28
JB
176 job
177 }
098eb377 178
0305db28 179 try {
c729caf6 180 await transcodeVOD(transcodeOptions)
098eb377 181
0305db28
JB
182 await remove(audioInputPath)
183 await remove(tmpPreviewPath)
184 } catch (err) {
185 await remove(tmpPreviewPath)
186 throw err
187 }
098eb377 188
0305db28
JB
189 // Important to do this before getVideoFilename() to take in account the new file extension
190 inputVideoFile.extname = newExtname
482b2623 191 inputVideoFile.resolution = resolution
0305db28 192 inputVideoFile.filename = generateWebTorrentVideoFilename(inputVideoFile.resolution, newExtname)
536598cf 193
0305db28
JB
194 const videoOutputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, inputVideoFile)
195 // ffmpeg generated a new video file, so update the video duration
196 // See https://trac.ffmpeg.org/ticket/5456
c729caf6 197 video.duration = await getVideoStreamDuration(videoTranscodedPath)
0305db28 198 await video.save()
536598cf 199
0305db28
JB
200 return onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath)
201 })
098eb377
C
202}
203
2650d6d4 204// Concat TS segments from a live video to a fragmented mp4 HLS playlist
24516aa2 205async function generateHlsPlaylistResolutionFromTS (options: {
4ec52d04 206 video: MVideo
3851e732 207 concatenatedTsFilePath: string
2650d6d4 208 resolution: VideoResolution
e772bdf1 209 isAAC: boolean
2650d6d4 210}) {
3851e732
C
211 return generateHlsPlaylistCommon({
212 video: options.video,
213 resolution: options.resolution,
3851e732 214 inputPath: options.concatenatedTsFilePath,
e772bdf1
C
215 type: 'hls-from-ts' as 'hls-from-ts',
216 isAAC: options.isAAC
3851e732 217 })
2650d6d4
C
218}
219
6b67897e 220// Generate an HLS playlist from an input file, and update the master playlist
24516aa2 221function generateHlsPlaylistResolution (options: {
4ec52d04 222 video: MVideo
b5b68755
C
223 videoInputPath: string
224 resolution: VideoResolution
225 copyCodecs: boolean
3b01f4c0 226 job?: Job
b5b68755 227}) {
2650d6d4
C
228 return generateHlsPlaylistCommon({
229 video: options.video,
230 resolution: options.resolution,
231 copyCodecs: options.copyCodecs,
2650d6d4 232 inputPath: options.videoInputPath,
3b01f4c0
C
233 type: 'hls' as 'hls',
234 job: options.job
2650d6d4
C
235 })
236}
237
238// ---------------------------------------------------------------------------
239
240export {
24516aa2
C
241 generateHlsPlaylistResolution,
242 generateHlsPlaylistResolutionFromTS,
2650d6d4 243 optimizeOriginalVideofile,
24516aa2 244 transcodeNewWebTorrentResolution,
1bcb03a1 245 mergeAudioVideofile
2650d6d4
C
246}
247
248// ---------------------------------------------------------------------------
249
24516aa2 250async function onWebTorrentVideoFileTranscoding (
90a8bd30 251 video: MVideoFullLight,
24516aa2
C
252 videoFile: MVideoFile,
253 transcodingPath: string,
254 outputPath: string
255) {
2650d6d4 256 const stats = await stat(transcodingPath)
c729caf6
C
257 const fps = await getVideoStreamFPS(transcodingPath)
258 const metadata = await buildFileMetadata(transcodingPath)
2650d6d4
C
259
260 await move(transcodingPath, outputPath, { overwrite: true })
261
262 videoFile.size = stats.size
263 videoFile.fps = fps
264 videoFile.metadata = metadata
265
8efc27bf 266 await createTorrentAndSetInfoHash(video, videoFile)
2650d6d4 267
7b6b445d 268 const oldFile = await VideoFileModel.loadWebTorrentFile({ videoId: video.id, fps: videoFile.fps, resolution: videoFile.resolution })
1bb4c9ab 269 if (oldFile) await video.removeWebTorrentFile(oldFile)
7b6b445d 270
2650d6d4
C
271 await VideoFileModel.customUpsert(videoFile, 'video', undefined)
272 video.VideoFiles = await video.$get('VideoFiles')
273
0305db28 274 return { video, videoFile }
2650d6d4
C
275}
276
277async function generateHlsPlaylistCommon (options: {
278 type: 'hls' | 'hls-from-ts'
4ec52d04 279 video: MVideo
2650d6d4
C
280 inputPath: string
281 resolution: VideoResolution
282 copyCodecs?: boolean
e772bdf1 283 isAAC?: boolean
3b01f4c0
C
284
285 job?: Job
2650d6d4 286}) {
84cae54e 287 const { type, video, inputPath, resolution, copyCodecs, isAAC, job } = options
aaedadd5 288 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
b5b68755 289
9129b769 290 const videoTranscodedBasePath = join(transcodeDirectory, type)
aaedadd5 291 await ensureDir(videoTranscodedBasePath)
09209296 292
83903cb6 293 const videoFilename = generateHLSVideoFilename(resolution)
764b1a14
C
294 const resolutionPlaylistFilename = getHlsResolutionPlaylistFilename(videoFilename)
295 const resolutionPlaylistFileTranscodePath = join(videoTranscodedBasePath, resolutionPlaylistFilename)
09209296
C
296
297 const transcodeOptions = {
2650d6d4 298 type,
9252a33d 299
2650d6d4 300 inputPath,
764b1a14 301 outputPath: resolutionPlaylistFileTranscodePath,
9252a33d 302
529b3752 303 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
1896bca0 304 profile: CONFIG.TRANSCODING.PROFILE,
9252a33d 305
09209296 306 resolution,
d7a25329 307 copyCodecs,
4c280004 308
e772bdf1
C
309 isAAC,
310
4c280004 311 hlsPlaylist: {
d7a25329 312 videoFilename
3b01f4c0
C
313 },
314
315 job
09209296
C
316 }
317
c729caf6 318 await transcodeVOD(transcodeOptions)
09209296 319
aaedadd5 320 // Create or update the playlist
1bb4c9ab 321 const playlist = await retryTransactionWrapper(() => {
3b052510 322 return sequelizeTypescript.transaction(async transaction => {
1bb4c9ab 323 return VideoStreamingPlaylistModel.loadOrGenerate(video, transaction)
3b052510
C
324 })
325 })
d7a25329
C
326
327 const newVideoFile = new VideoFileModel({
328 resolution,
1bb4c9ab 329 extname: extnameUtil(videoFilename),
d7a25329 330 size: 0,
83903cb6 331 filename: videoFilename,
d7a25329 332 fps: -1,
764b1a14 333 videoStreamingPlaylistId: playlist.id
09209296 334 })
d7a25329 335
0305db28 336 const videoFilePath = VideoPathManager.Instance.getFSVideoFileOutputPath(playlist, newVideoFile)
0305db28 337 await ensureDir(VideoPathManager.Instance.getFSHLSOutputPath(video))
aaedadd5
C
338
339 // Move playlist file
0305db28 340 const resolutionPlaylistPath = VideoPathManager.Instance.getFSHLSOutputPath(video, resolutionPlaylistFilename)
764b1a14 341 await move(resolutionPlaylistFileTranscodePath, resolutionPlaylistPath, { overwrite: true })
aaedadd5 342 // Move video file
69eddafb 343 await move(join(videoTranscodedBasePath, videoFilename), videoFilePath, { overwrite: true })
aaedadd5 344
d7a25329
C
345 const stats = await stat(videoFilePath)
346
347 newVideoFile.size = stats.size
c729caf6
C
348 newVideoFile.fps = await getVideoStreamFPS(videoFilePath)
349 newVideoFile.metadata = await buildFileMetadata(videoFilePath)
d7a25329 350
764b1a14 351 await createTorrentAndSetInfoHash(playlist, newVideoFile)
d7a25329 352
7b6b445d 353 const oldFile = await VideoFileModel.loadHLSFile({ playlistId: playlist.id, fps: newVideoFile.fps, resolution: newVideoFile.resolution })
1bb4c9ab
C
354 if (oldFile) {
355 await video.removeStreamingPlaylistVideoFile(playlist, oldFile)
356 await oldFile.destroy()
357 }
7b6b445d 358
0305db28 359 const savedVideoFile = await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined)
d7a25329 360
1bb4c9ab 361 await updatePlaylistAfterFileChange(video, playlist)
d7a25329 362
0305db28 363 return { resolutionPlaylistPath, videoFile: savedVideoFile }
098eb377 364}
84cae54e
C
365
366function buildOriginalFileResolution (inputResolution: number) {
367 if (CONFIG.TRANSCODING.ALWAYS_TRANSCODE_ORIGINAL_RESOLUTION === true) return toEven(inputResolution)
368
5e2afe42 369 const resolutions = computeResolutionsToTranscode({ input: inputResolution, type: 'vod', includeInput: false, strictLower: false })
84cae54e
C
370 if (resolutions.length === 0) return toEven(inputResolution)
371
372 return Math.max(...resolutions)
373}