aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/controllers
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2023-04-21 14:55:10 +0200
committerChocobozzz <chocobozzz@cpy.re>2023-05-09 08:57:34 +0200
commit0c9668f77901e7540e2c7045eb0f2974a4842a69 (patch)
tree226d3dd1565b0bb56588897af3b8530e6216e96b /server/controllers
parent6bcb854cdea8688a32240bc5719c7d139806e00b (diff)
downloadPeerTube-0c9668f77901e7540e2c7045eb0f2974a4842a69.tar.gz
PeerTube-0c9668f77901e7540e2c7045eb0f2974a4842a69.tar.zst
PeerTube-0c9668f77901e7540e2c7045eb0f2974a4842a69.zip
Implement remote runner jobs in server
Move ffmpeg functions to @shared
Diffstat (limited to 'server/controllers')
-rw-r--r--server/controllers/api/config.ts6
-rw-r--r--server/controllers/api/index.ts2
-rw-r--r--server/controllers/api/jobs.ts3
-rw-r--r--server/controllers/api/runners/index.ts18
-rw-r--r--server/controllers/api/runners/jobs-files.ts84
-rw-r--r--server/controllers/api/runners/jobs.ts352
-rw-r--r--server/controllers/api/runners/manage-runners.ts107
-rw-r--r--server/controllers/api/runners/registration-tokens.ts87
-rw-r--r--server/controllers/api/videos/transcoding.ts87
-rw-r--r--server/controllers/api/videos/upload.ts71
-rw-r--r--server/controllers/bots.ts6
-rw-r--r--server/controllers/object-storage-proxy.ts87
12 files changed, 710 insertions, 200 deletions
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts
index 60d168d12..0b9aaffda 100644
--- a/server/controllers/api/config.ts
+++ b/server/controllers/api/config.ts
@@ -217,6 +217,9 @@ function customConfig (): CustomConfig {
217 }, 217 },
218 transcoding: { 218 transcoding: {
219 enabled: CONFIG.TRANSCODING.ENABLED, 219 enabled: CONFIG.TRANSCODING.ENABLED,
220 remoteRunners: {
221 enabled: CONFIG.TRANSCODING.REMOTE_RUNNERS.ENABLED
222 },
220 allowAdditionalExtensions: CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS, 223 allowAdditionalExtensions: CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS,
221 allowAudioFiles: CONFIG.TRANSCODING.ALLOW_AUDIO_FILES, 224 allowAudioFiles: CONFIG.TRANSCODING.ALLOW_AUDIO_FILES,
222 threads: CONFIG.TRANSCODING.THREADS, 225 threads: CONFIG.TRANSCODING.THREADS,
@@ -252,6 +255,9 @@ function customConfig (): CustomConfig {
252 maxUserLives: CONFIG.LIVE.MAX_USER_LIVES, 255 maxUserLives: CONFIG.LIVE.MAX_USER_LIVES,
253 transcoding: { 256 transcoding: {
254 enabled: CONFIG.LIVE.TRANSCODING.ENABLED, 257 enabled: CONFIG.LIVE.TRANSCODING.ENABLED,
258 remoteRunners: {
259 enabled: CONFIG.LIVE.TRANSCODING.REMOTE_RUNNERS.ENABLED
260 },
255 threads: CONFIG.LIVE.TRANSCODING.THREADS, 261 threads: CONFIG.LIVE.TRANSCODING.THREADS,
256 profile: CONFIG.LIVE.TRANSCODING.PROFILE, 262 profile: CONFIG.LIVE.TRANSCODING.PROFILE,
257 resolutions: { 263 resolutions: {
diff --git a/server/controllers/api/index.ts b/server/controllers/api/index.ts
index e1d197c8a..646f9597e 100644
--- a/server/controllers/api/index.ts
+++ b/server/controllers/api/index.ts
@@ -15,6 +15,7 @@ import { metricsRouter } from './metrics'
15import { oauthClientsRouter } from './oauth-clients' 15import { oauthClientsRouter } from './oauth-clients'
16import { overviewsRouter } from './overviews' 16import { overviewsRouter } from './overviews'
17import { pluginRouter } from './plugins' 17import { pluginRouter } from './plugins'
18import { runnersRouter } from './runners'
18import { searchRouter } from './search' 19import { searchRouter } from './search'
19import { serverRouter } from './server' 20import { serverRouter } from './server'
20import { usersRouter } from './users' 21import { usersRouter } from './users'
@@ -55,6 +56,7 @@ apiRouter.use('/overviews', overviewsRouter)
55apiRouter.use('/plugins', pluginRouter) 56apiRouter.use('/plugins', pluginRouter)
56apiRouter.use('/custom-pages', customPageRouter) 57apiRouter.use('/custom-pages', customPageRouter)
57apiRouter.use('/blocklist', blocklistRouter) 58apiRouter.use('/blocklist', blocklistRouter)
59apiRouter.use('/runners', runnersRouter)
58apiRouter.use('/ping', pong) 60apiRouter.use('/ping', pong)
59apiRouter.use('/*', badRequest) 61apiRouter.use('/*', badRequest)
60 62
diff --git a/server/controllers/api/jobs.ts b/server/controllers/api/jobs.ts
index 6a53e3083..b63e2f962 100644
--- a/server/controllers/api/jobs.ts
+++ b/server/controllers/api/jobs.ts
@@ -93,6 +93,9 @@ async function formatJob (job: BullJob, state?: JobState): Promise<Job> {
93 state: state || await job.getState(), 93 state: state || await job.getState(),
94 type: job.queueName as JobType, 94 type: job.queueName as JobType,
95 data: job.data, 95 data: job.data,
96 parent: job.parent
97 ? { id: job.parent.id }
98 : undefined,
96 progress: job.progress as number, 99 progress: job.progress as number,
97 priority: job.opts.priority, 100 priority: job.opts.priority,
98 error, 101 error,
diff --git a/server/controllers/api/runners/index.ts b/server/controllers/api/runners/index.ts
new file mode 100644
index 000000000..c98ded354
--- /dev/null
+++ b/server/controllers/api/runners/index.ts
@@ -0,0 +1,18 @@
1import express from 'express'
2import { runnerJobsRouter } from './jobs'
3import { runnerJobFilesRouter } from './jobs-files'
4import { manageRunnersRouter } from './manage-runners'
5import { runnerRegistrationTokensRouter } from './registration-tokens'
6
7const runnersRouter = express.Router()
8
9runnersRouter.use('/', manageRunnersRouter)
10runnersRouter.use('/', runnerJobsRouter)
11runnersRouter.use('/', runnerJobFilesRouter)
12runnersRouter.use('/', runnerRegistrationTokensRouter)
13
14// ---------------------------------------------------------------------------
15
16export {
17 runnersRouter
18}
diff --git a/server/controllers/api/runners/jobs-files.ts b/server/controllers/api/runners/jobs-files.ts
new file mode 100644
index 000000000..e43ce35f5
--- /dev/null
+++ b/server/controllers/api/runners/jobs-files.ts
@@ -0,0 +1,84 @@
1import express from 'express'
2import { logger, loggerTagsFactory } from '@server/helpers/logger'
3import { proxifyHLS, proxifyWebTorrentFile } from '@server/lib/object-storage'
4import { VideoPathManager } from '@server/lib/video-path-manager'
5import { asyncMiddleware } from '@server/middlewares'
6import { jobOfRunnerGetValidator } from '@server/middlewares/validators/runners'
7import { runnerJobGetVideoTranscodingFileValidator } from '@server/middlewares/validators/runners/job-files'
8import { VideoStorage } from '@shared/models'
9
10const lTags = loggerTagsFactory('api', 'runner')
11
12const runnerJobFilesRouter = express.Router()
13
14runnerJobFilesRouter.post('/jobs/:jobUUID/files/videos/:videoId/max-quality',
15 asyncMiddleware(jobOfRunnerGetValidator),
16 asyncMiddleware(runnerJobGetVideoTranscodingFileValidator),
17 asyncMiddleware(getMaxQualityVideoFile)
18)
19
20runnerJobFilesRouter.post('/jobs/:jobUUID/files/videos/:videoId/previews/max-quality',
21 asyncMiddleware(jobOfRunnerGetValidator),
22 asyncMiddleware(runnerJobGetVideoTranscodingFileValidator),
23 getMaxQualityVideoPreview
24)
25
26// ---------------------------------------------------------------------------
27
28export {
29 runnerJobFilesRouter
30}
31
32// ---------------------------------------------------------------------------
33
34async function getMaxQualityVideoFile (req: express.Request, res: express.Response) {
35 const runnerJob = res.locals.runnerJob
36 const runner = runnerJob.Runner
37 const video = res.locals.videoAll
38
39 logger.info(
40 'Get max quality file of video %s of job %s for runner %s', video.uuid, runnerJob.uuid, runner.name,
41 lTags(runner.name, runnerJob.id, runnerJob.type)
42 )
43
44 const file = video.getMaxQualityFile()
45
46 if (file.storage === VideoStorage.OBJECT_STORAGE) {
47 if (file.isHLS()) {
48 return proxifyHLS({
49 req,
50 res,
51 filename: file.filename,
52 playlist: video.getHLSPlaylist(),
53 reinjectVideoFileToken: false,
54 video
55 })
56 }
57
58 // Web video
59 return proxifyWebTorrentFile({
60 req,
61 res,
62 filename: file.filename
63 })
64 }
65
66 return VideoPathManager.Instance.makeAvailableVideoFile(file, videoPath => {
67 return res.sendFile(videoPath)
68 })
69}
70
71function getMaxQualityVideoPreview (req: express.Request, res: express.Response) {
72 const runnerJob = res.locals.runnerJob
73 const runner = runnerJob.Runner
74 const video = res.locals.videoAll
75
76 logger.info(
77 'Get max quality preview file of video %s of job %s for runner %s', video.uuid, runnerJob.uuid, runner.name,
78 lTags(runner.name, runnerJob.id, runnerJob.type)
79 )
80
81 const file = video.getPreview()
82
83 return res.sendFile(file.getPath())
84}
diff --git a/server/controllers/api/runners/jobs.ts b/server/controllers/api/runners/jobs.ts
new file mode 100644
index 000000000..7d488ec11
--- /dev/null
+++ b/server/controllers/api/runners/jobs.ts
@@ -0,0 +1,352 @@
1import express, { UploadFiles } from 'express'
2import { createReqFiles } from '@server/helpers/express-utils'
3import { logger, loggerTagsFactory } from '@server/helpers/logger'
4import { generateRunnerJobToken } from '@server/helpers/token-generator'
5import { MIMETYPES } from '@server/initializers/constants'
6import { sequelizeTypescript } from '@server/initializers/database'
7import { getRunnerJobHandlerClass, updateLastRunnerContact } from '@server/lib/runners'
8import {
9 asyncMiddleware,
10 authenticate,
11 ensureUserHasRight,
12 paginationValidator,
13 runnerJobsSortValidator,
14 setDefaultPagination,
15 setDefaultSort
16} from '@server/middlewares'
17import {
18 abortRunnerJobValidator,
19 acceptRunnerJobValidator,
20 errorRunnerJobValidator,
21 getRunnerFromTokenValidator,
22 jobOfRunnerGetValidator,
23 runnerJobGetValidator,
24 successRunnerJobValidator,
25 updateRunnerJobValidator
26} from '@server/middlewares/validators/runners'
27import { RunnerModel } from '@server/models/runner/runner'
28import { RunnerJobModel } from '@server/models/runner/runner-job'
29import {
30 AbortRunnerJobBody,
31 AcceptRunnerJobResult,
32 ErrorRunnerJobBody,
33 HttpStatusCode,
34 ListRunnerJobsQuery,
35 LiveRTMPHLSTranscodingUpdatePayload,
36 RequestRunnerJobResult,
37 RunnerJobState,
38 RunnerJobSuccessBody,
39 RunnerJobSuccessPayload,
40 RunnerJobType,
41 RunnerJobUpdateBody,
42 RunnerJobUpdatePayload,
43 UserRight,
44 VODAudioMergeTranscodingSuccess,
45 VODHLSTranscodingSuccess,
46 VODWebVideoTranscodingSuccess
47} from '@shared/models'
48
49const postRunnerJobSuccessVideoFiles = createReqFiles(
50 [ 'payload[videoFile]', 'payload[resolutionPlaylistFile]' ],
51 { ...MIMETYPES.VIDEO.MIMETYPE_EXT, ...MIMETYPES.M3U8.MIMETYPE_EXT }
52)
53
54const runnerJobUpdateVideoFiles = createReqFiles(
55 [ 'payload[videoChunkFile]', 'payload[resolutionPlaylistFile]', 'payload[masterPlaylistFile]' ],
56 { ...MIMETYPES.VIDEO.MIMETYPE_EXT, ...MIMETYPES.M3U8.MIMETYPE_EXT }
57)
58
59const lTags = loggerTagsFactory('api', 'runner')
60
61const runnerJobsRouter = express.Router()
62
63// ---------------------------------------------------------------------------
64// Controllers for runners
65// ---------------------------------------------------------------------------
66
67runnerJobsRouter.post('/jobs/request',
68 asyncMiddleware(getRunnerFromTokenValidator),
69 asyncMiddleware(requestRunnerJob)
70)
71
72runnerJobsRouter.post('/jobs/:jobUUID/accept',
73 asyncMiddleware(runnerJobGetValidator),
74 acceptRunnerJobValidator,
75 asyncMiddleware(getRunnerFromTokenValidator),
76 asyncMiddleware(acceptRunnerJob)
77)
78
79runnerJobsRouter.post('/jobs/:jobUUID/abort',
80 asyncMiddleware(jobOfRunnerGetValidator),
81 abortRunnerJobValidator,
82 asyncMiddleware(abortRunnerJob)
83)
84
85runnerJobsRouter.post('/jobs/:jobUUID/update',
86 runnerJobUpdateVideoFiles,
87 asyncMiddleware(jobOfRunnerGetValidator),
88 updateRunnerJobValidator,
89 asyncMiddleware(updateRunnerJobController)
90)
91
92runnerJobsRouter.post('/jobs/:jobUUID/error',
93 asyncMiddleware(jobOfRunnerGetValidator),
94 errorRunnerJobValidator,
95 asyncMiddleware(errorRunnerJob)
96)
97
98runnerJobsRouter.post('/jobs/:jobUUID/success',
99 postRunnerJobSuccessVideoFiles,
100 asyncMiddleware(jobOfRunnerGetValidator),
101 successRunnerJobValidator,
102 asyncMiddleware(postRunnerJobSuccess)
103)
104
105// ---------------------------------------------------------------------------
106// Controllers for admins
107// ---------------------------------------------------------------------------
108
109runnerJobsRouter.post('/jobs/:jobUUID/cancel',
110 authenticate,
111 ensureUserHasRight(UserRight.MANAGE_RUNNERS),
112 asyncMiddleware(runnerJobGetValidator),
113 asyncMiddleware(cancelRunnerJob)
114)
115
116runnerJobsRouter.get('/jobs',
117 authenticate,
118 ensureUserHasRight(UserRight.MANAGE_RUNNERS),
119 paginationValidator,
120 runnerJobsSortValidator,
121 setDefaultSort,
122 setDefaultPagination,
123 asyncMiddleware(listRunnerJobs)
124)
125
126// ---------------------------------------------------------------------------
127
128export {
129 runnerJobsRouter
130}
131
132// ---------------------------------------------------------------------------
133
134// ---------------------------------------------------------------------------
135// Controllers for runners
136// ---------------------------------------------------------------------------
137
138async function requestRunnerJob (req: express.Request, res: express.Response) {
139 const runner = res.locals.runner
140 const availableJobs = await RunnerJobModel.listAvailableJobs()
141
142 logger.debug('Runner %s requests for a job.', runner.name, { availableJobs, ...lTags(runner.name) })
143
144 const result: RequestRunnerJobResult = {
145 availableJobs: availableJobs.map(j => ({
146 uuid: j.uuid,
147 type: j.type,
148 payload: j.payload
149 }))
150 }
151
152 updateLastRunnerContact(req, runner)
153
154 return res.json(result)
155}
156
157async function acceptRunnerJob (req: express.Request, res: express.Response) {
158 const runner = res.locals.runner
159 const runnerJob = res.locals.runnerJob
160
161 runnerJob.state = RunnerJobState.PROCESSING
162 runnerJob.processingJobToken = generateRunnerJobToken()
163 runnerJob.startedAt = new Date()
164 runnerJob.runnerId = runner.id
165
166 const newRunnerJob = await sequelizeTypescript.transaction(transaction => {
167 return runnerJob.save({ transaction })
168 })
169 newRunnerJob.Runner = runner as RunnerModel
170
171 const result: AcceptRunnerJobResult = {
172 job: {
173 ...newRunnerJob.toFormattedJSON(),
174
175 jobToken: newRunnerJob.processingJobToken
176 }
177 }
178
179 updateLastRunnerContact(req, runner)
180
181 logger.info(
182 'Remote runner %s has accepted job %s (%s)', runner.name, runnerJob.uuid, runnerJob.type,
183 lTags(runner.name, runnerJob.uuid, runnerJob.type)
184 )
185
186 return res.json(result)
187}
188
189async function abortRunnerJob (req: express.Request, res: express.Response) {
190 const runnerJob = res.locals.runnerJob
191 const runner = runnerJob.Runner
192 const body: AbortRunnerJobBody = req.body
193
194 logger.info(
195 'Remote runner %s is aborting job %s (%s)', runner.name, runnerJob.uuid, runnerJob.type,
196 { reason: body.reason, ...lTags(runner.name, runnerJob.uuid, runnerJob.type) }
197 )
198
199 const RunnerJobHandler = getRunnerJobHandlerClass(runnerJob)
200 await new RunnerJobHandler().abort({ runnerJob })
201
202 updateLastRunnerContact(req, runnerJob.Runner)
203
204 return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
205}
206
207async function errorRunnerJob (req: express.Request, res: express.Response) {
208 const runnerJob = res.locals.runnerJob
209 const runner = runnerJob.Runner
210 const body: ErrorRunnerJobBody = req.body
211
212 runnerJob.failures += 1
213
214 logger.error(
215 'Remote runner %s had an error with job %s (%s)', runner.name, runnerJob.uuid, runnerJob.type,
216 { errorMessage: body.message, totalFailures: runnerJob.failures, ...lTags(runner.name, runnerJob.uuid, runnerJob.type) }
217 )
218
219 const RunnerJobHandler = getRunnerJobHandlerClass(runnerJob)
220 await new RunnerJobHandler().error({ runnerJob, message: body.message })
221
222 updateLastRunnerContact(req, runnerJob.Runner)
223
224 return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
225}
226
227// ---------------------------------------------------------------------------
228
229const jobUpdateBuilders: {
230 [id in RunnerJobType]?: (payload: RunnerJobUpdatePayload, files?: UploadFiles) => RunnerJobUpdatePayload
231} = {
232 'live-rtmp-hls-transcoding': (payload: LiveRTMPHLSTranscodingUpdatePayload, files) => {
233 return {
234 ...payload,
235
236 masterPlaylistFile: files['payload[masterPlaylistFile]']?.[0].path,
237 resolutionPlaylistFile: files['payload[resolutionPlaylistFile]']?.[0].path,
238 videoChunkFile: files['payload[videoChunkFile]']?.[0].path
239 }
240 }
241}
242
243async function updateRunnerJobController (req: express.Request, res: express.Response) {
244 const runnerJob = res.locals.runnerJob
245 const runner = runnerJob.Runner
246 const body: RunnerJobUpdateBody = req.body
247
248 const payloadBuilder = jobUpdateBuilders[runnerJob.type]
249 const updatePayload = payloadBuilder
250 ? payloadBuilder(body.payload, req.files as UploadFiles)
251 : undefined
252
253 logger.debug(
254 'Remote runner %s is updating job %s (%s)', runnerJob.Runner.name, runnerJob.uuid, runnerJob.type,
255 { body, updatePayload, ...lTags(runner.name, runnerJob.uuid, runnerJob.type) }
256 )
257
258 const RunnerJobHandler = getRunnerJobHandlerClass(runnerJob)
259 await new RunnerJobHandler().update({
260 runnerJob,
261 progress: req.body.progress,
262 updatePayload
263 })
264
265 updateLastRunnerContact(req, runnerJob.Runner)
266
267 return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
268}
269
270// ---------------------------------------------------------------------------
271
272const jobSuccessPayloadBuilders: {
273 [id in RunnerJobType]: (payload: RunnerJobSuccessPayload, files?: UploadFiles) => RunnerJobSuccessPayload
274} = {
275 'vod-web-video-transcoding': (payload: VODWebVideoTranscodingSuccess, files) => {
276 return {
277 ...payload,
278
279 videoFile: files['payload[videoFile]'][0].path
280 }
281 },
282
283 'vod-hls-transcoding': (payload: VODHLSTranscodingSuccess, files) => {
284 return {
285 ...payload,
286
287 videoFile: files['payload[videoFile]'][0].path,
288 resolutionPlaylistFile: files['payload[resolutionPlaylistFile]'][0].path
289 }
290 },
291
292 'vod-audio-merge-transcoding': (payload: VODAudioMergeTranscodingSuccess, files) => {
293 return {
294 ...payload,
295
296 videoFile: files['payload[videoFile]'][0].path
297 }
298 },
299
300 'live-rtmp-hls-transcoding': () => ({})
301}
302
303async function postRunnerJobSuccess (req: express.Request, res: express.Response) {
304 const runnerJob = res.locals.runnerJob
305 const runner = runnerJob.Runner
306 const body: RunnerJobSuccessBody = req.body
307
308 const resultPayload = jobSuccessPayloadBuilders[runnerJob.type](body.payload, req.files as UploadFiles)
309
310 logger.info(
311 'Remote runner %s is sending success result for job %s (%s)', runnerJob.Runner.name, runnerJob.uuid, runnerJob.type,
312 { resultPayload, ...lTags(runner.name, runnerJob.uuid, runnerJob.type) }
313 )
314
315 const RunnerJobHandler = getRunnerJobHandlerClass(runnerJob)
316 await new RunnerJobHandler().complete({ runnerJob, resultPayload })
317
318 updateLastRunnerContact(req, runnerJob.Runner)
319
320 return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
321}
322
323// ---------------------------------------------------------------------------
324// Controllers for admins
325// ---------------------------------------------------------------------------
326
327async function cancelRunnerJob (req: express.Request, res: express.Response) {
328 const runnerJob = res.locals.runnerJob
329
330 logger.info('Cancelling job %s (%s)', runnerJob.type, lTags(runnerJob.uuid, runnerJob.type))
331
332 const RunnerJobHandler = getRunnerJobHandlerClass(runnerJob)
333 await new RunnerJobHandler().cancel({ runnerJob })
334
335 return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
336}
337
338async function listRunnerJobs (req: express.Request, res: express.Response) {
339 const query: ListRunnerJobsQuery = req.query
340
341 const resultList = await RunnerJobModel.listForApi({
342 start: query.start,
343 count: query.count,
344 sort: query.sort,
345 search: query.search
346 })
347
348 return res.json({
349 total: resultList.total,
350 data: resultList.data.map(d => d.toFormattedAdminJSON())
351 })
352}
diff --git a/server/controllers/api/runners/manage-runners.ts b/server/controllers/api/runners/manage-runners.ts
new file mode 100644
index 000000000..eb08c4b1d
--- /dev/null
+++ b/server/controllers/api/runners/manage-runners.ts
@@ -0,0 +1,107 @@
1import express from 'express'
2import { logger, loggerTagsFactory } from '@server/helpers/logger'
3import { generateRunnerToken } from '@server/helpers/token-generator'
4import {
5 asyncMiddleware,
6 authenticate,
7 ensureUserHasRight,
8 paginationValidator,
9 runnersSortValidator,
10 setDefaultPagination,
11 setDefaultSort
12} from '@server/middlewares'
13import { deleteRunnerValidator, getRunnerFromTokenValidator, registerRunnerValidator } from '@server/middlewares/validators/runners'
14import { RunnerModel } from '@server/models/runner/runner'
15import { HttpStatusCode, ListRunnersQuery, RegisterRunnerBody, UserRight } from '@shared/models'
16
17const lTags = loggerTagsFactory('api', 'runner')
18
19const manageRunnersRouter = express.Router()
20
21manageRunnersRouter.post('/register',
22 asyncMiddleware(registerRunnerValidator),
23 asyncMiddleware(registerRunner)
24)
25manageRunnersRouter.post('/unregister',
26 asyncMiddleware(getRunnerFromTokenValidator),
27 asyncMiddleware(unregisterRunner)
28)
29
30manageRunnersRouter.delete('/:runnerId',
31 authenticate,
32 ensureUserHasRight(UserRight.MANAGE_RUNNERS),
33 asyncMiddleware(deleteRunnerValidator),
34 asyncMiddleware(deleteRunner)
35)
36
37manageRunnersRouter.get('/',
38 authenticate,
39 ensureUserHasRight(UserRight.MANAGE_RUNNERS),
40 paginationValidator,
41 runnersSortValidator,
42 setDefaultSort,
43 setDefaultPagination,
44 asyncMiddleware(listRunners)
45)
46
47// ---------------------------------------------------------------------------
48
49export {
50 manageRunnersRouter
51}
52
53// ---------------------------------------------------------------------------
54
55async function registerRunner (req: express.Request, res: express.Response) {
56 const body: RegisterRunnerBody = req.body
57
58 const runnerToken = generateRunnerToken()
59
60 const runner = new RunnerModel({
61 runnerToken,
62 name: body.name,
63 description: body.description,
64 lastContact: new Date(),
65 ip: req.ip,
66 runnerRegistrationTokenId: res.locals.runnerRegistrationToken.id
67 })
68
69 await runner.save()
70
71 logger.info('Registered new runner %s', runner.name, { ...lTags(runner.name) })
72
73 return res.json({ id: runner.id, runnerToken })
74}
75async function unregisterRunner (req: express.Request, res: express.Response) {
76 const runner = res.locals.runner
77 await runner.destroy()
78
79 logger.info('Unregistered runner %s', runner.name, { ...lTags(runner.name) })
80
81 return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
82}
83
84async function deleteRunner (req: express.Request, res: express.Response) {
85 const runner = res.locals.runner
86
87 await runner.destroy()
88
89 logger.info('Deleted runner %s', runner.name, { ...lTags(runner.name) })
90
91 return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
92}
93
94async function listRunners (req: express.Request, res: express.Response) {
95 const query: ListRunnersQuery = req.query
96
97 const resultList = await RunnerModel.listForApi({
98 start: query.start,
99 count: query.count,
100 sort: query.sort
101 })
102
103 return res.json({
104 total: resultList.total,
105 data: resultList.data.map(d => d.toFormattedJSON())
106 })
107}
diff --git a/server/controllers/api/runners/registration-tokens.ts b/server/controllers/api/runners/registration-tokens.ts
new file mode 100644
index 000000000..5ac3773fe
--- /dev/null
+++ b/server/controllers/api/runners/registration-tokens.ts
@@ -0,0 +1,87 @@
1import express from 'express'
2import { generateRunnerRegistrationToken } from '@server/helpers/token-generator'
3import {
4 asyncMiddleware,
5 authenticate,
6 ensureUserHasRight,
7 paginationValidator,
8 runnerRegistrationTokensSortValidator,
9 setDefaultPagination,
10 setDefaultSort
11} from '@server/middlewares'
12import { deleteRegistrationTokenValidator } from '@server/middlewares/validators/runners'
13import { RunnerRegistrationTokenModel } from '@server/models/runner/runner-registration-token'
14import { HttpStatusCode, ListRunnerRegistrationTokensQuery, UserRight } from '@shared/models'
15import { logger, loggerTagsFactory } from '@server/helpers/logger'
16
17const lTags = loggerTagsFactory('api', 'runner')
18
19const runnerRegistrationTokensRouter = express.Router()
20
21runnerRegistrationTokensRouter.post('/registration-tokens/generate',
22 authenticate,
23 ensureUserHasRight(UserRight.MANAGE_RUNNERS),
24 asyncMiddleware(generateRegistrationToken)
25)
26
27runnerRegistrationTokensRouter.delete('/registration-tokens/:id',
28 authenticate,
29 ensureUserHasRight(UserRight.MANAGE_RUNNERS),
30 asyncMiddleware(deleteRegistrationTokenValidator),
31 asyncMiddleware(deleteRegistrationToken)
32)
33
34runnerRegistrationTokensRouter.get('/registration-tokens',
35 authenticate,
36 ensureUserHasRight(UserRight.MANAGE_RUNNERS),
37 paginationValidator,
38 runnerRegistrationTokensSortValidator,
39 setDefaultSort,
40 setDefaultPagination,
41 asyncMiddleware(listRegistrationTokens)
42)
43
44// ---------------------------------------------------------------------------
45
46export {
47 runnerRegistrationTokensRouter
48}
49
50// ---------------------------------------------------------------------------
51
52async function generateRegistrationToken (req: express.Request, res: express.Response) {
53 logger.info('Generating new runner registration token.', lTags())
54
55 const registrationToken = new RunnerRegistrationTokenModel({
56 registrationToken: generateRunnerRegistrationToken()
57 })
58
59 await registrationToken.save()
60
61 return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
62}
63
64async function deleteRegistrationToken (req: express.Request, res: express.Response) {
65 logger.info('Removing runner registration token.', lTags())
66
67 const runnerRegistrationToken = res.locals.runnerRegistrationToken
68
69 await runnerRegistrationToken.destroy()
70
71 return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
72}
73
74async function listRegistrationTokens (req: express.Request, res: express.Response) {
75 const query: ListRunnerRegistrationTokensQuery = req.query
76
77 const resultList = await RunnerRegistrationTokenModel.listForApi({
78 start: query.start,
79 count: query.count,
80 sort: query.sort
81 })
82
83 return res.json({
84 total: resultList.total,
85 data: resultList.data.map(d => d.toFormattedJSON())
86 })
87}
diff --git a/server/controllers/api/videos/transcoding.ts b/server/controllers/api/videos/transcoding.ts
index 8c9a5322b..54f484b2b 100644
--- a/server/controllers/api/videos/transcoding.ts
+++ b/server/controllers/api/videos/transcoding.ts
@@ -1,10 +1,8 @@
1import Bluebird from 'bluebird'
2import express from 'express' 1import express from 'express'
3import { computeResolutionsToTranscode } from '@server/helpers/ffmpeg'
4import { logger, loggerTagsFactory } from '@server/helpers/logger' 2import { logger, loggerTagsFactory } from '@server/helpers/logger'
5import { JobQueue } from '@server/lib/job-queue'
6import { Hooks } from '@server/lib/plugins/hooks' 3import { Hooks } from '@server/lib/plugins/hooks'
7import { buildTranscodingJob } from '@server/lib/video' 4import { createTranscodingJobs } from '@server/lib/transcoding/create-transcoding-job'
5import { computeResolutionsToTranscode } from '@server/lib/transcoding/transcoding-resolutions'
8import { HttpStatusCode, UserRight, VideoState, VideoTranscodingCreate } from '@shared/models' 6import { HttpStatusCode, UserRight, VideoState, VideoTranscodingCreate } from '@shared/models'
9import { asyncMiddleware, authenticate, createTranscodingValidator, ensureUserHasRight } from '../../../middlewares' 7import { asyncMiddleware, authenticate, createTranscodingValidator, ensureUserHasRight } from '../../../middlewares'
10 8
@@ -47,82 +45,13 @@ async function createTranscoding (req: express.Request, res: express.Response) {
47 video.state = VideoState.TO_TRANSCODE 45 video.state = VideoState.TO_TRANSCODE
48 await video.save() 46 await video.save()
49 47
50 const childrenResolutions = resolutions.filter(r => r !== maxResolution) 48 await createTranscodingJobs({
51 49 video,
52 logger.info('Manually creating transcoding jobs for %s.', body.transcodingType, { childrenResolutions, maxResolution }) 50 resolutions,
53 51 transcodingType: body.transcodingType,
54 const children = await Bluebird.mapSeries(childrenResolutions, resolution => {
55 if (body.transcodingType === 'hls') {
56 return buildHLSJobOption({
57 videoUUID: video.uuid,
58 hasAudio,
59 resolution,
60 isMaxQuality: false
61 })
62 }
63
64 if (body.transcodingType === 'webtorrent') {
65 return buildWebTorrentJobOption({
66 videoUUID: video.uuid,
67 hasAudio,
68 resolution
69 })
70 }
71 })
72
73 const parent = body.transcodingType === 'hls'
74 ? await buildHLSJobOption({
75 videoUUID: video.uuid,
76 hasAudio,
77 resolution: maxResolution,
78 isMaxQuality: false
79 })
80 : await buildWebTorrentJobOption({
81 videoUUID: video.uuid,
82 hasAudio,
83 resolution: maxResolution
84 })
85
86 // Porcess the last resolution after the other ones to prevent concurrency issue
87 // Because low resolutions use the biggest one as ffmpeg input
88 await JobQueue.Instance.createJobWithChildren(parent, children)
89
90 return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
91}
92
93function buildHLSJobOption (options: {
94 videoUUID: string
95 hasAudio: boolean
96 resolution: number
97 isMaxQuality: boolean
98}) {
99 const { videoUUID, hasAudio, resolution, isMaxQuality } = options
100
101 return buildTranscodingJob({
102 type: 'new-resolution-to-hls',
103 videoUUID,
104 resolution,
105 hasAudio,
106 copyCodecs: false,
107 isNewVideo: false, 52 isNewVideo: false,
108 autoDeleteWebTorrentIfNeeded: false, 53 user: null // Don't specify priority since these transcoding jobs are fired by the admin
109 isMaxQuality
110 }) 54 })
111}
112 55
113function buildWebTorrentJobOption (options: { 56 return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
114 videoUUID: string
115 hasAudio: boolean
116 resolution: number
117}) {
118 const { videoUUID, hasAudio, resolution } = options
119
120 return buildTranscodingJob({
121 type: 'new-resolution-to-webtorrent',
122 videoUUID,
123 isNewVideo: false,
124 resolution,
125 hasAudio,
126 createHLSIfNeeded: false
127 })
128} 57}
diff --git a/server/controllers/api/videos/upload.ts b/server/controllers/api/videos/upload.ts
index 43313a143..885ac8b81 100644
--- a/server/controllers/api/videos/upload.ts
+++ b/server/controllers/api/videos/upload.ts
@@ -3,28 +3,20 @@ import { move } from 'fs-extra'
3import { basename } from 'path' 3import { basename } from 'path'
4import { getResumableUploadPath } from '@server/helpers/upload' 4import { getResumableUploadPath } from '@server/helpers/upload'
5import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url' 5import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url'
6import { JobQueue } from '@server/lib/job-queue' 6import { CreateJobArgument, CreateJobOptions, JobQueue } from '@server/lib/job-queue'
7import { generateWebTorrentVideoFilename } from '@server/lib/paths'
8import { Redis } from '@server/lib/redis' 7import { Redis } from '@server/lib/redis'
9import { uploadx } from '@server/lib/uploadx' 8import { uploadx } from '@server/lib/uploadx'
10import { 9import { buildLocalVideoFromReq, buildMoveToObjectStorageJob, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video'
11 buildLocalVideoFromReq, 10import { buildNewFile } from '@server/lib/video-file'
12 buildMoveToObjectStorageJob,
13 buildOptimizeOrMergeAudioJob,
14 buildVideoThumbnailsFromReq,
15 setVideoTags
16} from '@server/lib/video'
17import { VideoPathManager } from '@server/lib/video-path-manager' 11import { VideoPathManager } from '@server/lib/video-path-manager'
18import { buildNextVideoState } from '@server/lib/video-state' 12import { buildNextVideoState } from '@server/lib/video-state'
19import { openapiOperationDoc } from '@server/middlewares/doc' 13import { openapiOperationDoc } from '@server/middlewares/doc'
20import { VideoSourceModel } from '@server/models/video/video-source' 14import { VideoSourceModel } from '@server/models/video/video-source'
21import { MUserId, MVideoFile, MVideoFullLight } from '@server/types/models' 15import { MUserId, MVideoFile, MVideoFullLight } from '@server/types/models'
22import { getLowercaseExtension } from '@shared/core-utils' 16import { uuidToShort } from '@shared/extra-utils'
23import { isAudioFile, uuidToShort } from '@shared/extra-utils' 17import { HttpStatusCode, VideoCreate, VideoState } from '@shared/models'
24import { HttpStatusCode, VideoCreate, VideoResolution, VideoState } from '@shared/models'
25import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' 18import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
26import { createReqFiles } from '../../../helpers/express-utils' 19import { createReqFiles } from '../../../helpers/express-utils'
27import { buildFileMetadata, ffprobePromise, getVideoStreamDimensionsInfo, getVideoStreamFPS } from '../../../helpers/ffmpeg'
28import { logger, loggerTagsFactory } from '../../../helpers/logger' 20import { logger, loggerTagsFactory } from '../../../helpers/logger'
29import { MIMETYPES } from '../../../initializers/constants' 21import { MIMETYPES } from '../../../initializers/constants'
30import { sequelizeTypescript } from '../../../initializers/database' 22import { sequelizeTypescript } from '../../../initializers/database'
@@ -41,7 +33,6 @@ import {
41} from '../../../middlewares' 33} from '../../../middlewares'
42import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update' 34import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update'
43import { VideoModel } from '../../../models/video/video' 35import { VideoModel } from '../../../models/video/video'
44import { VideoFileModel } from '../../../models/video/video-file'
45 36
46const lTags = loggerTagsFactory('api', 'video') 37const lTags = loggerTagsFactory('api', 'video')
47const auditLogger = auditLoggerFactory('videos') 38const auditLogger = auditLoggerFactory('videos')
@@ -148,7 +139,7 @@ async function addVideo (options: {
148 video.VideoChannel = videoChannel 139 video.VideoChannel = videoChannel
149 video.url = getLocalVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object 140 video.url = getLocalVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object
150 141
151 const videoFile = await buildNewFile(videoPhysicalFile) 142 const videoFile = await buildNewFile({ path: videoPhysicalFile.path, mode: 'web-video' })
152 const originalFilename = videoPhysicalFile.originalname 143 const originalFilename = videoPhysicalFile.originalname
153 144
154 // Move physical file 145 // Move physical file
@@ -227,30 +218,8 @@ async function addVideo (options: {
227 } 218 }
228} 219}
229 220
230async function buildNewFile (videoPhysicalFile: express.VideoUploadFile) {
231 const videoFile = new VideoFileModel({
232 extname: getLowercaseExtension(videoPhysicalFile.filename),
233 size: videoPhysicalFile.size,
234 videoStreamingPlaylistId: null,
235 metadata: await buildFileMetadata(videoPhysicalFile.path)
236 })
237
238 const probe = await ffprobePromise(videoPhysicalFile.path)
239
240 if (await isAudioFile(videoPhysicalFile.path, probe)) {
241 videoFile.resolution = VideoResolution.H_NOVIDEO
242 } else {
243 videoFile.fps = await getVideoStreamFPS(videoPhysicalFile.path, probe)
244 videoFile.resolution = (await getVideoStreamDimensionsInfo(videoPhysicalFile.path, probe)).resolution
245 }
246
247 videoFile.filename = generateWebTorrentVideoFilename(videoFile.resolution, videoFile.extname)
248
249 return videoFile
250}
251
252async function addVideoJobsAfterUpload (video: MVideoFullLight, videoFile: MVideoFile, user: MUserId) { 221async function addVideoJobsAfterUpload (video: MVideoFullLight, videoFile: MVideoFile, user: MUserId) {
253 return JobQueue.Instance.createSequentialJobFlow( 222 const jobs: (CreateJobArgument & CreateJobOptions)[] = [
254 { 223 {
255 type: 'manage-video-torrent' as 'manage-video-torrent', 224 type: 'manage-video-torrent' as 'manage-video-torrent',
256 payload: { 225 payload: {
@@ -274,16 +243,26 @@ async function addVideoJobsAfterUpload (video: MVideoFullLight, videoFile: MVide
274 videoUUID: video.uuid, 243 videoUUID: video.uuid,
275 isNewVideo: true 244 isNewVideo: true
276 } 245 }
277 }, 246 }
247 ]
278 248
279 video.state === VideoState.TO_MOVE_TO_EXTERNAL_STORAGE 249 if (video.state === VideoState.TO_MOVE_TO_EXTERNAL_STORAGE) {
280 ? await buildMoveToObjectStorageJob({ video, previousVideoState: undefined }) 250 jobs.push(await buildMoveToObjectStorageJob({ video, previousVideoState: undefined }))
281 : undefined, 251 }
252
253 if (video.state === VideoState.TO_TRANSCODE) {
254 jobs.push({
255 type: 'transcoding-job-builder' as 'transcoding-job-builder',
256 payload: {
257 videoUUID: video.uuid,
258 optimizeJob: {
259 isNewVideo: true
260 }
261 }
262 })
263 }
282 264
283 video.state === VideoState.TO_TRANSCODE 265 return JobQueue.Instance.createSequentialJobFlow(...jobs)
284 ? await buildOptimizeOrMergeAudioJob({ video, videoFile, user })
285 : undefined
286 )
287} 266}
288 267
289async function deleteUploadResumableCache (req: express.Request, res: express.Response, next: express.NextFunction) { 268async function deleteUploadResumableCache (req: express.Request, res: express.Response, next: express.NextFunction) {
diff --git a/server/controllers/bots.ts b/server/controllers/bots.ts
index a5ce1d79f..2b825a730 100644
--- a/server/controllers/bots.ts
+++ b/server/controllers/bots.ts
@@ -1,8 +1,8 @@
1import { getServerActor } from '@server/models/application/application'
2import { logger } from '@uploadx/core'
3import express from 'express' 1import express from 'express'
4import { truncate } from 'lodash' 2import { truncate } from 'lodash'
5import { SitemapStream, streamToPromise, ErrorLevel } from 'sitemap' 3import { ErrorLevel, SitemapStream, streamToPromise } from 'sitemap'
4import { logger } from '@server/helpers/logger'
5import { getServerActor } from '@server/models/application/application'
6import { buildNSFWFilter } from '../helpers/express-utils' 6import { buildNSFWFilter } from '../helpers/express-utils'
7import { ROUTE_CACHE_LIFETIME, WEBSERVER } from '../initializers/constants' 7import { ROUTE_CACHE_LIFETIME, WEBSERVER } from '../initializers/constants'
8import { asyncMiddleware } from '../middlewares' 8import { asyncMiddleware } from '../middlewares'
diff --git a/server/controllers/object-storage-proxy.ts b/server/controllers/object-storage-proxy.ts
index c530b57f8..8e2cc4af9 100644
--- a/server/controllers/object-storage-proxy.ts
+++ b/server/controllers/object-storage-proxy.ts
@@ -1,11 +1,7 @@
1import cors from 'cors' 1import cors from 'cors'
2import express from 'express' 2import express from 'express'
3import { PassThrough, pipeline } from 'stream'
4import { logger } from '@server/helpers/logger'
5import { StreamReplacer } from '@server/helpers/stream-replacer'
6import { OBJECT_STORAGE_PROXY_PATHS } from '@server/initializers/constants' 3import { OBJECT_STORAGE_PROXY_PATHS } from '@server/initializers/constants'
7import { injectQueryToPlaylistUrls } from '@server/lib/hls' 4import { proxifyHLS, proxifyWebTorrentFile } from '@server/lib/object-storage'
8import { getHLSFileReadStream, getWebTorrentFileReadStream } from '@server/lib/object-storage'
9import { 5import {
10 asyncMiddleware, 6 asyncMiddleware,
11 ensureCanAccessPrivateVideoHLSFiles, 7 ensureCanAccessPrivateVideoHLSFiles,
@@ -13,9 +9,7 @@ import {
13 ensurePrivateObjectStorageProxyIsEnabled, 9 ensurePrivateObjectStorageProxyIsEnabled,
14 optionalAuthenticate 10 optionalAuthenticate
15} from '@server/middlewares' 11} from '@server/middlewares'
16import { HttpStatusCode } from '@shared/models' 12import { doReinjectVideoFileToken } from './shared/m3u8-playlist'
17import { buildReinjectVideoFileTokenQuery, doReinjectVideoFileToken } from './shared/m3u8-playlist'
18import { GetObjectCommandOutput } from '@aws-sdk/client-s3'
19 13
20const objectStorageProxyRouter = express.Router() 14const objectStorageProxyRouter = express.Router()
21 15
@@ -25,14 +19,14 @@ objectStorageProxyRouter.get(OBJECT_STORAGE_PROXY_PATHS.PRIVATE_WEBSEED + ':file
25 ensurePrivateObjectStorageProxyIsEnabled, 19 ensurePrivateObjectStorageProxyIsEnabled,
26 optionalAuthenticate, 20 optionalAuthenticate,
27 asyncMiddleware(ensureCanAccessVideoPrivateWebTorrentFiles), 21 asyncMiddleware(ensureCanAccessVideoPrivateWebTorrentFiles),
28 asyncMiddleware(proxifyWebTorrent) 22 asyncMiddleware(proxifyWebTorrentController)
29) 23)
30 24
31objectStorageProxyRouter.get(OBJECT_STORAGE_PROXY_PATHS.STREAMING_PLAYLISTS.PRIVATE_HLS + ':videoUUID/:filename', 25objectStorageProxyRouter.get(OBJECT_STORAGE_PROXY_PATHS.STREAMING_PLAYLISTS.PRIVATE_HLS + ':videoUUID/:filename',
32 ensurePrivateObjectStorageProxyIsEnabled, 26 ensurePrivateObjectStorageProxyIsEnabled,
33 optionalAuthenticate, 27 optionalAuthenticate,
34 asyncMiddleware(ensureCanAccessPrivateVideoHLSFiles), 28 asyncMiddleware(ensureCanAccessPrivateVideoHLSFiles),
35 asyncMiddleware(proxifyHLS) 29 asyncMiddleware(proxifyHLSController)
36) 30)
37 31
38// --------------------------------------------------------------------------- 32// ---------------------------------------------------------------------------
@@ -41,76 +35,25 @@ export {
41 objectStorageProxyRouter 35 objectStorageProxyRouter
42} 36}
43 37
44async function proxifyWebTorrent (req: express.Request, res: express.Response) { 38function proxifyWebTorrentController (req: express.Request, res: express.Response) {
45 const filename = req.params.filename 39 const filename = req.params.filename
46 40
47 logger.debug('Proxifying WebTorrent file %s from object storage.', filename) 41 return proxifyWebTorrentFile({ req, res, filename })
48
49 try {
50 const { response: s3Response, stream } = await getWebTorrentFileReadStream({
51 filename,
52 rangeHeader: req.header('range')
53 })
54
55 setS3Headers(res, s3Response)
56
57 return stream.pipe(res)
58 } catch (err) {
59 return handleObjectStorageFailure(res, err)
60 }
61} 42}
62 43
63async function proxifyHLS (req: express.Request, res: express.Response) { 44function proxifyHLSController (req: express.Request, res: express.Response) {
64 const playlist = res.locals.videoStreamingPlaylist 45 const playlist = res.locals.videoStreamingPlaylist
65 const video = res.locals.onlyVideo 46 const video = res.locals.onlyVideo
66 const filename = req.params.filename 47 const filename = req.params.filename
67 48
68 logger.debug('Proxifying HLS file %s from object storage.', filename) 49 const reinjectVideoFileToken = filename.endsWith('.m3u8') && doReinjectVideoFileToken(req)
69
70 try {
71 const { response: s3Response, stream } = await getHLSFileReadStream({
72 playlist: playlist.withVideo(video),
73 filename,
74 rangeHeader: req.header('range')
75 })
76
77 setS3Headers(res, s3Response)
78
79 const streamReplacer = filename.endsWith('.m3u8') && doReinjectVideoFileToken(req)
80 ? new StreamReplacer(line => injectQueryToPlaylistUrls(line, buildReinjectVideoFileTokenQuery(req, filename.endsWith('master.m3u8'))))
81 : new PassThrough()
82 50
83 return pipeline( 51 return proxifyHLS({
84 stream, 52 req,
85 streamReplacer, 53 res,
86 res, 54 playlist,
87 err => { 55 video,
88 if (!err) return 56 filename,
89 57 reinjectVideoFileToken
90 handleObjectStorageFailure(res, err)
91 }
92 )
93 } catch (err) {
94 return handleObjectStorageFailure(res, err)
95 }
96}
97
98function handleObjectStorageFailure (res: express.Response, err: Error) {
99 if (err.name === 'NoSuchKey') {
100 logger.debug('Could not find key in object storage to proxify private HLS video file.', { err })
101 return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
102 }
103
104 return res.fail({
105 status: HttpStatusCode.INTERNAL_SERVER_ERROR_500,
106 message: err.message,
107 type: err.name
108 }) 58 })
109} 59}
110
111function setS3Headers (res: express.Response, s3Response: GetObjectCommandOutput) {
112 if (s3Response.$metadata.httpStatusCode === HttpStatusCode.PARTIAL_CONTENT_206) {
113 res.setHeader('Content-Range', s3Response.ContentRange)
114 res.status(HttpStatusCode.PARTIAL_CONTENT_206)
115 }
116}