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