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