]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/lib/video-transcoding.ts
Split ffmpeg utils with ffprobe utils
[github/Chocobozzz/PeerTube.git] / server / lib / video-transcoding.ts
CommitLineData
053aed43 1import { copyFile, ensureDir, move, remove, stat } from 'fs-extra'
d7a25329 2import { basename, extname as extnameUtil, join } from 'path'
053aed43
C
3import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
4import { MStreamingPlaylistFilesVideo, MVideoFile, MVideoWithAllFiles, MVideoWithFile } from '@server/types/models'
5import { VideoResolution } from '../../shared/models/videos'
6import { VideoStreamingPlaylistType } from '../../shared/models/videos/video-streaming-playlist.type'
daf6e480
C
7import { transcode, TranscodeOptions, TranscodeOptionsType } from '../helpers/ffmpeg-utils'
8import { canDoQuickTranscode, getDurationFromVideoFile, getMetadataFromFile, getVideoFileFPS } from '../helpers/ffprobe-utils'
098eb377 9import { logger } from '../helpers/logger'
053aed43
C
10import { CONFIG } from '../initializers/config'
11import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION, WEBSERVER } from '../initializers/constants'
098eb377 12import { VideoFileModel } from '../models/video/video-file'
09209296 13import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
053aed43 14import { updateMasterHLSPlaylist, updateSha256VODSegments } from './hls'
d7a25329 15import { generateVideoStreamingPlaylistName, getVideoFilename, getVideoFilePath } from './video-paths'
098eb377 16
658a47ab
FA
17/**
18 * Optimize the original video file and replace it. The resolution is not changed.
19 */
d7a25329 20async function optimizeOriginalVideofile (video: MVideoWithFile, inputVideoFileArg?: MVideoFile) {
2fbd5e25 21 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
098eb377 22 const newExtname = '.mp4'
9f1ddd24 23
d7a25329
C
24 const inputVideoFile = inputVideoFileArg || video.getMaxQualityFile()
25 const videoInputPath = getVideoFilePath(video, inputVideoFile)
2fbd5e25 26 const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
098eb377 27
536598cf
C
28 const transcodeType: TranscodeOptionsType = await canDoQuickTranscode(videoInputPath)
29 ? 'quick-transcode'
30 : 'video'
5ba49f26 31
536598cf 32 const transcodeOptions: TranscodeOptions = {
d7a25329 33 type: transcodeType,
098eb377 34 inputPath: videoInputPath,
09209296 35 outputPath: videoTranscodedPath,
536598cf 36 resolution: inputVideoFile.resolution
098eb377
C
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
536598cf 46 inputVideoFile.extname = newExtname
2fbd5e25 47
d7a25329 48 const videoOutputPath = getVideoFilePath(video, inputVideoFile)
098eb377 49
536598cf 50 await onVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath)
098eb377
C
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
658a47ab
FA
59/**
60 * Transcode the original video file to a lower resolution.
61 */
d7a25329 62async function transcodeNewResolution (video: MVideoWithFile, resolution: VideoResolution, isPortrait: boolean) {
2fbd5e25 63 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
098eb377
C
64 const extname = '.mp4'
65
66 // We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed
d7a25329 67 const videoInputPath = getVideoFilePath(video, video.getMaxQualityFile())
098eb377
C
68
69 const newVideoFile = new VideoFileModel({
70 resolution,
71 extname,
72 size: 0,
73 videoId: video.id
74 })
d7a25329
C
75 const videoOutputPath = getVideoFilePath(video, newVideoFile)
76 const videoTranscodedPath = join(transcodeDirectory, getVideoFilename(video, newVideoFile))
098eb377 77
5c7d6508 78 const transcodeOptions = resolution === VideoResolution.H_NOVIDEO
79 ? {
3a149e9f
C
80 type: 'only-audio' as 'only-audio',
81 inputPath: videoInputPath,
82 outputPath: videoTranscodedPath,
83 resolution
84 }
5c7d6508 85 : {
3a149e9f
C
86 type: 'video' as 'video',
87 inputPath: videoInputPath,
88 outputPath: videoTranscodedPath,
89 resolution,
90 isPortraitMode: isPortrait
91 }
098eb377
C
92
93 await transcode(transcodeOptions)
94
536598cf
C
95 return onVideoFileTranscoding(video, newVideoFile, videoTranscodedPath, videoOutputPath)
96}
97
d7a25329 98async function mergeAudioVideofile (video: MVideoWithAllFiles, resolution: VideoResolution) {
536598cf
C
99 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
100 const newExtname = '.mp4'
101
92e0f42e 102 const inputVideoFile = video.getMinQualityFile()
2fbd5e25 103
d7a25329 104 const audioInputPath = getVideoFilePath(video, inputVideoFile)
536598cf 105 const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
098eb377 106
eba06469
C
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
536598cf
C
112 const transcodeOptions = {
113 type: 'merge-audio' as 'merge-audio',
eba06469 114 inputPath: tmpPreviewPath,
536598cf
C
115 outputPath: videoTranscodedPath,
116 audioPath: audioInputPath,
117 resolution
118 }
098eb377 119
eba06469
C
120 try {
121 await transcode(transcodeOptions)
098eb377 122
eba06469
C
123 await remove(audioInputPath)
124 await remove(tmpPreviewPath)
125 } catch (err) {
126 await remove(tmpPreviewPath)
127 throw err
128 }
098eb377 129
536598cf
C
130 // Important to do this before getVideoFilename() to take in account the new file extension
131 inputVideoFile.extname = newExtname
132
d7a25329 133 const videoOutputPath = getVideoFilePath(video, inputVideoFile)
eba06469
C
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()
536598cf
C
138
139 return onVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath)
098eb377
C
140}
141
b5b68755
C
142async 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
9c6ca37f
C
151 const baseHlsDirectory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid)
152 await ensureDir(join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid))
09209296 153
09209296 154 const outputPath = join(baseHlsDirectory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(resolution))
d7a25329 155 const videoFilename = generateVideoStreamingPlaylistName(video.uuid, resolution)
09209296
C
156
157 const transcodeOptions = {
536598cf 158 type: 'hls' as 'hls',
09209296
C
159 inputPath: videoInputPath,
160 outputPath,
161 resolution,
d7a25329 162 copyCodecs,
09209296 163 isPortraitMode,
4c280004
C
164
165 hlsPlaylist: {
d7a25329 166 videoFilename
4c280004 167 }
09209296
C
168 }
169
d7a25329 170 await transcode(transcodeOptions)
09209296 171
6dd9de95 172 const playlistUrl = WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsMasterPlaylistStaticPath(video.uuid)
09209296 173
d7a25329 174 const [ videoStreamingPlaylist ] = await VideoStreamingPlaylistModel.upsert({
09209296
C
175 videoId: video.id,
176 playlistUrl,
c6c0fa6c 177 segmentsSha256Url: WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsSha256SegmentsStaticPath(video.uuid, video.isLive),
b5b68755 178 p2pMediaLoaderInfohashes: [],
594d0c6a 179 p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION,
09209296
C
180
181 type: VideoStreamingPlaylistType.HLS
d7a25329
C
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
09209296 191 })
d7a25329
C
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)
8319d6ae 198 newVideoFile.metadata = await getMetadataFromFile(videoFilePath)
d7a25329
C
199
200 await createTorrentAndSetInfoHash(videoStreamingPlaylist, newVideoFile)
201
c547bbf9 202 await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined)
e6122097 203 videoStreamingPlaylist.VideoFiles = await videoStreamingPlaylist.$get('VideoFiles')
d7a25329 204
b5b68755
C
205 videoStreamingPlaylist.p2pMediaLoaderInfohashes = VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(
206 playlistUrl, videoStreamingPlaylist.VideoFiles
207 )
208 await videoStreamingPlaylist.save()
209
d7a25329
C
210 video.setHLSPlaylist(videoStreamingPlaylist)
211
212 await updateMasterHLSPlaylist(video)
c6c0fa6c 213 await updateSha256VODSegments(video)
d7a25329
C
214
215 return video
09209296
C
216}
217
536598cf
C
218// ---------------------------------------------------------------------------
219
098eb377 220export {
09209296 221 generateHlsPlaylist,
d7a25329
C
222 optimizeOriginalVideofile,
223 transcodeNewResolution,
536598cf
C
224 mergeAudioVideofile
225}
226
227// ---------------------------------------------------------------------------
228
453e83ea 229async function onVideoFileTranscoding (video: MVideoWithFile, videoFile: MVideoFile, transcodingPath: string, outputPath: string) {
536598cf
C
230 const stats = await stat(transcodingPath)
231 const fps = await getVideoFileFPS(transcodingPath)
8319d6ae 232 const metadata = await getMetadataFromFile(transcodingPath)
536598cf 233
fb0f7f82 234 await move(transcodingPath, outputPath, { overwrite: true })
536598cf 235
453e83ea
C
236 videoFile.size = stats.size
237 videoFile.fps = fps
8319d6ae 238 videoFile.metadata = metadata
536598cf 239
d7a25329 240 await createTorrentAndSetInfoHash(video, videoFile)
536598cf 241
fb0f7f82
C
242 await VideoFileModel.customUpsert(videoFile, 'video', undefined)
243 video.VideoFiles = await video.$get('VideoFiles')
536598cf
C
244
245 return video
098eb377 246}