]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/lib/video-transcoding.ts
ca969b235903d8265a43ad91b137a983827caa8a
[github/Chocobozzz/PeerTube.git] / server / lib / video-transcoding.ts
1 import { copyFile, ensureDir, move, remove, stat } from 'fs-extra'
2 import { basename, extname as extnameUtil, join } from 'path'
3 import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
4 import { MStreamingPlaylistFilesVideo, MVideoFile, MVideoWithAllFiles, MVideoWithFile } from '@server/types/models'
5 import { VideoResolution } from '../../shared/models/videos'
6 import { VideoStreamingPlaylistType } from '../../shared/models/videos/video-streaming-playlist.type'
7 import { transcode, TranscodeOptions, TranscodeOptionsType } from '../helpers/ffmpeg-utils'
8 import { canDoQuickTranscode, getDurationFromVideoFile, getMetadataFromFile, getVideoFileFPS } from '../helpers/ffprobe-utils'
9 import { logger } from '../helpers/logger'
10 import { CONFIG } from '../initializers/config'
11 import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION, WEBSERVER } from '../initializers/constants'
12 import { VideoFileModel } from '../models/video/video-file'
13 import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
14 import { updateMasterHLSPlaylist, updateSha256VODSegments } from './hls'
15 import { generateVideoStreamingPlaylistName, getVideoFilename, getVideoFilePath } from './video-paths'
16
17 /**
18 * Optimize the original video file and replace it. The resolution is not changed.
19 */
20 async function optimizeOriginalVideofile (video: MVideoWithFile, inputVideoFileArg?: MVideoFile) {
21 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
22 const newExtname = '.mp4'
23
24 const inputVideoFile = inputVideoFileArg || video.getMaxQualityFile()
25 const videoInputPath = getVideoFilePath(video, inputVideoFile)
26 const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
27
28 const transcodeType: TranscodeOptionsType = await canDoQuickTranscode(videoInputPath)
29 ? 'quick-transcode'
30 : 'video'
31
32 const transcodeOptions: TranscodeOptions = {
33 type: transcodeType,
34 inputPath: videoInputPath,
35 outputPath: videoTranscodedPath,
36 resolution: inputVideoFile.resolution
37 }
38
39 // Could be very long!
40 await transcode(transcodeOptions)
41
42 try {
43 await remove(videoInputPath)
44
45 // Important to do this before getVideoFilename() to take in account the new file extension
46 inputVideoFile.extname = newExtname
47
48 const videoOutputPath = getVideoFilePath(video, inputVideoFile)
49
50 await onVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath)
51 } catch (err) {
52 // Auto destruction...
53 video.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', { err }))
54
55 throw err
56 }
57 }
58
59 /**
60 * Transcode the original video file to a lower resolution.
61 */
62 async function transcodeNewResolution (video: MVideoWithFile, resolution: VideoResolution, isPortrait: boolean) {
63 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
64 const extname = '.mp4'
65
66 // We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed
67 const videoInputPath = getVideoFilePath(video, video.getMaxQualityFile())
68
69 const newVideoFile = new VideoFileModel({
70 resolution,
71 extname,
72 size: 0,
73 videoId: video.id
74 })
75 const videoOutputPath = getVideoFilePath(video, newVideoFile)
76 const videoTranscodedPath = join(transcodeDirectory, getVideoFilename(video, newVideoFile))
77
78 const transcodeOptions = resolution === VideoResolution.H_NOVIDEO
79 ? {
80 type: 'only-audio' as 'only-audio',
81 inputPath: videoInputPath,
82 outputPath: videoTranscodedPath,
83 resolution
84 }
85 : {
86 type: 'video' as 'video',
87 inputPath: videoInputPath,
88 outputPath: videoTranscodedPath,
89 resolution,
90 isPortraitMode: isPortrait
91 }
92
93 await transcode(transcodeOptions)
94
95 return onVideoFileTranscoding(video, newVideoFile, videoTranscodedPath, videoOutputPath)
96 }
97
98 async function mergeAudioVideofile (video: MVideoWithAllFiles, resolution: VideoResolution) {
99 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
100 const newExtname = '.mp4'
101
102 const inputVideoFile = video.getMinQualityFile()
103
104 const audioInputPath = getVideoFilePath(video, inputVideoFile)
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 = getVideoFilePath(video, 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 (options: {
143 video: MVideoWithFile
144 videoInputPath: string
145 resolution: VideoResolution
146 copyCodecs: boolean
147 isPortraitMode: boolean
148 }) {
149 const { video, videoInputPath, resolution, copyCodecs, isPortraitMode } = options
150
151 const baseHlsDirectory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid)
152 await ensureDir(join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid))
153
154 const outputPath = join(baseHlsDirectory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(resolution))
155 const videoFilename = generateVideoStreamingPlaylistName(video.uuid, resolution)
156
157 const transcodeOptions = {
158 type: 'hls' as 'hls',
159 inputPath: videoInputPath,
160 outputPath,
161 resolution,
162 copyCodecs,
163 isPortraitMode,
164
165 hlsPlaylist: {
166 videoFilename
167 }
168 }
169
170 await transcode(transcodeOptions)
171
172 const playlistUrl = WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsMasterPlaylistStaticPath(video.uuid)
173
174 const [ videoStreamingPlaylist ] = await VideoStreamingPlaylistModel.upsert({
175 videoId: video.id,
176 playlistUrl,
177 segmentsSha256Url: WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsSha256SegmentsStaticPath(video.uuid, video.isLive),
178 p2pMediaLoaderInfohashes: [],
179 p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION,
180
181 type: VideoStreamingPlaylistType.HLS
182 }, { returning: true }) as [ MStreamingPlaylistFilesVideo, boolean ]
183 videoStreamingPlaylist.Video = video
184
185 const newVideoFile = new VideoFileModel({
186 resolution,
187 extname: extnameUtil(videoFilename),
188 size: 0,
189 fps: -1,
190 videoStreamingPlaylistId: videoStreamingPlaylist.id
191 })
192
193 const videoFilePath = getVideoFilePath(videoStreamingPlaylist, newVideoFile)
194 const stats = await stat(videoFilePath)
195
196 newVideoFile.size = stats.size
197 newVideoFile.fps = await getVideoFileFPS(videoFilePath)
198 newVideoFile.metadata = await getMetadataFromFile(videoFilePath)
199
200 await createTorrentAndSetInfoHash(videoStreamingPlaylist, newVideoFile)
201
202 await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined)
203 videoStreamingPlaylist.VideoFiles = await videoStreamingPlaylist.$get('VideoFiles')
204
205 videoStreamingPlaylist.p2pMediaLoaderInfohashes = VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(
206 playlistUrl, videoStreamingPlaylist.VideoFiles
207 )
208 await videoStreamingPlaylist.save()
209
210 video.setHLSPlaylist(videoStreamingPlaylist)
211
212 await updateMasterHLSPlaylist(video)
213 await updateSha256VODSegments(video)
214
215 return video
216 }
217
218 // ---------------------------------------------------------------------------
219
220 export {
221 generateHlsPlaylist,
222 optimizeOriginalVideofile,
223 transcodeNewResolution,
224 mergeAudioVideofile
225 }
226
227 // ---------------------------------------------------------------------------
228
229 async function onVideoFileTranscoding (video: MVideoWithFile, videoFile: MVideoFile, transcodingPath: string, outputPath: string) {
230 const stats = await stat(transcodingPath)
231 const fps = await getVideoFileFPS(transcodingPath)
232 const metadata = await getMetadataFromFile(transcodingPath)
233
234 await move(transcodingPath, outputPath, { overwrite: true })
235
236 videoFile.size = stats.size
237 videoFile.fps = fps
238 videoFile.metadata = metadata
239
240 await createTorrentAndSetInfoHash(video, videoFile)
241
242 await VideoFileModel.customUpsert(videoFile, 'video', undefined)
243 video.VideoFiles = await video.$get('VideoFiles')
244
245 return video
246 }