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, VideoStorage } 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 { CONFIG } from '../../initializers/config'
12 import { P2P_MEDIA_LOADER_PEER_VERSION } from '../../initializers/constants'
13 import { VideoFileModel } from '../../models/video/video-file'
14 import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist'
15 import { updateMasterHLSPlaylist, updateSha256VODSegments } from '../hls'
17 generateHLSMasterPlaylistFilename,
18 generateHlsSha256SegmentsFilename,
19 generateHLSVideoFilename,
20 generateWebTorrentVideoFilename,
21 getHlsResolutionPlaylistFilename
23 import { VideoPathManager } from '../video-path-manager'
24 import { VideoTranscodingProfilesManager } from './video-transcoding-profiles'
28 * Functions that run transcoding functions, update the database, cleanup files, create torrent files...
29 * Mainly called by the job queue
33 // Optimize the original video file and replace it. The resolution is not changed.
34 function optimizeOriginalVideofile (video: MVideoFullLight, inputVideoFile: MVideoFile, job?: Job) {
35 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
36 const newExtname = '.mp4'
38 return VideoPathManager.Instance.makeAvailableVideoFile(video, inputVideoFile, async videoInputPath => {
39 const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
41 const transcodeType: TranscodeOptionsType = await canDoQuickTranscode(videoInputPath)
45 const resolution = toEven(inputVideoFile.resolution)
47 const transcodeOptions: TranscodeOptions = {
50 inputPath: videoInputPath,
51 outputPath: videoTranscodedPath,
53 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
54 profile: CONFIG.TRANSCODING.PROFILE,
61 // Could be very long!
62 await transcode(transcodeOptions)
64 // Important to do this before getVideoFilename() to take in account the new filename
65 inputVideoFile.extname = newExtname
66 inputVideoFile.filename = generateWebTorrentVideoFilename(resolution, newExtname)
67 inputVideoFile.storage = VideoStorage.FILE_SYSTEM
69 const videoOutputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, inputVideoFile)
71 const { videoFile } = await onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath)
72 await remove(videoInputPath)
74 return { transcodeType, videoFile }
78 // Transcode the original video file to a lower resolution
79 // We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed
80 function transcodeNewWebTorrentResolution (video: MVideoFullLight, resolution: VideoResolution, isPortrait: boolean, job: Job) {
81 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
82 const extname = '.mp4'
84 return VideoPathManager.Instance.makeAvailableVideoFile(video, video.getMaxQualityFile(), async videoInputPath => {
85 const newVideoFile = new VideoFileModel({
88 filename: generateWebTorrentVideoFilename(resolution, extname),
93 const videoOutputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, newVideoFile)
94 const videoTranscodedPath = join(transcodeDirectory, newVideoFile.filename)
96 const transcodeOptions = resolution === VideoResolution.H_NOVIDEO
98 type: 'only-audio' as 'only-audio',
100 inputPath: videoInputPath,
101 outputPath: videoTranscodedPath,
103 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
104 profile: CONFIG.TRANSCODING.PROFILE,
111 type: 'video' as 'video',
112 inputPath: videoInputPath,
113 outputPath: videoTranscodedPath,
115 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
116 profile: CONFIG.TRANSCODING.PROFILE,
119 isPortraitMode: isPortrait,
124 await transcode(transcodeOptions)
126 return onWebTorrentVideoFileTranscoding(video, newVideoFile, videoTranscodedPath, videoOutputPath)
130 // Merge an image with an audio file to create a video
131 function mergeAudioVideofile (video: MVideoFullLight, resolution: VideoResolution, job: Job) {
132 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
133 const newExtname = '.mp4'
135 const inputVideoFile = video.getMinQualityFile()
137 return VideoPathManager.Instance.makeAvailableVideoFile(video, inputVideoFile, async audioInputPath => {
138 const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
140 // If the user updates the video preview during transcoding
141 const previewPath = video.getPreview().getPath()
142 const tmpPreviewPath = join(CONFIG.STORAGE.TMP_DIR, basename(previewPath))
143 await copyFile(previewPath, tmpPreviewPath)
145 const transcodeOptions = {
146 type: 'merge-audio' as 'merge-audio',
148 inputPath: tmpPreviewPath,
149 outputPath: videoTranscodedPath,
151 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
152 profile: CONFIG.TRANSCODING.PROFILE,
154 audioPath: audioInputPath,
161 await transcode(transcodeOptions)
163 await remove(audioInputPath)
164 await remove(tmpPreviewPath)
166 await remove(tmpPreviewPath)
170 // Important to do this before getVideoFilename() to take in account the new file extension
171 inputVideoFile.extname = newExtname
172 inputVideoFile.filename = generateWebTorrentVideoFilename(inputVideoFile.resolution, newExtname)
174 const videoOutputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, inputVideoFile)
175 // ffmpeg generated a new video file, so update the video duration
176 // See https://trac.ffmpeg.org/ticket/5456
177 video.duration = await getDurationFromVideoFile(videoTranscodedPath)
180 return onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath)
184 // Concat TS segments from a live video to a fragmented mp4 HLS playlist
185 async function generateHlsPlaylistResolutionFromTS (options: {
186 video: MVideoFullLight
187 concatenatedTsFilePath: string
188 resolution: VideoResolution
189 isPortraitMode: boolean
192 return generateHlsPlaylistCommon({
193 video: options.video,
194 resolution: options.resolution,
195 isPortraitMode: options.isPortraitMode,
196 inputPath: options.concatenatedTsFilePath,
197 type: 'hls-from-ts' as 'hls-from-ts',
202 // Generate an HLS playlist from an input file, and update the master playlist
203 function generateHlsPlaylistResolution (options: {
204 video: MVideoFullLight
205 videoInputPath: string
206 resolution: VideoResolution
208 isPortraitMode: boolean
211 return generateHlsPlaylistCommon({
212 video: options.video,
213 resolution: options.resolution,
214 copyCodecs: options.copyCodecs,
215 isPortraitMode: options.isPortraitMode,
216 inputPath: options.videoInputPath,
217 type: 'hls' as 'hls',
222 // ---------------------------------------------------------------------------
225 generateHlsPlaylistResolution,
226 generateHlsPlaylistResolutionFromTS,
227 optimizeOriginalVideofile,
228 transcodeNewWebTorrentResolution,
232 // ---------------------------------------------------------------------------
234 async function onWebTorrentVideoFileTranscoding (
235 video: MVideoFullLight,
236 videoFile: MVideoFile,
237 transcodingPath: string,
240 const stats = await stat(transcodingPath)
241 const fps = await getVideoFileFPS(transcodingPath)
242 const metadata = await getMetadataFromFile(transcodingPath)
244 await move(transcodingPath, outputPath, { overwrite: true })
246 videoFile.size = stats.size
248 videoFile.metadata = metadata
250 await createTorrentAndSetInfoHash(video, videoFile)
252 await VideoFileModel.customUpsert(videoFile, 'video', undefined)
253 video.VideoFiles = await video.$get('VideoFiles')
255 return { video, videoFile }
258 async function generateHlsPlaylistCommon (options: {
259 type: 'hls' | 'hls-from-ts'
260 video: MVideoFullLight
262 resolution: VideoResolution
265 isPortraitMode: boolean
269 const { type, video, inputPath, resolution, copyCodecs, isPortraitMode, isAAC, job } = options
270 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
272 const videoTranscodedBasePath = join(transcodeDirectory, type)
273 await ensureDir(videoTranscodedBasePath)
275 const videoFilename = generateHLSVideoFilename(resolution)
276 const resolutionPlaylistFilename = getHlsResolutionPlaylistFilename(videoFilename)
277 const resolutionPlaylistFileTranscodePath = join(videoTranscodedBasePath, resolutionPlaylistFilename)
279 const transcodeOptions = {
283 outputPath: resolutionPlaylistFileTranscodePath,
285 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
286 profile: CONFIG.TRANSCODING.PROFILE,
301 await transcode(transcodeOptions)
303 // Create or update the playlist
304 const playlist = await VideoStreamingPlaylistModel.loadOrGenerate(video)
306 if (!playlist.playlistFilename) {
307 playlist.playlistFilename = generateHLSMasterPlaylistFilename(video.isLive)
310 if (!playlist.segmentsSha256Filename) {
311 playlist.segmentsSha256Filename = generateHlsSha256SegmentsFilename(video.isLive)
314 playlist.p2pMediaLoaderInfohashes = []
315 playlist.p2pMediaLoaderPeerVersion = P2P_MEDIA_LOADER_PEER_VERSION
317 playlist.type = VideoStreamingPlaylistType.HLS
319 await playlist.save()
321 // Build the new playlist file
322 const extname = extnameUtil(videoFilename)
323 const newVideoFile = new VideoFileModel({
327 filename: videoFilename,
329 videoStreamingPlaylistId: playlist.id
332 const videoFilePath = VideoPathManager.Instance.getFSVideoFileOutputPath(playlist, newVideoFile)
334 // Move files from tmp transcoded directory to the appropriate place
335 await ensureDir(VideoPathManager.Instance.getFSHLSOutputPath(video))
337 // Move playlist file
338 const resolutionPlaylistPath = VideoPathManager.Instance.getFSHLSOutputPath(video, resolutionPlaylistFilename)
339 await move(resolutionPlaylistFileTranscodePath, resolutionPlaylistPath, { overwrite: true })
341 await move(join(videoTranscodedBasePath, videoFilename), videoFilePath, { overwrite: true })
343 const stats = await stat(videoFilePath)
345 newVideoFile.size = stats.size
346 newVideoFile.fps = await getVideoFileFPS(videoFilePath)
347 newVideoFile.metadata = await getMetadataFromFile(videoFilePath)
349 await createTorrentAndSetInfoHash(playlist, newVideoFile)
351 const savedVideoFile = await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined)
353 const playlistWithFiles = playlist as MStreamingPlaylistFilesVideo
354 playlistWithFiles.VideoFiles = await playlist.$get('VideoFiles')
355 playlist.assignP2PMediaLoaderInfoHashes(video, playlistWithFiles.VideoFiles)
357 await playlist.save()
359 video.setHLSPlaylist(playlist)
361 await updateMasterHLSPlaylist(video, playlistWithFiles)
362 await updateSha256VODSegments(video, playlistWithFiles)
364 return { resolutionPlaylistPath, videoFile: savedVideoFile }