diff options
author | Chocobozzz <me@florianbigard.com> | 2020-11-24 14:08:23 +0100 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2020-11-25 10:07:51 +0100 |
commit | 5a547f69d5dc5867e253f7721513479c754b4f15 (patch) | |
tree | 5ccad0e07d04e24d7a4c0b624a46d3b5a93ebce5 | |
parent | 9252a33d115bba85adcfbc18ab3725924642871c (diff) | |
download | PeerTube-5a547f69d5dc5867e253f7721513479c754b4f15.tar.gz PeerTube-5a547f69d5dc5867e253f7721513479c754b4f15.tar.zst PeerTube-5a547f69d5dc5867e253f7721513479c754b4f15.zip |
Support encoding profiles
-rwxr-xr-x | scripts/dev/cli.sh | 2 | ||||
-rw-r--r-- | server/helpers/ffmpeg-utils.ts | 223 | ||||
-rw-r--r-- | server/helpers/ffprobe-utils.ts | 19 | ||||
-rw-r--r-- | server/lib/live-manager.ts | 12 | ||||
-rw-r--r-- | server/lib/video-transcoding-profiles.ts | 95 | ||||
-rw-r--r-- | server/lib/video-transcoding.ts | 91 | ||||
-rw-r--r-- | server/tools/test.ts | 105 | ||||
-rw-r--r-- | shared/extra-utils/server/servers.ts | 12 | ||||
-rw-r--r-- | shared/models/videos/video-resolution.enum.ts | 2 |
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/ | |||
12 | mkdir -p "./dist/server/tools" | 12 | mkdir -p "./dist/server/tools" |
13 | cp -r "./server/tools/node_modules" "./dist/server/tools" | 13 | cp -r "./server/tools/node_modules" "./dist/server/tools" |
14 | 14 | ||
15 | npm run tsc -- --watch --project ./server/tools/tsconfig.json | 15 | npm 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 @@ | |||
1 | import * as ffmpeg from 'fluent-ffmpeg' | 1 | import * as ffmpeg from 'fluent-ffmpeg' |
2 | import { readFile, remove, writeFile } from 'fs-extra' | 2 | import { readFile, remove, writeFile } from 'fs-extra' |
3 | import { dirname, join } from 'path' | 3 | import { dirname, join } from 'path' |
4 | import { getTargetBitrate, VideoResolution } from '../../shared/models/videos' | 4 | import { FFMPEG_NICE, VIDEO_LIVE, VIDEO_TRANSCODING_ENCODERS, VIDEO_TRANSCODING_FPS } from '@server/initializers/constants' |
5 | import { VideoResolution } from '../../shared/models/videos' | ||
5 | import { checkFFmpegEncoders } from '../initializers/checker-before-init' | 6 | import { checkFFmpegEncoders } from '../initializers/checker-before-init' |
6 | import { CONFIG } from '../initializers/config' | 7 | import { CONFIG } from '../initializers/config' |
7 | import { FFMPEG_NICE, VIDEO_LIVE, VIDEO_TRANSCODING_ENCODERS, VIDEO_TRANSCODING_FPS } from '../initializers/constants' | ||
8 | import { getAudioStream, getClosestFramerateStandard, getVideoFileFPS } from './ffprobe-utils' | 8 | import { getAudioStream, getClosestFramerateStandard, getVideoFileFPS } from './ffprobe-utils' |
9 | import { processImage } from './image-utils' | 9 | import { processImage } from './image-utils' |
10 | import { logger } from './logger' | 10 | import { 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 | ||
26 | export interface EncoderOptions { | 27 | export 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 | ||
38 | export type AvailableEncoders = { | 40 | export 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 | ||
200 | function getLiveTranscodingCommand (rtmpUrl: string, outPath: string, resolutions: number[], fps: number, deleteSegments: boolean) { | 202 | async 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 | ||
323 | function buildStreamSuffix (base: string, streamNum?: number) { | ||
324 | if (streamNum !== undefined) { | ||
325 | return `${base}:${streamNum}` | ||
326 | } | ||
327 | |||
328 | return base | ||
329 | } | ||
330 | |||
285 | // --------------------------------------------------------------------------- | 331 | // --------------------------------------------------------------------------- |
286 | 332 | ||
287 | export { | 333 | export { |
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 | ||
305 | function addEncoderDefaultParams (command: ffmpeg.FfmpegCommand, encoder: 'libx264' | string, fps?: number) { | 352 | function 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 | ||
320 | function addDefaultLiveHLSParams (command: ffmpeg.FfmpegCommand, outPath: string, deleteSegments: boolean) { | 383 | function 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 | ||
512 | async 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 | |||
448 | async function presetVideo ( | 555 | async 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' | |||
198 | async function canDoQuickTranscode (path: string): Promise<boolean> { | 198 | async 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 | |||
205 | async 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 | |||
221 | async 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' | |||
22 | import { PeerTubeSocket } from './peertube-socket' | 22 | import { PeerTubeSocket } from './peertube-socket' |
23 | import { isAbleToUploadVideo } from './user' | 23 | import { isAbleToUploadVideo } from './user' |
24 | import { getHLSDirectory } from './video-paths' | 24 | import { getHLSDirectory } from './video-paths' |
25 | import { availableEncoders } from './video-transcoding-profiles' | ||
25 | 26 | ||
26 | import memoizee = require('memoizee') | 27 | import memoizee = require('memoizee') |
27 | const NodeRtmpServer = require('node-media-server/node_rtmp_server') | 28 | const 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 @@ | |||
1 | import { getTargetBitrate } from '../../shared/models/videos' | ||
2 | import { AvailableEncoders, buildStreamSuffix, EncoderOptionsBuilder } from '../helpers/ffmpeg-utils' | ||
3 | import { ffprobePromise, getAudioStream, getMaxAudioBitrate, getVideoFileBitrate, getVideoStreamFromFile } from '../helpers/ffprobe-utils' | ||
4 | import { 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 | |||
14 | const 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 | |||
35 | const 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 | |||
46 | const 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 | |||
63 | const defaultLibFDKAACVODOptionsBuilder: EncoderOptionsBuilder = ({ streamNum }) => { | ||
64 | return { outputOptions: [ buildStreamSuffix('-q:a', streamNum), '5' ] } | ||
65 | } | ||
66 | |||
67 | const 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 | |||
91 | export { | ||
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' | |||
2 | import { basename, extname as extnameUtil, join } from 'path' | 2 | import { basename, extname as extnameUtil, join } from 'path' |
3 | import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' | 3 | import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' |
4 | import { MStreamingPlaylistFilesVideo, MVideoFile, MVideoWithAllFiles, MVideoWithFile } from '@server/types/models' | 4 | import { MStreamingPlaylistFilesVideo, MVideoFile, MVideoWithAllFiles, MVideoWithFile } from '@server/types/models' |
5 | import { getTargetBitrate, VideoResolution } from '../../shared/models/videos' | 5 | import { VideoResolution } from '../../shared/models/videos' |
6 | import { VideoStreamingPlaylistType } from '../../shared/models/videos/video-streaming-playlist.type' | 6 | import { VideoStreamingPlaylistType } from '../../shared/models/videos/video-streaming-playlist.type' |
7 | import { AvailableEncoders, EncoderOptionsBuilder, transcode, TranscodeOptions, TranscodeOptionsType } from '../helpers/ffmpeg-utils' | 7 | import { transcode, TranscodeOptions, TranscodeOptionsType } from '../helpers/ffmpeg-utils' |
8 | import { | 8 | import { 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' | ||
17 | import { logger } from '../helpers/logger' | 9 | import { logger } from '../helpers/logger' |
18 | import { CONFIG } from '../initializers/config' | 10 | import { CONFIG } from '../initializers/config' |
19 | import { | 11 | import { 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' | ||
25 | import { VideoFileModel } from '../models/video/video-file' | 12 | import { VideoFileModel } from '../models/video/video-file' |
26 | import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' | 13 | import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' |
27 | import { updateMasterHLSPlaylist, updateSha256VODSegments } from './hls' | 14 | import { updateMasterHLSPlaylist, updateSha256VODSegments } from './hls' |
28 | import { generateVideoStreamingPlaylistName, getVideoFilename, getVideoFilePath } from './video-paths' | 15 | import { generateVideoStreamingPlaylistName, getVideoFilename, getVideoFilePath } from './video-paths' |
16 | import { 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 | |||
259 | const 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 | |||
278 | const 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 | |||
295 | const defaultLibFDKAACOptionsBuilder: EncoderOptionsBuilder = () => { | ||
296 | return { outputOptions: [ '-aq', '5' ] } | ||
297 | } | ||
298 | |||
299 | const 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 | ||
326 | export { | 245 | export { |
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 @@ | |||
1 | import { registerTSPaths } from '../helpers/register-ts-paths' | ||
2 | registerTSPaths() | ||
3 | |||
4 | import { LiveVideo, LiveVideoCreate, VideoPrivacy } from '@shared/models' | ||
5 | import * as program from 'commander' | ||
6 | import { | ||
7 | createLive, | ||
8 | flushAndRunServer, | ||
9 | getLive, | ||
10 | killallServers, | ||
11 | sendRTMPStream, | ||
12 | ServerInfo, | ||
13 | setAccessTokensToServers, | ||
14 | setDefaultVideoChannel, | ||
15 | updateCustomSubConfig | ||
16 | } from '../../shared/extra-utils' | ||
17 | |||
18 | type CommandType = 'live-mux' | 'live-transcoding' | ||
19 | |||
20 | registerTSPaths() | ||
21 | |||
22 | const command = program | ||
23 | .name('test') | ||
24 | .option('-t, --type <type>', 'live-muxing|live-transcoding') | ||
25 | .parse(process.argv) | ||
26 | |||
27 | run() | ||
28 | .catch(err => { | ||
29 | console.error(err) | ||
30 | process.exit(-1) | ||
31 | }) | ||
32 | |||
33 | async 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 | |||
89 | async 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 | ||
107 | async function flushAndRunServer (serverNumber: number, configOverride?: Object, args = []) { | 107 | async 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 | ||
139 | async function runServer (server: ServerInfo, configOverrideArg?: any, args = []) { | 139 | async 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 | /** |