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