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(inputVideoFile.withVideoOrPlaylist(video), 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.getMaxQualityFile().withVideoOrPlaylist(video), 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(inputVideoFile.withVideoOrPlaylist(video), 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.resolution = resolution
173 inputVideoFile.filename = generateWebTorrentVideoFilename(inputVideoFile.resolution, newExtname)
175 const videoOutputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, inputVideoFile)
176 // ffmpeg generated a new video file, so update the video duration
177 // See https://trac.ffmpeg.org/ticket/5456
178 video.duration = await getDurationFromVideoFile(videoTranscodedPath)
181 return onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath)
185 // Concat TS segments from a live video to a fragmented mp4 HLS playlist
186 async function generateHlsPlaylistResolutionFromTS (options: {
187 video: MVideoFullLight
188 concatenatedTsFilePath: string
189 resolution: VideoResolution
190 isPortraitMode: boolean
193 return generateHlsPlaylistCommon({
194 video: options.video,
195 resolution: options.resolution,
196 isPortraitMode: options.isPortraitMode,
197 inputPath: options.concatenatedTsFilePath,
198 type: 'hls-from-ts' as 'hls-from-ts',
203 // Generate an HLS playlist from an input file, and update the master playlist
204 function generateHlsPlaylistResolution (options: {
205 video: MVideoFullLight
206 videoInputPath: string
207 resolution: VideoResolution
209 isPortraitMode: boolean
212 return generateHlsPlaylistCommon({
213 video: options.video,
214 resolution: options.resolution,
215 copyCodecs: options.copyCodecs,
216 isPortraitMode: options.isPortraitMode,
217 inputPath: options.videoInputPath,
218 type: 'hls' as 'hls',
223 // ---------------------------------------------------------------------------
226 generateHlsPlaylistResolution,
227 generateHlsPlaylistResolutionFromTS,
228 optimizeOriginalVideofile,
229 transcodeNewWebTorrentResolution,
233 // ---------------------------------------------------------------------------
235 async function onWebTorrentVideoFileTranscoding (
236 video: MVideoFullLight,
237 videoFile: MVideoFile,
238 transcodingPath: string,
241 const stats = await stat(transcodingPath)
242 const fps = await getVideoFileFPS(transcodingPath)
243 const metadata = await getMetadataFromFile(transcodingPath)
245 await move(transcodingPath, outputPath, { overwrite: true })
247 videoFile.size = stats.size
249 videoFile.metadata = metadata
251 await createTorrentAndSetInfoHash(video, videoFile)
253 await VideoFileModel.customUpsert(videoFile, 'video', undefined)
254 video.VideoFiles = await video.$get('VideoFiles')
256 return { video, videoFile }
259 async function generateHlsPlaylistCommon (options: {
260 type: 'hls' | 'hls-from-ts'
261 video: MVideoFullLight
263 resolution: VideoResolution
266 isPortraitMode: boolean
270 const { type, video, inputPath, resolution, copyCodecs, isPortraitMode, isAAC, job } = options
271 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
273 const videoTranscodedBasePath = join(transcodeDirectory, type)
274 await ensureDir(videoTranscodedBasePath)
276 const videoFilename = generateHLSVideoFilename(resolution)
277 const resolutionPlaylistFilename = getHlsResolutionPlaylistFilename(videoFilename)
278 const resolutionPlaylistFileTranscodePath = join(videoTranscodedBasePath, resolutionPlaylistFilename)
280 const transcodeOptions = {
284 outputPath: resolutionPlaylistFileTranscodePath,
286 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
287 profile: CONFIG.TRANSCODING.PROFILE,
302 await transcode(transcodeOptions)
304 // Create or update the playlist
305 const playlist = await VideoStreamingPlaylistModel.loadOrGenerate(video)
307 if (!playlist.playlistFilename) {
308 playlist.playlistFilename = generateHLSMasterPlaylistFilename(video.isLive)
311 if (!playlist.segmentsSha256Filename) {
312 playlist.segmentsSha256Filename = generateHlsSha256SegmentsFilename(video.isLive)
315 playlist.p2pMediaLoaderInfohashes = []
316 playlist.p2pMediaLoaderPeerVersion = P2P_MEDIA_LOADER_PEER_VERSION
318 playlist.type = VideoStreamingPlaylistType.HLS
320 await playlist.save()
322 // Build the new playlist file
323 const extname = extnameUtil(videoFilename)
324 const newVideoFile = new VideoFileModel({
328 filename: videoFilename,
330 videoStreamingPlaylistId: playlist.id
333 const videoFilePath = VideoPathManager.Instance.getFSVideoFileOutputPath(playlist, newVideoFile)
335 // Move files from tmp transcoded directory to the appropriate place
336 await ensureDir(VideoPathManager.Instance.getFSHLSOutputPath(video))
338 // Move playlist file
339 const resolutionPlaylistPath = VideoPathManager.Instance.getFSHLSOutputPath(video, resolutionPlaylistFilename)
340 await move(resolutionPlaylistFileTranscodePath, resolutionPlaylistPath, { overwrite: true })
342 await move(join(videoTranscodedBasePath, videoFilename), videoFilePath, { overwrite: true })
344 const stats = await stat(videoFilePath)
346 newVideoFile.size = stats.size
347 newVideoFile.fps = await getVideoFileFPS(videoFilePath)
348 newVideoFile.metadata = await getMetadataFromFile(videoFilePath)
350 await createTorrentAndSetInfoHash(playlist, newVideoFile)
352 const savedVideoFile = await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined)
354 const playlistWithFiles = playlist as MStreamingPlaylistFilesVideo
355 playlistWithFiles.VideoFiles = await playlist.$get('VideoFiles')
356 playlist.assignP2PMediaLoaderInfoHashes(video, playlistWithFiles.VideoFiles)
358 await playlist.save()
360 video.setHLSPlaylist(playlist)
362 await updateMasterHLSPlaylist(video, playlistWithFiles)
363 await updateSha256VODSegments(video, playlistWithFiles)
365 return { resolutionPlaylistPath, videoFile: savedVideoFile }