aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorFelix Ableitner <me@nutomic.com>2018-10-08 09:26:04 -0500
committerChocobozzz <me@florianbigard.com>2018-10-08 16:26:04 +0200
commitedb4ffc7e0b13659d7c73b120f2c87b27e4c26a1 (patch)
treefb9df6826eaeb23ab3bcac7fe21773978c68d27c
parent2cae5f13076a31aa95774679aed1f13c3bd5f8ce (diff)
downloadPeerTube-edb4ffc7e0b13659d7c73b120f2c87b27e4c26a1.tar.gz
PeerTube-edb4ffc7e0b13659d7c73b120f2c87b27e4c26a1.tar.zst
PeerTube-edb4ffc7e0b13659d7c73b120f2c87b27e4c26a1.zip
Set bitrate limits for transcoding (fixes #638) (#1135)
* Set bitrate limits for transcoding (fixes #638) * added optimization script and test, changed stuff * fix test, improve docs * re-add optimize-old-videos script * added documentation * Don't optimize videos without valid UUID, or redundancy videos * move getUUIDFromFilename * fix tests? * update torrent and file size, some more fixes/improvements * use higher bitrate for high fps video, adjust bitrates * add test video * don't throw error if resolution is undefined * generate test fixture on the fly * use random noise video for bitrate test, add promise * shorten test video to avoid timeout * use existing function to optimize video * various fixes * increase test timeout * limit test fixture size, add link * test fixes * add await * more test fixes, add -b:v parameter * replace ffmpeg wiki link * fix ffmpeg params * fix unit test * add test fixture to .gitgnore * add video transcoding fps model * add missing file
-rw-r--r--.gitignore1
-rw-r--r--package.json1
-rwxr-xr-xscripts/help.sh1
-rw-r--r--scripts/optimize-old-videos.ts36
-rwxr-xr-xscripts/prune-storage.ts10
-rw-r--r--server/helpers/ffmpeg-utils.ts21
-rw-r--r--server/helpers/utils.ts17
-rw-r--r--server/initializers/constants.ts4
-rw-r--r--server/lib/activitypub/crawl.ts2
-rw-r--r--server/lib/job-queue/handlers/video-file.ts4
-rw-r--r--server/lib/video-transcoding.ts15
-rw-r--r--server/tests/api/videos/video-transcoder.ts62
-rw-r--r--shared/models/videos/index.ts1
-rw-r--r--shared/models/videos/video-resolution.enum.ts55
-rw-r--r--shared/models/videos/video-transcoding-fps.model.ts6
-rw-r--r--support/doc/tools.md11
16 files changed, 221 insertions, 26 deletions
diff --git a/.gitignore b/.gitignore
index 22478c444..a31da70a9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,6 +9,7 @@
9/test4/ 9/test4/
10/test5/ 10/test5/
11/test6/ 11/test6/
12/server/tests/fixtures/video_high_bitrate_1080p.mp4
12 13
13# Production 14# Production
14/storage/ 15/storage/
diff --git a/package.json b/package.json
index 80d5a04ac..034b40cbc 100644
--- a/package.json
+++ b/package.json
@@ -51,6 +51,7 @@
51 "generate-api-doc": "scripty", 51 "generate-api-doc": "scripty",
52 "parse-log": "node ./dist/scripts/parse-log.js", 52 "parse-log": "node ./dist/scripts/parse-log.js",
53 "prune-storage": "node ./dist/scripts/prune-storage.js", 53 "prune-storage": "node ./dist/scripts/prune-storage.js",
54 "optimize-old-videos": "node ./dist/scripts/optimize-old-videos.js",
54 "postinstall": "cd client && yarn install --pure-lockfile", 55 "postinstall": "cd client && yarn install --pure-lockfile",
55 "tsc": "tsc", 56 "tsc": "tsc",
56 "spectacle-docs": "node_modules/spectacle-docs/bin/spectacle.js", 57 "spectacle-docs": "node_modules/spectacle-docs/bin/spectacle.js",
diff --git a/scripts/help.sh b/scripts/help.sh
index 8ac090139..bc38bdb40 100755
--- a/scripts/help.sh
+++ b/scripts/help.sh
@@ -18,6 +18,7 @@ printf " reset-password -- -u [user] -> Reset the password of user [user]\n"
18printf " create-transcoding-job -- -v [video UUID] \n" 18printf " create-transcoding-job -- -v [video UUID] \n"
19printf " -> Create a transcoding job for a particular video\n" 19printf " -> Create a transcoding job for a particular video\n"
20printf " prune-storage -> Delete (after confirmation) unknown video files/thumbnails/previews... (due to a bad video deletion, transcoding job not finished...)\n" 20printf " prune-storage -> Delete (after confirmation) unknown video files/thumbnails/previews... (due to a bad video deletion, transcoding job not finished...)\n"
21printf " optimize-old-videos -> Re-transcode videos that have a high bitrate, to make them suitable for streaming over slow connections"
21printf " dev -> Watch, run the livereload and run the server so that you can develop the application\n" 22printf " dev -> Watch, run the livereload and run the server so that you can develop the application\n"
22printf " start -> Run the server\n" 23printf " start -> Run the server\n"
23printf " update-host -> Upgrade scheme/host in torrent files according to the webserver configuration (config/ folder)\n" 24printf " update-host -> Upgrade scheme/host in torrent files according to the webserver configuration (config/ folder)\n"
diff --git a/scripts/optimize-old-videos.ts b/scripts/optimize-old-videos.ts
new file mode 100644
index 000000000..ab44acfbe
--- /dev/null
+++ b/scripts/optimize-old-videos.ts
@@ -0,0 +1,36 @@
1import { join } from 'path'
2import { readdir } from 'fs-extra'
3import { CONFIG, VIDEO_TRANSCODING_FPS } from '../server/initializers/constants'
4import { getVideoFileResolution, getVideoFileBitrate, getVideoFileFPS } from '../server/helpers/ffmpeg-utils'
5import { getMaxBitrate } from '../shared/models/videos'
6import { VideoRedundancyModel } from '../server/models/redundancy/video-redundancy'
7import { VideoModel } from '../server/models/video/video'
8import { getUUIDFromFilename } from '../server/helpers/utils'
9import { optimizeVideofile } from '../server/lib/video-transcoding'
10
11run()
12 .then(() => process.exit(0))
13 .catch(err => {
14 console.error(err)
15 process.exit(-1)
16 })
17
18async function run () {
19 const files = await readdir(CONFIG.STORAGE.VIDEOS_DIR)
20 for (const file of files) {
21 const inputPath = join(CONFIG.STORAGE.VIDEOS_DIR, file)
22 const videoBitrate = await getVideoFileBitrate(inputPath)
23 const fps = await getVideoFileFPS(inputPath)
24 const resolution = await getVideoFileResolution(inputPath)
25 const uuid = getUUIDFromFilename(file)
26
27 const isLocalVideo = await VideoRedundancyModel.isLocalByVideoUUIDExists(uuid)
28 const isMaxBitrateExceeded =
29 videoBitrate > getMaxBitrate(resolution.videoFileResolution, fps, VIDEO_TRANSCODING_FPS)
30 if (uuid && isLocalVideo && isMaxBitrateExceeded) {
31 const videoModel = await VideoModel.loadByUUIDWithFile(uuid)
32 await optimizeVideofile(videoModel, inputPath)
33 }
34 }
35 console.log('Finished optimizing videos')
36}
diff --git a/scripts/prune-storage.ts b/scripts/prune-storage.ts
index 4088fa700..4ab0b4863 100755
--- a/scripts/prune-storage.ts
+++ b/scripts/prune-storage.ts
@@ -5,6 +5,7 @@ import { VideoModel } from '../server/models/video/video'
5import { initDatabaseModels } from '../server/initializers' 5import { initDatabaseModels } from '../server/initializers'
6import { remove, readdir } from 'fs-extra' 6import { remove, readdir } from 'fs-extra'
7import { VideoRedundancyModel } from '../server/models/redundancy/video-redundancy' 7import { VideoRedundancyModel } from '../server/models/redundancy/video-redundancy'
8import { getUUIDFromFilename } from '../server/helpers/utils'
8 9
9run() 10run()
10 .then(() => process.exit(0)) 11 .then(() => process.exit(0))
@@ -82,15 +83,6 @@ async function pruneDirectory (directory: string, onlyOwned = false) {
82 return toDelete 83 return toDelete
83} 84}
84 85
85function getUUIDFromFilename (filename: string) {
86 const regex = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/
87 const result = filename.match(regex)
88
89 if (!result || Array.isArray(result) === false) return null
90
91 return result[0]
92}
93
94async function askConfirmation () { 86async function askConfirmation () {
95 return new Promise((res, rej) => { 87 return new Promise((res, rej) => {
96 prompt.start() 88 prompt.start()
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts
index 22bc25476..8e4471173 100644
--- a/server/helpers/ffmpeg-utils.ts
+++ b/server/helpers/ffmpeg-utils.ts
@@ -1,6 +1,6 @@
1import * as ffmpeg from 'fluent-ffmpeg' 1import * as ffmpeg from 'fluent-ffmpeg'
2import { join } from 'path' 2import { join } from 'path'
3import { VideoResolution } from '../../shared/models/videos' 3import { VideoResolution, getTargetBitrate } from '../../shared/models/videos'
4import { CONFIG, FFMPEG_NICE, VIDEO_TRANSCODING_FPS } from '../initializers' 4import { CONFIG, FFMPEG_NICE, VIDEO_TRANSCODING_FPS } from '../initializers'
5import { processImage } from './image-utils' 5import { processImage } from './image-utils'
6import { logger } from './logger' 6import { logger } from './logger'
@@ -55,6 +55,16 @@ async function getVideoFileFPS (path: string) {
55 return 0 55 return 0
56} 56}
57 57
58async function getVideoFileBitrate (path: string) {
59 return new Promise<number>((res, rej) => {
60 ffmpeg.ffprobe(path, (err, metadata) => {
61 if (err) return rej(err)
62
63 return res(metadata.format.bit_rate)
64 })
65 })
66}
67
58function getDurationFromVideoFile (path: string) { 68function getDurationFromVideoFile (path: string) {
59 return new Promise<number>((res, rej) => { 69 return new Promise<number>((res, rej) => {
60 ffmpeg.ffprobe(path, (err, metadata) => { 70 ffmpeg.ffprobe(path, (err, metadata) => {
@@ -138,6 +148,12 @@ function transcode (options: TranscodeOptions) {
138 command = command.withFPS(fps) 148 command = command.withFPS(fps)
139 } 149 }
140 150
151 // Constrained Encoding (VBV)
152 // https://slhck.info/video/2017/03/01/rate-control.html
153 // https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate
154 const targetBitrate = getTargetBitrate(options.resolution, fps, VIDEO_TRANSCODING_FPS)
155 command.outputOptions([`-b:v ${ targetBitrate }`, `-maxrate ${ targetBitrate }`, `-bufsize ${ targetBitrate * 2 }`])
156
141 command 157 command
142 .on('error', (err, stdout, stderr) => { 158 .on('error', (err, stdout, stderr) => {
143 logger.error('Error in transcoding job.', { stdout, stderr }) 159 logger.error('Error in transcoding job.', { stdout, stderr })
@@ -157,7 +173,8 @@ export {
157 transcode, 173 transcode,
158 getVideoFileFPS, 174 getVideoFileFPS,
159 computeResolutionsToTranscode, 175 computeResolutionsToTranscode,
160 audio 176 audio,
177 getVideoFileBitrate
161} 178}
162 179
163// --------------------------------------------------------------------------- 180// ---------------------------------------------------------------------------
diff --git a/server/helpers/utils.ts b/server/helpers/utils.ts
index 6228fec04..39afb4e7b 100644
--- a/server/helpers/utils.ts
+++ b/server/helpers/utils.ts
@@ -77,6 +77,20 @@ async function getVersion () {
77 return require('../../../package.json').version 77 return require('../../../package.json').version
78} 78}
79 79
80/**
81 * From a filename like "ede4cba5-742b-46fa-a388-9a6eb3a3aeb3.mp4", returns
82 * only the "ede4cba5-742b-46fa-a388-9a6eb3a3aeb3" part. If the filename does
83 * not contain a UUID, returns null.
84 */
85function getUUIDFromFilename (filename: string) {
86 const regex = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/
87 const result = filename.match(regex)
88
89 if (!result || Array.isArray(result) === false) return null
90
91 return result[0]
92}
93
80// --------------------------------------------------------------------------- 94// ---------------------------------------------------------------------------
81 95
82export { 96export {
@@ -86,5 +100,6 @@ export {
86 getSecureTorrentName, 100 getSecureTorrentName,
87 getServerActor, 101 getServerActor,
88 getVersion, 102 getVersion,
89 generateVideoTmpPath 103 generateVideoTmpPath,
104 getUUIDFromFilename
90} 105}
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 1a3b52015..a3e5f5dd2 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -3,7 +3,7 @@ import { dirname, join } from 'path'
3import { JobType, VideoRateType, VideoState, VideosRedundancy } from '../../shared/models' 3import { JobType, VideoRateType, VideoState, VideosRedundancy } from '../../shared/models'
4import { ActivityPubActorType } from '../../shared/models/activitypub' 4import { ActivityPubActorType } from '../../shared/models/activitypub'
5import { FollowState } from '../../shared/models/actors' 5import { FollowState } from '../../shared/models/actors'
6import { VideoAbuseState, VideoImportState, VideoPrivacy } from '../../shared/models/videos' 6import { VideoAbuseState, VideoImportState, VideoPrivacy, VideoTranscodingFPS } from '../../shared/models/videos'
7// Do not use barrels, remain constants as independent as possible 7// Do not use barrels, remain constants as independent as possible
8import { buildPath, isTestInstance, parseDuration, root, sanitizeHost, sanitizeUrl } from '../helpers/core-utils' 8import { buildPath, isTestInstance, parseDuration, root, sanitizeHost, sanitizeUrl } from '../helpers/core-utils'
9import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type' 9import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type'
@@ -393,7 +393,7 @@ const RATES_LIMIT = {
393} 393}
394 394
395let VIDEO_VIEW_LIFETIME = 60000 * 60 // 1 hour 395let VIDEO_VIEW_LIFETIME = 60000 * 60 // 1 hour
396const VIDEO_TRANSCODING_FPS = { 396const VIDEO_TRANSCODING_FPS: VideoTranscodingFPS = {
397 MIN: 10, 397 MIN: 10,
398 AVERAGE: 30, 398 AVERAGE: 30,
399 MAX: 60, 399 MAX: 60,
diff --git a/server/lib/activitypub/crawl.ts b/server/lib/activitypub/crawl.ts
index 55912341c..db9ce3293 100644
--- a/server/lib/activitypub/crawl.ts
+++ b/server/lib/activitypub/crawl.ts
@@ -1,7 +1,7 @@
1import { ACTIVITY_PUB, JOB_REQUEST_TIMEOUT } from '../../initializers' 1import { ACTIVITY_PUB, JOB_REQUEST_TIMEOUT } from '../../initializers'
2import { doRequest } from '../../helpers/requests' 2import { doRequest } from '../../helpers/requests'
3import { logger } from '../../helpers/logger' 3import { logger } from '../../helpers/logger'
4import Bluebird = require('bluebird') 4import * as Bluebird from 'bluebird'
5 5
6async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Promise<any> | Bluebird<any>) { 6async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Promise<any> | Bluebird<any>) {
7 logger.info('Crawling ActivityPub data on %s.', uri) 7 logger.info('Crawling ActivityPub data on %s.', uri)
diff --git a/server/lib/job-queue/handlers/video-file.ts b/server/lib/job-queue/handlers/video-file.ts
index 1463c93fc..adc0a2a15 100644
--- a/server/lib/job-queue/handlers/video-file.ts
+++ b/server/lib/job-queue/handlers/video-file.ts
@@ -8,7 +8,7 @@ import { retryTransactionWrapper } from '../../../helpers/database-utils'
8import { sequelizeTypescript } from '../../../initializers' 8import { sequelizeTypescript } from '../../../initializers'
9import * as Bluebird from 'bluebird' 9import * as Bluebird from 'bluebird'
10import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils' 10import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils'
11import { importVideoFile, transcodeOriginalVideofile, optimizeOriginalVideofile } from '../../video-transcoding' 11import { importVideoFile, transcodeOriginalVideofile, optimizeVideofile } from '../../video-transcoding'
12 12
13export type VideoFilePayload = { 13export type VideoFilePayload = {
14 videoUUID: string 14 videoUUID: string
@@ -56,7 +56,7 @@ async function processVideoFile (job: Bull.Job) {
56 56
57 await retryTransactionWrapper(onVideoFileTranscoderOrImportSuccess, video) 57 await retryTransactionWrapper(onVideoFileTranscoderOrImportSuccess, video)
58 } else { 58 } else {
59 await optimizeOriginalVideofile(video) 59 await optimizeVideofile(video)
60 60
61 await retryTransactionWrapper(onVideoFileOptimizerSuccess, video, payload.isNewVideo) 61 await retryTransactionWrapper(onVideoFileOptimizerSuccess, video, payload.isNewVideo)
62 } 62 }
diff --git a/server/lib/video-transcoding.ts b/server/lib/video-transcoding.ts
index bf3ff78c2..04cadf74b 100644
--- a/server/lib/video-transcoding.ts
+++ b/server/lib/video-transcoding.ts
@@ -1,5 +1,5 @@
1import { CONFIG } from '../initializers' 1import { CONFIG } from '../initializers'
2import { join, extname } from 'path' 2import { join, extname, basename } from 'path'
3import { getVideoFileFPS, getVideoFileResolution, transcode } from '../helpers/ffmpeg-utils' 3import { getVideoFileFPS, getVideoFileResolution, transcode } from '../helpers/ffmpeg-utils'
4import { copy, remove, rename, stat } from 'fs-extra' 4import { copy, remove, rename, stat } from 'fs-extra'
5import { logger } from '../helpers/logger' 5import { logger } from '../helpers/logger'
@@ -7,11 +7,16 @@ import { VideoResolution } from '../../shared/models/videos'
7import { VideoFileModel } from '../models/video/video-file' 7import { VideoFileModel } from '../models/video/video-file'
8import { VideoModel } from '../models/video/video' 8import { VideoModel } from '../models/video/video'
9 9
10async function optimizeOriginalVideofile (video: VideoModel) { 10async function optimizeVideofile (video: VideoModel, videoInputPath?: string) {
11 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR 11 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
12 const newExtname = '.mp4' 12 const newExtname = '.mp4'
13 const inputVideoFile = video.getOriginalFile() 13 let inputVideoFile = null
14 const videoInputPath = join(videosDirectory, video.getVideoFilename(inputVideoFile)) 14 if (videoInputPath == null) {
15 inputVideoFile = video.getOriginalFile()
16 videoInputPath = join(videosDirectory, video.getVideoFilename(inputVideoFile))
17 } else {
18 inputVideoFile = basename(videoInputPath)
19 }
15 const videoTranscodedPath = join(videosDirectory, video.id + '-transcoded' + newExtname) 20 const videoTranscodedPath = join(videosDirectory, video.id + '-transcoded' + newExtname)
16 21
17 const transcodeOptions = { 22 const transcodeOptions = {
@@ -124,7 +129,7 @@ async function importVideoFile (video: VideoModel, inputFilePath: string) {
124} 129}
125 130
126export { 131export {
127 optimizeOriginalVideofile, 132 optimizeVideofile,
128 transcodeOriginalVideofile, 133 transcodeOriginalVideofile,
129 importVideoFile 134 importVideoFile
130} 135}
diff --git a/server/tests/api/videos/video-transcoder.ts b/server/tests/api/videos/video-transcoder.ts
index 0f83d4d57..ec554ed19 100644
--- a/server/tests/api/videos/video-transcoder.ts
+++ b/server/tests/api/videos/video-transcoder.ts
@@ -4,8 +4,8 @@ import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { omit } from 'lodash' 5import { omit } from 'lodash'
6import * as ffmpeg from 'fluent-ffmpeg' 6import * as ffmpeg from 'fluent-ffmpeg'
7import { VideoDetails, VideoState } from '../../../../shared/models/videos' 7import { VideoDetails, VideoState, getMaxBitrate, VideoResolution } from '../../../../shared/models/videos'
8import { getVideoFileFPS, audio } from '../../../helpers/ffmpeg-utils' 8import { getVideoFileFPS, audio, getVideoFileBitrate, getVideoFileResolution } from '../../../helpers/ffmpeg-utils'
9import { 9import {
10 buildAbsoluteFixturePath, 10 buildAbsoluteFixturePath,
11 doubleFollow, 11 doubleFollow,
@@ -20,8 +20,10 @@ import {
20 uploadVideo, 20 uploadVideo,
21 webtorrentAdd 21 webtorrentAdd
22} from '../../utils' 22} from '../../utils'
23import { join } from 'path' 23import { join, basename } from 'path'
24import { waitJobs } from '../../utils/server/jobs' 24import { waitJobs } from '../../utils/server/jobs'
25import { remove } from 'fs-extra'
26import { VIDEO_TRANSCODING_FPS } from '../../../../server/initializers/constants'
25 27
26const expect = chai.expect 28const expect = chai.expect
27 29
@@ -228,7 +230,7 @@ describe('Test video transcoding', function () {
228 } 230 }
229 }) 231 })
230 232
231 it('Should wait transcoding before publishing the video', async function () { 233 it('Should wait for transcoding before publishing the video', async function () {
232 this.timeout(80000) 234 this.timeout(80000)
233 235
234 { 236 {
@@ -281,7 +283,59 @@ describe('Test video transcoding', function () {
281 } 283 }
282 }) 284 })
283 285
286 const tempFixturePath = buildAbsoluteFixturePath('video_high_bitrate_1080p.mp4')
287 it('Should respect maximum bitrate values', async function () {
288 this.timeout(160000)
289
290 {
291 // Generate a random, high bitrate video on the fly, so we don't have to include
292 // a large file in the repo. The video needs to have a certain minimum length so
293 // that FFmpeg properly applies bitrate limits.
294 // https://stackoverflow.com/a/15795112
295 await new Promise<void>(async (res, rej) => {
296 ffmpeg()
297 .outputOptions(['-f rawvideo', '-video_size 1920x1080', '-i /dev/urandom'])
298 .outputOptions(['-ac 2', '-f s16le', '-i /dev/urandom', '-t 10'])
299 .outputOptions(['-maxrate 10M', '-bufsize 10M'])
300 .output(tempFixturePath)
301 .on('error', rej)
302 .on('end', res)
303 .run()
304 })
305
306 const bitrate = await getVideoFileBitrate(tempFixturePath)
307 expect(bitrate).to.be.above(getMaxBitrate(VideoResolution.H_1080P, 60, VIDEO_TRANSCODING_FPS))
308
309 const videoAttributes = {
310 name: 'high bitrate video',
311 description: 'high bitrate video',
312 fixture: basename(tempFixturePath)
313 }
314
315 await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
316
317 await waitJobs(servers)
318
319 for (const server of servers) {
320 const res = await getVideosList(server.url)
321
322 const video = res.body.data.find(v => v.name === videoAttributes.name)
323
324 for (const resolution of ['240', '360', '480', '720', '1080']) {
325 const path = join(root(), 'test2', 'videos', video.uuid + '-' + resolution + '.mp4')
326 const bitrate = await getVideoFileBitrate(path)
327 const fps = await getVideoFileFPS(path)
328 const resolution2 = await getVideoFileResolution(path)
329
330 expect(resolution2.videoFileResolution.toString()).to.equal(resolution)
331 expect(bitrate).to.be.below(getMaxBitrate(resolution2.videoFileResolution, fps, VIDEO_TRANSCODING_FPS))
332 }
333 }
334 }
335 })
336
284 after(async function () { 337 after(async function () {
338 remove(tempFixturePath)
285 killallServers(servers) 339 killallServers(servers)
286 }) 340 })
287}) 341})
diff --git a/shared/models/videos/index.ts b/shared/models/videos/index.ts
index 90a0e3053..056ae06da 100644
--- a/shared/models/videos/index.ts
+++ b/shared/models/videos/index.ts
@@ -21,6 +21,7 @@ export * from './video-update.model'
21export * from './video.model' 21export * from './video.model'
22export * from './video-query.type' 22export * from './video-query.type'
23export * from './video-state.enum' 23export * from './video-state.enum'
24export * from './video-transcoding-fps.model'
24export * from './caption/video-caption.model' 25export * from './caption/video-caption.model'
25export * from './caption/video-caption-update.model' 26export * from './caption/video-caption-update.model'
26export * from './import/video-import-create.model' 27export * from './import/video-import-create.model'
diff --git a/shared/models/videos/video-resolution.enum.ts b/shared/models/videos/video-resolution.enum.ts
index 100fc0e6e..3c52bbf98 100644
--- a/shared/models/videos/video-resolution.enum.ts
+++ b/shared/models/videos/video-resolution.enum.ts
@@ -1,3 +1,5 @@
1import { VideoTranscodingFPS } from './video-transcoding-fps.model'
2
1export enum VideoResolution { 3export enum VideoResolution {
2 H_240P = 240, 4 H_240P = 240,
3 H_360P = 360, 5 H_360P = 360,
@@ -5,3 +7,56 @@ export enum VideoResolution {
5 H_720P = 720, 7 H_720P = 720,
6 H_1080P = 1080 8 H_1080P = 1080
7} 9}
10
11/**
12 * Bitrate targets for different resolutions and frame rates, in bytes per second.
13 * Sources for individual quality levels:
14 * Google Live Encoder: https://support.google.com/youtube/answer/2853702?hl=en
15 * YouTube Video Info (tested with random music video): https://www.h3xed.com/blogmedia/youtube-info.php
16 */
17export function getTargetBitrate (resolution: VideoResolution, fps: number,
18 fpsTranscodingConstants: VideoTranscodingFPS) {
19 switch (resolution) {
20 case VideoResolution.H_240P:
21 // quality according to Google Live Encoder: 300 - 700 Kbps
22 // Quality according to YouTube Video Info: 186 Kbps
23 return 250 * 1000
24 case VideoResolution.H_360P:
25 // quality according to Google Live Encoder: 400 - 1,000 Kbps
26 // Quality according to YouTube Video Info: 480 Kbps
27 return 500 * 1000
28 case VideoResolution.H_480P:
29 // quality according to Google Live Encoder: 500 - 2,000 Kbps
30 // Quality according to YouTube Video Info: 879 Kbps
31 return 900 * 1000
32 case VideoResolution.H_720P:
33 if (fps === fpsTranscodingConstants.MAX) {
34 // quality according to Google Live Encoder: 2,250 - 6,000 Kbps
35 // Quality according to YouTube Video Info: 2634 Kbps
36 return 2600 * 1000
37 } else {
38 // quality according to Google Live Encoder: 1,500 - 4,000 Kbps
39 // Quality according to YouTube Video Info: 1752 Kbps
40 return 1750 * 1000
41 }
42 case VideoResolution.H_1080P: // fallthrough
43 default:
44 if (fps === fpsTranscodingConstants.MAX) {
45 // quality according to Google Live Encoder: 3000 - 6000 Kbps
46 // Quality according to YouTube Video Info: 4387 Kbps
47 return 4400 * 1000
48 } else {
49 // quality according to Google Live Encoder: 3000 - 6000 Kbps
50 // Quality according to YouTube Video Info: 3277 Kbps
51 return 3300 * 1000
52 }
53 }
54}
55
56/**
57 * The maximum bitrate we expect to see on a transcoded video in bytes per second.
58 */
59export function getMaxBitrate (resolution: VideoResolution, fps: number,
60 fpsTranscodingConstants: VideoTranscodingFPS) {
61 return getTargetBitrate(resolution, fps, fpsTranscodingConstants) * 2
62}
diff --git a/shared/models/videos/video-transcoding-fps.model.ts b/shared/models/videos/video-transcoding-fps.model.ts
new file mode 100644
index 000000000..82022d2f1
--- /dev/null
+++ b/shared/models/videos/video-transcoding-fps.model.ts
@@ -0,0 +1,6 @@
1export type VideoTranscodingFPS = {
2 MIN: number,
3 AVERAGE: number,
4 MAX: number,
5 KEEP_ORIGIN_FPS_RESOLUTION_MIN: number
6}
diff --git a/support/doc/tools.md b/support/doc/tools.md
index 1db29edc0..8efb0c13d 100644
--- a/support/doc/tools.md
+++ b/support/doc/tools.md
@@ -187,6 +187,17 @@ To delete them (a confirmation will be demanded first):
187$ sudo -u peertube NODE_CONFIG_DIR=/var/www/peertube/config NODE_ENV=production npm run prune-storage 187$ sudo -u peertube NODE_CONFIG_DIR=/var/www/peertube/config NODE_ENV=production npm run prune-storage
188``` 188```
189 189
190### optimize-old-videos.js
191
192Before version v1.0.0-beta.16, Peertube did not specify a bitrate for the transcoding of uploaded videos.
193This means that videos might be encoded into very large files that are too large for streaming. This script
194re-transcodes these videos so that they can be watched properly, even on slow connections.
195
196```
197$ sudo -u peertube NODE_CONFIG_DIR=/var/www/peertube/config NODE_ENV=production npm run optimize-old-videos
198```
199
200
190### update-host.js 201### update-host.js
191 202
192If you started PeerTube with a domain, and then changed it you will have invalid torrent files and invalid URLs in your database. 203If you started PeerTube with a domain, and then changed it you will have invalid torrent files and invalid URLs in your database.