Add 30 fps limit in transcoding
authorChocobozzz <me@florianbigard.com>
Mon, 26 Feb 2018 09:48:53 +0000 (10:48 +0100)
committerChocobozzz <me@florianbigard.com>
Mon, 26 Feb 2018 10:08:38 +0000 (11:08 +0100)
CHANGELOG.md
server/helpers/ffmpeg-utils.ts
server/initializers/constants.ts
server/tests/api/fixtures/video_60fps_short.mp4 [new file with mode: 0644]
server/tests/api/videos/video-transcoder.ts

index 9f919fef2d358965c92dbe3e7585a8df291a1f75..d780c2396193ac2d625b1cd641bb008cdd392cc4 100644 (file)
@@ -1,5 +1,18 @@
 # Changelog
 
+## v0.0.28-alpha
+
+### BREAKING CHANGES
+
+ * Enable original file transcoding by default in configuration
+ * Disable transcoding in other definitions in configuration
+
+### Features
+
+ * Fallback to HTTP if video cannot be loaded
+ * Limit to 30 FPS in transcoding
+
+
 ## v0.0.27-alpha
 
 ### Features
index c2581f4602315bef1b01349b8a38a6d90fc78328..ad6f2f867f657ab916e7f506893ceef4d43589f6 100644 (file)
@@ -1,16 +1,27 @@
 import * as ffmpeg from 'fluent-ffmpeg'
 import { VideoResolution } from '../../shared/models/videos'
-import { CONFIG } from '../initializers'
+import { CONFIG, MAX_VIDEO_TRANSCODING_FPS } from '../initializers'
 
-function getVideoFileHeight (path: string) {
-  return new Promise<number>((res, rej) => {
-    ffmpeg.ffprobe(path, (err, metadata) => {
-      if (err) return rej(err)
+async function getVideoFileHeight (path: string) {
+  const videoStream = await getVideoFileStream(path)
+  return videoStream.height
+}
 
-      const videoStream = metadata.streams.find(s => s.codec_type === 'video')
-      return res(videoStream.height)
-    })
-  })
+async function getVideoFileFPS (path: string) {
+  const videoStream = await getVideoFileStream(path)
+
+  for (const key of [ 'r_frame_rate' , 'avg_frame_rate' ]) {
+    const valuesText: string = videoStream[key]
+    if (!valuesText) continue
+
+    const [ frames, seconds ] = valuesText.split('/')
+    if (!frames || !seconds) continue
+
+    const result = parseInt(frames, 10) / parseInt(seconds, 10)
+    if (result > 0) return result
+  }
+
+  return 0
 }
 
 function getDurationFromVideoFile (path: string) {
@@ -49,7 +60,9 @@ type TranscodeOptions = {
 }
 
 function transcode (options: TranscodeOptions) {
-  return new Promise<void>((res, rej) => {
+  return new Promise<void>(async (res, rej) => {
+    const fps = await getVideoFileFPS(options.inputPath)
+
     let command = ffmpeg(options.inputPath)
                     .output(options.outputPath)
                     .videoCodec('libx264')
@@ -57,6 +70,8 @@ function transcode (options: TranscodeOptions) {
                     .outputOption('-movflags faststart')
                     // .outputOption('-crf 18')
 
+    if (fps > MAX_VIDEO_TRANSCODING_FPS) command = command.withFPS(MAX_VIDEO_TRANSCODING_FPS)
+
     if (options.resolution !== undefined) {
       const size = `?x${options.resolution}` // '?x720' for example
       command = command.size(size)
@@ -74,5 +89,21 @@ export {
   getVideoFileHeight,
   getDurationFromVideoFile,
   generateImageFromVideoFile,
-  transcode
+  transcode,
+  getVideoFileFPS
+}
+
+// ---------------------------------------------------------------------------
+
+function getVideoFileStream (path: string) {
+  return new Promise<any>((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)
+
+      return res(videoStream)
+    })
+  })
 }
index 2dc73770dc2b5f6194d372711c8e2899f0f17a8f..318df48bf04af079ce8fa3eb28b4eb389c6eb828 100644 (file)
@@ -232,6 +232,7 @@ const CONSTRAINTS_FIELDS = {
 }
 
 let VIDEO_VIEW_LIFETIME = 60000 * 60 // 1 hour
+const MAX_VIDEO_TRANSCODING_FPS = 30
 
 const VIDEO_RATE_TYPES: { [ id: string ]: VideoRateType } = {
   LIKE: 'like',
@@ -442,6 +443,7 @@ export {
   VIDEO_LICENCES,
   VIDEO_RATE_TYPES,
   VIDEO_MIMETYPE_EXT,
+  MAX_VIDEO_TRANSCODING_FPS,
   USER_PASSWORD_RESET_LIFETIME,
   IMAGE_MIMETYPE_EXT,
   SCHEDULER_INTERVAL,
diff --git a/server/tests/api/fixtures/video_60fps_short.mp4 b/server/tests/api/fixtures/video_60fps_short.mp4
new file mode 100644 (file)
index 0000000..ff0593c
Binary files /dev/null and b/server/tests/api/fixtures/video_60fps_short.mp4 differ
index c494e7f677c2140f0bfb654217a91c2ea0b239ff..ef929960d73ff9bde3d626cd882d81dfd1528a51 100644 (file)
@@ -2,10 +2,13 @@
 
 import * as chai from 'chai'
 import 'mocha'
+import { VideoDetails } from '../../../../shared/models/videos'
+import { getVideoFileFPS } from '../../../helpers/ffmpeg-utils'
 import {
-  flushAndRunMultipleServers, flushTests, getVideo, getVideosList, killallServers, ServerInfo, setAccessTokensToServers, uploadVideo,
+  flushAndRunMultipleServers, flushTests, getVideo, getVideosList, killallServers, root, ServerInfo, setAccessTokensToServers, uploadVideo,
   wait, webtorrentAdd
 } from '../../utils'
+import { join } from 'path'
 
 const expect = chai.expect
 
@@ -78,6 +81,34 @@ describe('Test video transcoding', function () {
     expect(torrent.files[0].path).match(/\.mp4$/)
   })
 
+  it('Should transcode to 30 FPS', async function () {
+    this.timeout(60000)
+
+    const videoAttributes = {
+      name: 'my super 30fps name for server 2',
+      description: 'my super 30fps description for server 2',
+      fixture: 'video_60fps_short.mp4'
+    }
+    await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
+
+    await wait(20000)
+
+    const res = await getVideosList(servers[1].url)
+
+    const video = res.body.data[0]
+    const res2 = await getVideo(servers[1].url, video.id)
+    const videoDetails: VideoDetails = res2.body
+
+    expect(videoDetails.files).to.have.lengthOf(1)
+
+    for (const resolution of [ '240' ]) {
+      const path = join(root(), 'test2', 'videos', video.uuid + '-' + resolution + '.mp4')
+      const fps = await getVideoFileFPS(path)
+
+      expect(fps).to.be.below(31)
+    }
+  })
+
   after(async function () {
     killallServers(servers)