diff options
-rw-r--r-- | server/helpers/ffmpeg-utils.ts | 121 | ||||
-rw-r--r-- | server/initializers/checker.ts | 28 | ||||
-rw-r--r-- | server/tests/api/videos/multiple-servers.ts | 16 | ||||
-rw-r--r-- | server/tests/utils/videos/videos.ts | 4 |
4 files changed, 153 insertions, 16 deletions
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts index f0623c88b..eb1c86ab9 100644 --- a/server/helpers/ffmpeg-utils.ts +++ b/server/helpers/ffmpeg-utils.ts | |||
@@ -5,6 +5,7 @@ import { CONFIG, VIDEO_TRANSCODING_FPS } from '../initializers' | |||
5 | import { unlinkPromise } from './core-utils' | 5 | import { unlinkPromise } from './core-utils' |
6 | import { processImage } from './image-utils' | 6 | import { processImage } from './image-utils' |
7 | import { logger } from './logger' | 7 | import { logger } from './logger' |
8 | import { checkFFmpegEncoders } from '../initializers/checker' | ||
8 | 9 | ||
9 | async function getVideoFileResolution (path: string) { | 10 | async function getVideoFileResolution (path: string) { |
10 | const videoStream = await getVideoFileStream(path) | 11 | const videoStream = await getVideoFileStream(path) |
@@ -85,12 +86,8 @@ function transcode (options: TranscodeOptions) { | |||
85 | return new Promise<void>(async (res, rej) => { | 86 | return new Promise<void>(async (res, rej) => { |
86 | let command = ffmpeg(options.inputPath) | 87 | let command = ffmpeg(options.inputPath) |
87 | .output(options.outputPath) | 88 | .output(options.outputPath) |
88 | .videoCodec('libx264') | ||
89 | .outputOption('-threads ' + CONFIG.TRANSCODING.THREADS) | 89 | .outputOption('-threads ' + CONFIG.TRANSCODING.THREADS) |
90 | .outputOption('-movflags faststart') | 90 | .preset(standard) |
91 | .outputOption('-b_strategy 1') // NOTE: b-strategy 1 - heuristic algorythm, 16 is optimal B-frames for it | ||
92 | .outputOption('-bf 16') // NOTE: Why 16: https://github.com/Chocobozzz/PeerTube/pull/774. b-strategy 2 -> B-frames<16 | ||
93 | // .outputOption('-crf 18') | ||
94 | 91 | ||
95 | let fps = await getVideoFileFPS(options.inputPath) | 92 | let fps = await getVideoFileFPS(options.inputPath) |
96 | if (options.resolution !== undefined) { | 93 | if (options.resolution !== undefined) { |
@@ -149,3 +146,117 @@ function getVideoFileStream (path: string) { | |||
149 | }) | 146 | }) |
150 | }) | 147 | }) |
151 | } | 148 | } |
149 | |||
150 | /** | ||
151 | * A slightly customised version of the 'veryfast' x264 preset | ||
152 | * | ||
153 | * The veryfast preset is right in the sweet spot of performance | ||
154 | * and quality. Superfast and ultrafast will give you better | ||
155 | * performance, but then quality is noticeably worse. | ||
156 | */ | ||
157 | function veryfast (ffmpeg) { | ||
158 | ffmpeg | ||
159 | .preset(standard) | ||
160 | .outputOption('-preset:v veryfast') | ||
161 | .outputOption(['--aq-mode=2', '--aq-strength=1.3']) | ||
162 | /* | ||
163 | MAIN reference: https://slhck.info/video/2017/03/01/rate-control.html | ||
164 | Our target situation is closer to a livestream than a stream, | ||
165 | since we want to reduce as much a possible the encoding burden, | ||
166 | altough not to the point of a livestream where there is a hard | ||
167 | constraint on the frames per second to be encoded. | ||
168 | |||
169 | why '--aq-mode=2 --aq-strength=1.3' instead of '-profile:v main'? | ||
170 | Make up for most of the loss of grain and macroblocking | ||
171 | with less computing power. | ||
172 | */ | ||
173 | } | ||
174 | |||
175 | /** | ||
176 | * A preset optimised for a stillimage audio video | ||
177 | */ | ||
178 | function audio (ffmpeg) { | ||
179 | ffmpeg | ||
180 | .preset(veryfast) | ||
181 | .outputOption('-tune stillimage') | ||
182 | } | ||
183 | |||
184 | /** | ||
185 | * A toolbox to play with audio | ||
186 | */ | ||
187 | namespace audio { | ||
188 | export const get = (ffmpeg, pos = 0) => { | ||
189 | // without position, ffprobe considers the last input only | ||
190 | // we make it consider the first input only | ||
191 | ffmpeg | ||
192 | .ffprobe(pos, (_,data) => { | ||
193 | return data['streams'].find(stream => { | ||
194 | return stream['codec_type'] === 'audio' | ||
195 | }) | ||
196 | }) | ||
197 | } | ||
198 | |||
199 | export namespace bitrate { | ||
200 | export const baseKbitrate = 384 | ||
201 | |||
202 | const toBits = (kbits: number): number => { return kbits * 8000 } | ||
203 | |||
204 | export const aac = (bitrate: number): number => { | ||
205 | switch (true) { | ||
206 | case bitrate > toBits(384): | ||
207 | return baseKbitrate | ||
208 | default: | ||
209 | return -1 // we interpret it as a signal to copy the audio stream as is | ||
210 | } | ||
211 | } | ||
212 | |||
213 | export const mp3 = (bitrate: number): number => { | ||
214 | switch (true) { | ||
215 | case bitrate <= toBits(192): | ||
216 | return 128 | ||
217 | case bitrate <= toBits(384): | ||
218 | return 256 | ||
219 | default: | ||
220 | return baseKbitrate | ||
221 | } | ||
222 | } | ||
223 | } | ||
224 | } | ||
225 | |||
226 | /** | ||
227 | * Standard profile, with variable bitrate audio and faststart. | ||
228 | * | ||
229 | * As for the audio, quality '5' is the highest and ensures 96-112kbps/channel | ||
230 | * See https://trac.ffmpeg.org/wiki/Encode/AAC#fdk_vbr | ||
231 | */ | ||
232 | async function standard (ffmpeg) { | ||
233 | let _bitrate = audio.bitrate.baseKbitrate | ||
234 | let _ffmpeg = ffmpeg | ||
235 | .format('mp4') | ||
236 | .videoCodec('libx264') | ||
237 | .outputOption('-level 3.1') // 3.1 is the minimal ressource allocation for our highest supported resolution | ||
238 | .outputOption('-b_strategy 1') // NOTE: b-strategy 1 - heuristic algorythm, 16 is optimal B-frames for it | ||
239 | .outputOption('-bf 16') // NOTE: Why 16: https://github.com/Chocobozzz/PeerTube/pull/774. b-strategy 2 -> B-frames<16 | ||
240 | .outputOption('-movflags faststart') | ||
241 | let _audio = audio.get(_ffmpeg) | ||
242 | |||
243 | if (!_audio) return _ffmpeg.noAudio() | ||
244 | |||
245 | // we try to reduce the ceiling bitrate by making rough correspondances of bitrates | ||
246 | // of course this is far from perfect, but it might save some space in the end | ||
247 | if (audio.bitrate[_audio['codec_name']]) { | ||
248 | _bitrate = audio.bitrate[_audio['codec_name']](_audio['bit_rate']) | ||
249 | if (_bitrate === -1) { | ||
250 | return _ffmpeg.audioCodec('copy') | ||
251 | } | ||
252 | } | ||
253 | |||
254 | // we favor VBR, if a good AAC encoder is available | ||
255 | if ((await checkFFmpegEncoders()).get('libfdk_aac')) { | ||
256 | return _ffmpeg | ||
257 | .audioCodec('libfdk_aac') | ||
258 | .audioQuality(5) | ||
259 | } | ||
260 | |||
261 | return _ffmpeg.audioBitrate(_bitrate) | ||
262 | } | ||
diff --git a/server/initializers/checker.ts b/server/initializers/checker.ts index 270cbf649..f1c2e80a9 100644 --- a/server/initializers/checker.ts +++ b/server/initializers/checker.ts | |||
@@ -84,11 +84,11 @@ function checkMissedConfig () { | |||
84 | async function checkFFmpeg (CONFIG: { TRANSCODING: { ENABLED: boolean } }) { | 84 | async function checkFFmpeg (CONFIG: { TRANSCODING: { ENABLED: boolean } }) { |
85 | const Ffmpeg = require('fluent-ffmpeg') | 85 | const Ffmpeg = require('fluent-ffmpeg') |
86 | const getAvailableCodecsPromise = promisify0(Ffmpeg.getAvailableCodecs) | 86 | const getAvailableCodecsPromise = promisify0(Ffmpeg.getAvailableCodecs) |
87 | |||
88 | const codecs = await getAvailableCodecsPromise() | 87 | const codecs = await getAvailableCodecsPromise() |
88 | const canEncode = [ 'libx264' ] | ||
89 | |||
89 | if (CONFIG.TRANSCODING.ENABLED === false) return undefined | 90 | if (CONFIG.TRANSCODING.ENABLED === false) return undefined |
90 | 91 | ||
91 | const canEncode = [ 'libx264' ] | ||
92 | for (const codec of canEncode) { | 92 | for (const codec of canEncode) { |
93 | if (codecs[codec] === undefined) { | 93 | if (codecs[codec] === undefined) { |
94 | throw new Error('Unknown codec ' + codec + ' in FFmpeg.') | 94 | throw new Error('Unknown codec ' + codec + ' in FFmpeg.') |
@@ -98,6 +98,29 @@ async function checkFFmpeg (CONFIG: { TRANSCODING: { ENABLED: boolean } }) { | |||
98 | throw new Error('Unavailable encode codec ' + codec + ' in FFmpeg') | 98 | throw new Error('Unavailable encode codec ' + codec + ' in FFmpeg') |
99 | } | 99 | } |
100 | } | 100 | } |
101 | |||
102 | checkFFmpegEncoders() | ||
103 | } | ||
104 | |||
105 | // Optional encoders, if present, can be used to improve transcoding | ||
106 | // Here we ask ffmpeg if it detects their presence on the system, so that we can later use them | ||
107 | let supportedOptionalEncoders: Map<string, boolean> | ||
108 | async function checkFFmpegEncoders (): Promise<Map<string, boolean>> { | ||
109 | if (supportedOptionalEncoders !== undefined) { | ||
110 | return supportedOptionalEncoders | ||
111 | } | ||
112 | |||
113 | const Ffmpeg = require('fluent-ffmpeg') | ||
114 | const getAvailableEncodersPromise = promisify0(Ffmpeg.getAvailableEncoders) | ||
115 | const encoders = await getAvailableEncodersPromise() | ||
116 | const optionalEncoders = [ 'libfdk_aac' ] | ||
117 | supportedOptionalEncoders = new Map<string, boolean>() | ||
118 | |||
119 | for (const encoder of optionalEncoders) { | ||
120 | supportedOptionalEncoders.set(encoder, | ||
121 | encoders[encoder] !== undefined | ||
122 | ) | ||
123 | } | ||
101 | } | 124 | } |
102 | 125 | ||
103 | // We get db by param to not import it in this file (import orders) | 126 | // We get db by param to not import it in this file (import orders) |
@@ -126,6 +149,7 @@ async function applicationExist () { | |||
126 | export { | 149 | export { |
127 | checkConfig, | 150 | checkConfig, |
128 | checkFFmpeg, | 151 | checkFFmpeg, |
152 | checkFFmpegEncoders, | ||
129 | checkMissedConfig, | 153 | checkMissedConfig, |
130 | clientsExist, | 154 | clientsExist, |
131 | usersExist, | 155 | usersExist, |
diff --git a/server/tests/api/videos/multiple-servers.ts b/server/tests/api/videos/multiple-servers.ts index cb18898ce..4681deb47 100644 --- a/server/tests/api/videos/multiple-servers.ts +++ b/server/tests/api/videos/multiple-servers.ts | |||
@@ -209,19 +209,19 @@ describe('Test multiple servers', function () { | |||
209 | files: [ | 209 | files: [ |
210 | { | 210 | { |
211 | resolution: 240, | 211 | resolution: 240, |
212 | size: 190000 | 212 | size: 100000 |
213 | }, | 213 | }, |
214 | { | 214 | { |
215 | resolution: 360, | 215 | resolution: 360, |
216 | size: 280000 | 216 | size: 180000 |
217 | }, | 217 | }, |
218 | { | 218 | { |
219 | resolution: 480, | 219 | resolution: 480, |
220 | size: 390000 | 220 | size: 280000 |
221 | }, | 221 | }, |
222 | { | 222 | { |
223 | resolution: 720, | 223 | resolution: 720, |
224 | size: 710000 | 224 | size: 630000 |
225 | } | 225 | } |
226 | ], | 226 | ], |
227 | thumbnailfile: 'thumbnail', | 227 | thumbnailfile: 'thumbnail', |
@@ -975,19 +975,19 @@ describe('Test multiple servers', function () { | |||
975 | files: [ | 975 | files: [ |
976 | { | 976 | { |
977 | resolution: 720, | 977 | resolution: 720, |
978 | size: 40315 | 978 | size: 31000 |
979 | }, | 979 | }, |
980 | { | 980 | { |
981 | resolution: 480, | 981 | resolution: 480, |
982 | size: 22808 | 982 | size: 16000 |
983 | }, | 983 | }, |
984 | { | 984 | { |
985 | resolution: 360, | 985 | resolution: 360, |
986 | size: 18617 | 986 | size: 12000 |
987 | }, | 987 | }, |
988 | { | 988 | { |
989 | resolution: 240, | 989 | resolution: 240, |
990 | size: 15217 | 990 | size: 10000 |
991 | } | 991 | } |
992 | ] | 992 | ] |
993 | } | 993 | } |
diff --git a/server/tests/utils/videos/videos.ts b/server/tests/utils/videos/videos.ts index 8c49eb02b..a9d449c58 100644 --- a/server/tests/utils/videos/videos.ts +++ b/server/tests/utils/videos/videos.ts | |||
@@ -522,7 +522,9 @@ async function completeVideoCheck ( | |||
522 | 522 | ||
523 | const minSize = attributeFile.size - ((10 * attributeFile.size) / 100) | 523 | const minSize = attributeFile.size - ((10 * attributeFile.size) / 100) |
524 | const maxSize = attributeFile.size + ((10 * attributeFile.size) / 100) | 524 | const maxSize = attributeFile.size + ((10 * attributeFile.size) / 100) |
525 | expect(file.size).to.be.above(minSize).and.below(maxSize) | 525 | expect(file.size, |
526 | 'File size for resolution ' + file.resolution.label + ' outside confidence interval.') | ||
527 | .to.be.above(minSize).and.below(maxSize) | ||
526 | 528 | ||
527 | { | 529 | { |
528 | await testImage(url, attributes.thumbnailfile || attributes.fixture, videoDetails.thumbnailPath) | 530 | await testImage(url, attributes.thumbnailfile || attributes.fixture, videoDetails.thumbnailPath) |