]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/lib/transcoding/video-transcoding.ts
ee228c011145859899371d8b2f25b5429a873d6f
[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, VideoStorage } 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 { 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 } from '../paths'
24 import { VideoPathManager } from '../video-path-manager'
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 function optimizeOriginalVideofile (video: MVideoFullLight, inputVideoFile: MVideoFile, job?: Job) {
36 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
37 const newExtname = '.mp4'
38
39 return VideoPathManager.Instance.makeAvailableVideoFile(video, inputVideoFile, async videoInputPath => {
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 inputVideoFile.storage = VideoStorage.FILE_SYSTEM
72
73 const videoOutputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, inputVideoFile)
74
75 const { videoFile } = await onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath)
76
77 return { transcodeType, videoFile }
78 } catch (err) {
79 // Auto destruction...
80 video.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', { err }))
81
82 throw err
83 }
84 })
85 }
86
87 // Transcode the original video file to a lower resolution
88 // We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed
89 function transcodeNewWebTorrentResolution (video: MVideoFullLight, resolution: VideoResolution, isPortrait: boolean, job: Job) {
90 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
91 const extname = '.mp4'
92
93 return VideoPathManager.Instance.makeAvailableVideoFile(video, video.getMaxQualityFile(), async videoInputPath => {
94 const newVideoFile = new VideoFileModel({
95 resolution,
96 extname,
97 filename: generateWebTorrentVideoFilename(resolution, extname),
98 size: 0,
99 videoId: video.id
100 })
101
102 const videoOutputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, newVideoFile)
103 const videoTranscodedPath = join(transcodeDirectory, newVideoFile.filename)
104
105 const transcodeOptions = resolution === VideoResolution.H_NOVIDEO
106 ? {
107 type: 'only-audio' as 'only-audio',
108
109 inputPath: videoInputPath,
110 outputPath: videoTranscodedPath,
111
112 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
113 profile: CONFIG.TRANSCODING.PROFILE,
114
115 resolution,
116
117 job
118 }
119 : {
120 type: 'video' as 'video',
121 inputPath: videoInputPath,
122 outputPath: videoTranscodedPath,
123
124 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
125 profile: CONFIG.TRANSCODING.PROFILE,
126
127 resolution,
128 isPortraitMode: isPortrait,
129
130 job
131 }
132
133 await transcode(transcodeOptions)
134
135 return onWebTorrentVideoFileTranscoding(video, newVideoFile, videoTranscodedPath, videoOutputPath)
136 })
137 }
138
139 // Merge an image with an audio file to create a video
140 function mergeAudioVideofile (video: MVideoFullLight, resolution: VideoResolution, job: Job) {
141 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
142 const newExtname = '.mp4'
143
144 const inputVideoFile = video.getMinQualityFile()
145
146 return VideoPathManager.Instance.makeAvailableVideoFile(video, inputVideoFile, async audioInputPath => {
147 const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
148
149 // If the user updates the video preview during transcoding
150 const previewPath = video.getPreview().getPath()
151 const tmpPreviewPath = join(CONFIG.STORAGE.TMP_DIR, basename(previewPath))
152 await copyFile(previewPath, tmpPreviewPath)
153
154 const transcodeOptions = {
155 type: 'merge-audio' as 'merge-audio',
156
157 inputPath: tmpPreviewPath,
158 outputPath: videoTranscodedPath,
159
160 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
161 profile: CONFIG.TRANSCODING.PROFILE,
162
163 audioPath: audioInputPath,
164 resolution,
165
166 job
167 }
168
169 try {
170 await transcode(transcodeOptions)
171
172 await remove(audioInputPath)
173 await remove(tmpPreviewPath)
174 } catch (err) {
175 await remove(tmpPreviewPath)
176 throw err
177 }
178
179 // Important to do this before getVideoFilename() to take in account the new file extension
180 inputVideoFile.extname = newExtname
181 inputVideoFile.filename = generateWebTorrentVideoFilename(inputVideoFile.resolution, newExtname)
182
183 const videoOutputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, inputVideoFile)
184 // ffmpeg generated a new video file, so update the video duration
185 // See https://trac.ffmpeg.org/ticket/5456
186 video.duration = await getDurationFromVideoFile(videoTranscodedPath)
187 await video.save()
188
189 return onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath)
190 })
191 }
192
193 // Concat TS segments from a live video to a fragmented mp4 HLS playlist
194 async function generateHlsPlaylistResolutionFromTS (options: {
195 video: MVideoFullLight
196 concatenatedTsFilePath: string
197 resolution: VideoResolution
198 isPortraitMode: boolean
199 isAAC: boolean
200 }) {
201 return generateHlsPlaylistCommon({
202 video: options.video,
203 resolution: options.resolution,
204 isPortraitMode: options.isPortraitMode,
205 inputPath: options.concatenatedTsFilePath,
206 type: 'hls-from-ts' as 'hls-from-ts',
207 isAAC: options.isAAC
208 })
209 }
210
211 // Generate an HLS playlist from an input file, and update the master playlist
212 function generateHlsPlaylistResolution (options: {
213 video: MVideoFullLight
214 videoInputPath: string
215 resolution: VideoResolution
216 copyCodecs: boolean
217 isPortraitMode: boolean
218 job?: Job
219 }) {
220 return generateHlsPlaylistCommon({
221 video: options.video,
222 resolution: options.resolution,
223 copyCodecs: options.copyCodecs,
224 isPortraitMode: options.isPortraitMode,
225 inputPath: options.videoInputPath,
226 type: 'hls' as 'hls',
227 job: options.job
228 })
229 }
230
231 // ---------------------------------------------------------------------------
232
233 export {
234 generateHlsPlaylistResolution,
235 generateHlsPlaylistResolutionFromTS,
236 optimizeOriginalVideofile,
237 transcodeNewWebTorrentResolution,
238 mergeAudioVideofile
239 }
240
241 // ---------------------------------------------------------------------------
242
243 async function onWebTorrentVideoFileTranscoding (
244 video: MVideoFullLight,
245 videoFile: MVideoFile,
246 transcodingPath: string,
247 outputPath: string
248 ) {
249 const stats = await stat(transcodingPath)
250 const fps = await getVideoFileFPS(transcodingPath)
251 const metadata = await getMetadataFromFile(transcodingPath)
252
253 await move(transcodingPath, outputPath, { overwrite: true })
254
255 videoFile.size = stats.size
256 videoFile.fps = fps
257 videoFile.metadata = metadata
258
259 await createTorrentAndSetInfoHash(video, videoFile)
260
261 await VideoFileModel.customUpsert(videoFile, 'video', undefined)
262 video.VideoFiles = await video.$get('VideoFiles')
263
264 return { video, videoFile }
265 }
266
267 async function generateHlsPlaylistCommon (options: {
268 type: 'hls' | 'hls-from-ts'
269 video: MVideoFullLight
270 inputPath: string
271 resolution: VideoResolution
272 copyCodecs?: boolean
273 isAAC?: boolean
274 isPortraitMode: boolean
275
276 job?: Job
277 }) {
278 const { type, video, inputPath, resolution, copyCodecs, isPortraitMode, isAAC, job } = options
279 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
280
281 const videoTranscodedBasePath = join(transcodeDirectory, type)
282 await ensureDir(videoTranscodedBasePath)
283
284 const videoFilename = generateHLSVideoFilename(resolution)
285 const resolutionPlaylistFilename = getHlsResolutionPlaylistFilename(videoFilename)
286 const resolutionPlaylistFileTranscodePath = join(videoTranscodedBasePath, resolutionPlaylistFilename)
287
288 const transcodeOptions = {
289 type,
290
291 inputPath,
292 outputPath: resolutionPlaylistFileTranscodePath,
293
294 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
295 profile: CONFIG.TRANSCODING.PROFILE,
296
297 resolution,
298 copyCodecs,
299 isPortraitMode,
300
301 isAAC,
302
303 hlsPlaylist: {
304 videoFilename
305 },
306
307 job
308 }
309
310 await transcode(transcodeOptions)
311
312 // Create or update the playlist
313 const playlist = await VideoStreamingPlaylistModel.loadOrGenerate(video)
314
315 if (!playlist.playlistFilename) {
316 playlist.playlistFilename = generateHLSMasterPlaylistFilename(video.isLive)
317 }
318
319 if (!playlist.segmentsSha256Filename) {
320 playlist.segmentsSha256Filename = generateHlsSha256SegmentsFilename(video.isLive)
321 }
322
323 playlist.p2pMediaLoaderInfohashes = []
324 playlist.p2pMediaLoaderPeerVersion = P2P_MEDIA_LOADER_PEER_VERSION
325
326 playlist.type = VideoStreamingPlaylistType.HLS
327
328 await playlist.save()
329
330 // Build the new playlist file
331 const extname = extnameUtil(videoFilename)
332 const newVideoFile = new VideoFileModel({
333 resolution,
334 extname,
335 size: 0,
336 filename: videoFilename,
337 fps: -1,
338 videoStreamingPlaylistId: playlist.id
339 })
340
341 const videoFilePath = VideoPathManager.Instance.getFSVideoFileOutputPath(playlist, newVideoFile)
342
343 // Move files from tmp transcoded directory to the appropriate place
344 await ensureDir(VideoPathManager.Instance.getFSHLSOutputPath(video))
345
346 // Move playlist file
347 const resolutionPlaylistPath = VideoPathManager.Instance.getFSHLSOutputPath(video, resolutionPlaylistFilename)
348 await move(resolutionPlaylistFileTranscodePath, resolutionPlaylistPath, { overwrite: true })
349 // Move video file
350 await move(join(videoTranscodedBasePath, videoFilename), videoFilePath, { overwrite: true })
351
352 const stats = await stat(videoFilePath)
353
354 newVideoFile.size = stats.size
355 newVideoFile.fps = await getVideoFileFPS(videoFilePath)
356 newVideoFile.metadata = await getMetadataFromFile(videoFilePath)
357
358 await createTorrentAndSetInfoHash(playlist, newVideoFile)
359
360 const savedVideoFile = await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined)
361
362 const playlistWithFiles = playlist as MStreamingPlaylistFilesVideo
363 playlistWithFiles.VideoFiles = await playlist.$get('VideoFiles')
364 playlist.assignP2PMediaLoaderInfoHashes(video, playlistWithFiles.VideoFiles)
365
366 await playlist.save()
367
368 video.setHLSPlaylist(playlist)
369
370 await updateMasterHLSPlaylist(video, playlistWithFiles)
371 await updateSha256VODSegments(video, playlistWithFiles)
372
373 return { resolutionPlaylistPath, videoFile: savedVideoFile }
374 }