diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts index 76b744de8..2fdf34cb7 100644 --- a/server/helpers/ffmpeg-utils.ts +++ b/server/helpers/ffmpeg-utils.ts @@ -1,6 +1,6 @@ import * as ffmpeg from 'fluent-ffmpeg' import { dirname, join } from 'path' -import { getTargetBitrate, VideoResolution } from '../../shared/models/videos' +import { getTargetBitrate, getMaxBitrate, VideoResolution } from '../../shared/models/videos' import { FFMPEG_NICE, VIDEO_TRANSCODING_FPS } from '../initializers/constants' import { processImage } from './image-utils' import { logger } from './logger' @@ -31,7 +31,7 @@ function computeResolutionsToTranscode (videoFileHeight: number) { } async function getVideoFileSize (path: string) { - const videoStream = await getVideoFileStream(path) + const videoStream = await getVideoStreamFromFile(path) return { width: videoStream.width, @@ -49,7 +49,7 @@ async function getVideoFileResolution (path: string) { } async function getVideoFileFPS (path: string) { - const videoStream = await getVideoFileStream(path) + const videoStream = await getVideoStreamFromFile(path) for (const key of [ 'avg_frame_rate', 'r_frame_rate' ]) { const valuesText: string = videoStream[key] @@ -122,6 +122,7 @@ type TranscodeOptions = { outputPath: string resolution: VideoResolution isPortraitMode?: boolean + doQuickTranscode?: Boolean hlsPlaylist?: { videoFilename: string @@ -134,7 +135,18 @@ function transcode (options: TranscodeOptions) { let command = ffmpeg(options.inputPath, { niceness: FFMPEG_NICE.TRANSCODING }) .output(options.outputPath) - if (options.hlsPlaylist) { + 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) { command = await buildHLSCommand(command, options) } else { command = await buildx264Command(command, options) @@ -162,6 +174,30 @@ function transcode (options: TranscodeOptions) { }) } +async function canDoQuickTranscode (path: string): Promise { + // NOTE: This could be optimized by running ffprobe only once (but it runs fast anyway) + const videoStream = await getVideoStreamFromFile(path) + const parsedAudio = await audio.get(path) + const fps = await getVideoFileFPS(path) + const bitRate = await getVideoFileBitrate(path) + const resolution = await getVideoFileResolution(path) + + // check video params + if (videoStream[ 'codec_name' ] !== 'h264') return false + if (fps < VIDEO_TRANSCODING_FPS.MIN || fps > VIDEO_TRANSCODING_FPS.MAX) return false + if (bitRate > getMaxBitrate(resolution.videoFileResolution, fps, VIDEO_TRANSCODING_FPS)) return false + + // check audio params (if audio stream exists) + if (parsedAudio.audioStream) { + if (parsedAudio.audioStream[ 'codec_name' ] !== 'aac') return false + + const maxAudioBitrate = audio.bitrate[ 'aac' ](parsedAudio.audioStream[ 'bit_rate' ]) + if (maxAudioBitrate !== -1 && parsedAudio.audioStream[ 'bit_rate' ] > maxAudioBitrate) return false + } + + return true +} + // --------------------------------------------------------------------------- export { @@ -173,7 +209,8 @@ export { getVideoFileFPS, computeResolutionsToTranscode, audio, - getVideoFileBitrate + getVideoFileBitrate, + canDoQuickTranscode } // --------------------------------------------------------------------------- @@ -243,7 +280,7 @@ async function onTranscodingSuccess (options: TranscodeOptions) { await writeFile(options.outputPath, newContent) } -function getVideoFileStream (path: string) { +function getVideoStreamFromFile (path: string) { return new Promise((res, rej) => { ffmpeg.ffprobe(path, (err, metadata) => { if (err) return rej(err) diff --git a/server/lib/video-transcoding.ts b/server/lib/video-transcoding.ts index 0fe0ff12a..8e906a1eb 100644 --- a/server/lib/video-transcoding.ts +++ b/server/lib/video-transcoding.ts @@ -1,6 +1,6 @@ import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION, WEBSERVER } from '../initializers/constants' import { join } from 'path' -import { getVideoFileFPS, transcode } from '../helpers/ffmpeg-utils' +import { getVideoFileFPS, transcode, canDoQuickTranscode } from '../helpers/ffmpeg-utils' import { ensureDir, move, remove, stat } from 'fs-extra' import { logger } from '../helpers/logger' import { VideoResolution } from '../../shared/models/videos' @@ -11,6 +11,9 @@ import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-pla import { VideoStreamingPlaylistType } from '../../shared/models/videos/video-streaming-playlist.type' import { CONFIG } from '../initializers/config' +/** + * Optimize the original video file and replace it. The resolution is not changed. + */ async function optimizeVideofile (video: VideoModel, inputVideoFileArg?: VideoFileModel) { const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR const newExtname = '.mp4' @@ -19,10 +22,13 @@ async function optimizeVideofile (video: VideoModel, inputVideoFileArg?: VideoFi const videoInputPath = join(videosDirectory, video.getVideoFilename(inputVideoFile)) const videoTranscodedPath = join(videosDirectory, video.id + '-transcoded' + newExtname) + const doQuickTranscode = await(canDoQuickTranscode(videoInputPath)) + const transcodeOptions = { inputPath: videoInputPath, outputPath: videoTranscodedPath, - resolution: inputVideoFile.resolution + resolution: inputVideoFile.resolution, + doQuickTranscode } // Could be very long! @@ -52,6 +58,9 @@ async function optimizeVideofile (video: VideoModel, inputVideoFileArg?: VideoFi } } +/** + * Transcode the original video file to a lower resolution. + */ async function transcodeOriginalVideofile (video: VideoModel, resolution: VideoResolution, isPortrait: boolean) { const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR const extname = '.mp4' diff --git a/server/tests/api/videos/video-transcoder.ts b/server/tests/api/videos/video-transcoder.ts index 45a8c09f0..cfd0c8430 100644 --- a/server/tests/api/videos/video-transcoder.ts +++ b/server/tests/api/videos/video-transcoder.ts @@ -4,7 +4,7 @@ import * as chai from 'chai' import 'mocha' import { omit } from 'lodash' import { getMaxBitrate, VideoDetails, VideoResolution, VideoState } from '../../../../shared/models/videos' -import { audio, getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils' +import { audio, canDoQuickTranscode, getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils' import { buildAbsoluteFixturePath, cleanupTests, @@ -18,10 +18,10 @@ import { ServerInfo, setAccessTokensToServers, uploadVideo, + waitJobs, webtorrentAdd } from '../../../../shared/extra-utils' import { join } from 'path' -import { waitJobs } from '../../../../shared/extra-utils/server/jobs' import { VIDEO_TRANSCODING_FPS } from '../../../../server/initializers/constants' const expect = chai.expect @@ -324,6 +324,15 @@ describe('Test video transcoding', function () { it('Should accept and transcode additional extensions', async function () { this.timeout(300000) + let tempFixturePath: string + + { + tempFixturePath = await generateHighBitrateVideo() + + const bitrate = await getVideoFileBitrate(tempFixturePath) + expect(bitrate).to.be.above(getMaxBitrate(VideoResolution.H_1080P, 60, VIDEO_TRANSCODING_FPS)) + } + for (const fixture of [ 'video_short.mkv', 'video_short.avi' ]) { const videoAttributes = { name: fixture, @@ -349,6 +358,13 @@ describe('Test video transcoding', function () { } }) + it('Should correctly detect if quick transcode is possible', async function () { + this.timeout(10000) + + expect(await canDoQuickTranscode(buildAbsoluteFixturePath('video_short.mp4'))).to.be.true + expect(await canDoQuickTranscode(buildAbsoluteFixturePath('video_short.webm'))).to.be.false + }) + after(async function () { await cleanupTests(servers) })