mirror of https://github.com/Chocobozzz/PeerTube
				
				
				
			
		
			
				
	
	
		
			268 lines
		
	
	
		
			7.8 KiB
		
	
	
	
		
			TypeScript
		
	
	
			
		
		
	
	
			268 lines
		
	
	
		
			7.8 KiB
		
	
	
	
		
			TypeScript
		
	
	
| import { MutexInterface } from 'async-mutex'
 | |
| import { Job } from 'bullmq'
 | |
| import { FfmpegCommand } from 'fluent-ffmpeg'
 | |
| import { readFile, writeFile } from 'fs-extra'
 | |
| import { dirname } from 'path'
 | |
| import { VIDEO_TRANSCODING_FPS } from '@server/initializers/constants'
 | |
| 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'
 | |
| 
 | |
| 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
 | |
| 
 | |
|   // Will be released after the ffmpeg started
 | |
|   // To prevent a bug where the input file does not exist anymore when running ffmpeg
 | |
|   inputFileMutexReleaser: MutexInterface.Releaser
 | |
| 
 | |
|   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)
 | |
| 
 | |
|   command.on('start', () => {
 | |
|     setTimeout(() => {
 | |
|       options.inputFileMutexReleaser()
 | |
|     }, 1000)
 | |
|   })
 | |
| 
 | |
|   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'
 | |
| }
 |