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