diff options
-rw-r--r-- | CHANGELOG.md | 13 | ||||
-rw-r--r-- | server/helpers/ffmpeg-utils.ts | 53 | ||||
-rw-r--r-- | server/initializers/constants.ts | 2 | ||||
-rw-r--r-- | server/tests/api/fixtures/video_60fps_short.mp4 | bin | 0 -> 33968 bytes | |||
-rw-r--r-- | server/tests/api/videos/video-transcoder.ts | 33 |
5 files changed, 89 insertions, 12 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f919fef2..d780c2396 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md | |||
@@ -1,5 +1,18 @@ | |||
1 | # Changelog | 1 | # Changelog |
2 | 2 | ||
3 | ## v0.0.28-alpha | ||
4 | |||
5 | ### BREAKING CHANGES | ||
6 | |||
7 | * Enable original file transcoding by default in configuration | ||
8 | * Disable transcoding in other definitions in configuration | ||
9 | |||
10 | ### Features | ||
11 | |||
12 | * Fallback to HTTP if video cannot be loaded | ||
13 | * Limit to 30 FPS in transcoding | ||
14 | |||
15 | |||
3 | ## v0.0.27-alpha | 16 | ## v0.0.27-alpha |
4 | 17 | ||
5 | ### Features | 18 | ### Features |
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts index c2581f460..ad6f2f867 100644 --- a/server/helpers/ffmpeg-utils.ts +++ b/server/helpers/ffmpeg-utils.ts | |||
@@ -1,16 +1,27 @@ | |||
1 | import * as ffmpeg from 'fluent-ffmpeg' | 1 | import * as ffmpeg from 'fluent-ffmpeg' |
2 | import { VideoResolution } from '../../shared/models/videos' | 2 | import { VideoResolution } from '../../shared/models/videos' |
3 | import { CONFIG } from '../initializers' | 3 | import { CONFIG, MAX_VIDEO_TRANSCODING_FPS } from '../initializers' |
4 | 4 | ||
5 | function getVideoFileHeight (path: string) { | 5 | async function getVideoFileHeight (path: string) { |
6 | return new Promise<number>((res, rej) => { | 6 | const videoStream = await getVideoFileStream(path) |
7 | ffmpeg.ffprobe(path, (err, metadata) => { | 7 | return videoStream.height |
8 | if (err) return rej(err) | 8 | } |
9 | 9 | ||
10 | const videoStream = metadata.streams.find(s => s.codec_type === 'video') | 10 | async function getVideoFileFPS (path: string) { |
11 | return res(videoStream.height) | 11 | const videoStream = await getVideoFileStream(path) |
12 | }) | 12 | |
13 | }) | 13 | for (const key of [ 'r_frame_rate' , 'avg_frame_rate' ]) { |
14 | const valuesText: string = videoStream[key] | ||
15 | if (!valuesText) continue | ||
16 | |||
17 | const [ frames, seconds ] = valuesText.split('/') | ||
18 | if (!frames || !seconds) continue | ||
19 | |||
20 | const result = parseInt(frames, 10) / parseInt(seconds, 10) | ||
21 | if (result > 0) return result | ||
22 | } | ||
23 | |||
24 | return 0 | ||
14 | } | 25 | } |
15 | 26 | ||
16 | function getDurationFromVideoFile (path: string) { | 27 | function getDurationFromVideoFile (path: string) { |
@@ -49,7 +60,9 @@ type TranscodeOptions = { | |||
49 | } | 60 | } |
50 | 61 | ||
51 | function transcode (options: TranscodeOptions) { | 62 | function transcode (options: TranscodeOptions) { |
52 | return new Promise<void>((res, rej) => { | 63 | return new Promise<void>(async (res, rej) => { |
64 | const fps = await getVideoFileFPS(options.inputPath) | ||
65 | |||
53 | let command = ffmpeg(options.inputPath) | 66 | let command = ffmpeg(options.inputPath) |
54 | .output(options.outputPath) | 67 | .output(options.outputPath) |
55 | .videoCodec('libx264') | 68 | .videoCodec('libx264') |
@@ -57,6 +70,8 @@ function transcode (options: TranscodeOptions) { | |||
57 | .outputOption('-movflags faststart') | 70 | .outputOption('-movflags faststart') |
58 | // .outputOption('-crf 18') | 71 | // .outputOption('-crf 18') |
59 | 72 | ||
73 | if (fps > MAX_VIDEO_TRANSCODING_FPS) command = command.withFPS(MAX_VIDEO_TRANSCODING_FPS) | ||
74 | |||
60 | if (options.resolution !== undefined) { | 75 | if (options.resolution !== undefined) { |
61 | const size = `?x${options.resolution}` // '?x720' for example | 76 | const size = `?x${options.resolution}` // '?x720' for example |
62 | command = command.size(size) | 77 | command = command.size(size) |
@@ -74,5 +89,21 @@ export { | |||
74 | getVideoFileHeight, | 89 | getVideoFileHeight, |
75 | getDurationFromVideoFile, | 90 | getDurationFromVideoFile, |
76 | generateImageFromVideoFile, | 91 | generateImageFromVideoFile, |
77 | transcode | 92 | transcode, |
93 | getVideoFileFPS | ||
94 | } | ||
95 | |||
96 | // --------------------------------------------------------------------------- | ||
97 | |||
98 | function getVideoFileStream (path: string) { | ||
99 | return new Promise<any>((res, rej) => { | ||
100 | ffmpeg.ffprobe(path, (err, metadata) => { | ||
101 | if (err) return rej(err) | ||
102 | |||
103 | const videoStream = metadata.streams.find(s => s.codec_type === 'video') | ||
104 | if (!videoStream) throw new Error('Cannot find video stream of ' + path) | ||
105 | |||
106 | return res(videoStream) | ||
107 | }) | ||
108 | }) | ||
78 | } | 109 | } |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 2dc73770d..318df48bf 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -232,6 +232,7 @@ const CONSTRAINTS_FIELDS = { | |||
232 | } | 232 | } |
233 | 233 | ||
234 | let VIDEO_VIEW_LIFETIME = 60000 * 60 // 1 hour | 234 | let VIDEO_VIEW_LIFETIME = 60000 * 60 // 1 hour |
235 | const MAX_VIDEO_TRANSCODING_FPS = 30 | ||
235 | 236 | ||
236 | const VIDEO_RATE_TYPES: { [ id: string ]: VideoRateType } = { | 237 | const VIDEO_RATE_TYPES: { [ id: string ]: VideoRateType } = { |
237 | LIKE: 'like', | 238 | LIKE: 'like', |
@@ -442,6 +443,7 @@ export { | |||
442 | VIDEO_LICENCES, | 443 | VIDEO_LICENCES, |
443 | VIDEO_RATE_TYPES, | 444 | VIDEO_RATE_TYPES, |
444 | VIDEO_MIMETYPE_EXT, | 445 | VIDEO_MIMETYPE_EXT, |
446 | MAX_VIDEO_TRANSCODING_FPS, | ||
445 | USER_PASSWORD_RESET_LIFETIME, | 447 | USER_PASSWORD_RESET_LIFETIME, |
446 | IMAGE_MIMETYPE_EXT, | 448 | IMAGE_MIMETYPE_EXT, |
447 | SCHEDULER_INTERVAL, | 449 | 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 index 000000000..ff0593cf3 --- /dev/null +++ b/server/tests/api/fixtures/video_60fps_short.mp4 | |||
Binary files differ | |||
diff --git a/server/tests/api/videos/video-transcoder.ts b/server/tests/api/videos/video-transcoder.ts index c494e7f67..ef929960d 100644 --- a/server/tests/api/videos/video-transcoder.ts +++ b/server/tests/api/videos/video-transcoder.ts | |||
@@ -2,10 +2,13 @@ | |||
2 | 2 | ||
3 | import * as chai from 'chai' | 3 | import * as chai from 'chai' |
4 | import 'mocha' | 4 | import 'mocha' |
5 | import { VideoDetails } from '../../../../shared/models/videos' | ||
6 | import { getVideoFileFPS } from '../../../helpers/ffmpeg-utils' | ||
5 | import { | 7 | import { |
6 | flushAndRunMultipleServers, flushTests, getVideo, getVideosList, killallServers, ServerInfo, setAccessTokensToServers, uploadVideo, | 8 | flushAndRunMultipleServers, flushTests, getVideo, getVideosList, killallServers, root, ServerInfo, setAccessTokensToServers, uploadVideo, |
7 | wait, webtorrentAdd | 9 | wait, webtorrentAdd |
8 | } from '../../utils' | 10 | } from '../../utils' |
11 | import { join } from 'path' | ||
9 | 12 | ||
10 | const expect = chai.expect | 13 | const expect = chai.expect |
11 | 14 | ||
@@ -78,6 +81,34 @@ describe('Test video transcoding', function () { | |||
78 | expect(torrent.files[0].path).match(/\.mp4$/) | 81 | expect(torrent.files[0].path).match(/\.mp4$/) |
79 | }) | 82 | }) |
80 | 83 | ||
84 | it('Should transcode to 30 FPS', async function () { | ||
85 | this.timeout(60000) | ||
86 | |||
87 | const videoAttributes = { | ||
88 | name: 'my super 30fps name for server 2', | ||
89 | description: 'my super 30fps description for server 2', | ||
90 | fixture: 'video_60fps_short.mp4' | ||
91 | } | ||
92 | await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes) | ||
93 | |||
94 | await wait(20000) | ||
95 | |||
96 | const res = await getVideosList(servers[1].url) | ||
97 | |||
98 | const video = res.body.data[0] | ||
99 | const res2 = await getVideo(servers[1].url, video.id) | ||
100 | const videoDetails: VideoDetails = res2.body | ||
101 | |||
102 | expect(videoDetails.files).to.have.lengthOf(1) | ||
103 | |||
104 | for (const resolution of [ '240' ]) { | ||
105 | const path = join(root(), 'test2', 'videos', video.uuid + '-' + resolution + '.mp4') | ||
106 | const fps = await getVideoFileFPS(path) | ||
107 | |||
108 | expect(fps).to.be.below(31) | ||
109 | } | ||
110 | }) | ||
111 | |||
81 | after(async function () { | 112 | after(async function () { |
82 | killallServers(servers) | 113 | killallServers(servers) |
83 | 114 | ||