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