aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
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 /server
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
Diffstat (limited to 'server')
-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
7 files changed, 108 insertions, 17 deletions
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})