diff options
Diffstat (limited to 'server')
-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 |
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 @@ | |||
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 | }) |