mirror of https://github.com/Chocobozzz/PeerTube
				
				
				
			
		
			
				
	
	
		
			257 lines
		
	
	
		
			7.5 KiB
		
	
	
	
		
			TypeScript
		
	
	
			
		
		
	
	
			257 lines
		
	
	
		
			7.5 KiB
		
	
	
	
		
			TypeScript
		
	
	
import { Job } from 'bullmq'
 | 
						|
import { FfmpegCommand } from 'fluent-ffmpeg'
 | 
						|
import { readFile, writeFile } from 'fs-extra'
 | 
						|
import { dirname } from 'path'
 | 
						|
import { pick } from '@shared/core-utils'
 | 
						|
import { AvailableEncoders, VideoResolution } from '@shared/models'
 | 
						|
import { logger, loggerTagsFactory } from '../logger'
 | 
						|
import { getFFmpeg, runCommand } from './ffmpeg-commons'
 | 
						|
import { presetCopy, presetOnlyAudio, presetVOD } from './ffmpeg-presets'
 | 
						|
import { computeFPS, ffprobePromise, getVideoStreamDimensionsInfo, getVideoStreamFPS } from './ffprobe-utils'
 | 
						|
import { VIDEO_TRANSCODING_FPS } from '@server/initializers/constants'
 | 
						|
 | 
						|
const lTags = loggerTagsFactory('ffmpeg')
 | 
						|
 | 
						|
// ---------------------------------------------------------------------------
 | 
						|
 | 
						|
type TranscodeVODOptionsType = 'hls' | 'hls-from-ts' | 'quick-transcode' | 'video' | 'merge-audio' | 'only-audio'
 | 
						|
 | 
						|
interface BaseTranscodeVODOptions {
 | 
						|
  type: TranscodeVODOptionsType
 | 
						|
 | 
						|
  inputPath: string
 | 
						|
  outputPath: string
 | 
						|
 | 
						|
  availableEncoders: AvailableEncoders
 | 
						|
  profile: string
 | 
						|
 | 
						|
  resolution: number
 | 
						|
 | 
						|
  job?: Job
 | 
						|
}
 | 
						|
 | 
						|
