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