1 import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION, WEBSERVER } from '../initializers/constants'
2 import { basename, join } from 'path'
5 getDurationFromVideoFile,
10 } from '../helpers/ffmpeg-utils'
11 import { copyFile, ensureDir, move, remove, stat } from 'fs-extra'
12 import { logger } from '../helpers/logger'
13 import { VideoResolution } from '../../shared/models/videos'
14 import { VideoFileModel } from '../models/video/video-file'
15 import { updateMasterHLSPlaylist, updateSha256Segments } from './hls'
16 import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
17 import { VideoStreamingPlaylistType } from '../../shared/models/videos/video-streaming-playlist.type'
18 import { CONFIG } from '../initializers/config'
19 import { MVideoFile, MVideoWithFile, MVideoWithFileThumbnail } from '@server/typings/models'
22 * Optimize the original video file and replace it. The resolution is not changed.
24 async function optimizeVideofile (video: MVideoWithFile, inputVideoFileArg?: MVideoFile) {
25 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
26 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
27 const newExtname = '.mp4'
29 const inputVideoFile = inputVideoFileArg ? inputVideoFileArg : video.getOriginalFile()
30 const videoInputPath = join(videosDirectory, video.getVideoFilename(inputVideoFile))
31 const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
33 const transcodeType: TranscodeOptionsType = await canDoQuickTranscode(videoInputPath)
37 const transcodeOptions: TranscodeOptions = {
38 type: transcodeType as any, // FIXME: typing issue
39 inputPath: videoInputPath,
40 outputPath: videoTranscodedPath,
41 resolution: inputVideoFile.resolution
44 // Could be very long!
45 await transcode(transcodeOptions)
48 await remove(videoInputPath)
50 // Important to do this before getVideoFilename() to take in account the new file extension
51 inputVideoFile.extname = newExtname
53 const videoOutputPath = video.getVideoFilePath(inputVideoFile)
55 await onVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath)
57 // Auto destruction...
58 video.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', { err }))
65 * Transcode the original video file to a lower resolution.
67 async function transcodeOriginalVideofile (video: MVideoWithFile, resolution: VideoResolution, isPortrait: boolean) {
68 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
69 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
70 const extname = '.mp4'
72 // We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed
73 const videoInputPath = join(videosDirectory, video.getVideoFilename(video.getOriginalFile()))
75 const newVideoFile = new VideoFileModel({
81 const videoOutputPath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename(newVideoFile))
82 const videoTranscodedPath = join(transcodeDirectory, video.getVideoFilename(newVideoFile))
84 const transcodeOptions = {
85 type: 'video' as 'video',
86 inputPath: videoInputPath,
87 outputPath: videoTranscodedPath,
89 isPortraitMode: isPortrait
92 await transcode(transcodeOptions)
94 return onVideoFileTranscoding(video, newVideoFile, videoTranscodedPath, videoOutputPath)
97 async function mergeAudioVideofile (video: MVideoWithFileThumbnail, resolution: VideoResolution) {
98 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
99 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
100 const newExtname = '.mp4'
102 const inputVideoFile = video.getOriginalFile()
104 const audioInputPath = join(videosDirectory, video.getVideoFilename(video.getOriginalFile()))
105 const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
107 // If the user updates the video preview during transcoding
108 const previewPath = video.getPreview().getPath()
109 const tmpPreviewPath = join(CONFIG.STORAGE.TMP_DIR, basename(previewPath))
110 await copyFile(previewPath, tmpPreviewPath)
112 const transcodeOptions = {
113 type: 'merge-audio' as 'merge-audio',
114 inputPath: tmpPreviewPath,
115 outputPath: videoTranscodedPath,
116 audioPath: audioInputPath,
121 await transcode(transcodeOptions)
123 await remove(audioInputPath)
124 await remove(tmpPreviewPath)
126 await remove(tmpPreviewPath)
130 // Important to do this before getVideoFilename() to take in account the new file extension
131 inputVideoFile.extname = newExtname
133 const videoOutputPath = video.getVideoFilePath(inputVideoFile)
134 // ffmpeg generated a new video file, so update the video duration
135 // See https://trac.ffmpeg.org/ticket/5456
136 video.duration = await getDurationFromVideoFile(videoTranscodedPath)
139 return onVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath)
142 async function generateHlsPlaylist (video: MVideoWithFile, resolution: VideoResolution, isPortraitMode: boolean) {
143 const baseHlsDirectory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid)
144 await ensureDir(join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid))
146 const videoInputPath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename(video.getFile(resolution)))
147 const outputPath = join(baseHlsDirectory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(resolution))
149 const transcodeOptions = {
150 type: 'hls' as 'hls',
151 inputPath: videoInputPath,
157 videoFilename: VideoStreamingPlaylistModel.getHlsVideoName(video.uuid, resolution)
161 await transcode(transcodeOptions)
163 await updateMasterHLSPlaylist(video)
164 await updateSha256Segments(video)
166 const playlistUrl = WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsMasterPlaylistStaticPath(video.uuid)
168 await VideoStreamingPlaylistModel.upsert({
171 segmentsSha256Url: WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsSha256SegmentsStaticPath(video.uuid),
172 p2pMediaLoaderInfohashes: VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(playlistUrl, video.VideoFiles),
173 p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION,
175 type: VideoStreamingPlaylistType.HLS
179 // ---------------------------------------------------------------------------
184 transcodeOriginalVideofile,
188 // ---------------------------------------------------------------------------
190 async function onVideoFileTranscoding (video: MVideoWithFile, videoFile: MVideoFile, transcodingPath: string, outputPath: string) {
191 const stats = await stat(transcodingPath)
192 const fps = await getVideoFileFPS(transcodingPath)
194 await move(transcodingPath, outputPath)
196 videoFile.size = stats.size
199 await video.createTorrentAndSetInfoHash(videoFile)
201 const updatedVideoFile = await videoFile.save()
203 // Add it if this is a new created file
204 if (video.VideoFiles.some(f => f.id === videoFile.id) === false) {
205 video.VideoFiles.push(updatedVideoFile)