aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2020-11-24 14:08:23 +0100
committerChocobozzz <chocobozzz@cpy.re>2020-11-25 10:07:51 +0100
commit5a547f69d5dc5867e253f7721513479c754b4f15 (patch)
tree5ccad0e07d04e24d7a4c0b624a46d3b5a93ebce5
parent9252a33d115bba85adcfbc18ab3725924642871c (diff)
downloadPeerTube-5a547f69d5dc5867e253f7721513479c754b4f15.tar.gz
PeerTube-5a547f69d5dc5867e253f7721513479c754b4f15.tar.zst
PeerTube-5a547f69d5dc5867e253f7721513479c754b4f15.zip
Support encoding profiles
-rwxr-xr-xscripts/dev/cli.sh2
-rw-r--r--server/helpers/ffmpeg-utils.ts223
-rw-r--r--server/helpers/ffprobe-utils.ts19
-rw-r--r--server/lib/live-manager.ts12
-rw-r--r--server/lib/video-transcoding-profiles.ts95
-rw-r--r--server/lib/video-transcoding.ts91
-rw-r--r--server/tools/test.ts105
-rw-r--r--shared/extra-utils/server/servers.ts12
-rw-r--r--shared/models/videos/video-resolution.enum.ts2
9 files changed, 403 insertions, 158 deletions
diff --git a/scripts/dev/cli.sh b/scripts/dev/cli.sh
index dc6f0af0d..4bf4808b8 100755
--- a/scripts/dev/cli.sh
+++ b/scripts/dev/cli.sh
@@ -12,4 +12,4 @@ rm -rf ./dist/server/tools/
12mkdir -p "./dist/server/tools" 12mkdir -p "./dist/server/tools"
13cp -r "./server/tools/node_modules" "./dist/server/tools" 13cp -r "./server/tools/node_modules" "./dist/server/tools"
14 14
15npm run tsc -- --watch --project ./server/tools/tsconfig.json 15npm run tsc -- --watch --sourceMap --project ./server/tools/tsconfig.json
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts
index e297108df..712ec757e 100644
--- a/server/helpers/ffmpeg-utils.ts
+++ b/server/helpers/ffmpeg-utils.ts
@@ -1,10 +1,10 @@
1import * as ffmpeg from 'fluent-ffmpeg' 1import * as ffmpeg from 'fluent-ffmpeg'
2import { readFile, remove, writeFile } from 'fs-extra' 2import { readFile, remove, writeFile } from 'fs-extra'
3import { dirname, join } from 'path' 3import { dirname, join } from 'path'
4import { getTargetBitrate, VideoResolution } from '../../shared/models/videos' 4import { FFMPEG_NICE, VIDEO_LIVE, VIDEO_TRANSCODING_ENCODERS, VIDEO_TRANSCODING_FPS } from '@server/initializers/constants'
5import { VideoResolution } from '../../shared/models/videos'
5import { checkFFmpegEncoders } from '../initializers/checker-before-init' 6import { checkFFmpegEncoders } from '../initializers/checker-before-init'
6import { CONFIG } from '../initializers/config' 7import { CONFIG } from '../initializers/config'
7import { FFMPEG_NICE, VIDEO_LIVE, VIDEO_TRANSCODING_ENCODERS, VIDEO_TRANSCODING_FPS } from '../initializers/constants'
8import { getAudioStream, getClosestFramerateStandard, getVideoFileFPS } from './ffprobe-utils' 8import { getAudioStream, getClosestFramerateStandard, getVideoFileFPS } from './ffprobe-utils'
9import { processImage } from './image-utils' 9import { processImage } from './image-utils'
10import { logger } from './logger' 10import { logger } from './logger'
@@ -19,11 +19,13 @@ export type EncoderOptionsBuilder = (params: {
19 input: string 19 input: string
20 resolution: VideoResolution 20 resolution: VideoResolution
21 fps?: number 21 fps?: number
22 streamNum?: number
22}) => Promise<EncoderOptions> | EncoderOptions 23}) => Promise<EncoderOptions> | EncoderOptions
23 24
24// Options types 25// Options types
25 26
26export interface EncoderOptions { 27export interface EncoderOptions {
28 copy?: boolean
27 outputOptions: string[] 29 outputOptions: string[]
28} 30}
29 31
@@ -37,7 +39,7 @@ export interface EncoderProfile <T> {
37 39
38export type AvailableEncoders = { 40export type AvailableEncoders = {
39 [ id in 'live' | 'vod' ]: { 41 [ id in 'live' | 'vod' ]: {
40 [ encoder in 'libx264' | 'aac' | 'libfdkAAC' ]: EncoderProfile<EncoderOptionsBuilder> 42 [ encoder in 'libx264' | 'aac' | 'libfdk_aac' ]?: EncoderProfile<EncoderOptionsBuilder>
41 } 43 }
42} 44}
43 45
@@ -197,8 +199,20 @@ async function transcode (options: TranscodeOptions) {
197// Live muxing/transcoding functions 199// Live muxing/transcoding functions
198// --------------------------------------------------------------------------- 200// ---------------------------------------------------------------------------
199 201
200function getLiveTranscodingCommand (rtmpUrl: string, outPath: string, resolutions: number[], fps: number, deleteSegments: boolean) { 202async function getLiveTranscodingCommand (options: {
201 const command = getFFmpeg(rtmpUrl) 203 rtmpUrl: string
204 outPath: string
205 resolutions: number[]
206 fps: number
207 deleteSegments: boolean
208
209 availableEncoders: AvailableEncoders
210 profile: string
211}) {
212 const { rtmpUrl, outPath, resolutions, fps, deleteSegments, availableEncoders, profile } = options
213 const input = rtmpUrl
214
215 const command = getFFmpeg(input)
202 command.inputOption('-fflags nobuffer') 216 command.inputOption('-fflags nobuffer')
203 217
204 const varStreamMap: string[] = [] 218 const varStreamMap: string[] = []
@@ -219,19 +233,43 @@ function getLiveTranscodingCommand (rtmpUrl: string, outPath: string, resolution
219 })) 233 }))
220 ]) 234 ])
221 235
222 addEncoderDefaultParams(command, 'libx264', fps)
223
224 command.outputOption('-preset superfast') 236 command.outputOption('-preset superfast')
225 237
226 for (let i = 0; i < resolutions.length; i++) { 238 for (let i = 0; i < resolutions.length; i++) {
227 const resolution = resolutions[i] 239 const resolution = resolutions[i]
240 const baseEncoderBuilderParams = { input, availableEncoders, profile, fps, resolution, streamNum: i, videoType: 'live' as 'live' }
241
242 {
243 const builderResult = await getEncoderBuilderResult(Object.assign({}, baseEncoderBuilderParams, { streamType: 'VIDEO' }))
244 if (!builderResult) {
245 throw new Error('No available live video encoder found')
246 }
247
248 command.outputOption(`-map [vout${resolution}]`)
249
250 addDefaultEncoderParams({ command, encoder: builderResult.encoder, fps, streamNum: i })
251
252 logger.debug('Apply ffmpeg live video params from %s.', builderResult.encoder, builderResult)
228 253
229 command.outputOption(`-map [vout${resolution}]`) 254 command.outputOption(`${buildStreamSuffix('-c:v', i)} ${builderResult.encoder}`)
230 command.outputOption(`-c:v:${i} libx264`) 255 command.addOutputOptions(builderResult.result.outputOptions)
231 command.outputOption(`-b:v:${i} ${getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS)}`) 256 }
257
258 {
259 const builderResult = await getEncoderBuilderResult(Object.assign({}, baseEncoderBuilderParams, { streamType: 'AUDIO' }))
260 if (!builderResult) {
261 throw new Error('No available live audio encoder found')
262 }
232 263
233 command.outputOption(`-map a:0`) 264 command.outputOption('-map a:0')
234 command.outputOption(`-c:a:${i} aac`) 265
266 addDefaultEncoderParams({ command, encoder: builderResult.encoder, fps, streamNum: i })
267
268 logger.debug('Apply ffmpeg live audio params from %s.', builderResult.encoder, builderResult)
269
270 command.outputOption(`${buildStreamSuffix('-c:a', i)} ${builderResult.encoder}`)
271 command.addOutputOptions(builderResult.result.outputOptions)
272 }
235 273
236 varStreamMap.push(`v:${i},a:${i}`) 274 varStreamMap.push(`v:${i},a:${i}`)
237 } 275 }
@@ -282,11 +320,20 @@ async function hlsPlaylistToFragmentedMP4 (hlsDirectory: string, segmentFiles: s
282 return runCommand(command, cleaner) 320 return runCommand(command, cleaner)
283} 321}
284 322
323function buildStreamSuffix (base: string, streamNum?: number) {
324 if (streamNum !== undefined) {
325 return `${base}:${streamNum}`
326 }
327
328 return base
329}
330
285// --------------------------------------------------------------------------- 331// ---------------------------------------------------------------------------
286 332
287export { 333export {
288 getLiveTranscodingCommand, 334 getLiveTranscodingCommand,
289 getLiveMuxingCommand, 335 getLiveMuxingCommand,
336 buildStreamSuffix,
290 convertWebPToJPG, 337 convertWebPToJPG,
291 processGIF, 338 processGIF,
292 generateImageFromVideoFile, 339 generateImageFromVideoFile,
@@ -302,19 +349,35 @@ export {
302// Default options 349// Default options
303// --------------------------------------------------------------------------- 350// ---------------------------------------------------------------------------
304 351
305function addEncoderDefaultParams (command: ffmpeg.FfmpegCommand, encoder: 'libx264' | string, fps?: number) { 352function addDefaultEncoderParams (options: {
306 if (encoder !== 'libx264') return 353 command: ffmpeg.FfmpegCommand
307 354 encoder: 'libx264' | string
308 command.outputOption('-level 3.1') // 3.1 is the minimal resource allocation for our highest supported resolution 355 streamNum?: number
309 .outputOption('-b_strategy 1') // NOTE: b-strategy 1 - heuristic algorithm, 16 is optimal B-frames for it 356 fps?: number
310 .outputOption('-bf 16') // NOTE: Why 16: https://github.com/Chocobozzz/PeerTube/pull/774. b-strategy 2 -> B-frames<16 357}) {
311 .outputOption('-pix_fmt yuv420p') // allows import of source material with incompatible pixel formats (e.g. MJPEG video) 358 const { command, encoder, fps, streamNum } = options
312 .outputOption('-map_metadata -1') // strip all metadata 359
313 .outputOption('-max_muxing_queue_size 1024') // avoid issues when transcoding some files: https://trac.ffmpeg.org/ticket/6375 360 if (encoder === 'libx264') {
314 // Keyframe interval of 2 seconds for faster seeking and resolution switching. 361 // 3.1 is the minimal resource allocation for our highest supported resolution
315 // https://streaminglearningcenter.com/blogs/whats-the-right-keyframe-interval.html 362 command.outputOption('-level 3.1')
316 // https://superuser.com/a/908325 363 // NOTE: b-strategy 1 - heuristic algorithm, 16 is optimal B-frames for it
317 .outputOption('-g ' + (fps * 2)) 364 .outputOption('-b_strategy 1')
365 // NOTE: Why 16: https://github.com/Chocobozzz/PeerTube/pull/774. b-strategy 2 -> B-frames<16
366 .outputOption('-bf 16')
367 // allows import of source material with incompatible pixel formats (e.g. MJPEG video)
368 .outputOption(buildStreamSuffix('-pix_fmt', streamNum) + ' yuv420p')
369 // strip all metadata
370 .outputOption('-map_metadata -1')
371 // avoid issues when transcoding some files: https://trac.ffmpeg.org/ticket/6375
372 .outputOption(buildStreamSuffix('-max_muxing_queue_size', streamNum) + ' 1024')
373
374 if (fps) {
375 // Keyframe interval of 2 seconds for faster seeking and resolution switching.
376 // https://streaminglearningcenter.com/blogs/whats-the-right-keyframe-interval.html
377 // https://superuser.com/a/908325
378 command.outputOption('-g ' + (fps * 2))
379 }
380 }
318} 381}
319 382
320function addDefaultLiveHLSParams (command: ffmpeg.FfmpegCommand, outPath: string, deleteSegments: boolean) { 383function addDefaultLiveHLSParams (command: ffmpeg.FfmpegCommand, outPath: string, deleteSegments: boolean) {
@@ -352,17 +415,18 @@ async function buildx264VODCommand (command: ffmpeg.FfmpegCommand, options: Tran
352 415
353 if (options.resolution !== undefined) { 416 if (options.resolution !== undefined) {
354 // '?x720' or '720x?' for example 417 // '?x720' or '720x?' for example
355 const size = options.isPortraitMode === true ? `${options.resolution}x?` : `?x${options.resolution}` 418 const size = options.isPortraitMode === true
419 ? `${options.resolution}x?`
420 : `?x${options.resolution}`
421
356 command = command.size(size) 422 command = command.size(size)
357 } 423 }
358 424
359 if (fps) { 425 // Hard FPS limits
360 // Hard FPS limits 426 if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = getClosestFramerateStandard(fps, 'HD_STANDARD')
361 if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = getClosestFramerateStandard(fps, 'HD_STANDARD') 427 else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN
362 else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN
363 428
364 command = command.withFPS(fps) 429 command = command.withFPS(fps)
365 }
366 430
367 return command 431 return command
368} 432}
@@ -445,6 +509,49 @@ function getHLSVideoPath (options: HLSTranscodeOptions) {
445// Transcoding presets 509// Transcoding presets
446// --------------------------------------------------------------------------- 510// ---------------------------------------------------------------------------
447 511
512async function getEncoderBuilderResult (options: {
513 streamType: string
514 input: string
515
516 availableEncoders: AvailableEncoders
517 profile: string
518
519 videoType: 'vod' | 'live'
520
521 resolution: number
522 fps?: number
523 streamNum?: number
524}) {
525 const { availableEncoders, input, profile, resolution, streamType, fps, streamNum, videoType } = options
526
527 const encodersToTry: string[] = VIDEO_TRANSCODING_ENCODERS[streamType]
528
529 for (const encoder of encodersToTry) {
530 if (!(await checkFFmpegEncoders()).get(encoder) || !availableEncoders[videoType][encoder]) continue
531
532 const builderProfiles: EncoderProfile<EncoderOptionsBuilder> = availableEncoders[videoType][encoder]
533 let builder = builderProfiles[profile]
534
535 if (!builder) {
536 logger.debug('Profile %s for encoder %s not available. Fallback to default.', profile, encoder)
537 builder = builderProfiles.default
538 }
539
540 const result = await builder({ input, resolution: resolution, fps, streamNum })
541
542 return {
543 result,
544
545 // If we don't have output options, then copy the input stream
546 encoder: result.copy === true
547 ? 'copy'
548 : encoder
549 }
550 }
551
552 return null
553}
554
448async function presetVideo ( 555async function presetVideo (
449 command: ffmpeg.FfmpegCommand, 556 command: ffmpeg.FfmpegCommand,
450 input: string, 557 input: string,
@@ -459,49 +566,41 @@ async function presetVideo (
459 const parsedAudio = await getAudioStream(input) 566 const parsedAudio = await getAudioStream(input)
460 567
461 let streamsToProcess = [ 'AUDIO', 'VIDEO' ] 568 let streamsToProcess = [ 'AUDIO', 'VIDEO' ]
462 const streamsFound = {
463 AUDIO: '',
464 VIDEO: ''
465 }
466 569
467 if (!parsedAudio.audioStream) { 570 if (!parsedAudio.audioStream) {
468 localCommand = localCommand.noAudio() 571 localCommand = localCommand.noAudio()
469 streamsToProcess = [ 'VIDEO' ] 572 streamsToProcess = [ 'VIDEO' ]
470 } 573 }
471 574
472 for (const stream of streamsToProcess) { 575 for (const streamType of streamsToProcess) {
473 const encodersToTry: string[] = VIDEO_TRANSCODING_ENCODERS[stream] 576 const { profile, resolution, availableEncoders } = transcodeOptions
474 577
475 for (const encoder of encodersToTry) { 578 const builderResult = await getEncoderBuilderResult({
476 if (!(await checkFFmpegEncoders()).get(encoder)) continue 579 streamType,
477 580 input,
478 const builderProfiles: EncoderProfile<EncoderOptionsBuilder> = transcodeOptions.availableEncoders.vod[encoder] 581 resolution,
479 let builder = builderProfiles[transcodeOptions.profile] 582 availableEncoders,
480 583 profile,
481 if (!builder) { 584 fps,
482 logger.debug('Profile %s for encoder %s not available. Fallback to default.', transcodeOptions.profile, encoder) 585 videoType: 'vod' as 'vod'
483 builder = builderProfiles.default 586 })
484 }
485
486 const builderResult = await builder({ input, resolution: transcodeOptions.resolution, fps })
487
488 logger.debug('Apply ffmpeg params from %s.', encoder, builderResult)
489 587
490 localCommand.outputOptions(builderResult.outputOptions) 588 if (!builderResult) {
589 throw new Error('No available encoder found for stream ' + streamType)
590 }
491 591
492 addEncoderDefaultParams(localCommand, encoder) 592 logger.debug('Apply ffmpeg params from %s.', builderResult.encoder, builderResult)
493 593
494 streamsFound[stream] = encoder 594 if (streamType === 'VIDEO') {
495 break 595 localCommand.videoCodec(builderResult.encoder)
596 } else if (streamType === 'AUDIO') {
597 localCommand.audioCodec(builderResult.encoder)
496 } 598 }
497 599
498 if (!streamsFound[stream]) { 600 command.addOutputOptions(builderResult.result.outputOptions)
499 throw new Error('No available encoder found ' + encodersToTry.join(', ')) 601 addDefaultEncoderParams({ command: localCommand, encoder: builderResult.encoder, fps })
500 }
501 } 602 }
502 603
503 localCommand.videoCodec(streamsFound.VIDEO)
504
505 return localCommand 604 return localCommand
506} 605}
507 606
diff --git a/server/helpers/ffprobe-utils.ts b/server/helpers/ffprobe-utils.ts
index 6159d3963..5545ddbbf 100644
--- a/server/helpers/ffprobe-utils.ts
+++ b/server/helpers/ffprobe-utils.ts
@@ -198,9 +198,12 @@ function computeResolutionsToTranscode (videoFileResolution: number, type: 'vod'
198async function canDoQuickTranscode (path: string): Promise<boolean> { 198async function canDoQuickTranscode (path: string): Promise<boolean> {
199 const probe = await ffprobePromise(path) 199 const probe = await ffprobePromise(path)
200 200
201 // NOTE: This could be optimized by running ffprobe only once (but it runs fast anyway) 201 return await canDoQuickVideoTranscode(path, probe) &&
202 await canDoQuickAudioTranscode(path, probe)
203}
204
205async function canDoQuickVideoTranscode (path: string, probe?: ffmpeg.FfprobeData): Promise<boolean> {
202 const videoStream = await getVideoStreamFromFile(path, probe) 206 const videoStream = await getVideoStreamFromFile(path, probe)
203 const parsedAudio = await getAudioStream(path, probe)
204 const fps = await getVideoFileFPS(path, probe) 207 const fps = await getVideoFileFPS(path, probe)
205 const bitRate = await getVideoFileBitrate(path, probe) 208 const bitRate = await getVideoFileBitrate(path, probe)
206 const resolution = await getVideoFileResolution(path, probe) 209 const resolution = await getVideoFileResolution(path, probe)
@@ -212,6 +215,12 @@ async function canDoQuickTranscode (path: string): Promise<boolean> {
212 if (fps < VIDEO_TRANSCODING_FPS.MIN || fps > VIDEO_TRANSCODING_FPS.MAX) return false 215 if (fps < VIDEO_TRANSCODING_FPS.MIN || fps > VIDEO_TRANSCODING_FPS.MAX) return false
213 if (bitRate > getMaxBitrate(resolution.videoFileResolution, fps, VIDEO_TRANSCODING_FPS)) return false 216 if (bitRate > getMaxBitrate(resolution.videoFileResolution, fps, VIDEO_TRANSCODING_FPS)) return false
214 217
218 return true
219}
220
221async function canDoQuickAudioTranscode (path: string, probe?: ffmpeg.FfprobeData): Promise<boolean> {
222 const parsedAudio = await getAudioStream(path, probe)
223
215 // check audio params (if audio stream exists) 224 // check audio params (if audio stream exists)
216 if (parsedAudio.audioStream) { 225 if (parsedAudio.audioStream) {
217 if (parsedAudio.audioStream['codec_name'] !== 'aac') return false 226 if (parsedAudio.audioStream['codec_name'] !== 'aac') return false
@@ -239,11 +248,15 @@ export {
239 getVideoFileResolution, 248 getVideoFileResolution,
240 getMetadataFromFile, 249 getMetadataFromFile,
241 getMaxAudioBitrate, 250 getMaxAudioBitrate,
251 getVideoStreamFromFile,
242 getDurationFromVideoFile, 252 getDurationFromVideoFile,
243 getAudioStream, 253 getAudioStream,
244 getVideoFileFPS, 254 getVideoFileFPS,
255 ffprobePromise,
245 getClosestFramerateStandard, 256 getClosestFramerateStandard,
246 computeResolutionsToTranscode, 257 computeResolutionsToTranscode,
247 getVideoFileBitrate, 258 getVideoFileBitrate,
248 canDoQuickTranscode 259 canDoQuickTranscode,
260 canDoQuickVideoTranscode,
261 canDoQuickAudioTranscode
249} 262}
diff --git a/server/lib/live-manager.ts b/server/lib/live-manager.ts
index b9e5d4561..ee0e4de37 100644
--- a/server/lib/live-manager.ts
+++ b/server/lib/live-manager.ts
@@ -22,6 +22,7 @@ import { JobQueue } from './job-queue'
22import { PeerTubeSocket } from './peertube-socket' 22import { PeerTubeSocket } from './peertube-socket'
23import { isAbleToUploadVideo } from './user' 23import { isAbleToUploadVideo } from './user'
24import { getHLSDirectory } from './video-paths' 24import { getHLSDirectory } from './video-paths'
25import { availableEncoders } from './video-transcoding-profiles'
25 26
26import memoizee = require('memoizee') 27import memoizee = require('memoizee')
27const NodeRtmpServer = require('node-media-server/node_rtmp_server') 28const NodeRtmpServer = require('node-media-server/node_rtmp_server')
@@ -264,7 +265,16 @@ class LiveManager {
264 const deleteSegments = videoLive.saveReplay === false 265 const deleteSegments = videoLive.saveReplay === false
265 266
266 const ffmpegExec = CONFIG.LIVE.TRANSCODING.ENABLED 267 const ffmpegExec = CONFIG.LIVE.TRANSCODING.ENABLED
267 ? getLiveTranscodingCommand(rtmpUrl, outPath, allResolutions, fps, deleteSegments) 268 ? await getLiveTranscodingCommand({
269 rtmpUrl,
270 outPath,
271 resolutions:
272 allResolutions,
273 fps,
274 deleteSegments,
275 availableEncoders,
276 profile: 'default'
277 })
268 : getLiveMuxingCommand(rtmpUrl, outPath, deleteSegments) 278 : getLiveMuxingCommand(rtmpUrl, outPath, deleteSegments)
269 279
270 logger.info('Running live muxing/transcoding for %s.', videoUUID) 280 logger.info('Running live muxing/transcoding for %s.', videoUUID)
diff --git a/server/lib/video-transcoding-profiles.ts b/server/lib/video-transcoding-profiles.ts
new file mode 100644
index 000000000..12e22a19d
--- /dev/null
+++ b/server/lib/video-transcoding-profiles.ts
@@ -0,0 +1,95 @@
1import { getTargetBitrate } from '../../shared/models/videos'
2import { AvailableEncoders, buildStreamSuffix, EncoderOptionsBuilder } from '../helpers/ffmpeg-utils'
3import { ffprobePromise, getAudioStream, getMaxAudioBitrate, getVideoFileBitrate, getVideoStreamFromFile } from '../helpers/ffprobe-utils'
4import { VIDEO_TRANSCODING_FPS } from '../initializers/constants'
5
6// ---------------------------------------------------------------------------
7// Available encoders profiles
8// ---------------------------------------------------------------------------
9
10// Resources:
11// * https://slhck.info/video/2017/03/01/rate-control.html
12// * https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate
13
14const defaultX264VODOptionsBuilder: EncoderOptionsBuilder = async ({ input, resolution, fps }) => {
15 let targetBitrate = getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS)
16
17 const probe = await ffprobePromise(input)
18
19 const videoStream = await getVideoStreamFromFile(input, probe)
20 if (!videoStream) {
21 return { outputOptions: [ ] }
22 }
23
24 // Don't transcode to an higher bitrate than the original file
25 const fileBitrate = await getVideoFileBitrate(input, probe)
26 targetBitrate = Math.min(targetBitrate, fileBitrate)
27
28 return {
29 outputOptions: [
30 `-maxrate ${targetBitrate}`, `-bufsize ${targetBitrate * 2}`
31 ]
32 }
33}
34
35const defaultX264LiveOptionsBuilder: EncoderOptionsBuilder = async ({ resolution, fps, streamNum }) => {
36 const targetBitrate = getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS)
37
38 return {
39 outputOptions: [
40 `${buildStreamSuffix('-b:v', streamNum)} ${targetBitrate}`,
41 `-maxrate ${targetBitrate}`, `-bufsize ${targetBitrate * 2}`
42 ]
43 }
44}
45
46const defaultAACOptionsBuilder: EncoderOptionsBuilder = async ({ input, streamNum }) => {
47 const parsedAudio = await getAudioStream(input)
48
49 // We try to reduce the ceiling bitrate by making rough matches of bitrates
50 // Of course this is far from perfect, but it might save some space in the end
51
52 const audioCodecName = parsedAudio.audioStream['codec_name']
53
54 const bitrate = getMaxAudioBitrate(audioCodecName, parsedAudio.bitrate)
55
56 if (bitrate !== undefined && bitrate !== -1) {
57 return { outputOptions: [ buildStreamSuffix('-b:a', streamNum), bitrate + 'k' ] }
58 }
59
60 return { copy: true, outputOptions: [] }
61}
62
63const defaultLibFDKAACVODOptionsBuilder: EncoderOptionsBuilder = ({ streamNum }) => {
64 return { outputOptions: [ buildStreamSuffix('-q:a', streamNum), '5' ] }
65}
66
67const availableEncoders: AvailableEncoders = {
68 vod: {
69 libx264: {
70 default: defaultX264VODOptionsBuilder
71 },
72 aac: {
73 default: defaultAACOptionsBuilder
74 },
75 libfdk_aac: {
76 default: defaultLibFDKAACVODOptionsBuilder
77 }
78 },
79 live: {
80 libx264: {
81 default: defaultX264LiveOptionsBuilder
82 },
83 aac: {
84 default: defaultAACOptionsBuilder
85 }
86 }
87}
88
89// ---------------------------------------------------------------------------
90
91export {
92 availableEncoders
93}
94
95// ---------------------------------------------------------------------------
diff --git a/server/lib/video-transcoding.ts b/server/lib/video-transcoding.ts
index 0e6a0e984..aaad219dd 100644
--- a/server/lib/video-transcoding.ts
+++ b/server/lib/video-transcoding.ts
@@ -2,30 +2,18 @@ import { copyFile, ensureDir, move, remove, stat } from 'fs-extra'
2import { basename, extname as extnameUtil, join } from 'path' 2import { basename, extname as extnameUtil, join } from 'path'
3import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' 3import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
4import { MStreamingPlaylistFilesVideo, MVideoFile, MVideoWithAllFiles, MVideoWithFile } from '@server/types/models' 4import { MStreamingPlaylistFilesVideo, MVideoFile, MVideoWithAllFiles, MVideoWithFile } from '@server/types/models'
5import { getTargetBitrate, VideoResolution } from '../../shared/models/videos' 5import { VideoResolution } from '../../shared/models/videos'
6import { VideoStreamingPlaylistType } from '../../shared/models/videos/video-streaming-playlist.type' 6import { VideoStreamingPlaylistType } from '../../shared/models/videos/video-streaming-playlist.type'
7import { AvailableEncoders, EncoderOptionsBuilder, transcode, TranscodeOptions, TranscodeOptionsType } from '../helpers/ffmpeg-utils' 7import { transcode, TranscodeOptions, TranscodeOptionsType } from '../helpers/ffmpeg-utils'
8import { 8import { canDoQuickTranscode, getDurationFromVideoFile, getMetadataFromFile, getVideoFileFPS } from '../helpers/ffprobe-utils'
9 canDoQuickTranscode,
10 getAudioStream,
11 getDurationFromVideoFile,
12 getMaxAudioBitrate,
13 getMetadataFromFile,
14 getVideoFileBitrate,
15 getVideoFileFPS
16} from '../helpers/ffprobe-utils'
17import { logger } from '../helpers/logger' 9import { logger } from '../helpers/logger'
18import { CONFIG } from '../initializers/config' 10import { CONFIG } from '../initializers/config'
19import { 11import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION, WEBSERVER } from '../initializers/constants'
20 HLS_STREAMING_PLAYLIST_DIRECTORY,
21 P2P_MEDIA_LOADER_PEER_VERSION,
22 VIDEO_TRANSCODING_FPS,
23 WEBSERVER
24} from '../initializers/constants'
25import { VideoFileModel } from '../models/video/video-file' 12import { VideoFileModel } from '../models/video/video-file'
26import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' 13import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
27import { updateMasterHLSPlaylist, updateSha256VODSegments } from './hls' 14import { updateMasterHLSPlaylist, updateSha256VODSegments } from './hls'
28import { generateVideoStreamingPlaylistName, getVideoFilename, getVideoFilePath } from './video-paths' 15import { generateVideoStreamingPlaylistName, getVideoFilename, getVideoFilePath } from './video-paths'
16import { availableEncoders } from './video-transcoding-profiles'
29 17
30/** 18/**
31 * Optimize the original video file and replace it. The resolution is not changed. 19 * Optimize the original video file and replace it. The resolution is not changed.
@@ -253,75 +241,6 @@ async function generateHlsPlaylist (options: {
253} 241}
254 242
255// --------------------------------------------------------------------------- 243// ---------------------------------------------------------------------------
256// Available encoders profiles
257// ---------------------------------------------------------------------------
258
259const defaultX264OptionsBuilder: EncoderOptionsBuilder = async ({ input, resolution, fps }) => {
260 if (!fps) return { outputOptions: [] }
261
262 let targetBitrate = getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS)
263
264 // Don't transcode to an higher bitrate than the original file
265 const fileBitrate = await getVideoFileBitrate(input)
266 targetBitrate = Math.min(targetBitrate, fileBitrate)
267
268 return {
269 outputOptions: [
270 // Constrained Encoding (VBV)
271 // https://slhck.info/video/2017/03/01/rate-control.html
272 // https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate
273 `-maxrate ${targetBitrate}`, `-bufsize ${targetBitrate * 2}`
274 ]
275 }
276}
277
278const defaultAACOptionsBuilder: EncoderOptionsBuilder = async ({ input }) => {
279 const parsedAudio = await getAudioStream(input)
280
281 // we try to reduce the ceiling bitrate by making rough matches of bitrates
282 // of course this is far from perfect, but it might save some space in the end
283
284 const audioCodecName = parsedAudio.audioStream['codec_name']
285
286 const bitrate = getMaxAudioBitrate(audioCodecName, parsedAudio.bitrate)
287
288 if (bitrate !== undefined && bitrate !== -1) {
289 return { outputOptions: [ '-b:a', bitrate + 'k' ] }
290 }
291
292 return { outputOptions: [] }
293}
294
295const defaultLibFDKAACOptionsBuilder: EncoderOptionsBuilder = () => {
296 return { outputOptions: [ '-aq', '5' ] }
297}
298
299const availableEncoders: AvailableEncoders = {
300 vod: {
301 libx264: {
302 default: defaultX264OptionsBuilder
303 },
304 aac: {
305 default: defaultAACOptionsBuilder
306 },
307 libfdkAAC: {
308 default: defaultLibFDKAACOptionsBuilder
309 }
310 },
311 live: {
312 libx264: {
313 default: defaultX264OptionsBuilder
314 },
315 aac: {
316 default: defaultAACOptionsBuilder
317 },
318 libfdkAAC: {
319 default: defaultLibFDKAACOptionsBuilder
320 }
321 }
322}
323
324// ---------------------------------------------------------------------------
325 244
326export { 245export {
327 generateHlsPlaylist, 246 generateHlsPlaylist,
diff --git a/server/tools/test.ts b/server/tools/test.ts
new file mode 100644
index 000000000..23bf0120f
--- /dev/null
+++ b/server/tools/test.ts
@@ -0,0 +1,105 @@
1import { registerTSPaths } from '../helpers/register-ts-paths'
2registerTSPaths()
3
4import { LiveVideo, LiveVideoCreate, VideoPrivacy } from '@shared/models'
5import * as program from 'commander'
6import {
7 createLive,
8 flushAndRunServer,
9 getLive,
10 killallServers,
11 sendRTMPStream,
12 ServerInfo,
13 setAccessTokensToServers,
14 setDefaultVideoChannel,
15 updateCustomSubConfig
16} from '../../shared/extra-utils'
17
18type CommandType = 'live-mux' | 'live-transcoding'
19
20registerTSPaths()
21
22const command = program
23 .name('test')
24 .option('-t, --type <type>', 'live-muxing|live-transcoding')
25 .parse(process.argv)
26
27run()
28 .catch(err => {
29 console.error(err)
30 process.exit(-1)
31 })
32
33async function run () {
34 const commandType: CommandType = command['type']
35 if (!commandType) {
36 console.error('Miss command type')
37 process.exit(-1)
38 }
39
40 console.log('Starting server.')
41
42 const server = await flushAndRunServer(1, {}, [], false)
43
44 const cleanup = () => {
45 console.log('Killing server')
46 killallServers([ server ])
47 }
48
49 process.on('exit', cleanup)
50 process.on('SIGINT', cleanup)
51
52 await setAccessTokensToServers([ server ])
53 await setDefaultVideoChannel([ server ])
54
55 await buildConfig(server, commandType)
56
57 const attributes: LiveVideoCreate = {
58 name: 'live',
59 saveReplay: true,
60 channelId: server.videoChannel.id,
61 privacy: VideoPrivacy.PUBLIC
62 }
63
64 console.log('Creating live.')
65
66 const res = await createLive(server.url, server.accessToken, attributes)
67 const liveVideoUUID = res.body.video.uuid
68
69 const resLive = await getLive(server.url, server.accessToken, liveVideoUUID)
70 const live: LiveVideo = resLive.body
71
72 console.log('Sending RTMP stream.')
73
74 const ffmpegCommand = sendRTMPStream(live.rtmpUrl, live.streamKey)
75
76 ffmpegCommand.on('error', err => {
77 console.error(err)
78 process.exit(-1)
79 })
80
81 ffmpegCommand.on('end', () => {
82 console.log('ffmpeg ended')
83 process.exit(0)
84 })
85}
86
87// ----------------------------------------------------------------------------
88
89async function buildConfig (server: ServerInfo, commandType: CommandType) {
90 await updateCustomSubConfig(server.url, server.accessToken, {
91 instance: {
92 customizations: {
93 javascript: '',
94 css: ''
95 }
96 },
97 live: {
98 enabled: true,
99 allowReplay: true,
100 transcoding: {
101 enabled: commandType === 'live-transcoding'
102 }
103 }
104 })
105}
diff --git a/shared/extra-utils/server/servers.ts b/shared/extra-utils/server/servers.ts
index a647b0eb4..75e79cc41 100644
--- a/shared/extra-utils/server/servers.ts
+++ b/shared/extra-utils/server/servers.ts
@@ -104,7 +104,7 @@ function randomRTMP () {
104 return randomInt(low, high) 104 return randomInt(low, high)
105} 105}
106 106
107async function flushAndRunServer (serverNumber: number, configOverride?: Object, args = []) { 107async function flushAndRunServer (serverNumber: number, configOverride?: Object, args = [], silent = true) {
108 const parallel = parallelTests() 108 const parallel = parallelTests()
109 109
110 const internalServerNumber = parallel ? randomServer() : serverNumber 110 const internalServerNumber = parallel ? randomServer() : serverNumber
@@ -133,10 +133,10 @@ async function flushAndRunServer (serverNumber: number, configOverride?: Object,
133 } 133 }
134 } 134 }
135 135
136 return runServer(server, configOverride, args) 136 return runServer(server, configOverride, args, silent)
137} 137}
138 138
139async function runServer (server: ServerInfo, configOverrideArg?: any, args = []) { 139async function runServer (server: ServerInfo, configOverrideArg?: any, args = [], silent?: boolean) {
140 // These actions are async so we need to be sure that they have both been done 140 // These actions are async so we need to be sure that they have both been done
141 const serverRunString = { 141 const serverRunString = {
142 'Server listening': false 142 'Server listening': false
@@ -240,7 +240,11 @@ async function runServer (server: ServerInfo, configOverrideArg?: any, args = []
240 // If no, there is maybe one thing not already initialized (client/user credentials generation...) 240 // If no, there is maybe one thing not already initialized (client/user credentials generation...)
241 if (dontContinue === true) return 241 if (dontContinue === true) return
242 242
243 server.app.stdout.removeListener('data', onStdout) 243 if (silent === false) {
244 console.log(data.toString())
245 } else {
246 server.app.stdout.removeListener('data', onStdout)
247 }
244 248
245 process.on('exit', () => { 249 process.on('exit', () => {
246 try { 250 try {
diff --git a/shared/models/videos/video-resolution.enum.ts b/shared/models/videos/video-resolution.enum.ts
index 571ab5d8f..dcd55dad8 100644
--- a/shared/models/videos/video-resolution.enum.ts
+++ b/shared/models/videos/video-resolution.enum.ts
@@ -81,7 +81,7 @@ export function getTargetBitrate (resolution: number, fps: number, fpsTranscodin
81 // Example outputs: 81 // Example outputs:
82 // 1080p10: 2420 kbps, 1080p30: 3300 kbps, 1080p60: 4620 kbps 82 // 1080p10: 2420 kbps, 1080p30: 3300 kbps, 1080p60: 4620 kbps
83 // 720p10: 1283 kbps, 720p30: 1750 kbps, 720p60: 2450 kbps 83 // 720p10: 1283 kbps, 720p30: 1750 kbps, 720p60: 2450 kbps
84 return baseBitrate + (fps - fpsTranscodingConstants.AVERAGE) * (maxBitrateDifference / maxFpsDifference) 84 return Math.floor(baseBitrate + (fps - fpsTranscodingConstants.AVERAGE) * (maxBitrateDifference / maxFpsDifference))
85} 85}
86 86
87/** 87/**