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'
15 computeResolutionsToTranscode,
17 getVideoStreamDuration,
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'
33 * Functions that run transcoding functions, update the database, cleanup files, create torrent files...
34 * Mainly called by the job queue
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
44 const { video, inputVideoFile, job } = options
46 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
47 const newExtname = '.mp4'
49 const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid)
54 const fileWithVideoOrPlaylist = inputVideoFile.withVideoOrPlaylist(video)
56 const result = await VideoPathManager.Instance.makeAvailableVideoFile(fileWithVideoOrPlaylist, async videoInputPath => {
57 const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
59 const transcodeType: TranscodeVODOptionsType = await canDoQuickTranscode(videoInputPath)
63 const resolution = buildOriginalFileResolution(inputVideoFile.resolution)
65 const transcodeOptions: TranscodeVODOptions = {
68 inputPath: videoInputPath,
69 outputPath: videoTranscodedPath,
71 inputFileMutexReleaser,
73 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
74 profile: CONFIG.TRANSCODING.PROFILE,
81 // Could be very long!
82 await transcodeVOD(transcodeOptions)
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
90 const { videoFile } = await onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, inputVideoFile)
91 await remove(videoInputPath)
93 return { transcodeType, videoFile }
98 inputFileMutexReleaser()
102 // Transcode the original video file to a lower resolution compatible with WebTorrent
103 async function transcodeNewWebTorrentResolution (options: {
104 video: MVideoFullLight
105 resolution: VideoResolution
108 const { video, resolution, job } = options
110 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
111 const newExtname = '.mp4'
113 const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid)
118 const file = video.getMaxQualityFile().withVideoOrPlaylist(video)
120 const result = await VideoPathManager.Instance.makeAvailableVideoFile(file, async videoInputPath => {
121 const newVideoFile = new VideoFileModel({
124 filename: generateWebTorrentVideoFilename(resolution, newExtname),
129 const videoTranscodedPath = join(transcodeDirectory, newVideoFile.filename)
131 const transcodeOptions = resolution === VideoResolution.H_NOVIDEO
133 type: 'only-audio' as 'only-audio',
135 inputPath: videoInputPath,
136 outputPath: videoTranscodedPath,
138 inputFileMutexReleaser,
140 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
141 profile: CONFIG.TRANSCODING.PROFILE,
148 type: 'video' as 'video',
149 inputPath: videoInputPath,
150 outputPath: videoTranscodedPath,
152 inputFileMutexReleaser,
154 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
155 profile: CONFIG.TRANSCODING.PROFILE,
162 await transcodeVOD(transcodeOptions)
164 return onWebTorrentVideoFileTranscoding(video, newVideoFile, videoTranscodedPath, newVideoFile)
169 inputFileMutexReleaser()
173 // Merge an image with an audio file to create a video
174 async function mergeAudioVideofile (options: {
175 video: MVideoFullLight
176 resolution: VideoResolution
179 const { video, resolution, job } = options
181 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
182 const newExtname = '.mp4'
184 const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid)
189 const inputVideoFile = video.getMinQualityFile()
191 const fileWithVideoOrPlaylist = inputVideoFile.withVideoOrPlaylist(video)
193 const result = await VideoPathManager.Instance.makeAvailableVideoFile(fileWithVideoOrPlaylist, async audioInputPath => {
194 const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
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)
201 const transcodeOptions = {
202 type: 'merge-audio' as 'merge-audio',
204 inputPath: tmpPreviewPath,
205 outputPath: videoTranscodedPath,
207 inputFileMutexReleaser,
209 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
210 profile: CONFIG.TRANSCODING.PROFILE,
212 audioPath: audioInputPath,
219 await transcodeVOD(transcodeOptions)
221 await remove(audioInputPath)
222 await remove(tmpPreviewPath)
224 await remove(tmpPreviewPath)
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)
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)
238 return onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, inputVideoFile)
243 inputFileMutexReleaser()
247 // Concat TS segments from a live video to a fragmented mp4 HLS playlist
248 async function generateHlsPlaylistResolutionFromTS (options: {
250 concatenatedTsFilePath: string
251 resolution: VideoResolution
253 inputFileMutexReleaser: MutexInterface.Releaser
255 return generateHlsPlaylistCommon({
256 type: 'hls-from-ts' as 'hls-from-ts',
257 inputPath: options.concatenatedTsFilePath,
259 ...pick(options, [ 'video', 'resolution', 'inputFileMutexReleaser', 'isAAC' ])
263 // Generate an HLS playlist from an input file, and update the master playlist
264 function generateHlsPlaylistResolution (options: {
266 videoInputPath: string
267 resolution: VideoResolution
269 inputFileMutexReleaser: MutexInterface.Releaser
272 return generateHlsPlaylistCommon({
273 type: 'hls' as 'hls',
274 inputPath: options.videoInputPath,
276 ...pick(options, [ 'video', 'resolution', 'copyCodecs', 'inputFileMutexReleaser', 'job' ])
280 // ---------------------------------------------------------------------------
283 generateHlsPlaylistResolution,
284 generateHlsPlaylistResolutionFromTS,
285 optimizeOriginalVideofile,
286 transcodeNewWebTorrentResolution,
290 // ---------------------------------------------------------------------------
292 async function onWebTorrentVideoFileTranscoding (
293 video: MVideoFullLight,
294 videoFile: MVideoFile,
295 transcodingPath: string,
296 newVideoFile: MVideoFile
298 const mutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid)
303 const outputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, newVideoFile)
305 const stats = await stat(transcodingPath)
307 const probe = await ffprobePromise(transcodingPath)
308 const fps = await getVideoStreamFPS(transcodingPath, probe)
309 const metadata = await buildFileMetadata(transcodingPath, probe)
311 await move(transcodingPath, outputPath, { overwrite: true })
313 videoFile.size = stats.size
315 videoFile.metadata = metadata
317 await createTorrentAndSetInfoHash(video, videoFile)
319 const oldFile = await VideoFileModel.loadWebTorrentFile({ videoId: video.id, fps: videoFile.fps, resolution: videoFile.resolution })
320 if (oldFile) await video.removeWebTorrentFile(oldFile)
322 await VideoFileModel.customUpsert(videoFile, 'video', undefined)
323 video.VideoFiles = await video.$get('VideoFiles')
325 return { video, videoFile }
331 async function generateHlsPlaylistCommon (options: {
332 type: 'hls' | 'hls-from-ts'
335 resolution: VideoResolution
337 inputFileMutexReleaser: MutexInterface.Releaser
344 const { type, video, inputPath, resolution, copyCodecs, isAAC, job, inputFileMutexReleaser } = options
345 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
347 const videoTranscodedBasePath = join(transcodeDirectory, type)
348 await ensureDir(videoTranscodedBasePath)
350 const videoFilename = generateHLSVideoFilename(resolution)
351 const resolutionPlaylistFilename = getHlsResolutionPlaylistFilename(videoFilename)
352 const resolutionPlaylistFileTranscodePath = join(videoTranscodedBasePath, resolutionPlaylistFilename)
354 const transcodeOptions = {
358 outputPath: resolutionPlaylistFileTranscodePath,
360 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
361 profile: CONFIG.TRANSCODING.PROFILE,
368 inputFileMutexReleaser,
377 await transcodeVOD(transcodeOptions)
379 // Create or update the playlist
380 const playlist = await retryTransactionWrapper(() => {
381 return sequelizeTypescript.transaction(async transaction => {
382 return VideoStreamingPlaylistModel.loadOrGenerate(video, transaction)
386 const newVideoFile = new VideoFileModel({
388 extname: extnameUtil(videoFilename),
390 filename: videoFilename,
392 videoStreamingPlaylistId: playlist.id
395 const mutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid)
398 // VOD transcoding is a long task, refresh video attributes
401 const videoFilePath = VideoPathManager.Instance.getFSVideoFileOutputPath(playlist, newVideoFile)
402 await ensureDir(VideoPathManager.Instance.getFSHLSOutputPath(video))
404 // Move playlist file
405 const resolutionPlaylistPath = VideoPathManager.Instance.getFSHLSOutputPath(video, resolutionPlaylistFilename)
406 await move(resolutionPlaylistFileTranscodePath, resolutionPlaylistPath, { overwrite: true })
408 await move(join(videoTranscodedBasePath, videoFilename), videoFilePath, { overwrite: true })
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)
416 const stats = await stat(videoFilePath)
418 newVideoFile.size = stats.size
419 newVideoFile.fps = await getVideoStreamFPS(videoFilePath)
420 newVideoFile.metadata = await buildFileMetadata(videoFilePath)
422 await createTorrentAndSetInfoHash(playlist, newVideoFile)
424 const oldFile = await VideoFileModel.loadHLSFile({
425 playlistId: playlist.id,
426 fps: newVideoFile.fps,
427 resolution: newVideoFile.resolution
431 await video.removeStreamingPlaylistVideoFile(playlist, oldFile)
432 await oldFile.destroy()
435 const savedVideoFile = await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined)
437 await updatePlaylistAfterFileChange(video, playlist)
439 return { resolutionPlaylistPath, videoFile: savedVideoFile }
445 function buildOriginalFileResolution (inputResolution: number) {
446 if (CONFIG.TRANSCODING.ALWAYS_TRANSCODE_ORIGINAL_RESOLUTION === true) return toEven(inputResolution)
448 const resolutions = computeResolutionsToTranscode({ input: inputResolution, type: 'vod', includeInput: false, strictLower: false })
449 if (resolutions.length === 0) return toEven(inputResolution)
451 return Math.max(...resolutions)