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 { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
6 import { MStreamingPlaylistFilesVideo, MVideoFile, MVideoFullLight } from '@server/types/models'
7 import { VideoResolution } from '../../../shared/models/videos'
8 import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
9 import { transcode, TranscodeOptions, TranscodeOptionsType } from '../../helpers/ffmpeg-utils'
10 import { canDoQuickTranscode, getDurationFromVideoFile, getMetadataFromFile, getVideoFileFPS } from '../../helpers/ffprobe-utils'
11 import { logger } from '../../helpers/logger'
12 import { CONFIG } from '../../initializers/config'
13 import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION } from '../../initializers/constants'
14 import { VideoFileModel } from '../../models/video/video-file'
15 import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist'
16 import { updateMasterHLSPlaylist, updateSha256VODSegments } from '../hls'
18 generateHLSMasterPlaylistFilename,
19 generateHlsSha256SegmentsFilename,
20 generateHLSVideoFilename,
21 generateWebTorrentVideoFilename,
22 getHlsResolutionPlaylistFilename,
24 } from '../video-paths'
25 import { VideoTranscodingProfilesManager } from './video-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 async function optimizeOriginalVideofile (video: MVideoFullLight, inputVideoFile: MVideoFile, job?: Job) {
36 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
37 const newExtname = '.mp4'
39 const videoInputPath = getVideoFilePath(video, inputVideoFile)
40 const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
42 const transcodeType: TranscodeOptionsType = await canDoQuickTranscode(videoInputPath)
46 const resolution = toEven(inputVideoFile.resolution)
48 const transcodeOptions: TranscodeOptions = {
51 inputPath: videoInputPath,
52 outputPath: videoTranscodedPath,
54 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
55 profile: CONFIG.TRANSCODING.PROFILE,
62 // Could be very long!
63 await transcode(transcodeOptions)
66 await remove(videoInputPath)
68 // Important to do this before getVideoFilename() to take in account the new filename
69 inputVideoFile.extname = newExtname
70 inputVideoFile.filename = generateWebTorrentVideoFilename(resolution, newExtname)
72 const videoOutputPath = getVideoFilePath(video, inputVideoFile)
74 await onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath)
78 // Auto destruction...
79 video.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', { err }))
85 // Transcode the original video file to a lower resolution.
86 async function transcodeNewWebTorrentResolution (video: MVideoFullLight, resolution: VideoResolution, isPortrait: boolean, job: Job) {
87 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
88 const extname = '.mp4'
90 // We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed
91 const videoInputPath = getVideoFilePath(video, video.getMaxQualityFile())
93 const newVideoFile = new VideoFileModel({
96 filename: generateWebTorrentVideoFilename(resolution, extname),
101 const videoOutputPath = getVideoFilePath(video, newVideoFile)
102 const videoTranscodedPath = join(transcodeDirectory, newVideoFile.filename)
104 const transcodeOptions = resolution === VideoResolution.H_NOVIDEO
106 type: 'only-audio' as 'only-audio',
108 inputPath: videoInputPath,
109 outputPath: videoTranscodedPath,
111 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
112 profile: CONFIG.TRANSCODING.PROFILE,
119 type: 'video' as 'video',
120 inputPath: videoInputPath,
121 outputPath: videoTranscodedPath,
123 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
124 profile: CONFIG.TRANSCODING.PROFILE,
127 isPortraitMode: isPortrait,
132 await transcode(transcodeOptions)
134 return onWebTorrentVideoFileTranscoding(video, newVideoFile, videoTranscodedPath, videoOutputPath)
137 // Merge an image with an audio file to create a video
138 async function mergeAudioVideofile (video: MVideoFullLight, resolution: VideoResolution, job: Job) {
139 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
140 const newExtname = '.mp4'
142 const inputVideoFile = video.getMinQualityFile()
144 const audioInputPath = getVideoFilePath(video, inputVideoFile)
145 const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
147 // If the user updates the video preview during transcoding
148 const previewPath = video.getPreview().getPath()
149 const tmpPreviewPath = join(CONFIG.STORAGE.TMP_DIR, basename(previewPath))
150 await copyFile(previewPath, tmpPreviewPath)
152 const transcodeOptions = {
153 type: 'merge-audio' as 'merge-audio',
155 inputPath: tmpPreviewPath,
156 outputPath: videoTranscodedPath,
158 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
159 profile: CONFIG.TRANSCODING.PROFILE,
161 audioPath: audioInputPath,
168 await transcode(transcodeOptions)
170 await remove(audioInputPath)
171 await remove(tmpPreviewPath)
173 await remove(tmpPreviewPath)
177 // Important to do this before getVideoFilename() to take in account the new file extension
178 inputVideoFile.extname = newExtname
179 inputVideoFile.filename = generateWebTorrentVideoFilename(inputVideoFile.resolution, newExtname)
181 const videoOutputPath = getVideoFilePath(video, inputVideoFile)
182 // ffmpeg generated a new video file, so update the video duration
183 // See https://trac.ffmpeg.org/ticket/5456
184 video.duration = await getDurationFromVideoFile(videoTranscodedPath)
187 return onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath)
190 // Concat TS segments from a live video to a fragmented mp4 HLS playlist
191 async function generateHlsPlaylistResolutionFromTS (options: {
192 video: MVideoFullLight
193 concatenatedTsFilePath: string
194 resolution: VideoResolution
195 isPortraitMode: boolean
198 return generateHlsPlaylistCommon({
199 video: options.video,
200 resolution: options.resolution,
201 isPortraitMode: options.isPortraitMode,
202 inputPath: options.concatenatedTsFilePath,
203 type: 'hls-from-ts' as 'hls-from-ts',
208 // Generate an HLS playlist from an input file, and update the master playlist
209 function generateHlsPlaylistResolution (options: {
210 video: MVideoFullLight
211 videoInputPath: string
212 resolution: VideoResolution
214 isPortraitMode: boolean
217 return generateHlsPlaylistCommon({
218 video: options.video,
219 resolution: options.resolution,
220 copyCodecs: options.copyCodecs,
221 isPortraitMode: options.isPortraitMode,
222 inputPath: options.videoInputPath,
223 type: 'hls' as 'hls',
228 // ---------------------------------------------------------------------------
231 generateHlsPlaylistResolution,
232 generateHlsPlaylistResolutionFromTS,
233 optimizeOriginalVideofile,
234 transcodeNewWebTorrentResolution,
238 // ---------------------------------------------------------------------------
240 async function onWebTorrentVideoFileTranscoding (
241 video: MVideoFullLight,
242 videoFile: MVideoFile,
243 transcodingPath: string,
246 const stats = await stat(transcodingPath)
247 const fps = await getVideoFileFPS(transcodingPath)
248 const metadata = await getMetadataFromFile(transcodingPath)
250 await move(transcodingPath, outputPath, { overwrite: true })
252 videoFile.size = stats.size
254 videoFile.metadata = metadata
256 await createTorrentAndSetInfoHash(video, videoFile)
258 await VideoFileModel.customUpsert(videoFile, 'video', undefined)
259 video.VideoFiles = await video.$get('VideoFiles')
264 async function generateHlsPlaylistCommon (options: {
265 type: 'hls' | 'hls-from-ts'
266 video: MVideoFullLight
268 resolution: VideoResolution
271 isPortraitMode: boolean
275 const { type, video, inputPath, resolution, copyCodecs, isPortraitMode, isAAC, job } = options
276 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
278 const videoTranscodedBasePath = join(transcodeDirectory, type)
279 await ensureDir(videoTranscodedBasePath)
281 const videoFilename = generateHLSVideoFilename(resolution)
282 const resolutionPlaylistFilename = getHlsResolutionPlaylistFilename(videoFilename)
283 const resolutionPlaylistFileTranscodePath = join(videoTranscodedBasePath, resolutionPlaylistFilename)
285 const transcodeOptions = {
289 outputPath: resolutionPlaylistFileTranscodePath,
291 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
292 profile: CONFIG.TRANSCODING.PROFILE,
307 await transcode(transcodeOptions)
309 // Create or update the playlist
310 const playlist = await VideoStreamingPlaylistModel.loadOrGenerate(video)
312 if (!playlist.playlistFilename) {
313 playlist.playlistFilename = generateHLSMasterPlaylistFilename(video.isLive)
316 if (!playlist.segmentsSha256Filename) {
317 playlist.segmentsSha256Filename = generateHlsSha256SegmentsFilename(video.isLive)
320 playlist.p2pMediaLoaderInfohashes = []
321 playlist.p2pMediaLoaderPeerVersion = P2P_MEDIA_LOADER_PEER_VERSION
323 playlist.type = VideoStreamingPlaylistType.HLS
325 await playlist.save()
327 // Build the new playlist file
328 const extname = extnameUtil(videoFilename)
329 const newVideoFile = new VideoFileModel({
333 filename: videoFilename,
335 videoStreamingPlaylistId: playlist.id
338 const videoFilePath = getVideoFilePath(playlist, newVideoFile)
340 // Move files from tmp transcoded directory to the appropriate place
341 const baseHlsDirectory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid)
342 await ensureDir(baseHlsDirectory)
344 // Move playlist file
345 const resolutionPlaylistPath = join(baseHlsDirectory, resolutionPlaylistFilename)
346 await move(resolutionPlaylistFileTranscodePath, resolutionPlaylistPath, { overwrite: true })
348 await move(join(videoTranscodedBasePath, videoFilename), videoFilePath, { overwrite: true })
350 const stats = await stat(videoFilePath)
352 newVideoFile.size = stats.size
353 newVideoFile.fps = await getVideoFileFPS(videoFilePath)
354 newVideoFile.metadata = await getMetadataFromFile(videoFilePath)
356 await createTorrentAndSetInfoHash(playlist, newVideoFile)
358 await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined)
360 const playlistWithFiles = playlist as MStreamingPlaylistFilesVideo
361 playlistWithFiles.VideoFiles = await playlist.$get('VideoFiles')
362 playlist.assignP2PMediaLoaderInfoHashes(video, playlistWithFiles.VideoFiles)
364 await playlist.save()
366 video.setHLSPlaylist(playlist)
368 await updateMasterHLSPlaylist(video, playlistWithFiles)
369 await updateSha256VODSegments(video, playlistWithFiles)
371 return resolutionPlaylistPath