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