Add audio support in upload
This commit is contained in:
parent
3daaa19274
commit
536598cfaf
27 changed files with 325 additions and 163 deletions
|
@ -117,37 +117,50 @@ async function generateImageFromVideoFile (fromPath: string, folder: string, ima
|
|||
}
|
||||
}
|
||||
|
||||
type TranscodeOptions = {
|
||||
type TranscodeOptionsType = 'hls' | 'quick-transcode' | 'video' | 'merge-audio'
|
||||
|
||||
interface BaseTranscodeOptions {
|
||||
type: TranscodeOptionsType
|
||||
inputPath: string
|
||||
outputPath: string
|
||||
resolution: VideoResolution
|
||||
isPortraitMode?: boolean
|
||||
doQuickTranscode?: Boolean
|
||||
}
|
||||
|
||||
hlsPlaylist?: {
|
||||
interface HLSTranscodeOptions extends BaseTranscodeOptions {
|
||||
type: 'hls'
|
||||
hlsPlaylist: {
|
||||
videoFilename: string
|
||||
}
|
||||
}
|
||||
|
||||
interface QuickTranscodeOptions extends BaseTranscodeOptions {
|
||||
type: 'quick-transcode'
|
||||
}
|
||||
|
||||
interface VideoTranscodeOptions extends BaseTranscodeOptions {
|
||||
type: 'video'
|
||||
}
|
||||
|
||||
interface MergeAudioTranscodeOptions extends BaseTranscodeOptions {
|
||||
type: 'merge-audio'
|
||||
audioPath: string
|
||||
}
|
||||
|
||||
type TranscodeOptions = HLSTranscodeOptions | VideoTranscodeOptions | MergeAudioTranscodeOptions | QuickTranscodeOptions
|
||||
|
||||
function transcode (options: TranscodeOptions) {
|
||||
return new Promise<void>(async (res, rej) => {
|
||||
try {
|
||||
let command = ffmpeg(options.inputPath, { niceness: FFMPEG_NICE.TRANSCODING })
|
||||
.output(options.outputPath)
|
||||
|
||||
if (options.doQuickTranscode) {
|
||||
if (options.hlsPlaylist) {
|
||||
throw(Error("Quick transcode and HLS can't be used at the same time"))
|
||||
}
|
||||
|
||||
command
|
||||
.format('mp4')
|
||||
.addOption('-c:v copy')
|
||||
.addOption('-c:a copy')
|
||||
.outputOption('-map_metadata -1') // strip all metadata
|
||||
.outputOption('-movflags faststart')
|
||||
} else if (options.hlsPlaylist) {
|
||||
if (options.type === 'quick-transcode') {
|
||||
command = await buildQuickTranscodeCommand(command)
|
||||
} else if (options.type === 'hls') {
|
||||
command = await buildHLSCommand(command, options)
|
||||
} else if (options.type === 'merge-audio') {
|
||||
command = await buildAudioMergeCommand(command, options)
|
||||
} else {
|
||||
command = await buildx264Command(command, options)
|
||||
}
|
||||
|
@ -163,7 +176,7 @@ function transcode (options: TranscodeOptions) {
|
|||
return rej(err)
|
||||
})
|
||||
.on('end', () => {
|
||||
return onTranscodingSuccess(options)
|
||||
return fixHLSPlaylistIfNeeded(options)
|
||||
.then(() => res())
|
||||
.catch(err => rej(err))
|
||||
})
|
||||
|
@ -205,6 +218,8 @@ export {
|
|||
getVideoFileResolution,
|
||||
getDurationFromVideoFile,
|
||||
generateImageFromVideoFile,
|
||||
TranscodeOptions,
|
||||
TranscodeOptionsType,
|
||||
transcode,
|
||||
getVideoFileFPS,
|
||||
computeResolutionsToTranscode,
|
||||
|
@ -215,7 +230,7 @@ export {
|
|||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function buildx264Command (command: ffmpeg.FfmpegCommand, options: TranscodeOptions) {
|
||||
async function buildx264Command (command: ffmpeg.FfmpegCommand, options: VideoTranscodeOptions) {
|
||||
let fps = await getVideoFileFPS(options.inputPath)
|
||||
// On small/medium resolutions, limit FPS
|
||||
if (
|
||||
|
@ -226,7 +241,7 @@ async function buildx264Command (command: ffmpeg.FfmpegCommand, options: Transco
|
|||
fps = VIDEO_TRANSCODING_FPS.AVERAGE
|
||||
}
|
||||
|
||||
command = await presetH264(command, options.resolution, fps)
|
||||
command = await presetH264(command, options.inputPath, options.resolution, fps)
|
||||
|
||||
if (options.resolution !== undefined) {
|
||||
// '?x720' or '720x?' for example
|
||||
|
@ -245,7 +260,29 @@ async function buildx264Command (command: ffmpeg.FfmpegCommand, options: Transco
|
|||
return command
|
||||
}
|
||||
|
||||
async function buildHLSCommand (command: ffmpeg.FfmpegCommand, options: TranscodeOptions) {
|
||||
async function buildAudioMergeCommand (command: ffmpeg.FfmpegCommand, options: MergeAudioTranscodeOptions) {
|
||||
command = command.loop(undefined)
|
||||
|
||||
command = await presetH264VeryFast(command, options.audioPath, options.resolution)
|
||||
|
||||
command = command.input(options.audioPath)
|
||||
.videoFilter('scale=trunc(iw/2)*2:trunc(ih/2)*2') // Avoid "height not divisible by 2" error
|
||||
.outputOption('-tune stillimage')
|
||||
.outputOption('-shortest')
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
async function buildQuickTranscodeCommand (command: ffmpeg.FfmpegCommand) {
|
||||
command = await presetCopy(command)
|
||||
|
||||
command = command.outputOption('-map_metadata -1') // strip all metadata
|
||||
.outputOption('-movflags faststart')
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
async function buildHLSCommand (command: ffmpeg.FfmpegCommand, options: HLSTranscodeOptions) {
|
||||
const videoPath = getHLSVideoPath(options)
|
||||
|
||||
command = await presetCopy(command)
|
||||
|
@ -261,19 +298,19 @@ async function buildHLSCommand (command: ffmpeg.FfmpegCommand, options: Transcod
|
|||
return command
|
||||
}
|
||||
|
||||
function getHLSVideoPath (options: TranscodeOptions) {
|
||||
function getHLSVideoPath (options: HLSTranscodeOptions) {
|
||||
return `${dirname(options.outputPath)}/${options.hlsPlaylist.videoFilename}`
|
||||
}
|
||||
|
||||
async function onTranscodingSuccess (options: TranscodeOptions) {
|
||||
if (!options.hlsPlaylist) return
|
||||
async function fixHLSPlaylistIfNeeded (options: TranscodeOptions) {
|
||||
if (options.type !== 'hls') return
|
||||
|
||||
// Fix wrong mapping with some ffmpeg versions
|
||||
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}",`)
|
||||
|
||||
|
@ -300,44 +337,27 @@ function getVideoStreamFromFile (path: string) {
|
|||
* and quality. Superfast and ultrafast will give you better
|
||||
* performance, but then quality is noticeably worse.
|
||||
*/
|
||||
async function presetH264VeryFast (command: ffmpeg.FfmpegCommand, resolution: VideoResolution, fps: number): Promise<ffmpeg.FfmpegCommand> {
|
||||
let localCommand = await presetH264(command, resolution, fps)
|
||||
async function presetH264VeryFast (command: ffmpeg.FfmpegCommand, input: string, resolution: VideoResolution, fps?: number) {
|
||||
let localCommand = await presetH264(command, input, resolution, fps)
|
||||
|
||||
localCommand = localCommand.outputOption('-preset:v veryfast')
|
||||
.outputOption([ '--aq-mode=2', '--aq-strength=1.3' ])
|
||||
|
||||
/*
|
||||
MAIN reference: https://slhck.info/video/2017/03/01/rate-control.html
|
||||
Our target situation is closer to a livestream than a stream,
|
||||
since we want to reduce as much a possible the encoding burden,
|
||||
altough not to the point of a livestream where there is a hard
|
||||
although not to the point of a livestream where there is a hard
|
||||
constraint on the frames per second to be encoded.
|
||||
|
||||
why '--aq-mode=2 --aq-strength=1.3' instead of '-profile:v main'?
|
||||
Make up for most of the loss of grain and macroblocking
|
||||
with less computing power.
|
||||
*/
|
||||
|
||||
return localCommand
|
||||
}
|
||||
|
||||
/**
|
||||
* A preset optimised for a stillimage audio video
|
||||
*/
|
||||
async function presetStillImageWithAudio (
|
||||
command: ffmpeg.FfmpegCommand,
|
||||
resolution: VideoResolution,
|
||||
fps: number
|
||||
): Promise<ffmpeg.FfmpegCommand> {
|
||||
let localCommand = await presetH264VeryFast(command, resolution, fps)
|
||||
localCommand = localCommand.outputOption('-tune stillimage')
|
||||
|
||||
return localCommand
|
||||
}
|
||||
|
||||
/**
|
||||
* A toolbox to play with audio
|
||||
*/
|
||||
namespace audio {
|
||||
export const get = (option: ffmpeg.FfmpegCommand | string) => {
|
||||
export const get = (option: string) => {
|
||||
// without position, ffprobe considers the last input only
|
||||
// we make it consider the first input only
|
||||
// if you pass a file path to pos, then ffprobe acts on that file directly
|
||||
|
@ -359,11 +379,7 @@ namespace audio {
|
|||
return res({ absolutePath: data.format.filename })
|
||||
}
|
||||
|
||||
if (typeof option === 'string') {
|
||||
return ffmpeg.ffprobe(option, parseFfprobe)
|
||||
}
|
||||
|
||||
return option.ffprobe(parseFfprobe)
|
||||
return ffmpeg.ffprobe(option, parseFfprobe)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -405,7 +421,7 @@ namespace audio {
|
|||
* As for the audio, quality '5' is the highest and ensures 96-112kbps/channel
|
||||
* See https://trac.ffmpeg.org/wiki/Encode/AAC#fdk_vbr
|
||||
*/
|
||||
async function presetH264 (command: ffmpeg.FfmpegCommand, resolution: VideoResolution, fps: number): Promise<ffmpeg.FfmpegCommand> {
|
||||
async function presetH264 (command: ffmpeg.FfmpegCommand, input: string, resolution: VideoResolution, fps?: number) {
|
||||
let localCommand = command
|
||||
.format('mp4')
|
||||
.videoCodec('libx264')
|
||||
|
@ -416,7 +432,7 @@ async function presetH264 (command: ffmpeg.FfmpegCommand, resolution: VideoResol
|
|||
.outputOption('-map_metadata -1') // strip all metadata
|
||||
.outputOption('-movflags faststart')
|
||||
|
||||
const parsedAudio = await audio.get(localCommand)
|
||||
const parsedAudio = await audio.get(input)
|
||||
|
||||
if (!parsedAudio.audioStream) {
|
||||
localCommand = localCommand.noAudio()
|
||||
|
@ -425,28 +441,30 @@ async function presetH264 (command: ffmpeg.FfmpegCommand, resolution: VideoResol
|
|||
.audioCodec('libfdk_aac')
|
||||
.audioQuality(5)
|
||||
} else {
|
||||
// we try to reduce the ceiling bitrate by making rough correspondances of bitrates
|
||||
// we try to reduce the ceiling bitrate by making rough matches of bitrates
|
||||
// of course this is far from perfect, but it might save some space in the end
|
||||
const audioCodecName = parsedAudio.audioStream[ 'codec_name' ]
|
||||
let bitrate: number
|
||||
if (audio.bitrate[ audioCodecName ]) {
|
||||
localCommand = localCommand.audioCodec('aac')
|
||||
localCommand = localCommand.audioCodec('aac')
|
||||
|
||||
bitrate = audio.bitrate[ audioCodecName ](parsedAudio.audioStream[ 'bit_rate' ])
|
||||
const audioCodecName = parsedAudio.audioStream[ 'codec_name' ]
|
||||
|
||||
if (audio.bitrate[ audioCodecName ]) {
|
||||
const bitrate = audio.bitrate[ audioCodecName ](parsedAudio.audioStream[ 'bit_rate' ])
|
||||
if (bitrate !== undefined && bitrate !== -1) localCommand = localCommand.audioBitrate(bitrate)
|
||||
}
|
||||
}
|
||||
|
||||
// Constrained Encoding (VBV)
|
||||
// https://slhck.info/video/2017/03/01/rate-control.html
|
||||
// https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate
|
||||
const targetBitrate = getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS)
|
||||
localCommand = localCommand.outputOptions([`-maxrate ${ targetBitrate }`, `-bufsize ${ targetBitrate * 2 }`])
|
||||
if (fps) {
|
||||
// Constrained Encoding (VBV)
|
||||
// https://slhck.info/video/2017/03/01/rate-control.html
|
||||
// https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate
|
||||
const targetBitrate = getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS)
|
||||
localCommand = localCommand.outputOptions([ `-maxrate ${targetBitrate}`, `-bufsize ${targetBitrate * 2}` ])
|
||||
|
||||
// Keyframe interval of 2 seconds for faster seeking and resolution switching.
|
||||
// https://streaminglearningcenter.com/blogs/whats-the-right-keyframe-interval.html
|
||||
// https://superuser.com/a/908325
|
||||
localCommand = localCommand.outputOption(`-g ${ fps * 2 }`)
|
||||
// Keyframe interval of 2 seconds for faster seeking and resolution switching.
|
||||
// https://streaminglearningcenter.com/blogs/whats-the-right-keyframe-interval.html
|
||||
// https://superuser.com/a/908325
|
||||
localCommand = localCommand.outputOption(`-g ${fps * 2}`)
|
||||
}
|
||||
|
||||
return localCommand
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue