aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/helpers/ffmpeg-utils.ts159
-rw-r--r--server/initializers/checker-before-init.ts8
-rw-r--r--server/tests/api/videos/multiple-servers.ts8
-rw-r--r--server/tests/api/videos/video-transcoder.ts8
4 files changed, 99 insertions, 84 deletions
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts
index 037bf703a..a108d46a0 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, getTargetBitrate } from '../../shared/models/videos' 3import { getTargetBitrate, VideoResolution } 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'
@@ -116,46 +116,50 @@ type TranscodeOptions = {
116 116
117function transcode (options: TranscodeOptions) { 117function transcode (options: TranscodeOptions) {
118 return new Promise<void>(async (res, rej) => { 118 return new Promise<void>(async (res, rej) => {
119 let fps = await getVideoFileFPS(options.inputPath) 119 try {
120 // On small/medium resolutions, limit FPS 120 let fps = await getVideoFileFPS(options.inputPath)
121 if ( 121 // On small/medium resolutions, limit FPS
122 options.resolution !== undefined && 122 if (
123 options.resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN && 123 options.resolution !== undefined &&
124 fps > VIDEO_TRANSCODING_FPS.AVERAGE 124 options.resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN &&
125 ) { 125 fps > VIDEO_TRANSCODING_FPS.AVERAGE
126 fps = VIDEO_TRANSCODING_FPS.AVERAGE 126 ) {
127 } 127 fps = VIDEO_TRANSCODING_FPS.AVERAGE
128 }
128 129
129 let command = ffmpeg(options.inputPath, { niceness: FFMPEG_NICE.TRANSCODING }) 130 let command = ffmpeg(options.inputPath, { niceness: FFMPEG_NICE.TRANSCODING })
130 .output(options.outputPath) 131 .output(options.outputPath)
131 command = await presetH264(command, options.resolution, fps) 132 command = await presetH264(command, options.resolution, fps)
132 133
133 if (CONFIG.TRANSCODING.THREADS > 0) { 134 if (CONFIG.TRANSCODING.THREADS > 0) {
134 // if we don't set any threads ffmpeg will chose automatically 135 // if we don't set any threads ffmpeg will chose automatically
135 command = command.outputOption('-threads ' + CONFIG.TRANSCODING.THREADS) 136 command = command.outputOption('-threads ' + CONFIG.TRANSCODING.THREADS)
136 } 137 }
137 138
138 if (options.resolution !== undefined) { 139 if (options.resolution !== undefined) {
139 // '?x720' or '720x?' for example 140 // '?x720' or '720x?' for example
140 const size = options.isPortraitMode === true ? `${options.resolution}x?` : `?x${options.resolution}` 141 const size = options.isPortraitMode === true ? `${options.resolution}x?` : `?x${options.resolution}`
141 command = command.size(size) 142 command = command.size(size)
142 } 143 }
143 144
144 if (fps) { 145 if (fps) {
145 // Hard FPS limits 146 // Hard FPS limits
146 if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = VIDEO_TRANSCODING_FPS.MAX 147 if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = VIDEO_TRANSCODING_FPS.MAX
147 else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN 148 else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN
148 149
149 command = command.withFPS(fps) 150 command = command.withFPS(fps)
150 } 151 }
151 152
152 command 153 command
153 .on('error', (err, stdout, stderr) => { 154 .on('error', (err, stdout, stderr) => {
154 logger.error('Error in transcoding job.', { stdout, stderr }) 155 logger.error('Error in transcoding job.', { stdout, stderr })
155 return rej(err) 156 return rej(err)
156 }) 157 })
157 .on('end', res) 158 .on('end', res)
158 .run() 159 .run()
160 } catch (err) {
161 return rej(err)
162 }
159 }) 163 })
160} 164}
161 165
@@ -194,11 +198,10 @@ function getVideoFileStream (path: string) {
194 * and quality. Superfast and ultrafast will give you better 198 * and quality. Superfast and ultrafast will give you better
195 * performance, but then quality is noticeably worse. 199 * performance, but then quality is noticeably worse.
196 */ 200 */
197async function presetH264VeryFast (ffmpeg: ffmpeg, resolution: VideoResolution, fps: number): ffmpeg { 201async function presetH264VeryFast (command: ffmpeg.FfmpegCommand, resolution: VideoResolution, fps: number): Promise<ffmpeg.FfmpegCommand> {
198 const localFfmpeg = await presetH264(ffmpeg, resolution, fps) 202 let localCommand = await presetH264(command, resolution, fps)
199 localFfmpeg 203 localCommand = localCommand.outputOption('-preset:v veryfast')
200 .outputOption('-preset:v veryfast') 204 .outputOption([ '--aq-mode=2', '--aq-strength=1.3' ])
201 .outputOption(['--aq-mode=2', '--aq-strength=1.3'])
202 /* 205 /*
203 MAIN reference: https://slhck.info/video/2017/03/01/rate-control.html 206 MAIN reference: https://slhck.info/video/2017/03/01/rate-control.html
204 Our target situation is closer to a livestream than a stream, 207 Our target situation is closer to a livestream than a stream,
@@ -210,31 +213,39 @@ async function presetH264VeryFast (ffmpeg: ffmpeg, resolution: VideoResolution,
210 Make up for most of the loss of grain and macroblocking 213 Make up for most of the loss of grain and macroblocking
211 with less computing power. 214 with less computing power.
212 */ 215 */
216
217 return localCommand
213} 218}
214 219
215/** 220/**
216 * A preset optimised for a stillimage audio video 221 * A preset optimised for a stillimage audio video
217 */ 222 */
218async function presetStillImageWithAudio (ffmpeg: ffmpeg, resolution: VideoResolution, fps: number): ffmpeg { 223async function presetStillImageWithAudio (
219 const localFfmpeg = await presetH264VeryFast(ffmpeg, resolution, fps) 224 command: ffmpeg.FfmpegCommand,
220 localFfmpeg 225 resolution: VideoResolution,
221 .outputOption('-tune stillimage') 226 fps: number
227): Promise<ffmpeg.FfmpegCommand> {
228 let localCommand = await presetH264VeryFast(command, resolution, fps)
229 localCommand = localCommand.outputOption('-tune stillimage')
230
231 return localCommand
222} 232}
223 233
224/** 234/**
225 * A toolbox to play with audio 235 * A toolbox to play with audio
226 */ 236 */
227namespace audio { 237namespace audio {
228 export const get = (_ffmpeg, pos: number | string = 0) => { 238 export const get = (option: ffmpeg.FfmpegCommand | string) => {
229 // without position, ffprobe considers the last input only 239 // without position, ffprobe considers the last input only
230 // we make it consider the first input only 240 // we make it consider the first input only
231 // if you pass a file path to pos, then ffprobe acts on that file directly 241 // if you pass a file path to pos, then ffprobe acts on that file directly
232 return new Promise<{ absolutePath: string, audioStream?: any }>((res, rej) => { 242 return new Promise<{ absolutePath: string, audioStream?: any }>((res, rej) => {
233 _ffmpeg.ffprobe(pos, (err,data) => { 243
244 function parseFfprobe (err: any, data: ffmpeg.FfprobeData) {
234 if (err) return rej(err) 245 if (err) return rej(err)
235 246
236 if ('streams' in data) { 247 if ('streams' in data) {
237 const audioStream = data['streams'].find(stream => stream['codec_type'] === 'audio') 248 const audioStream = data.streams.find(stream => stream['codec_type'] === 'audio')
238 if (audioStream) { 249 if (audioStream) {
239 return res({ 250 return res({
240 absolutePath: data.format.filename, 251 absolutePath: data.format.filename,
@@ -242,8 +253,15 @@ namespace audio {
242 }) 253 })
243 } 254 }
244 } 255 }
256
245 return res({ absolutePath: data.format.filename }) 257 return res({ absolutePath: data.format.filename })
246 }) 258 }
259
260 if (typeof option === 'string') {
261 return ffmpeg.ffprobe(option, parseFfprobe)
262 }
263
264 return option.ffprobe(parseFfprobe)
247 }) 265 })
248 } 266 }
249 267
@@ -285,8 +303,8 @@ namespace audio {
285 * As for the audio, quality '5' is the highest and ensures 96-112kbps/channel 303 * As for the audio, quality '5' is the highest and ensures 96-112kbps/channel
286 * See https://trac.ffmpeg.org/wiki/Encode/AAC#fdk_vbr 304 * See https://trac.ffmpeg.org/wiki/Encode/AAC#fdk_vbr
287 */ 305 */
288async function presetH264 (ffmpeg: ffmpeg, resolution: VideoResolution, fps: number): ffmpeg { 306async function presetH264 (command: ffmpeg.FfmpegCommand, resolution: VideoResolution, fps: number): Promise<ffmpeg.FfmpegCommand> {
289 let localFfmpeg = ffmpeg 307 let localCommand = command
290 .format('mp4') 308 .format('mp4')
291 .videoCodec('libx264') 309 .videoCodec('libx264')
292 .outputOption('-level 3.1') // 3.1 is the minimal ressource allocation for our highest supported resolution 310 .outputOption('-level 3.1') // 3.1 is the minimal ressource allocation for our highest supported resolution
@@ -294,41 +312,38 @@ async function presetH264 (ffmpeg: ffmpeg, resolution: VideoResolution, fps: num
294 .outputOption('-bf 16') // NOTE: Why 16: https://github.com/Chocobozzz/PeerTube/pull/774. b-strategy 2 -> B-frames<16 312 .outputOption('-bf 16') // NOTE: Why 16: https://github.com/Chocobozzz/PeerTube/pull/774. b-strategy 2 -> B-frames<16
295 .outputOption('-map_metadata -1') // strip all metadata 313 .outputOption('-map_metadata -1') // strip all metadata
296 .outputOption('-movflags faststart') 314 .outputOption('-movflags faststart')
297 const _audio = await audio.get(localFfmpeg)
298 315
299 if (!_audio.audioStream) { 316 const parsedAudio = await audio.get(localCommand)
300 return localFfmpeg.noAudio()
301 }
302 317
303 // we favor VBR, if a good AAC encoder is available 318 if (!parsedAudio.audioStream) {
304 if ((await checkFFmpegEncoders()).get('libfdk_aac')) { 319 localCommand = localCommand.noAudio()
305 return localFfmpeg 320 } else if ((await checkFFmpegEncoders()).get('libfdk_aac')) { // we favor VBR, if a good AAC encoder is available
321 localCommand = localCommand
306 .audioCodec('libfdk_aac') 322 .audioCodec('libfdk_aac')
307 .audioQuality(5) 323 .audioQuality(5)
324 } else {
325 // we try to reduce the ceiling bitrate by making rough correspondances of bitrates
326 // of course this is far from perfect, but it might save some space in the end
327 const audioCodecName = parsedAudio.audioStream[ 'codec_name' ]
328 let bitrate: number
329 if (audio.bitrate[ audioCodecName ]) {
330 bitrate = audio.bitrate[ audioCodecName ](parsedAudio.audioStream[ 'bit_rate' ])
331
332 if (bitrate === -1) localCommand = localCommand.audioCodec('copy')
333 else if (bitrate !== undefined) localCommand = localCommand.audioBitrate(bitrate)
334 }
308 } 335 }
309 336
310 // we try to reduce the ceiling bitrate by making rough correspondances of bitrates
311 // of course this is far from perfect, but it might save some space in the end
312 const audioCodecName = _audio.audioStream['codec_name']
313 let bitrate: number
314 if (audio.bitrate[audioCodecName]) {
315 bitrate = audio.bitrate[audioCodecName](_audio.audioStream['bit_rate'])
316
317 if (bitrate === -1) return localFfmpeg.audioCodec('copy')
318 }
319
320 if (bitrate !== undefined) return localFfmpeg.audioBitrate(bitrate)
321
322 // Constrained Encoding (VBV) 337 // Constrained Encoding (VBV)
323 // https://slhck.info/video/2017/03/01/rate-control.html 338 // https://slhck.info/video/2017/03/01/rate-control.html
324 // https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate 339 // https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate
325 const targetBitrate = getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS) 340 const targetBitrate = getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS)
326 localFfmpeg = localFfmpeg.outputOptions([`-maxrate ${ targetBitrate }`, `-bufsize ${ targetBitrate * 2 }`]) 341 localCommand = localCommand.outputOptions([`-maxrate ${ targetBitrate }`, `-bufsize ${ targetBitrate * 2 }`])
327 342
328 // Keyframe interval of 2 seconds for faster seeking and resolution switching. 343 // Keyframe interval of 2 seconds for faster seeking and resolution switching.
329 // https://streaminglearningcenter.com/blogs/whats-the-right-keyframe-interval.html 344 // https://streaminglearningcenter.com/blogs/whats-the-right-keyframe-interval.html
330 // https://superuser.com/a/908325 345 // https://superuser.com/a/908325
331 localFfmpeg = localFfmpeg.outputOption(`-g ${ fps * 2 }`) 346 localCommand = localCommand.outputOption(`-g ${ fps * 2 }`)
332 347
333 return localFfmpeg 348 return localCommand
334} 349}
diff --git a/server/initializers/checker-before-init.ts b/server/initializers/checker-before-init.ts
index 4f46d406a..9dfb5d68c 100644
--- a/server/initializers/checker-before-init.ts
+++ b/server/initializers/checker-before-init.ts
@@ -77,7 +77,7 @@ async function checkFFmpeg (CONFIG: { TRANSCODING: { ENABLED: boolean } }) {
77 } 77 }
78 } 78 }
79 79
80 checkFFmpegEncoders() 80 return checkFFmpegEncoders()
81} 81}
82 82
83// Optional encoders, if present, can be used to improve transcoding 83// Optional encoders, if present, can be used to improve transcoding
@@ -95,10 +95,10 @@ async function checkFFmpegEncoders (): Promise<Map<string, boolean>> {
95 supportedOptionalEncoders = new Map<string, boolean>() 95 supportedOptionalEncoders = new Map<string, boolean>()
96 96
97 for (const encoder of optionalEncoders) { 97 for (const encoder of optionalEncoders) {
98 supportedOptionalEncoders.set(encoder, 98 supportedOptionalEncoders.set(encoder, encoders[encoder] !== undefined)
99 encoders[encoder] !== undefined
100 )
101 } 99 }
100
101 return supportedOptionalEncoders
102} 102}
103 103
104// --------------------------------------------------------------------------- 104// ---------------------------------------------------------------------------
diff --git a/server/tests/api/videos/multiple-servers.ts b/server/tests/api/videos/multiple-servers.ts
index 4553ee855..b9ace2885 100644
--- a/server/tests/api/videos/multiple-servers.ts
+++ b/server/tests/api/videos/multiple-servers.ts
@@ -987,19 +987,19 @@ describe('Test multiple servers', function () {
987 files: [ 987 files: [
988 { 988 {
989 resolution: 720, 989 resolution: 720,
990 size: 36000 990 size: 72000
991 }, 991 },
992 { 992 {
993 resolution: 480, 993 resolution: 480,
994 size: 21000 994 size: 45000
995 }, 995 },
996 { 996 {
997 resolution: 360, 997 resolution: 360,
998 size: 17000 998 size: 34600
999 }, 999 },
1000 { 1000 {
1001 resolution: 240, 1001 resolution: 240,
1002 size: 13000 1002 size: 24770
1003 } 1003 }
1004 ] 1004 ]
1005 } 1005 }
diff --git a/server/tests/api/videos/video-transcoder.ts b/server/tests/api/videos/video-transcoder.ts
index 0ce5197ea..0a567873c 100644
--- a/server/tests/api/videos/video-transcoder.ts
+++ b/server/tests/api/videos/video-transcoder.ts
@@ -123,7 +123,7 @@ describe('Test video transcoding', function () {
123 expect(videoDetails.files).to.have.lengthOf(4) 123 expect(videoDetails.files).to.have.lengthOf(4)
124 124
125 const path = join(root(), 'test2', 'videos', video.uuid + '-240.mp4') 125 const path = join(root(), 'test2', 'videos', video.uuid + '-240.mp4')
126 const probe = await audio.get(ffmpeg, path) 126 const probe = await audio.get(path)
127 127
128 if (probe.audioStream) { 128 if (probe.audioStream) {
129 expect(probe.audioStream[ 'codec_name' ]).to.be.equal('aac') 129 expect(probe.audioStream[ 'codec_name' ]).to.be.equal('aac')
@@ -154,7 +154,7 @@ describe('Test video transcoding', function () {
154 154
155 expect(videoDetails.files).to.have.lengthOf(4) 155 expect(videoDetails.files).to.have.lengthOf(4)
156 const path = join(root(), 'test2', 'videos', video.uuid + '-240.mp4') 156 const path = join(root(), 'test2', 'videos', video.uuid + '-240.mp4')
157 const probe = await audio.get(ffmpeg, path) 157 const probe = await audio.get(path)
158 expect(probe).to.not.have.property('audioStream') 158 expect(probe).to.not.have.property('audioStream')
159 } 159 }
160 }) 160 })
@@ -179,9 +179,9 @@ describe('Test video transcoding', function () {
179 179
180 expect(videoDetails.files).to.have.lengthOf(4) 180 expect(videoDetails.files).to.have.lengthOf(4)
181 const fixturePath = buildAbsoluteFixturePath(videoAttributes.fixture) 181 const fixturePath = buildAbsoluteFixturePath(videoAttributes.fixture)
182 const fixtureVideoProbe = await audio.get(ffmpeg, fixturePath) 182 const fixtureVideoProbe = await audio.get(fixturePath)
183 const path = join(root(), 'test2', 'videos', video.uuid + '-240.mp4') 183 const path = join(root(), 'test2', 'videos', video.uuid + '-240.mp4')
184 const videoProbe = await audio.get(ffmpeg, path) 184 const videoProbe = await audio.get(path)
185 if (videoProbe.audioStream && fixtureVideoProbe.audioStream) { 185 if (videoProbe.audioStream && fixtureVideoProbe.audioStream) {
186 const toOmit = [ 'max_bit_rate', 'duration', 'duration_ts', 'nb_frames', 'start_time', 'start_pts' ] 186 const toOmit = [ 'max_bit_rate', 'duration', 'duration_ts', 'nb_frames', 'start_time', 'start_pts' ]
187 expect(omit(videoProbe.audioStream, toOmit)).to.be.deep.equal(omit(fixtureVideoProbe.audioStream, toOmit)) 187 expect(omit(videoProbe.audioStream, toOmit)).to.be.deep.equal(omit(fixtureVideoProbe.audioStream, toOmit))