1 import { Job } from 'bullmq'
2 import { copyFile, move, remove, stat } from 'fs-extra'
3 import { basename, join } from 'path'
4 import { computeOutputFPS } from '@server/helpers/ffmpeg'
5 import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
6 import { MVideoFile, MVideoFullLight } from '@server/types/models'
7 import { toEven } from '@shared/core-utils'
8 import { ffprobePromise, getVideoStreamDuration, getVideoStreamFPS, TranscodeVODOptionsType } from '@shared/ffmpeg'
9 import { VideoResolution, VideoStorage } from '@shared/models'
10 import { CONFIG } from '../../initializers/config'
11 import { VideoFileModel } from '../../models/video/video-file'
12 import { generateWebTorrentVideoFilename } from '../paths'
13 import { buildFileMetadata } from '../video-file'
14 import { VideoPathManager } from '../video-path-manager'
15 import { buildFFmpegVOD } from './shared'
16 import { computeResolutionsToTranscode } from './transcoding-resolutions'
17 import { VideoModel } from '@server/models/video/video'
19 // Optimize the original video file and replace it. The resolution is not changed.
20 export async function optimizeOriginalVideofile (options: {
21 video: MVideoFullLight
22 inputVideoFile: MVideoFile
23 quickTranscode: boolean
26 const { video, inputVideoFile, quickTranscode, job } = options
28 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
29 const newExtname = '.mp4'
31 // Will be released by our transcodeVOD function once ffmpeg is ran
32 const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid)
36 await inputVideoFile.reload()
38 const fileWithVideoOrPlaylist = inputVideoFile.withVideoOrPlaylist(video)
40 const result = await VideoPathManager.Instance.makeAvailableVideoFile(fileWithVideoOrPlaylist, async videoInputPath => {
41 const videoOutputPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
43 const transcodeType: TranscodeVODOptionsType = quickTranscode
47 const resolution = buildOriginalFileResolution(inputVideoFile.resolution)
48 const fps = computeOutputFPS({ inputFPS: inputVideoFile.fps, resolution })
50 // Could be very long!
51 await buildFFmpegVOD(job).transcode({
54 inputPath: videoInputPath,
55 outputPath: videoOutputPath,
57 inputFileMutexReleaser,
63 // Important to do this before getVideoFilename() to take in account the new filename
64 inputVideoFile.resolution = resolution
65 inputVideoFile.extname = newExtname
66 inputVideoFile.filename = generateWebTorrentVideoFilename(resolution, newExtname)
67 inputVideoFile.storage = VideoStorage.FILE_SYSTEM
69 const { videoFile } = await onWebTorrentVideoFileTranscoding({
71 videoFile: inputVideoFile,
75 await remove(videoInputPath)
77 return { transcodeType, videoFile }
82 inputFileMutexReleaser()
86 // Transcode the original video file to a lower resolution compatible with WebTorrent
87 export async function transcodeNewWebTorrentResolution (options: {
88 video: MVideoFullLight
89 resolution: VideoResolution
93 const { video: videoArg, resolution, fps, job } = options
95 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
96 const newExtname = '.mp4'
98 const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(videoArg.uuid)
101 const video = await VideoModel.loadFull(videoArg.uuid)
102 const file = video.getMaxQualityFile().withVideoOrPlaylist(video)
104 const result = await VideoPathManager.Instance.makeAvailableVideoFile(file, async videoInputPath => {
105 const newVideoFile = new VideoFileModel({
108 filename: generateWebTorrentVideoFilename(resolution, newExtname),
113 const videoOutputPath = join(transcodeDirectory, newVideoFile.filename)
115 const transcodeOptions = {
116 type: 'video' as 'video',
118 inputPath: videoInputPath,
119 outputPath: videoOutputPath,
121 inputFileMutexReleaser,
127 await buildFFmpegVOD(job).transcode(transcodeOptions)
129 return onWebTorrentVideoFileTranscoding({ video, videoFile: newVideoFile, videoOutputPath })
134 inputFileMutexReleaser()
138 // Merge an image with an audio file to create a video
139 export async function mergeAudioVideofile (options: {
140 video: MVideoFullLight
141 resolution: VideoResolution
145 const { video: videoArg, resolution, fps, job } = options
147 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
148 const newExtname = '.mp4'
150 const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(videoArg.uuid)
153 const video = await VideoModel.loadFull(videoArg.uuid)
154 const inputVideoFile = video.getMinQualityFile()
156 const fileWithVideoOrPlaylist = inputVideoFile.withVideoOrPlaylist(video)
158 const result = await VideoPathManager.Instance.makeAvailableVideoFile(fileWithVideoOrPlaylist, async audioInputPath => {
159 const videoOutputPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
161 // If the user updates the video preview during transcoding
162 const previewPath = video.getPreview().getPath()
163 const tmpPreviewPath = join(CONFIG.STORAGE.TMP_DIR, basename(previewPath))
164 await copyFile(previewPath, tmpPreviewPath)
166 const transcodeOptions = {
167 type: 'merge-audio' as 'merge-audio',
169 inputPath: tmpPreviewPath,
170 outputPath: videoOutputPath,
172 inputFileMutexReleaser,
174 audioPath: audioInputPath,
180 await buildFFmpegVOD(job).transcode(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 // ffmpeg generated a new video file, so update the video duration
195 // See https://trac.ffmpeg.org/ticket/5456
196 video.duration = await getVideoStreamDuration(videoOutputPath)
199 return onWebTorrentVideoFileTranscoding({
201 videoFile: inputVideoFile,
208 inputFileMutexReleaser()
212 export async function onWebTorrentVideoFileTranscoding (options: {
213 video: MVideoFullLight
214 videoFile: MVideoFile
215 videoOutputPath: string
217 const { video, videoFile, videoOutputPath } = options
219 const mutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid)
224 const outputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, videoFile)
226 const stats = await stat(videoOutputPath)
228 const probe = await ffprobePromise(videoOutputPath)
229 const fps = await getVideoStreamFPS(videoOutputPath, probe)
230 const metadata = await buildFileMetadata(videoOutputPath, probe)
232 await move(videoOutputPath, outputPath, { overwrite: true })
234 videoFile.size = stats.size
236 videoFile.metadata = metadata
238 await createTorrentAndSetInfoHash(video, videoFile)
240 const oldFile = await VideoFileModel.loadWebTorrentFile({ videoId: video.id, fps: videoFile.fps, resolution: videoFile.resolution })
241 if (oldFile) await video.removeWebTorrentFile(oldFile)
243 await VideoFileModel.customUpsert(videoFile, 'video', undefined)
244 video.VideoFiles = await video.$get('VideoFiles')
246 return { video, videoFile }
252 // ---------------------------------------------------------------------------
254 function buildOriginalFileResolution (inputResolution: number) {
255 if (CONFIG.TRANSCODING.ALWAYS_TRANSCODE_ORIGINAL_RESOLUTION === true) {
256 return toEven(inputResolution)
259 const resolutions = computeResolutionsToTranscode({
260 input: inputResolution,
264 // We don't really care about the audio resolution in this context
268 if (resolutions.length === 0) {
269 return toEven(inputResolution)
272 return Math.max(...resolutions)