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