]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blobdiff - server/helpers/ffprobe-utils.ts
Prevent caption listing of private videos
[github/Chocobozzz/PeerTube.git] / server / helpers / ffprobe-utils.ts
index d03ab91ac93c23bcdcaca0b0e6c080eb8b0e150e..e15628e2a7ff428251eab933e803e8cc1b800717 100644 (file)
@@ -1,13 +1,19 @@
-import * as ffmpeg from 'fluent-ffmpeg'
-import { VideoFileMetadata } from '@shared/models/videos/video-file-metadata'
-import { getMaxBitrate, VideoResolution } from '../../shared/models/videos'
+import { ffprobe, FfprobeData } from 'fluent-ffmpeg'
+import { getMaxBitrate } from '@shared/core-utils'
+import { VideoFileMetadata, VideoResolution, VideoTranscodingFPS } 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) => {
+  return new Promise<FfprobeData>((res, rej) => {
+    ffprobe(path, (err, data) => {
       if (err) return rej(err)
 
       return res(data)
@@ -15,7 +21,7 @@ function ffprobePromise (path: string) {
   })
 }
 
-async function getAudioStream (videoPath: string, existingProbe?: ffmpeg.FfprobeData) {
+async function getAudioStream (videoPath: string, existingProbe?: FfprobeData) {
   // 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
@@ -70,7 +76,7 @@ function getMaxAudioBitrate (type: 'aac' | 'mp3' | string, bitrate: number) {
   }
 }
 
-async function getVideoStreamSize (path: string, existingProbe?: ffmpeg.FfprobeData) {
+async function getVideoStreamSize (path: string, existingProbe?: FfprobeData): Promise<{ width: number, height: number }> {
   const videoStream = await getVideoStreamFromFile(path, existingProbe)
 
   return videoStream === null
@@ -85,47 +91,71 @@ async function getVideoStreamCodec (path: string) {
 
   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}`
 
   return `${videoCodec}.${baseProfile}${level}`
 }
 
-async function getAudioStreamCodec (path: string, existingProbe?: ffmpeg.FfprobeData) {
+async function getAudioStreamCodec (path: string, existingProbe?: FfprobeData) {
   const { audioStream } = await getAudioStream(path, existingProbe)
 
   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 })
 
   return 'mp4a.40.2' // Fallback
 }
 
-async function getVideoFileResolution (path: string, existingProbe?: ffmpeg.FfprobeData) {
+async function getVideoFileResolution (path: string, existingProbe?: FfprobeData) {
   const size = await getVideoStreamSize(path, existingProbe)
 
   return {
-    videoFileResolution: Math.min(size.height, size.width),
+    width: size.width,
+    height: size.height,
+    ratio: Math.max(size.height, size.width) / Math.min(size.height, size.width),
+    resolution: Math.min(size.height, size.width),
     isPortraitMode: size.height > size.width
   }
 }
 
-async function getVideoFileFPS (path: string, existingProbe?: ffmpeg.FfprobeData) {
+async function getVideoFileFPS (path: string, existingProbe?: FfprobeData) {
   const videoStream = await getVideoStreamFromFile(path, existingProbe)
   if (videoStream === null) return 0
 
@@ -143,31 +173,40 @@ async function getVideoFileFPS (path: string, existingProbe?: ffmpeg.FfprobeData
   return 0
 }
 
-async function getMetadataFromFile (path: string, existingProbe?: ffmpeg.FfprobeData) {
+async function getMetadataFromFile (path: string, existingProbe?: FfprobeData) {
   const metadata = existingProbe || await ffprobePromise(path)
 
   return new VideoFileMetadata(metadata)
 }
 
-async function getVideoFileBitrate (path: string, existingProbe?: ffmpeg.FfprobeData) {
+async function getVideoFileBitrate (path: string, existingProbe?: FfprobeData): Promise<number> {
   const metadata = await getMetadataFromFile(path, existingProbe)
 
-  return metadata.format.bit_rate as number
+  let bitrate = metadata.format.bit_rate as number
+  if (bitrate && !isNaN(bitrate)) return bitrate
+
+  const videoStream = await getVideoStreamFromFile(path, existingProbe)
+  if (!videoStream) return undefined
+
+  bitrate = videoStream?.bit_rate
+  if (bitrate && !isNaN(bitrate)) return bitrate
+
+  return undefined
 }
 
-async function getDurationFromVideoFile (path: string, existingProbe?: ffmpeg.FfprobeData) {
+async function getDurationFromVideoFile (path: string, existingProbe?: 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) {
+async function getVideoStreamFromFile (path: string, existingProbe?: FfprobeData) {
   const metadata = await getMetadataFromFile(path, existingProbe)
 
   return metadata.streams.find(s => s.codec_type === 'video') || null
 }
 
-function computeResolutionsToTranscode (videoFileResolution: number, type: 'vod' | 'live') {
+function computeLowerResolutionsToTranscode (videoFileResolution: number, type: 'vod' | 'live') {
   const configResolutions = type === 'vod'
     ? CONFIG.TRANSCODING.RESOLUTIONS
     : CONFIG.LIVE.TRANSCODING.RESOLUTIONS
@@ -175,13 +214,15 @@ function computeResolutionsToTranscode (videoFileResolution: number, type: 'vod'
   const resolutionsEnabled: number[] = []
 
   // Put in the order we want to proceed jobs
-  const resolutions = [
+  const resolutions: VideoResolution[] = [
     VideoResolution.H_NOVIDEO,
     VideoResolution.H_480P,
     VideoResolution.H_360P,
     VideoResolution.H_720P,
     VideoResolution.H_240P,
+    VideoResolution.H_144P,
     VideoResolution.H_1080P,
+    VideoResolution.H_1440P,
     VideoResolution.H_4K
   ]
 
@@ -195,17 +236,19 @@ function computeResolutionsToTranscode (videoFileResolution: number, type: 'vod'
 }
 
 async function canDoQuickTranscode (path: string): Promise<boolean> {
+  if (CONFIG.TRANSCODING.PROFILE !== 'default') return false
+
   const probe = await ffprobePromise(path)
 
   return await canDoQuickVideoTranscode(path, probe) &&
          await canDoQuickAudioTranscode(path, probe)
 }
 
-async function canDoQuickVideoTranscode (path: string, probe?: ffmpeg.FfprobeData): Promise<boolean> {
+async function canDoQuickVideoTranscode (path: string, probe?: FfprobeData): Promise<boolean> {
   const videoStream = await getVideoStreamFromFile(path, probe)
   const fps = await getVideoFileFPS(path, probe)
   const bitRate = await getVideoFileBitrate(path, probe)
-  const resolution = await getVideoFileResolution(path, probe)
+  const resolutionData = await getVideoFileResolution(path, probe)
 
   // If ffprobe did not manage to guess the bitrate
   if (!bitRate) return false
@@ -215,12 +258,12 @@ async function canDoQuickVideoTranscode (path: string, probe?: ffmpeg.FfprobeDat
   if (videoStream['codec_name'] !== 'h264') return false
   if (videoStream['pix_fmt'] !== 'yuv420p') 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
+  if (bitRate > getMaxBitrate({ ...resolutionData, fps })) return false
 
   return true
 }
 
-async function canDoQuickAudioTranscode (path: string, probe?: ffmpeg.FfprobeData): Promise<boolean> {
+async function canDoQuickAudioTranscode (path: string, probe?: FfprobeData): Promise<boolean> {
   const parsedAudio = await getAudioStream(path, probe)
 
   if (!parsedAudio.audioStream) return true
@@ -233,14 +276,41 @@ async function canDoQuickAudioTranscode (path: string, probe?: ffmpeg.FfprobeDat
   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
 }
 
-function getClosestFramerateStandard (fps: number, type: 'HD_STANDARD' | 'STANDARD'): number {
+function getClosestFramerateStandard <K extends keyof Pick<VideoTranscodingFPS, 'HD_STANDARD' | 'STANDARD'>> (fps: number, type: K) {
   return VIDEO_TRANSCODING_FPS[type].slice(0)
                                     .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')
+
+  if (fps < VIDEO_TRANSCODING_FPS.MIN) {
+    throw new Error(`Cannot compute FPS because ${fps} is lower than our minimum value ${VIDEO_TRANSCODING_FPS.MIN}`)
+  }
+
+  return fps
+}
+
 // ---------------------------------------------------------------------------
 
 export {
@@ -253,10 +323,11 @@ export {
   getVideoStreamFromFile,
   getDurationFromVideoFile,
   getAudioStream,
+  computeFPS,
   getVideoFileFPS,
   ffprobePromise,
   getClosestFramerateStandard,
-  computeResolutionsToTranscode,
+  computeLowerResolutionsToTranscode,
   getVideoFileBitrate,
   canDoQuickTranscode,
   canDoQuickVideoTranscode,