mirror of https://github.com/Chocobozzz/PeerTube
				
				
				
			
		
			
				
	
	
		
			257 lines
		
	
	
		
			7.6 KiB
		
	
	
	
		
			TypeScript
		
	
	
			
		
		
	
	
			257 lines
		
	
	
		
			7.6 KiB
		
	
	
	
		
			TypeScript
		
	
	
| import { MutexInterface } from 'async-mutex'
 | |
| import { FfmpegCommand } from 'fluent-ffmpeg'
 | |
| import { readFile, writeFile } from 'fs-extra'
 | |
| import { dirname } from 'path'
 | |
| import { pick } from '@shared/core-utils'
 | |
| import { VideoResolution } from '@shared/models'
 | |
| import { FFmpegCommandWrapper, FFmpegCommandWrapperOptions } from './ffmpeg-command-wrapper'
 | |
| import { ffprobePromise, getVideoStreamDimensionsInfo } from './ffprobe'
 | |
| import { presetCopy, presetOnlyAudio, presetVOD } from './shared/presets'
 | |
| 
 | |
| export type TranscodeVODOptionsType = 'hls' | 'hls-from-ts' | 'quick-transcode' | 'video' | 'merge-audio' | 'only-audio'
 | |
| 
 | |
| export 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
 | |
| 
 | |
|   resolution: number
 | |
|   fps: number
 | |
| }
 | |
| 
 | |
| export interface HLSTranscodeOptions extends BaseTranscodeVODOptions {
 | |
|   type: 'hls'
 | |
| 
 | |
|   copyCodecs: boolean
 | |
| 
 | |
|   hlsPlaylist: {
 | |
|     videoFilename: string
 | |
|   }
 | |
| }
 | |
| 
 | |
| export interface HLSFromTSTranscodeOptions extends BaseTranscodeVODOptions {
 | |
|   type: 'hls-from-ts'
 | |
| 
 | |
|   isAAC: boolean
 | |
| 
 | |
|   hlsPlaylist: {
 | |
|     videoFilename: string
 | |
|   }
 | |
| }
 | |
| 
 | |
| export interface QuickTranscodeOptions extends BaseTranscodeVODOptions {
 | |
|   type: 'quick-transcode'
 | |
| }
 | |
| 
 | |
| export interface VideoTranscodeOptions extends BaseTranscodeVODOptions {
 | |
|   type: 'video'
 | |
| }
 | |
| 
 | |
| export interface MergeAudioTranscodeOptions extends BaseTranscodeVODOptions {
 | |
|   type: 'merge-audio'
 | |
|   audioPath: string
 | |
| }
 | |
| 
 | |
| export interface OnlyAudioTranscodeOptions extends BaseTranscodeVODOptions {
 | |
|   type: 'only-audio'
 | |
| }
 | |
| 
 | |
| export type TranscodeVODOptions =
 | |
|   HLSTranscodeOptions
 | |
|   | HLSFromTSTranscodeOptions
 | |
|   | VideoTranscodeOptions
 | |
|   | MergeAudioTranscodeOptions
 | |
|   | OnlyAudioTranscodeOptions
 | |
|   | QuickTranscodeOptions
 | |
| 
 | |
| // ---------------------------------------------------------------------------
 | |
| 
 | |
