ffprobe-utils.ts 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. import { FfprobeData } from 'fluent-ffmpeg'
  2. import { getMaxBitrate } from '@shared/core-utils'
  3. import {
  4. buildFileMetadata,
  5. ffprobePromise,
  6. getAudioStream,
  7. getMaxAudioBitrate,
  8. getVideoStream,
  9. getVideoStreamBitrate,
  10. getVideoStreamDimensionsInfo,
  11. getVideoStreamDuration,
  12. getVideoStreamFPS,
  13. hasAudioStream
  14. } from '@shared/extra-utils/ffprobe'
  15. import { VideoResolution, VideoTranscodingFPS } from '@shared/models'
  16. import { CONFIG } from '../../initializers/config'
  17. import { VIDEO_TRANSCODING_FPS } from '../../initializers/constants'
  18. import { logger } from '../logger'
  19. /**
  20. *
  21. * Helpers to run ffprobe and extract data from the JSON output
  22. *
  23. */
  24. // ---------------------------------------------------------------------------
  25. // Codecs
  26. // ---------------------------------------------------------------------------
  27. async function getVideoStreamCodec (path: string) {
  28. const videoStream = await getVideoStream(path)
  29. if (!videoStream) return ''
  30. const videoCodec = videoStream.codec_tag_string
  31. if (videoCodec === 'vp09') return 'vp09.00.50.08'
  32. if (videoCodec === 'hev1') return 'hev1.1.6.L93.B0'
  33. const baseProfileMatrix = {
  34. avc1: {
  35. High: '6400',
  36. Main: '4D40',
  37. Baseline: '42E0'
  38. },
  39. av01: {
  40. High: '1',
  41. Main: '0',
  42. Professional: '2'
  43. }
  44. }
  45. let baseProfile = baseProfileMatrix[videoCodec][videoStream.profile]
  46. if (!baseProfile) {
  47. logger.warn('Cannot get video profile codec of %s.', path, { videoStream })
  48. baseProfile = baseProfileMatrix[videoCodec]['High'] // Fallback
  49. }
  50. if (videoCodec === 'av01') {
  51. let level = videoStream.level.toString()
  52. if (level.length === 1) level = `0${level}`
  53. // Guess the tier indicator and bit depth
  54. return `${videoCodec}.${baseProfile}.${level}M.08`
  55. }
  56. let level = videoStream.level.toString(16)
  57. if (level.length === 1) level = `0${level}`
  58. // Default, h264 codec
  59. return `${videoCodec}.${baseProfile}${level}`
  60. }
  61. async function getAudioStreamCodec (path: string, existingProbe?: FfprobeData) {
  62. const { audioStream } = await getAudioStream(path, existingProbe)
  63. if (!audioStream) return ''
  64. const audioCodecName = audioStream.codec_name
  65. if (audioCodecName === 'opus') return 'opus'
  66. if (audioCodecName === 'vorbis') return 'vorbis'
  67. if (audioCodecName === 'aac') return 'mp4a.40.2'
  68. if (audioCodecName === 'mp3') return 'mp4a.40.34'
  69. logger.warn('Cannot get audio codec of %s.', path, { audioStream })
  70. return 'mp4a.40.2' // Fallback
  71. }
  72. // ---------------------------------------------------------------------------
  73. // Resolutions
  74. // ---------------------------------------------------------------------------
  75. function computeResolutionsToTranscode (options: {
  76. input: number
  77. type: 'vod' | 'live'
  78. includeInput: boolean
  79. strictLower: boolean
  80. hasAudio: boolean
  81. }) {
  82. const { input, type, includeInput, strictLower, hasAudio } = options
  83. const configResolutions = type === 'vod'
  84. ? CONFIG.TRANSCODING.RESOLUTIONS
  85. : CONFIG.LIVE.TRANSCODING.RESOLUTIONS
  86. const resolutionsEnabled = new Set<number>()
  87. // Put in the order we want to proceed jobs
  88. const availableResolutions: VideoResolution[] = [
  89. VideoResolution.H_NOVIDEO,
  90. VideoResolution.H_480P,
  91. VideoResolution.H_360P,
  92. VideoResolution.H_720P,
  93. VideoResolution.H_240P,
  94. VideoResolution.H_144P,
  95. VideoResolution.H_1080P,
  96. VideoResolution.H_1440P,
  97. VideoResolution.H_4K
  98. ]
  99. for (const resolution of availableResolutions) {
  100. // Resolution not enabled
  101. if (configResolutions[resolution + 'p'] !== true) continue
  102. // Too big resolution for input file
  103. if (input < resolution) continue
  104. // We only want lower resolutions than input file
  105. if (strictLower && input === resolution) continue
  106. // Audio resolutio but no audio in the video
  107. if (resolution === VideoResolution.H_NOVIDEO && !hasAudio) continue
  108. resolutionsEnabled.add(resolution)
  109. }
  110. if (includeInput) {
  111. resolutionsEnabled.add(input)
  112. }
  113. return Array.from(resolutionsEnabled)
  114. }
  115. // ---------------------------------------------------------------------------
  116. // Can quick transcode
  117. // ---------------------------------------------------------------------------
  118. async function canDoQuickTranscode (path: string): Promise<boolean> {
  119. if (CONFIG.TRANSCODING.PROFILE !== 'default') return false
  120. const probe = await ffprobePromise(path)
  121. return await canDoQuickVideoTranscode(path, probe) &&
  122. await canDoQuickAudioTranscode(path, probe)
  123. }
  124. async function canDoQuickAudioTranscode (path: string, probe?: FfprobeData): Promise<boolean> {
  125. const parsedAudio = await getAudioStream(path, probe)
  126. if (!parsedAudio.audioStream) return true
  127. if (parsedAudio.audioStream['codec_name'] !== 'aac') return false
  128. const audioBitrate = parsedAudio.bitrate
  129. if (!audioBitrate) return false
  130. const maxAudioBitrate = getMaxAudioBitrate('aac', audioBitrate)
  131. if (maxAudioBitrate !== -1 && audioBitrate > maxAudioBitrate) return false
  132. const channelLayout = parsedAudio.audioStream['channel_layout']
  133. // Causes playback issues with Chrome
  134. if (!channelLayout || channelLayout === 'unknown' || channelLayout === 'quad') return false
  135. return true
  136. }
  137. async function canDoQuickVideoTranscode (path: string, probe?: FfprobeData): Promise<boolean> {
  138. const videoStream = await getVideoStream(path, probe)
  139. const fps = await getVideoStreamFPS(path, probe)
  140. const bitRate = await getVideoStreamBitrate(path, probe)
  141. const resolutionData = await getVideoStreamDimensionsInfo(path, probe)
  142. // If ffprobe did not manage to guess the bitrate
  143. if (!bitRate) return false
  144. // check video params
  145. if (!videoStream) return false
  146. if (videoStream['codec_name'] !== 'h264') return false
  147. if (videoStream['pix_fmt'] !== 'yuv420p') return false
  148. if (fps < VIDEO_TRANSCODING_FPS.MIN || fps > VIDEO_TRANSCODING_FPS.MAX) return false
  149. if (bitRate > getMaxBitrate({ ...resolutionData, fps })) return false
  150. return true
  151. }
  152. // ---------------------------------------------------------------------------
  153. // Framerate
  154. // ---------------------------------------------------------------------------
  155. function getClosestFramerateStandard <K extends keyof Pick<VideoTranscodingFPS, 'HD_STANDARD' | 'STANDARD'>> (fps: number, type: K) {
  156. return VIDEO_TRANSCODING_FPS[type].slice(0)
  157. .sort((a, b) => fps % a - fps % b)[0]
  158. }
  159. function computeFPS (fpsArg: number, resolution: VideoResolution) {
  160. let fps = fpsArg
  161. if (
  162. // On small/medium resolutions, limit FPS
  163. resolution !== undefined &&
  164. resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN &&
  165. fps > VIDEO_TRANSCODING_FPS.AVERAGE
  166. ) {
  167. // Get closest standard framerate by modulo: downsampling has to be done to a divisor of the nominal fps value
  168. fps = getClosestFramerateStandard(fps, 'STANDARD')
  169. }
  170. // Hard FPS limits
  171. if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = getClosestFramerateStandard(fps, 'HD_STANDARD')
  172. if (fps < VIDEO_TRANSCODING_FPS.MIN) {
  173. throw new Error(`Cannot compute FPS because ${fps} is lower than our minimum value ${VIDEO_TRANSCODING_FPS.MIN}`)
  174. }
  175. return fps
  176. }
  177. // ---------------------------------------------------------------------------
  178. export {
  179. // Re export ffprobe utils
  180. getVideoStreamDimensionsInfo,
  181. buildFileMetadata,
  182. getMaxAudioBitrate,
  183. getVideoStream,
  184. getVideoStreamDuration,
  185. getAudioStream,
  186. hasAudioStream,
  187. getVideoStreamFPS,
  188. ffprobePromise,
  189. getVideoStreamBitrate,
  190. getVideoStreamCodec,
  191. getAudioStreamCodec,
  192. computeFPS,
  193. getClosestFramerateStandard,
  194. computeResolutionsToTranscode,
  195. canDoQuickTranscode,
  196. canDoQuickVideoTranscode,
  197. canDoQuickAudioTranscode
  198. }