aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html33
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts5
-rw-r--r--client/src/app/shared/form-validators/custom-config-validators.ts8
-rw-r--r--config/default.yaml6
-rw-r--r--config/production.yaml.example6
-rw-r--r--config/test.yaml2
-rwxr-xr-xscripts/create-transcoding-job.ts3
-rw-r--r--server/controllers/api/config.ts2
-rw-r--r--server/initializers/checker-after-init.ts10
-rw-r--r--server/initializers/checker-before-init.ts4
-rw-r--r--server/initializers/config.ts3
-rw-r--r--server/initializers/constants.ts4
-rw-r--r--server/lib/job-queue/handlers/video-transcoding.ts44
-rw-r--r--server/lib/job-queue/job-queue.ts12
-rw-r--r--server/lib/video-transcoding.ts4
-rw-r--r--server/middlewares/validators/config.ts2
-rw-r--r--server/models/video/video.ts4
-rw-r--r--server/tests/api/check-params/config.ts2
-rw-r--r--server/tests/api/server/config.ts6
-rw-r--r--shared/extra-utils/server/config.ts2
-rw-r--r--shared/models/server/custom-config.model.ts3
-rw-r--r--shared/models/server/job.model.ts1
22 files changed, 133 insertions, 33 deletions
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
index 48678a194..5f0a5ff6c 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
@@ -436,8 +436,25 @@
436 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9"> 436 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
437 437
438 <ng-container formGroupName="import"> 438 <ng-container formGroupName="import">
439
439 <ng-container formGroupName="videos"> 440 <ng-container formGroupName="videos">
440 441
442 <div class="form-group mt-4">
443 <label i18n for="importConcurrency">Import jobs concurrency</label>
444 <span class="text-muted ml-1">
445 <span i18n>allows to import multiple videos in parallel. ⚠️ Requires a PeerTube restart.</span>
446 </span>
447
448 <div class="peertube-select-container">
449 <select id="importConcurrency" formControlName="concurrency" class="form-control">
450 <option *ngFor="let option of concurrencyOptions" [value]="option">
451 {{ option }}
452 </option>
453 </select>
454 </div>
455 <div *ngIf="formErrors.import.concurrency" class="form-error">{{ formErrors.import.concurrency }}</div>
456 </div>
457
441 <div class="form-group" formGroupName="http"> 458 <div class="form-group" formGroupName="http">
442 <my-peertube-checkbox 459 <my-peertube-checkbox
443 inputName="importVideosHttpEnabled" formControlName="enabled" 460 inputName="importVideosHttpEnabled" formControlName="enabled"
@@ -886,6 +903,22 @@
886 </div> 903 </div>
887 904
888 <div class="form-group mt-4" [ngClass]="{ 'disabled-checkbox-extra': !isTranscodingEnabled() }"> 905 <div class="form-group mt-4" [ngClass]="{ 'disabled-checkbox-extra': !isTranscodingEnabled() }">
906 <label i18n for="transcodingConcurrency">Transcoding jobs concurrency</label>
907 <span class="text-muted ml-1">
908 <span i18n>allows to transcode multiple files in parallel. ⚠️ Requires a PeerTube restart.</span>
909 </span>
910
911 <div class="peertube-select-container">
912 <select id="transcodingConcurrency" formControlName="concurrency" class="form-control">
913 <option *ngFor="let option of concurrencyOptions" [value]="option">
914 {{ option }}
915 </option>
916 </select>
917 </div>
918 <div *ngIf="formErrors.transcoding.concurrency" class="form-error">{{ formErrors.transcoding.concurrency }}</div>
919 </div>
920
921 <div class="form-group mt-4" [ngClass]="{ 'disabled-checkbox-extra': !isTranscodingEnabled() }">
889 <label i18n for="transcodingProfile">Transcoding profile</label> 922 <label i18n for="transcodingProfile">Transcoding profile</label>
890 <span class="text-muted ml-1" i18n>new transcoding profiles can be added by PeerTube plugins</span> 923 <span class="text-muted ml-1" i18n>new transcoding profiles can be added by PeerTube plugins</span>
891 924
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
index ae6a9e844..48fb86968 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
@@ -9,6 +9,7 @@ import {
9 ADMIN_EMAIL_VALIDATOR, 9 ADMIN_EMAIL_VALIDATOR,
10 CACHE_CAPTIONS_SIZE_VALIDATOR, 10 CACHE_CAPTIONS_SIZE_VALIDATOR,
11 CACHE_PREVIEWS_SIZE_VALIDATOR, 11 CACHE_PREVIEWS_SIZE_VALIDATOR,
12 CONCURRENCY_VALIDATOR,
12 INDEX_URL_VALIDATOR, 13 INDEX_URL_VALIDATOR,
13 INSTANCE_NAME_VALIDATOR, 14 INSTANCE_NAME_VALIDATOR,
14 INSTANCE_SHORT_DESCRIPTION_VALIDATOR, 15 INSTANCE_SHORT_DESCRIPTION_VALIDATOR,
@@ -36,6 +37,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit, A
36 37
37 resolutions: { id: string, label: string, description?: string }[] = [] 38 resolutions: { id: string, label: string, description?: string }[] = []
38 liveResolutions: { id: string, label: string, description?: string }[] = [] 39 liveResolutions: { id: string, label: string, description?: string }[] = []
40 concurrencyOptions: number[] = []
39 transcodingThreadOptions: { label: string, value: number }[] = [] 41 transcodingThreadOptions: { label: string, value: number }[] = []
40 liveMaxDurationOptions: { label: string, value: number }[] = [] 42 liveMaxDurationOptions: { label: string, value: number }[] = []
41 43
@@ -103,6 +105,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit, A
103 { value: 4, label: '4' }, 105 { value: 4, label: '4' },
104 { value: 8, label: '8' } 106 { value: 8, label: '8' }
105 ] 107 ]
108 this.concurrencyOptions = [ 1, 2, 3, 4, 5, 6 ]
106 109
107 this.vodTranscodingProfileOptions = [ 'default' ] 110 this.vodTranscodingProfileOptions = [ 'default' ]
108 this.liveTranscodingProfileOptions = [ 'default' ] 111 this.liveTranscodingProfileOptions = [ 'default' ]
@@ -230,6 +233,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit, A
230 }, 233 },
231 import: { 234 import: {
232 videos: { 235 videos: {
236 concurrency: CONCURRENCY_VALIDATOR,
233 http: { 237 http: {
234 enabled: null 238 enabled: null
235 }, 239 },
@@ -262,6 +266,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit, A
262 allowAdditionalExtensions: null, 266 allowAdditionalExtensions: null,
263 allowAudioFiles: null, 267 allowAudioFiles: null,
264 profile: null, 268 profile: null,
269 concurrency: CONCURRENCY_VALIDATOR,
265 resolutions: {}, 270 resolutions: {},
266 hls: { 271 hls: {
267 enabled: null 272 enabled: null
diff --git a/client/src/app/shared/form-validators/custom-config-validators.ts b/client/src/app/shared/form-validators/custom-config-validators.ts
index 41b3cbba9..23f2156c2 100644
--- a/client/src/app/shared/form-validators/custom-config-validators.ts
+++ b/client/src/app/shared/form-validators/custom-config-validators.ts
@@ -65,6 +65,14 @@ export const TRANSCODING_THREADS_VALIDATOR: BuildFormValidator = {
65 } 65 }
66} 66}
67 67
68export const CONCURRENCY_VALIDATOR: BuildFormValidator = {
69 VALIDATORS: [Validators.required, Validators.min(1)],
70 MESSAGES: {
71 'required': $localize`Concurrency is required.`,
72 'min': $localize`Concurrency should be greater or equal to 1.`
73 }
74}
75
68export const INDEX_URL_VALIDATOR: BuildFormValidator = { 76export const INDEX_URL_VALIDATOR: BuildFormValidator = {
69 VALIDATORS: [Validators.pattern(/^https:\/\//)], 77 VALIDATORS: [Validators.pattern(/^https:\/\//)],
70 MESSAGES: { 78 MESSAGES: {
diff --git a/config/default.yaml b/config/default.yaml
index 95df2e06c..3bbb3e5c4 100644
--- a/config/default.yaml
+++ b/config/default.yaml
@@ -233,7 +233,10 @@ transcoding:
233 # If a user uploads an audio file, PeerTube will create a video by merging the preview file and the audio file 233 # If a user uploads an audio file, PeerTube will create a video by merging the preview file and the audio file
234 allow_audio_files: true 234 allow_audio_files: true
235 235
236 # Amount of threads used by ffmpeg for 1 transcoding job
236 threads: 1 237 threads: 1
238 # Amount of transcoding jobs to execute in parallel
239 concurrency: 1
237 240
238 # Choose the transcoding profile 241 # Choose the transcoding profile
239 # New profiles can be added by plugins 242 # New profiles can be added by plugins
@@ -312,6 +315,9 @@ live:
312import: 315import:
313 # Add ability for your users to import remote videos (from YouTube, torrent...) 316 # Add ability for your users to import remote videos (from YouTube, torrent...)
314 videos: 317 videos:
318 # Amount of import jobs to execute in parallel
319 concurrency: 1
320
315 http: # Classic HTTP or all sites supported by youtube-dl https://rg3.github.io/youtube-dl/supportedsites.html 321 http: # Classic HTTP or all sites supported by youtube-dl https://rg3.github.io/youtube-dl/supportedsites.html
316 enabled: false 322 enabled: false
317 323
diff --git a/config/production.yaml.example b/config/production.yaml.example
index 13a646918..d75e30276 100644
--- a/config/production.yaml.example
+++ b/config/production.yaml.example
@@ -244,7 +244,10 @@ transcoding:
244 # If a user uploads an audio file, PeerTube will create a video by merging the preview file and the audio file 244 # If a user uploads an audio file, PeerTube will create a video by merging the preview file and the audio file
245 allow_audio_files: true 245 allow_audio_files: true
246 246
247 # Amount of threads used by ffmpeg for 1 transcoding job
247 threads: 1 248 threads: 1
249 # Amount of transcoding jobs to execute in parallel
250 concurrency: 1
248 251
249 # Choose the transcoding profile 252 # Choose the transcoding profile
250 # New profiles can be added by plugins 253 # New profiles can be added by plugins
@@ -323,6 +326,9 @@ live:
323import: 326import:
324 # Add ability for your users to import remote videos (from YouTube, torrent...) 327 # Add ability for your users to import remote videos (from YouTube, torrent...)
325 videos: 328 videos:
329 # Amount of import jobs to execute in parallel
330 concurrency: 1
331
326 http: # Classic HTTP or all sites supported by youtube-dl https://rg3.github.io/youtube-dl/supportedsites.html 332 http: # Classic HTTP or all sites supported by youtube-dl https://rg3.github.io/youtube-dl/supportedsites.html
327 enabled: false 333 enabled: false
328 334
diff --git a/config/test.yaml b/config/test.yaml
index 6fedb1dc3..ae8011ba5 100644
--- a/config/test.yaml
+++ b/config/test.yaml
@@ -71,6 +71,7 @@ transcoding:
71 allow_additional_extensions: false 71 allow_additional_extensions: false
72 allow_audio_files: false 72 allow_audio_files: false
73 threads: 2 73 threads: 2
74 concurrency: 2
74 resolutions: 75 resolutions:
75 0p: false 76 0p: false
76 240p: true 77 240p: true
@@ -106,6 +107,7 @@ live:
106 107
107import: 108import:
108 videos: 109 videos:
110 concurrency: 2
109 http: 111 http:
110 enabled: true 112 enabled: true
111 proxy: 113 proxy:
diff --git a/scripts/create-transcoding-job.ts b/scripts/create-transcoding-job.ts
index eb620aeca..5f56d6d36 100755
--- a/scripts/create-transcoding-job.ts
+++ b/scripts/create-transcoding-job.ts
@@ -53,7 +53,8 @@ async function run () {
53 videoUUID: video.uuid, 53 videoUUID: video.uuid,
54 resolution, 54 resolution,
55 isPortraitMode: false, 55 isPortraitMode: false,
56 copyCodecs: false 56 copyCodecs: false,
57 isMaxQuality: false
57 }) 58 })
58 } 59 }
59 } else if (options.resolution !== undefined) { 60 } else if (options.resolution !== undefined) {
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts
index 7fda06a87..5c242da04 100644
--- a/server/controllers/api/config.ts
+++ b/server/controllers/api/config.ts
@@ -417,6 +417,7 @@ function customConfig (): CustomConfig {
417 allowAdditionalExtensions: CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS, 417 allowAdditionalExtensions: CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS,
418 allowAudioFiles: CONFIG.TRANSCODING.ALLOW_AUDIO_FILES, 418 allowAudioFiles: CONFIG.TRANSCODING.ALLOW_AUDIO_FILES,
419 threads: CONFIG.TRANSCODING.THREADS, 419 threads: CONFIG.TRANSCODING.THREADS,
420 concurrency: CONFIG.TRANSCODING.CONCURRENCY,
420 profile: CONFIG.TRANSCODING.PROFILE, 421 profile: CONFIG.TRANSCODING.PROFILE,
421 resolutions: { 422 resolutions: {
422 '0p': CONFIG.TRANSCODING.RESOLUTIONS['0p'], 423 '0p': CONFIG.TRANSCODING.RESOLUTIONS['0p'],
@@ -458,6 +459,7 @@ function customConfig (): CustomConfig {
458 }, 459 },
459 import: { 460 import: {
460 videos: { 461 videos: {
462 concurrency: CONFIG.IMPORT.VIDEOS.CONCURRENCY,
461 http: { 463 http: {
462 enabled: CONFIG.IMPORT.VIDEOS.HTTP.ENABLED 464 enabled: CONFIG.IMPORT.VIDEOS.HTTP.ENABLED
463 }, 465 },
diff --git a/server/initializers/checker-after-init.ts b/server/initializers/checker-after-init.ts
index 979c97a8b..2b00e2047 100644
--- a/server/initializers/checker-after-init.ts
+++ b/server/initializers/checker-after-init.ts
@@ -116,6 +116,16 @@ function checkConfig () {
116 if (CONFIG.TRANSCODING.WEBTORRENT.ENABLED === false && CONFIG.TRANSCODING.HLS.ENABLED === false) { 116 if (CONFIG.TRANSCODING.WEBTORRENT.ENABLED === false && CONFIG.TRANSCODING.HLS.ENABLED === false) {
117 return 'You need to enable at least WebTorrent transcoding or HLS transcoding.' 117 return 'You need to enable at least WebTorrent transcoding or HLS transcoding.'
118 } 118 }
119
120 if (CONFIG.TRANSCODING.CONCURRENCY <= 0) {
121 return 'Transcoding concurrency should be > 0'
122 }
123 }
124
125 if (CONFIG.IMPORT.VIDEOS.HTTP.ENABLED || CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED) {
126 if (CONFIG.IMPORT.VIDEOS.CONCURRENCY <= 0) {
127 return 'Video import concurrency should be > 0'
128 }
119 } 129 }
120 130
121 // Broadcast message 131 // Broadcast message
diff --git a/server/initializers/checker-before-init.ts b/server/initializers/checker-before-init.ts
index cac24f0b6..a186afbdd 100644
--- a/server/initializers/checker-before-init.ts
+++ b/server/initializers/checker-before-init.ts
@@ -22,10 +22,10 @@ function checkMissedConfig () {
22 'signup.filters.cidr.whitelist', 'signup.filters.cidr.blacklist', 22 'signup.filters.cidr.whitelist', 'signup.filters.cidr.blacklist',
23 'redundancy.videos.strategies', 'redundancy.videos.check_interval', 23 'redundancy.videos.strategies', 'redundancy.videos.check_interval',
24 'transcoding.enabled', 'transcoding.threads', 'transcoding.allow_additional_extensions', 'transcoding.hls.enabled', 24 'transcoding.enabled', 'transcoding.threads', 'transcoding.allow_additional_extensions', 'transcoding.hls.enabled',
25 'transcoding.profile', 25 'transcoding.profile', 'transcoding.concurrency',
26 'transcoding.resolutions.0p', 'transcoding.resolutions.240p', 'transcoding.resolutions.360p', 'transcoding.resolutions.480p', 26 'transcoding.resolutions.0p', 'transcoding.resolutions.240p', 'transcoding.resolutions.360p', 'transcoding.resolutions.480p',
27 'transcoding.resolutions.720p', 'transcoding.resolutions.1080p', 'transcoding.resolutions.1440p', 'transcoding.resolutions.2160p', 27 'transcoding.resolutions.720p', 'transcoding.resolutions.1080p', 'transcoding.resolutions.1440p', 'transcoding.resolutions.2160p',
28 'import.videos.http.enabled', 'import.videos.torrent.enabled', 'auto_blacklist.videos.of_users.enabled', 28 'import.videos.http.enabled', 'import.videos.torrent.enabled', 'import.videos.concurrency', 'auto_blacklist.videos.of_users.enabled',
29 'trending.videos.interval_days', 29 'trending.videos.interval_days',
30 'instance.name', 'instance.short_description', 'instance.description', 'instance.terms', 'instance.default_client_route', 30 'instance.name', 'instance.short_description', 'instance.description', 'instance.terms', 'instance.default_client_route',
31 'instance.is_nsfw', 'instance.default_nsfw_policy', 'instance.robots', 'instance.securitytxt', 31 'instance.is_nsfw', 'instance.default_nsfw_policy', 'instance.robots', 'instance.securitytxt',
diff --git a/server/initializers/config.ts b/server/initializers/config.ts
index 3b44d82ed..930fd784e 100644
--- a/server/initializers/config.ts
+++ b/server/initializers/config.ts
@@ -188,6 +188,7 @@ const CONFIG = {
188 get ALLOW_ADDITIONAL_EXTENSIONS () { return config.get<boolean>('transcoding.allow_additional_extensions') }, 188 get ALLOW_ADDITIONAL_EXTENSIONS () { return config.get<boolean>('transcoding.allow_additional_extensions') },
189 get ALLOW_AUDIO_FILES () { return config.get<boolean>('transcoding.allow_audio_files') }, 189 get ALLOW_AUDIO_FILES () { return config.get<boolean>('transcoding.allow_audio_files') },
190 get THREADS () { return config.get<number>('transcoding.threads') }, 190 get THREADS () { return config.get<number>('transcoding.threads') },
191 get CONCURRENCY () { return config.get<number>('transcoding.concurrency') },
191 get PROFILE () { return config.get<string>('transcoding.profile') }, 192 get PROFILE () { return config.get<string>('transcoding.profile') },
192 RESOLUTIONS: { 193 RESOLUTIONS: {
193 get '0p' () { return config.get<boolean>('transcoding.resolutions.0p') }, 194 get '0p' () { return config.get<boolean>('transcoding.resolutions.0p') },
@@ -237,6 +238,8 @@ const CONFIG = {
237 }, 238 },
238 IMPORT: { 239 IMPORT: {
239 VIDEOS: { 240 VIDEOS: {
241 get CONCURRENCY () { return config.get<number>('import.videos.concurrency') },
242
240 HTTP: { 243 HTTP: {
241 get ENABLED () { return config.get<boolean>('import.videos.http.enabled') }, 244 get ENABLED () { return config.get<boolean>('import.videos.http.enabled') },
242 get FORCE_IPV4 () { return config.get<boolean>('import.videos.http.force_ipv4') }, 245 get FORCE_IPV4 () { return config.get<boolean>('import.videos.http.force_ipv4') },
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 9d9b3966c..7beaca238 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -146,14 +146,12 @@ const JOB_ATTEMPTS: { [id in JobType]: number } = {
146 'video-redundancy': 1, 146 'video-redundancy': 1,
147 'video-live-ending': 1 147 'video-live-ending': 1
148} 148}
149const JOB_CONCURRENCY: { [id in JobType]: number } = { 149const JOB_CONCURRENCY: { [id in JobType]?: number } = {
150 'activitypub-http-broadcast': 1, 150 'activitypub-http-broadcast': 1,
151 'activitypub-http-unicast': 5, 151 'activitypub-http-unicast': 5,
152 'activitypub-http-fetcher': 1, 152 'activitypub-http-fetcher': 1,
153 'activitypub-follow': 1, 153 'activitypub-follow': 1,
154 'video-file-import': 1, 154 'video-file-import': 1,
155 'video-transcoding': 1,
156 'video-import': 1,
157 'email': 5, 155 'email': 5,
158 'videos-views': 1, 156 'videos-views': 1,
159 'activitypub-refresher': 1, 157 'activitypub-refresher': 1,
diff --git a/server/lib/job-queue/handlers/video-transcoding.ts b/server/lib/job-queue/handlers/video-transcoding.ts
index 4718a7d5c..e248b645e 100644
--- a/server/lib/job-queue/handlers/video-transcoding.ts
+++ b/server/lib/job-queue/handlers/video-transcoding.ts
@@ -76,7 +76,7 @@ async function processVideoTranscoding (job: Bull.Job) {
76// Job handlers 76// Job handlers
77// --------------------------------------------------------------------------- 77// ---------------------------------------------------------------------------
78 78
79async function handleHLSJob (job: Bull.Job, payload: HLSTranscodingPayload, video: MVideoFullLight) { 79async function handleHLSJob (job: Bull.Job, payload: HLSTranscodingPayload, video: MVideoFullLight, user: MUser) {
80 const videoFileInput = payload.copyCodecs 80 const videoFileInput = payload.copyCodecs
81 ? video.getWebTorrentFile(payload.resolution) 81 ? video.getWebTorrentFile(payload.resolution)
82 : video.getMaxQualityFile() 82 : video.getMaxQualityFile()
@@ -93,7 +93,7 @@ async function handleHLSJob (job: Bull.Job, payload: HLSTranscodingPayload, vide
93 job 93 job
94 }) 94 })
95 95
96 await retryTransactionWrapper(onHlsPlaylistGeneration, video, payload.resolution) 96 await retryTransactionWrapper(onHlsPlaylistGeneration, video, user, payload)
97} 97}
98 98
99async function handleNewWebTorrentResolutionJob ( 99async function handleNewWebTorrentResolutionJob (
@@ -110,9 +110,7 @@ async function handleNewWebTorrentResolutionJob (
110async function handleWebTorrentMergeAudioJob (job: Bull.Job, payload: MergeAudioTranscodingPayload, video: MVideoFullLight, user: MUserId) { 110async function handleWebTorrentMergeAudioJob (job: Bull.Job, payload: MergeAudioTranscodingPayload, video: MVideoFullLight, user: MUserId) {
111 await mergeAudioVideofile(video, payload.resolution, job) 111 await mergeAudioVideofile(video, payload.resolution, job)
112 112
113 await retryTransactionWrapper(onNewWebTorrentFileResolution, video, user, payload) 113 await retryTransactionWrapper(onVideoFileOptimizer, video, payload, 'video', user)
114
115 await createLowerResolutionsJobs(video, user, payload.resolution, false)
116} 114}
117 115
118async function handleWebTorrentOptimizeJob (job: Bull.Job, payload: OptimizeTranscodingPayload, video: MVideoFullLight, user: MUserId) { 116async function handleWebTorrentOptimizeJob (job: Bull.Job, payload: OptimizeTranscodingPayload, video: MVideoFullLight, user: MUserId) {
@@ -123,13 +121,11 @@ async function handleWebTorrentOptimizeJob (job: Bull.Job, payload: OptimizeTran
123 121
124// --------------------------------------------------------------------------- 122// ---------------------------------------------------------------------------
125 123
126async function onHlsPlaylistGeneration (video: MVideoFullLight, resolution: number) { 124async function onHlsPlaylistGeneration (video: MVideoFullLight, user: MUser, payload: HLSTranscodingPayload) {
127 if (video === undefined) return undefined 125 if (video === undefined) return undefined
128 126
129 const maxQualityFile = video.getMaxQualityFile() 127 if (payload.isMaxQuality && CONFIG.TRANSCODING.WEBTORRENT.ENABLED === false) {
130 128 // Remove webtorrent files if not enabled
131 // We generated the max quality HLS playlist, we don't need the webtorrent files anymore if the admin disabled it
132 if (CONFIG.TRANSCODING.WEBTORRENT.ENABLED === false && video.hasWebTorrentFiles() && maxQualityFile.resolution === resolution) {
133 for (const file of video.VideoFiles) { 129 for (const file of video.VideoFiles) {
134 await video.removeFile(file) 130 await video.removeFile(file)
135 await video.removeTorrent(file) 131 await video.removeTorrent(file)
@@ -137,6 +133,9 @@ async function onHlsPlaylistGeneration (video: MVideoFullLight, resolution: numb
137 } 133 }
138 134
139 video.VideoFiles = [] 135 video.VideoFiles = []
136
137 // Create HLS new resolution jobs
138 await createLowerResolutionsJobs(video, user, payload.resolution, payload.isPortraitMode, 'hls')
140 } 139 }
141 140
142 return publishAndFederateIfNeeded(video) 141 return publishAndFederateIfNeeded(video)
@@ -144,7 +143,7 @@ async function onHlsPlaylistGeneration (video: MVideoFullLight, resolution: numb
144 143
145async function onVideoFileOptimizer ( 144async function onVideoFileOptimizer (
146 videoArg: MVideoWithFile, 145 videoArg: MVideoWithFile,
147 payload: OptimizeTranscodingPayload, 146 payload: OptimizeTranscodingPayload | MergeAudioTranscodingPayload,
148 transcodeType: TranscodeOptionsType, 147 transcodeType: TranscodeOptionsType,
149 user: MUserId 148 user: MUserId
150) { 149) {
@@ -166,11 +165,12 @@ async function onVideoFileOptimizer (
166 isPortraitMode, 165 isPortraitMode,
167 resolution: videoDatabase.getMaxQualityFile().resolution, 166 resolution: videoDatabase.getMaxQualityFile().resolution,
168 // If we quick transcoded original file, force transcoding for HLS to avoid some weird playback issues 167 // If we quick transcoded original file, force transcoding for HLS to avoid some weird playback issues
169 copyCodecs: transcodeType !== 'quick-transcode' 168 copyCodecs: transcodeType !== 'quick-transcode',
169 isMaxQuality: true
170 }) 170 })
171 await createHlsJobIfEnabled(user, originalFileHLSPayload) 171 await createHlsJobIfEnabled(user, originalFileHLSPayload)
172 172
173 const hasNewResolutions = createLowerResolutionsJobs(videoDatabase, user, videoFileResolution, isPortraitMode) 173 const hasNewResolutions = await createLowerResolutionsJobs(videoDatabase, user, videoFileResolution, isPortraitMode, 'webtorrent')
174 174
175 if (!hasNewResolutions) { 175 if (!hasNewResolutions) {
176 // No transcoding to do, it's now published 176 // No transcoding to do, it's now published
@@ -193,7 +193,7 @@ async function onNewWebTorrentFileResolution (
193) { 193) {
194 await publishAndFederateIfNeeded(video) 194 await publishAndFederateIfNeeded(video)
195 195
196 await createHlsJobIfEnabled(user, Object.assign({}, payload, { copyCodecs: true })) 196 await createHlsJobIfEnabled(user, Object.assign({}, payload, { copyCodecs: true, isMaxQuality: false }))
197} 197}
198 198
199// --------------------------------------------------------------------------- 199// ---------------------------------------------------------------------------
@@ -210,6 +210,7 @@ async function createHlsJobIfEnabled (user: MUserId, payload: {
210 resolution: number 210 resolution: number
211 isPortraitMode?: boolean 211 isPortraitMode?: boolean
212 copyCodecs: boolean 212 copyCodecs: boolean
213 isMaxQuality: boolean
213}) { 214}) {
214 if (!payload || CONFIG.TRANSCODING.HLS.ENABLED !== true) return 215 if (!payload || CONFIG.TRANSCODING.HLS.ENABLED !== true) return
215 216
@@ -222,7 +223,8 @@ async function createHlsJobIfEnabled (user: MUserId, payload: {
222 videoUUID: payload.videoUUID, 223 videoUUID: payload.videoUUID,
223 resolution: payload.resolution, 224 resolution: payload.resolution,
224 isPortraitMode: payload.isPortraitMode, 225 isPortraitMode: payload.isPortraitMode,
225 copyCodecs: payload.copyCodecs 226 copyCodecs: payload.copyCodecs,
227 isMaxQuality: payload.isMaxQuality
226 } 228 }
227 229
228 return JobQueue.Instance.createJobWithPromise({ type: 'video-transcoding', payload: hlsTranscodingPayload }, jobOptions) 230 return JobQueue.Instance.createJobWithPromise({ type: 'video-transcoding', payload: hlsTranscodingPayload }, jobOptions)
@@ -232,7 +234,8 @@ async function createLowerResolutionsJobs (
232 video: MVideoFullLight, 234 video: MVideoFullLight,
233 user: MUserId, 235 user: MUserId,
234 videoFileResolution: number, 236 videoFileResolution: number,
235 isPortraitMode: boolean 237 isPortraitMode: boolean,
238 type: 'hls' | 'webtorrent'
236) { 239) {
237 // Create transcoding jobs if there are enabled resolutions 240 // Create transcoding jobs if there are enabled resolutions
238 const resolutionsEnabled = computeResolutionsToTranscode(videoFileResolution, 'vod') 241 const resolutionsEnabled = computeResolutionsToTranscode(videoFileResolution, 'vod')
@@ -250,7 +253,7 @@ async function createLowerResolutionsJobs (
250 for (const resolution of resolutionsEnabled) { 253 for (const resolution of resolutionsEnabled) {
251 let dataInput: VideoTranscodingPayload 254 let dataInput: VideoTranscodingPayload
252 255
253 if (CONFIG.TRANSCODING.WEBTORRENT.ENABLED) { 256 if (CONFIG.TRANSCODING.WEBTORRENT.ENABLED && type === 'webtorrent') {
254 // WebTorrent will create subsequent HLS job 257 // WebTorrent will create subsequent HLS job
255 dataInput = { 258 dataInput = {
256 type: 'new-resolution-to-webtorrent', 259 type: 'new-resolution-to-webtorrent',
@@ -258,13 +261,16 @@ async function createLowerResolutionsJobs (
258 resolution, 261 resolution,
259 isPortraitMode 262 isPortraitMode
260 } 263 }
261 } else if (CONFIG.TRANSCODING.HLS.ENABLED) { 264 }
265
266 if (CONFIG.TRANSCODING.HLS.ENABLED && type === 'hls') {
262 dataInput = { 267 dataInput = {
263 type: 'new-resolution-to-hls', 268 type: 'new-resolution-to-hls',
264 videoUUID: video.uuid, 269 videoUUID: video.uuid,
265 resolution, 270 resolution,
266 isPortraitMode, 271 isPortraitMode,
267 copyCodecs: false 272 copyCodecs: false,
273 isMaxQuality: false
268 } 274 }
269 } 275 }
270 276
diff --git a/server/lib/job-queue/job-queue.ts b/server/lib/job-queue/job-queue.ts
index 38b1d6f1f..72fed6072 100644
--- a/server/lib/job-queue/job-queue.ts
+++ b/server/lib/job-queue/job-queue.ts
@@ -1,5 +1,6 @@
1import * as Bull from 'bull' 1import * as Bull from 'bull'
2import { jobStates } from '@server/helpers/custom-validators/jobs' 2import { jobStates } from '@server/helpers/custom-validators/jobs'
3import { CONFIG } from '@server/initializers/config'
3import { processVideoRedundancy } from '@server/lib/job-queue/handlers/video-redundancy' 4import { processVideoRedundancy } from '@server/lib/job-queue/handlers/video-redundancy'
4import { 5import {
5 ActivitypubFollowPayload, 6 ActivitypubFollowPayload,
@@ -105,11 +106,11 @@ class JobQueue {
105 } 106 }
106 } 107 }
107 108
108 for (const handlerName of Object.keys(handlers)) { 109 for (const handlerName of (Object.keys(handlers) as JobType[])) {
109 const queue = new Bull(handlerName, queueOptions) 110 const queue = new Bull(handlerName, queueOptions)
110 const handler = handlers[handlerName] 111 const handler = handlers[handlerName]
111 112
112 queue.process(JOB_CONCURRENCY[handlerName], handler) 113 queue.process(this.getJobConcurrency(handlerName), handler)
113 .catch(err => logger.error('Error in job queue processor %s.', handlerName, { err })) 114 .catch(err => logger.error('Error in job queue processor %s.', handlerName, { err }))
114 115
115 queue.on('failed', (job, err) => { 116 queue.on('failed', (job, err) => {
@@ -235,6 +236,13 @@ class JobQueue {
235 return jobTypes.filter(t => t === jobType) 236 return jobTypes.filter(t => t === jobType)
236 } 237 }
237 238
239 private getJobConcurrency (jobType: JobType) {
240 if (jobType === 'video-transcoding') return CONFIG.TRANSCODING.CONCURRENCY
241 if (jobType === 'video-import') return CONFIG.IMPORT.VIDEOS.CONCURRENCY
242
243 return JOB_CONCURRENCY[jobType]
244 }
245
238 static get Instance () { 246 static get Instance () {
239 return this.instance || (this.instance = new this()) 247 return this.instance || (this.instance = new this())
240 } 248 }
diff --git a/server/lib/video-transcoding.ts b/server/lib/video-transcoding.ts
index 88a6e0673..a58c9dd20 100644
--- a/server/lib/video-transcoding.ts
+++ b/server/lib/video-transcoding.ts
@@ -272,7 +272,7 @@ async function generateHlsPlaylistCommon (options: {
272 const { type, video, inputPath, resolution, copyCodecs, isPortraitMode, isAAC, job } = options 272 const { type, video, inputPath, resolution, copyCodecs, isPortraitMode, isAAC, job } = options
273 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR 273 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
274 274
275 const videoTranscodedBasePath = join(transcodeDirectory, type, video.uuid) 275 const videoTranscodedBasePath = join(transcodeDirectory, type)
276 await ensureDir(videoTranscodedBasePath) 276 await ensureDir(videoTranscodedBasePath)
277 277
278 const videoFilename = generateVideoStreamingPlaylistName(video.uuid, resolution) 278 const videoFilename = generateVideoStreamingPlaylistName(video.uuid, resolution)
@@ -337,8 +337,6 @@ async function generateHlsPlaylistCommon (options: {
337 await move(playlistFileTranscodePath, playlistPath, { overwrite: true }) 337 await move(playlistFileTranscodePath, playlistPath, { overwrite: true })
338 // Move video file 338 // Move video file
339 await move(join(videoTranscodedBasePath, videoFilename), videoFilePath, { overwrite: true }) 339 await move(join(videoTranscodedBasePath, videoFilename), videoFilePath, { overwrite: true })
340 // Cleanup directory
341 await remove(videoTranscodedBasePath)
342 340
343 const stats = await stat(videoFilePath) 341 const stats = await stat(videoFilePath)
344 342
diff --git a/server/middlewares/validators/config.ts b/server/middlewares/validators/config.ts
index 52c28799f..71098ff99 100644
--- a/server/middlewares/validators/config.ts
+++ b/server/middlewares/validators/config.ts
@@ -39,6 +39,7 @@ const customConfigUpdateValidator = [
39 body('transcoding.enabled').isBoolean().withMessage('Should have a valid transcoding enabled boolean'), 39 body('transcoding.enabled').isBoolean().withMessage('Should have a valid transcoding enabled boolean'),
40 body('transcoding.allowAdditionalExtensions').isBoolean().withMessage('Should have a valid additional extensions boolean'), 40 body('transcoding.allowAdditionalExtensions').isBoolean().withMessage('Should have a valid additional extensions boolean'),
41 body('transcoding.threads').isInt().withMessage('Should have a valid transcoding threads number'), 41 body('transcoding.threads').isInt().withMessage('Should have a valid transcoding threads number'),
42 body('transcoding.concurrency').isInt({ min: 1 }).withMessage('Should have a valid transcoding concurrency number'),
42 body('transcoding.resolutions.0p').isBoolean().withMessage('Should have a valid transcoding 0p resolution enabled boolean'), 43 body('transcoding.resolutions.0p').isBoolean().withMessage('Should have a valid transcoding 0p resolution enabled boolean'),
43 body('transcoding.resolutions.240p').isBoolean().withMessage('Should have a valid transcoding 240p resolution enabled boolean'), 44 body('transcoding.resolutions.240p').isBoolean().withMessage('Should have a valid transcoding 240p resolution enabled boolean'),
44 body('transcoding.resolutions.360p').isBoolean().withMessage('Should have a valid transcoding 360p resolution enabled boolean'), 45 body('transcoding.resolutions.360p').isBoolean().withMessage('Should have a valid transcoding 360p resolution enabled boolean'),
@@ -51,6 +52,7 @@ const customConfigUpdateValidator = [
51 body('transcoding.webtorrent.enabled').isBoolean().withMessage('Should have a valid webtorrent transcoding enabled boolean'), 52 body('transcoding.webtorrent.enabled').isBoolean().withMessage('Should have a valid webtorrent transcoding enabled boolean'),
52 body('transcoding.hls.enabled').isBoolean().withMessage('Should have a valid hls transcoding enabled boolean'), 53 body('transcoding.hls.enabled').isBoolean().withMessage('Should have a valid hls transcoding enabled boolean'),
53 54
55 body('import.videos.concurrency').isInt({ min: 0 }).withMessage('Should have a valid import concurrency number'),
54 body('import.videos.http.enabled').isBoolean().withMessage('Should have a valid import video http enabled boolean'), 56 body('import.videos.http.enabled').isBoolean().withMessage('Should have a valid import video http enabled boolean'),
55 body('import.videos.torrent.enabled').isBoolean().withMessage('Should have a valid import video torrent enabled boolean'), 57 body('import.videos.torrent.enabled').isBoolean().withMessage('Should have a valid import video torrent enabled boolean'),
56 58
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 3f6fd8dc0..14e80a3ba 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -345,7 +345,6 @@ export type AvailableForListIDsOptions = {
345 include: [ 345 include: [
346 { 346 {
347 model: VideoFileModel, 347 model: VideoFileModel,
348 separate: true, // We may have multiple files, having multiple redundancies so let's separate this join
349 required: false, 348 required: false,
350 include: subInclude 349 include: subInclude
351 } 350 }
@@ -372,7 +371,6 @@ export type AvailableForListIDsOptions = {
372 include: [ 371 include: [
373 { 372 {
374 model: VideoStreamingPlaylistModel.unscoped(), 373 model: VideoStreamingPlaylistModel.unscoped(),
375 separate: true, // We may have multiple streaming playlists, having multiple redundancies so let's separate this join
376 required: false, 374 required: false,
377 include: subInclude 375 include: subInclude
378 } 376 }
@@ -1689,7 +1687,7 @@ export class VideoModel extends Model {
1689 1687
1690 channelModel.Account = accountModel 1688 channelModel.Account = accountModel
1691 1689
1692 const videoModel = new VideoModel(pick(row, videoKeys)) 1690 const videoModel = new VideoModel(pick(row, videoKeys), buildOpts)
1693 videoModel.VideoChannel = channelModel 1691 videoModel.VideoChannel = channelModel
1694 1692
1695 videoModel.UserVideoHistories = [] 1693 videoModel.UserVideoHistories = []
diff --git a/server/tests/api/check-params/config.ts b/server/tests/api/check-params/config.ts
index d6c20f7af..c7eb3189b 100644
--- a/server/tests/api/check-params/config.ts
+++ b/server/tests/api/check-params/config.ts
@@ -86,6 +86,7 @@ describe('Test config API validators', function () {
86 enabled: true, 86 enabled: true,
87 allowAdditionalExtensions: true, 87 allowAdditionalExtensions: true,
88 allowAudioFiles: true, 88 allowAudioFiles: true,
89 concurrency: 1,
89 threads: 1, 90 threads: 1,
90 profile: 'vod_profile', 91 profile: 'vod_profile',
91 resolutions: { 92 resolutions: {
@@ -130,6 +131,7 @@ describe('Test config API validators', function () {
130 }, 131 },
131 import: { 132 import: {
132 videos: { 133 videos: {
134 concurrency: 1,
133 http: { 135 http: {
134 enabled: false 136 enabled: false
135 }, 137 },
diff --git a/server/tests/api/server/config.ts b/server/tests/api/server/config.ts
index 26df8373e..b2371614f 100644
--- a/server/tests/api/server/config.ts
+++ b/server/tests/api/server/config.ts
@@ -70,6 +70,7 @@ function checkInitialConfig (server: ServerInfo, data: CustomConfig) {
70 expect(data.transcoding.allowAdditionalExtensions).to.be.false 70 expect(data.transcoding.allowAdditionalExtensions).to.be.false
71 expect(data.transcoding.allowAudioFiles).to.be.false 71 expect(data.transcoding.allowAudioFiles).to.be.false
72 expect(data.transcoding.threads).to.equal(2) 72 expect(data.transcoding.threads).to.equal(2)
73 expect(data.transcoding.concurrency).to.equal(2)
73 expect(data.transcoding.profile).to.equal('default') 74 expect(data.transcoding.profile).to.equal('default')
74 expect(data.transcoding.resolutions['240p']).to.be.true 75 expect(data.transcoding.resolutions['240p']).to.be.true
75 expect(data.transcoding.resolutions['360p']).to.be.true 76 expect(data.transcoding.resolutions['360p']).to.be.true
@@ -97,6 +98,7 @@ function checkInitialConfig (server: ServerInfo, data: CustomConfig) {
97 expect(data.live.transcoding.resolutions['1440p']).to.be.false 98 expect(data.live.transcoding.resolutions['1440p']).to.be.false
98 expect(data.live.transcoding.resolutions['2160p']).to.be.false 99 expect(data.live.transcoding.resolutions['2160p']).to.be.false
99 100
101 expect(data.import.videos.concurrency).to.equal(2)
100 expect(data.import.videos.http.enabled).to.be.true 102 expect(data.import.videos.http.enabled).to.be.true
101 expect(data.import.videos.torrent.enabled).to.be.true 103 expect(data.import.videos.torrent.enabled).to.be.true
102 expect(data.autoBlacklist.videos.ofUsers.enabled).to.be.false 104 expect(data.autoBlacklist.videos.ofUsers.enabled).to.be.false
@@ -159,6 +161,7 @@ function checkUpdatedConfig (data: CustomConfig) {
159 161
160 expect(data.transcoding.enabled).to.be.true 162 expect(data.transcoding.enabled).to.be.true
161 expect(data.transcoding.threads).to.equal(1) 163 expect(data.transcoding.threads).to.equal(1)
164 expect(data.transcoding.concurrency).to.equal(3)
162 expect(data.transcoding.allowAdditionalExtensions).to.be.true 165 expect(data.transcoding.allowAdditionalExtensions).to.be.true
163 expect(data.transcoding.allowAudioFiles).to.be.true 166 expect(data.transcoding.allowAudioFiles).to.be.true
164 expect(data.transcoding.profile).to.equal('vod_profile') 167 expect(data.transcoding.profile).to.equal('vod_profile')
@@ -186,6 +189,7 @@ function checkUpdatedConfig (data: CustomConfig) {
186 expect(data.live.transcoding.resolutions['1080p']).to.be.true 189 expect(data.live.transcoding.resolutions['1080p']).to.be.true
187 expect(data.live.transcoding.resolutions['2160p']).to.be.true 190 expect(data.live.transcoding.resolutions['2160p']).to.be.true
188 191
192 expect(data.import.videos.concurrency).to.equal(4)
189 expect(data.import.videos.http.enabled).to.be.false 193 expect(data.import.videos.http.enabled).to.be.false
190 expect(data.import.videos.torrent.enabled).to.be.false 194 expect(data.import.videos.torrent.enabled).to.be.false
191 expect(data.autoBlacklist.videos.ofUsers.enabled).to.be.true 195 expect(data.autoBlacklist.videos.ofUsers.enabled).to.be.true
@@ -323,6 +327,7 @@ describe('Test config', function () {
323 allowAdditionalExtensions: true, 327 allowAdditionalExtensions: true,
324 allowAudioFiles: true, 328 allowAudioFiles: true,
325 threads: 1, 329 threads: 1,
330 concurrency: 3,
326 profile: 'vod_profile', 331 profile: 'vod_profile',
327 resolutions: { 332 resolutions: {
328 '0p': false, 333 '0p': false,
@@ -364,6 +369,7 @@ describe('Test config', function () {
364 }, 369 },
365 import: { 370 import: {
366 videos: { 371 videos: {
372 concurrency: 4,
367 http: { 373 http: {
368 enabled: false 374 enabled: false
369 }, 375 },
diff --git a/shared/extra-utils/server/config.ts b/shared/extra-utils/server/config.ts
index db5a473ca..efe569243 100644
--- a/shared/extra-utils/server/config.ts
+++ b/shared/extra-utils/server/config.ts
@@ -112,6 +112,7 @@ function updateCustomSubConfig (url: string, token: string, newConfig: DeepParti
112 allowAdditionalExtensions: true, 112 allowAdditionalExtensions: true,
113 allowAudioFiles: true, 113 allowAudioFiles: true,
114 threads: 1, 114 threads: 1,
115 concurrency: 3,
115 profile: 'default', 116 profile: 'default',
116 resolutions: { 117 resolutions: {
117 '0p': false, 118 '0p': false,
@@ -153,6 +154,7 @@ function updateCustomSubConfig (url: string, token: string, newConfig: DeepParti
153 }, 154 },
154 import: { 155 import: {
155 videos: { 156 videos: {
157 concurrency: 3,
156 http: { 158 http: {
157 enabled: false 159 enabled: false
158 }, 160 },
diff --git a/shared/models/server/custom-config.model.ts b/shared/models/server/custom-config.model.ts
index d23b8abef..93b2d3417 100644
--- a/shared/models/server/custom-config.model.ts
+++ b/shared/models/server/custom-config.model.ts
@@ -87,6 +87,7 @@ export interface CustomConfig {
87 allowAudioFiles: boolean 87 allowAudioFiles: boolean
88 88
89 threads: number 89 threads: number
90 concurrency: number
90 91
91 profile: string 92 profile: string
92 93
@@ -120,6 +121,8 @@ export interface CustomConfig {
120 121
121 import: { 122 import: {
122 videos: { 123 videos: {
124 concurrency: number
125
123 http: { 126 http: {
124 enabled: boolean 127 enabled: boolean
125 } 128 }
diff --git a/shared/models/server/job.model.ts b/shared/models/server/job.model.ts
index ddd678b91..44f92abf1 100644
--- a/shared/models/server/job.model.ts
+++ b/shared/models/server/job.model.ts
@@ -106,6 +106,7 @@ export interface HLSTranscodingPayload extends BaseTranscodingPayload {
106 isPortraitMode?: boolean 106 isPortraitMode?: boolean
107 resolution: VideoResolution 107 resolution: VideoResolution
108 copyCodecs: boolean 108 copyCodecs: boolean
109 isMaxQuality: boolean
109} 110}
110 111
111export interface NewResolutionTranscodingPayload extends BaseTranscodingPayload { 112export interface NewResolutionTranscodingPayload extends BaseTranscodingPayload {