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