diff options
author | frankdelange <yetangitu-f@unternet.org> | 2019-11-01 02:06:19 +0100 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2019-11-25 10:59:47 +0100 |
commit | 5c7d650827cc471a03e7fa18362bcbcbe5d30838 (patch) | |
tree | 41c96e3c9e2dcd4f15166e4f13b427ef116ea4f1 | |
parent | dee6fe1e4f5c024fd387e8c2b306c174b24aa8b3 (diff) | |
download | PeerTube-5c7d650827cc471a03e7fa18362bcbcbe5d30838.tar.gz PeerTube-5c7d650827cc471a03e7fa18362bcbcbe5d30838.tar.zst PeerTube-5c7d650827cc471a03e7fa18362bcbcbe5d30838.zip |
Add audio-only option to transcoders and player
This patch adds an audio-only option to PeerTube by means of a new transcoding configuration which creates mp4 files which only contain an audio stream. This new transcoder has a resolution of '0' and is presented in the preferences and in the player resolution menu as 'Audio-only' (localised). When playing such streams the player shows the file thumbnail as background and disables controls autohide.
Audio-only files can be shared and streamed just like any other file. They can be downloaded as well, the resulting file will be an mp4 container with a single audio stream.
This patch is a proof of concept to show the feasibility of 'true' audio-only support. There are better ways of doing this which also enable multiple audio streams for a given video stream (e.g. DASH) but as this would entail a fundamental change in the way PeerTube works it is a bridge too far for a simple proof of concept.
-rw-r--r-- | client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts | 4 | ||||
-rw-r--r-- | client/src/assets/player/videojs-components/resolution-menu-button.ts | 2 | ||||
-rw-r--r-- | client/src/assets/player/webtorrent/webtorrent-plugin.ts | 16 | ||||
-rw-r--r-- | config/default.yaml | 1 | ||||
-rw-r--r-- | config/production.yaml.example | 1 | ||||
-rw-r--r-- | config/test.yaml | 1 | ||||
-rw-r--r-- | server/controllers/api/config.ts | 2 | ||||
-rw-r--r-- | server/helpers/ffmpeg-utils.ts | 45 | ||||
-rw-r--r-- | server/initializers/config.ts | 1 | ||||
-rw-r--r-- | server/lib/video-transcoding.ts | 46 | ||||
-rw-r--r-- | server/middlewares/validators/config.ts | 1 | ||||
-rw-r--r-- | server/tests/api/check-params/config.ts | 1 | ||||
-rw-r--r-- | server/tests/api/server/config.ts | 1 | ||||
-rw-r--r-- | shared/extra-utils/server/config.ts | 1 | ||||
-rw-r--r-- | shared/models/server/custom-config.model.ts | 1 | ||||
-rw-r--r-- | shared/models/videos/video-resolution.enum.ts | 5 |
16 files changed, 115 insertions, 14 deletions
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 8411c4f4f..5f23c80a2 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 | |||
@@ -36,6 +36,10 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit { | |||
36 | super() | 36 | super() |
37 | 37 | ||
38 | this.resolutions = [ | 38 | this.resolutions = [ |
39 | { | ||
40 | id: '0p', | ||
41 | label: this.i18n('Audio-only') | ||
42 | }, | ||
39 | { | 43 | { |
40 | id: '240p', | 44 | id: '240p', |
41 | label: this.i18n('240p') | 45 | label: this.i18n('240p') |
diff --git a/client/src/assets/player/videojs-components/resolution-menu-button.ts b/client/src/assets/player/videojs-components/resolution-menu-button.ts index aeb48888f..445b14b2b 100644 --- a/client/src/assets/player/videojs-components/resolution-menu-button.ts +++ b/client/src/assets/player/videojs-components/resolution-menu-button.ts | |||
@@ -79,7 +79,7 @@ class ResolutionMenuButton extends MenuButton { | |||
79 | this.player_, | 79 | this.player_, |
80 | { | 80 | { |
81 | id: d.id, | 81 | id: d.id, |
82 | label: d.label, | 82 | label: d.id == 0 ? this.player .localize('Audio-only') : d.label, |
83 | selected: d.selected, | 83 | selected: d.selected, |
84 | callback: data.qualitySwitchCallback | 84 | callback: data.qualitySwitchCallback |
85 | }) | 85 | }) |
diff --git a/client/src/assets/player/webtorrent/webtorrent-plugin.ts b/client/src/assets/player/webtorrent/webtorrent-plugin.ts index 4a0b38703..007fc58cc 100644 --- a/client/src/assets/player/webtorrent/webtorrent-plugin.ts +++ b/client/src/assets/player/webtorrent/webtorrent-plugin.ts | |||
@@ -181,20 +181,29 @@ class WebTorrentPlugin extends Plugin { | |||
181 | const currentTime = this.player.currentTime() | 181 | const currentTime = this.player.currentTime() |
182 | const isPaused = this.player.paused() | 182 | const isPaused = this.player.paused() |
183 | 183 | ||
184 | // Remove poster to have black background | ||
185 | this.playerElement.poster = '' | ||
186 | |||
187 | // Hide bigPlayButton | 184 | // Hide bigPlayButton |
188 | if (!isPaused) { | 185 | if (!isPaused) { |
189 | this.player.bigPlayButton.hide() | 186 | this.player.bigPlayButton.hide() |
190 | } | 187 | } |
191 | 188 | ||
189 | // Audio-only (resolutionId == 0) gets special treatment | ||
190 | if (resolutionId > 0) { | ||
191 | // Hide poster to have black background | ||
192 | this.player.removeClass('vjs-playing-audio-only-content') | ||
193 | this.player.posterImage.hide() | ||
194 | } else { | ||
195 | // Audio-only: show poster, do not auto-hide controls | ||
196 | this.player.addClass('vjs-playing-audio-only-content') | ||
197 | this.player.posterImage.show() | ||
198 | } | ||
199 | |||
192 | const newVideoFile = this.videoFiles.find(f => f.resolution.id === resolutionId) | 200 | const newVideoFile = this.videoFiles.find(f => f.resolution.id === resolutionId) |
193 | const options = { | 201 | const options = { |
194 | forcePlay: false, | 202 | forcePlay: false, |
195 | delay, | 203 | delay, |
196 | seek: currentTime + (delay / 1000) | 204 | seek: currentTime + (delay / 1000) |
197 | } | 205 | } |
206 | |||
198 | this.updateVideoFile(newVideoFile, options) | 207 | this.updateVideoFile(newVideoFile, options) |
199 | } | 208 | } |
200 | 209 | ||
@@ -327,6 +336,7 @@ class WebTorrentPlugin extends Plugin { | |||
327 | this.player.posterImage.show() | 336 | this.player.posterImage.show() |
328 | this.player.removeClass('vjs-has-autoplay') | 337 | this.player.removeClass('vjs-has-autoplay') |
329 | this.player.removeClass('vjs-has-big-play-button-clicked') | 338 | this.player.removeClass('vjs-has-big-play-button-clicked') |
339 | this.player.removeClass('vjs-playing-audio-only-content') | ||
330 | 340 | ||
331 | return done() | 341 | return done() |
332 | }) | 342 | }) |
diff --git a/config/default.yaml b/config/default.yaml index 9d102f760..07fd4d24f 100644 --- a/config/default.yaml +++ b/config/default.yaml | |||
@@ -203,6 +203,7 @@ transcoding: | |||
203 | allow_audio_files: true | 203 | allow_audio_files: true |
204 | threads: 1 | 204 | threads: 1 |
205 | resolutions: # Only created if the original video has a higher resolution, uses more storage! | 205 | resolutions: # Only created if the original video has a higher resolution, uses more storage! |
206 | 0p: false # audio-only (creates mp4 without video stream) | ||
206 | 240p: false | 207 | 240p: false |
207 | 360p: false | 208 | 360p: false |
208 | 480p: false | 209 | 480p: false |
diff --git a/config/production.yaml.example b/config/production.yaml.example index 68ae22944..d7bbc39cf 100644 --- a/config/production.yaml.example +++ b/config/production.yaml.example | |||
@@ -217,6 +217,7 @@ transcoding: | |||
217 | allow_audio_files: true | 217 | allow_audio_files: true |
218 | threads: 1 | 218 | threads: 1 |
219 | resolutions: # Only created if the original video has a higher resolution, uses more storage! | 219 | resolutions: # Only created if the original video has a higher resolution, uses more storage! |
220 | 0p: false # audio-only (creates mp4 without video stream) | ||
220 | 240p: false | 221 | 240p: false |
221 | 360p: false | 222 | 360p: false |
222 | 480p: false | 223 | 480p: false |
diff --git a/config/test.yaml b/config/test.yaml index 8843bb2dc..eedd28537 100644 --- a/config/test.yaml +++ b/config/test.yaml | |||
@@ -72,6 +72,7 @@ transcoding: | |||
72 | allow_audio_files: false | 72 | allow_audio_files: false |
73 | threads: 2 | 73 | threads: 2 |
74 | resolutions: | 74 | resolutions: |
75 | 0p: true | ||
75 | 240p: true | 76 | 240p: true |
76 | 360p: true | 77 | 360p: true |
77 | 480p: true | 78 | 480p: true |
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts index 70e8aa970..8a00f9835 100644 --- a/server/controllers/api/config.ts +++ b/server/controllers/api/config.ts | |||
@@ -300,6 +300,7 @@ function customConfig (): CustomConfig { | |||
300 | allowAudioFiles: CONFIG.TRANSCODING.ALLOW_AUDIO_FILES, | 300 | allowAudioFiles: CONFIG.TRANSCODING.ALLOW_AUDIO_FILES, |
301 | threads: CONFIG.TRANSCODING.THREADS, | 301 | threads: CONFIG.TRANSCODING.THREADS, |
302 | resolutions: { | 302 | resolutions: { |
303 | '0p': CONFIG.TRANSCODING.RESOLUTIONS[ '0p' ], | ||
303 | '240p': CONFIG.TRANSCODING.RESOLUTIONS[ '240p' ], | 304 | '240p': CONFIG.TRANSCODING.RESOLUTIONS[ '240p' ], |
304 | '360p': CONFIG.TRANSCODING.RESOLUTIONS[ '360p' ], | 305 | '360p': CONFIG.TRANSCODING.RESOLUTIONS[ '360p' ], |
305 | '480p': CONFIG.TRANSCODING.RESOLUTIONS[ '480p' ], | 306 | '480p': CONFIG.TRANSCODING.RESOLUTIONS[ '480p' ], |
@@ -356,6 +357,7 @@ function convertCustomConfigBody (body: CustomConfig) { | |||
356 | function keyConverter (k: string) { | 357 | function keyConverter (k: string) { |
357 | // Transcoding resolutions exception | 358 | // Transcoding resolutions exception |
358 | if (/^\d{3,4}p$/.exec(k)) return k | 359 | if (/^\d{3,4}p$/.exec(k)) return k |
360 | if (/^0p$/.exec(k)) return k | ||
359 | 361 | ||
360 | return snakeCase(k) | 362 | return snakeCase(k) |
361 | } | 363 | } |
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts index 7a4ac0970..2d9ce2bfa 100644 --- a/server/helpers/ffmpeg-utils.ts +++ b/server/helpers/ffmpeg-utils.ts | |||
@@ -14,6 +14,7 @@ function computeResolutionsToTranscode (videoFileHeight: number) { | |||
14 | 14 | ||
15 | // Put in the order we want to proceed jobs | 15 | // Put in the order we want to proceed jobs |
16 | const resolutions = [ | 16 | const resolutions = [ |
17 | VideoResolution.H_NOVIDEO, | ||
17 | VideoResolution.H_480P, | 18 | VideoResolution.H_480P, |
18 | VideoResolution.H_360P, | 19 | VideoResolution.H_360P, |
19 | VideoResolution.H_720P, | 20 | VideoResolution.H_720P, |
@@ -34,10 +35,15 @@ function computeResolutionsToTranscode (videoFileHeight: number) { | |||
34 | async function getVideoFileSize (path: string) { | 35 | async function getVideoFileSize (path: string) { |
35 | const videoStream = await getVideoStreamFromFile(path) | 36 | const videoStream = await getVideoStreamFromFile(path) |
36 | 37 | ||
37 | return { | 38 | return videoStream == null |
38 | width: videoStream.width, | 39 | ? { |
39 | height: videoStream.height | 40 | width: 0, |
40 | } | 41 | height: 0 |
42 | } | ||
43 | : { | ||
44 | width: videoStream.width, | ||
45 | height: videoStream.height | ||
46 | } | ||
41 | } | 47 | } |
42 | 48 | ||
43 | async function getVideoFileResolution (path: string) { | 49 | async function getVideoFileResolution (path: string) { |
@@ -52,6 +58,10 @@ async function getVideoFileResolution (path: string) { | |||
52 | async function getVideoFileFPS (path: string) { | 58 | async function getVideoFileFPS (path: string) { |
53 | const videoStream = await getVideoStreamFromFile(path) | 59 | const videoStream = await getVideoStreamFromFile(path) |
54 | 60 | ||
61 | if (videoStream == null) { | ||
62 | return 0 | ||
63 | } | ||
64 | |||
55 | for (const key of [ 'avg_frame_rate', 'r_frame_rate' ]) { | 65 | for (const key of [ 'avg_frame_rate', 'r_frame_rate' ]) { |
56 | const valuesText: string = videoStream[key] | 66 | const valuesText: string = videoStream[key] |
57 | if (!valuesText) continue | 67 | if (!valuesText) continue |
@@ -118,7 +128,7 @@ async function generateImageFromVideoFile (fromPath: string, folder: string, ima | |||
118 | } | 128 | } |
119 | } | 129 | } |
120 | 130 | ||
121 | type TranscodeOptionsType = 'hls' | 'quick-transcode' | 'video' | 'merge-audio' | 131 | type TranscodeOptionsType = 'hls' | 'quick-transcode' | 'video' | 'merge-audio' | 'split-audio' |
122 | 132 | ||
123 | interface BaseTranscodeOptions { | 133 | interface BaseTranscodeOptions { |
124 | type: TranscodeOptionsType | 134 | type: TranscodeOptionsType |
@@ -149,7 +159,11 @@ interface MergeAudioTranscodeOptions extends BaseTranscodeOptions { | |||
149 | audioPath: string | 159 | audioPath: string |
150 | } | 160 | } |
151 | 161 | ||
152 | type TranscodeOptions = HLSTranscodeOptions | VideoTranscodeOptions | MergeAudioTranscodeOptions | QuickTranscodeOptions | 162 | interface SplitAudioTranscodeOptions extends BaseTranscodeOptions { |
163 | type: 'split-audio' | ||
164 | } | ||
165 | |||
166 | type TranscodeOptions = HLSTranscodeOptions | VideoTranscodeOptions | MergeAudioTranscodeOptions | SplitAudioTranscodeOptions | QuickTranscodeOptions | ||
153 | 167 | ||
154 | function transcode (options: TranscodeOptions) { | 168 | function transcode (options: TranscodeOptions) { |
155 | return new Promise<void>(async (res, rej) => { | 169 | return new Promise<void>(async (res, rej) => { |
@@ -163,6 +177,8 @@ function transcode (options: TranscodeOptions) { | |||
163 | command = await buildHLSCommand(command, options) | 177 | command = await buildHLSCommand(command, options) |
164 | } else if (options.type === 'merge-audio') { | 178 | } else if (options.type === 'merge-audio') { |
165 | command = await buildAudioMergeCommand(command, options) | 179 | command = await buildAudioMergeCommand(command, options) |
180 | } else if (options.type === 'split-audio') { | ||
181 | command = await buildAudioSplitCommand(command, options) | ||
166 | } else { | 182 | } else { |
167 | command = await buildx264Command(command, options) | 183 | command = await buildx264Command(command, options) |
168 | } | 184 | } |
@@ -198,6 +214,7 @@ async function canDoQuickTranscode (path: string): Promise<boolean> { | |||
198 | const resolution = await getVideoFileResolution(path) | 214 | const resolution = await getVideoFileResolution(path) |
199 | 215 | ||
200 | // check video params | 216 | // check video params |
217 | if (videoStream == null) return false | ||
201 | if (videoStream[ 'codec_name' ] !== 'h264') return false | 218 | if (videoStream[ 'codec_name' ] !== 'h264') return false |
202 | if (videoStream[ 'pix_fmt' ] !== 'yuv420p') return false | 219 | if (videoStream[ 'pix_fmt' ] !== 'yuv420p') return false |
203 | if (fps < VIDEO_TRANSCODING_FPS.MIN || fps > VIDEO_TRANSCODING_FPS.MAX) return false | 220 | if (fps < VIDEO_TRANSCODING_FPS.MIN || fps > VIDEO_TRANSCODING_FPS.MAX) return false |
@@ -276,6 +293,12 @@ async function buildAudioMergeCommand (command: ffmpeg.FfmpegCommand, options: M | |||
276 | return command | 293 | return command |
277 | } | 294 | } |
278 | 295 | ||
296 | async function buildAudioSplitCommand (command: ffmpeg.FfmpegCommand, options: SplitAudioTranscodeOptions) { | ||
297 | command = await presetAudioSplit(command) | ||
298 | |||
299 | return command | ||
300 | } | ||
301 | |||
279 | async function buildQuickTranscodeCommand (command: ffmpeg.FfmpegCommand) { | 302 | async function buildQuickTranscodeCommand (command: ffmpeg.FfmpegCommand) { |
280 | command = await presetCopy(command) | 303 | command = await presetCopy(command) |
281 | 304 | ||
@@ -327,7 +350,7 @@ function getVideoStreamFromFile (path: string) { | |||
327 | if (err) return rej(err) | 350 | if (err) return rej(err) |
328 | 351 | ||
329 | const videoStream = metadata.streams.find(s => s.codec_type === 'video') | 352 | const videoStream = metadata.streams.find(s => s.codec_type === 'video') |
330 | if (!videoStream) return rej(new Error('Cannot find video stream of ' + path)) | 353 | //if (!videoStream) return rej(new Error('Cannot find video stream of ' + path)) |
331 | 354 | ||
332 | return res(videoStream) | 355 | return res(videoStream) |
333 | }) | 356 | }) |
@@ -482,3 +505,11 @@ async function presetCopy (command: ffmpeg.FfmpegCommand): Promise<ffmpeg.Ffmpeg | |||
482 | .videoCodec('copy') | 505 | .videoCodec('copy') |
483 | .audioCodec('copy') | 506 | .audioCodec('copy') |
484 | } | 507 | } |
508 | |||
509 | |||
510 | async function presetAudioSplit (command: ffmpeg.FfmpegCommand): Promise<ffmpeg.FfmpegCommand> { | ||
511 | return command | ||
512 | .format('mp4') | ||
513 | .audioCodec('copy') | ||
514 | .noVideo() | ||
515 | } | ||
diff --git a/server/initializers/config.ts b/server/initializers/config.ts index 6d5d55487..c6e478f57 100644 --- a/server/initializers/config.ts +++ b/server/initializers/config.ts | |||
@@ -168,6 +168,7 @@ const CONFIG = { | |||
168 | get ALLOW_AUDIO_FILES () { return config.get<boolean>('transcoding.allow_audio_files') }, | 168 | get ALLOW_AUDIO_FILES () { return config.get<boolean>('transcoding.allow_audio_files') }, |
169 | get THREADS () { return config.get<number>('transcoding.threads') }, | 169 | get THREADS () { return config.get<number>('transcoding.threads') }, |
170 | RESOLUTIONS: { | 170 | RESOLUTIONS: { |
171 | get '0p' () { return config.get<boolean>('transcoding.resolutions.0p') }, | ||
171 | get '240p' () { return config.get<boolean>('transcoding.resolutions.240p') }, | 172 | get '240p' () { return config.get<boolean>('transcoding.resolutions.240p') }, |
172 | get '360p' () { return config.get<boolean>('transcoding.resolutions.360p') }, | 173 | get '360p' () { return config.get<boolean>('transcoding.resolutions.360p') }, |
173 | get '480p' () { return config.get<boolean>('transcoding.resolutions.480p') }, | 174 | get '480p' () { return config.get<boolean>('transcoding.resolutions.480p') }, |
diff --git a/server/lib/video-transcoding.ts b/server/lib/video-transcoding.ts index 9243d1742..9dd54837f 100644 --- a/server/lib/video-transcoding.ts +++ b/server/lib/video-transcoding.ts | |||
@@ -81,12 +81,52 @@ async function transcodeNewResolution (video: MVideoWithFile, resolution: VideoR | |||
81 | const videoOutputPath = getVideoFilePath(video, newVideoFile) | 81 | const videoOutputPath = getVideoFilePath(video, newVideoFile) |
82 | const videoTranscodedPath = join(transcodeDirectory, getVideoFilename(video, newVideoFile)) | 82 | const videoTranscodedPath = join(transcodeDirectory, getVideoFilename(video, newVideoFile)) |
83 | 83 | ||
84 | const transcodeOptions = resolution === VideoResolution.H_NOVIDEO | ||
85 | ? { | ||
86 | type: 'split-audio' as 'split-audio', | ||
87 | inputPath: videoInputPath, | ||
88 | outputPath: videoTranscodedPath, | ||
89 | resolution, | ||
90 | } | ||
91 | : { | ||
92 | type: 'video' as 'video', | ||
93 | inputPath: videoInputPath, | ||
94 | outputPath: videoTranscodedPath, | ||
95 | resolution, | ||
96 | isPortraitMode: isPortrait | ||
97 | } | ||
98 | |||
99 | await transcode(transcodeOptions) | ||
100 | |||
101 | return onVideoFileTranscoding(video, newVideoFile, videoTranscodedPath, videoOutputPath) | ||
102 | } | ||
103 | |||
104 | /** | ||
105 | * Extract audio into a separate audio-only mp4. | ||
106 | */ | ||
107 | async function splitAudioFile (video: MVideoWithFile) { | ||
108 | const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR | ||
109 | const transcodeDirectory = CONFIG.STORAGE.TMP_DIR | ||
110 | const extname = '.mp4' | ||
111 | const resolution = VideoResolution.H_NOVIDEO | ||
112 | |||
113 | // We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed | ||
114 | const videoInputPath = join(videosDirectory, video.getVideoFilename(video.getOriginalFile())) | ||
115 | |||
116 | const newVideoFile = new VideoFileModel({ | ||
117 | resolution, | ||
118 | extname, | ||
119 | size: 0, | ||
120 | videoId: video.id | ||
121 | }) | ||
122 | const videoOutputPath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename(newVideoFile)) | ||
123 | const videoTranscodedPath = join(transcodeDirectory, video.getVideoFilename(newVideoFile)) | ||
124 | |||
84 | const transcodeOptions = { | 125 | const transcodeOptions = { |
85 | type: 'video' as 'video', | 126 | type: 'split-audio' as 'split-audio', |
86 | inputPath: videoInputPath, | 127 | inputPath: videoInputPath, |
87 | outputPath: videoTranscodedPath, | 128 | outputPath: videoTranscodedPath, |
88 | resolution, | 129 | resolution |
89 | isPortraitMode: isPortrait | ||
90 | } | 130 | } |
91 | 131 | ||
92 | await transcode(transcodeOptions) | 132 | await transcode(transcodeOptions) |
diff --git a/server/middlewares/validators/config.ts b/server/middlewares/validators/config.ts index 1db907f91..d86fa700b 100644 --- a/server/middlewares/validators/config.ts +++ b/server/middlewares/validators/config.ts | |||
@@ -37,6 +37,7 @@ const customConfigUpdateValidator = [ | |||
37 | body('transcoding.enabled').isBoolean().withMessage('Should have a valid transcoding enabled boolean'), | 37 | body('transcoding.enabled').isBoolean().withMessage('Should have a valid transcoding enabled boolean'), |
38 | body('transcoding.allowAdditionalExtensions').isBoolean().withMessage('Should have a valid additional extensions boolean'), | 38 | body('transcoding.allowAdditionalExtensions').isBoolean().withMessage('Should have a valid additional extensions boolean'), |
39 | body('transcoding.threads').isInt().withMessage('Should have a valid transcoding threads number'), | 39 | body('transcoding.threads').isInt().withMessage('Should have a valid transcoding threads number'), |
40 | body('transcoding.resolutions.0p').isBoolean().withMessage('Should have a valid transcoding 0p resolution enabled boolean'), | ||
40 | body('transcoding.resolutions.240p').isBoolean().withMessage('Should have a valid transcoding 240p resolution enabled boolean'), | 41 | body('transcoding.resolutions.240p').isBoolean().withMessage('Should have a valid transcoding 240p resolution enabled boolean'), |
41 | body('transcoding.resolutions.360p').isBoolean().withMessage('Should have a valid transcoding 360p resolution enabled boolean'), | 42 | body('transcoding.resolutions.360p').isBoolean().withMessage('Should have a valid transcoding 360p resolution enabled boolean'), |
42 | body('transcoding.resolutions.480p').isBoolean().withMessage('Should have a valid transcoding 480p resolution enabled boolean'), | 43 | body('transcoding.resolutions.480p').isBoolean().withMessage('Should have a valid transcoding 480p resolution enabled boolean'), |
diff --git a/server/tests/api/check-params/config.ts b/server/tests/api/check-params/config.ts index 3c558d4ea..443fbcb60 100644 --- a/server/tests/api/check-params/config.ts +++ b/server/tests/api/check-params/config.ts | |||
@@ -85,6 +85,7 @@ describe('Test config API validators', function () { | |||
85 | allowAudioFiles: true, | 85 | allowAudioFiles: true, |
86 | threads: 1, | 86 | threads: 1, |
87 | resolutions: { | 87 | resolutions: { |
88 | '0p': false, | ||
88 | '240p': false, | 89 | '240p': false, |
89 | '360p': true, | 90 | '360p': true, |
90 | '480p': true, | 91 | '480p': true, |
diff --git a/server/tests/api/server/config.ts b/server/tests/api/server/config.ts index a494858b3..cf99e5c0a 100644 --- a/server/tests/api/server/config.ts +++ b/server/tests/api/server/config.ts | |||
@@ -274,6 +274,7 @@ describe('Test config', function () { | |||
274 | allowAudioFiles: true, | 274 | allowAudioFiles: true, |
275 | threads: 1, | 275 | threads: 1, |
276 | resolutions: { | 276 | resolutions: { |
277 | '0p': false, | ||
277 | '240p': false, | 278 | '240p': false, |
278 | '360p': true, | 279 | '360p': true, |
279 | '480p': true, | 280 | '480p': true, |
diff --git a/shared/extra-utils/server/config.ts b/shared/extra-utils/server/config.ts index ada173313..35b08477f 100644 --- a/shared/extra-utils/server/config.ts +++ b/shared/extra-utils/server/config.ts | |||
@@ -111,6 +111,7 @@ function updateCustomSubConfig (url: string, token: string, newConfig: DeepParti | |||
111 | allowAudioFiles: true, | 111 | allowAudioFiles: true, |
112 | threads: 1, | 112 | threads: 1, |
113 | resolutions: { | 113 | resolutions: { |
114 | '0p': false, | ||
114 | '240p': false, | 115 | '240p': false, |
115 | '360p': true, | 116 | '360p': true, |
116 | '480p': true, | 117 | '480p': true, |
diff --git a/shared/models/server/custom-config.model.ts b/shared/models/server/custom-config.model.ts index 97972b759..032b91a29 100644 --- a/shared/models/server/custom-config.model.ts +++ b/shared/models/server/custom-config.model.ts | |||
@@ -75,6 +75,7 @@ export interface CustomConfig { | |||
75 | 75 | ||
76 | threads: number | 76 | threads: number |
77 | resolutions: { | 77 | resolutions: { |
78 | '0p': boolean | ||
78 | '240p': boolean | 79 | '240p': boolean |
79 | '360p': boolean | 80 | '360p': boolean |
80 | '480p': boolean | 81 | '480p': boolean |
diff --git a/shared/models/videos/video-resolution.enum.ts b/shared/models/videos/video-resolution.enum.ts index fa26fc3cc..dc53294f6 100644 --- a/shared/models/videos/video-resolution.enum.ts +++ b/shared/models/videos/video-resolution.enum.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import { VideoTranscodingFPS } from './video-transcoding-fps.model' | 1 | import { VideoTranscodingFPS } from './video-transcoding-fps.model' |
2 | 2 | ||
3 | export enum VideoResolution { | 3 | export enum VideoResolution { |
4 | H_NOVIDEO = 0, | ||
4 | H_240P = 240, | 5 | H_240P = 240, |
5 | H_360P = 360, | 6 | H_360P = 360, |
6 | H_480P = 480, | 7 | H_480P = 480, |
@@ -18,6 +19,10 @@ export enum VideoResolution { | |||
18 | */ | 19 | */ |
19 | function getBaseBitrate (resolution: VideoResolution) { | 20 | function getBaseBitrate (resolution: VideoResolution) { |
20 | switch (resolution) { | 21 | switch (resolution) { |
22 | case VideoResolution.H_NOVIDEO: | ||
23 | // audio-only | ||
24 | return 64 * 1000 | ||
25 | |||
21 | case VideoResolution.H_240P: | 26 | case VideoResolution.H_240P: |
22 | // quality according to Google Live Encoder: 300 - 700 Kbps | 27 | // quality according to Google Live Encoder: 300 - 700 Kbps |
23 | // Quality according to YouTube Video Info: 186 Kbps | 28 | // Quality according to YouTube Video Info: 186 Kbps |