]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/lib/transcoding/video-transcoding.ts
Try to fix ARM docker builds
[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.resolution = resolution
173 inputVideoFile.filename = generateWebTorrentVideoFilename(inputVideoFile.resolution, newExtname)
174
175 const videoOutputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, inputVideoFile)
176 // ffmpeg generated a new video file, so update the video duration
177 // See https://trac.ffmpeg.org/ticket/5456
178 video.duration = await getDurationFromVideoFile(videoTranscodedPath)
179 await video.save()
180
181 return onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath)
182 })
183 }
184
185 // Concat TS segments from a live video to a fragmented mp4 HLS playlist
186 async function generateHlsPlaylistResolutionFromTS (options: {
187 video: MVideoFullLight
188 concatenatedTsFilePath: string
189 resolution: VideoResolution
190 isPortraitMode: boolean
191 isAAC: boolean
192 }) {
193 return generateHlsPlaylistCommon({
194 video: options.video,
195 resolution: options.resolution,
196 isPortraitMode: options.isPortraitMode,
197 inputPath: options.concatenatedTsFilePath,
198 type: 'hls-from-ts' as 'hls-from-ts',
199 isAAC: options.isAAC
200 })
201 }
202
203 // Generate an HLS playlist from an input file, and update the master playlist
204 function generateHlsPlaylistResolution (options: {
205 video: MVideoFullLight
206 videoInputPath: string
207 resolution: VideoResolution
208 copyCodecs: boolean
209 isPortraitMode: boolean
210 job?: Job
211 }) {
212 return generateHlsPlaylistCommon({
213 video: options.video,
214 resolution: options.resolution,
215 copyCodecs: options.copyCodecs,
216 isPortraitMode: options.isPortraitMode,
217 inputPath: options.videoInputPath,
218 type: 'hls' as 'hls',
219 job: options.job
220 })
221 }
222
223 // ---------------------------------------------------------------------------
224
225 export {
226 generateHlsPlaylistResolution,
227 generateHlsPlaylistResolutionFromTS,
228 optimizeOriginalVideofile,
229 transcodeNewWebTorrentResolution,
230 mergeAudioVideofile
231 }
232
233 // ---------------------------------------------------------------------------
234
235 async function onWebTorrentVideoFileTranscoding (
236 video: MVideoFullLight,
237 videoFile: MVideoFile,
238 transcodingPath: string,
239 outputPath: string
240 ) {
241 const stats = await stat(transcodingPath)
242 const fps = await getVideoFileFPS(transcodingPath)
243 const metadata = await getMetadataFromFile(transcodingPath)
244
245 await move(transcodingPath, outputPath, { overwrite: true })
246
247 videoFile.size = stats.size
248 videoFile.fps = fps
249 videoFile.metadata = metadata
250
251 await createTorrentAndSetInfoHash(video, videoFile)
252
253 await VideoFileModel.customUpsert(videoFile, 'video', undefined)
254 video.VideoFiles = await video.$get('VideoFiles')
255
256 return { video, videoFile }
257 }
258
259 async function generateHlsPlaylistCommon (options: {
260 type: 'hls' | 'hls-from-ts'
261 video: MVideoFullLight
262 inputPath: string
263 resolution: VideoResolution
264 copyCodecs?: boolean
265 isAAC?: boolean
266 isPortraitMode: boolean
267
268 job?: Job
269 }) {
270 const { type, video, inputPath, resolution, copyCodecs, isPortraitMode, isAAC, job } = options
271 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
272
273 const videoTranscodedBasePath = join(transcodeDirectory, type)
274 await ensureDir(videoTranscodedBasePath)
275
276 const videoFilename = generateHLSVideoFilename(resolution)
277 const resolutionPlaylistFilename = getHlsResolutionPlaylistFilename(videoFilename)
278 const resolutionPlaylistFileTranscodePath = join(videoTranscodedBasePath, resolutionPlaylistFilename)
279
280 const transcodeOptions = {
281 type,
282
283 inputPath,
284 outputPath: resolutionPlaylistFileTranscodePath,
285
286 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
287 profile: CONFIG.TRANSCODING.PROFILE,
288
289 resolution,
290 copyCodecs,
291 isPortraitMode,
292
293 isAAC,
294
295 hlsPlaylist: {
296 videoFilename
297 },
298
299 job
300 }
301
302 await transcode(transcodeOptions)
303
304 // Create or update the playlist
305 const playlist = await VideoStreamingPlaylistModel.loadOrGenerate(video)
306
307 if (!playlist.playlistFilename) {
308 playlist.playlistFilename = generateHLSMasterPlaylistFilename(video.isLive)
309 }
310
311 if (!playlist.segmentsSha256Filename) {
312 playlist.segmentsSha256Filename = generateHlsSha256SegmentsFilename(video.isLive)
313 }
314
315 playlist.p2pMediaLoaderInfohashes = []
316 playlist.p2pMediaLoaderPeerVersion = P2P_MEDIA_LOADER_PEER_VERSION
317
318 playlist.type = VideoStreamingPlaylistType.HLS
319
320 await playlist.save()
321
322 // Build the new playlist file
323 const extname = extnameUtil(videoFilename)
324 const newVideoFile = new VideoFileModel({
325 resolution,
326 extname,
327 size: 0,
328 filename: videoFilename,
329 fps: -1,
330 videoStreamingPlaylistId: playlist.id
331 })
332
333 const videoFilePath = VideoPathManager.Instance.getFSVideoFileOutputPath(playlist, newVideoFile)
334
335 // Move files from tmp transcoded directory to the appropriate place
336 await ensureDir(VideoPathManager.Instance.getFSHLSOutputPath(video))
337
338 // Move playlist file
339 const resolutionPlaylistPath = VideoPathManager.Instance.getFSHLSOutputPath(video, resolutionPlaylistFilename)
340 await move(resolutionPlaylistFileTranscodePath, resolutionPlaylistPath, { overwrite: true })
341 // Move video file
342 await move(join(videoTranscodedBasePath, videoFilename), videoFilePath, { overwrite: true })
343
344 const stats = await stat(videoFilePath)
345
346 newVideoFile.size = stats.size
347 newVideoFile.fps = await getVideoFileFPS(videoFilePath)
348 newVideoFile.metadata = await getMetadataFromFile(videoFilePath)
349
350 await createTorrentAndSetInfoHash(playlist, newVideoFile)
351
352 const savedVideoFile = await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined)
353
354 const playlistWithFiles = playlist as MStreamingPlaylistFilesVideo
355 playlistWithFiles.VideoFiles = await playlist.$get('VideoFiles')
356 playlist.assignP2PMediaLoaderInfoHashes(video, playlistWithFiles.VideoFiles)
357
358 await playlist.save()
359
360 video.setHLSPlaylist(playlist)
361
362 await updateMasterHLSPlaylist(video, playlistWithFiles)
363 await updateSha256VODSegments(video, playlistWithFiles)
364
365 return { resolutionPlaylistPath, videoFile: savedVideoFile }
366 }