interface HLSTranscodeOptions extends BaseTranscodeVODOptions {
 | 
						|
  type: 'hls'
 | 
						|
  copyCodecs: boolean
 | 
						|
  hlsPlaylist: {
 | 
						|
    videoFilename: string
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
interface HLSFromTSTranscodeOptions extends BaseTranscodeVODOptions {
 | 
						|
  type: 'hls-from-ts'
 | 
						|
 | 
						|
  isAAC: boolean
 | 
						|
 | 
						|
  hlsPlaylist: {
 | 
						|
    videoFilename: string
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
interface QuickTranscodeOptions extends BaseTranscodeVODOptions {
 | 
						|
  type: 'quick-transcode'
 | 
						|
}
 | 
						|
 | 
						|
interface VideoTranscodeOptions extends BaseTranscodeVODOptions {
 | 
						|
  type: 'video'
 | 
						|
}
 | 
						|
 | 
						|
interface MergeAudioTranscodeOptions extends BaseTranscodeVODOptions {
 | 
						|
  type: 'merge-audio'
 | 
						|
  audioPath: string
 | 
						|
}
 | 
						|
 | 
						|
interface OnlyAudioTranscodeOptions extends BaseTranscodeVODOptions {
 | 
						|
  type: 'only-audio'
 | 
						|
}
 | 
						|
 | 
						|
type TranscodeVODOptions =
 | 
						|
  HLSTranscodeOptions
 | 
						|
  | HLSFromTSTranscodeOptions
 | 
						|
  | VideoTranscodeOptions
 | 
						|
  | MergeAudioTranscodeOptions
 | 
						|
  | OnlyAudioTranscodeOptions
 | 
						|
  | QuickTranscodeOptions
 | 
						|
 | 
						|
// ---------------------------------------------------------------------------
 | 
						|
 | 
						|
const builders: {
 | 
						|
  [ type in TranscodeVODOptionsType ]: (c: FfmpegCommand, o?: TranscodeVODOptions) => Promise<FfmpegCommand> | FfmpegCommand
 | 
						|
} = {
 | 
						|
  'quick-transcode': buildQuickTranscodeCommand,
 | 
						|
  'hls': buildHLSVODCommand,
 | 
						|
  'hls-from-ts': buildHLSVODFromTSCommand,
 | 
						|
  'merge-audio': buildAudioMergeCommand,
 | 
						|
  'only-audio': buildOnlyAudioCommand,
 | 
						|
  'video': buildVODCommand
 | 
						|
}
 | 
						|
 | 
						|
async function transcodeVOD (options: TranscodeVODOptions) {
 | 
						|
  logger.debug('Will run transcode.', { options, ...lTags() })
 | 
						|
 | 
						|
  let command = getFFmpeg(options.inputPath, 'vod')
 | 
						|
    .output(options.outputPath)
 | 
						|
 | 
						|
  command = await builders[options.type](command, options)
 | 
						|
 | 
						|
  await runCommand({ command, job: options.job })
 | 
						|
 | 
						|
  await fixHLSPlaylistIfNeeded(options)
 | 
						|
}
 | 
						|
 | 
						|
// ---------------------------------------------------------------------------
 | 
						|
 | 
						|
export {
 | 
						|
  transcodeVOD,
 | 
						|
 | 
						|
  buildVODCommand,
 | 
						|
 | 
						|
  TranscodeVODOptions,
 | 
						|
  TranscodeVODOptionsType
 | 
						|
}
 | 
						|
 | 
						|
// ---------------------------------------------------------------------------
 | 
						|
 | 
						|
async function buildVODCommand (command: FfmpegCommand, options: TranscodeVODOptions) {
 | 
						|
  const probe = await ffprobePromise(options.inputPath)
 | 
						|
 | 
						|
  let fps = await getVideoStreamFPS(options.inputPath, probe)
 | 
						|
  fps = computeFPS(fps, options.resolution)
 | 
						|
 | 
						|
  let scaleFilterValue: string
 | 
						|
 | 
						|
  if (options.resolution !== undefined) {
 | 
						|
    const videoStreamInfo = await getVideoStreamDimensionsInfo(options.inputPath, probe)
 | 
						|
 | 
						|
    scaleFilterValue = videoStreamInfo?.isPortraitMode === true
 | 
						|
      ? `w=${options.resolution}:h=-2`
 | 
						|
      : `w=-2:h=${options.resolution}`
 | 
						|
  }
 | 
						|
 | 
						|
  command = await presetVOD({
 | 
						|
    ...pick(options, [ 'resolution', 'availableEncoders', 'profile' ]),
 | 
						|
 | 
						|
    command,
 | 
						|
    input: options.inputPath,
 | 
						|
    canCopyAudio: true,
 | 
						|
    canCopyVideo: true,
 | 
						|
    fps,
 | 
						|
    scaleFilterValue
 | 
						|
  })
 | 
						|
 | 
						|
  return command
 | 
						|
}
 | 
						|
 | 
						|
function buildQuickTranscodeCommand (command: FfmpegCommand) {
 | 
						|
  command = presetCopy(command)
 | 
						|
 | 
						|
  command = command.outputOption('-map_metadata -1') // strip all metadata
 | 
						|
                   .outputOption('-movflags faststart')
 | 
						|
 | 
						|
  return command
 | 
						|
}
 | 
						|
 | 
						|
// ---------------------------------------------------------------------------
 | 
						|
// Audio transcoding
 | 
						|
// ---------------------------------------------------------------------------
 | 
						|
 | 
						|
async function buildAudioMergeCommand (command: FfmpegCommand, options: MergeAudioTranscodeOptions) {
 | 
						|
  command = command.loop(undefined)
 | 
						|
 | 
						|
  const scaleFilterValue = getMergeAudioScaleFilterValue()
 | 
						|
  command = await presetVOD({
 | 
						|
    ...pick(options, [ 'resolution', 'availableEncoders', 'profile' ]),
 | 
						|
 | 
						|
    command,
 | 
						|
    input: options.audioPath,
 | 
						|
    canCopyAudio: true,
 | 
						|
    canCopyVideo: true,
 | 
						|
    fps: VIDEO_TRANSCODING_FPS.AUDIO_MERGE,
 | 
						|
    scaleFilterValue
 | 
						|
  })
 | 
						|
 | 
						|
  command.outputOption('-preset:v veryfast')
 | 
						|
 | 
						|
  command = command.input(options.audioPath)
 | 
						|
                   .outputOption('-tune stillimage')
 | 
						|
                   .outputOption('-shortest')
 | 
						|
 | 
						|
  return command
 | 
						|
}
 | 
						|
 | 
						|
function buildOnlyAudioCommand (command: FfmpegCommand, _options: OnlyAudioTranscodeOptions) {
 | 
						|
  command = presetOnlyAudio(command)
 | 
						|
 | 
						|
  return command
 | 
						|
}
 | 
						|
 | 
						|
// ---------------------------------------------------------------------------
 | 
						|
// HLS transcoding
 | 
						|
// ---------------------------------------------------------------------------
 | 
						|
 | 
						|
async function buildHLSVODCommand (command: FfmpegCommand, options: HLSTranscodeOptions) {
 | 
						|
  const videoPath = getHLSVideoPath(options)
 | 
						|
 | 
						|
  if (options.copyCodecs) command = presetCopy(command)
 | 
						|
  else if (options.resolution === VideoResolution.H_NOVIDEO) command = presetOnlyAudio(command)
 | 
						|
  else command = await buildVODCommand(command, options)
 | 
						|
 | 
						|
  addCommonHLSVODCommandOptions(command, videoPath)
 | 
						|
 | 
						|
  return command
 | 
						|
}
 | 
						|
 | 
						|
function buildHLSVODFromTSCommand (command: FfmpegCommand, options: HLSFromTSTranscodeOptions) {
 | 
						|
  const videoPath = getHLSVideoPath(options)
 | 
						|
 | 
						|
  command.outputOption('-c copy')
 | 
						|
 | 
						|
  if (options.isAAC) {
 | 
						|
    // Required for example when copying an AAC stream from an MPEG-TS
 | 
						|
    // Since it's a bitstream filter, we don't need to reencode the audio
 | 
						|
    command.outputOption('-bsf:a aac_adtstoasc')
 | 
						|
  }
 | 
						|
 | 
						|
  addCommonHLSVODCommandOptions(command, videoPath)
 | 
						|
 | 
						|
  return command
 | 
						|
}
 | 
						|
 | 
						|
function addCommonHLSVODCommandOptions (command: FfmpegCommand, outputPath: string) {
 | 
						|
  return command.outputOption('-hls_time 4')
 | 
						|
                .outputOption('-hls_list_size 0')
 | 
						|
                .outputOption('-hls_playlist_type vod')
 | 
						|
                .outputOption('-hls_segment_filename ' + outputPath)
 | 
						|
                .outputOption('-hls_segment_type fmp4')
 | 
						|
                .outputOption('-f hls')
 | 
						|
                .outputOption('-hls_flags single_file')
 | 
						|
}
 | 
						|
 | 
						|
async function fixHLSPlaylistIfNeeded (options: TranscodeVODOptions) {
 | 
						|
  if (options.type !== 'hls' && options.type !== 'hls-from-ts') return
 | 
						|
 | 
						|
  const fileContent = await readFile(options.outputPath)
 | 
						|
 | 
						|
  const videoFileName = options.hlsPlaylist.videoFilename
 | 
						|
  const videoFilePath = getHLSVideoPath(options)
 | 
						|
 | 
						|
  // Fix wrong mapping with some ffmpeg versions
 | 
						|
  const newContent = fileContent.toString()
 | 
						|
                                .replace(`#EXT-X-MAP:URI="${videoFilePath}",`, `#EXT-X-MAP:URI="${videoFileName}",`)
 | 
						|
 | 
						|
  await writeFile(options.outputPath, newContent)
 | 
						|
}
 | 
						|
 | 
						|
// ---------------------------------------------------------------------------
 | 
						|
// Helpers
 | 
						|
// ---------------------------------------------------------------------------
 | 
						|
 | 
						|
function getHLSVideoPath (options: HLSTranscodeOptions | HLSFromTSTranscodeOptions) {
 | 
						|
  return `${dirname(options.outputPath)}/${options.hlsPlaylist.videoFilename}`
 | 
						|
}
 | 
						|
 | 
						|
// Avoid "height not divisible by 2" error
 | 
						|
function getMergeAudioScaleFilterValue () {
 | 
						|
  return 'trunc(iw/2)*2:trunc(ih/2)*2'
 | 
						|
}
 |