| export class FFmpegVOD {
 | |
|   private readonly commandWrapper: FFmpegCommandWrapper
 | |
| 
 | |
|   private ended = false
 | |
| 
 | |
|   constructor (options: FFmpegCommandWrapperOptions) {
 | |
|     this.commandWrapper = new FFmpegCommandWrapper(options)
 | |
|   }
 | |
| 
 | |
|   async transcode (options: TranscodeVODOptions) {
 | |
|     const builders: {
 | |
|       [ type in TranscodeVODOptionsType ]: (options: TranscodeVODOptions) => Promise<void> | void
 | |
|     } = {
 | |
|       'quick-transcode': this.buildQuickTranscodeCommand.bind(this),
 | |
|       'hls': this.buildHLSVODCommand.bind(this),
 | |
|       'hls-from-ts': this.buildHLSVODFromTSCommand.bind(this),
 | |
|       'merge-audio': this.buildAudioMergeCommand.bind(this),
 | |
|       // TODO: remove, we merge this in buildWebVideoCommand
 | |
|       'only-audio': this.buildOnlyAudioCommand.bind(this),
 | |
|       'video': this.buildWebVideoCommand.bind(this)
 | |
|     }
 | |
| 
 | |
|     this.commandWrapper.debugLog('Will run transcode.', { options })
 | |
| 
 | |
|     const command = this.commandWrapper.buildCommand(options.inputPath)
 | |
|       .output(options.outputPath)
 | |
| 
 | |
|     await builders[options.type](options)
 | |
| 
 | |
|     command.on('start', () => {
 | |
|       setTimeout(() => {
 | |
|         options.inputFileMutexReleaser()
 | |
|       }, 1000)
 | |
|     })
 | |
| 
 | |
|     await this.commandWrapper.runCommand()
 | |
| 
 | |
|     await this.fixHLSPlaylistIfNeeded(options)
 | |
| 
 | |
|     this.ended = true
 | |
|   }
 | |
| 
 | |
|   isEnded () {
 | |
|     return this.ended
 | |
|   }
 | |
| 
 | |
|   private async buildWebVideoCommand (options: TranscodeVODOptions) {
 | |
|     const { resolution, fps, inputPath } = options
 | |
| 
 | |
|     if (resolution === VideoResolution.H_NOVIDEO) {
 | |
|       presetOnlyAudio(this.commandWrapper)
 | |
|       return
 | |
|     }
 | |
| 
 | |
|     let scaleFilterValue: string
 | |
| 
 | |
|     if (resolution !== undefined) {
 | |
|       const probe = await ffprobePromise(inputPath)
 | |
|       const videoStreamInfo = await getVideoStreamDimensionsInfo(inputPath, probe)
 | |
| 
 | |
|       scaleFilterValue = videoStreamInfo?.isPortraitMode === true
 | |
|         ? `w=${resolution}:h=-2`
 | |
|         : `w=-2:h=${resolution}`
 | |
|     }
 | |
| 
 | |
|     await presetVOD({
 | |
|       commandWrapper: this.commandWrapper,
 | |
| 
 | |
|       resolution,
 | |
|       input: inputPath,
 | |
|       canCopyAudio: true,
 | |
|       canCopyVideo: true,
 | |
|       fps,
 | |
|       scaleFilterValue
 | |
|     })
 | |
|   }
 | |
| 
 | |
|   private buildQuickTranscodeCommand (_options: TranscodeVODOptions) {
 | |
|     const command = this.commandWrapper.getCommand()
 | |
| 
 | |
|     presetCopy(this.commandWrapper)
 | |
| 
 | |
|     command.outputOption('-map_metadata -1') // strip all metadata
 | |
|       .outputOption('-movflags faststart')
 | |
|   }
 | |
| 
 | |
|   // ---------------------------------------------------------------------------
 | |
|   // Audio transcoding
 | |
|   // ---------------------------------------------------------------------------
 | |
| 
 | |
|   private async buildAudioMergeCommand (options: MergeAudioTranscodeOptions) {
 | |
|     const command = this.commandWrapper.getCommand()
 | |
| 
 | |
|     command.loop(undefined)
 | |
| 
 | |
|     await presetVOD({
 | |
|       ...pick(options, [ 'resolution' ]),
 | |
| 
 | |
|       commandWrapper: this.commandWrapper,
 | |
|       input: options.audioPath,
 | |
|       canCopyAudio: true,
 | |
|       canCopyVideo: true,
 | |
|       fps: options.fps,
 | |
|       scaleFilterValue: this.getMergeAudioScaleFilterValue()
 | |
|     })
 | |
| 
 | |
|     command.outputOption('-preset:v veryfast')
 | |
| 
 | |
|     command.input(options.audioPath)
 | |
|       .outputOption('-tune stillimage')
 | |
|       .outputOption('-shortest')
 | |
|   }
 | |
| 
 | |
|   private buildOnlyAudioCommand (_options: OnlyAudioTranscodeOptions) {
 | |
|     presetOnlyAudio(this.commandWrapper)
 | |
|   }
 | |
| 
 | |
|   // Avoid "height not divisible by 2" error
 | |
|   private getMergeAudioScaleFilterValue () {
 | |
|     return 'trunc(iw/2)*2:trunc(ih/2)*2'
 | |
|   }
 | |
| 
 | |
|   // ---------------------------------------------------------------------------
 | |
|   // HLS transcoding
 | |
|   // ---------------------------------------------------------------------------
 | |
| 
 | |
|   private async buildHLSVODCommand (options: HLSTranscodeOptions) {
 | |
|     const command = this.commandWrapper.getCommand()
 | |
| 
 | |
|     const videoPath = this.getHLSVideoPath(options)
 | |
| 
 | |
|     if (options.copyCodecs) presetCopy(this.commandWrapper)
 | |
|     else if (options.resolution === VideoResolution.H_NOVIDEO) presetOnlyAudio(this.commandWrapper)
 | |
|     else await this.buildWebVideoCommand(options)
 | |
| 
 | |
|     this.addCommonHLSVODCommandOptions(command, videoPath)
 | |
|   }
 | |
| 
 | |
|   private buildHLSVODFromTSCommand (options: HLSFromTSTranscodeOptions) {
 | |
|     const command = this.commandWrapper.getCommand()
 | |
| 
 | |
|     const videoPath = this.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')
 | |
|     }
 | |
| 
 | |
|     this.addCommonHLSVODCommandOptions(command, videoPath)
 | |
|   }
 | |
| 
 | |
|   private 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')
 | |
|   }
 | |
| 
 | |
|   private async 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 = this.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)
 | |
|   }
 | |
| 
 | |
|   private getHLSVideoPath (options: HLSTranscodeOptions | HLSFromTSTranscodeOptions) {
 | |
|     return `${dirname(options.outputPath)}/${options.hlsPlaylist.videoFilename}`
 | |
|   }
 | |
| }
 |