]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/lib/video-transcoding.ts
Fix tsconfig with CLI tools
[github/Chocobozzz/PeerTube.git] / server / lib / video-transcoding.ts
1 import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION, WEBSERVER } from '../initializers/constants'
2 import { basename, join } from 'path'
3 import {
4 canDoQuickTranscode,
5 getDurationFromVideoFile,
6 getVideoFileFPS,
7 transcode,
8 TranscodeOptions,
9 TranscodeOptionsType
10 } from '../helpers/ffmpeg-utils'
11 import { copyFile, ensureDir, move, remove, stat } from 'fs-extra'
12 import { logger } from '../helpers/logger'
13 import { VideoResolution } from '../../shared/models/videos'
14 import { VideoFileModel } from '../models/video/video-file'
15 import { updateMasterHLSPlaylist, updateSha256Segments } from './hls'
16 import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
17 import { VideoStreamingPlaylistType } from '../../shared/models/videos/video-streaming-playlist.type'
18 import { CONFIG } from '../initializers/config'
19 import { MVideoFile, MVideoWithFile, MVideoWithFileThumbnail } from '@server/typings/models'
20
21 /**
22 * Optimize the original video file and replace it. The resolution is not changed.
23 */
24 async function optimizeVideofile (video: MVideoWithFile, inputVideoFileArg?: MVideoFile) {
25 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
26 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
27 const newExtname = '.mp4'
28
29 const inputVideoFile = inputVideoFileArg ? inputVideoFileArg : video.getOriginalFile()
30 const videoInputPath = join(videosDirectory, video.getVideoFilename(inputVideoFile))
31 const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
32
33 const transcodeType: TranscodeOptionsType = await canDoQuickTranscode(videoInputPath)
34 ? 'quick-transcode'
35 : 'video'
36
37 const transcodeOptions: TranscodeOptions = {
38 type: transcodeType as any, // FIXME: typing issue
39 inputPath: videoInputPath,
40 outputPath: videoTranscodedPath,
41 resolution: inputVideoFile.resolution
42 }
43
44 // Could be very long!
45 await transcode(transcodeOptions)
46
47 try {
48 await remove(videoInputPath)
49
50 // Important to do this before getVideoFilename() to take in account the new file extension
51 inputVideoFile.extname = newExtname
52
53 const videoOutputPath = video.getVideoFilePath(inputVideoFile)
54
55 await onVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath)
56 } catch (err) {
57 // Auto destruction...
58 video.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', { err }))
59
60 throw err
61 }
62 }
63
64 /**
65 * Transcode the original video file to a lower resolution.
66 */
67 async function transcodeOriginalVideofile (video: MVideoWithFile, resolution: VideoResolution, isPortrait: boolean) {
68 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
69 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
70 const extname = '.mp4'
71
72 // We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed
73 const videoInputPath = join(videosDirectory, video.getVideoFilename(video.getOriginalFile()))
74
75 const newVideoFile = new VideoFileModel({
76 resolution,
77 extname,
78 size: 0,
79 videoId: video.id
80 })
81 const videoOutputPath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename(newVideoFile))
82 const videoTranscodedPath = join(transcodeDirectory, video.getVideoFilename(newVideoFile))
83
84 const transcodeOptions = {
85 type: 'video' as 'video',
86 inputPath: videoInputPath,
87 outputPath: videoTranscodedPath,
88 resolution,
89 isPortraitMode: isPortrait
90 }
91
92 await transcode(transcodeOptions)
93
94 return onVideoFileTranscoding(video, newVideoFile, videoTranscodedPath, videoOutputPath)
95 }
96
97 async function mergeAudioVideofile (video: MVideoWithFileThumbnail, resolution: VideoResolution) {
98 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
99 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
100 const newExtname = '.mp4'
101
102 const inputVideoFile = video.getOriginalFile()
103
104 const audioInputPath = join(videosDirectory, video.getVideoFilename(video.getOriginalFile()))
105 const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
106
107 // If the user updates the video preview during transcoding
108 const previewPath = video.getPreview().getPath()
109 const tmpPreviewPath = join(CONFIG.STORAGE.TMP_DIR, basename(previewPath))
110 await copyFile(previewPath, tmpPreviewPath)
111
112 const transcodeOptions = {
113 type: 'merge-audio' as 'merge-audio',
114 inputPath: tmpPreviewPath,
115 outputPath: videoTranscodedPath,
116 audioPath: audioInputPath,
117 resolution
118 }
119
120 try {
121 await transcode(transcodeOptions)
122
123 await remove(audioInputPath)
124 await remove(tmpPreviewPath)
125 } catch (err) {
126 await remove(tmpPreviewPath)
127 throw err
128 }
129
130 // Important to do this before getVideoFilename() to take in account the new file extension
131 inputVideoFile.extname = newExtname
132
133 const videoOutputPath = video.getVideoFilePath(inputVideoFile)
134 // ffmpeg generated a new video file, so update the video duration
135 // See https://trac.ffmpeg.org/ticket/5456
136 video.duration = await getDurationFromVideoFile(videoTranscodedPath)
137 await video.save()
138
139 return onVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath)
140 }
141
142 async function generateHlsPlaylist (video: MVideoWithFile, resolution: VideoResolution, isPortraitMode: boolean) {
143 const baseHlsDirectory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid)
144 await ensureDir(join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid))
145
146 const videoInputPath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename(video.getFile(resolution)))
147 const outputPath = join(baseHlsDirectory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(resolution))
148
149 const transcodeOptions = {
150 type: 'hls' as 'hls',
151 inputPath: videoInputPath,
152 outputPath,
153 resolution,
154 isPortraitMode,
155
156 hlsPlaylist: {
157 videoFilename: VideoStreamingPlaylistModel.getHlsVideoName(video.uuid, resolution)
158 }
159 }
160
161 await transcode(transcodeOptions)
162
163 await updateMasterHLSPlaylist(video)
164 await updateSha256Segments(video)
165
166 const playlistUrl = WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsMasterPlaylistStaticPath(video.uuid)
167
168 await VideoStreamingPlaylistModel.upsert({
169 videoId: video.id,
170 playlistUrl,
171 segmentsSha256Url: WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsSha256SegmentsStaticPath(video.uuid),
172 p2pMediaLoaderInfohashes: VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(playlistUrl, video.VideoFiles),
173 p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION,
174
175 type: VideoStreamingPlaylistType.HLS
176 })
177 }
178
179 // ---------------------------------------------------------------------------
180
181 export {
182 generateHlsPlaylist,
183 optimizeVideofile,
184 transcodeOriginalVideofile,
185 mergeAudioVideofile
186 }
187
188 // ---------------------------------------------------------------------------
189
190 async function onVideoFileTranscoding (video: MVideoWithFile, videoFile: MVideoFile, transcodingPath: string, outputPath: string) {
191 const stats = await stat(transcodingPath)
192 const fps = await getVideoFileFPS(transcodingPath)
193
194 await move(transcodingPath, outputPath)
195
196 videoFile.size = stats.size
197 videoFile.fps = fps
198
199 await video.createTorrentAndSetInfoHash(videoFile)
200
201 const updatedVideoFile = await videoFile.save()
202
203 // Add it if this is a new created file
204 if (video.VideoFiles.some(f => f.id === videoFile.id) === false) {
205 video.VideoFiles.push(updatedVideoFile)
206 }
207
208 return video
209 }