]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/lib/transcoding/video-transcoding.ts
Use random names for VOD HLS playlists
[github/Chocobozzz/PeerTube.git] / server / lib / transcoding / video-transcoding.ts
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 } 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 { logger } from '../../helpers/logger'
12 import { CONFIG } from '../../initializers/config'
13 import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION } from '../../initializers/constants'
14 import { VideoFileModel } from '../../models/video/video-file'
15 import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist'
16 import { updateMasterHLSPlaylist, updateSha256VODSegments } from '../hls'
17 import {
18 generateHLSMasterPlaylistFilename,
19 generateHlsSha256SegmentsFilename,
20 generateHLSVideoFilename,
21 generateWebTorrentVideoFilename,
22 getHlsResolutionPlaylistFilename,
23 getVideoFilePath
24 } from '../video-paths'
25 import { VideoTranscodingProfilesManager } from './video-transcoding-profiles'
26
27 /**
28 *
29 * Functions that run transcoding functions, update the database, cleanup files, create torrent files...
30 * Mainly called by the job queue
31 *
32 */
33
34 // Optimize the original video file and replace it. The resolution is not changed.
35 async function optimizeOriginalVideofile (video: MVideoFullLight, inputVideoFile: MVideoFile, job?: Job) {
36 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
37 const newExtname = '.mp4'
38
39 const videoInputPath = getVideoFilePath(video, inputVideoFile)
40 const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
41
42 const transcodeType: TranscodeOptionsType = await canDoQuickTranscode(videoInputPath)
43 ? 'quick-transcode'
44 : 'video'
45
46 const resolution = toEven(inputVideoFile.resolution)
47
48 const transcodeOptions: TranscodeOptions = {
49 type: transcodeType,
50
51 inputPath: videoInputPath,
52 outputPath: videoTranscodedPath,
53
54 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
55 profile: CONFIG.TRANSCODING.PROFILE,
56
57 resolution,
58
59 job
60 }
61
62 // Could be very long!
63 await transcode(transcodeOptions)
64
65 try {
66 await remove(videoInputPath)
67
68 // Important to do this before getVideoFilename() to take in account the new filename
69 inputVideoFile.extname = newExtname
70 inputVideoFile.filename = generateWebTorrentVideoFilename(resolution, newExtname)
71
72 const videoOutputPath = getVideoFilePath(video, inputVideoFile)
73
74 await onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath)
75
76 return transcodeType
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
85 // Transcode the original video file to a lower resolution.
86 async function transcodeNewWebTorrentResolution (video: MVideoFullLight, resolution: VideoResolution, isPortrait: boolean, job: Job) {
87 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
88 const extname = '.mp4'
89
90 // We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed
91 const videoInputPath = getVideoFilePath(video, video.getMaxQualityFile())
92
93 const newVideoFile = new VideoFileModel({
94 resolution,
95 extname,
96 filename: generateWebTorrentVideoFilename(resolution, extname),
97 size: 0,
98 videoId: video.id
99 })
100
101 const videoOutputPath = getVideoFilePath(video, newVideoFile)
102 const videoTranscodedPath = join(transcodeDirectory, newVideoFile.filename)
103
104 const transcodeOptions = resolution === VideoResolution.H_NOVIDEO
105 ? {
106 type: 'only-audio' as 'only-audio',
107
108 inputPath: videoInputPath,
109 outputPath: videoTranscodedPath,
110
111 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
112 profile: CONFIG.TRANSCODING.PROFILE,
113
114 resolution,
115
116 job
117 }
118 : {
119 type: 'video' as 'video',
120 inputPath: videoInputPath,
121 outputPath: videoTranscodedPath,
122
123 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
124 profile: CONFIG.TRANSCODING.PROFILE,
125
126 resolution,
127 isPortraitMode: isPortrait,
128
129 job
130 }
131
132 await transcode(transcodeOptions)
133
134 return onWebTorrentVideoFileTranscoding(video, newVideoFile, videoTranscodedPath, videoOutputPath)
135 }
136
137 // Merge an image with an audio file to create a video
138 async function mergeAudioVideofile (video: MVideoFullLight, resolution: VideoResolution, job: Job) {
139 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
140 const newExtname = '.mp4'
141
142 const inputVideoFile = video.getMinQualityFile()
143
144 const audioInputPath = getVideoFilePath(video, inputVideoFile)
145 const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
146
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
152 const transcodeOptions = {
153 type: 'merge-audio' as 'merge-audio',
154
155 inputPath: tmpPreviewPath,
156 outputPath: videoTranscodedPath,
157
158 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
159 profile: CONFIG.TRANSCODING.PROFILE,
160
161 audioPath: audioInputPath,
162 resolution,
163
164 job
165 }
166
167 try {
168 await transcode(transcodeOptions)
169
170 await remove(audioInputPath)
171 await remove(tmpPreviewPath)
172 } catch (err) {
173 await remove(tmpPreviewPath)
174 throw err
175 }
176
177 // Important to do this before getVideoFilename() to take in account the new file extension
178 inputVideoFile.extname = newExtname
179 inputVideoFile.filename = generateWebTorrentVideoFilename(inputVideoFile.resolution, newExtname)
180
181 const videoOutputPath = getVideoFilePath(video, inputVideoFile)
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()
186
187 return onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath)
188 }
189
190 // Concat TS segments from a live video to a fragmented mp4 HLS playlist
191 async function generateHlsPlaylistResolutionFromTS (options: {
192 video: MVideoFullLight
193 concatenatedTsFilePath: string
194 resolution: VideoResolution
195 isPortraitMode: boolean
196 isAAC: boolean
197 }) {
198 return generateHlsPlaylistCommon({
199 video: options.video,
200 resolution: options.resolution,
201 isPortraitMode: options.isPortraitMode,
202 inputPath: options.concatenatedTsFilePath,
203 type: 'hls-from-ts' as 'hls-from-ts',
204 isAAC: options.isAAC
205 })
206 }
207
208 // Generate an HLS playlist from an input file, and update the master playlist
209 function generateHlsPlaylistResolution (options: {
210 video: MVideoFullLight
211 videoInputPath: string
212 resolution: VideoResolution
213 copyCodecs: boolean
214 isPortraitMode: boolean
215 job?: Job
216 }) {
217 return generateHlsPlaylistCommon({
218 video: options.video,
219 resolution: options.resolution,
220 copyCodecs: options.copyCodecs,
221 isPortraitMode: options.isPortraitMode,
222 inputPath: options.videoInputPath,
223 type: 'hls' as 'hls',
224 job: options.job
225 })
226 }
227
228 // ---------------------------------------------------------------------------
229
230 export {
231 generateHlsPlaylistResolution,
232 generateHlsPlaylistResolutionFromTS,
233 optimizeOriginalVideofile,
234 transcodeNewWebTorrentResolution,
235 mergeAudioVideofile
236 }
237
238 // ---------------------------------------------------------------------------
239
240 async function onWebTorrentVideoFileTranscoding (
241 video: MVideoFullLight,
242 videoFile: MVideoFile,
243 transcodingPath: string,
244 outputPath: string
245 ) {
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
256 await createTorrentAndSetInfoHash(video, videoFile)
257
258 await VideoFileModel.customUpsert(videoFile, 'video', undefined)
259 video.VideoFiles = await video.$get('VideoFiles')
260
261 return video
262 }
263
264 async function generateHlsPlaylistCommon (options: {
265 type: 'hls' | 'hls-from-ts'
266 video: MVideoFullLight
267 inputPath: string
268 resolution: VideoResolution
269 copyCodecs?: boolean
270 isAAC?: boolean
271 isPortraitMode: boolean
272
273 job?: Job
274 }) {
275 const { type, video, inputPath, resolution, copyCodecs, isPortraitMode, isAAC, job } = options
276 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
277
278 const videoTranscodedBasePath = join(transcodeDirectory, type)
279 await ensureDir(videoTranscodedBasePath)
280
281 const videoFilename = generateHLSVideoFilename(resolution)
282 const resolutionPlaylistFilename = getHlsResolutionPlaylistFilename(videoFilename)
283 const resolutionPlaylistFileTranscodePath = join(videoTranscodedBasePath, resolutionPlaylistFilename)
284
285 const transcodeOptions = {
286 type,
287
288 inputPath,
289 outputPath: resolutionPlaylistFileTranscodePath,
290
291 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
292 profile: CONFIG.TRANSCODING.PROFILE,
293
294 resolution,
295 copyCodecs,
296 isPortraitMode,
297
298 isAAC,
299
300 hlsPlaylist: {
301 videoFilename
302 },
303
304 job
305 }
306
307 await transcode(transcodeOptions)
308
309 // Create or update the playlist
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
322
323 playlist.type = VideoStreamingPlaylistType.HLS
324
325 await playlist.save()
326
327 // Build the new playlist file
328 const extname = extnameUtil(videoFilename)
329 const newVideoFile = new VideoFileModel({
330 resolution,
331 extname,
332 size: 0,
333 filename: videoFilename,
334 fps: -1,
335 videoStreamingPlaylistId: playlist.id
336 })
337
338 const videoFilePath = getVideoFilePath(playlist, newVideoFile)
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
345 const resolutionPlaylistPath = join(baseHlsDirectory, resolutionPlaylistFilename)
346 await move(resolutionPlaylistFileTranscodePath, resolutionPlaylistPath, { overwrite: true })
347 // Move video file
348 await move(join(videoTranscodedBasePath, videoFilename), videoFilePath, { overwrite: true })
349
350 const stats = await stat(videoFilePath)
351
352 newVideoFile.size = stats.size
353 newVideoFile.fps = await getVideoFileFPS(videoFilePath)
354 newVideoFile.metadata = await getMetadataFromFile(videoFilePath)
355
356 await createTorrentAndSetInfoHash(playlist, newVideoFile)
357
358 await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined)
359
360 const playlistWithFiles = playlist as MStreamingPlaylistFilesVideo
361 playlistWithFiles.VideoFiles = await playlist.$get('VideoFiles')
362 playlist.assignP2PMediaLoaderInfoHashes(video, playlistWithFiles.VideoFiles)
363
364 await playlist.save()
365
366 video.setHLSPlaylist(playlist)
367
368 await updateMasterHLSPlaylist(video, playlistWithFiles)
369 await updateSha256VODSegments(video, playlistWithFiles)
370
371 return resolutionPlaylistPath
372 }