X-Git-Url: https://git.librecmc.org/?a=blobdiff_plain;f=server%2Fhelpers%2Fffmpeg-utils.ts;h=d818c459c66d5752d0da68fbb1d7709cdcca0da2;hb=4ee7a4c9ac9280cda930a281c2d5a9a4c409cc14;hp=8b9045038dba412f4dbb54187d1f7c4c9e88accd;hpb=be0f59b4eec3c2c4dcd151e2b174be39dff1568e;p=oweals%2Fpeertube.git diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts index 8b9045038..d818c459c 100644 --- a/server/helpers/ffmpeg-utils.ts +++ b/server/helpers/ffmpeg-utils.ts @@ -1,11 +1,12 @@ import * as ffmpeg from 'fluent-ffmpeg' -import { join } from 'path' +import { dirname, join } from 'path' import { getTargetBitrate, VideoResolution } from '../../shared/models/videos' -import { CONFIG, FFMPEG_NICE, VIDEO_TRANSCODING_FPS } from '../initializers' +import { FFMPEG_NICE, VIDEO_TRANSCODING_FPS } from '../initializers/constants' import { processImage } from './image-utils' import { logger } from './logger' import { checkFFmpegEncoders } from '../initializers/checker-before-init' -import { remove } from 'fs-extra' +import { readFile, remove, writeFile } from 'fs-extra' +import { CONFIG } from '../initializers/config' function computeResolutionsToTranscode (videoFileHeight: number) { const resolutionsEnabled: number[] = [] @@ -29,19 +30,28 @@ function computeResolutionsToTranscode (videoFileHeight: number) { return resolutionsEnabled } -async function getVideoFileResolution (path: string) { +async function getVideoFileSize (path: string) { const videoStream = await getVideoFileStream(path) return { - videoFileResolution: Math.min(videoStream.height, videoStream.width), - isPortraitMode: videoStream.height > videoStream.width + width: videoStream.width, + height: videoStream.height + } +} + +async function getVideoFileResolution (path: string) { + const size = await getVideoFileSize(path) + + return { + videoFileResolution: Math.min(size.height, size.width), + isPortraitMode: size.height > size.width } } async function getVideoFileFPS (path: string) { const videoStream = await getVideoFileStream(path) - for (const key of [ 'r_frame_rate' , 'avg_frame_rate' ]) { + for (const key of [ 'avg_frame_rate', 'r_frame_rate' ]) { const valuesText: string = videoStream[key] if (!valuesText) continue @@ -110,52 +120,41 @@ async function generateImageFromVideoFile (fromPath: string, folder: string, ima type TranscodeOptions = { inputPath: string outputPath: string - resolution?: VideoResolution + resolution: VideoResolution isPortraitMode?: boolean + + hlsPlaylist?: { + videoFilename: string + } } function transcode (options: TranscodeOptions) { return new Promise(async (res, rej) => { try { - let fps = await getVideoFileFPS(options.inputPath) - // On small/medium resolutions, limit FPS - if ( - options.resolution !== undefined && - options.resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN && - fps > VIDEO_TRANSCODING_FPS.AVERAGE - ) { - fps = VIDEO_TRANSCODING_FPS.AVERAGE - } - let command = ffmpeg(options.inputPath, { niceness: FFMPEG_NICE.TRANSCODING }) .output(options.outputPath) - command = await presetH264(command, options.resolution, fps) + + if (options.hlsPlaylist) { + command = await buildHLSCommand(command, options) + } else { + command = await buildx264Command(command, options) + } if (CONFIG.TRANSCODING.THREADS > 0) { // if we don't set any threads ffmpeg will chose automatically command = command.outputOption('-threads ' + CONFIG.TRANSCODING.THREADS) } - if (options.resolution !== undefined) { - // '?x720' or '720x?' for example - const size = options.isPortraitMode === true ? `${options.resolution}x?` : `?x${options.resolution}` - command = command.size(size) - } - - if (fps) { - // Hard FPS limits - if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = VIDEO_TRANSCODING_FPS.MAX - else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN - - command = command.withFPS(fps) - } - command .on('error', (err, stdout, stderr) => { logger.error('Error in transcoding job.', { stdout, stderr }) return rej(err) }) - .on('end', res) + .on('end', () => { + return onTranscodingSuccess(options) + .then(() => res()) + .catch(err => rej(err)) + }) .run() } catch (err) { return rej(err) @@ -166,6 +165,7 @@ function transcode (options: TranscodeOptions) { // --------------------------------------------------------------------------- export { + getVideoFileSize, getVideoFileResolution, getDurationFromVideoFile, generateImageFromVideoFile, @@ -178,13 +178,78 @@ export { // --------------------------------------------------------------------------- +async function buildx264Command (command: ffmpeg.FfmpegCommand, options: TranscodeOptions) { + let fps = await getVideoFileFPS(options.inputPath) + // On small/medium resolutions, limit FPS + if ( + options.resolution !== undefined && + options.resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN && + fps > VIDEO_TRANSCODING_FPS.AVERAGE + ) { + fps = VIDEO_TRANSCODING_FPS.AVERAGE + } + + command = await presetH264(command, options.resolution, fps) + + if (options.resolution !== undefined) { + // '?x720' or '720x?' for example + const size = options.isPortraitMode === true ? `${options.resolution}x?` : `?x${options.resolution}` + command = command.size(size) + } + + if (fps) { + // Hard FPS limits + if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = VIDEO_TRANSCODING_FPS.MAX + else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN + + command = command.withFPS(fps) + } + + return command +} + +async function buildHLSCommand (command: ffmpeg.FfmpegCommand, options: TranscodeOptions) { + const videoPath = getHLSVideoPath(options) + + command = await presetCopy(command) + + command = command.outputOption('-hls_time 4') + .outputOption('-hls_list_size 0') + .outputOption('-hls_playlist_type vod') + .outputOption('-hls_segment_filename ' + videoPath) + .outputOption('-hls_segment_type fmp4') + .outputOption('-f hls') + .outputOption('-hls_flags single_file') + + return command +} + +function getHLSVideoPath (options: TranscodeOptions) { + return `${dirname(options.outputPath)}/${options.hlsPlaylist.videoFilename}` +} + +async function onTranscodingSuccess (options: TranscodeOptions) { + if (!options.hlsPlaylist) return + + // Fix wrong mapping with some ffmpeg versions + const fileContent = await readFile(options.outputPath) + + const videoFileName = options.hlsPlaylist.videoFilename + const videoFilePath = getHLSVideoPath(options) + + const newContent = fileContent.toString() + .replace(`#EXT-X-MAP:URI="${videoFilePath}",`, `#EXT-X-MAP:URI="${videoFileName}",`) + + await writeFile(options.outputPath, newContent) +} + function getVideoFileStream (path: string) { return new Promise((res, rej) => { ffmpeg.ffprobe(path, (err, metadata) => { if (err) return rej(err) const videoStream = metadata.streams.find(s => s.codec_type === 'video') - if (!videoStream) throw new Error('Cannot find video stream of ' + path) + if (!videoStream) return rej(new Error('Cannot find video stream of ' + path)) return res(videoStream) }) @@ -328,10 +393,10 @@ async function presetH264 (command: ffmpeg.FfmpegCommand, resolution: VideoResol const audioCodecName = parsedAudio.audioStream[ 'codec_name' ] let bitrate: number if (audio.bitrate[ audioCodecName ]) { - bitrate = audio.bitrate[ audioCodecName ](parsedAudio.audioStream[ 'bit_rate' ]) + localCommand = localCommand.audioCodec('aac') - if (bitrate === -1) localCommand = localCommand.audioCodec('copy') - else if (bitrate !== undefined) localCommand = localCommand.audioBitrate(bitrate) + bitrate = audio.bitrate[ audioCodecName ](parsedAudio.audioStream[ 'bit_rate' ]) + if (bitrate !== undefined && bitrate !== -1) localCommand = localCommand.audioBitrate(bitrate) } } @@ -348,3 +413,10 @@ async function presetH264 (command: ffmpeg.FfmpegCommand, resolution: VideoResol return localCommand } + +async function presetCopy (command: ffmpeg.FfmpegCommand): Promise { + return command + .format('mp4') + .videoCodec('copy') + .audioCodec('copy') +}