]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/lib/transcoding/video-transcoding.ts
Generate random uuid for video files
[github/Chocobozzz/PeerTube.git] / server / lib / transcoding / video-transcoding.ts
CommitLineData
3b01f4c0 1import { Job } from 'bull'
3851e732 2import { copyFile, ensureDir, move, remove, stat } from 'fs-extra'
d7a25329 3import { basename, extname as extnameUtil, join } from 'path'
318b0bd0 4import { toEven } from '@server/helpers/core-utils'
053aed43 5import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
90a8bd30 6import { MStreamingPlaylistFilesVideo, MVideoFile, MVideoFullLight } from '@server/types/models'
c07902b9
C
7import { VideoResolution } from '../../../shared/models/videos'
8import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
9import { transcode, TranscodeOptions, TranscodeOptionsType } from '../../helpers/ffmpeg-utils'
10import { canDoQuickTranscode, getDurationFromVideoFile, getMetadataFromFile, getVideoFileFPS } from '../../helpers/ffprobe-utils'
11import { logger } from '../../helpers/logger'
12import { CONFIG } from '../../initializers/config'
13import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION, WEBSERVER } from '../../initializers/constants'
14import { VideoFileModel } from '../../models/video/video-file'
15import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist'
16import { updateMasterHLSPlaylist, updateSha256VODSegments } from '../hls'
83903cb6 17import { generateHLSVideoFilename, generateWebTorrentVideoFilename, getVideoFilePath } from '../video-paths'
529b3752 18import { VideoTranscodingProfilesManager } from './video-transcoding-profiles'
098eb377 19
658a47ab 20/**
6b67897e
C
21 *
22 * Functions that run transcoding functions, update the database, cleanup files, create torrent files...
23 * Mainly called by the job queue
24 *
658a47ab 25 */
6b67897e
C
26
27// Optimize the original video file and replace it. The resolution is not changed.
90a8bd30 28async function optimizeOriginalVideofile (video: MVideoFullLight, inputVideoFile: MVideoFile, job?: Job) {
2fbd5e25 29 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
098eb377 30 const newExtname = '.mp4'
9f1ddd24 31
d7a25329 32 const videoInputPath = getVideoFilePath(video, inputVideoFile)
2fbd5e25 33 const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
098eb377 34
536598cf
C
35 const transcodeType: TranscodeOptionsType = await canDoQuickTranscode(videoInputPath)
36 ? 'quick-transcode'
37 : 'video'
5ba49f26 38
318b0bd0
C
39 const resolution = toEven(inputVideoFile.resolution)
40
536598cf 41 const transcodeOptions: TranscodeOptions = {
d7a25329 42 type: transcodeType,
9252a33d 43
098eb377 44 inputPath: videoInputPath,
09209296 45 outputPath: videoTranscodedPath,
9252a33d 46
529b3752 47 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
1896bca0 48 profile: CONFIG.TRANSCODING.PROFILE,
9252a33d 49
318b0bd0 50 resolution,
3b01f4c0
C
51
52 job
098eb377
C
53 }
54
55 // Could be very long!
56 await transcode(transcodeOptions)
57
58 try {
59 await remove(videoInputPath)
60
90a8bd30 61 // Important to do this before getVideoFilename() to take in account the new filename
536598cf 62 inputVideoFile.extname = newExtname
83903cb6 63 inputVideoFile.filename = generateWebTorrentVideoFilename(resolution, newExtname)
2fbd5e25 64
d7a25329 65 const videoOutputPath = getVideoFilePath(video, inputVideoFile)
098eb377 66
24516aa2 67 await onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath)
236841a1
C
68
69 return transcodeType
098eb377
C
70 } catch (err) {
71 // Auto destruction...
72 video.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', { err }))
73
74 throw err
75 }
76}
77
6b67897e 78// Transcode the original video file to a lower resolution.
90a8bd30 79async function transcodeNewWebTorrentResolution (video: MVideoFullLight, resolution: VideoResolution, isPortrait: boolean, job: Job) {
2fbd5e25 80 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
098eb377
C
81 const extname = '.mp4'
82
83 // We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed
d7a25329 84 const videoInputPath = getVideoFilePath(video, video.getMaxQualityFile())
098eb377
C
85
86 const newVideoFile = new VideoFileModel({
87 resolution,
88 extname,
83903cb6 89 filename: generateWebTorrentVideoFilename(resolution, extname),
098eb377
C
90 size: 0,
91 videoId: video.id
92 })
90a8bd30 93
d7a25329 94 const videoOutputPath = getVideoFilePath(video, newVideoFile)
90a8bd30 95 const videoTranscodedPath = join(transcodeDirectory, newVideoFile.filename)
098eb377 96
5c7d6508 97 const transcodeOptions = resolution === VideoResolution.H_NOVIDEO
98 ? {
3a149e9f 99 type: 'only-audio' as 'only-audio',
9252a33d 100
3a149e9f
C
101 inputPath: videoInputPath,
102 outputPath: videoTranscodedPath,
9252a33d 103
529b3752 104 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
1896bca0 105 profile: CONFIG.TRANSCODING.PROFILE,
9252a33d 106
3b01f4c0
C
107 resolution,
108
109 job
3a149e9f 110 }
5c7d6508 111 : {
3a149e9f
C
112 type: 'video' as 'video',
113 inputPath: videoInputPath,
114 outputPath: videoTranscodedPath,
9252a33d 115
529b3752 116 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
1896bca0 117 profile: CONFIG.TRANSCODING.PROFILE,
9252a33d 118
3a149e9f 119 resolution,
3b01f4c0
C
120 isPortraitMode: isPortrait,
121
122 job
3a149e9f 123 }
098eb377
C
124
125 await transcode(transcodeOptions)
126
24516aa2 127 return onWebTorrentVideoFileTranscoding(video, newVideoFile, videoTranscodedPath, videoOutputPath)
536598cf
C
128}
129
6b67897e 130// Merge an image with an audio file to create a video
90a8bd30 131async function mergeAudioVideofile (video: MVideoFullLight, resolution: VideoResolution, job: Job) {
536598cf
C
132 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
133 const newExtname = '.mp4'
134
92e0f42e 135 const inputVideoFile = video.getMinQualityFile()
2fbd5e25 136
d7a25329 137 const audioInputPath = getVideoFilePath(video, inputVideoFile)
536598cf 138 const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
098eb377 139
eba06469
C
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)
144
536598cf
C
145 const transcodeOptions = {
146 type: 'merge-audio' as 'merge-audio',
9252a33d 147
eba06469 148 inputPath: tmpPreviewPath,
536598cf 149 outputPath: videoTranscodedPath,
9252a33d 150
529b3752 151 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
1896bca0 152 profile: CONFIG.TRANSCODING.PROFILE,
9252a33d 153
536598cf 154 audioPath: audioInputPath,
3b01f4c0
C
155 resolution,
156
157 job
536598cf 158 }
098eb377 159
eba06469
C
160 try {
161 await transcode(transcodeOptions)
098eb377 162
eba06469
C
163 await remove(audioInputPath)
164 await remove(tmpPreviewPath)
165 } catch (err) {
166 await remove(tmpPreviewPath)
167 throw err
168 }
098eb377 169
536598cf
C
170 // Important to do this before getVideoFilename() to take in account the new file extension
171 inputVideoFile.extname = newExtname
83903cb6 172 inputVideoFile.filename = generateWebTorrentVideoFilename(inputVideoFile.resolution, newExtname)
536598cf 173
d7a25329 174 const videoOutputPath = getVideoFilePath(video, inputVideoFile)
eba06469
C
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)
178 await video.save()
536598cf 179
24516aa2 180 return onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath)
098eb377
C
181}
182
2650d6d4 183// Concat TS segments from a live video to a fragmented mp4 HLS playlist
24516aa2 184async function generateHlsPlaylistResolutionFromTS (options: {
90a8bd30 185 video: MVideoFullLight
3851e732 186 concatenatedTsFilePath: string
2650d6d4
C
187 resolution: VideoResolution
188 isPortraitMode: boolean
e772bdf1 189 isAAC: boolean
2650d6d4 190}) {
3851e732
C
191 return generateHlsPlaylistCommon({
192 video: options.video,
193 resolution: options.resolution,
194 isPortraitMode: options.isPortraitMode,
195 inputPath: options.concatenatedTsFilePath,
e772bdf1
C
196 type: 'hls-from-ts' as 'hls-from-ts',
197 isAAC: options.isAAC
3851e732 198 })
2650d6d4
C
199}
200
6b67897e 201// Generate an HLS playlist from an input file, and update the master playlist
24516aa2 202function generateHlsPlaylistResolution (options: {
90a8bd30 203 video: MVideoFullLight
b5b68755
C
204 videoInputPath: string
205 resolution: VideoResolution
206 copyCodecs: boolean
207 isPortraitMode: boolean
3b01f4c0 208 job?: Job
b5b68755 209}) {
2650d6d4
C
210 return generateHlsPlaylistCommon({
211 video: options.video,
212 resolution: options.resolution,
213 copyCodecs: options.copyCodecs,
214 isPortraitMode: options.isPortraitMode,
215 inputPath: options.videoInputPath,
3b01f4c0
C
216 type: 'hls' as 'hls',
217 job: options.job
2650d6d4
C
218 })
219}
220
221// ---------------------------------------------------------------------------
222
223export {
24516aa2
C
224 generateHlsPlaylistResolution,
225 generateHlsPlaylistResolutionFromTS,
2650d6d4 226 optimizeOriginalVideofile,
24516aa2 227 transcodeNewWebTorrentResolution,
1bcb03a1 228 mergeAudioVideofile
2650d6d4
C
229}
230
231// ---------------------------------------------------------------------------
232
24516aa2 233async function onWebTorrentVideoFileTranscoding (
90a8bd30 234 video: MVideoFullLight,
24516aa2
C
235 videoFile: MVideoFile,
236 transcodingPath: string,
237 outputPath: string
238) {
2650d6d4
C
239 const stats = await stat(transcodingPath)
240 const fps = await getVideoFileFPS(transcodingPath)
241 const metadata = await getMetadataFromFile(transcodingPath)
242
243 await move(transcodingPath, outputPath, { overwrite: true })
244
245 videoFile.size = stats.size
246 videoFile.fps = fps
247 videoFile.metadata = metadata
248
8efc27bf 249 await createTorrentAndSetInfoHash(video, videoFile)
2650d6d4
C
250
251 await VideoFileModel.customUpsert(videoFile, 'video', undefined)
252 video.VideoFiles = await video.$get('VideoFiles')
253
254 return video
255}
256
257async function generateHlsPlaylistCommon (options: {
258 type: 'hls' | 'hls-from-ts'
90a8bd30 259 video: MVideoFullLight
2650d6d4
C
260 inputPath: string
261 resolution: VideoResolution
262 copyCodecs?: boolean
e772bdf1 263 isAAC?: boolean
2650d6d4 264 isPortraitMode: boolean
3b01f4c0
C
265
266 job?: Job
2650d6d4 267}) {
3b01f4c0 268 const { type, video, inputPath, resolution, copyCodecs, isPortraitMode, isAAC, job } = options
aaedadd5 269 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
b5b68755 270
9129b769 271 const videoTranscodedBasePath = join(transcodeDirectory, type)
aaedadd5 272 await ensureDir(videoTranscodedBasePath)
09209296 273
83903cb6 274 const videoFilename = generateHLSVideoFilename(resolution)
aaedadd5
C
275 const playlistFilename = VideoStreamingPlaylistModel.getHlsPlaylistFilename(resolution)
276 const playlistFileTranscodePath = join(videoTranscodedBasePath, playlistFilename)
09209296
C
277
278 const transcodeOptions = {
2650d6d4 279 type,
9252a33d 280
2650d6d4 281 inputPath,
aaedadd5 282 outputPath: playlistFileTranscodePath,
9252a33d 283
529b3752 284 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
1896bca0 285 profile: CONFIG.TRANSCODING.PROFILE,
9252a33d 286
09209296 287 resolution,
d7a25329 288 copyCodecs,
09209296 289 isPortraitMode,
4c280004 290
e772bdf1
C
291 isAAC,
292
4c280004 293 hlsPlaylist: {
d7a25329 294 videoFilename
3b01f4c0
C
295 },
296
297 job
09209296
C
298 }
299
d7a25329 300 await transcode(transcodeOptions)
09209296 301
6dd9de95 302 const playlistUrl = WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsMasterPlaylistStaticPath(video.uuid)
09209296 303
aaedadd5 304 // Create or update the playlist
d7a25329 305 const [ videoStreamingPlaylist ] = await VideoStreamingPlaylistModel.upsert({
09209296
C
306 videoId: video.id,
307 playlistUrl,
c6c0fa6c 308 segmentsSha256Url: WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsSha256SegmentsStaticPath(video.uuid, video.isLive),
b5b68755 309 p2pMediaLoaderInfohashes: [],
594d0c6a 310 p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION,
09209296
C
311
312 type: VideoStreamingPlaylistType.HLS
d7a25329
C
313 }, { returning: true }) as [ MStreamingPlaylistFilesVideo, boolean ]
314 videoStreamingPlaylist.Video = video
315
aaedadd5 316 // Build the new playlist file
90a8bd30 317 const extname = extnameUtil(videoFilename)
d7a25329
C
318 const newVideoFile = new VideoFileModel({
319 resolution,
90a8bd30 320 extname,
d7a25329 321 size: 0,
83903cb6 322 filename: videoFilename,
d7a25329
C
323 fps: -1,
324 videoStreamingPlaylistId: videoStreamingPlaylist.id
09209296 325 })
d7a25329
C
326
327 const videoFilePath = getVideoFilePath(videoStreamingPlaylist, newVideoFile)
aaedadd5
C
328
329 // Move files from tmp transcoded directory to the appropriate place
330 const baseHlsDirectory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid)
331 await ensureDir(baseHlsDirectory)
332
333 // Move playlist file
334 const playlistPath = join(baseHlsDirectory, playlistFilename)
69eddafb 335 await move(playlistFileTranscodePath, playlistPath, { overwrite: true })
aaedadd5 336 // Move video file
69eddafb 337 await move(join(videoTranscodedBasePath, videoFilename), videoFilePath, { overwrite: true })
aaedadd5 338
d7a25329
C
339 const stats = await stat(videoFilePath)
340
341 newVideoFile.size = stats.size
342 newVideoFile.fps = await getVideoFileFPS(videoFilePath)
8319d6ae 343 newVideoFile.metadata = await getMetadataFromFile(videoFilePath)
d7a25329 344
8efc27bf 345 await createTorrentAndSetInfoHash(videoStreamingPlaylist, newVideoFile)
d7a25329 346
c547bbf9 347 await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined)
e6122097 348 videoStreamingPlaylist.VideoFiles = await videoStreamingPlaylist.$get('VideoFiles')
d7a25329 349
b5b68755
C
350 videoStreamingPlaylist.p2pMediaLoaderInfohashes = VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(
351 playlistUrl, videoStreamingPlaylist.VideoFiles
352 )
353 await videoStreamingPlaylist.save()
354
d7a25329
C
355 video.setHLSPlaylist(videoStreamingPlaylist)
356
357 await updateMasterHLSPlaylist(video)
c6c0fa6c 358 await updateSha256VODSegments(video)
d7a25329 359
aaedadd5 360 return playlistPath
098eb377 361}