1 import { Job } from 'bullmq'
2 import { copyFile, ensureDir, move, remove, stat } from 'fs-extra'
3 import { basename, extname as extnameUtil, join } from 'path'
4 import { toEven } from '@server/helpers/core-utils'
5 import { retryTransactionWrapper } from '@server/helpers/database-utils'
6 import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
7 import { sequelizeTypescript } from '@server/initializers/database'
8 import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models'
9 import { VideoResolution, VideoStorage } from '../../../shared/models/videos'
13 computeResolutionsToTranscode,
14 getVideoStreamDuration,
18 TranscodeVODOptionsType
19 } from '../../helpers/ffmpeg'
20 import { CONFIG } from '../../initializers/config'
21 import { VideoFileModel } from '../../models/video/video-file'
22 import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist'
23 import { updatePlaylistAfterFileChange } from '../hls'
24 import { generateHLSVideoFilename, generateWebTorrentVideoFilename, getHlsResolutionPlaylistFilename } from '../paths'
25 import { VideoPathManager } from '../video-path-manager'
26 import { VideoTranscodingProfilesManager } from './default-transcoding-profiles'
30 * Functions that run transcoding functions, update the database, cleanup files, create torrent files...
31 * Mainly called by the job queue
35 // Optimize the original video file and replace it. The resolution is not changed.
36 function optimizeOriginalVideofile (options: {
37 video: MVideoFullLight
38 inputVideoFile: MVideoFile
41 const { video, inputVideoFile, job } = options
43 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
44 const newExtname = '.mp4'
46 return VideoPathManager.Instance.makeAvailableVideoFile(inputVideoFile.withVideoOrPlaylist(video), async videoInputPath => {
47 const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
49 const transcodeType: TranscodeVODOptionsType = await canDoQuickTranscode(videoInputPath)
53 const resolution = buildOriginalFileResolution(inputVideoFile.resolution)
55 const transcodeOptions: TranscodeVODOptions = {
58 inputPath: videoInputPath,
59 outputPath: videoTranscodedPath,
61 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
62 profile: CONFIG.TRANSCODING.PROFILE,
69 // Could be very long!
70 await transcodeVOD(transcodeOptions)
72 // Important to do this before getVideoFilename() to take in account the new filename
73 inputVideoFile.resolution = resolution
74 inputVideoFile.extname = newExtname
75 inputVideoFile.filename = generateWebTorrentVideoFilename(resolution, newExtname)
76 inputVideoFile.storage = VideoStorage.FILE_SYSTEM
78 const videoOutputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, inputVideoFile)
80 const { videoFile } = await onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath)
81 await remove(videoInputPath)
83 return { transcodeType, videoFile }
87 // Transcode the original video file to a lower resolution compatible with WebTorrent
88 function transcodeNewWebTorrentResolution (options: {
89 video: MVideoFullLight
90 resolution: VideoResolution
93 const { video, resolution, job } = options
95 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
96 const newExtname = '.mp4'
98 return VideoPathManager.Instance.makeAvailableVideoFile(video.getMaxQualityFile().withVideoOrPlaylist(video), async videoInputPath => {
99 const newVideoFile = new VideoFileModel({
102 filename: generateWebTorrentVideoFilename(resolution, newExtname),
107 const videoOutputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, newVideoFile)
108 const videoTranscodedPath = join(transcodeDirectory, newVideoFile.filename)
110 const transcodeOptions = resolution === VideoResolution.H_NOVIDEO
112 type: 'only-audio' as 'only-audio',
114 inputPath: videoInputPath,
115 outputPath: videoTranscodedPath,
117 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
118 profile: CONFIG.TRANSCODING.PROFILE,
125 type: 'video' as 'video',
126 inputPath: videoInputPath,
127 outputPath: videoTranscodedPath,
129 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
130 profile: CONFIG.TRANSCODING.PROFILE,
137 await transcodeVOD(transcodeOptions)
139 return onWebTorrentVideoFileTranscoding(video, newVideoFile, videoTranscodedPath, videoOutputPath)
143 // Merge an image with an audio file to create a video
144 function mergeAudioVideofile (options: {
145 video: MVideoFullLight
146 resolution: VideoResolution
149 const { video, resolution, job } = options
151 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
152 const newExtname = '.mp4'
154 const inputVideoFile = video.getMinQualityFile()
156 return VideoPathManager.Instance.makeAvailableVideoFile(inputVideoFile.withVideoOrPlaylist(video), async audioInputPath => {
157 const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
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)
164 const transcodeOptions = {
165 type: 'merge-audio' as 'merge-audio',
167 inputPath: tmpPreviewPath,
168 outputPath: videoTranscodedPath,
170 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
171 profile: CONFIG.TRANSCODING.PROFILE,
173 audioPath: audioInputPath,
180 await transcodeVOD(transcodeOptions)
182 await remove(audioInputPath)
183 await remove(tmpPreviewPath)
185 await remove(tmpPreviewPath)
189 // Important to do this before getVideoFilename() to take in account the new file extension
190 inputVideoFile.extname = newExtname
191 inputVideoFile.resolution = resolution
192 inputVideoFile.filename = generateWebTorrentVideoFilename(inputVideoFile.resolution, newExtname)
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
197 video.duration = await getVideoStreamDuration(videoTranscodedPath)
200 return onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath)
204 // Concat TS segments from a live video to a fragmented mp4 HLS playlist
205 async function generateHlsPlaylistResolutionFromTS (options: {
207 concatenatedTsFilePath: string
208 resolution: VideoResolution
211 return generateHlsPlaylistCommon({
212 video: options.video,
213 resolution: options.resolution,
214 inputPath: options.concatenatedTsFilePath,
215 type: 'hls-from-ts' as 'hls-from-ts',
220 // Generate an HLS playlist from an input file, and update the master playlist
221 function generateHlsPlaylistResolution (options: {
223 videoInputPath: string
224 resolution: VideoResolution
228 return generateHlsPlaylistCommon({
229 video: options.video,
230 resolution: options.resolution,
231 copyCodecs: options.copyCodecs,
232 inputPath: options.videoInputPath,
233 type: 'hls' as 'hls',
238 // ---------------------------------------------------------------------------
241 generateHlsPlaylistResolution,
242 generateHlsPlaylistResolutionFromTS,
243 optimizeOriginalVideofile,
244 transcodeNewWebTorrentResolution,
248 // ---------------------------------------------------------------------------
250 async function onWebTorrentVideoFileTranscoding (
251 video: MVideoFullLight,
252 videoFile: MVideoFile,
253 transcodingPath: string,
256 const stats = await stat(transcodingPath)
257 const fps = await getVideoStreamFPS(transcodingPath)
258 const metadata = await buildFileMetadata(transcodingPath)
260 await move(transcodingPath, outputPath, { overwrite: true })
262 videoFile.size = stats.size
264 videoFile.metadata = metadata
266 await createTorrentAndSetInfoHash(video, videoFile)
268 const oldFile = await VideoFileModel.loadWebTorrentFile({ videoId: video.id, fps: videoFile.fps, resolution: videoFile.resolution })
269 if (oldFile) await video.removeWebTorrentFile(oldFile)
271 await VideoFileModel.customUpsert(videoFile, 'video', undefined)
272 video.VideoFiles = await video.$get('VideoFiles')
274 return { video, videoFile }
277 async function generateHlsPlaylistCommon (options: {
278 type: 'hls' | 'hls-from-ts'
281 resolution: VideoResolution
287 const { type, video, inputPath, resolution, copyCodecs, isAAC, job } = options
288 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
290 const videoTranscodedBasePath = join(transcodeDirectory, type)
291 await ensureDir(videoTranscodedBasePath)
293 const videoFilename = generateHLSVideoFilename(resolution)
294 const resolutionPlaylistFilename = getHlsResolutionPlaylistFilename(videoFilename)
295 const resolutionPlaylistFileTranscodePath = join(videoTranscodedBasePath, resolutionPlaylistFilename)
297 const transcodeOptions = {
301 outputPath: resolutionPlaylistFileTranscodePath,
303 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
304 profile: CONFIG.TRANSCODING.PROFILE,
318 await transcodeVOD(transcodeOptions)
320 // Create or update the playlist
321 const playlist = await retryTransactionWrapper(() => {
322 return sequelizeTypescript.transaction(async transaction => {
323 return VideoStreamingPlaylistModel.loadOrGenerate(video, transaction)
327 const newVideoFile = new VideoFileModel({
329 extname: extnameUtil(videoFilename),
331 filename: videoFilename,
333 videoStreamingPlaylistId: playlist.id
336 const videoFilePath = VideoPathManager.Instance.getFSVideoFileOutputPath(playlist, newVideoFile)
337 await ensureDir(VideoPathManager.Instance.getFSHLSOutputPath(video))
339 // Move playlist file
340 const resolutionPlaylistPath = VideoPathManager.Instance.getFSHLSOutputPath(video, resolutionPlaylistFilename)
341 await move(resolutionPlaylistFileTranscodePath, resolutionPlaylistPath, { overwrite: true })
343 await move(join(videoTranscodedBasePath, videoFilename), videoFilePath, { overwrite: true })
345 const stats = await stat(videoFilePath)
347 newVideoFile.size = stats.size
348 newVideoFile.fps = await getVideoStreamFPS(videoFilePath)
349 newVideoFile.metadata = await buildFileMetadata(videoFilePath)
351 await createTorrentAndSetInfoHash(playlist, newVideoFile)
353 const oldFile = await VideoFileModel.loadHLSFile({ playlistId: playlist.id, fps: newVideoFile.fps, resolution: newVideoFile.resolution })
355 await video.removeStreamingPlaylistVideoFile(playlist, oldFile)
356 await oldFile.destroy()
359 const savedVideoFile = await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined)
361 await updatePlaylistAfterFileChange(video, playlist)
363 return { resolutionPlaylistPath, videoFile: savedVideoFile }
366 function buildOriginalFileResolution (inputResolution: number) {
367 if (CONFIG.TRANSCODING.ALWAYS_TRANSCODE_ORIGINAL_RESOLUTION === true) return toEven(inputResolution)
369 const resolutions = computeResolutionsToTranscode({ input: inputResolution, type: 'vod', includeInput: false, strictLower: false })
370 if (resolutions.length === 0) return toEven(inputResolution)
372 return Math.max(...resolutions)