123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245 |
- import { logger } from '@server/helpers/logger'
- import { AvailableEncoders, EncoderOptionsBuilder, getTargetBitrate, VideoResolution } from '../../../shared/models/videos'
- import { buildStreamSuffix, resetSupportedEncoders } from '../../helpers/ffmpeg-utils'
- import { canDoQuickAudioTranscode, ffprobePromise, getAudioStream, getMaxAudioBitrate } from '../../helpers/ffprobe-utils'
- import { VIDEO_TRANSCODING_FPS } from '../../initializers/constants'
- /**
- *
- * Available encoders and profiles for the transcoding jobs
- * These functions are used by ffmpeg-utils that will get the encoders and options depending on the chosen profile
- *
- */
- // Resources:
- // * https://slhck.info/video/2017/03/01/rate-control.html
- // * https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate
- const defaultX264VODOptionsBuilder: EncoderOptionsBuilder = async ({ inputBitrate, resolution, fps }) => {
- const targetBitrate = buildTargetBitrate({ inputBitrate, resolution, fps })
- if (!targetBitrate) return { outputOptions: [ ] }
- return {
- outputOptions: [
- `-preset veryfast`,
- `-r ${fps}`,
- `-maxrate ${targetBitrate}`,
- `-bufsize ${targetBitrate * 2}`
- ]
- }
- }
- const defaultX264LiveOptionsBuilder: EncoderOptionsBuilder = async ({ resolution, fps, inputBitrate, streamNum }) => {
- const targetBitrate = buildTargetBitrate({ inputBitrate, resolution, fps })
- return {
- outputOptions: [
- `-preset veryfast`,
- `${buildStreamSuffix('-r:v', streamNum)} ${fps}`,
- `${buildStreamSuffix('-b:v', streamNum)} ${targetBitrate}`,
- `-maxrate ${targetBitrate}`,
- `-bufsize ${targetBitrate * 2}`
- ]
- }
- }
- const defaultAACOptionsBuilder: EncoderOptionsBuilder = async ({ input, streamNum }) => {
- const probe = await ffprobePromise(input)
- if (await canDoQuickAudioTranscode(input, probe)) {
- logger.debug('Copy audio stream %s by AAC encoder.', input)
- return { copy: true, outputOptions: [ ] }
- }
- const parsedAudio = await getAudioStream(input, probe)
- // We try to reduce the ceiling bitrate by making rough matches of bitrates
- // Of course this is far from perfect, but it might save some space in the end
- const audioCodecName = parsedAudio.audioStream['codec_name']
- const bitrate = getMaxAudioBitrate(audioCodecName, parsedAudio.bitrate)
- logger.debug('Calculating audio bitrate of %s by AAC encoder.', input, { bitrate: parsedAudio.bitrate, audioCodecName })
- if (bitrate !== undefined && bitrate !== -1) {
- return { outputOptions: [ buildStreamSuffix('-b:a', streamNum), bitrate + 'k' ] }
- }
- return { outputOptions: [ ] }
- }
- const defaultLibFDKAACVODOptionsBuilder: EncoderOptionsBuilder = ({ streamNum }) => {
- return { outputOptions: [ buildStreamSuffix('-q:a', streamNum), '5' ] }
- }
- // Used to get and update available encoders
- class VideoTranscodingProfilesManager {
- private static instance: VideoTranscodingProfilesManager
- // 1 === less priority
- private readonly encodersPriorities = {
- vod: this.buildDefaultEncodersPriorities(),
- live: this.buildDefaultEncodersPriorities()
- }
- private readonly availableEncoders = {
- vod: {
- libx264: {
- default: defaultX264VODOptionsBuilder
- },
- aac: {
- default: defaultAACOptionsBuilder
- },
- libfdk_aac: {
- default: defaultLibFDKAACVODOptionsBuilder
- }
- },
- live: {
- libx264: {
- default: defaultX264LiveOptionsBuilder
- },
- aac: {
- default: defaultAACOptionsBuilder
- }
- }
- }
- private availableProfiles = {
- vod: [] as string[],
- live: [] as string[]
- }
- private constructor () {
- this.buildAvailableProfiles()
- }
- getAvailableEncoders (): AvailableEncoders {
- return {
- available: this.availableEncoders,
- encodersToTry: {
- vod: {
- video: this.getEncodersByPriority('vod', 'video'),
- audio: this.getEncodersByPriority('vod', 'audio')
- },
- live: {
- video: this.getEncodersByPriority('live', 'video'),
- audio: this.getEncodersByPriority('live', 'audio')
- }
- }
- }
- }
- getAvailableProfiles (type: 'vod' | 'live') {
- return this.availableProfiles[type]
- }
- addProfile (options: {
- type: 'vod' | 'live'
- encoder: string
- profile: string
- builder: EncoderOptionsBuilder
- }) {
- const { type, encoder, profile, builder } = options
- const encoders = this.availableEncoders[type]
- if (!encoders[encoder]) encoders[encoder] = {}
- encoders[encoder][profile] = builder
- this.buildAvailableProfiles()
- }
- removeProfile (options: {
- type: 'vod' | 'live'
- encoder: string
- profile: string
- }) {
- const { type, encoder, profile } = options
- delete this.availableEncoders[type][encoder][profile]
- this.buildAvailableProfiles()
- }
- addEncoderPriority (type: 'vod' | 'live', streamType: 'audio' | 'video', encoder: string, priority: number) {
- this.encodersPriorities[type][streamType].push({ name: encoder, priority })
- resetSupportedEncoders()
- }
- removeEncoderPriority (type: 'vod' | 'live', streamType: 'audio' | 'video', encoder: string, priority: number) {
- this.encodersPriorities[type][streamType] = this.encodersPriorities[type][streamType]
- .filter(o => o.name !== encoder && o.priority !== priority)
- resetSupportedEncoders()
- }
- private getEncodersByPriority (type: 'vod' | 'live', streamType: 'audio' | 'video') {
- return this.encodersPriorities[type][streamType]
- .sort((e1, e2) => {
- if (e1.priority > e2.priority) return -1
- else if (e1.priority === e2.priority) return 0
- return 1
- })
- .map(e => e.name)
- }
- private buildAvailableProfiles () {
- for (const type of [ 'vod', 'live' ]) {
- const result = new Set()
- const encoders = this.availableEncoders[type]
- for (const encoderName of Object.keys(encoders)) {
- for (const profile of Object.keys(encoders[encoderName])) {
- result.add(profile)
- }
- }
- this.availableProfiles[type] = Array.from(result)
- }
- logger.debug('Available transcoding profiles built.', { availableProfiles: this.availableProfiles })
- }
- private buildDefaultEncodersPriorities () {
- return {
- video: [
- { name: 'libx264', priority: 100 }
- ],
- // Try the first one, if not available try the second one etc
- audio: [
- // we favor VBR, if a good AAC encoder is available
- { name: 'libfdk_aac', priority: 200 },
- { name: 'aac', priority: 100 }
- ]
- }
- }
- static get Instance () {
- return this.instance || (this.instance = new this())
- }
- }
- // ---------------------------------------------------------------------------
- export {
- VideoTranscodingProfilesManager
- }
- // ---------------------------------------------------------------------------
- function buildTargetBitrate (options: {
- inputBitrate: number
- resolution: VideoResolution
- fps: number
- }) {
- const { inputBitrate, resolution, fps } = options
- const targetBitrate = getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS)
- if (!inputBitrate) return targetBitrate
- return Math.min(targetBitrate, inputBitrate)
- }
|