video-transcoding-profiles.ts 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. import { logger } from '@server/helpers/logger'
  2. import { AvailableEncoders, EncoderOptionsBuilder, getTargetBitrate, VideoResolution } from '../../../shared/models/videos'
  3. import { buildStreamSuffix, resetSupportedEncoders } from '../../helpers/ffmpeg-utils'
  4. import { canDoQuickAudioTranscode, ffprobePromise, getAudioStream, getMaxAudioBitrate } from '../../helpers/ffprobe-utils'
  5. import { VIDEO_TRANSCODING_FPS } from '../../initializers/constants'
  6. /**
  7. *
  8. * Available encoders and profiles for the transcoding jobs
  9. * These functions are used by ffmpeg-utils that will get the encoders and options depending on the chosen profile
  10. *
  11. */
  12. // Resources:
  13. // * https://slhck.info/video/2017/03/01/rate-control.html
  14. // * https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate
  15. const defaultX264VODOptionsBuilder: EncoderOptionsBuilder = async ({ inputBitrate, resolution, fps }) => {
  16. const targetBitrate = buildTargetBitrate({ inputBitrate, resolution, fps })
  17. if (!targetBitrate) return { outputOptions: [ ] }
  18. return {
  19. outputOptions: [
  20. `-preset veryfast`,
  21. `-r ${fps}`,
  22. `-maxrate ${targetBitrate}`,
  23. `-bufsize ${targetBitrate * 2}`
  24. ]
  25. }
  26. }
  27. const defaultX264LiveOptionsBuilder: EncoderOptionsBuilder = async ({ resolution, fps, inputBitrate, streamNum }) => {
  28. const targetBitrate = buildTargetBitrate({ inputBitrate, resolution, fps })
  29. return {
  30. outputOptions: [
  31. `-preset veryfast`,
  32. `${buildStreamSuffix('-r:v', streamNum)} ${fps}`,
  33. `${buildStreamSuffix('-b:v', streamNum)} ${targetBitrate}`,
  34. `-maxrate ${targetBitrate}`,
  35. `-bufsize ${targetBitrate * 2}`
  36. ]
  37. }
  38. }
  39. const defaultAACOptionsBuilder: EncoderOptionsBuilder = async ({ input, streamNum }) => {
  40. const probe = await ffprobePromise(input)
  41. if (await canDoQuickAudioTranscode(input, probe)) {
  42. logger.debug('Copy audio stream %s by AAC encoder.', input)
  43. return { copy: true, outputOptions: [ ] }
  44. }
  45. const parsedAudio = await getAudioStream(input, probe)
  46. // We try to reduce the ceiling bitrate by making rough matches of bitrates
  47. // Of course this is far from perfect, but it might save some space in the end
  48. const audioCodecName = parsedAudio.audioStream['codec_name']
  49. const bitrate = getMaxAudioBitrate(audioCodecName, parsedAudio.bitrate)
  50. logger.debug('Calculating audio bitrate of %s by AAC encoder.', input, { bitrate: parsedAudio.bitrate, audioCodecName })
  51. if (bitrate !== undefined && bitrate !== -1) {
  52. return { outputOptions: [ buildStreamSuffix('-b:a', streamNum), bitrate + 'k' ] }
  53. }
  54. return { outputOptions: [ ] }
  55. }
  56. const defaultLibFDKAACVODOptionsBuilder: EncoderOptionsBuilder = ({ streamNum }) => {
  57. return { outputOptions: [ buildStreamSuffix('-q:a', streamNum), '5' ] }
  58. }
  59. // Used to get and update available encoders
  60. class VideoTranscodingProfilesManager {
  61. private static instance: VideoTranscodingProfilesManager
  62. // 1 === less priority
  63. private readonly encodersPriorities = {
  64. vod: this.buildDefaultEncodersPriorities(),
  65. live: this.buildDefaultEncodersPriorities()
  66. }
  67. private readonly 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. private availableProfiles = {
  89. vod: [] as string[],
  90. live: [] as string[]
  91. }
  92. private constructor () {
  93. this.buildAvailableProfiles()
  94. }
  95. getAvailableEncoders (): AvailableEncoders {
  96. return {
  97. available: this.availableEncoders,
  98. encodersToTry: {
  99. vod: {
  100. video: this.getEncodersByPriority('vod', 'video'),
  101. audio: this.getEncodersByPriority('vod', 'audio')
  102. },
  103. live: {
  104. video: this.getEncodersByPriority('live', 'video'),
  105. audio: this.getEncodersByPriority('live', 'audio')
  106. }
  107. }
  108. }
  109. }
  110. getAvailableProfiles (type: 'vod' | 'live') {
  111. return this.availableProfiles[type]
  112. }
  113. addProfile (options: {
  114. type: 'vod' | 'live'
  115. encoder: string
  116. profile: string
  117. builder: EncoderOptionsBuilder
  118. }) {
  119. const { type, encoder, profile, builder } = options
  120. const encoders = this.availableEncoders[type]
  121. if (!encoders[encoder]) encoders[encoder] = {}
  122. encoders[encoder][profile] = builder
  123. this.buildAvailableProfiles()
  124. }
  125. removeProfile (options: {
  126. type: 'vod' | 'live'
  127. encoder: string
  128. profile: string
  129. }) {
  130. const { type, encoder, profile } = options
  131. delete this.availableEncoders[type][encoder][profile]
  132. this.buildAvailableProfiles()
  133. }
  134. addEncoderPriority (type: 'vod' | 'live', streamType: 'audio' | 'video', encoder: string, priority: number) {
  135. this.encodersPriorities[type][streamType].push({ name: encoder, priority })
  136. resetSupportedEncoders()
  137. }
  138. removeEncoderPriority (type: 'vod' | 'live', streamType: 'audio' | 'video', encoder: string, priority: number) {
  139. this.encodersPriorities[type][streamType] = this.encodersPriorities[type][streamType]
  140. .filter(o => o.name !== encoder && o.priority !== priority)
  141. resetSupportedEncoders()
  142. }
  143. private getEncodersByPriority (type: 'vod' | 'live', streamType: 'audio' | 'video') {
  144. return this.encodersPriorities[type][streamType]
  145. .sort((e1, e2) => {
  146. if (e1.priority > e2.priority) return -1
  147. else if (e1.priority === e2.priority) return 0
  148. return 1
  149. })
  150. .map(e => e.name)
  151. }
  152. private buildAvailableProfiles () {
  153. for (const type of [ 'vod', 'live' ]) {
  154. const result = new Set()
  155. const encoders = this.availableEncoders[type]
  156. for (const encoderName of Object.keys(encoders)) {
  157. for (const profile of Object.keys(encoders[encoderName])) {
  158. result.add(profile)
  159. }
  160. }
  161. this.availableProfiles[type] = Array.from(result)
  162. }
  163. logger.debug('Available transcoding profiles built.', { availableProfiles: this.availableProfiles })
  164. }
  165. private buildDefaultEncodersPriorities () {
  166. return {
  167. video: [
  168. { name: 'libx264', priority: 100 }
  169. ],
  170. // Try the first one, if not available try the second one etc
  171. audio: [
  172. // we favor VBR, if a good AAC encoder is available
  173. { name: 'libfdk_aac', priority: 200 },
  174. { name: 'aac', priority: 100 }
  175. ]
  176. }
  177. }
  178. static get Instance () {
  179. return this.instance || (this.instance = new this())
  180. }
  181. }
  182. // ---------------------------------------------------------------------------
  183. export {
  184. VideoTranscodingProfilesManager
  185. }
  186. // ---------------------------------------------------------------------------
  187. function buildTargetBitrate (options: {
  188. inputBitrate: number
  189. resolution: VideoResolution
  190. fps: number
  191. }) {
  192. const { inputBitrate, resolution, fps } = options
  193. const targetBitrate = getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS)
  194. if (!inputBitrate) return targetBitrate
  195. return Math.min(targetBitrate, inputBitrate)
  196. }