diff options
54 files changed, 715 insertions, 103 deletions
diff --git a/client/src/app/+admin/overview/videos/video-list.component.html b/client/src/app/+admin/overview/videos/video-list.component.html index 6e4fb4c6f..738bcedee 100644 --- a/client/src/app/+admin/overview/videos/video-list.component.html +++ b/client/src/app/+admin/overview/videos/video-list.component.html | |||
@@ -56,8 +56,8 @@ | |||
56 | 56 | ||
57 | <td class="action-cell"> | 57 | <td class="action-cell"> |
58 | <my-video-actions-dropdown | 58 | <my-video-actions-dropdown |
59 | placement="bottom auto" buttonDirection="horizontal" [buttonStyled]="true" [video]="video" | 59 | placement="bottom auto" buttonDirection="horizontal" [buttonStyled]="true" [video]="video" [displayOptions]="videoActionsOptions" |
60 | [displayOptions]="videoActionsOptions" (videoRemoved)="reloadData()" (videoFilesRemoved)="reloadData()" | 60 | (videoRemoved)="reloadData()" (videoFilesRemoved)="reloadData()" (transcodingCreated)="reloadData()" |
61 | ></my-video-actions-dropdown> | 61 | ></my-video-actions-dropdown> |
62 | </td> | 62 | </td> |
63 | 63 | ||
diff --git a/client/src/app/+admin/overview/videos/video-list.component.ts b/client/src/app/+admin/overview/videos/video-list.component.ts index 3c21adb44..4aed5221b 100644 --- a/client/src/app/+admin/overview/videos/video-list.component.ts +++ b/client/src/app/+admin/overview/videos/video-list.component.ts | |||
@@ -40,7 +40,8 @@ export class VideoListComponent extends RestTable implements OnInit { | |||
40 | duplicate: true, | 40 | duplicate: true, |
41 | mute: true, | 41 | mute: true, |
42 | liveInfo: false, | 42 | liveInfo: false, |
43 | removeFiles: true | 43 | removeFiles: true, |
44 | transcoding: true | ||
44 | } | 45 | } |
45 | 46 | ||
46 | loading = true | 47 | loading = true |
@@ -90,15 +91,27 @@ export class VideoListComponent extends RestTable implements OnInit { | |||
90 | ], | 91 | ], |
91 | [ | 92 | [ |
92 | { | 93 | { |
94 | label: $localize`Run HLS transcoding`, | ||
95 | handler: videos => this.runTranscoding(videos, 'hls'), | ||
96 | isDisplayed: videos => videos.every(v => v.canRunTranscoding(this.authUser)), | ||
97 | iconName: 'cog' | ||
98 | }, | ||
99 | { | ||
100 | label: $localize`Run WebTorrent transcoding`, | ||
101 | handler: videos => this.runTranscoding(videos, 'webtorrent'), | ||
102 | isDisplayed: videos => videos.every(v => v.canRunTranscoding(this.authUser)), | ||
103 | iconName: 'cog' | ||
104 | }, | ||
105 | { | ||
93 | label: $localize`Delete HLS files`, | 106 | label: $localize`Delete HLS files`, |
94 | handler: videos => this.removeVideoFiles(videos, 'hls'), | 107 | handler: videos => this.removeVideoFiles(videos, 'hls'), |
95 | isDisplayed: videos => this.authUser.hasRight(UserRight.MANAGE_VIDEO_FILES) && videos.every(v => v.hasHLS() && v.hasWebTorrent()), | 108 | isDisplayed: videos => videos.every(v => v.canRemoveFiles(this.authUser)), |
96 | iconName: 'delete' | 109 | iconName: 'delete' |
97 | }, | 110 | }, |
98 | { | 111 | { |
99 | label: $localize`Delete WebTorrent files`, | 112 | label: $localize`Delete WebTorrent files`, |
100 | handler: videos => this.removeVideoFiles(videos, 'webtorrent'), | 113 | handler: videos => this.removeVideoFiles(videos, 'webtorrent'), |
101 | isDisplayed: videos => this.authUser.hasRight(UserRight.MANAGE_VIDEO_FILES) && videos.every(v => v.hasHLS() && v.hasWebTorrent()), | 114 | isDisplayed: videos => videos.every(v => v.canRemoveFiles(this.authUser)), |
102 | iconName: 'delete' | 115 | iconName: 'delete' |
103 | } | 116 | } |
104 | ] | 117 | ] |
@@ -226,4 +239,17 @@ export class VideoListComponent extends RestTable implements OnInit { | |||
226 | error: err => this.notifier.error(err.message) | 239 | error: err => this.notifier.error(err.message) |
227 | }) | 240 | }) |
228 | } | 241 | } |
242 | |||
243 | private runTranscoding (videos: Video[], type: 'hls' | 'webtorrent') { | ||
244 | this.videoService.runTranscoding(videos.map(v => v.id), type) | ||
245 | .subscribe({ | ||
246 | next: () => { | ||
247 | this.notifier.success($localize`Transcoding jobs created.`) | ||
248 | |||
249 | this.reloadData() | ||
250 | }, | ||
251 | |||
252 | error: err => this.notifier.error(err.message) | ||
253 | }) | ||
254 | } | ||
229 | } | 255 | } |
diff --git a/client/src/app/shared/shared-main/video/video.model.ts b/client/src/app/shared/shared-main/video/video.model.ts index 4203ff1c0..eefa90489 100644 --- a/client/src/app/shared/shared-main/video/video.model.ts +++ b/client/src/app/shared/shared-main/video/video.model.ts | |||
@@ -220,6 +220,18 @@ export class Video implements VideoServerModel { | |||
220 | return user && this.isLocal === true && (this.account.name === user.username || user.hasRight(UserRight.UPDATE_ANY_VIDEO)) | 220 | return user && this.isLocal === true && (this.account.name === user.username || user.hasRight(UserRight.UPDATE_ANY_VIDEO)) |
221 | } | 221 | } |
222 | 222 | ||
223 | canRemoveFiles (user: AuthUser) { | ||
224 | return user.hasRight(UserRight.MANAGE_VIDEO_FILES) && | ||
225 | this.state.id !== VideoState.TO_TRANSCODE && | ||
226 | this.hasHLS() && | ||
227 | this.hasWebTorrent() | ||
228 | } | ||
229 | |||
230 | canRunTranscoding (user: AuthUser) { | ||
231 | return user.hasRight(UserRight.RUN_VIDEO_TRANSCODING) && | ||
232 | this.state.id !== VideoState.TO_TRANSCODE | ||
233 | } | ||
234 | |||
223 | hasHLS () { | 235 | hasHLS () { |
224 | return this.streamingPlaylists?.some(p => p.type === VideoStreamingPlaylistType.HLS) | 236 | return this.streamingPlaylists?.some(p => p.type === VideoStreamingPlaylistType.HLS) |
225 | } | 237 | } |
diff --git a/client/src/app/shared/shared-main/video/video.service.ts b/client/src/app/shared/shared-main/video/video.service.ts index d135a27dc..9bfa397f8 100644 --- a/client/src/app/shared/shared-main/video/video.service.ts +++ b/client/src/app/shared/shared-main/video/video.service.ts | |||
@@ -21,6 +21,7 @@ import { | |||
21 | VideoInclude, | 21 | VideoInclude, |
22 | VideoPrivacy, | 22 | VideoPrivacy, |
23 | VideoSortField, | 23 | VideoSortField, |
24 | VideoTranscodingCreate, | ||
24 | VideoUpdate | 25 | VideoUpdate |
25 | } from '@shared/models' | 26 | } from '@shared/models' |
26 | import { environment } from '../../../../environments/environment' | 27 | import { environment } from '../../../../environments/environment' |
@@ -308,6 +309,17 @@ export class VideoService { | |||
308 | ) | 309 | ) |
309 | } | 310 | } |
310 | 311 | ||
312 | runTranscoding (videoIds: (number | string)[], type: 'hls' | 'webtorrent') { | ||
313 | const body: VideoTranscodingCreate = { transcodingType: type } | ||
314 | |||
315 | return from(videoIds) | ||
316 | .pipe( | ||
317 | concatMap(id => this.authHttp.post(VideoService.BASE_VIDEO_URL + '/' + id + '/transcoding', body)), | ||
318 | toArray(), | ||
319 | catchError(err => this.restExtractor.handleError(err)) | ||
320 | ) | ||
321 | } | ||
322 | |||
311 | loadCompleteDescription (descriptionPath: string) { | 323 | loadCompleteDescription (descriptionPath: string) { |
312 | return this.authHttp | 324 | return this.authHttp |
313 | .get<{ description: string }>(environment.apiUrl + descriptionPath) | 325 | .get<{ description: string }>(environment.apiUrl + descriptionPath) |
diff --git a/client/src/app/shared/shared-video-miniature/video-actions-dropdown.component.ts b/client/src/app/shared/shared-video-miniature/video-actions-dropdown.component.ts index 82c084791..2ab9f4739 100644 --- a/client/src/app/shared/shared-video-miniature/video-actions-dropdown.component.ts +++ b/client/src/app/shared/shared-video-miniature/video-actions-dropdown.component.ts | |||
@@ -2,7 +2,7 @@ import { Component, EventEmitter, Input, OnChanges, Output, ViewChild } from '@a | |||
2 | import { AuthService, ConfirmService, Notifier, ScreenService } from '@app/core' | 2 | import { AuthService, ConfirmService, Notifier, ScreenService } from '@app/core' |
3 | import { BlocklistService, VideoBlockComponent, VideoBlockService, VideoReportComponent } from '@app/shared/shared-moderation' | 3 | import { BlocklistService, VideoBlockComponent, VideoBlockService, VideoReportComponent } from '@app/shared/shared-moderation' |
4 | import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap' | 4 | import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap' |
5 | import { UserRight, VideoCaption } from '@shared/models' | 5 | import { UserRight, VideoCaption, VideoState } from '@shared/models' |
6 | import { | 6 | import { |
7 | Actor, | 7 | Actor, |
8 | DropdownAction, | 8 | DropdownAction, |
@@ -28,6 +28,7 @@ export type VideoActionsDisplayType = { | |||
28 | mute?: boolean | 28 | mute?: boolean |
29 | liveInfo?: boolean | 29 | liveInfo?: boolean |
30 | removeFiles?: boolean | 30 | removeFiles?: boolean |
31 | transcoding?: boolean | ||
31 | } | 32 | } |
32 | 33 | ||
33 | @Component({ | 34 | @Component({ |
@@ -56,7 +57,9 @@ export class VideoActionsDropdownComponent implements OnChanges { | |||
56 | report: true, | 57 | report: true, |
57 | duplicate: true, | 58 | duplicate: true, |
58 | mute: true, | 59 | mute: true, |
59 | liveInfo: false | 60 | liveInfo: false, |
61 | removeFiles: false, | ||
62 | transcoding: false | ||
60 | } | 63 | } |
61 | @Input() placement = 'left' | 64 | @Input() placement = 'left' |
62 | 65 | ||
@@ -71,6 +74,7 @@ export class VideoActionsDropdownComponent implements OnChanges { | |||
71 | @Output() videoUnblocked = new EventEmitter() | 74 | @Output() videoUnblocked = new EventEmitter() |
72 | @Output() videoBlocked = new EventEmitter() | 75 | @Output() videoBlocked = new EventEmitter() |
73 | @Output() videoAccountMuted = new EventEmitter() | 76 | @Output() videoAccountMuted = new EventEmitter() |
77 | @Output() transcodingCreated = new EventEmitter() | ||
74 | @Output() modalOpened = new EventEmitter() | 78 | @Output() modalOpened = new EventEmitter() |
75 | 79 | ||
76 | videoActions: DropdownAction<{ video: Video }>[][] = [] | 80 | videoActions: DropdownAction<{ video: Video }>[][] = [] |
@@ -177,7 +181,11 @@ export class VideoActionsDropdownComponent implements OnChanges { | |||
177 | } | 181 | } |
178 | 182 | ||
179 | canRemoveVideoFiles () { | 183 | canRemoveVideoFiles () { |
180 | return this.user.hasRight(UserRight.MANAGE_VIDEO_FILES) && this.video.hasHLS() && this.video.hasWebTorrent() | 184 | return this.video.canRemoveFiles(this.user) |
185 | } | ||
186 | |||
187 | canRunTranscoding () { | ||
188 | return this.video.canRunTranscoding(this.user) | ||
181 | } | 189 | } |
182 | 190 | ||
183 | /* Action handlers */ | 191 | /* Action handlers */ |
@@ -268,6 +276,18 @@ export class VideoActionsDropdownComponent implements OnChanges { | |||
268 | }) | 276 | }) |
269 | } | 277 | } |
270 | 278 | ||
279 | runTranscoding (video: Video, type: 'hls' | 'webtorrent') { | ||
280 | this.videoService.runTranscoding([ video.id ], type) | ||
281 | .subscribe({ | ||
282 | next: () => { | ||
283 | this.notifier.success($localize`Transcoding jobs created for ${video.name}.`) | ||
284 | this.transcodingCreated.emit() | ||
285 | }, | ||
286 | |||
287 | error: err => this.notifier.error(err.message) | ||
288 | }) | ||
289 | } | ||
290 | |||
271 | onVideoBlocked () { | 291 | onVideoBlocked () { |
272 | this.videoBlocked.emit() | 292 | this.videoBlocked.emit() |
273 | } | 293 | } |
@@ -342,6 +362,18 @@ export class VideoActionsDropdownComponent implements OnChanges { | |||
342 | ], | 362 | ], |
343 | [ | 363 | [ |
344 | { | 364 | { |
365 | label: $localize`Run HLS transcoding`, | ||
366 | handler: ({ video }) => this.runTranscoding(video, 'hls'), | ||
367 | isDisplayed: () => this.displayOptions.transcoding && this.canRunTranscoding(), | ||
368 | iconName: 'cog' | ||
369 | }, | ||
370 | { | ||
371 | label: $localize`Run WebTorrent transcoding`, | ||
372 | handler: ({ video }) => this.runTranscoding(video, 'webtorrent'), | ||
373 | isDisplayed: () => this.displayOptions.transcoding && this.canRunTranscoding(), | ||
374 | iconName: 'cog' | ||
375 | }, | ||
376 | { | ||
345 | label: $localize`Delete HLS files`, | 377 | label: $localize`Delete HLS files`, |
346 | handler: ({ video }) => this.removeVideoFiles(video, 'hls'), | 378 | handler: ({ video }) => this.removeVideoFiles(video, 'hls'), |
347 | isDisplayed: () => this.displayOptions.removeFiles && this.canRemoveVideoFiles(), | 379 | isDisplayed: () => this.displayOptions.removeFiles && this.canRemoveVideoFiles(), |
diff --git a/scripts/create-transcoding-job.ts b/scripts/create-transcoding-job.ts index 29c398822..244c38fcd 100755 --- a/scripts/create-transcoding-job.ts +++ b/scripts/create-transcoding-job.ts | |||
@@ -5,7 +5,7 @@ import { program } from 'commander' | |||
5 | import { VideoModel } from '../server/models/video/video' | 5 | import { VideoModel } from '../server/models/video/video' |
6 | import { initDatabaseModels } from '../server/initializers/database' | 6 | import { initDatabaseModels } from '../server/initializers/database' |
7 | import { JobQueue } from '../server/lib/job-queue' | 7 | import { JobQueue } from '../server/lib/job-queue' |
8 | import { computeResolutionsToTranscode } from '@server/helpers/ffprobe-utils' | 8 | import { computeLowerResolutionsToTranscode } from '@server/helpers/ffprobe-utils' |
9 | import { VideoState, VideoTranscodingPayload } from '@shared/models' | 9 | import { VideoState, VideoTranscodingPayload } from '@shared/models' |
10 | import { CONFIG } from '@server/initializers/config' | 10 | import { CONFIG } from '@server/initializers/config' |
11 | import { isUUIDValid, toCompleteUUID } from '@server/helpers/custom-validators/misc' | 11 | import { isUUIDValid, toCompleteUUID } from '@server/helpers/custom-validators/misc' |
@@ -50,13 +50,13 @@ async function run () { | |||
50 | if (!video) throw new Error('Video not found.') | 50 | if (!video) throw new Error('Video not found.') |
51 | 51 | ||
52 | const dataInput: VideoTranscodingPayload[] = [] | 52 | const dataInput: VideoTranscodingPayload[] = [] |
53 | const resolution = video.getMaxQualityFile().resolution | 53 | const maxResolution = video.getMaxQualityFile().resolution |
54 | 54 | ||
55 | // Generate HLS files | 55 | // Generate HLS files |
56 | if (options.generateHls || CONFIG.TRANSCODING.WEBTORRENT.ENABLED === false) { | 56 | if (options.generateHls || CONFIG.TRANSCODING.WEBTORRENT.ENABLED === false) { |
57 | const resolutionsEnabled = options.resolution | 57 | const resolutionsEnabled = options.resolution |
58 | ? [ parseInt(options.resolution) ] | 58 | ? [ parseInt(options.resolution) ] |
59 | : computeResolutionsToTranscode(resolution, 'vod').concat([ resolution ]) | 59 | : computeLowerResolutionsToTranscode(maxResolution, 'vod').concat([ maxResolution ]) |
60 | 60 | ||
61 | for (const resolution of resolutionsEnabled) { | 61 | for (const resolution of resolutionsEnabled) { |
62 | dataInput.push({ | 62 | dataInput.push({ |
@@ -66,7 +66,8 @@ async function run () { | |||
66 | isPortraitMode: false, | 66 | isPortraitMode: false, |
67 | copyCodecs: false, | 67 | copyCodecs: false, |
68 | isNewVideo: false, | 68 | isNewVideo: false, |
69 | isMaxQuality: false | 69 | isMaxQuality: maxResolution === resolution, |
70 | autoDeleteWebTorrentIfNeeded: false | ||
70 | }) | 71 | }) |
71 | } | 72 | } |
72 | } else { | 73 | } else { |
diff --git a/server/controllers/api/videos/files.ts b/server/controllers/api/videos/files.ts index 2fe4b5a3f..a8b32411d 100644 --- a/server/controllers/api/videos/files.ts +++ b/server/controllers/api/videos/files.ts | |||
@@ -3,10 +3,11 @@ import toInt from 'validator/lib/toInt' | |||
3 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | 3 | import { logger, loggerTagsFactory } from '@server/helpers/logger' |
4 | import { federateVideoIfNeeded } from '@server/lib/activitypub/videos' | 4 | import { federateVideoIfNeeded } from '@server/lib/activitypub/videos' |
5 | import { VideoFileModel } from '@server/models/video/video-file' | 5 | import { VideoFileModel } from '@server/models/video/video-file' |
6 | import { HttpStatusCode } from '@shared/models' | 6 | import { HttpStatusCode, UserRight } from '@shared/models' |
7 | import { | 7 | import { |
8 | asyncMiddleware, | 8 | asyncMiddleware, |
9 | authenticate, | 9 | authenticate, |
10 | ensureUserHasRight, | ||
10 | videoFileMetadataGetValidator, | 11 | videoFileMetadataGetValidator, |
11 | videoFilesDeleteHLSValidator, | 12 | videoFilesDeleteHLSValidator, |
12 | videoFilesDeleteWebTorrentValidator | 13 | videoFilesDeleteWebTorrentValidator |
@@ -22,12 +23,14 @@ filesRouter.get('/:id/metadata/:videoFileId', | |||
22 | 23 | ||
23 | filesRouter.delete('/:id/hls', | 24 | filesRouter.delete('/:id/hls', |
24 | authenticate, | 25 | authenticate, |
26 | ensureUserHasRight(UserRight.MANAGE_VIDEO_FILES), | ||
25 | asyncMiddleware(videoFilesDeleteHLSValidator), | 27 | asyncMiddleware(videoFilesDeleteHLSValidator), |
26 | asyncMiddleware(removeHLSPlaylist) | 28 | asyncMiddleware(removeHLSPlaylist) |
27 | ) | 29 | ) |
28 | 30 | ||
29 | filesRouter.delete('/:id/webtorrent', | 31 | filesRouter.delete('/:id/webtorrent', |
30 | authenticate, | 32 | authenticate, |
33 | ensureUserHasRight(UserRight.MANAGE_VIDEO_FILES), | ||
31 | asyncMiddleware(videoFilesDeleteWebTorrentValidator), | 34 | asyncMiddleware(videoFilesDeleteWebTorrentValidator), |
32 | asyncMiddleware(removeWebTorrentFiles) | 35 | asyncMiddleware(removeWebTorrentFiles) |
33 | ) | 36 | ) |
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index 2d088a73e..fc1bcc73d 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts | |||
@@ -40,6 +40,7 @@ import { videoImportsRouter } from './import' | |||
40 | import { liveRouter } from './live' | 40 | import { liveRouter } from './live' |
41 | import { ownershipVideoRouter } from './ownership' | 41 | import { ownershipVideoRouter } from './ownership' |
42 | import { rateVideoRouter } from './rate' | 42 | import { rateVideoRouter } from './rate' |
43 | import { transcodingRouter } from './transcoding' | ||
43 | import { updateRouter } from './update' | 44 | import { updateRouter } from './update' |
44 | import { uploadRouter } from './upload' | 45 | import { uploadRouter } from './upload' |
45 | import { watchingRouter } from './watching' | 46 | import { watchingRouter } from './watching' |
@@ -58,6 +59,7 @@ videosRouter.use('/', liveRouter) | |||
58 | videosRouter.use('/', uploadRouter) | 59 | videosRouter.use('/', uploadRouter) |
59 | videosRouter.use('/', updateRouter) | 60 | videosRouter.use('/', updateRouter) |
60 | videosRouter.use('/', filesRouter) | 61 | videosRouter.use('/', filesRouter) |
62 | videosRouter.use('/', transcodingRouter) | ||
61 | 63 | ||
62 | videosRouter.get('/categories', | 64 | videosRouter.get('/categories', |
63 | openapiOperationDoc({ operationId: 'getCategories' }), | 65 | openapiOperationDoc({ operationId: 'getCategories' }), |
diff --git a/server/controllers/api/videos/transcoding.ts b/server/controllers/api/videos/transcoding.ts new file mode 100644 index 000000000..dd6fbd3de --- /dev/null +++ b/server/controllers/api/videos/transcoding.ts | |||
@@ -0,0 +1,62 @@ | |||
1 | import express from 'express' | ||
2 | import { computeLowerResolutionsToTranscode } from '@server/helpers/ffprobe-utils' | ||
3 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | ||
4 | import { addTranscodingJob } from '@server/lib/video' | ||
5 | import { HttpStatusCode, UserRight, VideoState, VideoTranscodingCreate } from '@shared/models' | ||
6 | import { asyncMiddleware, authenticate, createTranscodingValidator, ensureUserHasRight } from '../../../middlewares' | ||
7 | |||
8 | const lTags = loggerTagsFactory('api', 'video') | ||
9 | const transcodingRouter = express.Router() | ||
10 | |||
11 | transcodingRouter.post('/:videoId/transcoding', | ||
12 | authenticate, | ||
13 | ensureUserHasRight(UserRight.RUN_VIDEO_TRANSCODING), | ||
14 | asyncMiddleware(createTranscodingValidator), | ||
15 | asyncMiddleware(createTranscoding) | ||
16 | ) | ||
17 | |||
18 | // --------------------------------------------------------------------------- | ||
19 | |||
20 | export { | ||
21 | transcodingRouter | ||
22 | } | ||
23 | |||
24 | // --------------------------------------------------------------------------- | ||
25 | |||
26 | async function createTranscoding (req: express.Request, res: express.Response) { | ||
27 | const video = res.locals.videoAll | ||
28 | logger.info('Creating %s transcoding job for %s.', req.body.type, video.url, lTags()) | ||
29 | |||
30 | const body: VideoTranscodingCreate = req.body | ||
31 | |||
32 | const { resolution: maxResolution, isPortraitMode } = await video.getMaxQualityResolution() | ||
33 | const resolutions = computeLowerResolutionsToTranscode(maxResolution, 'vod').concat([ maxResolution ]) | ||
34 | |||
35 | video.state = VideoState.TO_TRANSCODE | ||
36 | await video.save() | ||
37 | |||
38 | for (const resolution of resolutions) { | ||
39 | if (body.transcodingType === 'hls') { | ||
40 | await addTranscodingJob({ | ||
41 | type: 'new-resolution-to-hls', | ||
42 | videoUUID: video.uuid, | ||
43 | resolution, | ||
44 | isPortraitMode, | ||
45 | copyCodecs: false, | ||
46 | isNewVideo: false, | ||
47 | autoDeleteWebTorrentIfNeeded: false, | ||
48 | isMaxQuality: maxResolution === resolution | ||
49 | }) | ||
50 | } else if (body.transcodingType === 'webtorrent') { | ||
51 | await addTranscodingJob({ | ||
52 | type: 'new-resolution-to-webtorrent', | ||
53 | videoUUID: video.uuid, | ||
54 | isNewVideo: false, | ||
55 | resolution: resolution, | ||
56 | isPortraitMode | ||
57 | }) | ||
58 | } | ||
59 | } | ||
60 | |||
61 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | ||
62 | } | ||
diff --git a/server/controllers/download.ts b/server/controllers/download.ts index 8da710669..43d525f83 100644 --- a/server/controllers/download.ts +++ b/server/controllers/download.ts | |||
@@ -85,7 +85,7 @@ async function downloadVideoFile (req: express.Request, res: express.Response) { | |||
85 | return res.redirect(videoFile.getObjectStorageUrl()) | 85 | return res.redirect(videoFile.getObjectStorageUrl()) |
86 | } | 86 | } |
87 | 87 | ||
88 | await VideoPathManager.Instance.makeAvailableVideoFile(video, videoFile, path => { | 88 | await VideoPathManager.Instance.makeAvailableVideoFile(videoFile.withVideoOrPlaylist(video), path => { |
89 | const filename = `${video.name}-${videoFile.resolution}p${videoFile.extname}` | 89 | const filename = `${video.name}-${videoFile.resolution}p${videoFile.extname}` |
90 | 90 | ||
91 | return res.download(path, filename) | 91 | return res.download(path, filename) |
@@ -119,7 +119,7 @@ async function downloadHLSVideoFile (req: express.Request, res: express.Response | |||
119 | return res.redirect(videoFile.getObjectStorageUrl()) | 119 | return res.redirect(videoFile.getObjectStorageUrl()) |
120 | } | 120 | } |
121 | 121 | ||
122 | await VideoPathManager.Instance.makeAvailableVideoFile(streamingPlaylist, videoFile, path => { | 122 | await VideoPathManager.Instance.makeAvailableVideoFile(videoFile.withVideoOrPlaylist(streamingPlaylist), path => { |
123 | const filename = `${video.name}-${videoFile.resolution}p-${streamingPlaylist.getStringType()}${videoFile.extname}` | 123 | const filename = `${video.name}-${videoFile.resolution}p-${streamingPlaylist.getStringType()}${videoFile.extname}` |
124 | 124 | ||
125 | return res.download(path, filename) | 125 | return res.download(path, filename) |
diff --git a/server/helpers/custom-validators/video-transcoding.ts b/server/helpers/custom-validators/video-transcoding.ts new file mode 100644 index 000000000..cf792f996 --- /dev/null +++ b/server/helpers/custom-validators/video-transcoding.ts | |||
@@ -0,0 +1,12 @@ | |||
1 | import { exists } from './misc' | ||
2 | |||
3 | function isValidCreateTranscodingType (value: any) { | ||
4 | return exists(value) && | ||
5 | (value === 'hls' || value === 'webtorrent') | ||
6 | } | ||
7 | |||
8 | // --------------------------------------------------------------------------- | ||
9 | |||
10 | export { | ||
11 | isValidCreateTranscodingType | ||
12 | } | ||
diff --git a/server/helpers/ffprobe-utils.ts b/server/helpers/ffprobe-utils.ts index 907f13651..e15628e2a 100644 --- a/server/helpers/ffprobe-utils.ts +++ b/server/helpers/ffprobe-utils.ts | |||
@@ -206,7 +206,7 @@ async function getVideoStreamFromFile (path: string, existingProbe?: FfprobeData | |||
206 | return metadata.streams.find(s => s.codec_type === 'video') || null | 206 | return metadata.streams.find(s => s.codec_type === 'video') || null |
207 | } | 207 | } |
208 | 208 | ||
209 | function computeResolutionsToTranscode (videoFileResolution: number, type: 'vod' | 'live') { | 209 | function computeLowerResolutionsToTranscode (videoFileResolution: number, type: 'vod' | 'live') { |
210 | const configResolutions = type === 'vod' | 210 | const configResolutions = type === 'vod' |
211 | ? CONFIG.TRANSCODING.RESOLUTIONS | 211 | ? CONFIG.TRANSCODING.RESOLUTIONS |
212 | : CONFIG.LIVE.TRANSCODING.RESOLUTIONS | 212 | : CONFIG.LIVE.TRANSCODING.RESOLUTIONS |
@@ -214,7 +214,7 @@ function computeResolutionsToTranscode (videoFileResolution: number, type: 'vod' | |||
214 | const resolutionsEnabled: number[] = [] | 214 | const resolutionsEnabled: number[] = [] |
215 | 215 | ||
216 | // Put in the order we want to proceed jobs | 216 | // Put in the order we want to proceed jobs |
217 | const resolutions = [ | 217 | const resolutions: VideoResolution[] = [ |
218 | VideoResolution.H_NOVIDEO, | 218 | VideoResolution.H_NOVIDEO, |
219 | VideoResolution.H_480P, | 219 | VideoResolution.H_480P, |
220 | VideoResolution.H_360P, | 220 | VideoResolution.H_360P, |
@@ -327,7 +327,7 @@ export { | |||
327 | getVideoFileFPS, | 327 | getVideoFileFPS, |
328 | ffprobePromise, | 328 | ffprobePromise, |
329 | getClosestFramerateStandard, | 329 | getClosestFramerateStandard, |
330 | computeResolutionsToTranscode, | 330 | computeLowerResolutionsToTranscode, |
331 | getVideoFileBitrate, | 331 | getVideoFileBitrate, |
332 | canDoQuickTranscode, | 332 | canDoQuickTranscode, |
333 | canDoQuickVideoTranscode, | 333 | canDoQuickVideoTranscode, |
diff --git a/server/helpers/webtorrent.ts b/server/helpers/webtorrent.ts index 5e1ea6198..c75c058e4 100644 --- a/server/helpers/webtorrent.ts +++ b/server/helpers/webtorrent.ts | |||
@@ -100,7 +100,7 @@ function createTorrentAndSetInfoHash (videoOrPlaylist: MVideo | MStreamingPlayli | |||
100 | urlList: buildUrlList(video, videoFile) | 100 | urlList: buildUrlList(video, videoFile) |
101 | } | 101 | } |
102 | 102 | ||
103 | return VideoPathManager.Instance.makeAvailableVideoFile(videoOrPlaylist, videoFile, async videoPath => { | 103 | return VideoPathManager.Instance.makeAvailableVideoFile(videoFile.withVideoOrPlaylist(videoOrPlaylist), async videoPath => { |
104 | const torrentContent = await createTorrentPromise(videoPath, options) | 104 | const torrentContent = await createTorrentPromise(videoPath, options) |
105 | 105 | ||
106 | const torrentFilename = generateTorrentFileName(videoOrPlaylist, videoFile.resolution) | 106 | const torrentFilename = generateTorrentFileName(videoOrPlaylist, videoFile.resolution) |
diff --git a/server/lib/hls.ts b/server/lib/hls.ts index 8160e7949..d969549b8 100644 --- a/server/lib/hls.ts +++ b/server/lib/hls.ts | |||
@@ -37,7 +37,7 @@ async function updateMasterHLSPlaylist (video: MVideo, playlist: MStreamingPlayl | |||
37 | for (const file of playlist.VideoFiles) { | 37 | for (const file of playlist.VideoFiles) { |
38 | const playlistFilename = getHlsResolutionPlaylistFilename(file.filename) | 38 | const playlistFilename = getHlsResolutionPlaylistFilename(file.filename) |
39 | 39 | ||
40 | await VideoPathManager.Instance.makeAvailableVideoFile(playlist, file, async videoFilePath => { | 40 | await VideoPathManager.Instance.makeAvailableVideoFile(file.withVideoOrPlaylist(playlist), async videoFilePath => { |
41 | const size = await getVideoStreamSize(videoFilePath) | 41 | const size = await getVideoStreamSize(videoFilePath) |
42 | 42 | ||
43 | const bandwidth = 'BANDWIDTH=' + video.getBandwidthBits(file) | 43 | const bandwidth = 'BANDWIDTH=' + video.getBandwidthBits(file) |
@@ -69,10 +69,11 @@ async function updateSha256VODSegments (video: MVideoUUID, playlist: MStreamingP | |||
69 | // For all the resolutions available for this video | 69 | // For all the resolutions available for this video |
70 | for (const file of playlist.VideoFiles) { | 70 | for (const file of playlist.VideoFiles) { |
71 | const rangeHashes: { [range: string]: string } = {} | 71 | const rangeHashes: { [range: string]: string } = {} |
72 | const fileWithPlaylist = file.withVideoOrPlaylist(playlist) | ||
72 | 73 | ||
73 | await VideoPathManager.Instance.makeAvailableVideoFile(playlist, file, videoPath => { | 74 | await VideoPathManager.Instance.makeAvailableVideoFile(fileWithPlaylist, videoPath => { |
74 | 75 | ||
75 | return VideoPathManager.Instance.makeAvailableResolutionPlaylistFile(playlist, file, async resolutionPlaylistPath => { | 76 | return VideoPathManager.Instance.makeAvailableResolutionPlaylistFile(fileWithPlaylist, async resolutionPlaylistPath => { |
76 | const playlistContent = await readFile(resolutionPlaylistPath) | 77 | const playlistContent = await readFile(resolutionPlaylistPath) |
77 | const ranges = getRangesFromPlaylist(playlistContent.toString()) | 78 | const ranges = getRangesFromPlaylist(playlistContent.toString()) |
78 | 79 | ||
diff --git a/server/lib/job-queue/handlers/move-to-object-storage.ts b/server/lib/job-queue/handlers/move-to-object-storage.ts index 4beca3d75..54a7c566b 100644 --- a/server/lib/job-queue/handlers/move-to-object-storage.ts +++ b/server/lib/job-queue/handlers/move-to-object-storage.ts | |||
@@ -56,16 +56,17 @@ async function moveWebTorrentFiles (video: MVideoWithAllFiles) { | |||
56 | 56 | ||
57 | async function moveHLSFiles (video: MVideoWithAllFiles) { | 57 | async function moveHLSFiles (video: MVideoWithAllFiles) { |
58 | for (const playlist of video.VideoStreamingPlaylists) { | 58 | for (const playlist of video.VideoStreamingPlaylists) { |
59 | const playlistWithVideo = playlist.withVideo(video) | ||
59 | 60 | ||
60 | for (const file of playlist.VideoFiles) { | 61 | for (const file of playlist.VideoFiles) { |
61 | if (file.storage !== VideoStorage.FILE_SYSTEM) continue | 62 | if (file.storage !== VideoStorage.FILE_SYSTEM) continue |
62 | 63 | ||
63 | // Resolution playlist | 64 | // Resolution playlist |
64 | const playlistFilename = getHlsResolutionPlaylistFilename(file.filename) | 65 | const playlistFilename = getHlsResolutionPlaylistFilename(file.filename) |
65 | await storeHLSFile(playlist, video, playlistFilename) | 66 | await storeHLSFile(playlistWithVideo, playlistFilename) |
66 | 67 | ||
67 | // Resolution fragmented file | 68 | // Resolution fragmented file |
68 | const fileUrl = await storeHLSFile(playlist, video, file.filename) | 69 | const fileUrl = await storeHLSFile(playlistWithVideo, file.filename) |
69 | 70 | ||
70 | const oldPath = join(getHLSDirectory(video), file.filename) | 71 | const oldPath = join(getHLSDirectory(video), file.filename) |
71 | 72 | ||
@@ -78,10 +79,12 @@ async function doAfterLastJob (video: MVideoWithAllFiles, isNewVideo: boolean) { | |||
78 | for (const playlist of video.VideoStreamingPlaylists) { | 79 | for (const playlist of video.VideoStreamingPlaylists) { |
79 | if (playlist.storage === VideoStorage.OBJECT_STORAGE) continue | 80 | if (playlist.storage === VideoStorage.OBJECT_STORAGE) continue |
80 | 81 | ||
82 | const playlistWithVideo = playlist.withVideo(video) | ||
83 | |||
81 | // Master playlist | 84 | // Master playlist |
82 | playlist.playlistUrl = await storeHLSFile(playlist, video, playlist.playlistFilename) | 85 | playlist.playlistUrl = await storeHLSFile(playlistWithVideo, playlist.playlistFilename) |
83 | // Sha256 segments file | 86 | // Sha256 segments file |
84 | playlist.segmentsSha256Url = await storeHLSFile(playlist, video, playlist.segmentsSha256Filename) | 87 | playlist.segmentsSha256Url = await storeHLSFile(playlistWithVideo, playlist.segmentsSha256Filename) |
85 | 88 | ||
86 | playlist.storage = VideoStorage.OBJECT_STORAGE | 89 | playlist.storage = VideoStorage.OBJECT_STORAGE |
87 | 90 | ||
diff --git a/server/lib/job-queue/handlers/video-transcoding.ts b/server/lib/job-queue/handlers/video-transcoding.ts index 904ef2e3c..2d0798e12 100644 --- a/server/lib/job-queue/handlers/video-transcoding.ts +++ b/server/lib/job-queue/handlers/video-transcoding.ts | |||
@@ -14,7 +14,7 @@ import { | |||
14 | VideoTranscodingPayload | 14 | VideoTranscodingPayload |
15 | } from '../../../../shared' | 15 | } from '../../../../shared' |
16 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | 16 | import { retryTransactionWrapper } from '../../../helpers/database-utils' |
17 | import { computeResolutionsToTranscode } from '../../../helpers/ffprobe-utils' | 17 | import { computeLowerResolutionsToTranscode } from '../../../helpers/ffprobe-utils' |
18 | import { logger, loggerTagsFactory } from '../../../helpers/logger' | 18 | import { logger, loggerTagsFactory } from '../../../helpers/logger' |
19 | import { CONFIG } from '../../../initializers/config' | 19 | import { CONFIG } from '../../../initializers/config' |
20 | import { VideoModel } from '../../../models/video/video' | 20 | import { VideoModel } from '../../../models/video/video' |
@@ -81,7 +81,7 @@ async function handleHLSJob (job: Job, payload: HLSTranscodingPayload, video: MV | |||
81 | 81 | ||
82 | const videoOrStreamingPlaylist = videoFileInput.getVideoOrStreamingPlaylist() | 82 | const videoOrStreamingPlaylist = videoFileInput.getVideoOrStreamingPlaylist() |
83 | 83 | ||
84 | await VideoPathManager.Instance.makeAvailableVideoFile(videoOrStreamingPlaylist, videoFileInput, videoInputPath => { | 84 | await VideoPathManager.Instance.makeAvailableVideoFile(videoFileInput.withVideoOrPlaylist(videoOrStreamingPlaylist), videoInputPath => { |
85 | return generateHlsPlaylistResolution({ | 85 | return generateHlsPlaylistResolution({ |
86 | video, | 86 | video, |
87 | videoInputPath, | 87 | videoInputPath, |
@@ -135,7 +135,7 @@ async function handleWebTorrentOptimizeJob (job: Job, payload: OptimizeTranscodi | |||
135 | // --------------------------------------------------------------------------- | 135 | // --------------------------------------------------------------------------- |
136 | 136 | ||
137 | async function onHlsPlaylistGeneration (video: MVideoFullLight, user: MUser, payload: HLSTranscodingPayload) { | 137 | async function onHlsPlaylistGeneration (video: MVideoFullLight, user: MUser, payload: HLSTranscodingPayload) { |
138 | if (payload.isMaxQuality && CONFIG.TRANSCODING.WEBTORRENT.ENABLED === false) { | 138 | if (payload.isMaxQuality && payload.autoDeleteWebTorrentIfNeeded && CONFIG.TRANSCODING.WEBTORRENT.ENABLED === false) { |
139 | // Remove webtorrent files if not enabled | 139 | // Remove webtorrent files if not enabled |
140 | for (const file of video.VideoFiles) { | 140 | for (const file of video.VideoFiles) { |
141 | await video.removeWebTorrentFileAndTorrent(file) | 141 | await video.removeWebTorrentFileAndTorrent(file) |
@@ -232,6 +232,7 @@ async function createHlsJobIfEnabled (user: MUserId, payload: { | |||
232 | isPortraitMode: payload.isPortraitMode, | 232 | isPortraitMode: payload.isPortraitMode, |
233 | copyCodecs: payload.copyCodecs, | 233 | copyCodecs: payload.copyCodecs, |
234 | isMaxQuality: payload.isMaxQuality, | 234 | isMaxQuality: payload.isMaxQuality, |
235 | autoDeleteWebTorrentIfNeeded: true, | ||
235 | isNewVideo: payload.isNewVideo | 236 | isNewVideo: payload.isNewVideo |
236 | } | 237 | } |
237 | 238 | ||
@@ -261,7 +262,7 @@ async function createLowerResolutionsJobs (options: { | |||
261 | const { video, user, videoFileResolution, isPortraitMode, isNewVideo, type } = options | 262 | const { video, user, videoFileResolution, isPortraitMode, isNewVideo, type } = options |
262 | 263 | ||
263 | // Create transcoding jobs if there are enabled resolutions | 264 | // Create transcoding jobs if there are enabled resolutions |
264 | const resolutionsEnabled = computeResolutionsToTranscode(videoFileResolution, 'vod') | 265 | const resolutionsEnabled = computeLowerResolutionsToTranscode(videoFileResolution, 'vod') |
265 | const resolutionCreated: string[] = [] | 266 | const resolutionCreated: string[] = [] |
266 | 267 | ||
267 | for (const resolution of resolutionsEnabled) { | 268 | for (const resolution of resolutionsEnabled) { |
@@ -288,6 +289,7 @@ async function createLowerResolutionsJobs (options: { | |||
288 | isPortraitMode, | 289 | isPortraitMode, |
289 | copyCodecs: false, | 290 | copyCodecs: false, |
290 | isMaxQuality: false, | 291 | isMaxQuality: false, |
292 | autoDeleteWebTorrentIfNeeded: true, | ||
291 | isNewVideo | 293 | isNewVideo |
292 | } | 294 | } |
293 | 295 | ||
diff --git a/server/lib/live/live-manager.ts b/server/lib/live/live-manager.ts index 2562edb75..b3bf5a999 100644 --- a/server/lib/live/live-manager.ts +++ b/server/lib/live/live-manager.ts | |||
@@ -3,7 +3,7 @@ import { readFile } from 'fs-extra' | |||
3 | import { createServer, Server } from 'net' | 3 | import { createServer, Server } from 'net' |
4 | import { createServer as createServerTLS, Server as ServerTLS } from 'tls' | 4 | import { createServer as createServerTLS, Server as ServerTLS } from 'tls' |
5 | import { | 5 | import { |
6 | computeResolutionsToTranscode, | 6 | computeLowerResolutionsToTranscode, |
7 | ffprobePromise, | 7 | ffprobePromise, |
8 | getVideoFileBitrate, | 8 | getVideoFileBitrate, |
9 | getVideoFileFPS, | 9 | getVideoFileFPS, |
@@ -402,7 +402,7 @@ class LiveManager { | |||
402 | 402 | ||
403 | private buildAllResolutionsToTranscode (originResolution: number) { | 403 | private buildAllResolutionsToTranscode (originResolution: number) { |
404 | const resolutionsEnabled = CONFIG.LIVE.TRANSCODING.ENABLED | 404 | const resolutionsEnabled = CONFIG.LIVE.TRANSCODING.ENABLED |
405 | ? computeResolutionsToTranscode(originResolution, 'live') | 405 | ? computeLowerResolutionsToTranscode(originResolution, 'live') |
406 | : [] | 406 | : [] |
407 | 407 | ||
408 | return resolutionsEnabled.concat([ originResolution ]) | 408 | return resolutionsEnabled.concat([ originResolution ]) |
diff --git a/server/lib/object-storage/keys.ts b/server/lib/object-storage/keys.ts index 12acb3aec..4f17073f4 100644 --- a/server/lib/object-storage/keys.ts +++ b/server/lib/object-storage/keys.ts | |||
@@ -1,12 +1,12 @@ | |||
1 | import { join } from 'path' | 1 | import { join } from 'path' |
2 | import { MStreamingPlaylist, MVideoUUID } from '@server/types/models' | 2 | import { MStreamingPlaylistVideo } from '@server/types/models' |
3 | 3 | ||
4 | function generateHLSObjectStorageKey (playlist: MStreamingPlaylist, video: MVideoUUID, filename: string) { | 4 | function generateHLSObjectStorageKey (playlist: MStreamingPlaylistVideo, filename: string) { |
5 | return join(generateHLSObjectBaseStorageKey(playlist, video), filename) | 5 | return join(generateHLSObjectBaseStorageKey(playlist), filename) |
6 | } | 6 | } |
7 | 7 | ||
8 | function generateHLSObjectBaseStorageKey (playlist: MStreamingPlaylist, video: MVideoUUID) { | 8 | function generateHLSObjectBaseStorageKey (playlist: MStreamingPlaylistVideo) { |
9 | return join(playlist.getStringType(), video.uuid) | 9 | return join(playlist.getStringType(), playlist.Video.uuid) |
10 | } | 10 | } |
11 | 11 | ||
12 | function generateWebTorrentObjectStorageKey (filename: string) { | 12 | function generateWebTorrentObjectStorageKey (filename: string) { |
diff --git a/server/lib/object-storage/videos.ts b/server/lib/object-storage/videos.ts index 15b8f58d5..8988f3e2a 100644 --- a/server/lib/object-storage/videos.ts +++ b/server/lib/object-storage/videos.ts | |||
@@ -1,17 +1,17 @@ | |||
1 | import { join } from 'path' | 1 | import { join } from 'path' |
2 | import { logger } from '@server/helpers/logger' | 2 | import { logger } from '@server/helpers/logger' |
3 | import { CONFIG } from '@server/initializers/config' | 3 | import { CONFIG } from '@server/initializers/config' |
4 | import { MStreamingPlaylist, MVideoFile, MVideoUUID } from '@server/types/models' | 4 | import { MStreamingPlaylistVideo, MVideoFile } from '@server/types/models' |
5 | import { getHLSDirectory } from '../paths' | 5 | import { getHLSDirectory } from '../paths' |
6 | import { generateHLSObjectBaseStorageKey, generateHLSObjectStorageKey, generateWebTorrentObjectStorageKey } from './keys' | 6 | import { generateHLSObjectBaseStorageKey, generateHLSObjectStorageKey, generateWebTorrentObjectStorageKey } from './keys' |
7 | import { lTags, makeAvailable, removeObject, removePrefix, storeObject } from './shared' | 7 | import { lTags, makeAvailable, removeObject, removePrefix, storeObject } from './shared' |
8 | 8 | ||
9 | function storeHLSFile (playlist: MStreamingPlaylist, video: MVideoUUID, filename: string) { | 9 | function storeHLSFile (playlist: MStreamingPlaylistVideo, filename: string) { |
10 | const baseHlsDirectory = getHLSDirectory(video) | 10 | const baseHlsDirectory = getHLSDirectory(playlist.Video) |
11 | 11 | ||
12 | return storeObject({ | 12 | return storeObject({ |
13 | inputPath: join(baseHlsDirectory, filename), | 13 | inputPath: join(baseHlsDirectory, filename), |
14 | objectStorageKey: generateHLSObjectStorageKey(playlist, video, filename), | 14 | objectStorageKey: generateHLSObjectStorageKey(playlist, filename), |
15 | bucketInfo: CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS | 15 | bucketInfo: CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS |
16 | }) | 16 | }) |
17 | } | 17 | } |
@@ -24,16 +24,16 @@ function storeWebTorrentFile (filename: string) { | |||
24 | }) | 24 | }) |
25 | } | 25 | } |
26 | 26 | ||
27 | function removeHLSObjectStorage (playlist: MStreamingPlaylist, video: MVideoUUID) { | 27 | function removeHLSObjectStorage (playlist: MStreamingPlaylistVideo) { |
28 | return removePrefix(generateHLSObjectBaseStorageKey(playlist, video), CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS) | 28 | return removePrefix(generateHLSObjectBaseStorageKey(playlist), CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS) |
29 | } | 29 | } |
30 | 30 | ||
31 | function removeWebTorrentObjectStorage (videoFile: MVideoFile) { | 31 | function removeWebTorrentObjectStorage (videoFile: MVideoFile) { |
32 | return removeObject(generateWebTorrentObjectStorageKey(videoFile.filename), CONFIG.OBJECT_STORAGE.VIDEOS) | 32 | return removeObject(generateWebTorrentObjectStorageKey(videoFile.filename), CONFIG.OBJECT_STORAGE.VIDEOS) |
33 | } | 33 | } |
34 | 34 | ||
35 | async function makeHLSFileAvailable (playlist: MStreamingPlaylist, video: MVideoUUID, filename: string, destination: string) { | 35 | async function makeHLSFileAvailable (playlist: MStreamingPlaylistVideo, filename: string, destination: string) { |
36 | const key = generateHLSObjectStorageKey(playlist, video, filename) | 36 | const key = generateHLSObjectStorageKey(playlist, filename) |
37 | 37 | ||
38 | logger.info('Fetching HLS file %s from object storage to %s.', key, destination, lTags()) | 38 | logger.info('Fetching HLS file %s from object storage to %s.', key, destination, lTags()) |
39 | 39 | ||
diff --git a/server/lib/thumbnail.ts b/server/lib/thumbnail.ts index d2384f53c..36270e5c1 100644 --- a/server/lib/thumbnail.ts +++ b/server/lib/thumbnail.ts | |||
@@ -115,7 +115,7 @@ function generateVideoMiniature (options: { | |||
115 | }) { | 115 | }) { |
116 | const { video, videoFile, type } = options | 116 | const { video, videoFile, type } = options |
117 | 117 | ||
118 | return VideoPathManager.Instance.makeAvailableVideoFile(video, videoFile, input => { | 118 | return VideoPathManager.Instance.makeAvailableVideoFile(videoFile.withVideoOrPlaylist(video), input => { |
119 | const { filename, basePath, height, width, existingThumbnail, outputPath } = buildMetadataFromVideo(video, type) | 119 | const { filename, basePath, height, width, existingThumbnail, outputPath } = buildMetadataFromVideo(video, type) |
120 | 120 | ||
121 | const thumbnailCreator = videoFile.isAudio() | 121 | const thumbnailCreator = videoFile.isAudio() |
diff --git a/server/lib/transcoding/video-transcoding.ts b/server/lib/transcoding/video-transcoding.ts index 250a678eb..d0db05216 100644 --- a/server/lib/transcoding/video-transcoding.ts +++ b/server/lib/transcoding/video-transcoding.ts | |||
@@ -35,7 +35,7 @@ function optimizeOriginalVideofile (video: MVideoFullLight, inputVideoFile: MVid | |||
35 | const transcodeDirectory = CONFIG.STORAGE.TMP_DIR | 35 | const transcodeDirectory = CONFIG.STORAGE.TMP_DIR |
36 | const newExtname = '.mp4' | 36 | const newExtname = '.mp4' |
37 | 37 | ||
38 | return VideoPathManager.Instance.makeAvailableVideoFile(video, inputVideoFile, async videoInputPath => { | 38 | return VideoPathManager.Instance.makeAvailableVideoFile(inputVideoFile.withVideoOrPlaylist(video), async videoInputPath => { |
39 | const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname) | 39 | const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname) |
40 | 40 | ||
41 | const transcodeType: TranscodeOptionsType = await canDoQuickTranscode(videoInputPath) | 41 | const transcodeType: TranscodeOptionsType = await canDoQuickTranscode(videoInputPath) |
@@ -81,7 +81,7 @@ function transcodeNewWebTorrentResolution (video: MVideoFullLight, resolution: V | |||
81 | const transcodeDirectory = CONFIG.STORAGE.TMP_DIR | 81 | const transcodeDirectory = CONFIG.STORAGE.TMP_DIR |
82 | const extname = '.mp4' | 82 | const extname = '.mp4' |
83 | 83 | ||
84 | return VideoPathManager.Instance.makeAvailableVideoFile(video, video.getMaxQualityFile(), async videoInputPath => { | 84 | return VideoPathManager.Instance.makeAvailableVideoFile(video.getMaxQualityFile().withVideoOrPlaylist(video), async videoInputPath => { |
85 | const newVideoFile = new VideoFileModel({ | 85 | const newVideoFile = new VideoFileModel({ |
86 | resolution, | 86 | resolution, |
87 | extname, | 87 | extname, |
@@ -134,7 +134,7 @@ function mergeAudioVideofile (video: MVideoFullLight, resolution: VideoResolutio | |||
134 | 134 | ||
135 | const inputVideoFile = video.getMinQualityFile() | 135 | const inputVideoFile = video.getMinQualityFile() |
136 | 136 | ||
137 | return VideoPathManager.Instance.makeAvailableVideoFile(video, inputVideoFile, async audioInputPath => { | 137 | return VideoPathManager.Instance.makeAvailableVideoFile(inputVideoFile.withVideoOrPlaylist(video), async audioInputPath => { |
138 | const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname) | 138 | const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname) |
139 | 139 | ||
140 | // If the user updates the video preview during transcoding | 140 | // If the user updates the video preview during transcoding |
diff --git a/server/lib/video-path-manager.ts b/server/lib/video-path-manager.ts index 4c5d0c89d..27058005c 100644 --- a/server/lib/video-path-manager.ts +++ b/server/lib/video-path-manager.ts | |||
@@ -3,7 +3,14 @@ import { extname, join } from 'path' | |||
3 | import { buildUUID } from '@server/helpers/uuid' | 3 | import { buildUUID } from '@server/helpers/uuid' |
4 | import { extractVideo } from '@server/helpers/video' | 4 | import { extractVideo } from '@server/helpers/video' |
5 | import { CONFIG } from '@server/initializers/config' | 5 | import { CONFIG } from '@server/initializers/config' |
6 | import { MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoUUID } from '@server/types/models' | 6 | import { |
7 | MStreamingPlaylistVideo, | ||
8 | MVideo, | ||
9 | MVideoFile, | ||
10 | MVideoFileStreamingPlaylistVideo, | ||
11 | MVideoFileVideo, | ||
12 | MVideoUUID | ||
13 | } from '@server/types/models' | ||
7 | import { VideoStorage } from '@shared/models' | 14 | import { VideoStorage } from '@shared/models' |
8 | import { makeHLSFileAvailable, makeWebTorrentFileAvailable } from './object-storage' | 15 | import { makeHLSFileAvailable, makeWebTorrentFileAvailable } from './object-storage' |
9 | import { getHLSDirectory, getHLSRedundancyDirectory, getHlsResolutionPlaylistFilename } from './paths' | 16 | import { getHLSDirectory, getHLSRedundancyDirectory, getHlsResolutionPlaylistFilename } from './paths' |
@@ -43,10 +50,10 @@ class VideoPathManager { | |||
43 | return join(CONFIG.STORAGE.VIDEOS_DIR, videoFile.filename) | 50 | return join(CONFIG.STORAGE.VIDEOS_DIR, videoFile.filename) |
44 | } | 51 | } |
45 | 52 | ||
46 | async makeAvailableVideoFile <T> (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, videoFile: MVideoFile, cb: MakeAvailableCB<T>) { | 53 | async makeAvailableVideoFile <T> (videoFile: MVideoFileVideo | MVideoFileStreamingPlaylistVideo, cb: MakeAvailableCB<T>) { |
47 | if (videoFile.storage === VideoStorage.FILE_SYSTEM) { | 54 | if (videoFile.storage === VideoStorage.FILE_SYSTEM) { |
48 | return this.makeAvailableFactory( | 55 | return this.makeAvailableFactory( |
49 | () => this.getFSVideoFileOutputPath(videoOrPlaylist, videoFile), | 56 | () => this.getFSVideoFileOutputPath(videoFile.getVideoOrStreamingPlaylist(), videoFile), |
50 | false, | 57 | false, |
51 | cb | 58 | cb |
52 | ) | 59 | ) |
@@ -55,10 +62,10 @@ class VideoPathManager { | |||
55 | const destination = this.buildTMPDestination(videoFile.filename) | 62 | const destination = this.buildTMPDestination(videoFile.filename) |
56 | 63 | ||
57 | if (videoFile.isHLS()) { | 64 | if (videoFile.isHLS()) { |
58 | const video = extractVideo(videoOrPlaylist) | 65 | const playlist = (videoFile as MVideoFileStreamingPlaylistVideo).VideoStreamingPlaylist |
59 | 66 | ||
60 | return this.makeAvailableFactory( | 67 | return this.makeAvailableFactory( |
61 | () => makeHLSFileAvailable(videoOrPlaylist as MStreamingPlaylistVideo, video, videoFile.filename, destination), | 68 | () => makeHLSFileAvailable(playlist, videoFile.filename, destination), |
62 | true, | 69 | true, |
63 | cb | 70 | cb |
64 | ) | 71 | ) |
@@ -71,19 +78,20 @@ class VideoPathManager { | |||
71 | ) | 78 | ) |
72 | } | 79 | } |
73 | 80 | ||
74 | async makeAvailableResolutionPlaylistFile <T> (playlist: MStreamingPlaylistVideo, videoFile: MVideoFile, cb: MakeAvailableCB<T>) { | 81 | async makeAvailableResolutionPlaylistFile <T> (videoFile: MVideoFileStreamingPlaylistVideo, cb: MakeAvailableCB<T>) { |
75 | const filename = getHlsResolutionPlaylistFilename(videoFile.filename) | 82 | const filename = getHlsResolutionPlaylistFilename(videoFile.filename) |
76 | 83 | ||
77 | if (videoFile.storage === VideoStorage.FILE_SYSTEM) { | 84 | if (videoFile.storage === VideoStorage.FILE_SYSTEM) { |
78 | return this.makeAvailableFactory( | 85 | return this.makeAvailableFactory( |
79 | () => join(getHLSDirectory(playlist.Video), filename), | 86 | () => join(getHLSDirectory(videoFile.getVideo()), filename), |
80 | false, | 87 | false, |
81 | cb | 88 | cb |
82 | ) | 89 | ) |
83 | } | 90 | } |
84 | 91 | ||
92 | const playlist = videoFile.VideoStreamingPlaylist | ||
85 | return this.makeAvailableFactory( | 93 | return this.makeAvailableFactory( |
86 | () => makeHLSFileAvailable(playlist, playlist.Video, filename, this.buildTMPDestination(filename)), | 94 | () => makeHLSFileAvailable(playlist, filename, this.buildTMPDestination(filename)), |
87 | true, | 95 | true, |
88 | cb | 96 | cb |
89 | ) | 97 | ) |
@@ -99,7 +107,7 @@ class VideoPathManager { | |||
99 | } | 107 | } |
100 | 108 | ||
101 | return this.makeAvailableFactory( | 109 | return this.makeAvailableFactory( |
102 | () => makeHLSFileAvailable(playlist, playlist.Video, filename, this.buildTMPDestination(filename)), | 110 | () => makeHLSFileAvailable(playlist, filename, this.buildTMPDestination(filename)), |
103 | true, | 111 | true, |
104 | cb | 112 | cb |
105 | ) | 113 | ) |
diff --git a/server/lib/video-state.ts b/server/lib/video-state.ts index 0b51f5c6b..bf6dd4bc8 100644 --- a/server/lib/video-state.ts +++ b/server/lib/video-state.ts | |||
@@ -80,6 +80,8 @@ async function moveToExternalStorageState (video: MVideoFullLight, isNewVideo: b | |||
80 | } | 80 | } |
81 | 81 | ||
82 | function moveToFailedTranscodingState (video: MVideoFullLight) { | 82 | function moveToFailedTranscodingState (video: MVideoFullLight) { |
83 | if (video.state === VideoState.TRANSCODING_FAILED) return | ||
84 | |||
83 | return video.setNewState(VideoState.TRANSCODING_FAILED, false, undefined) | 85 | return video.setNewState(VideoState.TRANSCODING_FAILED, false, undefined) |
84 | } | 86 | } |
85 | 87 | ||
diff --git a/server/lib/video.ts b/server/lib/video.ts index 0a2b93cc0..1cfe4f27c 100644 --- a/server/lib/video.ts +++ b/server/lib/video.ts | |||
@@ -105,7 +105,7 @@ async function addOptimizeOrMergeAudioJob (video: MVideoUUID, videoFile: MVideoF | |||
105 | return addTranscodingJob(dataInput, jobOptions) | 105 | return addTranscodingJob(dataInput, jobOptions) |
106 | } | 106 | } |
107 | 107 | ||
108 | async function addTranscodingJob (payload: VideoTranscodingPayload, options: CreateJobOptions) { | 108 | async function addTranscodingJob (payload: VideoTranscodingPayload, options: CreateJobOptions = {}) { |
109 | await VideoJobInfoModel.increaseOrCreate(payload.videoUUID, 'pendingTranscode') | 109 | await VideoJobInfoModel.increaseOrCreate(payload.videoUUID, 'pendingTranscode') |
110 | 110 | ||
111 | return JobQueue.Instance.createJobWithPromise({ type: 'video-transcoding', payload: payload }, options) | 111 | return JobQueue.Instance.createJobWithPromise({ type: 'video-transcoding', payload: payload }, options) |
diff --git a/server/middlewares/validators/videos/index.ts b/server/middlewares/validators/videos/index.ts index fd1d58093..f365d8ee1 100644 --- a/server/middlewares/validators/videos/index.ts +++ b/server/middlewares/validators/videos/index.ts | |||
@@ -9,4 +9,5 @@ export * from './video-ownership-changes' | |||
9 | export * from './video-watch' | 9 | export * from './video-watch' |
10 | export * from './video-rates' | 10 | export * from './video-rates' |
11 | export * from './video-shares' | 11 | export * from './video-shares' |
12 | export * from './video-transcoding' | ||
12 | export * from './videos' | 13 | export * from './videos' |
diff --git a/server/middlewares/validators/videos/video-files.ts b/server/middlewares/validators/videos/video-files.ts index 282594ab6..c1fa77502 100644 --- a/server/middlewares/validators/videos/video-files.ts +++ b/server/middlewares/validators/videos/video-files.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import express from 'express' | 1 | import express from 'express' |
2 | import { MUser, MVideo } from '@server/types/models' | 2 | import { MVideo } from '@server/types/models' |
3 | import { HttpStatusCode, UserRight } from '../../../../shared' | 3 | import { HttpStatusCode } from '../../../../shared' |
4 | import { logger } from '../../../helpers/logger' | 4 | import { logger } from '../../../helpers/logger' |
5 | import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared' | 5 | import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared' |
6 | 6 | ||
@@ -14,9 +14,7 @@ const videoFilesDeleteWebTorrentValidator = [ | |||
14 | if (!await doesVideoExist(req.params.id, res)) return | 14 | if (!await doesVideoExist(req.params.id, res)) return |
15 | 15 | ||
16 | const video = res.locals.videoAll | 16 | const video = res.locals.videoAll |
17 | const user = res.locals.oauth.token.User | ||
18 | 17 | ||
19 | if (!checkUserCanDeleteFiles(user, res)) return | ||
20 | if (!checkLocalVideo(video, res)) return | 18 | if (!checkLocalVideo(video, res)) return |
21 | 19 | ||
22 | if (!video.hasWebTorrentFiles()) { | 20 | if (!video.hasWebTorrentFiles()) { |
@@ -47,9 +45,7 @@ const videoFilesDeleteHLSValidator = [ | |||
47 | if (!await doesVideoExist(req.params.id, res)) return | 45 | if (!await doesVideoExist(req.params.id, res)) return |
48 | 46 | ||
49 | const video = res.locals.videoAll | 47 | const video = res.locals.videoAll |
50 | const user = res.locals.oauth.token.User | ||
51 | 48 | ||
52 | if (!checkUserCanDeleteFiles(user, res)) return | ||
53 | if (!checkLocalVideo(video, res)) return | 49 | if (!checkLocalVideo(video, res)) return |
54 | 50 | ||
55 | if (!video.getHLSPlaylist()) { | 51 | if (!video.getHLSPlaylist()) { |
@@ -89,16 +85,3 @@ function checkLocalVideo (video: MVideo, res: express.Response) { | |||
89 | 85 | ||
90 | return true | 86 | return true |
91 | } | 87 | } |
92 | |||
93 | function checkUserCanDeleteFiles (user: MUser, res: express.Response) { | ||
94 | if (user.hasRight(UserRight.MANAGE_VIDEO_FILES) !== true) { | ||
95 | res.fail({ | ||
96 | status: HttpStatusCode.FORBIDDEN_403, | ||
97 | message: 'User cannot update video files' | ||
98 | }) | ||
99 | |||
100 | return false | ||
101 | } | ||
102 | |||
103 | return true | ||
104 | } | ||
diff --git a/server/middlewares/validators/videos/video-transcoding.ts b/server/middlewares/validators/videos/video-transcoding.ts new file mode 100644 index 000000000..34f231d45 --- /dev/null +++ b/server/middlewares/validators/videos/video-transcoding.ts | |||
@@ -0,0 +1,55 @@ | |||
1 | import express from 'express' | ||
2 | import { body } from 'express-validator' | ||
3 | import { isValidCreateTranscodingType } from '@server/helpers/custom-validators/video-transcoding' | ||
4 | import { CONFIG } from '@server/initializers/config' | ||
5 | import { VideoJobInfoModel } from '@server/models/video/video-job-info' | ||
6 | import { HttpStatusCode } from '@shared/models' | ||
7 | import { logger } from '../../../helpers/logger' | ||
8 | import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared' | ||
9 | |||
10 | const createTranscodingValidator = [ | ||
11 | isValidVideoIdParam('videoId'), | ||
12 | |||
13 | body('transcodingType') | ||
14 | .custom(isValidCreateTranscodingType).withMessage('Should have a valid transcoding type'), | ||
15 | |||
16 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
17 | logger.debug('Checking createTranscodingValidator parameters', { parameters: req.body }) | ||
18 | |||
19 | if (areValidationErrors(req, res)) return | ||
20 | if (!await doesVideoExist(req.params.videoId, res, 'all')) return | ||
21 | |||
22 | const video = res.locals.videoAll | ||
23 | |||
24 | if (video.remote) { | ||
25 | return res.fail({ | ||
26 | status: HttpStatusCode.BAD_REQUEST_400, | ||
27 | message: 'Cannot run transcoding job on a remote video' | ||
28 | }) | ||
29 | } | ||
30 | |||
31 | if (CONFIG.TRANSCODING.ENABLED !== true) { | ||
32 | return res.fail({ | ||
33 | status: HttpStatusCode.BAD_REQUEST_400, | ||
34 | message: 'Cannot run transcoding job because transcoding is disabled on this instance' | ||
35 | }) | ||
36 | } | ||
37 | |||
38 | // Prefer using job info table instead of video state because before 4.0 failed transcoded video were stuck in "TO_TRANSCODE" state | ||
39 | const info = await VideoJobInfoModel.load(video.id) | ||
40 | if (info && info.pendingTranscode !== 0) { | ||
41 | return res.fail({ | ||
42 | status: HttpStatusCode.CONFLICT_409, | ||
43 | message: 'This video is already being transcoded' | ||
44 | }) | ||
45 | } | ||
46 | |||
47 | return next() | ||
48 | } | ||
49 | ] | ||
50 | |||
51 | // --------------------------------------------------------------------------- | ||
52 | |||
53 | export { | ||
54 | createTranscodingValidator | ||
55 | } | ||
diff --git a/server/models/video/formatter/video-format-utils.ts b/server/models/video/formatter/video-format-utils.ts index 461e296df..fd4da68ed 100644 --- a/server/models/video/formatter/video-format-utils.ts +++ b/server/models/video/formatter/video-format-utils.ts | |||
@@ -2,8 +2,7 @@ import { uuidToShort } from '@server/helpers/uuid' | |||
2 | import { generateMagnetUri } from '@server/helpers/webtorrent' | 2 | import { generateMagnetUri } from '@server/helpers/webtorrent' |
3 | import { getLocalVideoFileMetadataUrl } from '@server/lib/video-urls' | 3 | import { getLocalVideoFileMetadataUrl } from '@server/lib/video-urls' |
4 | import { VideoViews } from '@server/lib/video-views' | 4 | import { VideoViews } from '@server/lib/video-views' |
5 | import { VideosCommonQueryAfterSanitize } from '@shared/models' | 5 | import { VideoFile, VideosCommonQueryAfterSanitize } from '@shared/models' |
6 | import { VideoFile } from '@shared/models/videos/video-file.model' | ||
7 | import { ActivityTagObject, ActivityUrlObject, VideoObject } from '../../../../shared/models/activitypub/objects' | 6 | import { ActivityTagObject, ActivityUrlObject, VideoObject } from '../../../../shared/models/activitypub/objects' |
8 | import { Video, VideoDetails, VideoInclude } from '../../../../shared/models/videos' | 7 | import { Video, VideoDetails, VideoInclude } from '../../../../shared/models/videos' |
9 | import { VideoStreamingPlaylist } from '../../../../shared/models/videos/video-streaming-playlist.model' | 8 | import { VideoStreamingPlaylist } from '../../../../shared/models/videos/video-streaming-playlist.model' |
diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts index 106f9602b..87311c0ed 100644 --- a/server/models/video/video-file.ts +++ b/server/models/video/video-file.ts | |||
@@ -25,7 +25,7 @@ import { logger } from '@server/helpers/logger' | |||
25 | import { extractVideo } from '@server/helpers/video' | 25 | import { extractVideo } from '@server/helpers/video' |
26 | import { getHLSPublicFileUrl, getWebTorrentPublicFileUrl } from '@server/lib/object-storage' | 26 | import { getHLSPublicFileUrl, getWebTorrentPublicFileUrl } from '@server/lib/object-storage' |
27 | import { getFSTorrentFilePath } from '@server/lib/paths' | 27 | import { getFSTorrentFilePath } from '@server/lib/paths' |
28 | import { MStreamingPlaylistVideo, MVideo, MVideoWithHost } from '@server/types/models' | 28 | import { isStreamingPlaylist, MStreamingPlaylistVideo, MVideo, MVideoWithHost } from '@server/types/models' |
29 | import { AttributesOnly } from '@shared/core-utils' | 29 | import { AttributesOnly } from '@shared/core-utils' |
30 | import { VideoStorage } from '@shared/models' | 30 | import { VideoStorage } from '@shared/models' |
31 | import { | 31 | import { |
@@ -536,4 +536,10 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel> | |||
536 | (this.videoStreamingPlaylistId !== null && this.videoStreamingPlaylistId === other.videoStreamingPlaylistId) | 536 | (this.videoStreamingPlaylistId !== null && this.videoStreamingPlaylistId === other.videoStreamingPlaylistId) |
537 | ) | 537 | ) |
538 | } | 538 | } |
539 | |||
540 | withVideoOrPlaylist (videoOrPlaylist: MVideo | MStreamingPlaylistVideo) { | ||
541 | if (isStreamingPlaylist(videoOrPlaylist)) return Object.assign(this, { VideoStreamingPlaylist: videoOrPlaylist }) | ||
542 | |||
543 | return Object.assign(this, { Video: videoOrPlaylist }) | ||
544 | } | ||
539 | } | 545 | } |
diff --git a/server/models/video/video-job-info.ts b/server/models/video/video-job-info.ts index 7c1fe6734..cb1f3f2f0 100644 --- a/server/models/video/video-job-info.ts +++ b/server/models/video/video-job-info.ts | |||
@@ -49,7 +49,7 @@ export class VideoJobInfoModel extends Model<Partial<AttributesOnly<VideoJobInfo | |||
49 | }) | 49 | }) |
50 | Video: VideoModel | 50 | Video: VideoModel |
51 | 51 | ||
52 | static load (videoId: number, transaction: Transaction) { | 52 | static load (videoId: number, transaction?: Transaction) { |
53 | const where = { | 53 | const where = { |
54 | videoId | 54 | videoId |
55 | } | 55 | } |
diff --git a/server/models/video/video-streaming-playlist.ts b/server/models/video/video-streaming-playlist.ts index 18d96c750..4643c5452 100644 --- a/server/models/video/video-streaming-playlist.ts +++ b/server/models/video/video-streaming-playlist.ts | |||
@@ -239,6 +239,10 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi | |||
239 | this.videoId === other.videoId | 239 | this.videoId === other.videoId |
240 | } | 240 | } |
241 | 241 | ||
242 | withVideo (video: MVideo) { | ||
243 | return Object.assign(this, { Video: video }) | ||
244 | } | ||
245 | |||
242 | private getMasterPlaylistStaticPath (videoUUID: string) { | 246 | private getMasterPlaylistStaticPath (videoUUID: string) { |
243 | return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, this.playlistFilename) | 247 | return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, this.playlistFilename) |
244 | } | 248 | } |
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 6eeb6b312..c49df1d5e 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -33,9 +33,8 @@ import { getHLSDirectory, getHLSRedundancyDirectory } from '@server/lib/paths' | |||
33 | import { VideoPathManager } from '@server/lib/video-path-manager' | 33 | import { VideoPathManager } from '@server/lib/video-path-manager' |
34 | import { getServerActor } from '@server/models/application/application' | 34 | import { getServerActor } from '@server/models/application/application' |
35 | import { ModelCache } from '@server/models/model-cache' | 35 | import { ModelCache } from '@server/models/model-cache' |
36 | import { AttributesOnly, buildVideoEmbedPath, buildVideoWatchPath, pick } from '@shared/core-utils' | 36 | import { AttributesOnly, buildVideoEmbedPath, buildVideoWatchPath, isThisWeek, pick } from '@shared/core-utils' |
37 | import { VideoInclude } from '@shared/models' | 37 | import { VideoFile, VideoInclude } from '@shared/models' |
38 | import { VideoFile } from '@shared/models/videos/video-file.model' | ||
39 | import { ResultList, UserRight, VideoPrivacy, VideoState } from '../../../shared' | 38 | import { ResultList, UserRight, VideoPrivacy, VideoState } from '../../../shared' |
40 | import { VideoObject } from '../../../shared/models/activitypub/objects' | 39 | import { VideoObject } from '../../../shared/models/activitypub/objects' |
41 | import { Video, VideoDetails, VideoRateType, VideoStorage } from '../../../shared/models/videos' | 40 | import { Video, VideoDetails, VideoRateType, VideoStorage } from '../../../shared/models/videos' |
@@ -1673,7 +1672,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1673 | const file = this.getMaxQualityFile() | 1672 | const file = this.getMaxQualityFile() |
1674 | const videoOrPlaylist = file.getVideoOrStreamingPlaylist() | 1673 | const videoOrPlaylist = file.getVideoOrStreamingPlaylist() |
1675 | 1674 | ||
1676 | return VideoPathManager.Instance.makeAvailableVideoFile(videoOrPlaylist, file, originalFilePath => { | 1675 | return VideoPathManager.Instance.makeAvailableVideoFile(file.withVideoOrPlaylist(videoOrPlaylist), originalFilePath => { |
1677 | return getVideoFileResolution(originalFilePath) | 1676 | return getVideoFileResolution(originalFilePath) |
1678 | }) | 1677 | }) |
1679 | } | 1678 | } |
@@ -1742,7 +1741,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1742 | ) | 1741 | ) |
1743 | 1742 | ||
1744 | if (streamingPlaylist.storage === VideoStorage.OBJECT_STORAGE) { | 1743 | if (streamingPlaylist.storage === VideoStorage.OBJECT_STORAGE) { |
1745 | await removeHLSObjectStorage(streamingPlaylist, this) | 1744 | await removeHLSObjectStorage(streamingPlaylist.withVideo(this)) |
1746 | } | 1745 | } |
1747 | } | 1746 | } |
1748 | } | 1747 | } |
diff --git a/server/tests/api/check-params/index.ts b/server/tests/api/check-params/index.ts index ff7dc4abb..e052296db 100644 --- a/server/tests/api/check-params/index.ts +++ b/server/tests/api/check-params/index.ts | |||
@@ -15,6 +15,7 @@ import './plugins' | |||
15 | import './redundancy' | 15 | import './redundancy' |
16 | import './search' | 16 | import './search' |
17 | import './services' | 17 | import './services' |
18 | import './transcoding' | ||
18 | import './upload-quota' | 19 | import './upload-quota' |
19 | import './user-notifications' | 20 | import './user-notifications' |
20 | import './user-subscriptions' | 21 | import './user-subscriptions' |
diff --git a/server/tests/api/check-params/transcoding.ts b/server/tests/api/check-params/transcoding.ts new file mode 100644 index 000000000..a8daafe3e --- /dev/null +++ b/server/tests/api/check-params/transcoding.ts | |||
@@ -0,0 +1,104 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import 'mocha' | ||
4 | import { cleanupTests, createMultipleServers, doubleFollow, PeerTubeServer, setAccessTokensToServers, waitJobs } from '@shared/extra-utils' | ||
5 | import { HttpStatusCode, UserRole } from '@shared/models' | ||
6 | |||
7 | describe('Test transcoding API validators', function () { | ||
8 | let servers: PeerTubeServer[] | ||
9 | |||
10 | let userToken: string | ||
11 | let moderatorToken: string | ||
12 | |||
13 | let remoteId: string | ||
14 | let validId: string | ||
15 | |||
16 | // --------------------------------------------------------------- | ||
17 | |||
18 | before(async function () { | ||
19 | this.timeout(60000) | ||
20 | |||
21 | servers = await createMultipleServers(2) | ||
22 | await setAccessTokensToServers(servers) | ||
23 | |||
24 | await doubleFollow(servers[0], servers[1]) | ||
25 | |||
26 | userToken = await servers[0].users.generateUserAndToken('user', UserRole.USER) | ||
27 | moderatorToken = await servers[0].users.generateUserAndToken('moderator', UserRole.MODERATOR) | ||
28 | |||
29 | { | ||
30 | const { uuid } = await servers[1].videos.quickUpload({ name: 'remote video' }) | ||
31 | remoteId = uuid | ||
32 | } | ||
33 | |||
34 | { | ||
35 | const { uuid } = await servers[0].videos.quickUpload({ name: 'both 1' }) | ||
36 | validId = uuid | ||
37 | } | ||
38 | |||
39 | await waitJobs(servers) | ||
40 | |||
41 | await servers[0].config.enableTranscoding() | ||
42 | }) | ||
43 | |||
44 | it('Should not run transcoding of a unknown video', async function () { | ||
45 | await servers[0].videos.runTranscoding({ videoId: 404, transcodingType: 'hls', expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
46 | await servers[0].videos.runTranscoding({ videoId: 404, transcodingType: 'webtorrent', expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
47 | }) | ||
48 | |||
49 | it('Should not run transcoding of a remote video', async function () { | ||
50 | const expectedStatus = HttpStatusCode.BAD_REQUEST_400 | ||
51 | |||
52 | await servers[0].videos.runTranscoding({ videoId: remoteId, transcodingType: 'hls', expectedStatus }) | ||
53 | await servers[0].videos.runTranscoding({ videoId: remoteId, transcodingType: 'webtorrent', expectedStatus }) | ||
54 | }) | ||
55 | |||
56 | it('Should not run transcoding by a non admin user', async function () { | ||
57 | const expectedStatus = HttpStatusCode.FORBIDDEN_403 | ||
58 | |||
59 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'hls', token: userToken, expectedStatus }) | ||
60 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'webtorrent', token: moderatorToken, expectedStatus }) | ||
61 | }) | ||
62 | |||
63 | it('Should not run transcoding without transcoding type', async function () { | ||
64 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: undefined, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
65 | }) | ||
66 | |||
67 | it('Should not run transcoding with an incorrect transcoding type', async function () { | ||
68 | const expectedStatus = HttpStatusCode.BAD_REQUEST_400 | ||
69 | |||
70 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'toto' as any, expectedStatus }) | ||
71 | }) | ||
72 | |||
73 | it('Should not run transcoding if the instance disabled it', async function () { | ||
74 | const expectedStatus = HttpStatusCode.BAD_REQUEST_400 | ||
75 | |||
76 | await servers[0].config.disableTranscoding() | ||
77 | |||
78 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'hls', expectedStatus }) | ||
79 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'webtorrent', expectedStatus }) | ||
80 | }) | ||
81 | |||
82 | it('Should run transcoding', async function () { | ||
83 | this.timeout(120_000) | ||
84 | |||
85 | await servers[0].config.enableTranscoding() | ||
86 | |||
87 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'hls' }) | ||
88 | await waitJobs(servers) | ||
89 | |||
90 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'webtorrent' }) | ||
91 | await waitJobs(servers) | ||
92 | }) | ||
93 | |||
94 | it('Should not run transcoding on a video that is already being transcoded', async function () { | ||
95 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'webtorrent' }) | ||
96 | |||
97 | const expectedStatus = HttpStatusCode.CONFLICT_409 | ||
98 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'webtorrent', expectedStatus }) | ||
99 | }) | ||
100 | |||
101 | after(async function () { | ||
102 | await cleanupTests(servers) | ||
103 | }) | ||
104 | }) | ||
diff --git a/server/tests/api/check-params/video-files.ts b/server/tests/api/check-params/video-files.ts index 48b10d2b5..61936d562 100644 --- a/server/tests/api/check-params/video-files.ts +++ b/server/tests/api/check-params/video-files.ts | |||
@@ -1,16 +1,19 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | 1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ |
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import { cleanupTests, createMultipleServers, PeerTubeServer, setAccessTokensToServers, waitJobs } from '@shared/extra-utils' | 4 | import { cleanupTests, createMultipleServers, doubleFollow, PeerTubeServer, setAccessTokensToServers, waitJobs } from '@shared/extra-utils' |
5 | import { HttpStatusCode, UserRole } from '@shared/models' | 5 | import { HttpStatusCode, UserRole } from '@shared/models' |
6 | 6 | ||
7 | describe('Test videos files', function () { | 7 | describe('Test videos files', function () { |
8 | let servers: PeerTubeServer[] | 8 | let servers: PeerTubeServer[] |
9 | |||
9 | let webtorrentId: string | 10 | let webtorrentId: string |
10 | let hlsId: string | 11 | let hlsId: string |
11 | let remoteId: string | 12 | let remoteId: string |
13 | |||
12 | let userToken: string | 14 | let userToken: string |
13 | let moderatorToken: string | 15 | let moderatorToken: string |
16 | |||
14 | let validId1: string | 17 | let validId1: string |
15 | let validId2: string | 18 | let validId2: string |
16 | 19 | ||
@@ -22,10 +25,17 @@ describe('Test videos files', function () { | |||
22 | servers = await createMultipleServers(2) | 25 | servers = await createMultipleServers(2) |
23 | await setAccessTokensToServers(servers) | 26 | await setAccessTokensToServers(servers) |
24 | 27 | ||
28 | await doubleFollow(servers[0], servers[1]) | ||
29 | |||
25 | userToken = await servers[0].users.generateUserAndToken('user', UserRole.USER) | 30 | userToken = await servers[0].users.generateUserAndToken('user', UserRole.USER) |
26 | moderatorToken = await servers[0].users.generateUserAndToken('moderator', UserRole.MODERATOR) | 31 | moderatorToken = await servers[0].users.generateUserAndToken('moderator', UserRole.MODERATOR) |
27 | 32 | ||
28 | { | 33 | { |
34 | const { uuid } = await servers[1].videos.quickUpload({ name: 'remote video' }) | ||
35 | remoteId = uuid | ||
36 | } | ||
37 | |||
38 | { | ||
29 | await servers[0].config.enableTranscoding(true, true) | 39 | await servers[0].config.enableTranscoding(true, true) |
30 | 40 | ||
31 | { | 41 | { |
@@ -58,6 +68,11 @@ describe('Test videos files', function () { | |||
58 | await waitJobs(servers) | 68 | await waitJobs(servers) |
59 | }) | 69 | }) |
60 | 70 | ||
71 | it('Should not delete files of a unknown video', async function () { | ||
72 | await servers[0].videos.removeHLSFiles({ videoId: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
73 | await servers[0].videos.removeWebTorrentFiles({ videoId: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
74 | }) | ||
75 | |||
61 | it('Should not delete files of a remote video', async function () { | 76 | it('Should not delete files of a remote video', async function () { |
62 | await servers[0].videos.removeHLSFiles({ videoId: remoteId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | 77 | await servers[0].videos.removeHLSFiles({ videoId: remoteId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) |
63 | await servers[0].videos.removeWebTorrentFiles({ videoId: remoteId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | 78 | await servers[0].videos.removeWebTorrentFiles({ videoId: remoteId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) |
diff --git a/server/tests/api/videos/index.ts b/server/tests/api/videos/index.ts index f92e339e7..bedb9b8b6 100644 --- a/server/tests/api/videos/index.ts +++ b/server/tests/api/videos/index.ts | |||
@@ -6,6 +6,7 @@ import './video-captions' | |||
6 | import './video-change-ownership' | 6 | import './video-change-ownership' |
7 | import './video-channels' | 7 | import './video-channels' |
8 | import './video-comments' | 8 | import './video-comments' |
9 | import './video-create-transcoding' | ||
9 | import './video-description' | 10 | import './video-description' |
10 | import './video-files' | 11 | import './video-files' |
11 | import './video-hls' | 12 | import './video-hls' |
diff --git a/server/tests/api/videos/video-create-transcoding.ts b/server/tests/api/videos/video-create-transcoding.ts new file mode 100644 index 000000000..bae06ac6c --- /dev/null +++ b/server/tests/api/videos/video-create-transcoding.ts | |||
@@ -0,0 +1,156 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import 'mocha' | ||
4 | import * as chai from 'chai' | ||
5 | import { | ||
6 | areObjectStorageTestsDisabled, | ||
7 | cleanupTests, | ||
8 | createMultipleServers, | ||
9 | doubleFollow, | ||
10 | expectStartWith, | ||
11 | makeRawRequest, | ||
12 | ObjectStorageCommand, | ||
13 | PeerTubeServer, | ||
14 | setAccessTokensToServers, | ||
15 | waitJobs | ||
16 | } from '@shared/extra-utils' | ||
17 | import { HttpStatusCode, VideoDetails } from '@shared/models' | ||
18 | |||
19 | const expect = chai.expect | ||
20 | |||
21 | async function checkFilesInObjectStorage (video: VideoDetails) { | ||
22 | for (const file of video.files) { | ||
23 | expectStartWith(file.fileUrl, ObjectStorageCommand.getWebTorrentBaseUrl()) | ||
24 | await makeRawRequest(file.fileUrl, HttpStatusCode.OK_200) | ||
25 | } | ||
26 | |||
27 | for (const file of video.streamingPlaylists[0].files) { | ||
28 | expectStartWith(file.fileUrl, ObjectStorageCommand.getPlaylistBaseUrl()) | ||
29 | await makeRawRequest(file.fileUrl, HttpStatusCode.OK_200) | ||
30 | } | ||
31 | } | ||
32 | |||
33 | async function expectNoFailedTranscodingJob (server: PeerTubeServer) { | ||
34 | const { data } = await server.jobs.listFailed({ jobType: 'video-transcoding' }) | ||
35 | expect(data).to.have.lengthOf(0) | ||
36 | } | ||
37 | |||
38 | function runTests (objectStorage: boolean) { | ||
39 | let servers: PeerTubeServer[] = [] | ||
40 | let videoUUID: string | ||
41 | let publishedAt: string | ||
42 | |||
43 | before(async function () { | ||
44 | this.timeout(120000) | ||
45 | |||
46 | const config = objectStorage | ||
47 | ? ObjectStorageCommand.getDefaultConfig() | ||
48 | : {} | ||
49 | |||
50 | // Run server 2 to have transcoding enabled | ||
51 | servers = await createMultipleServers(2, config) | ||
52 | await setAccessTokensToServers(servers) | ||
53 | |||
54 | await servers[0].config.disableTranscoding() | ||
55 | |||
56 | await doubleFollow(servers[0], servers[1]) | ||
57 | |||
58 | if (objectStorage) await ObjectStorageCommand.prepareDefaultBuckets() | ||
59 | |||
60 | const { shortUUID } = await servers[0].videos.quickUpload({ name: 'video' }) | ||
61 | videoUUID = shortUUID | ||
62 | |||
63 | const video = await servers[0].videos.get({ id: videoUUID }) | ||
64 | publishedAt = video.publishedAt as string | ||
65 | |||
66 | await servers[0].config.enableTranscoding() | ||
67 | |||
68 | await waitJobs(servers) | ||
69 | }) | ||
70 | |||
71 | it('Should generate HLS', async function () { | ||
72 | this.timeout(60000) | ||
73 | |||
74 | await servers[0].videos.runTranscoding({ | ||
75 | videoId: videoUUID, | ||
76 | transcodingType: 'hls' | ||
77 | }) | ||
78 | |||
79 | await waitJobs(servers) | ||
80 | await expectNoFailedTranscodingJob(servers[0]) | ||
81 | |||
82 | for (const server of servers) { | ||
83 | const videoDetails = await server.videos.get({ id: videoUUID }) | ||
84 | |||
85 | expect(videoDetails.files).to.have.lengthOf(1) | ||
86 | expect(videoDetails.streamingPlaylists).to.have.lengthOf(1) | ||
87 | expect(videoDetails.streamingPlaylists[0].files).to.have.lengthOf(5) | ||
88 | |||
89 | if (objectStorage) await checkFilesInObjectStorage(videoDetails) | ||
90 | } | ||
91 | }) | ||
92 | |||
93 | it('Should generate WebTorrent', async function () { | ||
94 | this.timeout(60000) | ||
95 | |||
96 | await servers[0].videos.runTranscoding({ | ||
97 | videoId: videoUUID, | ||
98 | transcodingType: 'webtorrent' | ||
99 | }) | ||
100 | |||
101 | await waitJobs(servers) | ||
102 | |||
103 | for (const server of servers) { | ||
104 | const videoDetails = await server.videos.get({ id: videoUUID }) | ||
105 | |||
106 | expect(videoDetails.files).to.have.lengthOf(5) | ||
107 | expect(videoDetails.streamingPlaylists).to.have.lengthOf(1) | ||
108 | expect(videoDetails.streamingPlaylists[0].files).to.have.lengthOf(5) | ||
109 | |||
110 | if (objectStorage) await checkFilesInObjectStorage(videoDetails) | ||
111 | } | ||
112 | }) | ||
113 | |||
114 | it('Should generate WebTorrent from HLS only video', async function () { | ||
115 | this.timeout(60000) | ||
116 | |||
117 | await servers[0].videos.removeWebTorrentFiles({ videoId: videoUUID }) | ||
118 | await waitJobs(servers) | ||
119 | |||
120 | await servers[0].videos.runTranscoding({ videoId: videoUUID, transcodingType: 'webtorrent' }) | ||
121 | await waitJobs(servers) | ||
122 | |||
123 | for (const server of servers) { | ||
124 | const videoDetails = await server.videos.get({ id: videoUUID }) | ||
125 | |||
126 | expect(videoDetails.files).to.have.lengthOf(5) | ||
127 | expect(videoDetails.streamingPlaylists).to.have.lengthOf(1) | ||
128 | expect(videoDetails.streamingPlaylists[0].files).to.have.lengthOf(5) | ||
129 | |||
130 | if (objectStorage) await checkFilesInObjectStorage(videoDetails) | ||
131 | } | ||
132 | }) | ||
133 | |||
134 | it('Should not have updated published at attributes', async function () { | ||
135 | const video = await servers[0].videos.get({ id: videoUUID }) | ||
136 | |||
137 | expect(video.publishedAt).to.equal(publishedAt) | ||
138 | }) | ||
139 | |||
140 | after(async function () { | ||
141 | await cleanupTests(servers) | ||
142 | }) | ||
143 | } | ||
144 | |||
145 | describe('Test create transcoding jobs from API', function () { | ||
146 | |||
147 | describe('On filesystem', function () { | ||
148 | runTests(false) | ||
149 | }) | ||
150 | |||
151 | describe('On object storage', function () { | ||
152 | if (areObjectStorageTestsDisabled()) return | ||
153 | |||
154 | runTests(true) | ||
155 | }) | ||
156 | }) | ||
diff --git a/shared/extra-utils/server/jobs-command.ts b/shared/extra-utils/server/jobs-command.ts index f28397816..6636e7e4d 100644 --- a/shared/extra-utils/server/jobs-command.ts +++ b/shared/extra-utils/server/jobs-command.ts | |||
@@ -36,6 +36,21 @@ export class JobsCommand extends AbstractCommand { | |||
36 | }) | 36 | }) |
37 | } | 37 | } |
38 | 38 | ||
39 | listFailed (options: OverrideCommandOptions & { | ||
40 | jobType?: JobType | ||
41 | }) { | ||
42 | const path = this.buildJobsUrl('failed') | ||
43 | |||
44 | return this.getRequestBody<ResultList<Job>>({ | ||
45 | ...options, | ||
46 | |||
47 | path, | ||
48 | query: { start: 0, count: 50 }, | ||
49 | implicitToken: true, | ||
50 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
51 | }) | ||
52 | } | ||
53 | |||
39 | private buildJobsUrl (state?: JobState) { | 54 | private buildJobsUrl (state?: JobState) { |
40 | let path = '/api/v1/jobs' | 55 | let path = '/api/v1/jobs' |
41 | 56 | ||
diff --git a/shared/extra-utils/videos/videos-command.ts b/shared/extra-utils/videos/videos-command.ts index 13a7d0e1c..7ec9c3647 100644 --- a/shared/extra-utils/videos/videos-command.ts +++ b/shared/extra-utils/videos/videos-command.ts | |||
@@ -18,7 +18,8 @@ import { | |||
18 | VideoDetails, | 18 | VideoDetails, |
19 | VideoFileMetadata, | 19 | VideoFileMetadata, |
20 | VideoPrivacy, | 20 | VideoPrivacy, |
21 | VideosCommonQuery | 21 | VideosCommonQuery, |
22 | VideoTranscodingCreate | ||
22 | } from '@shared/models' | 23 | } from '@shared/models' |
23 | import { buildAbsoluteFixturePath, wait } from '../miscs' | 24 | import { buildAbsoluteFixturePath, wait } from '../miscs' |
24 | import { unwrapBody } from '../requests' | 25 | import { unwrapBody } from '../requests' |
@@ -630,6 +631,24 @@ export class VideosCommand extends AbstractCommand { | |||
630 | }) | 631 | }) |
631 | } | 632 | } |
632 | 633 | ||
634 | runTranscoding (options: OverrideCommandOptions & { | ||
635 | videoId: number | string | ||
636 | transcodingType: 'hls' | 'webtorrent' | ||
637 | }) { | ||
638 | const path = '/api/v1/videos/' + options.videoId + '/transcoding' | ||
639 | |||
640 | const fields: VideoTranscodingCreate = pick(options, [ 'transcodingType' ]) | ||
641 | |||
642 | return this.postBodyRequest({ | ||
643 | ...options, | ||
644 | |||
645 | path, | ||
646 | fields, | ||
647 | implicitToken: true, | ||
648 | defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
649 | }) | ||
650 | } | ||
651 | |||
633 | // --------------------------------------------------------------------------- | 652 | // --------------------------------------------------------------------------- |
634 | 653 | ||
635 | private buildListQuery (options: VideosCommonQuery) { | 654 | private buildListQuery (options: VideosCommonQuery) { |
diff --git a/shared/models/plugins/server/managers/plugin-transcoding-manager.model.ts b/shared/models/plugins/server/managers/plugin-transcoding-manager.model.ts index a0422a460..b6fb46ba0 100644 --- a/shared/models/plugins/server/managers/plugin-transcoding-manager.model.ts +++ b/shared/models/plugins/server/managers/plugin-transcoding-manager.model.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { EncoderOptionsBuilder } from '../../../videos/video-transcoding.model' | 1 | import { EncoderOptionsBuilder } from '../../../videos/transcoding' |
2 | 2 | ||
3 | export interface PluginTranscodingManager { | 3 | export interface PluginTranscodingManager { |
4 | addLiveProfile (encoder: string, profile: string, builder: EncoderOptionsBuilder): boolean | 4 | addLiveProfile (encoder: string, profile: string, builder: EncoderOptionsBuilder): boolean |
diff --git a/shared/models/server/job.model.ts b/shared/models/server/job.model.ts index 6da2753b3..ecc960da5 100644 --- a/shared/models/server/job.model.ts +++ b/shared/models/server/job.model.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { ContextType } from '../activitypub/context' | 1 | import { ContextType } from '../activitypub/context' |
2 | import { VideoResolution } from '../videos/video-resolution.enum' | 2 | import { VideoResolution } from '../videos/file/video-resolution.enum' |
3 | import { SendEmailOptions } from './emailer.model' | 3 | import { SendEmailOptions } from './emailer.model' |
4 | 4 | ||
5 | export type JobState = 'active' | 'completed' | 'failed' | 'waiting' | 'delayed' | 'paused' | 5 | export type JobState = 'active' | 'completed' | 'failed' | 'waiting' | 'delayed' | 'paused' |
@@ -106,6 +106,8 @@ export interface HLSTranscodingPayload extends BaseTranscodingPayload { | |||
106 | isPortraitMode?: boolean | 106 | isPortraitMode?: boolean |
107 | resolution: VideoResolution | 107 | resolution: VideoResolution |
108 | copyCodecs: boolean | 108 | copyCodecs: boolean |
109 | |||
110 | autoDeleteWebTorrentIfNeeded: boolean | ||
109 | isMaxQuality: boolean | 111 | isMaxQuality: boolean |
110 | } | 112 | } |
111 | 113 | ||
diff --git a/shared/models/users/user-right.enum.ts b/shared/models/users/user-right.enum.ts index 96bccaf2f..6415ca6f2 100644 --- a/shared/models/users/user-right.enum.ts +++ b/shared/models/users/user-right.enum.ts | |||
@@ -40,5 +40,6 @@ export const enum UserRight { | |||
40 | 40 | ||
41 | MANAGE_VIDEOS_REDUNDANCIES, | 41 | MANAGE_VIDEOS_REDUNDANCIES, |
42 | 42 | ||
43 | MANAGE_VIDEO_FILES | 43 | MANAGE_VIDEO_FILES, |
44 | RUN_VIDEO_TRANSCODING | ||
44 | } | 45 | } |
diff --git a/shared/models/videos/file/index.ts b/shared/models/videos/file/index.ts new file mode 100644 index 000000000..78a784a3c --- /dev/null +++ b/shared/models/videos/file/index.ts | |||
@@ -0,0 +1,3 @@ | |||
1 | export * from './video-file-metadata.model' | ||
2 | export * from './video-file.model' | ||
3 | export * from './video-resolution.enum' | ||
diff --git a/shared/models/videos/video-file-metadata.model.ts b/shared/models/videos/file/video-file-metadata.model.ts index 8f527c0a7..8f527c0a7 100644 --- a/shared/models/videos/video-file-metadata.model.ts +++ b/shared/models/videos/file/video-file-metadata.model.ts | |||
diff --git a/shared/models/videos/video-file.model.ts b/shared/models/videos/file/video-file.model.ts index 28fce0aaf..0ea857e7a 100644 --- a/shared/models/videos/video-file.model.ts +++ b/shared/models/videos/file/video-file.model.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { VideoConstant } from './video-constant.model' | 1 | import { VideoConstant } from '../video-constant.model' |
2 | import { VideoFileMetadata } from './video-file-metadata.model' | 2 | import { VideoFileMetadata } from './video-file-metadata.model' |
3 | import { VideoResolution } from './video-resolution.enum' | 3 | import { VideoResolution } from './video-resolution.enum' |
4 | 4 | ||
diff --git a/shared/models/videos/video-resolution.enum.ts b/shared/models/videos/file/video-resolution.enum.ts index 5b48ad353..5b48ad353 100644 --- a/shared/models/videos/video-resolution.enum.ts +++ b/shared/models/videos/file/video-resolution.enum.ts | |||
diff --git a/shared/models/videos/index.ts b/shared/models/videos/index.ts index 3d3eedcc6..67614efc9 100644 --- a/shared/models/videos/index.ts +++ b/shared/models/videos/index.ts | |||
@@ -4,9 +4,11 @@ export * from './change-ownership' | |||
4 | export * from './channel' | 4 | export * from './channel' |
5 | export * from './comment' | 5 | export * from './comment' |
6 | export * from './live' | 6 | export * from './live' |
7 | export * from './file' | ||
7 | export * from './import' | 8 | export * from './import' |
8 | export * from './playlist' | 9 | export * from './playlist' |
9 | export * from './rate' | 10 | export * from './rate' |
11 | export * from './transcoding' | ||
10 | 12 | ||
11 | export * from './nsfw-policy.type' | 13 | export * from './nsfw-policy.type' |
12 | 14 | ||
@@ -15,14 +17,10 @@ export * from './thumbnail.type' | |||
15 | export * from './video-constant.model' | 17 | export * from './video-constant.model' |
16 | export * from './video-create.model' | 18 | export * from './video-create.model' |
17 | 19 | ||
18 | export * from './video-file-metadata.model' | ||
19 | export * from './video-file.model' | ||
20 | |||
21 | export * from './video-privacy.enum' | 20 | export * from './video-privacy.enum' |
22 | export * from './video-filter.type' | 21 | export * from './video-filter.type' |
23 | export * from './video-include.enum' | 22 | export * from './video-include.enum' |
24 | export * from './video-rate.type' | 23 | export * from './video-rate.type' |
25 | export * from './video-resolution.enum' | ||
26 | 24 | ||
27 | export * from './video-schedule-update.model' | 25 | export * from './video-schedule-update.model' |
28 | export * from './video-sort-field.type' | 26 | export * from './video-sort-field.type' |
@@ -32,9 +30,6 @@ export * from './video-storage.enum' | |||
32 | export * from './video-streaming-playlist.model' | 30 | export * from './video-streaming-playlist.model' |
33 | export * from './video-streaming-playlist.type' | 31 | export * from './video-streaming-playlist.type' |
34 | 32 | ||
35 | export * from './video-transcoding.model' | ||
36 | export * from './video-transcoding-fps.model' | ||
37 | |||
38 | export * from './video-update.model' | 33 | export * from './video-update.model' |
39 | export * from './video.model' | 34 | export * from './video.model' |
40 | export * from './video-create-result.model' | 35 | export * from './video-create-result.model' |
diff --git a/shared/models/videos/transcoding/index.ts b/shared/models/videos/transcoding/index.ts new file mode 100644 index 000000000..14472d900 --- /dev/null +++ b/shared/models/videos/transcoding/index.ts | |||
@@ -0,0 +1,3 @@ | |||
1 | export * from './video-transcoding-create.model' | ||
2 | export * from './video-transcoding-fps.model' | ||
3 | export * from './video-transcoding.model' | ||
diff --git a/shared/models/videos/transcoding/video-transcoding-create.model.ts b/shared/models/videos/transcoding/video-transcoding-create.model.ts new file mode 100644 index 000000000..aeb393e57 --- /dev/null +++ b/shared/models/videos/transcoding/video-transcoding-create.model.ts | |||
@@ -0,0 +1,3 @@ | |||
1 | export interface VideoTranscodingCreate { | ||
2 | transcodingType: 'hls' | 'webtorrent' | ||
3 | } | ||
diff --git a/shared/models/videos/video-transcoding-fps.model.ts b/shared/models/videos/transcoding/video-transcoding-fps.model.ts index 25fc1c2da..25fc1c2da 100644 --- a/shared/models/videos/video-transcoding-fps.model.ts +++ b/shared/models/videos/transcoding/video-transcoding-fps.model.ts | |||
diff --git a/shared/models/videos/video-transcoding.model.ts b/shared/models/videos/transcoding/video-transcoding.model.ts index 83b8e98a0..3a7fb6472 100644 --- a/shared/models/videos/video-transcoding.model.ts +++ b/shared/models/videos/transcoding/video-transcoding.model.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { VideoResolution } from './video-resolution.enum' | 1 | import { VideoResolution } from '../file/video-resolution.enum' |
2 | 2 | ||
3 | // Types used by plugins and ffmpeg-utils | 3 | // Types used by plugins and ffmpeg-utils |
4 | 4 | ||
diff --git a/shared/models/videos/video-streaming-playlist.model.ts b/shared/models/videos/video-streaming-playlist.model.ts index b547a0ac7..11919a4ee 100644 --- a/shared/models/videos/video-streaming-playlist.model.ts +++ b/shared/models/videos/video-streaming-playlist.model.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { VideoStreamingPlaylistType } from './video-streaming-playlist.type' | 1 | import { VideoStreamingPlaylistType } from './video-streaming-playlist.type' |
2 | import { VideoFile } from './video-file.model' | 2 | import { VideoFile } from './file' |
3 | 3 | ||
4 | export interface VideoStreamingPlaylist { | 4 | export interface VideoStreamingPlaylist { |
5 | id: number | 5 | id: number |
diff --git a/shared/models/videos/video.model.ts b/shared/models/videos/video.model.ts index 8d223cded..f98eed012 100644 --- a/shared/models/videos/video.model.ts +++ b/shared/models/videos/video.model.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { Account, AccountSummary } from '../actors' | 1 | import { Account, AccountSummary } from '../actors' |
2 | import { VideoChannel, VideoChannelSummary } from './channel/video-channel.model' | 2 | import { VideoChannel, VideoChannelSummary } from './channel/video-channel.model' |
3 | import { VideoFile } from './file' | ||
3 | import { VideoConstant } from './video-constant.model' | 4 | import { VideoConstant } from './video-constant.model' |
4 | import { VideoFile } from './video-file.model' | ||
5 | import { VideoPrivacy } from './video-privacy.enum' | 5 | import { VideoPrivacy } from './video-privacy.enum' |
6 | import { VideoScheduleUpdate } from './video-schedule-update.model' | 6 | import { VideoScheduleUpdate } from './video-schedule-update.model' |
7 | import { VideoState } from './video-state.enum' | 7 | import { VideoState } from './video-state.enum' |
diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml index 88a089fc7..cfba7b361 100644 --- a/support/doc/api/openapi.yaml +++ b/support/doc/api/openapi.yaml | |||
@@ -267,6 +267,10 @@ tags: | |||
267 | description: Like/dislike a video. | 267 | description: Like/dislike a video. |
268 | - name: Video Playlists | 268 | - name: Video Playlists |
269 | description: Operations dealing with playlists of videos. Playlists are bound to users and/or channels. | 269 | description: Operations dealing with playlists of videos. Playlists are bound to users and/or channels. |
270 | - name: Video Files | ||
271 | description: Operations on video files | ||
272 | - name: Video Transcoding | ||
273 | description: Video transcoding related operations | ||
270 | - name: Feeds | 274 | - name: Feeds |
271 | description: Server syndication feeds | 275 | description: Server syndication feeds |
272 | - name: Search | 276 | - name: Search |
@@ -309,6 +313,8 @@ x-tagGroups: | |||
309 | - Video Playlists | 313 | - Video Playlists |
310 | - Video Ownership Change | 314 | - Video Ownership Change |
311 | - Video Mirroring | 315 | - Video Mirroring |
316 | - Video Files | ||
317 | - Video Transcoding | ||
312 | - Live Videos | 318 | - Live Videos |
313 | - Feeds | 319 | - Feeds |
314 | - name: Search | 320 | - name: Search |
@@ -3568,6 +3574,69 @@ paths: | |||
3568 | '404': | 3574 | '404': |
3569 | description: video does not exist | 3575 | description: video does not exist |
3570 | 3576 | ||
3577 | '/videos/{id}/hls': | ||
3578 | delete: | ||
3579 | summary: Delete video HLS files | ||
3580 | security: | ||
3581 | - OAuth2: | ||
3582 | - admin | ||
3583 | tags: | ||
3584 | - Video Files | ||
3585 | operationId: delVideoHLS | ||
3586 | parameters: | ||
3587 | - $ref: '#/components/parameters/idOrUUID' | ||
3588 | responses: | ||
3589 | '204': | ||
3590 | description: successful operation | ||
3591 | '404': | ||
3592 | description: video does not exist | ||
3593 | '/videos/{id}/webtorrent': | ||
3594 | delete: | ||
3595 | summary: Delete video WebTorrent files | ||
3596 | security: | ||
3597 | - OAuth2: | ||
3598 | - admin | ||
3599 | tags: | ||
3600 | - Video Files | ||
3601 | operationId: delVideoWebTorrent | ||
3602 | parameters: | ||
3603 | - $ref: '#/components/parameters/idOrUUID' | ||
3604 | responses: | ||
3605 | '204': | ||
3606 | description: successful operation | ||
3607 | '404': | ||
3608 | description: video does not exist | ||
3609 | |||
3610 | '/videos/{id}/transcoding': | ||
3611 | post: | ||
3612 | summary: Create a transcoding job | ||
3613 | security: | ||
3614 | - OAuth2: | ||
3615 | - admin | ||
3616 | tags: | ||
3617 | - Video Transcoding | ||
3618 | operationId: createVideoTranscoding | ||
3619 | parameters: | ||
3620 | - $ref: '#/components/parameters/idOrUUID' | ||
3621 | requestBody: | ||
3622 | content: | ||
3623 | application/json: | ||
3624 | schema: | ||
3625 | type: object | ||
3626 | properties: | ||
3627 | transcodingType: | ||
3628 | type: string | ||
3629 | enum: | ||
3630 | - hls | ||
3631 | - webtorrent | ||
3632 | required: | ||
3633 | - transcodingType | ||
3634 | responses: | ||
3635 | '204': | ||
3636 | description: successful operation | ||
3637 | '404': | ||
3638 | description: video does not exist | ||
3639 | |||
3571 | /search/videos: | 3640 | /search/videos: |
3572 | get: | 3641 | get: |
3573 | tags: | 3642 | tags: |