aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts4
-rw-r--r--client/src/assets/player/videojs-components/resolution-menu-button.ts2
-rw-r--r--client/src/assets/player/webtorrent/webtorrent-plugin.ts16
-rw-r--r--config/default.yaml1
-rw-r--r--config/production.yaml.example1
-rw-r--r--config/test.yaml1
-rw-r--r--server/controllers/api/config.ts2
-rw-r--r--server/helpers/ffmpeg-utils.ts45
-rw-r--r--server/initializers/config.ts1
-rw-r--r--server/lib/video-transcoding.ts46
-rw-r--r--server/middlewares/validators/config.ts1
-rw-r--r--server/tests/api/check-params/config.ts1
-rw-r--r--server/tests/api/server/config.ts1
-rw-r--r--shared/extra-utils/server/config.ts1
-rw-r--r--shared/models/server/custom-config.model.ts1
-rw-r--r--shared/models/videos/video-resolution.enum.ts5
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) {
34async function getVideoFileSize (path: string) { 35async 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
43async function getVideoFileResolution (path: string) { 49async function getVideoFileResolution (path: string) {
@@ -52,6 +58,10 @@ async function getVideoFileResolution (path: string) {
52async function getVideoFileFPS (path: string) { 58async 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
121type TranscodeOptionsType = 'hls' | 'quick-transcode' | 'video' | 'merge-audio' 131type TranscodeOptionsType = 'hls' | 'quick-transcode' | 'video' | 'merge-audio' | 'split-audio'
122 132
123interface BaseTranscodeOptions { 133interface 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
152type TranscodeOptions = HLSTranscodeOptions | VideoTranscodeOptions | MergeAudioTranscodeOptions | QuickTranscodeOptions 162interface SplitAudioTranscodeOptions extends BaseTranscodeOptions {
163 type: 'split-audio'
164}
165
166type TranscodeOptions = HLSTranscodeOptions | VideoTranscodeOptions | MergeAudioTranscodeOptions | SplitAudioTranscodeOptions | QuickTranscodeOptions
153 167
154function transcode (options: TranscodeOptions) { 168function 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
296async function buildAudioSplitCommand (command: ffmpeg.FfmpegCommand, options: SplitAudioTranscodeOptions) {
297 command = await presetAudioSplit(command)
298
299 return command
300}
301
279async function buildQuickTranscodeCommand (command: ffmpeg.FfmpegCommand) { 302async 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
510async 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 */
107async 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 @@
1import { VideoTranscodingFPS } from './video-transcoding-fps.model' 1import { VideoTranscodingFPS } from './video-transcoding-fps.model'
2 2
3export enum VideoResolution { 3export 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 */
19function getBaseBitrate (resolution: VideoResolution) { 20function 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