From 536598cfafab1c5e24e881db1c528489f804fb6b Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 16 May 2019 16:55:34 +0200 Subject: Add audio support in upload --- server/helpers/ffmpeg-utils.ts | 154 +++++++++++++++++++++++------------------ 1 file changed, 86 insertions(+), 68 deletions(-) (limited to 'server/helpers') diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts index 2fdf34cb7..c180da832 100644 --- a/server/helpers/ffmpeg-utils.ts +++ b/server/helpers/ffmpeg-utils.ts @@ -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(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 { - 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 { - 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 { +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 + localCommand = localCommand.audioCodec('aac') + const audioCodecName = parsedAudio.audioStream[ 'codec_name' ] - let bitrate: number - if (audio.bitrate[ audioCodecName ]) { - localCommand = localCommand.audioCodec('aac') - bitrate = audio.bitrate[ audioCodecName ](parsedAudio.audioStream[ 'bit_rate' ]) + 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 }`]) - - // 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 }`) + 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}`) + } return localCommand } -- cgit v1.2.3 From 820d79c8ac7ba144d9357320c49b85b980387d43 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 21 May 2019 09:53:03 +0200 Subject: Fix OGG bug with firefox --- server/helpers/express-utils.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) (limited to 'server/helpers') diff --git a/server/helpers/express-utils.ts b/server/helpers/express-utils.ts index e0a1d56a5..00f3f198b 100644 --- a/server/helpers/express-utils.ts +++ b/server/helpers/express-utils.ts @@ -74,7 +74,18 @@ function createReqFiles ( }, filename: async (req, file, cb) => { - const extension = mimeTypes[ file.mimetype ] || extname(file.originalname) + let extension: string + const fileExtension = extname(file.originalname) + const extensionFromMimetype = mimeTypes[ file.mimetype ] + + // Take the file extension if we don't understand the mime type + // We have the OGG/OGV exception too because firefox sends a bad mime type when sending an OGG file + if (fileExtension === '.ogg' || fileExtension === '.ogv' || !extensionFromMimetype) { + extension = fileExtension + } else { + extension = extensionFromMimetype + } + let randomString = '' try { -- cgit v1.2.3