diff options
Diffstat (limited to 'server/lib/transcoding')
-rw-r--r-- | server/lib/transcoding/transcoding.ts | 386 |
1 files changed, 236 insertions, 150 deletions
diff --git a/server/lib/transcoding/transcoding.ts b/server/lib/transcoding/transcoding.ts index 44e26754d..c7b61e9ba 100644 --- a/server/lib/transcoding/transcoding.ts +++ b/server/lib/transcoding/transcoding.ts | |||
@@ -1,3 +1,4 @@ | |||
1 | import { MutexInterface } from 'async-mutex' | ||
1 | import { Job } from 'bullmq' | 2 | import { Job } from 'bullmq' |
2 | import { copyFile, ensureDir, move, remove, stat } from 'fs-extra' | 3 | import { copyFile, ensureDir, move, remove, stat } from 'fs-extra' |
3 | import { basename, extname as extnameUtil, join } from 'path' | 4 | import { basename, extname as extnameUtil, join } from 'path' |
@@ -6,11 +7,13 @@ import { retryTransactionWrapper } from '@server/helpers/database-utils' | |||
6 | import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' | 7 | import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' |
7 | import { sequelizeTypescript } from '@server/initializers/database' | 8 | import { sequelizeTypescript } from '@server/initializers/database' |
8 | import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models' | 9 | import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models' |
10 | import { pick } from '@shared/core-utils' | ||
9 | import { VideoResolution, VideoStorage } from '../../../shared/models/videos' | 11 | import { VideoResolution, VideoStorage } from '../../../shared/models/videos' |
10 | import { | 12 | import { |
11 | buildFileMetadata, | 13 | buildFileMetadata, |
12 | canDoQuickTranscode, | 14 | canDoQuickTranscode, |
13 | computeResolutionsToTranscode, | 15 | computeResolutionsToTranscode, |
16 | ffprobePromise, | ||
14 | getVideoStreamDuration, | 17 | getVideoStreamDuration, |
15 | getVideoStreamFPS, | 18 | getVideoStreamFPS, |
16 | transcodeVOD, | 19 | transcodeVOD, |
@@ -33,7 +36,7 @@ import { VideoTranscodingProfilesManager } from './default-transcoding-profiles' | |||
33 | */ | 36 | */ |
34 | 37 | ||
35 | // Optimize the original video file and replace it. The resolution is not changed. | 38 | // Optimize the original video file and replace it. The resolution is not changed. |
36 | function optimizeOriginalVideofile (options: { | 39 | async function optimizeOriginalVideofile (options: { |
37 | video: MVideoFullLight | 40 | video: MVideoFullLight |
38 | inputVideoFile: MVideoFile | 41 | inputVideoFile: MVideoFile |
39 | job: Job | 42 | job: Job |
@@ -43,49 +46,62 @@ function optimizeOriginalVideofile (options: { | |||
43 | const transcodeDirectory = CONFIG.STORAGE.TMP_DIR | 46 | const transcodeDirectory = CONFIG.STORAGE.TMP_DIR |
44 | const newExtname = '.mp4' | 47 | const newExtname = '.mp4' |
45 | 48 | ||
46 | return VideoPathManager.Instance.makeAvailableVideoFile(inputVideoFile.withVideoOrPlaylist(video), async videoInputPath => { | 49 | // Will be released by our transcodeVOD function once ffmpeg is ran |
47 | const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname) | 50 | const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid) |
48 | 51 | ||
49 | const transcodeType: TranscodeVODOptionsType = await canDoQuickTranscode(videoInputPath) | 52 | try { |
50 | ? 'quick-transcode' | 53 | await video.reload() |
51 | : 'video' | ||
52 | 54 | ||
53 | const resolution = buildOriginalFileResolution(inputVideoFile.resolution) | 55 | const fileWithVideoOrPlaylist = inputVideoFile.withVideoOrPlaylist(video) |
54 | 56 | ||
55 | const transcodeOptions: TranscodeVODOptions = { | 57 | const result = await VideoPathManager.Instance.makeAvailableVideoFile(fileWithVideoOrPlaylist, async videoInputPath => { |
56 | type: transcodeType, | 58 | const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname) |
57 | 59 | ||
58 | inputPath: videoInputPath, | 60 | const transcodeType: TranscodeVODOptionsType = await canDoQuickTranscode(videoInputPath) |
59 | outputPath: videoTranscodedPath, | 61 | ? 'quick-transcode' |
62 | : 'video' | ||
60 | 63 | ||
61 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), | 64 | const resolution = buildOriginalFileResolution(inputVideoFile.resolution) |
62 | profile: CONFIG.TRANSCODING.PROFILE, | ||
63 | 65 | ||
64 | resolution, | 66 | const transcodeOptions: TranscodeVODOptions = { |
67 | type: transcodeType, | ||
65 | 68 | ||
66 | job | 69 | inputPath: videoInputPath, |
67 | } | 70 | outputPath: videoTranscodedPath, |
68 | 71 | ||
69 | // Could be very long! | 72 | inputFileMutexReleaser, |
70 | await transcodeVOD(transcodeOptions) | ||
71 | 73 | ||
72 | // Important to do this before getVideoFilename() to take in account the new filename | 74 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), |
73 | inputVideoFile.resolution = resolution | 75 | profile: CONFIG.TRANSCODING.PROFILE, |
74 | inputVideoFile.extname = newExtname | ||
75 | inputVideoFile.filename = generateWebTorrentVideoFilename(resolution, newExtname) | ||
76 | inputVideoFile.storage = VideoStorage.FILE_SYSTEM | ||
77 | 76 | ||
78 | const videoOutputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, inputVideoFile) | 77 | resolution, |
79 | 78 | ||
80 | const { videoFile } = await onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath) | 79 | job |
81 | await remove(videoInputPath) | 80 | } |
82 | 81 | ||
83 | return { transcodeType, videoFile } | 82 | // Could be very long! |
84 | }) | 83 | await transcodeVOD(transcodeOptions) |
84 | |||
85 | // Important to do this before getVideoFilename() to take in account the new filename | ||
86 | inputVideoFile.resolution = resolution | ||
87 | inputVideoFile.extname = newExtname | ||
88 | inputVideoFile.filename = generateWebTorrentVideoFilename(resolution, newExtname) | ||
89 | inputVideoFile.storage = VideoStorage.FILE_SYSTEM | ||
90 | |||
91 | const { videoFile } = await onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, inputVideoFile) | ||
92 | await remove(videoInputPath) | ||
93 | |||
94 | return { transcodeType, videoFile } | ||
95 | }) | ||
96 | |||
97 | return result | ||
98 | } finally { | ||
99 | inputFileMutexReleaser() | ||
100 | } | ||
85 | } | 101 | } |
86 | 102 | ||
87 | // Transcode the original video file to a lower resolution compatible with WebTorrent | 103 | // Transcode the original video file to a lower resolution compatible with WebTorrent |
88 | function transcodeNewWebTorrentResolution (options: { | 104 | async function transcodeNewWebTorrentResolution (options: { |
89 | video: MVideoFullLight | 105 | video: MVideoFullLight |
90 | resolution: VideoResolution | 106 | resolution: VideoResolution |
91 | job: Job | 107 | job: Job |
@@ -95,53 +111,68 @@ function transcodeNewWebTorrentResolution (options: { | |||
95 | const transcodeDirectory = CONFIG.STORAGE.TMP_DIR | 111 | const transcodeDirectory = CONFIG.STORAGE.TMP_DIR |
96 | const newExtname = '.mp4' | 112 | const newExtname = '.mp4' |
97 | 113 | ||
98 | return VideoPathManager.Instance.makeAvailableVideoFile(video.getMaxQualityFile().withVideoOrPlaylist(video), async videoInputPath => { | 114 | const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid) |
99 | const newVideoFile = new VideoFileModel({ | ||
100 | resolution, | ||
101 | extname: newExtname, | ||
102 | filename: generateWebTorrentVideoFilename(resolution, newExtname), | ||
103 | size: 0, | ||
104 | videoId: video.id | ||
105 | }) | ||
106 | 115 | ||
107 | const videoOutputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, newVideoFile) | 116 | try { |
108 | const videoTranscodedPath = join(transcodeDirectory, newVideoFile.filename) | 117 | await video.reload() |
109 | 118 | ||
110 | const transcodeOptions = resolution === VideoResolution.H_NOVIDEO | 119 | const file = video.getMaxQualityFile().withVideoOrPlaylist(video) |
111 | ? { | ||
112 | type: 'only-audio' as 'only-audio', | ||
113 | 120 | ||
114 | inputPath: videoInputPath, | 121 | const result = await VideoPathManager.Instance.makeAvailableVideoFile(file, async videoInputPath => { |
115 | outputPath: videoTranscodedPath, | 122 | const newVideoFile = new VideoFileModel({ |
123 | resolution, | ||
124 | extname: newExtname, | ||
125 | filename: generateWebTorrentVideoFilename(resolution, newExtname), | ||
126 | size: 0, | ||
127 | videoId: video.id | ||
128 | }) | ||
116 | 129 | ||
117 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), | 130 | const videoTranscodedPath = join(transcodeDirectory, newVideoFile.filename) |
118 | profile: CONFIG.TRANSCODING.PROFILE, | ||
119 | 131 | ||
120 | resolution, | 132 | const transcodeOptions = resolution === VideoResolution.H_NOVIDEO |
133 | ? { | ||
134 | type: 'only-audio' as 'only-audio', | ||
121 | 135 | ||
122 | job | 136 | inputPath: videoInputPath, |
123 | } | 137 | outputPath: videoTranscodedPath, |
124 | : { | ||
125 | type: 'video' as 'video', | ||
126 | inputPath: videoInputPath, | ||
127 | outputPath: videoTranscodedPath, | ||
128 | 138 | ||
129 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), | 139 | inputFileMutexReleaser, |
130 | profile: CONFIG.TRANSCODING.PROFILE, | ||
131 | 140 | ||
132 | resolution, | 141 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), |
142 | profile: CONFIG.TRANSCODING.PROFILE, | ||
133 | 143 | ||
134 | job | 144 | resolution, |
135 | } | ||
136 | 145 | ||
137 | await transcodeVOD(transcodeOptions) | 146 | job |
147 | } | ||
148 | : { | ||
149 | type: 'video' as 'video', | ||
150 | inputPath: videoInputPath, | ||
151 | outputPath: videoTranscodedPath, | ||
138 | 152 | ||
139 | return onWebTorrentVideoFileTranscoding(video, newVideoFile, videoTranscodedPath, videoOutputPath) | 153 | inputFileMutexReleaser, |
140 | }) | 154 | |
155 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), | ||
156 | profile: CONFIG.TRANSCODING.PROFILE, | ||
157 | |||
158 | resolution, | ||
159 | |||
160 | job | ||
161 | } | ||
162 | |||
163 | await transcodeVOD(transcodeOptions) | ||
164 | |||
165 | return onWebTorrentVideoFileTranscoding(video, newVideoFile, videoTranscodedPath, newVideoFile) | ||
166 | }) | ||
167 | |||
168 | return result | ||
169 | } finally { | ||
170 | inputFileMutexReleaser() | ||
171 | } | ||
141 | } | 172 | } |
142 | 173 | ||
143 | // Merge an image with an audio file to create a video | 174 | // Merge an image with an audio file to create a video |
144 | function mergeAudioVideofile (options: { | 175 | async function mergeAudioVideofile (options: { |
145 | video: MVideoFullLight | 176 | video: MVideoFullLight |
146 | resolution: VideoResolution | 177 | resolution: VideoResolution |
147 | job: Job | 178 | job: Job |
@@ -151,54 +182,67 @@ function mergeAudioVideofile (options: { | |||
151 | const transcodeDirectory = CONFIG.STORAGE.TMP_DIR | 182 | const transcodeDirectory = CONFIG.STORAGE.TMP_DIR |
152 | const newExtname = '.mp4' | 183 | const newExtname = '.mp4' |
153 | 184 | ||
154 | const inputVideoFile = video.getMinQualityFile() | 185 | const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid) |
155 | 186 | ||
156 | return VideoPathManager.Instance.makeAvailableVideoFile(inputVideoFile.withVideoOrPlaylist(video), async audioInputPath => { | 187 | try { |
157 | const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname) | 188 | await video.reload() |
158 | 189 | ||
159 | // If the user updates the video preview during transcoding | 190 | const inputVideoFile = video.getMinQualityFile() |
160 | const previewPath = video.getPreview().getPath() | ||
161 | const tmpPreviewPath = join(CONFIG.STORAGE.TMP_DIR, basename(previewPath)) | ||
162 | await copyFile(previewPath, tmpPreviewPath) | ||
163 | 191 | ||
164 | const transcodeOptions = { | 192 | const fileWithVideoOrPlaylist = inputVideoFile.withVideoOrPlaylist(video) |
165 | type: 'merge-audio' as 'merge-audio', | ||
166 | 193 | ||
167 | inputPath: tmpPreviewPath, | 194 | const result = await VideoPathManager.Instance.makeAvailableVideoFile(fileWithVideoOrPlaylist, async audioInputPath => { |
168 | outputPath: videoTranscodedPath, | 195 | const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname) |
169 | 196 | ||
170 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), | 197 | // If the user updates the video preview during transcoding |
171 | profile: CONFIG.TRANSCODING.PROFILE, | 198 | const previewPath = video.getPreview().getPath() |
199 | const tmpPreviewPath = join(CONFIG.STORAGE.TMP_DIR, basename(previewPath)) | ||
200 | await copyFile(previewPath, tmpPreviewPath) | ||
172 | 201 | ||
173 | audioPath: audioInputPath, | 202 | const transcodeOptions = { |
174 | resolution, | 203 | type: 'merge-audio' as 'merge-audio', |
175 | 204 | ||
176 | job | 205 | inputPath: tmpPreviewPath, |
177 | } | 206 | outputPath: videoTranscodedPath, |
178 | 207 | ||
179 | try { | 208 | inputFileMutexReleaser, |
180 | await transcodeVOD(transcodeOptions) | ||
181 | 209 | ||
182 | await remove(audioInputPath) | 210 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), |
183 | await remove(tmpPreviewPath) | 211 | profile: CONFIG.TRANSCODING.PROFILE, |
184 | } catch (err) { | ||
185 | await remove(tmpPreviewPath) | ||
186 | throw err | ||
187 | } | ||
188 | 212 | ||
189 | // Important to do this before getVideoFilename() to take in account the new file extension | 213 | audioPath: audioInputPath, |
190 | inputVideoFile.extname = newExtname | 214 | resolution, |
191 | inputVideoFile.resolution = resolution | ||
192 | inputVideoFile.filename = generateWebTorrentVideoFilename(inputVideoFile.resolution, newExtname) | ||
193 | 215 | ||
194 | const videoOutputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, inputVideoFile) | 216 | job |
195 | // ffmpeg generated a new video file, so update the video duration | 217 | } |
196 | // See https://trac.ffmpeg.org/ticket/5456 | ||
197 | video.duration = await getVideoStreamDuration(videoTranscodedPath) | ||
198 | await video.save() | ||
199 | 218 | ||
200 | return onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath) | 219 | try { |
201 | }) | 220 | await transcodeVOD(transcodeOptions) |
221 | |||
222 | await remove(audioInputPath) | ||
223 | await remove(tmpPreviewPath) | ||
224 | } catch (err) { | ||
225 | await remove(tmpPreviewPath) | ||
226 | throw err | ||
227 | } | ||
228 | |||
229 | // Important to do this before getVideoFilename() to take in account the new file extension | ||
230 | inputVideoFile.extname = newExtname | ||
231 | inputVideoFile.resolution = resolution | ||
232 | inputVideoFile.filename = generateWebTorrentVideoFilename(inputVideoFile.resolution, newExtname) | ||
233 | |||
234 | // ffmpeg generated a new video file, so update the video duration | ||
235 | // See https://trac.ffmpeg.org/ticket/5456 | ||
236 | video.duration = await getVideoStreamDuration(videoTranscodedPath) | ||
237 | await video.save() | ||
238 | |||
239 | return onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, inputVideoFile) | ||
240 | }) | ||
241 | |||
242 | return result | ||
243 | } finally { | ||
244 | inputFileMutexReleaser() | ||
245 | } | ||
202 | } | 246 | } |
203 | 247 | ||
204 | // Concat TS segments from a live video to a fragmented mp4 HLS playlist | 248 | // Concat TS segments from a live video to a fragmented mp4 HLS playlist |
@@ -207,13 +251,13 @@ async function generateHlsPlaylistResolutionFromTS (options: { | |||
207 | concatenatedTsFilePath: string | 251 | concatenatedTsFilePath: string |
208 | resolution: VideoResolution | 252 | resolution: VideoResolution |
209 | isAAC: boolean | 253 | isAAC: boolean |
254 | inputFileMutexReleaser: MutexInterface.Releaser | ||
210 | }) { | 255 | }) { |
211 | return generateHlsPlaylistCommon({ | 256 | return generateHlsPlaylistCommon({ |
212 | video: options.video, | ||
213 | resolution: options.resolution, | ||
214 | inputPath: options.concatenatedTsFilePath, | ||
215 | type: 'hls-from-ts' as 'hls-from-ts', | 257 | type: 'hls-from-ts' as 'hls-from-ts', |
216 | isAAC: options.isAAC | 258 | inputPath: options.concatenatedTsFilePath, |
259 | |||
260 | ...pick(options, [ 'video', 'resolution', 'inputFileMutexReleaser', 'isAAC' ]) | ||
217 | }) | 261 | }) |
218 | } | 262 | } |
219 | 263 | ||
@@ -223,15 +267,14 @@ function generateHlsPlaylistResolution (options: { | |||
223 | videoInputPath: string | 267 | videoInputPath: string |
224 | resolution: VideoResolution | 268 | resolution: VideoResolution |
225 | copyCodecs: boolean | 269 | copyCodecs: boolean |
270 | inputFileMutexReleaser: MutexInterface.Releaser | ||
226 | job?: Job | 271 | job?: Job |
227 | }) { | 272 | }) { |
228 | return generateHlsPlaylistCommon({ | 273 | return generateHlsPlaylistCommon({ |
229 | video: options.video, | ||
230 | resolution: options.resolution, | ||
231 | copyCodecs: options.copyCodecs, | ||
232 | inputPath: options.videoInputPath, | ||
233 | type: 'hls' as 'hls', | 274 | type: 'hls' as 'hls', |
234 | job: options.job | 275 | inputPath: options.videoInputPath, |
276 | |||
277 | ...pick(options, [ 'video', 'resolution', 'copyCodecs', 'inputFileMutexReleaser', 'job' ]) | ||
235 | }) | 278 | }) |
236 | } | 279 | } |
237 | 280 | ||
@@ -251,27 +294,39 @@ async function onWebTorrentVideoFileTranscoding ( | |||
251 | video: MVideoFullLight, | 294 | video: MVideoFullLight, |
252 | videoFile: MVideoFile, | 295 | videoFile: MVideoFile, |
253 | transcodingPath: string, | 296 | transcodingPath: string, |
254 | outputPath: string | 297 | newVideoFile: MVideoFile |
255 | ) { | 298 | ) { |
256 | const stats = await stat(transcodingPath) | 299 | const mutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid) |
257 | const fps = await getVideoStreamFPS(transcodingPath) | ||
258 | const metadata = await buildFileMetadata(transcodingPath) | ||
259 | 300 | ||
260 | await move(transcodingPath, outputPath, { overwrite: true }) | 301 | try { |
302 | await video.reload() | ||
261 | 303 | ||
262 | videoFile.size = stats.size | 304 | const outputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, newVideoFile) |
263 | videoFile.fps = fps | ||
264 | videoFile.metadata = metadata | ||
265 | 305 | ||
266 | await createTorrentAndSetInfoHash(video, videoFile) | 306 | const stats = await stat(transcodingPath) |
267 | 307 | ||
268 | const oldFile = await VideoFileModel.loadWebTorrentFile({ videoId: video.id, fps: videoFile.fps, resolution: videoFile.resolution }) | 308 | const probe = await ffprobePromise(transcodingPath) |
269 | if (oldFile) await video.removeWebTorrentFile(oldFile) | 309 | const fps = await getVideoStreamFPS(transcodingPath, probe) |
310 | const metadata = await buildFileMetadata(transcodingPath, probe) | ||
270 | 311 | ||
271 | await VideoFileModel.customUpsert(videoFile, 'video', undefined) | 312 | await move(transcodingPath, outputPath, { overwrite: true }) |
272 | video.VideoFiles = await video.$get('VideoFiles') | ||
273 | 313 | ||
274 | return { video, videoFile } | 314 | videoFile.size = stats.size |
315 | videoFile.fps = fps | ||
316 | videoFile.metadata = metadata | ||
317 | |||
318 | await createTorrentAndSetInfoHash(video, videoFile) | ||
319 | |||
320 | const oldFile = await VideoFileModel.loadWebTorrentFile({ videoId: video.id, fps: videoFile.fps, resolution: videoFile.resolution }) | ||
321 | if (oldFile) await video.removeWebTorrentFile(oldFile) | ||
322 | |||
323 | await VideoFileModel.customUpsert(videoFile, 'video', undefined) | ||
324 | video.VideoFiles = await video.$get('VideoFiles') | ||
325 | |||
326 | return { video, videoFile } | ||
327 | } finally { | ||
328 | mutexReleaser() | ||
329 | } | ||
275 | } | 330 | } |
276 | 331 | ||
277 | async function generateHlsPlaylistCommon (options: { | 332 | async function generateHlsPlaylistCommon (options: { |
@@ -279,12 +334,15 @@ async function generateHlsPlaylistCommon (options: { | |||
279 | video: MVideo | 334 | video: MVideo |
280 | inputPath: string | 335 | inputPath: string |
281 | resolution: VideoResolution | 336 | resolution: VideoResolution |
337 | |||
338 | inputFileMutexReleaser: MutexInterface.Releaser | ||
339 | |||
282 | copyCodecs?: boolean | 340 | copyCodecs?: boolean |
283 | isAAC?: boolean | 341 | isAAC?: boolean |
284 | 342 | ||
285 | job?: Job | 343 | job?: Job |
286 | }) { | 344 | }) { |
287 | const { type, video, inputPath, resolution, copyCodecs, isAAC, job } = options | 345 | const { type, video, inputPath, resolution, copyCodecs, isAAC, job, inputFileMutexReleaser } = options |
288 | const transcodeDirectory = CONFIG.STORAGE.TMP_DIR | 346 | const transcodeDirectory = CONFIG.STORAGE.TMP_DIR |
289 | 347 | ||
290 | const videoTranscodedBasePath = join(transcodeDirectory, type) | 348 | const videoTranscodedBasePath = join(transcodeDirectory, type) |
@@ -308,6 +366,8 @@ async function generateHlsPlaylistCommon (options: { | |||
308 | 366 | ||
309 | isAAC, | 367 | isAAC, |
310 | 368 | ||
369 | inputFileMutexReleaser, | ||
370 | |||
311 | hlsPlaylist: { | 371 | hlsPlaylist: { |
312 | videoFilename | 372 | videoFilename |
313 | }, | 373 | }, |
@@ -333,47 +393,73 @@ async function generateHlsPlaylistCommon (options: { | |||
333 | videoStreamingPlaylistId: playlist.id | 393 | videoStreamingPlaylistId: playlist.id |
334 | }) | 394 | }) |
335 | 395 | ||
336 | const videoFilePath = VideoPathManager.Instance.getFSVideoFileOutputPath(playlist, newVideoFile) | 396 | const mutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid) |
337 | await ensureDir(VideoPathManager.Instance.getFSHLSOutputPath(video)) | ||
338 | 397 | ||
339 | // Move playlist file | 398 | try { |
340 | const resolutionPlaylistPath = VideoPathManager.Instance.getFSHLSOutputPath(video, resolutionPlaylistFilename) | 399 | // VOD transcoding is a long task, refresh video attributes |
341 | await move(resolutionPlaylistFileTranscodePath, resolutionPlaylistPath, { overwrite: true }) | 400 | await video.reload() |
342 | // Move video file | ||
343 | await move(join(videoTranscodedBasePath, videoFilename), videoFilePath, { overwrite: true }) | ||
344 | 401 | ||
345 | // Update video duration if it was not set (in case of a live for example) | 402 | const videoFilePath = VideoPathManager.Instance.getFSVideoFileOutputPath(playlist, newVideoFile) |
346 | if (!video.duration) { | 403 | await ensureDir(VideoPathManager.Instance.getFSHLSOutputPath(video)) |
347 | video.duration = await getVideoStreamDuration(videoFilePath) | ||
348 | await video.save() | ||
349 | } | ||
350 | 404 | ||
351 | const stats = await stat(videoFilePath) | 405 | // Move playlist file |
406 | const resolutionPlaylistPath = VideoPathManager.Instance.getFSHLSOutputPath(video, resolutionPlaylistFilename) | ||
407 | await move(resolutionPlaylistFileTranscodePath, resolutionPlaylistPath, { overwrite: true }) | ||
408 | // Move video file | ||
409 | await move(join(videoTranscodedBasePath, videoFilename), videoFilePath, { overwrite: true }) | ||
352 | 410 | ||
353 | newVideoFile.size = stats.size | 411 | // Update video duration if it was not set (in case of a live for example) |
354 | newVideoFile.fps = await getVideoStreamFPS(videoFilePath) | 412 | if (!video.duration) { |
355 | newVideoFile.metadata = await buildFileMetadata(videoFilePath) | 413 | video.duration = await getVideoStreamDuration(videoFilePath) |
414 | await video.save() | ||
415 | } | ||
356 | 416 | ||
357 | await createTorrentAndSetInfoHash(playlist, newVideoFile) | 417 | const stats = await stat(videoFilePath) |
358 | 418 | ||
359 | const oldFile = await VideoFileModel.loadHLSFile({ playlistId: playlist.id, fps: newVideoFile.fps, resolution: newVideoFile.resolution }) | 419 | newVideoFile.size = stats.size |
360 | if (oldFile) { | 420 | newVideoFile.fps = await getVideoStreamFPS(videoFilePath) |
361 | await video.removeStreamingPlaylistVideoFile(playlist, oldFile) | 421 | newVideoFile.metadata = await buildFileMetadata(videoFilePath) |
362 | await oldFile.destroy() | ||
363 | } | ||
364 | 422 | ||
365 | const savedVideoFile = await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined) | 423 | await createTorrentAndSetInfoHash(playlist, newVideoFile) |
366 | 424 | ||
367 | await updatePlaylistAfterFileChange(video, playlist) | 425 | const oldFile = await VideoFileModel.loadHLSFile({ |
426 | playlistId: playlist.id, | ||
427 | fps: newVideoFile.fps, | ||
428 | resolution: newVideoFile.resolution | ||
429 | }) | ||
430 | |||
431 | if (oldFile) { | ||
432 | await video.removeStreamingPlaylistVideoFile(playlist, oldFile) | ||
433 | await oldFile.destroy() | ||
434 | } | ||
435 | |||
436 | const savedVideoFile = await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined) | ||
437 | |||
438 | await updatePlaylistAfterFileChange(video, playlist) | ||
368 | 439 | ||
369 | return { resolutionPlaylistPath, videoFile: savedVideoFile } | 440 | return { resolutionPlaylistPath, videoFile: savedVideoFile } |
441 | } finally { | ||
442 | mutexReleaser() | ||
443 | } | ||
370 | } | 444 | } |
371 | 445 | ||
372 | function buildOriginalFileResolution (inputResolution: number) { | 446 | function buildOriginalFileResolution (inputResolution: number) { |
373 | if (CONFIG.TRANSCODING.ALWAYS_TRANSCODE_ORIGINAL_RESOLUTION === true) return toEven(inputResolution) | 447 | if (CONFIG.TRANSCODING.ALWAYS_TRANSCODE_ORIGINAL_RESOLUTION === true) { |
448 | return toEven(inputResolution) | ||
449 | } | ||
374 | 450 | ||
375 | const resolutions = computeResolutionsToTranscode({ input: inputResolution, type: 'vod', includeInput: false, strictLower: false }) | 451 | const resolutions = computeResolutionsToTranscode({ |
376 | if (resolutions.length === 0) return toEven(inputResolution) | 452 | input: inputResolution, |
453 | type: 'vod', | ||
454 | includeInput: false, | ||
455 | strictLower: false, | ||
456 | // We don't really care about the audio resolution in this context | ||
457 | hasAudio: true | ||
458 | }) | ||
459 | |||
460 | if (resolutions.length === 0) { | ||
461 | return toEven(inputResolution) | ||
462 | } | ||
377 | 463 | ||
378 | return Math.max(...resolutions) | 464 | return Math.max(...resolutions) |
379 | } | 465 | } |