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