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