diff options
author | Chocobozzz <me@florianbigard.com> | 2021-01-28 15:52:44 +0100 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2021-01-28 15:55:39 +0100 |
commit | 1896bca09e088b0da9d5e845407ecebae330618c (patch) | |
tree | 56041c445c0cd49aca536d0fd6b586730f4d341e | |
parent | 529b37527cff5203a0689a15ce73dcee6e1eece2 (diff) | |
download | PeerTube-1896bca09e088b0da9d5e845407ecebae330618c.tar.gz PeerTube-1896bca09e088b0da9d5e845407ecebae330618c.tar.zst PeerTube-1896bca09e088b0da9d5e845407ecebae330618c.zip |
Support transcoding options/encoders by plugins
32 files changed, 744 insertions, 125 deletions
diff --git a/config/default.yaml b/config/default.yaml index b9e382fa7..283e0ab93 100644 --- a/config/default.yaml +++ b/config/default.yaml | |||
@@ -223,11 +223,20 @@ user: | |||
223 | # Please, do not disable transcoding since many uploaded videos will not work | 223 | # Please, do not disable transcoding since many uploaded videos will not work |
224 | transcoding: | 224 | transcoding: |
225 | enabled: true | 225 | enabled: true |
226 | |||
226 | # Allow your users to upload .mkv, .mov, .avi, .wmv, .flv, .f4v, .3g2, .3gp, .mts, m2ts, .mxf, .nut videos | 227 | # Allow your users to upload .mkv, .mov, .avi, .wmv, .flv, .f4v, .3g2, .3gp, .mts, m2ts, .mxf, .nut videos |
227 | allow_additional_extensions: true | 228 | allow_additional_extensions: true |
229 | |||
228 | # If a user uploads an audio file, PeerTube will create a video by merging the preview file and the audio file | 230 | # If a user uploads an audio file, PeerTube will create a video by merging the preview file and the audio file |
229 | allow_audio_files: true | 231 | allow_audio_files: true |
232 | |||
230 | threads: 1 | 233 | threads: 1 |
234 | |||
235 | # Choose the transcoding profile | ||
236 | # New profiles can be added by plugins | ||
237 | # Available in core PeerTube: 'default' | ||
238 | profile: 'default' | ||
239 | |||
231 | resolutions: # Only created if the original video has a higher resolution, uses more storage! | 240 | resolutions: # Only created if the original video has a higher resolution, uses more storage! |
232 | 0p: false # audio-only (creates mp4 without video stream, always created when enabled) | 241 | 0p: false # audio-only (creates mp4 without video stream, always created when enabled) |
233 | 240p: false | 242 | 240p: false |
@@ -283,6 +292,11 @@ live: | |||
283 | enabled: true | 292 | enabled: true |
284 | threads: 2 | 293 | threads: 2 |
285 | 294 | ||
295 | # Choose the transcoding profile | ||
296 | # New profiles can be added by plugins | ||
297 | # Available in core PeerTube: 'default' | ||
298 | profile: 'default' | ||
299 | |||
286 | resolutions: | 300 | resolutions: |
287 | 240p: false | 301 | 240p: false |
288 | 360p: false | 302 | 360p: false |
diff --git a/config/production.yaml.example b/config/production.yaml.example index b616c6ced..66c981dd5 100644 --- a/config/production.yaml.example +++ b/config/production.yaml.example | |||
@@ -236,11 +236,20 @@ user: | |||
236 | # Please, do not disable transcoding since many uploaded videos will not work | 236 | # Please, do not disable transcoding since many uploaded videos will not work |
237 | transcoding: | 237 | transcoding: |
238 | enabled: true | 238 | enabled: true |
239 | |||
239 | # Allow your users to upload .mkv, .mov, .avi, .wmv, .flv, .f4v, .3g2, .3gp, .mts, m2ts, .mxf, .nut videos | 240 | # Allow your users to upload .mkv, .mov, .avi, .wmv, .flv, .f4v, .3g2, .3gp, .mts, m2ts, .mxf, .nut videos |
240 | allow_additional_extensions: true | 241 | allow_additional_extensions: true |
242 | |||
241 | # If a user uploads an audio file, PeerTube will create a video by merging the preview file and the audio file | 243 | # If a user uploads an audio file, PeerTube will create a video by merging the preview file and the audio file |
242 | allow_audio_files: true | 244 | allow_audio_files: true |
245 | |||
243 | threads: 1 | 246 | threads: 1 |
247 | |||
248 | # Choose the transcoding profile | ||
249 | # New profiles can be added by plugins | ||
250 | # Available in core PeerTube: 'default' | ||
251 | profile: 'default' | ||
252 | |||
244 | resolutions: # Only created if the original video has a higher resolution, uses more storage! | 253 | resolutions: # Only created if the original video has a higher resolution, uses more storage! |
245 | 0p: false # audio-only (creates mp4 without video stream, always created when enabled) | 254 | 0p: false # audio-only (creates mp4 without video stream, always created when enabled) |
246 | 240p: false | 255 | 240p: false |
@@ -270,7 +279,7 @@ live: | |||
270 | enabled: false | 279 | enabled: false |
271 | 280 | ||
272 | # Limit lives duration | 281 | # Limit lives duration |
273 | # Set null to disable duration limit | 282 | # -1 == unlimited |
274 | max_duration: -1 # For example: '5 hours' | 283 | max_duration: -1 # For example: '5 hours' |
275 | 284 | ||
276 | # Limit max number of live videos created on your instance | 285 | # Limit max number of live videos created on your instance |
@@ -296,6 +305,11 @@ live: | |||
296 | enabled: true | 305 | enabled: true |
297 | threads: 2 | 306 | threads: 2 |
298 | 307 | ||
308 | # Choose the transcoding profile | ||
309 | # New profiles can be added by plugins | ||
310 | # Available in core PeerTube: 'default' | ||
311 | profile: 'default' | ||
312 | |||
299 | resolutions: | 313 | resolutions: |
300 | 240p: false | 314 | 240p: false |
301 | 360p: false | 315 | 360p: false |
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts index 45c03be24..7fda06a87 100644 --- a/server/controllers/api/config.ts +++ b/server/controllers/api/config.ts | |||
@@ -18,6 +18,7 @@ import { PluginManager } from '../../lib/plugins/plugin-manager' | |||
18 | import { getThemeOrDefault } from '../../lib/plugins/theme-utils' | 18 | import { getThemeOrDefault } from '../../lib/plugins/theme-utils' |
19 | import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../middlewares' | 19 | import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../middlewares' |
20 | import { customConfigUpdateValidator } from '../../middlewares/validators/config' | 20 | import { customConfigUpdateValidator } from '../../middlewares/validators/config' |
21 | import { VideoTranscodingProfilesManager } from '@server/lib/video-transcoding-profiles' | ||
21 | 22 | ||
22 | const configRouter = express.Router() | 23 | const configRouter = express.Router() |
23 | 24 | ||
@@ -114,7 +115,9 @@ async function getConfig (req: express.Request, res: express.Response) { | |||
114 | webtorrent: { | 115 | webtorrent: { |
115 | enabled: CONFIG.TRANSCODING.WEBTORRENT.ENABLED | 116 | enabled: CONFIG.TRANSCODING.WEBTORRENT.ENABLED |
116 | }, | 117 | }, |
117 | enabledResolutions: getEnabledResolutions('vod') | 118 | enabledResolutions: getEnabledResolutions('vod'), |
119 | profile: CONFIG.TRANSCODING.PROFILE, | ||
120 | availableProfiles: VideoTranscodingProfilesManager.Instance.getAvailableProfiles('vod') | ||
118 | }, | 121 | }, |
119 | live: { | 122 | live: { |
120 | enabled: CONFIG.LIVE.ENABLED, | 123 | enabled: CONFIG.LIVE.ENABLED, |
@@ -126,7 +129,9 @@ async function getConfig (req: express.Request, res: express.Response) { | |||
126 | 129 | ||
127 | transcoding: { | 130 | transcoding: { |
128 | enabled: CONFIG.LIVE.TRANSCODING.ENABLED, | 131 | enabled: CONFIG.LIVE.TRANSCODING.ENABLED, |
129 | enabledResolutions: getEnabledResolutions('live') | 132 | enabledResolutions: getEnabledResolutions('live'), |
133 | profile: CONFIG.LIVE.TRANSCODING.PROFILE, | ||
134 | availableProfiles: VideoTranscodingProfilesManager.Instance.getAvailableProfiles('live') | ||
130 | }, | 135 | }, |
131 | 136 | ||
132 | rtmp: { | 137 | rtmp: { |
@@ -412,6 +417,7 @@ function customConfig (): CustomConfig { | |||
412 | allowAdditionalExtensions: CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS, | 417 | allowAdditionalExtensions: CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS, |
413 | allowAudioFiles: CONFIG.TRANSCODING.ALLOW_AUDIO_FILES, | 418 | allowAudioFiles: CONFIG.TRANSCODING.ALLOW_AUDIO_FILES, |
414 | threads: CONFIG.TRANSCODING.THREADS, | 419 | threads: CONFIG.TRANSCODING.THREADS, |
420 | profile: CONFIG.TRANSCODING.PROFILE, | ||
415 | resolutions: { | 421 | resolutions: { |
416 | '0p': CONFIG.TRANSCODING.RESOLUTIONS['0p'], | 422 | '0p': CONFIG.TRANSCODING.RESOLUTIONS['0p'], |
417 | '240p': CONFIG.TRANSCODING.RESOLUTIONS['240p'], | 423 | '240p': CONFIG.TRANSCODING.RESOLUTIONS['240p'], |
@@ -438,6 +444,7 @@ function customConfig (): CustomConfig { | |||
438 | transcoding: { | 444 | transcoding: { |
439 | enabled: CONFIG.LIVE.TRANSCODING.ENABLED, | 445 | enabled: CONFIG.LIVE.TRANSCODING.ENABLED, |
440 | threads: CONFIG.LIVE.TRANSCODING.THREADS, | 446 | threads: CONFIG.LIVE.TRANSCODING.THREADS, |
447 | profile: CONFIG.LIVE.TRANSCODING.PROFILE, | ||
441 | resolutions: { | 448 | resolutions: { |
442 | '240p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['240p'], | 449 | '240p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['240p'], |
443 | '360p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['360p'], | 450 | '360p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['360p'], |
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts index 7d46130ec..33c625c9e 100644 --- a/server/helpers/ffmpeg-utils.ts +++ b/server/helpers/ffmpeg-utils.ts | |||
@@ -3,9 +3,9 @@ import * as ffmpeg from 'fluent-ffmpeg' | |||
3 | import { readFile, remove, writeFile } from 'fs-extra' | 3 | import { readFile, remove, writeFile } from 'fs-extra' |
4 | import { dirname, join } from 'path' | 4 | import { dirname, join } from 'path' |
5 | import { FFMPEG_NICE, VIDEO_LIVE } from '@server/initializers/constants' | 5 | import { FFMPEG_NICE, VIDEO_LIVE } from '@server/initializers/constants' |
6 | import { VideoResolution } from '../../shared/models/videos' | 6 | import { AvailableEncoders, EncoderOptionsBuilder, EncoderProfile, VideoResolution } from '../../shared/models/videos' |
7 | import { checkFFmpegEncoders } from '../initializers/checker-before-init' | ||
8 | import { CONFIG } from '../initializers/config' | 7 | import { CONFIG } from '../initializers/config' |
8 | import { promisify0 } from './core-utils' | ||
9 | import { computeFPS, getAudioStream, getVideoFileFPS } from './ffprobe-utils' | 9 | import { computeFPS, getAudioStream, getVideoFileFPS } from './ffprobe-utils' |
10 | import { processImage } from './image-utils' | 10 | import { processImage } from './image-utils' |
11 | import { logger } from './logger' | 11 | import { logger } from './logger' |
@@ -21,46 +21,45 @@ import { logger } from './logger' | |||
21 | // Encoder options | 21 | // Encoder options |
22 | // --------------------------------------------------------------------------- | 22 | // --------------------------------------------------------------------------- |
23 | 23 | ||
24 | // Options builders | 24 | type StreamType = 'audio' | 'video' |
25 | |||
26 | export type EncoderOptionsBuilder = (params: { | ||
27 | input: string | ||
28 | resolution: VideoResolution | ||
29 | fps?: number | ||
30 | streamNum?: number | ||
31 | }) => Promise<EncoderOptions> | EncoderOptions | ||
32 | 25 | ||
33 | // Options types | 26 | // --------------------------------------------------------------------------- |
27 | // Encoders support | ||
28 | // --------------------------------------------------------------------------- | ||
34 | 29 | ||
35 | export interface EncoderOptions { | 30 | // Detect supported encoders by ffmpeg |
36 | copy?: boolean | 31 | let supportedEncoders: Map<string, boolean> |
37 | outputOptions: string[] | 32 | async function checkFFmpegEncoders (peertubeAvailableEncoders: AvailableEncoders): Promise<Map<string, boolean>> { |
38 | } | 33 | if (supportedEncoders !== undefined) { |
34 | return supportedEncoders | ||
35 | } | ||
39 | 36 | ||
40 | // All our encoders | 37 | const getAvailableEncodersPromise = promisify0(ffmpeg.getAvailableEncoders) |
38 | const availableFFmpegEncoders = await getAvailableEncodersPromise() | ||
41 | 39 | ||
42 | export interface EncoderProfile <T> { | 40 | const searchEncoders = new Set<string>() |
43 | [ profile: string ]: T | 41 | for (const type of [ 'live', 'vod' ]) { |
42 | for (const streamType of [ 'audio', 'video' ]) { | ||
43 | for (const encoder of peertubeAvailableEncoders.encodersToTry[type][streamType]) { | ||
44 | searchEncoders.add(encoder) | ||
45 | } | ||
46 | } | ||
47 | } | ||
44 | 48 | ||
45 | default: T | 49 | supportedEncoders = new Map<string, boolean>() |
46 | } | ||
47 | 50 | ||
48 | export type AvailableEncoders = { | 51 | for (const searchEncoder of searchEncoders) { |
49 | live: { | 52 | supportedEncoders.set(searchEncoder, availableFFmpegEncoders[searchEncoder] !== undefined) |
50 | [ encoder: string ]: EncoderProfile<EncoderOptionsBuilder> | ||
51 | } | 53 | } |
52 | 54 | ||
53 | vod: { | 55 | logger.info('Built supported ffmpeg encoders.', { supportedEncoders, searchEncoders }) |
54 | [ encoder: string ]: EncoderProfile<EncoderOptionsBuilder> | ||
55 | } | ||
56 | 56 | ||
57 | encodersToTry: { | 57 | return supportedEncoders |
58 | video: string[] | ||
59 | audio: string[] | ||
60 | } | ||
61 | } | 58 | } |
62 | 59 | ||
63 | type StreamType = 'audio' | 'video' | 60 | function resetSupportedEncoders () { |
61 | supportedEncoders = undefined | ||
62 | } | ||
64 | 63 | ||
65 | // --------------------------------------------------------------------------- | 64 | // --------------------------------------------------------------------------- |
66 | // Image manipulation | 65 | // Image manipulation |
@@ -275,7 +274,7 @@ async function getLiveTranscodingCommand (options: { | |||
275 | 274 | ||
276 | addDefaultEncoderParams({ command, encoder: builderResult.encoder, fps: resolutionFPS, streamNum: i }) | 275 | addDefaultEncoderParams({ command, encoder: builderResult.encoder, fps: resolutionFPS, streamNum: i }) |
277 | 276 | ||
278 | logger.debug('Apply ffmpeg live video params from %s.', builderResult.encoder, builderResult) | 277 | logger.debug('Apply ffmpeg live video params from %s using %s profile.', builderResult.encoder, profile, builderResult) |
279 | 278 | ||
280 | command.outputOption(`${buildStreamSuffix('-c:v', i)} ${builderResult.encoder}`) | 279 | command.outputOption(`${buildStreamSuffix('-c:v', i)} ${builderResult.encoder}`) |
281 | command.addOutputOptions(builderResult.result.outputOptions) | 280 | command.addOutputOptions(builderResult.result.outputOptions) |
@@ -292,7 +291,7 @@ async function getLiveTranscodingCommand (options: { | |||
292 | 291 | ||
293 | addDefaultEncoderParams({ command, encoder: builderResult.encoder, fps: resolutionFPS, streamNum: i }) | 292 | addDefaultEncoderParams({ command, encoder: builderResult.encoder, fps: resolutionFPS, streamNum: i }) |
294 | 293 | ||
295 | logger.debug('Apply ffmpeg live audio params from %s.', builderResult.encoder, builderResult) | 294 | logger.debug('Apply ffmpeg live audio params from %s using %s profile.', builderResult.encoder, profile, builderResult) |
296 | 295 | ||
297 | command.outputOption(`${buildStreamSuffix('-c:a', i)} ${builderResult.encoder}`) | 296 | command.outputOption(`${buildStreamSuffix('-c:a', i)} ${builderResult.encoder}`) |
298 | command.addOutputOptions(builderResult.result.outputOptions) | 297 | command.addOutputOptions(builderResult.result.outputOptions) |
@@ -513,11 +512,19 @@ async function getEncoderBuilderResult (options: { | |||
513 | }) { | 512 | }) { |
514 | const { availableEncoders, input, profile, resolution, streamType, fps, streamNum, videoType } = options | 513 | const { availableEncoders, input, profile, resolution, streamType, fps, streamNum, videoType } = options |
515 | 514 | ||
516 | const encodersToTry = availableEncoders.encodersToTry[streamType] | 515 | const encodersToTry = availableEncoders.encodersToTry[videoType][streamType] |
517 | const encoders = availableEncoders[videoType] | 516 | const encoders = availableEncoders.available[videoType] |
518 | 517 | ||
519 | for (const encoder of encodersToTry) { | 518 | for (const encoder of encodersToTry) { |
520 | if (!(await checkFFmpegEncoders()).get(encoder) || !encoders[encoder]) continue | 519 | if (!(await checkFFmpegEncoders(availableEncoders)).get(encoder)) { |
520 | logger.debug('Encoder %s not available in ffmpeg, skipping.', encoder) | ||
521 | continue | ||
522 | } | ||
523 | |||
524 | if (!encoders[encoder]) { | ||
525 | logger.debug('Encoder %s not available in peertube encoders, skipping.', encoder) | ||
526 | continue | ||
527 | } | ||
521 | 528 | ||
522 | // An object containing available profiles for this encoder | 529 | // An object containing available profiles for this encoder |
523 | const builderProfiles: EncoderProfile<EncoderOptionsBuilder> = encoders[encoder] | 530 | const builderProfiles: EncoderProfile<EncoderOptionsBuilder> = encoders[encoder] |
@@ -567,7 +574,7 @@ async function presetVideo ( | |||
567 | 574 | ||
568 | if (!parsedAudio.audioStream) { | 575 | if (!parsedAudio.audioStream) { |
569 | localCommand = localCommand.noAudio() | 576 | localCommand = localCommand.noAudio() |
570 | streamsToProcess = [ 'audio' ] | 577 | streamsToProcess = [ 'video' ] |
571 | } | 578 | } |
572 | 579 | ||
573 | for (const streamType of streamsToProcess) { | 580 | for (const streamType of streamsToProcess) { |
@@ -587,7 +594,10 @@ async function presetVideo ( | |||
587 | throw new Error('No available encoder found for stream ' + streamType) | 594 | throw new Error('No available encoder found for stream ' + streamType) |
588 | } | 595 | } |
589 | 596 | ||
590 | logger.debug('Apply ffmpeg params from %s.', builderResult.encoder, builderResult) | 597 | logger.debug( |
598 | 'Apply ffmpeg params from %s for %s stream of input %s using %s profile.', | ||
599 | builderResult.encoder, streamType, input, profile, builderResult | ||
600 | ) | ||
591 | 601 | ||
592 | if (streamType === 'video') { | 602 | if (streamType === 'video') { |
593 | localCommand.videoCodec(builderResult.encoder) | 603 | localCommand.videoCodec(builderResult.encoder) |
@@ -679,6 +689,8 @@ export { | |||
679 | transcode, | 689 | transcode, |
680 | runCommand, | 690 | runCommand, |
681 | 691 | ||
692 | resetSupportedEncoders, | ||
693 | |||
682 | // builders | 694 | // builders |
683 | buildx264VODCommand | 695 | buildx264VODCommand |
684 | } | 696 | } |
diff --git a/server/helpers/logger.ts b/server/helpers/logger.ts index 05ec4a6b9..746b2e0a6 100644 --- a/server/helpers/logger.ts +++ b/server/helpers/logger.ts | |||
@@ -27,6 +27,14 @@ function getLoggerReplacer () { | |||
27 | seen.add(value) | 27 | seen.add(value) |
28 | } | 28 | } |
29 | 29 | ||
30 | if (value instanceof Set) { | ||
31 | return Array.from(value) | ||
32 | } | ||
33 | |||
34 | if (value instanceof Map) { | ||
35 | return Array.from(value.entries()) | ||
36 | } | ||
37 | |||
30 | if (value instanceof Error) { | 38 | if (value instanceof Error) { |
31 | const error = {} | 39 | const error = {} |
32 | 40 | ||
diff --git a/server/initializers/checker-before-init.ts b/server/initializers/checker-before-init.ts index 7945e8586..9c4e0048a 100644 --- a/server/initializers/checker-before-init.ts +++ b/server/initializers/checker-before-init.ts | |||
@@ -97,34 +97,6 @@ async function checkFFmpeg (CONFIG: { TRANSCODING: { ENABLED: boolean } }) { | |||
97 | throw new Error('Unavailable encode codec ' + codec + ' in FFmpeg') | 97 | throw new Error('Unavailable encode codec ' + codec + ' in FFmpeg') |
98 | } | 98 | } |
99 | } | 99 | } |
100 | |||
101 | return checkFFmpegEncoders() | ||
102 | } | ||
103 | |||
104 | // Detect supported encoders by ffmpeg | ||
105 | let supportedEncoders: Map<string, boolean> | ||
106 | async function checkFFmpegEncoders (): Promise<Map<string, boolean>> { | ||
107 | if (supportedEncoders !== undefined) { | ||
108 | return supportedEncoders | ||
109 | } | ||
110 | |||
111 | const Ffmpeg = require('fluent-ffmpeg') | ||
112 | const getAvailableEncodersPromise = promisify0(Ffmpeg.getAvailableEncoders) | ||
113 | const availableEncoders = await getAvailableEncodersPromise() | ||
114 | |||
115 | const searchEncoders = [ | ||
116 | 'aac', | ||
117 | 'libfdk_aac', | ||
118 | 'libx264' | ||
119 | ] | ||
120 | |||
121 | supportedEncoders = new Map<string, boolean>() | ||
122 | |||
123 | for (const searchEncoder of searchEncoders) { | ||
124 | supportedEncoders.set(searchEncoder, availableEncoders[searchEncoder] !== undefined) | ||
125 | } | ||
126 | |||
127 | return supportedEncoders | ||
128 | } | 100 | } |
129 | 101 | ||
130 | function checkNodeVersion () { | 102 | function checkNodeVersion () { |
@@ -143,7 +115,6 @@ function checkNodeVersion () { | |||
143 | 115 | ||
144 | export { | 116 | export { |
145 | checkFFmpeg, | 117 | checkFFmpeg, |
146 | checkFFmpegEncoders, | ||
147 | checkMissedConfig, | 118 | checkMissedConfig, |
148 | checkNodeVersion | 119 | checkNodeVersion |
149 | } | 120 | } |
diff --git a/server/initializers/config.ts b/server/initializers/config.ts index fc4a8b709..7322b89e2 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 PROFILE () { return config.get<string>('transcoding.profile') }, | ||
191 | RESOLUTIONS: { | 192 | RESOLUTIONS: { |
192 | get '0p' () { return config.get<boolean>('transcoding.resolutions.0p') }, | 193 | get '0p' () { return config.get<boolean>('transcoding.resolutions.0p') }, |
193 | get '240p' () { return config.get<boolean>('transcoding.resolutions.240p') }, | 194 | get '240p' () { return config.get<boolean>('transcoding.resolutions.240p') }, |
@@ -221,6 +222,7 @@ const CONFIG = { | |||
221 | TRANSCODING: { | 222 | TRANSCODING: { |
222 | get ENABLED () { return config.get<boolean>('live.transcoding.enabled') }, | 223 | get ENABLED () { return config.get<boolean>('live.transcoding.enabled') }, |
223 | get THREADS () { return config.get<number>('live.transcoding.threads') }, | 224 | get THREADS () { return config.get<number>('live.transcoding.threads') }, |
225 | get PROFILE () { return config.get<string>('live.transcoding.profile') }, | ||
224 | 226 | ||
225 | RESOLUTIONS: { | 227 | RESOLUTIONS: { |
226 | get '240p' () { return config.get<boolean>('live.transcoding.resolutions.240p') }, | 228 | get '240p' () { return config.get<boolean>('live.transcoding.resolutions.240p') }, |
diff --git a/server/lib/live-manager.ts b/server/lib/live-manager.ts index c8e5bcb77..9f17b8820 100644 --- a/server/lib/live-manager.ts +++ b/server/lib/live-manager.ts | |||
@@ -338,7 +338,7 @@ class LiveManager { | |||
338 | resolutions: allResolutions, | 338 | resolutions: allResolutions, |
339 | fps, | 339 | fps, |
340 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), | 340 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), |
341 | profile: 'default' | 341 | profile: CONFIG.LIVE.TRANSCODING.PROFILE |
342 | }) | 342 | }) |
343 | : getLiveMuxingCommand(rtmpUrl, outPath) | 343 | : getLiveMuxingCommand(rtmpUrl, outPath) |
344 | 344 | ||
diff --git a/server/lib/plugins/plugin-helpers.ts b/server/lib/plugins/plugin-helpers-builder.ts index 39773f693..39773f693 100644 --- a/server/lib/plugins/plugin-helpers.ts +++ b/server/lib/plugins/plugin-helpers-builder.ts | |||
diff --git a/server/lib/plugins/plugin-manager.ts b/server/lib/plugins/plugin-manager.ts index 8e7491257..c19b40135 100644 --- a/server/lib/plugins/plugin-manager.ts +++ b/server/lib/plugins/plugin-manager.ts | |||
@@ -20,7 +20,7 @@ import { PLUGIN_GLOBAL_CSS_PATH } from '../../initializers/constants' | |||
20 | import { PluginModel } from '../../models/server/plugin' | 20 | import { PluginModel } from '../../models/server/plugin' |
21 | import { PluginLibrary, RegisterServerAuthExternalOptions, RegisterServerAuthPassOptions, RegisterServerOptions } from '../../types/plugins' | 21 | import { PluginLibrary, RegisterServerAuthExternalOptions, RegisterServerAuthPassOptions, RegisterServerOptions } from '../../types/plugins' |
22 | import { ClientHtml } from '../client-html' | 22 | import { ClientHtml } from '../client-html' |
23 | import { RegisterHelpersStore } from './register-helpers-store' | 23 | import { RegisterHelpers } from './register-helpers' |
24 | import { installNpmPlugin, installNpmPluginFromDisk, removeNpmPlugin } from './yarn' | 24 | import { installNpmPlugin, installNpmPluginFromDisk, removeNpmPlugin } from './yarn' |
25 | 25 | ||
26 | export interface RegisteredPlugin { | 26 | export interface RegisteredPlugin { |
@@ -40,7 +40,7 @@ export interface RegisteredPlugin { | |||
40 | css: string[] | 40 | css: string[] |
41 | 41 | ||
42 | // Only if this is a plugin | 42 | // Only if this is a plugin |
43 | registerHelpersStore?: RegisterHelpersStore | 43 | registerHelpers?: RegisterHelpers |
44 | unregister?: Function | 44 | unregister?: Function |
45 | } | 45 | } |
46 | 46 | ||
@@ -109,7 +109,7 @@ export class PluginManager implements ServerHook { | |||
109 | npmName: p.npmName, | 109 | npmName: p.npmName, |
110 | name: p.name, | 110 | name: p.name, |
111 | version: p.version, | 111 | version: p.version, |
112 | idAndPassAuths: p.registerHelpersStore.getIdAndPassAuths() | 112 | idAndPassAuths: p.registerHelpers.getIdAndPassAuths() |
113 | })) | 113 | })) |
114 | .filter(v => v.idAndPassAuths.length !== 0) | 114 | .filter(v => v.idAndPassAuths.length !== 0) |
115 | } | 115 | } |
@@ -120,7 +120,7 @@ export class PluginManager implements ServerHook { | |||
120 | npmName: p.npmName, | 120 | npmName: p.npmName, |
121 | name: p.name, | 121 | name: p.name, |
122 | version: p.version, | 122 | version: p.version, |
123 | externalAuths: p.registerHelpersStore.getExternalAuths() | 123 | externalAuths: p.registerHelpers.getExternalAuths() |
124 | })) | 124 | })) |
125 | .filter(v => v.externalAuths.length !== 0) | 125 | .filter(v => v.externalAuths.length !== 0) |
126 | } | 126 | } |
@@ -129,14 +129,14 @@ export class PluginManager implements ServerHook { | |||
129 | const result = this.getRegisteredPluginOrTheme(npmName) | 129 | const result = this.getRegisteredPluginOrTheme(npmName) |
130 | if (!result || result.type !== PluginType.PLUGIN) return [] | 130 | if (!result || result.type !== PluginType.PLUGIN) return [] |
131 | 131 | ||
132 | return result.registerHelpersStore.getSettings() | 132 | return result.registerHelpers.getSettings() |
133 | } | 133 | } |
134 | 134 | ||
135 | getRouter (npmName: string) { | 135 | getRouter (npmName: string) { |
136 | const result = this.getRegisteredPluginOrTheme(npmName) | 136 | const result = this.getRegisteredPluginOrTheme(npmName) |
137 | if (!result || result.type !== PluginType.PLUGIN) return null | 137 | if (!result || result.type !== PluginType.PLUGIN) return null |
138 | 138 | ||
139 | return result.registerHelpersStore.getRouter() | 139 | return result.registerHelpers.getRouter() |
140 | } | 140 | } |
141 | 141 | ||
142 | getTranslations (locale: string) { | 142 | getTranslations (locale: string) { |
@@ -194,7 +194,7 @@ export class PluginManager implements ServerHook { | |||
194 | logger.error('Cannot find plugin %s to call on settings changed.', name) | 194 | logger.error('Cannot find plugin %s to call on settings changed.', name) |
195 | } | 195 | } |
196 | 196 | ||
197 | for (const cb of registered.registerHelpersStore.getOnSettingsChangedCallbacks()) { | 197 | for (const cb of registered.registerHelpers.getOnSettingsChangedCallbacks()) { |
198 | try { | 198 | try { |
199 | cb(settings) | 199 | cb(settings) |
200 | } catch (err) { | 200 | } catch (err) { |
@@ -268,8 +268,9 @@ export class PluginManager implements ServerHook { | |||
268 | this.hooks[key] = this.hooks[key].filter(h => h.npmName !== npmName) | 268 | this.hooks[key] = this.hooks[key].filter(h => h.npmName !== npmName) |
269 | } | 269 | } |
270 | 270 | ||
271 | const store = plugin.registerHelpersStore | 271 | const store = plugin.registerHelpers |
272 | store.reinitVideoConstants(plugin.npmName) | 272 | store.reinitVideoConstants(plugin.npmName) |
273 | store.reinitTranscodingProfilesAndEncoders(plugin.npmName) | ||
273 | 274 | ||
274 | logger.info('Regenerating registered plugin CSS to global file.') | 275 | logger.info('Regenerating registered plugin CSS to global file.') |
275 | await this.regeneratePluginGlobalCSS() | 276 | await this.regeneratePluginGlobalCSS() |
@@ -375,11 +376,11 @@ export class PluginManager implements ServerHook { | |||
375 | this.sanitizeAndCheckPackageJSONOrThrow(packageJSON, plugin.type) | 376 | this.sanitizeAndCheckPackageJSONOrThrow(packageJSON, plugin.type) |
376 | 377 | ||
377 | let library: PluginLibrary | 378 | let library: PluginLibrary |
378 | let registerHelpersStore: RegisterHelpersStore | 379 | let registerHelpers: RegisterHelpers |
379 | if (plugin.type === PluginType.PLUGIN) { | 380 | if (plugin.type === PluginType.PLUGIN) { |
380 | const result = await this.registerPlugin(plugin, pluginPath, packageJSON) | 381 | const result = await this.registerPlugin(plugin, pluginPath, packageJSON) |
381 | library = result.library | 382 | library = result.library |
382 | registerHelpersStore = result.registerStore | 383 | registerHelpers = result.registerStore |
383 | } | 384 | } |
384 | 385 | ||
385 | const clientScripts: { [id: string]: ClientScript } = {} | 386 | const clientScripts: { [id: string]: ClientScript } = {} |
@@ -398,7 +399,7 @@ export class PluginManager implements ServerHook { | |||
398 | staticDirs: packageJSON.staticDirs, | 399 | staticDirs: packageJSON.staticDirs, |
399 | clientScripts, | 400 | clientScripts, |
400 | css: packageJSON.css, | 401 | css: packageJSON.css, |
401 | registerHelpersStore: registerHelpersStore || undefined, | 402 | registerHelpers: registerHelpers || undefined, |
402 | unregister: library ? library.unregister : undefined | 403 | unregister: library ? library.unregister : undefined |
403 | } | 404 | } |
404 | 405 | ||
@@ -512,8 +513,8 @@ export class PluginManager implements ServerHook { | |||
512 | const plugin = this.getRegisteredPluginOrTheme(npmName) | 513 | const plugin = this.getRegisteredPluginOrTheme(npmName) |
513 | if (!plugin || plugin.type !== PluginType.PLUGIN) return null | 514 | if (!plugin || plugin.type !== PluginType.PLUGIN) return null |
514 | 515 | ||
515 | let auths: (RegisterServerAuthPassOptions | RegisterServerAuthExternalOptions)[] = plugin.registerHelpersStore.getIdAndPassAuths() | 516 | let auths: (RegisterServerAuthPassOptions | RegisterServerAuthExternalOptions)[] = plugin.registerHelpers.getIdAndPassAuths() |
516 | auths = auths.concat(plugin.registerHelpersStore.getExternalAuths()) | 517 | auths = auths.concat(plugin.registerHelpers.getExternalAuths()) |
517 | 518 | ||
518 | return auths.find(a => a.authName === authName) | 519 | return auths.find(a => a.authName === authName) |
519 | } | 520 | } |
@@ -538,7 +539,7 @@ export class PluginManager implements ServerHook { | |||
538 | private getRegisterHelpers ( | 539 | private getRegisterHelpers ( |
539 | npmName: string, | 540 | npmName: string, |
540 | plugin: PluginModel | 541 | plugin: PluginModel |
541 | ): { registerStore: RegisterHelpersStore, registerOptions: RegisterServerOptions } { | 542 | ): { registerStore: RegisterHelpers, registerOptions: RegisterServerOptions } { |
542 | const onHookAdded = (options: RegisterServerHookOptions) => { | 543 | const onHookAdded = (options: RegisterServerHookOptions) => { |
543 | if (!this.hooks[options.target]) this.hooks[options.target] = [] | 544 | if (!this.hooks[options.target]) this.hooks[options.target] = [] |
544 | 545 | ||
@@ -550,11 +551,11 @@ export class PluginManager implements ServerHook { | |||
550 | }) | 551 | }) |
551 | } | 552 | } |
552 | 553 | ||
553 | const registerHelpersStore = new RegisterHelpersStore(npmName, plugin, onHookAdded.bind(this)) | 554 | const registerHelpers = new RegisterHelpers(npmName, plugin, onHookAdded.bind(this)) |
554 | 555 | ||
555 | return { | 556 | return { |
556 | registerStore: registerHelpersStore, | 557 | registerStore: registerHelpers, |
557 | registerOptions: registerHelpersStore.buildRegisterHelpers() | 558 | registerOptions: registerHelpers.buildRegisterHelpers() |
558 | } | 559 | } |
559 | } | 560 | } |
560 | 561 | ||
diff --git a/server/lib/plugins/register-helpers-store.ts b/server/lib/plugins/register-helpers.ts index c73079302..3a38a4835 100644 --- a/server/lib/plugins/register-helpers-store.ts +++ b/server/lib/plugins/register-helpers.ts | |||
@@ -17,6 +17,7 @@ import { | |||
17 | RegisterServerOptions | 17 | RegisterServerOptions |
18 | } from '@server/types/plugins' | 18 | } from '@server/types/plugins' |
19 | import { | 19 | import { |
20 | EncoderOptionsBuilder, | ||
20 | PluginPlaylistPrivacyManager, | 21 | PluginPlaylistPrivacyManager, |
21 | PluginSettingsManager, | 22 | PluginSettingsManager, |
22 | PluginStorageManager, | 23 | PluginStorageManager, |
@@ -28,7 +29,8 @@ import { | |||
28 | RegisterServerSettingOptions | 29 | RegisterServerSettingOptions |
29 | } from '@shared/models' | 30 | } from '@shared/models' |
30 | import { serverHookObject } from '@shared/models/plugins/server-hook.model' | 31 | import { serverHookObject } from '@shared/models/plugins/server-hook.model' |
31 | import { buildPluginHelpers } from './plugin-helpers' | 32 | import { VideoTranscodingProfilesManager } from '../video-transcoding-profiles' |
33 | import { buildPluginHelpers } from './plugin-helpers-builder' | ||
32 | 34 | ||
33 | type AlterableVideoConstant = 'language' | 'licence' | 'category' | 'privacy' | 'playlistPrivacy' | 35 | type AlterableVideoConstant = 'language' | 'licence' | 'category' | 'privacy' | 'playlistPrivacy' |
34 | type VideoConstant = { [key in number | string]: string } | 36 | type VideoConstant = { [key in number | string]: string } |
@@ -40,7 +42,7 @@ type UpdatedVideoConstant = { | |||
40 | } | 42 | } |
41 | } | 43 | } |
42 | 44 | ||
43 | export class RegisterHelpersStore { | 45 | export class RegisterHelpers { |
44 | private readonly updatedVideoConstants: UpdatedVideoConstant = { | 46 | private readonly updatedVideoConstants: UpdatedVideoConstant = { |
45 | playlistPrivacy: { added: [], deleted: [] }, | 47 | playlistPrivacy: { added: [], deleted: [] }, |
46 | privacy: { added: [], deleted: [] }, | 48 | privacy: { added: [], deleted: [] }, |
@@ -49,6 +51,23 @@ export class RegisterHelpersStore { | |||
49 | category: { added: [], deleted: [] } | 51 | category: { added: [], deleted: [] } |
50 | } | 52 | } |
51 | 53 | ||
54 | private readonly transcodingProfiles: { | ||
55 | [ npmName: string ]: { | ||
56 | type: 'vod' | 'live' | ||
57 | encoder: string | ||
58 | profile: string | ||
59 | }[] | ||
60 | } = {} | ||
61 | |||
62 | private readonly transcodingEncoders: { | ||
63 | [ npmName: string ]: { | ||
64 | type: 'vod' | 'live' | ||
65 | streamType: 'audio' | 'video' | ||
66 | encoder: string | ||
67 | priority: number | ||
68 | }[] | ||
69 | } = {} | ||
70 | |||
52 | private readonly settings: RegisterServerSettingOptions[] = [] | 71 | private readonly settings: RegisterServerSettingOptions[] = [] |
53 | 72 | ||
54 | private idAndPassAuths: RegisterServerAuthPassOptions[] = [] | 73 | private idAndPassAuths: RegisterServerAuthPassOptions[] = [] |
@@ -83,6 +102,8 @@ export class RegisterHelpersStore { | |||
83 | const videoPrivacyManager = this.buildVideoPrivacyManager() | 102 | const videoPrivacyManager = this.buildVideoPrivacyManager() |
84 | const playlistPrivacyManager = this.buildPlaylistPrivacyManager() | 103 | const playlistPrivacyManager = this.buildPlaylistPrivacyManager() |
85 | 104 | ||
105 | const transcodingManager = this.buildTranscodingManager() | ||
106 | |||
86 | const registerIdAndPassAuth = this.buildRegisterIdAndPassAuth() | 107 | const registerIdAndPassAuth = this.buildRegisterIdAndPassAuth() |
87 | const registerExternalAuth = this.buildRegisterExternalAuth() | 108 | const registerExternalAuth = this.buildRegisterExternalAuth() |
88 | const unregisterIdAndPassAuth = this.buildUnregisterIdAndPassAuth() | 109 | const unregisterIdAndPassAuth = this.buildUnregisterIdAndPassAuth() |
@@ -106,6 +127,8 @@ export class RegisterHelpersStore { | |||
106 | videoPrivacyManager, | 127 | videoPrivacyManager, |
107 | playlistPrivacyManager, | 128 | playlistPrivacyManager, |
108 | 129 | ||
130 | transcodingManager, | ||
131 | |||
109 | registerIdAndPassAuth, | 132 | registerIdAndPassAuth, |
110 | registerExternalAuth, | 133 | registerExternalAuth, |
111 | unregisterIdAndPassAuth, | 134 | unregisterIdAndPassAuth, |
@@ -141,6 +164,22 @@ export class RegisterHelpersStore { | |||
141 | } | 164 | } |
142 | } | 165 | } |
143 | 166 | ||
167 | reinitTranscodingProfilesAndEncoders (npmName: string) { | ||
168 | const profiles = this.transcodingProfiles[npmName] | ||
169 | if (Array.isArray(profiles)) { | ||
170 | for (const profile of profiles) { | ||
171 | VideoTranscodingProfilesManager.Instance.removeProfile(profile) | ||
172 | } | ||
173 | } | ||
174 | |||
175 | const encoders = this.transcodingEncoders[npmName] | ||
176 | if (Array.isArray(encoders)) { | ||
177 | for (const o of encoders) { | ||
178 | VideoTranscodingProfilesManager.Instance.removeEncoderPriority(o.type, o.streamType, o.encoder, o.priority) | ||
179 | } | ||
180 | } | ||
181 | } | ||
182 | |||
144 | getSettings () { | 183 | getSettings () { |
145 | return this.settings | 184 | return this.settings |
146 | } | 185 | } |
@@ -354,4 +393,52 @@ export class RegisterHelpersStore { | |||
354 | 393 | ||
355 | return true | 394 | return true |
356 | } | 395 | } |
396 | |||
397 | private buildTranscodingManager () { | ||
398 | const self = this | ||
399 | |||
400 | function addProfile (type: 'live' | 'vod', encoder: string, profile: string, builder: EncoderOptionsBuilder) { | ||
401 | if (profile === 'default') { | ||
402 | logger.error('A plugin cannot add a default live transcoding profile') | ||
403 | return false | ||
404 | } | ||
405 | |||
406 | VideoTranscodingProfilesManager.Instance.addProfile({ | ||
407 | type, | ||
408 | encoder, | ||
409 | profile, | ||
410 | builder | ||
411 | }) | ||
412 | |||
413 | if (!self.transcodingProfiles[self.npmName]) self.transcodingProfiles[self.npmName] = [] | ||
414 | self.transcodingProfiles[self.npmName].push({ type, encoder, profile }) | ||
415 | |||
416 | return true | ||
417 | } | ||
418 | |||
419 | function addEncoderPriority (type: 'live' | 'vod', streamType: 'audio' | 'video', encoder: string, priority: number) { | ||
420 | VideoTranscodingProfilesManager.Instance.addEncoderPriority(type, streamType, encoder, priority) | ||
421 | |||
422 | if (!self.transcodingEncoders[self.npmName]) self.transcodingEncoders[self.npmName] = [] | ||
423 | self.transcodingEncoders[self.npmName].push({ type, streamType, encoder, priority }) | ||
424 | } | ||
425 | |||
426 | return { | ||
427 | addLiveProfile (encoder: string, profile: string, builder: EncoderOptionsBuilder) { | ||
428 | return addProfile('live', encoder, profile, builder) | ||
429 | }, | ||
430 | |||
431 | addVODProfile (encoder: string, profile: string, builder: EncoderOptionsBuilder) { | ||
432 | return addProfile('vod', encoder, profile, builder) | ||
433 | }, | ||
434 | |||
435 | addLiveEncoderPriority (streamType: 'audio' | 'video', encoder: string, priority: number) { | ||
436 | return addEncoderPriority('live', streamType, encoder, priority) | ||
437 | }, | ||
438 | |||
439 | addVODEncoderPriority (streamType: 'audio' | 'video', encoder: string, priority: number) { | ||
440 | return addEncoderPriority('vod', streamType, encoder, priority) | ||
441 | } | ||
442 | } | ||
443 | } | ||
357 | } | 444 | } |
diff --git a/server/lib/video-transcoding-profiles.ts b/server/lib/video-transcoding-profiles.ts index bbe556e75..76d38b6ca 100644 --- a/server/lib/video-transcoding-profiles.ts +++ b/server/lib/video-transcoding-profiles.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { logger } from '@server/helpers/logger' | 1 | import { logger } from '@server/helpers/logger' |
2 | import { getTargetBitrate, VideoResolution } from '../../shared/models/videos' | 2 | import { AvailableEncoders, EncoderOptionsBuilder, getTargetBitrate, VideoResolution } from '../../shared/models/videos' |
3 | import { AvailableEncoders, buildStreamSuffix, EncoderOptionsBuilder } from '../helpers/ffmpeg-utils' | 3 | import { buildStreamSuffix, resetSupportedEncoders } from '../helpers/ffmpeg-utils' |
4 | import { | 4 | import { |
5 | canDoQuickAudioTranscode, | 5 | canDoQuickAudioTranscode, |
6 | ffprobePromise, | 6 | ffprobePromise, |
@@ -84,16 +84,8 @@ class VideoTranscodingProfilesManager { | |||
84 | 84 | ||
85 | // 1 === less priority | 85 | // 1 === less priority |
86 | private readonly encodersPriorities = { | 86 | private readonly encodersPriorities = { |
87 | video: [ | 87 | vod: this.buildDefaultEncodersPriorities(), |
88 | { name: 'libx264', priority: 100 } | 88 | live: this.buildDefaultEncodersPriorities() |
89 | ], | ||
90 | |||
91 | // Try the first one, if not available try the second one etc | ||
92 | audio: [ | ||
93 | // we favor VBR, if a good AAC encoder is available | ||
94 | { name: 'libfdk_aac', priority: 200 }, | ||
95 | { name: 'aac', priority: 100 } | ||
96 | ] | ||
97 | } | 89 | } |
98 | 90 | ||
99 | private readonly availableEncoders = { | 91 | private readonly availableEncoders = { |
@@ -118,25 +110,77 @@ class VideoTranscodingProfilesManager { | |||
118 | } | 110 | } |
119 | } | 111 | } |
120 | 112 | ||
121 | private constructor () { | 113 | private availableProfiles = { |
114 | vod: [] as string[], | ||
115 | live: [] as string[] | ||
116 | } | ||
122 | 117 | ||
118 | private constructor () { | ||
119 | this.buildAvailableProfiles() | ||
123 | } | 120 | } |
124 | 121 | ||
125 | getAvailableEncoders (): AvailableEncoders { | 122 | getAvailableEncoders (): AvailableEncoders { |
126 | const encodersToTry = { | 123 | return { |
127 | video: this.getEncodersByPriority('video'), | 124 | available: this.availableEncoders, |
128 | audio: this.getEncodersByPriority('audio') | 125 | encodersToTry: { |
126 | vod: { | ||
127 | video: this.getEncodersByPriority('vod', 'video'), | ||
128 | audio: this.getEncodersByPriority('vod', 'audio') | ||
129 | }, | ||
130 | live: { | ||
131 | video: this.getEncodersByPriority('live', 'video'), | ||
132 | audio: this.getEncodersByPriority('live', 'audio') | ||
133 | } | ||
134 | } | ||
129 | } | 135 | } |
130 | |||
131 | return Object.assign({}, this.availableEncoders, { encodersToTry }) | ||
132 | } | 136 | } |
133 | 137 | ||
134 | getAvailableProfiles (type: 'vod' | 'live') { | 138 | getAvailableProfiles (type: 'vod' | 'live') { |
135 | return this.availableEncoders[type] | 139 | return this.availableProfiles[type] |
140 | } | ||
141 | |||
142 | addProfile (options: { | ||
143 | type: 'vod' | 'live' | ||
144 | encoder: string | ||
145 | profile: string | ||
146 | builder: EncoderOptionsBuilder | ||
147 | }) { | ||
148 | const { type, encoder, profile, builder } = options | ||
149 | |||
150 | const encoders = this.availableEncoders[type] | ||
151 | |||
152 | if (!encoders[encoder]) encoders[encoder] = {} | ||
153 | encoders[encoder][profile] = builder | ||
154 | |||
155 | this.buildAvailableProfiles() | ||
156 | } | ||
157 | |||
158 | removeProfile (options: { | ||
159 | type: 'vod' | 'live' | ||
160 | encoder: string | ||
161 | profile: string | ||
162 | }) { | ||
163 | const { type, encoder, profile } = options | ||
164 | |||
165 | delete this.availableEncoders[type][encoder][profile] | ||
166 | this.buildAvailableProfiles() | ||
136 | } | 167 | } |
137 | 168 | ||
138 | private getEncodersByPriority (type: 'video' | 'audio') { | 169 | addEncoderPriority (type: 'vod' | 'live', streamType: 'audio' | 'video', encoder: string, priority: number) { |
139 | return this.encodersPriorities[type] | 170 | this.encodersPriorities[type][streamType].push({ name: encoder, priority }) |
171 | |||
172 | resetSupportedEncoders() | ||
173 | } | ||
174 | |||
175 | removeEncoderPriority (type: 'vod' | 'live', streamType: 'audio' | 'video', encoder: string, priority: number) { | ||
176 | this.encodersPriorities[type][streamType] = this.encodersPriorities[type][streamType] | ||
177 | .filter(o => o.name !== encoder && o.priority !== priority) | ||
178 | |||
179 | resetSupportedEncoders() | ||
180 | } | ||
181 | |||
182 | private getEncodersByPriority (type: 'vod' | 'live', streamType: 'audio' | 'video') { | ||
183 | return this.encodersPriorities[type][streamType] | ||
140 | .sort((e1, e2) => { | 184 | .sort((e1, e2) => { |
141 | if (e1.priority > e2.priority) return -1 | 185 | if (e1.priority > e2.priority) return -1 |
142 | else if (e1.priority === e2.priority) return 0 | 186 | else if (e1.priority === e2.priority) return 0 |
@@ -146,6 +190,39 @@ class VideoTranscodingProfilesManager { | |||
146 | .map(e => e.name) | 190 | .map(e => e.name) |
147 | } | 191 | } |
148 | 192 | ||
193 | private buildAvailableProfiles () { | ||
194 | for (const type of [ 'vod', 'live' ]) { | ||
195 | const result = new Set() | ||
196 | |||
197 | const encoders = this.availableEncoders[type] | ||
198 | |||
199 | for (const encoderName of Object.keys(encoders)) { | ||
200 | for (const profile of Object.keys(encoders[encoderName])) { | ||
201 | result.add(profile) | ||
202 | } | ||
203 | } | ||
204 | |||
205 | this.availableProfiles[type] = Array.from(result) | ||
206 | } | ||
207 | |||
208 | logger.debug('Available transcoding profiles built.', { availableProfiles: this.availableProfiles }) | ||
209 | } | ||
210 | |||
211 | private buildDefaultEncodersPriorities () { | ||
212 | return { | ||
213 | video: [ | ||
214 | { name: 'libx264', priority: 100 } | ||
215 | ], | ||
216 | |||
217 | // Try the first one, if not available try the second one etc | ||
218 | audio: [ | ||
219 | // we favor VBR, if a good AAC encoder is available | ||
220 | { name: 'libfdk_aac', priority: 200 }, | ||
221 | { name: 'aac', priority: 100 } | ||
222 | ] | ||
223 | } | ||
224 | } | ||
225 | |||
149 | static get Instance () { | 226 | static get Instance () { |
150 | return this.instance || (this.instance = new this()) | 227 | return this.instance || (this.instance = new this()) |
151 | } | 228 | } |
diff --git a/server/lib/video-transcoding.ts b/server/lib/video-transcoding.ts index c4b3425d1..37a4f3019 100644 --- a/server/lib/video-transcoding.ts +++ b/server/lib/video-transcoding.ts | |||
@@ -42,7 +42,7 @@ async function optimizeOriginalVideofile (video: MVideoWithFile, inputVideoFile: | |||
42 | outputPath: videoTranscodedPath, | 42 | outputPath: videoTranscodedPath, |
43 | 43 | ||
44 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), | 44 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), |
45 | profile: 'default', | 45 | profile: CONFIG.TRANSCODING.PROFILE, |
46 | 46 | ||
47 | resolution: inputVideoFile.resolution, | 47 | resolution: inputVideoFile.resolution, |
48 | 48 | ||
@@ -96,7 +96,7 @@ async function transcodeNewWebTorrentResolution (video: MVideoWithFile, resoluti | |||
96 | outputPath: videoTranscodedPath, | 96 | outputPath: videoTranscodedPath, |
97 | 97 | ||
98 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), | 98 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), |
99 | profile: 'default', | 99 | profile: CONFIG.TRANSCODING.PROFILE, |
100 | 100 | ||
101 | resolution, | 101 | resolution, |
102 | 102 | ||
@@ -108,7 +108,7 @@ async function transcodeNewWebTorrentResolution (video: MVideoWithFile, resoluti | |||
108 | outputPath: videoTranscodedPath, | 108 | outputPath: videoTranscodedPath, |
109 | 109 | ||
110 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), | 110 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), |
111 | profile: 'default', | 111 | profile: CONFIG.TRANSCODING.PROFILE, |
112 | 112 | ||
113 | resolution, | 113 | resolution, |
114 | isPortraitMode: isPortrait, | 114 | isPortraitMode: isPortrait, |
@@ -143,7 +143,7 @@ async function mergeAudioVideofile (video: MVideoWithAllFiles, resolution: Video | |||
143 | outputPath: videoTranscodedPath, | 143 | outputPath: videoTranscodedPath, |
144 | 144 | ||
145 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), | 145 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), |
146 | profile: 'default', | 146 | profile: CONFIG.TRANSCODING.PROFILE, |
147 | 147 | ||
148 | audioPath: audioInputPath, | 148 | audioPath: audioInputPath, |
149 | resolution, | 149 | resolution, |
@@ -284,7 +284,7 @@ async function generateHlsPlaylistCommon (options: { | |||
284 | outputPath, | 284 | outputPath, |
285 | 285 | ||
286 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), | 286 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), |
287 | profile: 'default', | 287 | profile: CONFIG.TRANSCODING.PROFILE, |
288 | 288 | ||
289 | resolution, | 289 | resolution, |
290 | copyCodecs, | 290 | copyCodecs, |
diff --git a/server/middlewares/validators/plugins.ts b/server/middlewares/validators/plugins.ts index bbac55a50..1083e0afa 100644 --- a/server/middlewares/validators/plugins.ts +++ b/server/middlewares/validators/plugins.ts | |||
@@ -50,9 +50,9 @@ const getExternalAuthValidator = [ | |||
50 | if (areValidationErrors(req, res)) return | 50 | if (areValidationErrors(req, res)) return |
51 | 51 | ||
52 | const plugin = res.locals.registeredPlugin | 52 | const plugin = res.locals.registeredPlugin |
53 | if (!plugin.registerHelpersStore) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) | 53 | if (!plugin.registerHelpers) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) |
54 | 54 | ||
55 | const externalAuth = plugin.registerHelpersStore.getExternalAuths().find(a => a.authName === req.params.authName) | 55 | const externalAuth = plugin.registerHelpers.getExternalAuths().find(a => a.authName === req.params.authName) |
56 | if (!externalAuth) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) | 56 | if (!externalAuth) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) |
57 | 57 | ||
58 | res.locals.externalAuth = externalAuth | 58 | res.locals.externalAuth = externalAuth |
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index c56fbfbf2..ea6c9d44b 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -96,10 +96,11 @@ import { | |||
96 | MVideoWithRights | 96 | MVideoWithRights |
97 | } from '../../types/models' | 97 | } from '../../types/models' |
98 | import { MThumbnail } from '../../types/models/video/thumbnail' | 98 | import { MThumbnail } from '../../types/models/video/thumbnail' |
99 | import { MVideoFile, MVideoFileRedundanciesOpt, MVideoFileStreamingPlaylistVideo } from '../../types/models/video/video-file' | 99 | import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../types/models/video/video-file' |
100 | import { VideoAbuseModel } from '../abuse/video-abuse' | 100 | import { VideoAbuseModel } from '../abuse/video-abuse' |
101 | import { AccountModel } from '../account/account' | 101 | import { AccountModel } from '../account/account' |
102 | import { AccountVideoRateModel } from '../account/account-video-rate' | 102 | import { AccountVideoRateModel } from '../account/account-video-rate' |
103 | import { UserModel } from '../account/user' | ||
103 | import { UserVideoHistoryModel } from '../account/user-video-history' | 104 | import { UserVideoHistoryModel } from '../account/user-video-history' |
104 | import { ActorModel } from '../activitypub/actor' | 105 | import { ActorModel } from '../activitypub/actor' |
105 | import { AvatarModel } from '../avatar/avatar' | 106 | import { AvatarModel } from '../avatar/avatar' |
@@ -129,7 +130,6 @@ import { VideoShareModel } from './video-share' | |||
129 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' | 130 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' |
130 | import { VideoTagModel } from './video-tag' | 131 | import { VideoTagModel } from './video-tag' |
131 | import { VideoViewModel } from './video-view' | 132 | import { VideoViewModel } from './video-view' |
132 | import { UserModel } from '../account/user' | ||
133 | 133 | ||
134 | export enum ScopeNames { | 134 | export enum ScopeNames { |
135 | AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS', | 135 | AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS', |
diff --git a/server/tests/api/check-params/config.ts b/server/tests/api/check-params/config.ts index d3ae5fe0a..e6309b5f7 100644 --- a/server/tests/api/check-params/config.ts +++ b/server/tests/api/check-params/config.ts | |||
@@ -87,6 +87,7 @@ describe('Test config API validators', function () { | |||
87 | allowAdditionalExtensions: true, | 87 | allowAdditionalExtensions: true, |
88 | allowAudioFiles: true, | 88 | allowAudioFiles: true, |
89 | threads: 1, | 89 | threads: 1, |
90 | profile: 'vod_profile', | ||
90 | resolutions: { | 91 | resolutions: { |
91 | '0p': false, | 92 | '0p': false, |
92 | '240p': false, | 93 | '240p': false, |
@@ -115,6 +116,7 @@ describe('Test config API validators', function () { | |||
115 | transcoding: { | 116 | transcoding: { |
116 | enabled: true, | 117 | enabled: true, |
117 | threads: 4, | 118 | threads: 4, |
119 | profile: 'live_profile', | ||
118 | resolutions: { | 120 | resolutions: { |
119 | '240p': true, | 121 | '240p': true, |
120 | '360p': true, | 122 | '360p': true, |
diff --git a/server/tests/api/server/config.ts b/server/tests/api/server/config.ts index e0575bdfd..e5bab0b77 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.profile).to.equal('default') | ||
73 | expect(data.transcoding.resolutions['240p']).to.be.true | 74 | expect(data.transcoding.resolutions['240p']).to.be.true |
74 | expect(data.transcoding.resolutions['360p']).to.be.true | 75 | expect(data.transcoding.resolutions['360p']).to.be.true |
75 | expect(data.transcoding.resolutions['480p']).to.be.true | 76 | expect(data.transcoding.resolutions['480p']).to.be.true |
@@ -87,6 +88,7 @@ function checkInitialConfig (server: ServerInfo, data: CustomConfig) { | |||
87 | expect(data.live.maxUserLives).to.equal(3) | 88 | expect(data.live.maxUserLives).to.equal(3) |
88 | expect(data.live.transcoding.enabled).to.be.false | 89 | expect(data.live.transcoding.enabled).to.be.false |
89 | expect(data.live.transcoding.threads).to.equal(2) | 90 | expect(data.live.transcoding.threads).to.equal(2) |
91 | expect(data.live.transcoding.profile).to.equal('default') | ||
90 | expect(data.live.transcoding.resolutions['240p']).to.be.false | 92 | expect(data.live.transcoding.resolutions['240p']).to.be.false |
91 | expect(data.live.transcoding.resolutions['360p']).to.be.false | 93 | expect(data.live.transcoding.resolutions['360p']).to.be.false |
92 | expect(data.live.transcoding.resolutions['480p']).to.be.false | 94 | expect(data.live.transcoding.resolutions['480p']).to.be.false |
@@ -159,6 +161,7 @@ function checkUpdatedConfig (data: CustomConfig) { | |||
159 | expect(data.transcoding.threads).to.equal(1) | 161 | expect(data.transcoding.threads).to.equal(1) |
160 | expect(data.transcoding.allowAdditionalExtensions).to.be.true | 162 | expect(data.transcoding.allowAdditionalExtensions).to.be.true |
161 | expect(data.transcoding.allowAudioFiles).to.be.true | 163 | expect(data.transcoding.allowAudioFiles).to.be.true |
164 | expect(data.transcoding.profile).to.equal('vod_profile') | ||
162 | expect(data.transcoding.resolutions['240p']).to.be.false | 165 | expect(data.transcoding.resolutions['240p']).to.be.false |
163 | expect(data.transcoding.resolutions['360p']).to.be.true | 166 | expect(data.transcoding.resolutions['360p']).to.be.true |
164 | expect(data.transcoding.resolutions['480p']).to.be.true | 167 | expect(data.transcoding.resolutions['480p']).to.be.true |
@@ -175,6 +178,7 @@ function checkUpdatedConfig (data: CustomConfig) { | |||
175 | expect(data.live.maxUserLives).to.equal(10) | 178 | expect(data.live.maxUserLives).to.equal(10) |
176 | expect(data.live.transcoding.enabled).to.be.true | 179 | expect(data.live.transcoding.enabled).to.be.true |
177 | expect(data.live.transcoding.threads).to.equal(4) | 180 | expect(data.live.transcoding.threads).to.equal(4) |
181 | expect(data.live.transcoding.profile).to.equal('live_profile') | ||
178 | expect(data.live.transcoding.resolutions['240p']).to.be.true | 182 | expect(data.live.transcoding.resolutions['240p']).to.be.true |
179 | expect(data.live.transcoding.resolutions['360p']).to.be.true | 183 | expect(data.live.transcoding.resolutions['360p']).to.be.true |
180 | expect(data.live.transcoding.resolutions['480p']).to.be.true | 184 | expect(data.live.transcoding.resolutions['480p']).to.be.true |
@@ -319,6 +323,7 @@ describe('Test config', function () { | |||
319 | allowAdditionalExtensions: true, | 323 | allowAdditionalExtensions: true, |
320 | allowAudioFiles: true, | 324 | allowAudioFiles: true, |
321 | threads: 1, | 325 | threads: 1, |
326 | profile: 'vod_profile', | ||
322 | resolutions: { | 327 | resolutions: { |
323 | '0p': false, | 328 | '0p': false, |
324 | '240p': false, | 329 | '240p': false, |
@@ -345,6 +350,7 @@ describe('Test config', function () { | |||
345 | transcoding: { | 350 | transcoding: { |
346 | enabled: true, | 351 | enabled: true, |
347 | threads: 4, | 352 | threads: 4, |
353 | profile: 'live_profile', | ||
348 | resolutions: { | 354 | resolutions: { |
349 | '240p': true, | 355 | '240p': true, |
350 | '360p': true, | 356 | '360p': true, |
diff --git a/server/tests/api/videos/video-transcoder.ts b/server/tests/api/videos/video-transcoder.ts index 631230f26..5ad02df2f 100644 --- a/server/tests/api/videos/video-transcoder.ts +++ b/server/tests/api/videos/video-transcoder.ts | |||
@@ -511,7 +511,9 @@ describe('Test video transcoding', function () { | |||
511 | 511 | ||
512 | const resolutions = [ 240, 360, 480, 720, 1080 ] | 512 | const resolutions = [ 240, 360, 480, 720, 1080 ] |
513 | for (const r of resolutions) { | 513 | for (const r of resolutions) { |
514 | expect(await getServerFileSize(servers[1], `videos/${videoUUID}-${r}.mp4`)).to.be.below(60_000) | 514 | const path = `videos/${videoUUID}-${r}.mp4` |
515 | const size = await getServerFileSize(servers[1], path) | ||
516 | expect(size, `${path} not below ${60_000}`).to.be.below(60_000) | ||
515 | } | 517 | } |
516 | }) | 518 | }) |
517 | 519 | ||
diff --git a/server/tests/fixtures/peertube-plugin-test-transcoding-one/main.js b/server/tests/fixtures/peertube-plugin-test-transcoding-one/main.js new file mode 100644 index 000000000..5990ce1ce --- /dev/null +++ b/server/tests/fixtures/peertube-plugin-test-transcoding-one/main.js | |||
@@ -0,0 +1,35 @@ | |||
1 | async function register ({ transcodingManager }) { | ||
2 | |||
3 | { | ||
4 | const builder = () => { | ||
5 | return { | ||
6 | outputOptions: [ | ||
7 | '-r 10' | ||
8 | ] | ||
9 | } | ||
10 | } | ||
11 | |||
12 | transcodingManager.addVODProfile('libx264', 'low-vod', builder) | ||
13 | } | ||
14 | |||
15 | { | ||
16 | const builder = (options) => { | ||
17 | return { | ||
18 | outputOptions: [ | ||
19 | '-r:' + options.streamNum + ' 5' | ||
20 | ] | ||
21 | } | ||
22 | } | ||
23 | |||
24 | transcodingManager.addLiveProfile('libx264', 'low-live', builder) | ||
25 | } | ||
26 | } | ||
27 | |||
28 | async function unregister () { | ||
29 | return | ||
30 | } | ||
31 | |||
32 | module.exports = { | ||
33 | register, | ||
34 | unregister | ||
35 | } | ||
diff --git a/server/tests/fixtures/peertube-plugin-test-transcoding-one/package.json b/server/tests/fixtures/peertube-plugin-test-transcoding-one/package.json new file mode 100644 index 000000000..bedbfa051 --- /dev/null +++ b/server/tests/fixtures/peertube-plugin-test-transcoding-one/package.json | |||
@@ -0,0 +1,20 @@ | |||
1 | { | ||
2 | "name": "peertube-plugin-test-transcoding-one", | ||
3 | "version": "0.0.1", | ||
4 | "description": "Plugin test transcoding 1", | ||
5 | "engine": { | ||
6 | "peertube": ">=1.3.0" | ||
7 | }, | ||
8 | "keywords": [ | ||
9 | "peertube", | ||
10 | "plugin" | ||
11 | ], | ||
12 | "homepage": "https://github.com/Chocobozzz/PeerTube", | ||
13 | "author": "Chocobozzz", | ||
14 | "bugs": "https://github.com/Chocobozzz/PeerTube/issues", | ||
15 | "library": "./main.js", | ||
16 | "staticDirs": {}, | ||
17 | "css": [], | ||
18 | "clientScripts": [], | ||
19 | "translations": {} | ||
20 | } | ||
diff --git a/server/tests/fixtures/peertube-plugin-test-transcoding-two/main.js b/server/tests/fixtures/peertube-plugin-test-transcoding-two/main.js new file mode 100644 index 000000000..a914bce49 --- /dev/null +++ b/server/tests/fixtures/peertube-plugin-test-transcoding-two/main.js | |||
@@ -0,0 +1,38 @@ | |||
1 | async function register ({ transcodingManager }) { | ||
2 | |||
3 | { | ||
4 | const builder = () => { | ||
5 | return { | ||
6 | outputOptions: [] | ||
7 | } | ||
8 | } | ||
9 | |||
10 | transcodingManager.addVODProfile('libopus', 'test-vod-profile', builder) | ||
11 | transcodingManager.addVODProfile('libvpx-vp9', 'test-vod-profile', builder) | ||
12 | |||
13 | transcodingManager.addVODEncoderPriority('audio', 'libopus', 1000) | ||
14 | transcodingManager.addVODEncoderPriority('video', 'libvpx-vp9', 1000) | ||
15 | } | ||
16 | |||
17 | { | ||
18 | const builder = (options) => { | ||
19 | return { | ||
20 | outputOptions: [ | ||
21 | '-b:' + options.streamNum + ' 10K' | ||
22 | ] | ||
23 | } | ||
24 | } | ||
25 | |||
26 | transcodingManager.addLiveProfile('libopus', 'test-live-profile', builder) | ||
27 | transcodingManager.addLiveEncoderPriority('audio', 'libopus', 1000) | ||
28 | } | ||
29 | } | ||
30 | |||
31 | async function unregister () { | ||
32 | return | ||
33 | } | ||
34 | |||
35 | module.exports = { | ||
36 | register, | ||
37 | unregister | ||
38 | } | ||
diff --git a/server/tests/fixtures/peertube-plugin-test-transcoding-two/package.json b/server/tests/fixtures/peertube-plugin-test-transcoding-two/package.json new file mode 100644 index 000000000..34be0454b --- /dev/null +++ b/server/tests/fixtures/peertube-plugin-test-transcoding-two/package.json | |||
@@ -0,0 +1,20 @@ | |||
1 | { | ||
2 | "name": "peertube-plugin-test-transcoding-two", | ||
3 | "version": "0.0.1", | ||
4 | "description": "Plugin test transcoding 2", | ||
5 | "engine": { | ||
6 | "peertube": ">=1.3.0" | ||
7 | }, | ||
8 | "keywords": [ | ||
9 | "peertube", | ||
10 | "plugin" | ||
11 | ], | ||
12 | "homepage": "https://github.com/Chocobozzz/PeerTube", | ||
13 | "author": "Chocobozzz", | ||
14 | "bugs": "https://github.com/Chocobozzz/PeerTube/issues", | ||
15 | "library": "./main.js", | ||
16 | "staticDirs": {}, | ||
17 | "css": [], | ||
18 | "clientScripts": [], | ||
19 | "translations": {} | ||
20 | } | ||
diff --git a/server/tests/plugins/index.ts b/server/tests/plugins/index.ts index b870a4055..fd7116efd 100644 --- a/server/tests/plugins/index.ts +++ b/server/tests/plugins/index.ts | |||
@@ -1,10 +1,11 @@ | |||
1 | import './action-hooks' | 1 | import './action-hooks' |
2 | import './html-injection' | ||
3 | import './id-and-pass-auth' | ||
4 | import './external-auth' | 2 | import './external-auth' |
5 | import './filter-hooks' | 3 | import './filter-hooks' |
6 | import './translations' | 4 | import './html-injection' |
7 | import './video-constants' | 5 | import './id-and-pass-auth' |
8 | import './plugin-helpers' | 6 | import './plugin-helpers' |
9 | import './plugin-router' | 7 | import './plugin-router' |
10 | import './plugin-storage' | 8 | import './plugin-storage' |
9 | import './plugin-transcoding' | ||
10 | import './translations' | ||
11 | import './video-constants' | ||
diff --git a/server/tests/plugins/plugin-transcoding.ts b/server/tests/plugins/plugin-transcoding.ts new file mode 100644 index 000000000..96ff4c2fe --- /dev/null +++ b/server/tests/plugins/plugin-transcoding.ts | |||
@@ -0,0 +1,226 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import 'mocha' | ||
4 | import { expect } from 'chai' | ||
5 | import { join } from 'path' | ||
6 | import { getAudioStream, getVideoFileFPS, getVideoStreamFromFile } from '@server/helpers/ffprobe-utils' | ||
7 | import { ServerConfig, VideoDetails, VideoPrivacy } from '@shared/models' | ||
8 | import { | ||
9 | buildServerDirectory, | ||
10 | createLive, | ||
11 | getConfig, | ||
12 | getPluginTestPath, | ||
13 | getVideo, | ||
14 | installPlugin, | ||
15 | sendRTMPStreamInVideo, | ||
16 | setAccessTokensToServers, | ||
17 | setDefaultVideoChannel, | ||
18 | uninstallPlugin, | ||
19 | updateCustomSubConfig, | ||
20 | uploadVideoAndGetId, | ||
21 | waitJobs, | ||
22 | waitUntilLivePublished | ||
23 | } from '../../../shared/extra-utils' | ||
24 | import { cleanupTests, flushAndRunServer, ServerInfo } from '../../../shared/extra-utils/server/servers' | ||
25 | |||
26 | async function createLiveWrapper (server: ServerInfo) { | ||
27 | const liveAttributes = { | ||
28 | name: 'live video', | ||
29 | channelId: server.videoChannel.id, | ||
30 | privacy: VideoPrivacy.PUBLIC | ||
31 | } | ||
32 | |||
33 | const res = await createLive(server.url, server.accessToken, liveAttributes) | ||
34 | return res.body.video.uuid | ||
35 | } | ||
36 | |||
37 | function updateConf (server: ServerInfo, vodProfile: string, liveProfile: string) { | ||
38 | return updateCustomSubConfig(server.url, server.accessToken, { | ||
39 | transcoding: { | ||
40 | enabled: true, | ||
41 | profile: vodProfile, | ||
42 | hls: { | ||
43 | enabled: true | ||
44 | }, | ||
45 | webtorrent: { | ||
46 | enabled: true | ||
47 | } | ||
48 | }, | ||
49 | live: { | ||
50 | transcoding: { | ||
51 | profile: liveProfile, | ||
52 | enabled: true | ||
53 | } | ||
54 | } | ||
55 | }) | ||
56 | } | ||
57 | |||
58 | describe('Test transcoding plugins', function () { | ||
59 | let server: ServerInfo | ||
60 | |||
61 | before(async function () { | ||
62 | this.timeout(60000) | ||
63 | |||
64 | server = await flushAndRunServer(1) | ||
65 | await setAccessTokensToServers([ server ]) | ||
66 | await setDefaultVideoChannel([ server ]) | ||
67 | |||
68 | await updateConf(server, 'default', 'default') | ||
69 | }) | ||
70 | |||
71 | describe('When using a plugin adding profiles to existing encoders', function () { | ||
72 | |||
73 | async function checkVideoFPS (uuid: string, type: 'above' | 'below', fps: number) { | ||
74 | const res = await getVideo(server.url, uuid) | ||
75 | const video = res.body as VideoDetails | ||
76 | const files = video.files.concat(...video.streamingPlaylists.map(p => p.files)) | ||
77 | |||
78 | for (const file of files) { | ||
79 | if (type === 'above') { | ||
80 | expect(file.fps).to.be.above(fps) | ||
81 | } else { | ||
82 | expect(file.fps).to.be.below(fps) | ||
83 | } | ||
84 | } | ||
85 | } | ||
86 | |||
87 | async function checkLiveFPS (uuid: string, type: 'above' | 'below', fps: number) { | ||
88 | const playlistUrl = `${server.url}/static/streaming-playlists/hls/${uuid}/0.m3u8` | ||
89 | const videoFPS = await getVideoFileFPS(playlistUrl) | ||
90 | |||
91 | if (type === 'above') { | ||
92 | expect(videoFPS).to.be.above(fps) | ||
93 | } else { | ||
94 | expect(videoFPS).to.be.below(fps) | ||
95 | } | ||
96 | } | ||
97 | |||
98 | before(async function () { | ||
99 | await installPlugin({ | ||
100 | url: server.url, | ||
101 | accessToken: server.accessToken, | ||
102 | path: getPluginTestPath('-transcoding-one') | ||
103 | }) | ||
104 | }) | ||
105 | |||
106 | it('Should have the appropriate available profiles', async function () { | ||
107 | const res = await getConfig(server.url) | ||
108 | const config = res.body as ServerConfig | ||
109 | |||
110 | expect(config.transcoding.availableProfiles).to.have.members([ 'default', 'low-vod' ]) | ||
111 | expect(config.live.transcoding.availableProfiles).to.have.members([ 'default', 'low-live' ]) | ||
112 | }) | ||
113 | |||
114 | it('Should not use the plugin profile if not chosen by the admin', async function () { | ||
115 | this.timeout(120000) | ||
116 | |||
117 | const videoUUID = (await uploadVideoAndGetId({ server, videoName: 'video' })).uuid | ||
118 | await waitJobs([ server ]) | ||
119 | |||
120 | await checkVideoFPS(videoUUID, 'above', 20) | ||
121 | }) | ||
122 | |||
123 | it('Should use the vod profile', async function () { | ||
124 | this.timeout(120000) | ||
125 | |||
126 | await updateConf(server, 'low-vod', 'default') | ||
127 | |||
128 | const videoUUID = (await uploadVideoAndGetId({ server, videoName: 'video' })).uuid | ||
129 | await waitJobs([ server ]) | ||
130 | |||
131 | await checkVideoFPS(videoUUID, 'below', 12) | ||
132 | }) | ||
133 | |||
134 | it('Should not use the plugin profile if not chosen by the admin', async function () { | ||
135 | this.timeout(120000) | ||
136 | |||
137 | const liveVideoId = await createLiveWrapper(server) | ||
138 | |||
139 | await sendRTMPStreamInVideo(server.url, server.accessToken, liveVideoId, 'video_short2.webm') | ||
140 | await waitUntilLivePublished(server.url, server.accessToken, liveVideoId) | ||
141 | await waitJobs([ server ]) | ||
142 | |||
143 | await checkLiveFPS(liveVideoId, 'above', 20) | ||
144 | }) | ||
145 | |||
146 | it('Should use the live profile', async function () { | ||
147 | this.timeout(120000) | ||
148 | |||
149 | await updateConf(server, 'low-vod', 'low-live') | ||
150 | |||
151 | const liveVideoId = await createLiveWrapper(server) | ||
152 | |||
153 | await sendRTMPStreamInVideo(server.url, server.accessToken, liveVideoId, 'video_short2.webm') | ||
154 | await waitUntilLivePublished(server.url, server.accessToken, liveVideoId) | ||
155 | await waitJobs([ server ]) | ||
156 | |||
157 | await checkLiveFPS(liveVideoId, 'below', 12) | ||
158 | }) | ||
159 | |||
160 | it('Should default to the default profile if the specified profile does not exist', async function () { | ||
161 | this.timeout(120000) | ||
162 | |||
163 | await uninstallPlugin({ url: server.url, accessToken: server.accessToken, npmName: 'peertube-plugin-test-transcoding-one' }) | ||
164 | |||
165 | const res = await getConfig(server.url) | ||
166 | const config = res.body as ServerConfig | ||
167 | |||
168 | expect(config.transcoding.availableProfiles).to.deep.equal([ 'default' ]) | ||
169 | expect(config.live.transcoding.availableProfiles).to.deep.equal([ 'default' ]) | ||
170 | |||
171 | const videoUUID = (await uploadVideoAndGetId({ server, videoName: 'video' })).uuid | ||
172 | await waitJobs([ server ]) | ||
173 | |||
174 | await checkVideoFPS(videoUUID, 'above', 20) | ||
175 | }) | ||
176 | |||
177 | }) | ||
178 | |||
179 | describe('When using a plugin adding new encoders', function () { | ||
180 | |||
181 | before(async function () { | ||
182 | await installPlugin({ | ||
183 | url: server.url, | ||
184 | accessToken: server.accessToken, | ||
185 | path: getPluginTestPath('-transcoding-two') | ||
186 | }) | ||
187 | |||
188 | await updateConf(server, 'test-vod-profile', 'test-live-profile') | ||
189 | }) | ||
190 | |||
191 | it('Should use the new vod encoders', async function () { | ||
192 | this.timeout(240000) | ||
193 | |||
194 | const videoUUID = (await uploadVideoAndGetId({ server, videoName: 'video' })).uuid | ||
195 | await waitJobs([ server ]) | ||
196 | |||
197 | const path = buildServerDirectory(server, join('videos', videoUUID + '-720.mp4')) | ||
198 | const audioProbe = await getAudioStream(path) | ||
199 | expect(audioProbe.audioStream.codec_name).to.equal('opus') | ||
200 | |||
201 | const videoProbe = await getVideoStreamFromFile(path) | ||
202 | expect(videoProbe.codec_name).to.equal('vp9') | ||
203 | }) | ||
204 | |||
205 | it('Should use the new live encoders', async function () { | ||
206 | this.timeout(120000) | ||
207 | |||
208 | const liveVideoId = await createLiveWrapper(server) | ||
209 | |||
210 | await sendRTMPStreamInVideo(server.url, server.accessToken, liveVideoId, 'video_short2.webm') | ||
211 | await waitUntilLivePublished(server.url, server.accessToken, liveVideoId) | ||
212 | await waitJobs([ server ]) | ||
213 | |||
214 | const playlistUrl = `${server.url}/static/streaming-playlists/hls/${liveVideoId}/0.m3u8` | ||
215 | const audioProbe = await getAudioStream(playlistUrl) | ||
216 | expect(audioProbe.audioStream.codec_name).to.equal('opus') | ||
217 | |||
218 | const videoProbe = await getVideoStreamFromFile(playlistUrl) | ||
219 | expect(videoProbe.codec_name).to.equal('h264') | ||
220 | }) | ||
221 | }) | ||
222 | |||
223 | after(async function () { | ||
224 | await cleanupTests([ server ]) | ||
225 | }) | ||
226 | }) | ||
diff --git a/server/types/plugins/register-server-option.model.ts b/server/types/plugins/register-server-option.model.ts index 2e52d1efd..ccd5a060d 100644 --- a/server/types/plugins/register-server-option.model.ts +++ b/server/types/plugins/register-server-option.model.ts | |||
@@ -5,6 +5,7 @@ import { | |||
5 | PluginPlaylistPrivacyManager, | 5 | PluginPlaylistPrivacyManager, |
6 | PluginSettingsManager, | 6 | PluginSettingsManager, |
7 | PluginStorageManager, | 7 | PluginStorageManager, |
8 | PluginTranscodingManager, | ||
8 | PluginVideoCategoryManager, | 9 | PluginVideoCategoryManager, |
9 | PluginVideoLanguageManager, | 10 | PluginVideoLanguageManager, |
10 | PluginVideoLicenceManager, | 11 | PluginVideoLicenceManager, |
@@ -68,6 +69,8 @@ export type RegisterServerOptions = { | |||
68 | videoPrivacyManager: PluginVideoPrivacyManager | 69 | videoPrivacyManager: PluginVideoPrivacyManager |
69 | playlistPrivacyManager: PluginPlaylistPrivacyManager | 70 | playlistPrivacyManager: PluginPlaylistPrivacyManager |
70 | 71 | ||
72 | transcodingManager: PluginTranscodingManager | ||
73 | |||
71 | registerIdAndPassAuth: (options: RegisterServerAuthPassOptions) => void | 74 | registerIdAndPassAuth: (options: RegisterServerAuthPassOptions) => void |
72 | registerExternalAuth: (options: RegisterServerAuthExternalOptions) => RegisterServerAuthExternalResult | 75 | registerExternalAuth: (options: RegisterServerAuthExternalOptions) => RegisterServerAuthExternalResult |
73 | unregisterIdAndPassAuth: (authName: string) => void | 76 | unregisterIdAndPassAuth: (authName: string) => void |
diff --git a/shared/extra-utils/server/config.ts b/shared/extra-utils/server/config.ts index 4e09e0412..8998da8b6 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 | profile: 'default', | ||
115 | resolutions: { | 116 | resolutions: { |
116 | '0p': false, | 117 | '0p': false, |
117 | '240p': false, | 118 | '240p': false, |
@@ -138,6 +139,7 @@ function updateCustomSubConfig (url: string, token: string, newConfig: DeepParti | |||
138 | transcoding: { | 139 | transcoding: { |
139 | enabled: true, | 140 | enabled: true, |
140 | threads: 4, | 141 | threads: 4, |
142 | profile: 'default', | ||
141 | resolutions: { | 143 | resolutions: { |
142 | '240p': true, | 144 | '240p': true, |
143 | '360p': true, | 145 | '360p': true, |
diff --git a/shared/models/plugins/index.ts b/shared/models/plugins/index.ts index 83ed6f583..96621460a 100644 --- a/shared/models/plugins/index.ts +++ b/shared/models/plugins/index.ts | |||
@@ -11,6 +11,7 @@ export * from './plugin-package-json.model' | |||
11 | export * from './plugin-playlist-privacy-manager.model' | 11 | export * from './plugin-playlist-privacy-manager.model' |
12 | export * from './plugin-settings-manager.model' | 12 | export * from './plugin-settings-manager.model' |
13 | export * from './plugin-storage-manager.model' | 13 | export * from './plugin-storage-manager.model' |
14 | export * from './plugin-transcoding-manager.model' | ||
14 | export * from './plugin-translation.model' | 15 | export * from './plugin-translation.model' |
15 | export * from './plugin-video-category-manager.model' | 16 | export * from './plugin-video-category-manager.model' |
16 | export * from './plugin-video-language-manager.model' | 17 | export * from './plugin-video-language-manager.model' |
diff --git a/shared/models/plugins/plugin-transcoding-manager.model.ts b/shared/models/plugins/plugin-transcoding-manager.model.ts new file mode 100644 index 000000000..ff89687e9 --- /dev/null +++ b/shared/models/plugins/plugin-transcoding-manager.model.ts | |||
@@ -0,0 +1,11 @@ | |||
1 | import { EncoderOptionsBuilder } from '../videos/video-transcoding.model' | ||
2 | |||
3 | export interface PluginTranscodingManager { | ||
4 | addLiveProfile (encoder: string, profile: string, builder: EncoderOptionsBuilder): boolean | ||
5 | |||
6 | addVODProfile (encoder: string, profile: string, builder: EncoderOptionsBuilder): boolean | ||
7 | |||
8 | addLiveEncoderPriority (streamType: 'audio' | 'video', encoder: string, priority: number): void | ||
9 | |||
10 | addVODEncoderPriority (streamType: 'audio' | 'video', encoder: string, priority: number): void | ||
11 | } | ||
diff --git a/shared/models/server/custom-config.model.ts b/shared/models/server/custom-config.model.ts index a57237414..d23b8abef 100644 --- a/shared/models/server/custom-config.model.ts +++ b/shared/models/server/custom-config.model.ts | |||
@@ -87,6 +87,9 @@ export interface CustomConfig { | |||
87 | allowAudioFiles: boolean | 87 | allowAudioFiles: boolean |
88 | 88 | ||
89 | threads: number | 89 | threads: number |
90 | |||
91 | profile: string | ||
92 | |||
90 | resolutions: ConfigResolutions & { '0p': boolean } | 93 | resolutions: ConfigResolutions & { '0p': boolean } |
91 | 94 | ||
92 | webtorrent: { | 95 | webtorrent: { |
@@ -110,6 +113,7 @@ export interface CustomConfig { | |||
110 | transcoding: { | 113 | transcoding: { |
111 | enabled: boolean | 114 | enabled: boolean |
112 | threads: number | 115 | threads: number |
116 | profile: string | ||
113 | resolutions: ConfigResolutions | 117 | resolutions: ConfigResolutions |
114 | } | 118 | } |
115 | } | 119 | } |
diff --git a/shared/models/server/server-config.model.ts b/shared/models/server/server-config.model.ts index 47d0e623b..efde4ad9d 100644 --- a/shared/models/server/server-config.model.ts +++ b/shared/models/server/server-config.model.ts | |||
@@ -96,6 +96,9 @@ export interface ServerConfig { | |||
96 | } | 96 | } |
97 | 97 | ||
98 | enabledResolutions: number[] | 98 | enabledResolutions: number[] |
99 | |||
100 | profile: string | ||
101 | availableProfiles: string[] | ||
99 | } | 102 | } |
100 | 103 | ||
101 | live: { | 104 | live: { |
@@ -110,6 +113,9 @@ export interface ServerConfig { | |||
110 | enabled: boolean | 113 | enabled: boolean |
111 | 114 | ||
112 | enabledResolutions: number[] | 115 | enabledResolutions: number[] |
116 | |||
117 | profile: string | ||
118 | availableProfiles: string[] | ||
113 | } | 119 | } |
114 | 120 | ||
115 | rtmp: { | 121 | rtmp: { |
diff --git a/shared/models/videos/index.ts b/shared/models/videos/index.ts index abf144f23..fac3e0b2f 100644 --- a/shared/models/videos/index.ts +++ b/shared/models/videos/index.ts | |||
@@ -34,6 +34,7 @@ export * from './video-state.enum' | |||
34 | export * from './video-streaming-playlist.model' | 34 | export * from './video-streaming-playlist.model' |
35 | export * from './video-streaming-playlist.type' | 35 | export * from './video-streaming-playlist.type' |
36 | 36 | ||
37 | export * from './video-transcoding.model' | ||
37 | export * from './video-transcoding-fps.model' | 38 | export * from './video-transcoding-fps.model' |
38 | 39 | ||
39 | export * from './video-update.model' | 40 | export * from './video-update.model' |
diff --git a/shared/models/videos/video-transcoding.model.ts b/shared/models/videos/video-transcoding.model.ts new file mode 100644 index 000000000..06b555c16 --- /dev/null +++ b/shared/models/videos/video-transcoding.model.ts | |||
@@ -0,0 +1,48 @@ | |||
1 | import { VideoResolution } from './video-resolution.enum' | ||
2 | |||
3 | // Types used by plugins and ffmpeg-utils | ||
4 | |||
5 | export type EncoderOptionsBuilder = (params: { | ||
6 | input: string | ||
7 | resolution: VideoResolution | ||
8 | fps?: number | ||
9 | streamNum?: number | ||
10 | }) => Promise<EncoderOptions> | EncoderOptions | ||
11 | |||
12 | export interface EncoderOptions { | ||
13 | copy?: boolean // Copy stream? Default to false | ||
14 | |||
15 | outputOptions: string[] | ||
16 | } | ||
17 | |||
18 | // All our encoders | ||
19 | |||
20 | export interface EncoderProfile <T> { | ||
21 | [ profile: string ]: T | ||
22 | |||
23 | default: T | ||
24 | } | ||
25 | |||
26 | export type AvailableEncoders = { | ||
27 | available: { | ||
28 | live: { | ||
29 | [ encoder: string ]: EncoderProfile<EncoderOptionsBuilder> | ||
30 | } | ||
31 | |||
32 | vod: { | ||
33 | [ encoder: string ]: EncoderProfile<EncoderOptionsBuilder> | ||
34 | } | ||
35 | } | ||
36 | |||
37 | encodersToTry: { | ||
38 | vod: { | ||
39 | video: string[] | ||
40 | audio: string[] | ||
41 | } | ||
42 | |||
43 | live: { | ||
44 | video: string[] | ||
45 | audio: string[] | ||
46 | } | ||
47 | } | ||
48 | } | ||