aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib/transcoding
diff options
context:
space:
mode:
Diffstat (limited to 'server/lib/transcoding')
-rw-r--r--server/lib/transcoding/transcoding.ts367
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 @@
1import { MutexInterface } from 'async-mutex'
1import { Job } from 'bullmq' 2import { Job } from 'bullmq'
2import { copyFile, ensureDir, move, remove, stat } from 'fs-extra' 3import { copyFile, ensureDir, move, remove, stat } from 'fs-extra'
3import { basename, extname as extnameUtil, join } from 'path' 4import { basename, extname as extnameUtil, join } from 'path'
@@ -6,11 +7,13 @@ import { retryTransactionWrapper } from '@server/helpers/database-utils'
6import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' 7import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
7import { sequelizeTypescript } from '@server/initializers/database' 8import { sequelizeTypescript } from '@server/initializers/database'
8import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models' 9import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models'
10import { pick } from '@shared/core-utils'
9import { VideoResolution, VideoStorage } from '../../../shared/models/videos' 11import { VideoResolution, VideoStorage } from '../../../shared/models/videos'
10import { 12import {
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.
36function optimizeOriginalVideofile (options: { 39async 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
88function transcodeNewWebTorrentResolution (options: { 103async 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
144function mergeAudioVideofile (options: { 174async 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
277async function generateHlsPlaylistCommon (options: { 331async 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
372function buildOriginalFileResolution (inputResolution: number) { 445function buildOriginalFileResolution (inputResolution: number) {