diff options
author | Chocobozzz <me@florianbigard.com> | 2023-04-21 14:55:10 +0200 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2023-05-09 08:57:34 +0200 |
commit | 0c9668f77901e7540e2c7045eb0f2974a4842a69 (patch) | |
tree | 226d3dd1565b0bb56588897af3b8530e6216e96b /server/controllers | |
parent | 6bcb854cdea8688a32240bc5719c7d139806e00b (diff) | |
download | PeerTube-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.ts | 6 | ||||
-rw-r--r-- | server/controllers/api/index.ts | 2 | ||||
-rw-r--r-- | server/controllers/api/jobs.ts | 3 | ||||
-rw-r--r-- | server/controllers/api/runners/index.ts | 18 | ||||
-rw-r--r-- | server/controllers/api/runners/jobs-files.ts | 84 | ||||
-rw-r--r-- | server/controllers/api/runners/jobs.ts | 352 | ||||
-rw-r--r-- | server/controllers/api/runners/manage-runners.ts | 107 | ||||
-rw-r--r-- | server/controllers/api/runners/registration-tokens.ts | 87 | ||||
-rw-r--r-- | server/controllers/api/videos/transcoding.ts | 87 | ||||
-rw-r--r-- | server/controllers/api/videos/upload.ts | 71 | ||||
-rw-r--r-- | server/controllers/bots.ts | 6 | ||||
-rw-r--r-- | server/controllers/object-storage-proxy.ts | 87 |
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' | |||
15 | import { oauthClientsRouter } from './oauth-clients' | 15 | import { oauthClientsRouter } from './oauth-clients' |
16 | import { overviewsRouter } from './overviews' | 16 | import { overviewsRouter } from './overviews' |
17 | import { pluginRouter } from './plugins' | 17 | import { pluginRouter } from './plugins' |
18 | import { runnersRouter } from './runners' | ||
18 | import { searchRouter } from './search' | 19 | import { searchRouter } from './search' |
19 | import { serverRouter } from './server' | 20 | import { serverRouter } from './server' |
20 | import { usersRouter } from './users' | 21 | import { usersRouter } from './users' |
@@ -55,6 +56,7 @@ apiRouter.use('/overviews', overviewsRouter) | |||
55 | apiRouter.use('/plugins', pluginRouter) | 56 | apiRouter.use('/plugins', pluginRouter) |
56 | apiRouter.use('/custom-pages', customPageRouter) | 57 | apiRouter.use('/custom-pages', customPageRouter) |
57 | apiRouter.use('/blocklist', blocklistRouter) | 58 | apiRouter.use('/blocklist', blocklistRouter) |
59 | apiRouter.use('/runners', runnersRouter) | ||
58 | apiRouter.use('/ping', pong) | 60 | apiRouter.use('/ping', pong) |
59 | apiRouter.use('/*', badRequest) | 61 | apiRouter.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 @@ | |||
1 | import express from 'express' | ||
2 | import { runnerJobsRouter } from './jobs' | ||
3 | import { runnerJobFilesRouter } from './jobs-files' | ||
4 | import { manageRunnersRouter } from './manage-runners' | ||
5 | import { runnerRegistrationTokensRouter } from './registration-tokens' | ||
6 | |||
7 | const runnersRouter = express.Router() | ||
8 | |||
9 | runnersRouter.use('/', manageRunnersRouter) | ||
10 | runnersRouter.use('/', runnerJobsRouter) | ||
11 | runnersRouter.use('/', runnerJobFilesRouter) | ||
12 | runnersRouter.use('/', runnerRegistrationTokensRouter) | ||
13 | |||
14 | // --------------------------------------------------------------------------- | ||
15 | |||
16 | export { | ||
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 @@ | |||
1 | import express from 'express' | ||
2 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | ||
3 | import { proxifyHLS, proxifyWebTorrentFile } from '@server/lib/object-storage' | ||
4 | import { VideoPathManager } from '@server/lib/video-path-manager' | ||
5 | import { asyncMiddleware } from '@server/middlewares' | ||
6 | import { jobOfRunnerGetValidator } from '@server/middlewares/validators/runners' | ||
7 | import { runnerJobGetVideoTranscodingFileValidator } from '@server/middlewares/validators/runners/job-files' | ||
8 | import { VideoStorage } from '@shared/models' | ||
9 | |||
10 | const lTags = loggerTagsFactory('api', 'runner') | ||
11 | |||
12 | const runnerJobFilesRouter = express.Router() | ||
13 | |||
14 | runnerJobFilesRouter.post('/jobs/:jobUUID/files/videos/:videoId/max-quality', | ||
15 | asyncMiddleware(jobOfRunnerGetValidator), | ||
16 | asyncMiddleware(runnerJobGetVideoTranscodingFileValidator), | ||
17 | asyncMiddleware(getMaxQualityVideoFile) | ||
18 | ) | ||
19 | |||
20 | runnerJobFilesRouter.post('/jobs/:jobUUID/files/videos/:videoId/previews/max-quality', | ||
21 | asyncMiddleware(jobOfRunnerGetValidator), | ||
22 | asyncMiddleware(runnerJobGetVideoTranscodingFileValidator), | ||
23 | getMaxQualityVideoPreview | ||
24 | ) | ||
25 | |||
26 | // --------------------------------------------------------------------------- | ||
27 | |||
28 | export { | ||
29 | runnerJobFilesRouter | ||
30 | } | ||
31 | |||
32 | // --------------------------------------------------------------------------- | ||
33 | |||
34 | async 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 | |||
71 | function 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 @@ | |||
1 | import express, { UploadFiles } from 'express' | ||
2 | import { createReqFiles } from '@server/helpers/express-utils' | ||
3 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | ||
4 | import { generateRunnerJobToken } from '@server/helpers/token-generator' | ||
5 | import { MIMETYPES } from '@server/initializers/constants' | ||
6 | import { sequelizeTypescript } from '@server/initializers/database' | ||
7 | import { getRunnerJobHandlerClass, updateLastRunnerContact } from '@server/lib/runners' | ||
8 | import { | ||
9 | asyncMiddleware, | ||
10 | authenticate, | ||
11 | ensureUserHasRight, | ||
12 | paginationValidator, | ||
13 | runnerJobsSortValidator, | ||
14 | setDefaultPagination, | ||
15 | setDefaultSort | ||
16 | } from '@server/middlewares' | ||
17 | import { | ||
18 | abortRunnerJobValidator, | ||
19 | acceptRunnerJobValidator, | ||
20 | errorRunnerJobValidator, | ||
21 | getRunnerFromTokenValidator, | ||
22 | jobOfRunnerGetValidator, | ||
23 | runnerJobGetValidator, | ||
24 | successRunnerJobValidator, | ||
25 | updateRunnerJobValidator | ||
26 | } from '@server/middlewares/validators/runners' | ||
27 | import { RunnerModel } from '@server/models/runner/runner' | ||
28 | import { RunnerJobModel } from '@server/models/runner/runner-job' | ||
29 | import { | ||
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 | |||
49 | const postRunnerJobSuccessVideoFiles = createReqFiles( | ||
50 | [ 'payload[videoFile]', 'payload[resolutionPlaylistFile]' ], | ||
51 | { ...MIMETYPES.VIDEO.MIMETYPE_EXT, ...MIMETYPES.M3U8.MIMETYPE_EXT } | ||
52 | ) | ||
53 | |||
54 | const runnerJobUpdateVideoFiles = createReqFiles( | ||
55 | [ 'payload[videoChunkFile]', 'payload[resolutionPlaylistFile]', 'payload[masterPlaylistFile]' ], | ||
56 | { ...MIMETYPES.VIDEO.MIMETYPE_EXT, ...MIMETYPES.M3U8.MIMETYPE_EXT } | ||
57 | ) | ||
58 | |||
59 | const lTags = loggerTagsFactory('api', 'runner') | ||
60 | |||
61 | const runnerJobsRouter = express.Router() | ||
62 | |||
63 | // --------------------------------------------------------------------------- | ||
64 | // Controllers for runners | ||
65 | // --------------------------------------------------------------------------- | ||
66 | |||
67 | runnerJobsRouter.post('/jobs/request', | ||
68 | asyncMiddleware(getRunnerFromTokenValidator), | ||
69 | asyncMiddleware(requestRunnerJob) | ||
70 | ) | ||
71 | |||
72 | runnerJobsRouter.post('/jobs/:jobUUID/accept', | ||
73 | asyncMiddleware(runnerJobGetValidator), | ||
74 | acceptRunnerJobValidator, | ||
75 | asyncMiddleware(getRunnerFromTokenValidator), | ||
76 | asyncMiddleware(acceptRunnerJob) | ||
77 | ) | ||
78 | |||
79 | runnerJobsRouter.post('/jobs/:jobUUID/abort', | ||
80 | asyncMiddleware(jobOfRunnerGetValidator), | ||
81 | abortRunnerJobValidator, | ||
82 | asyncMiddleware(abortRunnerJob) | ||
83 | ) | ||
84 | |||
85 | runnerJobsRouter.post('/jobs/:jobUUID/update', | ||
86 | runnerJobUpdateVideoFiles, | ||
87 | asyncMiddleware(jobOfRunnerGetValidator), | ||
88 | updateRunnerJobValidator, | ||
89 | asyncMiddleware(updateRunnerJobController) | ||
90 | ) | ||
91 | |||
92 | runnerJobsRouter.post('/jobs/:jobUUID/error', | ||
93 | asyncMiddleware(jobOfRunnerGetValidator), | ||
94 | errorRunnerJobValidator, | ||
95 | asyncMiddleware(errorRunnerJob) | ||
96 | ) | ||
97 | |||
98 | runnerJobsRouter.post('/jobs/:jobUUID/success', | ||
99 | postRunnerJobSuccessVideoFiles, | ||
100 | asyncMiddleware(jobOfRunnerGetValidator), | ||
101 | successRunnerJobValidator, | ||
102 | asyncMiddleware(postRunnerJobSuccess) | ||
103 | ) | ||
104 | |||
105 | // --------------------------------------------------------------------------- | ||
106 | // Controllers for admins | ||
107 | // --------------------------------------------------------------------------- | ||
108 | |||
109 | runnerJobsRouter.post('/jobs/:jobUUID/cancel', | ||
110 | authenticate, | ||
111 | ensureUserHasRight(UserRight.MANAGE_RUNNERS), | ||
112 | asyncMiddleware(runnerJobGetValidator), | ||
113 | asyncMiddleware(cancelRunnerJob) | ||
114 | ) | ||
115 | |||
116 | runnerJobsRouter.get('/jobs', | ||
117 | authenticate, | ||
118 | ensureUserHasRight(UserRight.MANAGE_RUNNERS), | ||
119 | paginationValidator, | ||
120 | runnerJobsSortValidator, | ||
121 | setDefaultSort, | ||
122 | setDefaultPagination, | ||
123 | asyncMiddleware(listRunnerJobs) | ||
124 | ) | ||
125 | |||
126 | // --------------------------------------------------------------------------- | ||
127 | |||
128 | export { | ||
129 | runnerJobsRouter | ||
130 | } | ||
131 | |||
132 | // --------------------------------------------------------------------------- | ||
133 | |||
134 | // --------------------------------------------------------------------------- | ||
135 | // Controllers for runners | ||
136 | // --------------------------------------------------------------------------- | ||
137 | |||
138 | async 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 | |||
157 | async 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 | |||
189 | async 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 | |||
207 | async 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 | |||
229 | const 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 | |||
243 | async 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 | |||
272 | const 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 | |||
303 | async 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 | |||
327 | async 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 | |||
338 | async 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 @@ | |||
1 | import express from 'express' | ||
2 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | ||
3 | import { generateRunnerToken } from '@server/helpers/token-generator' | ||
4 | import { | ||
5 | asyncMiddleware, | ||
6 | authenticate, | ||
7 | ensureUserHasRight, | ||
8 | paginationValidator, | ||
9 | runnersSortValidator, | ||
10 | setDefaultPagination, | ||
11 | setDefaultSort | ||
12 | } from '@server/middlewares' | ||
13 | import { deleteRunnerValidator, getRunnerFromTokenValidator, registerRunnerValidator } from '@server/middlewares/validators/runners' | ||
14 | import { RunnerModel } from '@server/models/runner/runner' | ||
15 | import { HttpStatusCode, ListRunnersQuery, RegisterRunnerBody, UserRight } from '@shared/models' | ||
16 | |||
17 | const lTags = loggerTagsFactory('api', 'runner') | ||
18 | |||
19 | const manageRunnersRouter = express.Router() | ||
20 | |||
21 | manageRunnersRouter.post('/register', | ||
22 | asyncMiddleware(registerRunnerValidator), | ||
23 | asyncMiddleware(registerRunner) | ||
24 | ) | ||
25 | manageRunnersRouter.post('/unregister', | ||
26 | asyncMiddleware(getRunnerFromTokenValidator), | ||
27 | asyncMiddleware(unregisterRunner) | ||
28 | ) | ||
29 | |||
30 | manageRunnersRouter.delete('/:runnerId', | ||
31 | authenticate, | ||
32 | ensureUserHasRight(UserRight.MANAGE_RUNNERS), | ||
33 | asyncMiddleware(deleteRunnerValidator), | ||
34 | asyncMiddleware(deleteRunner) | ||
35 | ) | ||
36 | |||
37 | manageRunnersRouter.get('/', | ||
38 | authenticate, | ||
39 | ensureUserHasRight(UserRight.MANAGE_RUNNERS), | ||
40 | paginationValidator, | ||
41 | runnersSortValidator, | ||
42 | setDefaultSort, | ||
43 | setDefaultPagination, | ||
44 | asyncMiddleware(listRunners) | ||
45 | ) | ||
46 | |||
47 | // --------------------------------------------------------------------------- | ||
48 | |||
49 | export { | ||
50 | manageRunnersRouter | ||
51 | } | ||
52 | |||
53 | // --------------------------------------------------------------------------- | ||
54 | |||
55 | async 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 | } | ||
75 | async 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 | |||
84 | async 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 | |||
94 | async 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 @@ | |||
1 | import express from 'express' | ||
2 | import { generateRunnerRegistrationToken } from '@server/helpers/token-generator' | ||
3 | import { | ||
4 | asyncMiddleware, | ||
5 | authenticate, | ||
6 | ensureUserHasRight, | ||
7 | paginationValidator, | ||
8 | runnerRegistrationTokensSortValidator, | ||
9 | setDefaultPagination, | ||
10 | setDefaultSort | ||
11 | } from '@server/middlewares' | ||
12 | import { deleteRegistrationTokenValidator } from '@server/middlewares/validators/runners' | ||
13 | import { RunnerRegistrationTokenModel } from '@server/models/runner/runner-registration-token' | ||
14 | import { HttpStatusCode, ListRunnerRegistrationTokensQuery, UserRight } from '@shared/models' | ||
15 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | ||
16 | |||
17 | const lTags = loggerTagsFactory('api', 'runner') | ||
18 | |||
19 | const runnerRegistrationTokensRouter = express.Router() | ||
20 | |||
21 | runnerRegistrationTokensRouter.post('/registration-tokens/generate', | ||
22 | authenticate, | ||
23 | ensureUserHasRight(UserRight.MANAGE_RUNNERS), | ||
24 | asyncMiddleware(generateRegistrationToken) | ||
25 | ) | ||
26 | |||
27 | runnerRegistrationTokensRouter.delete('/registration-tokens/:id', | ||
28 | authenticate, | ||
29 | ensureUserHasRight(UserRight.MANAGE_RUNNERS), | ||
30 | asyncMiddleware(deleteRegistrationTokenValidator), | ||
31 | asyncMiddleware(deleteRegistrationToken) | ||
32 | ) | ||
33 | |||
34 | runnerRegistrationTokensRouter.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 | |||
46 | export { | ||
47 | runnerRegistrationTokensRouter | ||
48 | } | ||
49 | |||
50 | // --------------------------------------------------------------------------- | ||
51 | |||
52 | async 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 | |||
64 | async 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 | |||
74 | async 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 @@ | |||
1 | import Bluebird from 'bluebird' | ||
2 | import express from 'express' | 1 | import express from 'express' |
3 | import { computeResolutionsToTranscode } from '@server/helpers/ffmpeg' | ||
4 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | 2 | import { logger, loggerTagsFactory } from '@server/helpers/logger' |
5 | import { JobQueue } from '@server/lib/job-queue' | ||
6 | import { Hooks } from '@server/lib/plugins/hooks' | 3 | import { Hooks } from '@server/lib/plugins/hooks' |
7 | import { buildTranscodingJob } from '@server/lib/video' | 4 | import { createTranscodingJobs } from '@server/lib/transcoding/create-transcoding-job' |
5 | import { computeResolutionsToTranscode } from '@server/lib/transcoding/transcoding-resolutions' | ||
8 | import { HttpStatusCode, UserRight, VideoState, VideoTranscodingCreate } from '@shared/models' | 6 | import { HttpStatusCode, UserRight, VideoState, VideoTranscodingCreate } from '@shared/models' |
9 | import { asyncMiddleware, authenticate, createTranscodingValidator, ensureUserHasRight } from '../../../middlewares' | 7 | import { 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 | |||
93 | function 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 | ||
113 | function 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' | |||
3 | import { basename } from 'path' | 3 | import { basename } from 'path' |
4 | import { getResumableUploadPath } from '@server/helpers/upload' | 4 | import { getResumableUploadPath } from '@server/helpers/upload' |
5 | import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url' | 5 | import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url' |
6 | import { JobQueue } from '@server/lib/job-queue' | 6 | import { CreateJobArgument, CreateJobOptions, JobQueue } from '@server/lib/job-queue' |
7 | import { generateWebTorrentVideoFilename } from '@server/lib/paths' | ||
8 | import { Redis } from '@server/lib/redis' | 7 | import { Redis } from '@server/lib/redis' |
9 | import { uploadx } from '@server/lib/uploadx' | 8 | import { uploadx } from '@server/lib/uploadx' |
10 | import { | 9 | import { buildLocalVideoFromReq, buildMoveToObjectStorageJob, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video' |
11 | buildLocalVideoFromReq, | 10 | import { buildNewFile } from '@server/lib/video-file' |
12 | buildMoveToObjectStorageJob, | ||
13 | buildOptimizeOrMergeAudioJob, | ||
14 | buildVideoThumbnailsFromReq, | ||
15 | setVideoTags | ||
16 | } from '@server/lib/video' | ||
17 | import { VideoPathManager } from '@server/lib/video-path-manager' | 11 | import { VideoPathManager } from '@server/lib/video-path-manager' |
18 | import { buildNextVideoState } from '@server/lib/video-state' | 12 | import { buildNextVideoState } from '@server/lib/video-state' |
19 | import { openapiOperationDoc } from '@server/middlewares/doc' | 13 | import { openapiOperationDoc } from '@server/middlewares/doc' |
20 | import { VideoSourceModel } from '@server/models/video/video-source' | 14 | import { VideoSourceModel } from '@server/models/video/video-source' |
21 | import { MUserId, MVideoFile, MVideoFullLight } from '@server/types/models' | 15 | import { MUserId, MVideoFile, MVideoFullLight } from '@server/types/models' |
22 | import { getLowercaseExtension } from '@shared/core-utils' | 16 | import { uuidToShort } from '@shared/extra-utils' |
23 | import { isAudioFile, uuidToShort } from '@shared/extra-utils' | 17 | import { HttpStatusCode, VideoCreate, VideoState } from '@shared/models' |
24 | import { HttpStatusCode, VideoCreate, VideoResolution, VideoState } from '@shared/models' | ||
25 | import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' | 18 | import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' |
26 | import { createReqFiles } from '../../../helpers/express-utils' | 19 | import { createReqFiles } from '../../../helpers/express-utils' |
27 | import { buildFileMetadata, ffprobePromise, getVideoStreamDimensionsInfo, getVideoStreamFPS } from '../../../helpers/ffmpeg' | ||
28 | import { logger, loggerTagsFactory } from '../../../helpers/logger' | 20 | import { logger, loggerTagsFactory } from '../../../helpers/logger' |
29 | import { MIMETYPES } from '../../../initializers/constants' | 21 | import { MIMETYPES } from '../../../initializers/constants' |
30 | import { sequelizeTypescript } from '../../../initializers/database' | 22 | import { sequelizeTypescript } from '../../../initializers/database' |
@@ -41,7 +33,6 @@ import { | |||
41 | } from '../../../middlewares' | 33 | } from '../../../middlewares' |
42 | import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update' | 34 | import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update' |
43 | import { VideoModel } from '../../../models/video/video' | 35 | import { VideoModel } from '../../../models/video/video' |
44 | import { VideoFileModel } from '../../../models/video/video-file' | ||
45 | 36 | ||
46 | const lTags = loggerTagsFactory('api', 'video') | 37 | const lTags = loggerTagsFactory('api', 'video') |
47 | const auditLogger = auditLoggerFactory('videos') | 38 | const 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 | ||
230 | async 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 | |||
252 | async function addVideoJobsAfterUpload (video: MVideoFullLight, videoFile: MVideoFile, user: MUserId) { | 221 | async 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 | ||
289 | async function deleteUploadResumableCache (req: express.Request, res: express.Response, next: express.NextFunction) { | 268 | async 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 @@ | |||
1 | import { getServerActor } from '@server/models/application/application' | ||
2 | import { logger } from '@uploadx/core' | ||
3 | import express from 'express' | 1 | import express from 'express' |
4 | import { truncate } from 'lodash' | 2 | import { truncate } from 'lodash' |
5 | import { SitemapStream, streamToPromise, ErrorLevel } from 'sitemap' | 3 | import { ErrorLevel, SitemapStream, streamToPromise } from 'sitemap' |
4 | import { logger } from '@server/helpers/logger' | ||
5 | import { getServerActor } from '@server/models/application/application' | ||
6 | import { buildNSFWFilter } from '../helpers/express-utils' | 6 | import { buildNSFWFilter } from '../helpers/express-utils' |
7 | import { ROUTE_CACHE_LIFETIME, WEBSERVER } from '../initializers/constants' | 7 | import { ROUTE_CACHE_LIFETIME, WEBSERVER } from '../initializers/constants' |
8 | import { asyncMiddleware } from '../middlewares' | 8 | import { 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 @@ | |||
1 | import cors from 'cors' | 1 | import cors from 'cors' |
2 | import express from 'express' | 2 | import express from 'express' |
3 | import { PassThrough, pipeline } from 'stream' | ||
4 | import { logger } from '@server/helpers/logger' | ||
5 | import { StreamReplacer } from '@server/helpers/stream-replacer' | ||
6 | import { OBJECT_STORAGE_PROXY_PATHS } from '@server/initializers/constants' | 3 | import { OBJECT_STORAGE_PROXY_PATHS } from '@server/initializers/constants' |
7 | import { injectQueryToPlaylistUrls } from '@server/lib/hls' | 4 | import { proxifyHLS, proxifyWebTorrentFile } from '@server/lib/object-storage' |
8 | import { getHLSFileReadStream, getWebTorrentFileReadStream } from '@server/lib/object-storage' | ||
9 | import { | 5 | import { |
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' |
16 | import { HttpStatusCode } from '@shared/models' | 12 | import { doReinjectVideoFileToken } from './shared/m3u8-playlist' |
17 | import { buildReinjectVideoFileTokenQuery, doReinjectVideoFileToken } from './shared/m3u8-playlist' | ||
18 | import { GetObjectCommandOutput } from '@aws-sdk/client-s3' | ||
19 | 13 | ||
20 | const objectStorageProxyRouter = express.Router() | 14 | const 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 | ||
31 | objectStorageProxyRouter.get(OBJECT_STORAGE_PROXY_PATHS.STREAMING_PLAYLISTS.PRIVATE_HLS + ':videoUUID/:filename', | 25 | objectStorageProxyRouter.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 | ||
44 | async function proxifyWebTorrent (req: express.Request, res: express.Response) { | 38 | function 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 | ||
63 | async function proxifyHLS (req: express.Request, res: express.Response) { | 44 | function 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 | |||
98 | function 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 | |||
111 | function 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 | } | ||