]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/lib/transcoding/video-transcoding.ts
Speed up client lint
[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'
11import { logger } from '../../helpers/logger'
12import { CONFIG } from '../../initializers/config'
0305db28 13import { 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,
0305db28
JB
22 getHlsResolutionPlaylistFilename
23} from '../paths'
24import { VideoPathManager } from '../video-path-manager'
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.
0305db28 35function optimizeOriginalVideofile (video: MVideoFullLight, inputVideoFile: MVideoFile, job?: Job) {
2fbd5e25 36 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
098eb377 37 const newExtname = '.mp4'
9f1ddd24 38
0305db28
JB
39 return VideoPathManager.Instance.makeAvailableVideoFile(video, inputVideoFile, async videoInputPath => {
40 const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
098eb377 41
0305db28
JB
42 const transcodeType: TranscodeOptionsType = await canDoQuickTranscode(videoInputPath)
43 ? 'quick-transcode'
44 : 'video'
5ba49f26 45
0305db28 46 const resolution = toEven(inputVideoFile.resolution)
318b0bd0 47
0305db28
JB
48 const transcodeOptions: TranscodeOptions = {
49 type: transcodeType,
9252a33d 50
0305db28
JB
51 inputPath: videoInputPath,
52 outputPath: videoTranscodedPath,
9252a33d 53
0305db28
JB
54 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
55 profile: CONFIG.TRANSCODING.PROFILE,
9252a33d 56
0305db28 57 resolution,
3b01f4c0 58
0305db28
JB
59 job
60 }
098eb377 61
0305db28
JB
62 // Could be very long!
63 await transcode(transcodeOptions)
098eb377 64
0305db28
JB
65 try {
66 await remove(videoInputPath)
098eb377 67
0305db28
JB
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
2fbd5e25 72
0305db28 73 const videoOutputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, inputVideoFile)
098eb377 74
0305db28 75 const { videoFile } = await onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath)
236841a1 76
0305db28
JB
77 return { transcodeType, videoFile }
78 } catch (err) {
79 // Auto destruction...
80 video.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', { err }))
098eb377 81
0305db28
JB
82 throw err
83 }
84 })
098eb377
C
85}
86
0305db28
JB
87// Transcode the original video file to a lower resolution
88// We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed
89function transcodeNewWebTorrentResolution (video: MVideoFullLight, resolution: VideoResolution, isPortrait: boolean, job: Job) {
2fbd5e25 90 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
098eb377
C
91 const extname = '.mp4'
92
0305db28
JB
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 })
098eb377 101
0305db28
JB
102 const videoOutputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, newVideoFile)
103 const videoTranscodedPath = join(transcodeDirectory, newVideoFile.filename)
90a8bd30 104
0305db28
JB
105 const transcodeOptions = resolution === VideoResolution.H_NOVIDEO
106 ? {
107 type: 'only-audio' as 'only-audio',
098eb377 108
0305db28
JB
109 inputPath: videoInputPath,
110 outputPath: videoTranscodedPath,
9252a33d 111
0305db28
JB
112 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
113 profile: CONFIG.TRANSCODING.PROFILE,
9252a33d 114
0305db28 115 resolution,
9252a33d 116
0305db28
JB
117 job
118 }
119 : {
120 type: 'video' as 'video',
121 inputPath: videoInputPath,
122 outputPath: videoTranscodedPath,
3b01f4c0 123
0305db28
JB
124 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
125 profile: CONFIG.TRANSCODING.PROFILE,
9252a33d 126
0305db28
JB
127 resolution,
128 isPortraitMode: isPortrait,
9252a33d 129
0305db28
JB
130 job
131 }
3b01f4c0 132
0305db28 133 await transcode(transcodeOptions)
098eb377 134
0305db28
JB
135 return onWebTorrentVideoFileTranscoding(video, newVideoFile, videoTranscodedPath, videoOutputPath)
136 })
536598cf
C
137}
138
6b67897e 139// Merge an image with an audio file to create a video
0305db28 140function mergeAudioVideofile (video: MVideoFullLight, resolution: VideoResolution, job: Job) {
536598cf
C
141 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
142 const newExtname = '.mp4'
143
92e0f42e 144 const inputVideoFile = video.getMinQualityFile()
2fbd5e25 145
0305db28
JB
146 return VideoPathManager.Instance.makeAvailableVideoFile(video, inputVideoFile, async audioInputPath => {
147 const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
098eb377 148
0305db28
JB
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)
eba06469 153
0305db28
JB
154 const transcodeOptions = {
155 type: 'merge-audio' as 'merge-audio',
9252a33d 156
0305db28
JB
157 inputPath: tmpPreviewPath,
158 outputPath: videoTranscodedPath,
9252a33d 159
0305db28
JB
160 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
161 profile: CONFIG.TRANSCODING.PROFILE,
9252a33d 162
0305db28
JB
163 audioPath: audioInputPath,
164 resolution,
3b01f4c0 165
0305db28
JB
166 job
167 }
098eb377 168
0305db28
JB
169 try {
170 await transcode(transcodeOptions)
098eb377 171
0305db28
JB
172 await remove(audioInputPath)
173 await remove(tmpPreviewPath)
174 } catch (err) {
175 await remove(tmpPreviewPath)
176 throw err
177 }
098eb377 178
0305db28
JB
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)
536598cf 182
0305db28
JB
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()
536598cf 188
0305db28
JB
189 return onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath)
190 })
098eb377
C
191}
192
2650d6d4 193// Concat TS segments from a live video to a fragmented mp4 HLS playlist
24516aa2 194async function generateHlsPlaylistResolutionFromTS (options: {
90a8bd30 195 video: MVideoFullLight
3851e732 196 concatenatedTsFilePath: string
2650d6d4
C
197 resolution: VideoResolution
198 isPortraitMode: boolean
e772bdf1 199 isAAC: boolean
2650d6d4 200}) {
3851e732
C
201 return generateHlsPlaylistCommon({
202 video: options.video,
203 resolution: options.resolution,
204 isPortraitMode: options.isPortraitMode,
205 inputPath: options.concatenatedTsFilePath,
e772bdf1
C
206 type: 'hls-from-ts' as 'hls-from-ts',
207 isAAC: options.isAAC
3851e732 208 })
2650d6d4
C
209}
210
6b67897e 211// Generate an HLS playlist from an input file, and update the master playlist
24516aa2 212function generateHlsPlaylistResolution (options: {
90a8bd30 213 video: MVideoFullLight
b5b68755
C
214 videoInputPath: string
215 resolution: VideoResolution
216 copyCodecs: boolean
217 isPortraitMode: boolean
3b01f4c0 218 job?: Job
b5b68755 219}) {
2650d6d4
C
220 return generateHlsPlaylistCommon({
221 video: options.video,
222 resolution: options.resolution,
223 copyCodecs: options.copyCodecs,
224 isPortraitMode: options.isPortraitMode,
225 inputPath: options.videoInputPath,
3b01f4c0
C
226 type: 'hls' as 'hls',
227 job: options.job
2650d6d4
C
228 })
229}
230
231// ---------------------------------------------------------------------------
232
233export {
24516aa2
C
234 generateHlsPlaylistResolution,
235 generateHlsPlaylistResolutionFromTS,
2650d6d4 236 optimizeOriginalVideofile,
24516aa2 237 transcodeNewWebTorrentResolution,
1bcb03a1 238 mergeAudioVideofile
2650d6d4
C
239}
240
241// ---------------------------------------------------------------------------
242
24516aa2 243async function onWebTorrentVideoFileTranscoding (
90a8bd30 244 video: MVideoFullLight,
24516aa2
C
245 videoFile: MVideoFile,
246 transcodingPath: string,
247 outputPath: string
248) {
2650d6d4
C
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
8efc27bf 259 await createTorrentAndSetInfoHash(video, videoFile)
2650d6d4
C
260
261 await VideoFileModel.customUpsert(videoFile, 'video', undefined)
262 video.VideoFiles = await video.$get('VideoFiles')
263
0305db28 264 return { video, videoFile }
2650d6d4
C
265}
266
267async function generateHlsPlaylistCommon (options: {
268 type: 'hls' | 'hls-from-ts'
90a8bd30 269 video: MVideoFullLight
2650d6d4
C
270 inputPath: string
271 resolution: VideoResolution
272 copyCodecs?: boolean
e772bdf1 273 isAAC?: boolean
2650d6d4 274 isPortraitMode: boolean
3b01f4c0
C
275
276 job?: Job
2650d6d4 277}) {
3b01f4c0 278 const { type, video, inputPath, resolution, copyCodecs, isPortraitMode, isAAC, job } = options
aaedadd5 279 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
b5b68755 280
9129b769 281 const videoTranscodedBasePath = join(transcodeDirectory, type)
aaedadd5 282 await ensureDir(videoTranscodedBasePath)
09209296 283
83903cb6 284 const videoFilename = generateHLSVideoFilename(resolution)
764b1a14
C
285 const resolutionPlaylistFilename = getHlsResolutionPlaylistFilename(videoFilename)
286 const resolutionPlaylistFileTranscodePath = join(videoTranscodedBasePath, resolutionPlaylistFilename)
09209296
C
287
288 const transcodeOptions = {
2650d6d4 289 type,
9252a33d 290
2650d6d4 291 inputPath,
764b1a14 292 outputPath: resolutionPlaylistFileTranscodePath,
9252a33d 293
529b3752 294 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
1896bca0 295 profile: CONFIG.TRANSCODING.PROFILE,
9252a33d 296
09209296 297 resolution,
d7a25329 298 copyCodecs,
09209296 299 isPortraitMode,
4c280004 300
e772bdf1
C
301 isAAC,
302
4c280004 303 hlsPlaylist: {
d7a25329 304 videoFilename
3b01f4c0
C
305 },
306
307 job
09209296
C
308 }
309
d7a25329 310 await transcode(transcodeOptions)
09209296 311
aaedadd5 312 // Create or update the playlist
764b1a14
C
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
09209296 325
764b1a14
C
326 playlist.type = VideoStreamingPlaylistType.HLS
327
328 await playlist.save()
d7a25329 329
aaedadd5 330 // Build the new playlist file
90a8bd30 331 const extname = extnameUtil(videoFilename)
d7a25329
C
332 const newVideoFile = new VideoFileModel({
333 resolution,
90a8bd30 334 extname,
d7a25329 335 size: 0,
83903cb6 336 filename: videoFilename,
d7a25329 337 fps: -1,
764b1a14 338 videoStreamingPlaylistId: playlist.id
09209296 339 })
d7a25329 340
0305db28 341 const videoFilePath = VideoPathManager.Instance.getFSVideoFileOutputPath(playlist, newVideoFile)
aaedadd5
C
342
343 // Move files from tmp transcoded directory to the appropriate place
0305db28 344 await ensureDir(VideoPathManager.Instance.getFSHLSOutputPath(video))
aaedadd5
C
345
346 // Move playlist file
0305db28 347 const resolutionPlaylistPath = VideoPathManager.Instance.getFSHLSOutputPath(video, resolutionPlaylistFilename)
764b1a14 348 await move(resolutionPlaylistFileTranscodePath, resolutionPlaylistPath, { overwrite: true })
aaedadd5 349 // Move video file
69eddafb 350 await move(join(videoTranscodedBasePath, videoFilename), videoFilePath, { overwrite: true })
aaedadd5 351
d7a25329
C
352 const stats = await stat(videoFilePath)
353
354 newVideoFile.size = stats.size
355 newVideoFile.fps = await getVideoFileFPS(videoFilePath)
8319d6ae 356 newVideoFile.metadata = await getMetadataFromFile(videoFilePath)
d7a25329 357
764b1a14 358 await createTorrentAndSetInfoHash(playlist, newVideoFile)
d7a25329 359
0305db28 360 const savedVideoFile = await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined)
d7a25329 361
764b1a14
C
362 const playlistWithFiles = playlist as MStreamingPlaylistFilesVideo
363 playlistWithFiles.VideoFiles = await playlist.$get('VideoFiles')
364 playlist.assignP2PMediaLoaderInfoHashes(video, playlistWithFiles.VideoFiles)
365
366 await playlist.save()
b5b68755 367
764b1a14 368 video.setHLSPlaylist(playlist)
d7a25329 369
764b1a14
C
370 await updateMasterHLSPlaylist(video, playlistWithFiles)
371 await updateSha256VODSegments(video, playlistWithFiles)
d7a25329 372
0305db28 373 return { resolutionPlaylistPath, videoFile: savedVideoFile }
098eb377 374}