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/api/videos | |
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/api/videos')
-rw-r--r-- | server/controllers/api/videos/transcoding.ts | 87 | ||||
-rw-r--r-- | server/controllers/api/videos/upload.ts | 71 |
2 files changed, 33 insertions, 125 deletions
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) { |