import * as ffmpeg from 'fluent-ffmpeg'
-import { VideoFileMetadata } from '@shared/models/videos/video-file-metadata'
-import { getMaxBitrate, VideoResolution } from '../../shared/models/videos'
+import { getMaxBitrate, VideoFileMetadata, VideoResolution } from '../../shared/models/videos'
import { CONFIG } from '../initializers/config'
import { VIDEO_TRANSCODING_FPS } from '../initializers/constants'
import { logger } from './logger'
+/**
+ *
+ * Helpers to run ffprobe and extract data from the JSON output
+ *
+ */
+
function ffprobePromise (path: string) {
return new Promise<ffmpeg.FfprobeData>((res, rej) => {
ffmpeg.ffprobe(path, (err, data) => {
}
function getMaxAudioBitrate (type: 'aac' | 'mp3' | string, bitrate: number) {
- const baseKbitrate = 384
- const toBits = (kbits: number) => kbits * 8000
+ const maxKBitrate = 384
+ const kToBits = (kbits: number) => kbits * 1000
+
+ // If we did not manage to get the bitrate, use an average value
+ if (!bitrate) return 256
if (type === 'aac') {
switch (true) {
- case bitrate > toBits(baseKbitrate):
- return baseKbitrate
+ case bitrate > kToBits(maxKBitrate):
+ return maxKBitrate
default:
return -1 // we interpret it as a signal to copy the audio stream as is
}
}
- if (type === 'mp3') {
- /*
- a 192kbit/sec mp3 doesn't hold as much information as a 192kbit/sec aac.
- That's why, when using aac, we can go to lower kbit/sec. The equivalences
- made here are not made to be accurate, especially with good mp3 encoders.
- */
- switch (true) {
- case bitrate <= toBits(192):
- return 128
+ /*
+ a 192kbit/sec mp3 doesn't hold as much information as a 192kbit/sec aac.
+ That's why, when using aac, we can go to lower kbit/sec. The equivalences
+ made here are not made to be accurate, especially with good mp3 encoders.
+ */
+ switch (true) {
+ case bitrate <= kToBits(192):
+ return 128
- case bitrate <= toBits(384):
- return 256
+ case bitrate <= kToBits(384):
+ return 256
- default:
- return baseKbitrate
- }
+ default:
+ return maxKBitrate
}
-
- return undefined
}
async function getVideoStreamSize (path: string, existingProbe?: ffmpeg.FfprobeData) {
const videoCodec = videoStream.codec_tag_string
+ if (videoCodec === 'vp09') return 'vp09.00.50.08'
+ if (videoCodec === 'hev1') return 'hev1.1.6.L93.B0'
+
const baseProfileMatrix = {
- High: '6400',
- Main: '4D40',
- Baseline: '42E0'
+ avc1: {
+ High: '6400',
+ Main: '4D40',
+ Baseline: '42E0'
+ },
+ av01: {
+ High: '1',
+ Main: '0',
+ Professional: '2'
+ }
}
- let baseProfile = baseProfileMatrix[videoStream.profile]
+ let baseProfile = baseProfileMatrix[videoCodec][videoStream.profile]
if (!baseProfile) {
logger.warn('Cannot get video profile codec of %s.', path, { videoStream })
- baseProfile = baseProfileMatrix['High'] // Fallback
+ baseProfile = baseProfileMatrix[videoCodec]['High'] // Fallback
}
+ if (videoCodec === 'av01') {
+ const level = videoStream.level
+
+ // Guess the tier indicator and bit depth
+ return `${videoCodec}.${baseProfile}.${level}M.08`
+ }
+
+ // Default, h264 codec
let level = videoStream.level.toString(16)
if (level.length === 1) level = `0${level}`
if (!audioStream) return ''
- const audioCodec = audioStream.codec_name
- if (audioCodec === 'aac') return 'mp4a.40.2'
+ const audioCodecName = audioStream.codec_name
+
+ if (audioCodecName === 'opus') return 'opus'
+ if (audioCodecName === 'vorbis') return 'vorbis'
+ if (audioCodecName === 'aac') return 'mp4a.40.2'
logger.warn('Cannot get audio codec of %s.', path, { audioStream })
async function getDurationFromVideoFile (path: string, existingProbe?: ffmpeg.FfprobeData) {
const metadata = await getMetadataFromFile(path, existingProbe)
- return Math.floor(metadata.format.duration)
+ return Math.round(metadata.format.duration)
}
async function getVideoStreamFromFile (path: string, existingProbe?: ffmpeg.FfprobeData) {
VideoResolution.H_720P,
VideoResolution.H_240P,
VideoResolution.H_1080P,
+ VideoResolution.H_1440P,
VideoResolution.H_4K
]
}
async function canDoQuickTranscode (path: string): Promise<boolean> {
+ if (CONFIG.TRANSCODING.PROFILE !== 'default') return false
+
const probe = await ffprobePromise(path)
- // NOTE: This could be optimized by running ffprobe only once (but it runs fast anyway)
+ return await canDoQuickVideoTranscode(path, probe) &&
+ await canDoQuickAudioTranscode(path, probe)
+}
+
+async function canDoQuickVideoTranscode (path: string, probe?: ffmpeg.FfprobeData): Promise<boolean> {
const videoStream = await getVideoStreamFromFile(path, probe)
- const parsedAudio = await getAudioStream(path, probe)
const fps = await getVideoFileFPS(path, probe)
const bitRate = await getVideoFileBitrate(path, probe)
const resolution = await getVideoFileResolution(path, probe)
+ // If ffprobe did not manage to guess the bitrate
+ if (!bitRate) return false
+
// check video params
if (videoStream == null) return false
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
+ return true
+}
+
+async function canDoQuickAudioTranscode (path: string, probe?: ffmpeg.FfprobeData): Promise<boolean> {
+ const parsedAudio = await getAudioStream(path, probe)
+
+ if (!parsedAudio.audioStream) return true
- const audioBitrate = parsedAudio.bitrate
+ if (parsedAudio.audioStream['codec_name'] !== 'aac') return false
- const maxAudioBitrate = getMaxAudioBitrate('aac', audioBitrate)
- if (maxAudioBitrate !== -1 && audioBitrate > maxAudioBitrate) return false
- }
+ const audioBitrate = parsedAudio.bitrate
+ if (!audioBitrate) return false
+
+ const maxAudioBitrate = getMaxAudioBitrate('aac', audioBitrate)
+ if (maxAudioBitrate !== -1 && audioBitrate > maxAudioBitrate) return false
+
+ const channelLayout = parsedAudio.audioStream['channel_layout']
+ // Causes playback issues with Chrome
+ if (!channelLayout || channelLayout === 'unknown') return false
return true
}
.sort((a, b) => fps % a - fps % b)[0]
}
+function computeFPS (fpsArg: number, resolution: VideoResolution) {
+ let fps = fpsArg
+
+ if (
+ // On small/medium resolutions, limit FPS
+ resolution !== undefined &&
+ resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN &&
+ fps > VIDEO_TRANSCODING_FPS.AVERAGE
+ ) {
+ // Get closest standard framerate by modulo: downsampling has to be done to a divisor of the nominal fps value
+ fps = getClosestFramerateStandard(fps, 'STANDARD')
+ }
+
+ // Hard FPS limits
+ if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = getClosestFramerateStandard(fps, 'HD_STANDARD')
+ else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN
+
+ return fps
+}
+
// ---------------------------------------------------------------------------
export {
getVideoFileResolution,
getMetadataFromFile,
getMaxAudioBitrate,
+ getVideoStreamFromFile,
getDurationFromVideoFile,
getAudioStream,
+ computeFPS,
getVideoFileFPS,
+ ffprobePromise,
getClosestFramerateStandard,
computeResolutionsToTranscode,
getVideoFileBitrate,
- canDoQuickTranscode
+ canDoQuickTranscode,
+ canDoQuickVideoTranscode,
+ canDoQuickAudioTranscode
}