]>
Commit | Line | Data |
---|---|---|
0c9668f7 C |
1 | import { Job } from 'bullmq' |
2 | import { copyFile, move, remove, stat } from 'fs-extra' | |
3 | import { basename, join } from 'path' | |
4 | import { computeOutputFPS } from '@server/helpers/ffmpeg' | |
5 | import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' | |
6 | import { MVideoFile, MVideoFullLight } from '@server/types/models' | |
7 | import { toEven } from '@shared/core-utils' | |
8 | import { ffprobePromise, getVideoStreamDuration, getVideoStreamFPS, TranscodeVODOptionsType } from '@shared/ffmpeg' | |
9 | import { VideoResolution, VideoStorage } from '@shared/models' | |
10 | import { CONFIG } from '../../initializers/config' | |
11 | import { VideoFileModel } from '../../models/video/video-file' | |
12 | import { generateWebTorrentVideoFilename } from '../paths' | |
13 | import { buildFileMetadata } from '../video-file' | |
14 | import { VideoPathManager } from '../video-path-manager' | |
15 | import { buildFFmpegVOD } from './shared' | |
16 | import { computeResolutionsToTranscode } from './transcoding-resolutions' | |
cc2abbc3 | 17 | import { VideoModel } from '@server/models/video/video' |
0c9668f7 C |
18 | |
19 | // Optimize the original video file and replace it. The resolution is not changed. | |
20 | export async function optimizeOriginalVideofile (options: { | |
21 | video: MVideoFullLight | |
22 | inputVideoFile: MVideoFile | |
23 | quickTranscode: boolean | |
24 | job: Job | |
25 | }) { | |
26 | const { video, inputVideoFile, quickTranscode, job } = options | |
27 | ||
28 | const transcodeDirectory = CONFIG.STORAGE.TMP_DIR | |
29 | const newExtname = '.mp4' | |
30 | ||
31 | // Will be released by our transcodeVOD function once ffmpeg is ran | |
32 | const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid) | |
33 | ||
34 | try { | |
35 | await video.reload() | |
cc2abbc3 | 36 | await inputVideoFile.reload() |
0c9668f7 C |
37 | |
38 | const fileWithVideoOrPlaylist = inputVideoFile.withVideoOrPlaylist(video) | |
39 | ||
40 | const result = await VideoPathManager.Instance.makeAvailableVideoFile(fileWithVideoOrPlaylist, async videoInputPath => { | |
41 | const videoOutputPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname) | |
42 | ||
43 | const transcodeType: TranscodeVODOptionsType = quickTranscode | |
44 | ? 'quick-transcode' | |
45 | : 'video' | |
46 | ||
47 | const resolution = buildOriginalFileResolution(inputVideoFile.resolution) | |
48 | const fps = computeOutputFPS({ inputFPS: inputVideoFile.fps, resolution }) | |
49 | ||
50 | // Could be very long! | |
51 | await buildFFmpegVOD(job).transcode({ | |
52 | type: transcodeType, | |
53 | ||
54 | inputPath: videoInputPath, | |
55 | outputPath: videoOutputPath, | |
56 | ||
57 | inputFileMutexReleaser, | |
58 | ||
59 | resolution, | |
60 | fps | |
61 | }) | |
62 | ||
63 | // Important to do this before getVideoFilename() to take in account the new filename | |
64 | inputVideoFile.resolution = resolution | |
65 | inputVideoFile.extname = newExtname | |
66 | inputVideoFile.filename = generateWebTorrentVideoFilename(resolution, newExtname) | |
67 | inputVideoFile.storage = VideoStorage.FILE_SYSTEM | |
68 | ||
69 | const { videoFile } = await onWebTorrentVideoFileTranscoding({ | |
70 | video, | |
71 | videoFile: inputVideoFile, | |
72 | videoOutputPath | |
73 | }) | |
74 | ||
75 | await remove(videoInputPath) | |
76 | ||
77 | return { transcodeType, videoFile } | |
78 | }) | |
79 | ||
80 | return result | |
81 | } finally { | |
82 | inputFileMutexReleaser() | |
83 | } | |
84 | } | |
85 | ||
86 | // Transcode the original video file to a lower resolution compatible with WebTorrent | |
87 | export async function transcodeNewWebTorrentResolution (options: { | |
88 | video: MVideoFullLight | |
89 | resolution: VideoResolution | |
90 | fps: number | |
91 | job: Job | |
92 | }) { | |
cc2abbc3 | 93 | const { video: videoArg, resolution, fps, job } = options |
0c9668f7 C |
94 | |
95 | const transcodeDirectory = CONFIG.STORAGE.TMP_DIR | |
96 | const newExtname = '.mp4' | |
97 | ||
cc2abbc3 | 98 | const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(videoArg.uuid) |
0c9668f7 C |
99 | |
100 | try { | |
cc2abbc3 | 101 | const video = await VideoModel.loadFull(videoArg.uuid) |
0c9668f7 C |
102 | const file = video.getMaxQualityFile().withVideoOrPlaylist(video) |
103 | ||
104 | const result = await VideoPathManager.Instance.makeAvailableVideoFile(file, async videoInputPath => { | |
105 | const newVideoFile = new VideoFileModel({ | |
106 | resolution, | |
107 | extname: newExtname, | |
108 | filename: generateWebTorrentVideoFilename(resolution, newExtname), | |
109 | size: 0, | |
110 | videoId: video.id | |
111 | }) | |
112 | ||
113 | const videoOutputPath = join(transcodeDirectory, newVideoFile.filename) | |
114 | ||
115 | const transcodeOptions = { | |
116 | type: 'video' as 'video', | |
117 | ||
118 | inputPath: videoInputPath, | |
119 | outputPath: videoOutputPath, | |
120 | ||
121 | inputFileMutexReleaser, | |
122 | ||
123 | resolution, | |
124 | fps | |
125 | } | |
126 | ||
127 | await buildFFmpegVOD(job).transcode(transcodeOptions) | |
128 | ||
129 | return onWebTorrentVideoFileTranscoding({ video, videoFile: newVideoFile, videoOutputPath }) | |
130 | }) | |
131 | ||
132 | return result | |
133 | } finally { | |
134 | inputFileMutexReleaser() | |
135 | } | |
136 | } | |
137 | ||
138 | // Merge an image with an audio file to create a video | |
139 | export async function mergeAudioVideofile (options: { | |
140 | video: MVideoFullLight | |
141 | resolution: VideoResolution | |
142 | fps: number | |
143 | job: Job | |
144 | }) { | |
cc2abbc3 | 145 | const { video: videoArg, resolution, fps, job } = options |
0c9668f7 C |
146 | |
147 | const transcodeDirectory = CONFIG.STORAGE.TMP_DIR | |
148 | const newExtname = '.mp4' | |
149 | ||
cc2abbc3 | 150 | const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(videoArg.uuid) |
0c9668f7 C |
151 | |
152 | try { | |
cc2abbc3 | 153 | const video = await VideoModel.loadFull(videoArg.uuid) |
0c9668f7 C |
154 | const inputVideoFile = video.getMinQualityFile() |
155 | ||
156 | const fileWithVideoOrPlaylist = inputVideoFile.withVideoOrPlaylist(video) | |
157 | ||
158 | const result = await VideoPathManager.Instance.makeAvailableVideoFile(fileWithVideoOrPlaylist, async audioInputPath => { | |
159 | const videoOutputPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname) | |
160 | ||
161 | // If the user updates the video preview during transcoding | |
162 | const previewPath = video.getPreview().getPath() | |
163 | const tmpPreviewPath = join(CONFIG.STORAGE.TMP_DIR, basename(previewPath)) | |
164 | await copyFile(previewPath, tmpPreviewPath) | |
165 | ||
166 | const transcodeOptions = { | |
167 | type: 'merge-audio' as 'merge-audio', | |
168 | ||
169 | inputPath: tmpPreviewPath, | |
170 | outputPath: videoOutputPath, | |
171 | ||
172 | inputFileMutexReleaser, | |
173 | ||
174 | audioPath: audioInputPath, | |
175 | resolution, | |
176 | fps | |
177 | } | |
178 | ||
179 | try { | |
180 | await buildFFmpegVOD(job).transcode(transcodeOptions) | |
181 | ||
182 | await remove(audioInputPath) | |
183 | await remove(tmpPreviewPath) | |
184 | } catch (err) { | |
185 | await remove(tmpPreviewPath) | |
186 | throw err | |
187 | } | |
188 | ||
189 | // Important to do this before getVideoFilename() to take in account the new file extension | |
190 | inputVideoFile.extname = newExtname | |
191 | inputVideoFile.resolution = resolution | |
192 | inputVideoFile.filename = generateWebTorrentVideoFilename(inputVideoFile.resolution, newExtname) | |
193 | ||
194 | // ffmpeg generated a new video file, so update the video duration | |
195 | // See https://trac.ffmpeg.org/ticket/5456 | |
196 | video.duration = await getVideoStreamDuration(videoOutputPath) | |
197 | await video.save() | |
198 | ||
199 | return onWebTorrentVideoFileTranscoding({ | |
200 | video, | |
201 | videoFile: inputVideoFile, | |
202 | videoOutputPath | |
203 | }) | |
204 | }) | |
205 | ||
206 | return result | |
207 | } finally { | |
208 | inputFileMutexReleaser() | |
209 | } | |
210 | } | |
211 | ||
212 | export async function onWebTorrentVideoFileTranscoding (options: { | |
213 | video: MVideoFullLight | |
214 | videoFile: MVideoFile | |
215 | videoOutputPath: string | |
216 | }) { | |
217 | const { video, videoFile, videoOutputPath } = options | |
218 | ||
219 | const mutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid) | |
220 | ||
221 | try { | |
222 | await video.reload() | |
223 | ||
224 | const outputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, videoFile) | |
225 | ||
226 | const stats = await stat(videoOutputPath) | |
227 | ||
228 | const probe = await ffprobePromise(videoOutputPath) | |
229 | const fps = await getVideoStreamFPS(videoOutputPath, probe) | |
230 | const metadata = await buildFileMetadata(videoOutputPath, probe) | |
231 | ||
232 | await move(videoOutputPath, outputPath, { overwrite: true }) | |
233 | ||
234 | videoFile.size = stats.size | |
235 | videoFile.fps = fps | |
236 | videoFile.metadata = metadata | |
237 | ||
238 | await createTorrentAndSetInfoHash(video, videoFile) | |
239 | ||
240 | const oldFile = await VideoFileModel.loadWebTorrentFile({ videoId: video.id, fps: videoFile.fps, resolution: videoFile.resolution }) | |
241 | if (oldFile) await video.removeWebTorrentFile(oldFile) | |
242 | ||
243 | await VideoFileModel.customUpsert(videoFile, 'video', undefined) | |
244 | video.VideoFiles = await video.$get('VideoFiles') | |
245 | ||
246 | return { video, videoFile } | |
247 | } finally { | |
248 | mutexReleaser() | |
249 | } | |
250 | } | |
251 | ||
252 | // --------------------------------------------------------------------------- | |
253 | ||
254 | function buildOriginalFileResolution (inputResolution: number) { | |
255 | if (CONFIG.TRANSCODING.ALWAYS_TRANSCODE_ORIGINAL_RESOLUTION === true) { | |
256 | return toEven(inputResolution) | |
257 | } | |
258 | ||
259 | const resolutions = computeResolutionsToTranscode({ | |
260 | input: inputResolution, | |
261 | type: 'vod', | |
262 | includeInput: false, | |
263 | strictLower: false, | |
264 | // We don't really care about the audio resolution in this context | |
265 | hasAudio: true | |
266 | }) | |
267 | ||
268 | if (resolutions.length === 0) { | |
269 | return toEven(inputResolution) | |
270 | } | |
271 | ||
272 | return Math.max(...resolutions) | |
273 | } |