diff options
author | Felix Ableitner <me@nutomic.com> | 2018-10-08 09:26:04 -0500 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2018-10-08 16:26:04 +0200 |
commit | edb4ffc7e0b13659d7c73b120f2c87b27e4c26a1 (patch) | |
tree | fb9df6826eaeb23ab3bcac7fe21773978c68d27c | |
parent | 2cae5f13076a31aa95774679aed1f13c3bd5f8ce (diff) | |
download | PeerTube-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-- | .gitignore | 1 | ||||
-rw-r--r-- | package.json | 1 | ||||
-rwxr-xr-x | scripts/help.sh | 1 | ||||
-rw-r--r-- | scripts/optimize-old-videos.ts | 36 | ||||
-rwxr-xr-x | scripts/prune-storage.ts | 10 | ||||
-rw-r--r-- | server/helpers/ffmpeg-utils.ts | 21 | ||||
-rw-r--r-- | server/helpers/utils.ts | 17 | ||||
-rw-r--r-- | server/initializers/constants.ts | 4 | ||||
-rw-r--r-- | server/lib/activitypub/crawl.ts | 2 | ||||
-rw-r--r-- | server/lib/job-queue/handlers/video-file.ts | 4 | ||||
-rw-r--r-- | server/lib/video-transcoding.ts | 15 | ||||
-rw-r--r-- | server/tests/api/videos/video-transcoder.ts | 62 | ||||
-rw-r--r-- | shared/models/videos/index.ts | 1 | ||||
-rw-r--r-- | shared/models/videos/video-resolution.enum.ts | 55 | ||||
-rw-r--r-- | shared/models/videos/video-transcoding-fps.model.ts | 6 | ||||
-rw-r--r-- | support/doc/tools.md | 11 |
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" | |||
18 | printf " create-transcoding-job -- -v [video UUID] \n" | 18 | printf " create-transcoding-job -- -v [video UUID] \n" |
19 | printf " -> Create a transcoding job for a particular video\n" | 19 | printf " -> Create a transcoding job for a particular video\n" |
20 | printf " prune-storage -> Delete (after confirmation) unknown video files/thumbnails/previews... (due to a bad video deletion, transcoding job not finished...)\n" | 20 | printf " prune-storage -> Delete (after confirmation) unknown video files/thumbnails/previews... (due to a bad video deletion, transcoding job not finished...)\n" |
21 | printf " optimize-old-videos -> Re-transcode videos that have a high bitrate, to make them suitable for streaming over slow connections" | ||
21 | printf " dev -> Watch, run the livereload and run the server so that you can develop the application\n" | 22 | printf " dev -> Watch, run the livereload and run the server so that you can develop the application\n" |
22 | printf " start -> Run the server\n" | 23 | printf " start -> Run the server\n" |
23 | printf " update-host -> Upgrade scheme/host in torrent files according to the webserver configuration (config/ folder)\n" | 24 | printf " 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 @@ | |||
1 | import { join } from 'path' | ||
2 | import { readdir } from 'fs-extra' | ||
3 | import { CONFIG, VIDEO_TRANSCODING_FPS } from '../server/initializers/constants' | ||
4 | import { getVideoFileResolution, getVideoFileBitrate, getVideoFileFPS } from '../server/helpers/ffmpeg-utils' | ||
5 | import { getMaxBitrate } from '../shared/models/videos' | ||
6 | import { VideoRedundancyModel } from '../server/models/redundancy/video-redundancy' | ||
7 | import { VideoModel } from '../server/models/video/video' | ||
8 | import { getUUIDFromFilename } from '../server/helpers/utils' | ||
9 | import { optimizeVideofile } from '../server/lib/video-transcoding' | ||
10 | |||
11 | run() | ||
12 | .then(() => process.exit(0)) | ||
13 | .catch(err => { | ||
14 | console.error(err) | ||
15 | process.exit(-1) | ||
16 | }) | ||
17 | |||
18 | async 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' | |||
5 | import { initDatabaseModels } from '../server/initializers' | 5 | import { initDatabaseModels } from '../server/initializers' |
6 | import { remove, readdir } from 'fs-extra' | 6 | import { remove, readdir } from 'fs-extra' |
7 | import { VideoRedundancyModel } from '../server/models/redundancy/video-redundancy' | 7 | import { VideoRedundancyModel } from '../server/models/redundancy/video-redundancy' |
8 | import { getUUIDFromFilename } from '../server/helpers/utils' | ||
8 | 9 | ||
9 | run() | 10 | run() |
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 | ||
85 | function 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 | |||
94 | async function askConfirmation () { | 86 | async 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 @@ | |||
1 | import * as ffmpeg from 'fluent-ffmpeg' | 1 | import * as ffmpeg from 'fluent-ffmpeg' |
2 | import { join } from 'path' | 2 | import { join } from 'path' |
3 | import { VideoResolution } from '../../shared/models/videos' | 3 | import { VideoResolution, getTargetBitrate } from '../../shared/models/videos' |
4 | import { CONFIG, FFMPEG_NICE, VIDEO_TRANSCODING_FPS } from '../initializers' | 4 | import { CONFIG, FFMPEG_NICE, VIDEO_TRANSCODING_FPS } from '../initializers' |
5 | import { processImage } from './image-utils' | 5 | import { processImage } from './image-utils' |
6 | import { logger } from './logger' | 6 | import { logger } from './logger' |
@@ -55,6 +55,16 @@ async function getVideoFileFPS (path: string) { | |||
55 | return 0 | 55 | return 0 |
56 | } | 56 | } |
57 | 57 | ||
58 | async 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 | |||
58 | function getDurationFromVideoFile (path: string) { | 68 | function 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 | */ | ||
85 | function 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 | ||
82 | export { | 96 | export { |
@@ -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' | |||
3 | import { JobType, VideoRateType, VideoState, VideosRedundancy } from '../../shared/models' | 3 | import { JobType, VideoRateType, VideoState, VideosRedundancy } from '../../shared/models' |
4 | import { ActivityPubActorType } from '../../shared/models/activitypub' | 4 | import { ActivityPubActorType } from '../../shared/models/activitypub' |
5 | import { FollowState } from '../../shared/models/actors' | 5 | import { FollowState } from '../../shared/models/actors' |
6 | import { VideoAbuseState, VideoImportState, VideoPrivacy } from '../../shared/models/videos' | 6 | import { 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 |
8 | import { buildPath, isTestInstance, parseDuration, root, sanitizeHost, sanitizeUrl } from '../helpers/core-utils' | 8 | import { buildPath, isTestInstance, parseDuration, root, sanitizeHost, sanitizeUrl } from '../helpers/core-utils' |
9 | import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type' | 9 | import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type' |
@@ -393,7 +393,7 @@ const RATES_LIMIT = { | |||
393 | } | 393 | } |
394 | 394 | ||
395 | let VIDEO_VIEW_LIFETIME = 60000 * 60 // 1 hour | 395 | let VIDEO_VIEW_LIFETIME = 60000 * 60 // 1 hour |
396 | const VIDEO_TRANSCODING_FPS = { | 396 | const 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 @@ | |||
1 | import { ACTIVITY_PUB, JOB_REQUEST_TIMEOUT } from '../../initializers' | 1 | import { ACTIVITY_PUB, JOB_REQUEST_TIMEOUT } from '../../initializers' |
2 | import { doRequest } from '../../helpers/requests' | 2 | import { doRequest } from '../../helpers/requests' |
3 | import { logger } from '../../helpers/logger' | 3 | import { logger } from '../../helpers/logger' |
4 | import Bluebird = require('bluebird') | 4 | import * as Bluebird from 'bluebird' |
5 | 5 | ||
6 | async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Promise<any> | Bluebird<any>) { | 6 | async 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' | |||
8 | import { sequelizeTypescript } from '../../../initializers' | 8 | import { sequelizeTypescript } from '../../../initializers' |
9 | import * as Bluebird from 'bluebird' | 9 | import * as Bluebird from 'bluebird' |
10 | import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils' | 10 | import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils' |
11 | import { importVideoFile, transcodeOriginalVideofile, optimizeOriginalVideofile } from '../../video-transcoding' | 11 | import { importVideoFile, transcodeOriginalVideofile, optimizeVideofile } from '../../video-transcoding' |
12 | 12 | ||
13 | export type VideoFilePayload = { | 13 | export 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 @@ | |||
1 | import { CONFIG } from '../initializers' | 1 | import { CONFIG } from '../initializers' |
2 | import { join, extname } from 'path' | 2 | import { join, extname, basename } from 'path' |
3 | import { getVideoFileFPS, getVideoFileResolution, transcode } from '../helpers/ffmpeg-utils' | 3 | import { getVideoFileFPS, getVideoFileResolution, transcode } from '../helpers/ffmpeg-utils' |
4 | import { copy, remove, rename, stat } from 'fs-extra' | 4 | import { copy, remove, rename, stat } from 'fs-extra' |
5 | import { logger } from '../helpers/logger' | 5 | import { logger } from '../helpers/logger' |
@@ -7,11 +7,16 @@ import { VideoResolution } from '../../shared/models/videos' | |||
7 | import { VideoFileModel } from '../models/video/video-file' | 7 | import { VideoFileModel } from '../models/video/video-file' |
8 | import { VideoModel } from '../models/video/video' | 8 | import { VideoModel } from '../models/video/video' |
9 | 9 | ||
10 | async function optimizeOriginalVideofile (video: VideoModel) { | 10 | async 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 | ||
126 | export { | 131 | export { |
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' | |||
4 | import 'mocha' | 4 | import 'mocha' |
5 | import { omit } from 'lodash' | 5 | import { omit } from 'lodash' |
6 | import * as ffmpeg from 'fluent-ffmpeg' | 6 | import * as ffmpeg from 'fluent-ffmpeg' |
7 | import { VideoDetails, VideoState } from '../../../../shared/models/videos' | 7 | import { VideoDetails, VideoState, getMaxBitrate, VideoResolution } from '../../../../shared/models/videos' |
8 | import { getVideoFileFPS, audio } from '../../../helpers/ffmpeg-utils' | 8 | import { getVideoFileFPS, audio, getVideoFileBitrate, getVideoFileResolution } from '../../../helpers/ffmpeg-utils' |
9 | import { | 9 | import { |
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' |
23 | import { join } from 'path' | 23 | import { join, basename } from 'path' |
24 | import { waitJobs } from '../../utils/server/jobs' | 24 | import { waitJobs } from '../../utils/server/jobs' |
25 | import { remove } from 'fs-extra' | ||
26 | import { VIDEO_TRANSCODING_FPS } from '../../../../server/initializers/constants' | ||
25 | 27 | ||
26 | const expect = chai.expect | 28 | const 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' | |||
21 | export * from './video.model' | 21 | export * from './video.model' |
22 | export * from './video-query.type' | 22 | export * from './video-query.type' |
23 | export * from './video-state.enum' | 23 | export * from './video-state.enum' |
24 | export * from './video-transcoding-fps.model' | ||
24 | export * from './caption/video-caption.model' | 25 | export * from './caption/video-caption.model' |
25 | export * from './caption/video-caption-update.model' | 26 | export * from './caption/video-caption-update.model' |
26 | export * from './import/video-import-create.model' | 27 | export * 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 @@ | |||
1 | import { VideoTranscodingFPS } from './video-transcoding-fps.model' | ||
2 | |||
1 | export enum VideoResolution { | 3 | export 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 | */ | ||
17 | export 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 | */ | ||
59 | export 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 @@ | |||
1 | export 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 | |||
192 | Before version v1.0.0-beta.16, Peertube did not specify a bitrate for the transcoding of uploaded videos. | ||
193 | This means that videos might be encoded into very large files that are too large for streaming. This script | ||
194 | re-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 | ||
192 | If you started PeerTube with a domain, and then changed it you will have invalid torrent files and invalid URLs in your database. | 203 | If you started PeerTube with a domain, and then changed it you will have invalid torrent files and invalid URLs in your database. |