ニジカ投稿局 https://tv.nizika.tv
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ffmpeg-vod.ts 7.5 KiB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. import { pick } from '@peertube/peertube-core-utils'
  2. import { VideoResolution } from '@peertube/peertube-models'
  3. import { MutexInterface } from 'async-mutex'
  4. import { FfmpegCommand } from 'fluent-ffmpeg'
  5. import { readFile, writeFile } from 'fs/promises'
  6. import { dirname } from 'path'
  7. import { FFmpegCommandWrapper, FFmpegCommandWrapperOptions } from './ffmpeg-command-wrapper.js'
  8. import { ffprobePromise, getVideoStreamDimensionsInfo } from './ffprobe.js'
  9. import { presetCopy, presetOnlyAudio, presetVOD } from './shared/presets.js'
  10. export type TranscodeVODOptionsType = 'hls' | 'hls-from-ts' | 'quick-transcode' | 'video' | 'merge-audio'
  11. export interface BaseTranscodeVODOptions {
  12. type: TranscodeVODOptionsType
  13. inputPath: string
  14. outputPath: string
  15. // Will be released after the ffmpeg started
  16. // To prevent a bug where the input file does not exist anymore when running ffmpeg
  17. inputFileMutexReleaser: MutexInterface.Releaser
  18. resolution: number
  19. fps: number
  20. }
  21. export interface HLSTranscodeOptions extends BaseTranscodeVODOptions {
  22. type: 'hls'
  23. copyCodecs: boolean
  24. hlsPlaylist: {
  25. videoFilename: string
  26. }
  27. }
  28. export interface HLSFromTSTranscodeOptions extends BaseTranscodeVODOptions {
  29. type: 'hls-from-ts'
  30. isAAC: boolean
  31. hlsPlaylist: {
  32. videoFilename: string
  33. }
  34. }
  35. export interface QuickTranscodeOptions extends BaseTranscodeVODOptions {
  36. type: 'quick-transcode'
  37. }
  38. export interface VideoTranscodeOptions extends BaseTranscodeVODOptions {
  39. type: 'video'
  40. }
  41. export interface MergeAudioTranscodeOptions extends BaseTranscodeVODOptions {
  42. type: 'merge-audio'
  43. audioPath: string
  44. }
  45. export type TranscodeVODOptions =
  46. HLSTranscodeOptions
  47. | HLSFromTSTranscodeOptions
  48. | VideoTranscodeOptions
  49. | MergeAudioTranscodeOptions
  50. | QuickTranscodeOptions
  51. // ---------------------------------------------------------------------------
  52. export class FFmpegVOD {
  53. private readonly commandWrapper: FFmpegCommandWrapper
  54. private ended = false
  55. constructor (options: FFmpegCommandWrapperOptions) {
  56. this.commandWrapper = new FFmpegCommandWrapper(options)
  57. }
  58. async transcode (options: TranscodeVODOptions) {
  59. const builders: {
  60. [ type in TranscodeVODOptionsType ]: (options: TranscodeVODOptions) => Promise<void> | void
  61. } = {
  62. 'quick-transcode': this.buildQuickTranscodeCommand.bind(this),
  63. 'hls': this.buildHLSVODCommand.bind(this),
  64. 'hls-from-ts': this.buildHLSVODFromTSCommand.bind(this),
  65. 'merge-audio': this.buildAudioMergeCommand.bind(this),
  66. 'video': this.buildWebVideoCommand.bind(this)
  67. }
  68. this.commandWrapper.debugLog('Will run transcode.', { options })
  69. this.commandWrapper.buildCommand(options.inputPath, options.inputFileMutexReleaser)
  70. .output(options.outputPath)
  71. await builders[options.type](options)
  72. await this.commandWrapper.runCommand()
  73. await this.fixHLSPlaylistIfNeeded(options)
  74. this.ended = true
  75. }
  76. isEnded () {
  77. return this.ended
  78. }
  79. private async buildWebVideoCommand (options: TranscodeVODOptions & { canCopyAudio?: boolean, canCopyVideo?: boolean }) {
  80. const { resolution, fps, inputPath, canCopyAudio = true, canCopyVideo = true } = options
  81. if (resolution === VideoResolution.H_NOVIDEO) {
  82. presetOnlyAudio(this.commandWrapper)
  83. return
  84. }
  85. let scaleFilterValue: string
  86. if (resolution !== undefined) {
  87. const probe = await ffprobePromise(inputPath)
  88. const videoStreamInfo = await getVideoStreamDimensionsInfo(inputPath, probe)
  89. scaleFilterValue = videoStreamInfo?.isPortraitMode === true
  90. ? `w=${resolution}:h=-2`
  91. : `w=-2:h=${resolution}`
  92. }
  93. await presetVOD({
  94. commandWrapper: this.commandWrapper,
  95. resolution,
  96. input: inputPath,
  97. canCopyAudio,
  98. canCopyVideo,
  99. fps,
  100. scaleFilterValue
  101. })
  102. }
  103. private buildQuickTranscodeCommand (_options: TranscodeVODOptions) {
  104. const command = this.commandWrapper.getCommand()
  105. presetCopy(this.commandWrapper)
  106. command.outputOption('-map_metadata -1') // strip all metadata
  107. .outputOption('-movflags faststart')
  108. }
  109. // ---------------------------------------------------------------------------
  110. // Audio transcoding
  111. // ---------------------------------------------------------------------------
  112. private async buildAudioMergeCommand (options: MergeAudioTranscodeOptions) {
  113. const command = this.commandWrapper.getCommand()
  114. command.loop(undefined)
  115. await presetVOD({
  116. ...pick(options, [ 'resolution' ]),
  117. commandWrapper: this.commandWrapper,
  118. input: options.audioPath,
  119. canCopyAudio: true,
  120. canCopyVideo: true,
  121. fps: options.fps,
  122. scaleFilterValue: this.getMergeAudioScaleFilterValue()
  123. })
  124. command.outputOption('-preset:v veryfast')
  125. command.input(options.audioPath)
  126. .outputOption('-tune stillimage')
  127. .outputOption('-shortest')
  128. }
  129. // Avoid "height not divisible by 2" error
  130. private getMergeAudioScaleFilterValue () {
  131. return 'trunc(iw/2)*2:trunc(ih/2)*2'
  132. }
  133. // ---------------------------------------------------------------------------
  134. // HLS transcoding
  135. // ---------------------------------------------------------------------------
  136. private async buildHLSVODCommand (options: HLSTranscodeOptions) {
  137. const command = this.commandWrapper.getCommand()
  138. const videoPath = this.getHLSVideoPath(options)
  139. if (options.copyCodecs) {
  140. presetCopy(this.commandWrapper)
  141. } else if (options.resolution === VideoResolution.H_NOVIDEO) {
  142. presetOnlyAudio(this.commandWrapper)
  143. } else {
  144. // If we cannot copy codecs, we do not copy them at all to prevent issues like audio desync
  145. // See for example https://github.com/Chocobozzz/PeerTube/issues/6438
  146. await this.buildWebVideoCommand({ ...options, canCopyAudio: false, canCopyVideo: false })
  147. }
  148. this.addCommonHLSVODCommandOptions(command, videoPath)
  149. }
  150. private buildHLSVODFromTSCommand (options: HLSFromTSTranscodeOptions) {
  151. const command = this.commandWrapper.getCommand()
  152. const videoPath = this.getHLSVideoPath(options)
  153. command.outputOption('-c copy')
  154. if (options.isAAC) {
  155. // Required for example when copying an AAC stream from an MPEG-TS
  156. // Since it's a bitstream filter, we don't need to reencode the audio
  157. command.outputOption('-bsf:a aac_adtstoasc')
  158. }
  159. this.addCommonHLSVODCommandOptions(command, videoPath)
  160. }
  161. private addCommonHLSVODCommandOptions (command: FfmpegCommand, outputPath: string) {
  162. return command.outputOption('-hls_time 4')
  163. .outputOption('-hls_list_size 0')
  164. .outputOption('-hls_playlist_type vod')
  165. .outputOption('-hls_segment_filename ' + outputPath)
  166. .outputOption('-hls_segment_type fmp4')
  167. .outputOption('-f hls')
  168. .outputOption('-hls_flags single_file')
  169. }
  170. private async fixHLSPlaylistIfNeeded (options: TranscodeVODOptions) {
  171. if (options.type !== 'hls' && options.type !== 'hls-from-ts') return
  172. const fileContent = await readFile(options.outputPath)
  173. const videoFileName = options.hlsPlaylist.videoFilename
  174. const videoFilePath = this.getHLSVideoPath(options)
  175. // Fix wrong mapping with some ffmpeg versions
  176. const newContent = fileContent.toString()
  177. .replace(`#EXT-X-MAP:URI="${videoFilePath}",`, `#EXT-X-MAP:URI="${videoFileName}",`)
  178. await writeFile(options.outputPath, newContent)
  179. }
  180. private getHLSVideoPath (options: HLSTranscodeOptions | HLSFromTSTranscodeOptions) {
  181. return `${dirname(options.outputPath)}/${options.hlsPlaylist.videoFilename}`
  182. }
  183. }