1 import { Job } from 'bull'
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 getVideoStreamDuration,
17 TranscodeVODOptionsType
18 } from '../../helpers/ffmpeg'
19 import { CONFIG } from '../../initializers/config'
20 import { VideoFileModel } from '../../models/video/video-file'
21 import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist'
22 import { updatePlaylistAfterFileChange } from '../hls'
23 import { generateHLSVideoFilename, generateWebTorrentVideoFilename, getHlsResolutionPlaylistFilename } from '../paths'
24 import { VideoPathManager } from '../video-path-manager'
25 import { VideoTranscodingProfilesManager } from './default-transcoding-profiles'
29 * Functions that run transcoding functions, update the database, cleanup files, create torrent files...
30 * Mainly called by the job queue
34 // Optimize the original video file and replace it. The resolution is not changed.
35 function optimizeOriginalVideofile (video: MVideoFullLight, inputVideoFile: MVideoFile, job?: Job) {
36 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
37 const newExtname = '.mp4'
39 return VideoPathManager.Instance.makeAvailableVideoFile(inputVideoFile.withVideoOrPlaylist(video), async videoInputPath => {
40 const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
42 const transcodeType: TranscodeVODOptionsType = await canDoQuickTranscode(videoInputPath)
46 const resolution = toEven(inputVideoFile.resolution)
48 const transcodeOptions: TranscodeVODOptions = {
51 inputPath: videoInputPath,
52 outputPath: videoTranscodedPath,
54 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
55 profile: CONFIG.TRANSCODING.PROFILE,
62 // Could be very long!
63 await transcodeVOD(transcodeOptions)
65 // Important to do this before getVideoFilename() to take in account the new filename
66 inputVideoFile.extname = newExtname
67 inputVideoFile.filename = generateWebTorrentVideoFilename(resolution, newExtname)
68 inputVideoFile.storage = VideoStorage.FILE_SYSTEM
70 const videoOutputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, inputVideoFile)
72 const { videoFile } = await onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath)
73 await remove(videoInputPath)
75 return { transcodeType, videoFile }
79 // Transcode the original video file to a lower resolution
80 // We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed
81 function transcodeNewWebTorrentResolution (video: MVideoFullLight, resolution: VideoResolution, isPortrait: boolean, job: Job) {
82 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
83 const extname = '.mp4'
85 return VideoPathManager.Instance.makeAvailableVideoFile(video.getMaxQualityFile().withVideoOrPlaylist(video), async videoInputPath => {
86 const newVideoFile = new VideoFileModel({
89 filename: generateWebTorrentVideoFilename(resolution, extname),
94 const videoOutputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, newVideoFile)
95 const videoTranscodedPath = join(transcodeDirectory, newVideoFile.filename)
97 const transcodeOptions = resolution === VideoResolution.H_NOVIDEO
99 type: 'only-audio' as 'only-audio',
101 inputPath: videoInputPath,
102 outputPath: videoTranscodedPath,
104 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
105 profile: CONFIG.TRANSCODING.PROFILE,
112 type: 'video' as 'video',
113 inputPath: videoInputPath,
114 outputPath: videoTranscodedPath,
116 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
117 profile: CONFIG.TRANSCODING.PROFILE,
120 isPortraitMode: isPortrait,
125 await transcodeVOD(transcodeOptions)
127 return onWebTorrentVideoFileTranscoding(video, newVideoFile, videoTranscodedPath, videoOutputPath)
131 // Merge an image with an audio file to create a video
132 function mergeAudioVideofile (video: MVideoFullLight, resolution: VideoResolution, job: Job) {
133 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
134 const newExtname = '.mp4'
136 const inputVideoFile = video.getMinQualityFile()
138 return VideoPathManager.Instance.makeAvailableVideoFile(inputVideoFile.withVideoOrPlaylist(video), async audioInputPath => {
139 const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
141 // If the user updates the video preview during transcoding
142 const previewPath = video.getPreview().getPath()
143 const tmpPreviewPath = join(CONFIG.STORAGE.TMP_DIR, basename(previewPath))
144 await copyFile(previewPath, tmpPreviewPath)
146 const transcodeOptions = {
147 type: 'merge-audio' as 'merge-audio',
149 inputPath: tmpPreviewPath,
150 outputPath: videoTranscodedPath,
152 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
153 profile: CONFIG.TRANSCODING.PROFILE,
155 audioPath: audioInputPath,
162 await transcodeVOD(transcodeOptions)
164 await remove(audioInputPath)
165 await remove(tmpPreviewPath)
167 await remove(tmpPreviewPath)
171 // Important to do this before getVideoFilename() to take in account the new file extension
172 inputVideoFile.extname = newExtname
173 inputVideoFile.resolution = resolution
174 inputVideoFile.filename = generateWebTorrentVideoFilename(inputVideoFile.resolution, newExtname)
176 const videoOutputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, inputVideoFile)
177 // ffmpeg generated a new video file, so update the video duration
178 // See https://trac.ffmpeg.org/ticket/5456
179 video.duration = await getVideoStreamDuration(videoTranscodedPath)
182 return onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath)
186 // Concat TS segments from a live video to a fragmented mp4 HLS playlist
187 async function generateHlsPlaylistResolutionFromTS (options: {
189 concatenatedTsFilePath: string
190 resolution: VideoResolution
191 isPortraitMode: boolean
194 return generateHlsPlaylistCommon({
195 video: options.video,
196 resolution: options.resolution,
197 isPortraitMode: options.isPortraitMode,
198 inputPath: options.concatenatedTsFilePath,
199 type: 'hls-from-ts' as 'hls-from-ts',
204 // Generate an HLS playlist from an input file, and update the master playlist
205 function generateHlsPlaylistResolution (options: {
207 videoInputPath: string
208 resolution: VideoResolution
210 isPortraitMode: boolean
213 return generateHlsPlaylistCommon({
214 video: options.video,
215 resolution: options.resolution,
216 copyCodecs: options.copyCodecs,
217 isPortraitMode: options.isPortraitMode,
218 inputPath: options.videoInputPath,
219 type: 'hls' as 'hls',
224 // ---------------------------------------------------------------------------
227 generateHlsPlaylistResolution,
228 generateHlsPlaylistResolutionFromTS,
229 optimizeOriginalVideofile,
230 transcodeNewWebTorrentResolution,
234 // ---------------------------------------------------------------------------
236 async function onWebTorrentVideoFileTranscoding (
237 video: MVideoFullLight,
238 videoFile: MVideoFile,
239 transcodingPath: string,
242 const stats = await stat(transcodingPath)
243 const fps = await getVideoStreamFPS(transcodingPath)
244 const metadata = await buildFileMetadata(transcodingPath)
246 await move(transcodingPath, outputPath, { overwrite: true })
248 videoFile.size = stats.size
250 videoFile.metadata = metadata
252 await createTorrentAndSetInfoHash(video, videoFile)
254 const oldFile = await VideoFileModel.loadWebTorrentFile({ videoId: video.id, fps: videoFile.fps, resolution: videoFile.resolution })
255 if (oldFile) await video.removeWebTorrentFile(oldFile)
257 await VideoFileModel.customUpsert(videoFile, 'video', undefined)
258 video.VideoFiles = await video.$get('VideoFiles')
260 return { video, videoFile }
263 async function generateHlsPlaylistCommon (options: {
264 type: 'hls' | 'hls-from-ts'
267 resolution: VideoResolution
270 isPortraitMode: boolean
274 const { type, video, inputPath, resolution, copyCodecs, isPortraitMode, isAAC, job } = options
275 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
277 const videoTranscodedBasePath = join(transcodeDirectory, type)
278 await ensureDir(videoTranscodedBasePath)
280 const videoFilename = generateHLSVideoFilename(resolution)
281 const resolutionPlaylistFilename = getHlsResolutionPlaylistFilename(videoFilename)
282 const resolutionPlaylistFileTranscodePath = join(videoTranscodedBasePath, resolutionPlaylistFilename)
284 const transcodeOptions = {
288 outputPath: resolutionPlaylistFileTranscodePath,
290 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
291 profile: CONFIG.TRANSCODING.PROFILE,
306 await transcodeVOD(transcodeOptions)
308 // Create or update the playlist
309 const playlist = await retryTransactionWrapper(() => {
310 return sequelizeTypescript.transaction(async transaction => {
311 return VideoStreamingPlaylistModel.loadOrGenerate(video, transaction)
315 const newVideoFile = new VideoFileModel({
317 extname: extnameUtil(videoFilename),
319 filename: videoFilename,
321 videoStreamingPlaylistId: playlist.id
324 const videoFilePath = VideoPathManager.Instance.getFSVideoFileOutputPath(playlist, newVideoFile)
325 await ensureDir(VideoPathManager.Instance.getFSHLSOutputPath(video))
327 // Move playlist file
328 const resolutionPlaylistPath = VideoPathManager.Instance.getFSHLSOutputPath(video, resolutionPlaylistFilename)
329 await move(resolutionPlaylistFileTranscodePath, resolutionPlaylistPath, { overwrite: true })
331 await move(join(videoTranscodedBasePath, videoFilename), videoFilePath, { overwrite: true })
333 const stats = await stat(videoFilePath)
335 newVideoFile.size = stats.size
336 newVideoFile.fps = await getVideoStreamFPS(videoFilePath)
337 newVideoFile.metadata = await buildFileMetadata(videoFilePath)
339 await createTorrentAndSetInfoHash(playlist, newVideoFile)
341 const oldFile = await VideoFileModel.loadHLSFile({ playlistId: playlist.id, fps: newVideoFile.fps, resolution: newVideoFile.resolution })
343 await video.removeStreamingPlaylistVideoFile(playlist, oldFile)
344 await oldFile.destroy()
347 const savedVideoFile = await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined)
349 await updatePlaylistAfterFileChange(video, playlist)
351 return { resolutionPlaylistPath, videoFile: savedVideoFile }