diff options
Diffstat (limited to 'server')
-rw-r--r-- | server/controllers/api/videos/index.ts | 7 | ||||
-rw-r--r-- | server/helpers/custom-validators/videos.ts | 5 | ||||
-rw-r--r-- | server/helpers/ffmpeg-utils.ts | 25 | ||||
-rw-r--r-- | server/initializers/constants.ts | 6 | ||||
-rw-r--r-- | server/initializers/migrations/0225-video-fps.ts | 22 | ||||
-rw-r--r-- | server/models/video/video-file.ts | 15 | ||||
-rw-r--r-- | server/models/video/video.ts | 26 | ||||
-rw-r--r-- | server/tests/api/videos/video-transcoder.ts | 17 | ||||
-rw-r--r-- | server/tests/fixtures/60fps_720p_small.mp4 | bin | 0 -> 276786 bytes | |||
-rw-r--r-- | server/tests/fixtures/video_60fps_short.mp4 | bin | 33968 -> 0 bytes |
10 files changed, 99 insertions, 24 deletions
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index b4ced8c1e..8c93ae89c 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts | |||
@@ -2,7 +2,7 @@ import * as express from 'express' | |||
2 | import { extname, join } from 'path' | 2 | import { extname, join } from 'path' |
3 | import { VideoCreate, VideoPrivacy, VideoState, VideoUpdate } from '../../../../shared' | 3 | import { VideoCreate, VideoPrivacy, VideoState, VideoUpdate } from '../../../../shared' |
4 | import { renamePromise } from '../../../helpers/core-utils' | 4 | import { renamePromise } from '../../../helpers/core-utils' |
5 | import { getVideoFileResolution } from '../../../helpers/ffmpeg-utils' | 5 | import { getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils' |
6 | import { processImage } from '../../../helpers/image-utils' | 6 | import { processImage } from '../../../helpers/image-utils' |
7 | import { logger } from '../../../helpers/logger' | 7 | import { logger } from '../../../helpers/logger' |
8 | import { getFormattedObjects, getServerActor, resetSequelizeInstance } from '../../../helpers/utils' | 8 | import { getFormattedObjects, getServerActor, resetSequelizeInstance } from '../../../helpers/utils' |
@@ -184,10 +184,13 @@ async function addVideo (req: express.Request, res: express.Response) { | |||
184 | 184 | ||
185 | // Build the file object | 185 | // Build the file object |
186 | const { videoFileResolution } = await getVideoFileResolution(videoPhysicalFile.path) | 186 | const { videoFileResolution } = await getVideoFileResolution(videoPhysicalFile.path) |
187 | const fps = await getVideoFileFPS(videoPhysicalFile.path) | ||
188 | |||
187 | const videoFileData = { | 189 | const videoFileData = { |
188 | extname: extname(videoPhysicalFile.filename), | 190 | extname: extname(videoPhysicalFile.filename), |
189 | resolution: videoFileResolution, | 191 | resolution: videoFileResolution, |
190 | size: videoPhysicalFile.size | 192 | size: videoPhysicalFile.size, |
193 | fps | ||
191 | } | 194 | } |
192 | const videoFile = new VideoFileModel(videoFileData) | 195 | const videoFile = new VideoFileModel(videoFileData) |
193 | 196 | ||
diff --git a/server/helpers/custom-validators/videos.ts b/server/helpers/custom-validators/videos.ts index ae392f8c2..672f06dc0 100644 --- a/server/helpers/custom-validators/videos.ts +++ b/server/helpers/custom-validators/videos.ts | |||
@@ -118,6 +118,10 @@ function isVideoFileResolutionValid (value: string) { | |||
118 | return exists(value) && validator.isInt(value + '') | 118 | return exists(value) && validator.isInt(value + '') |
119 | } | 119 | } |
120 | 120 | ||
121 | function isVideoFPSResolutionValid (value: string) { | ||
122 | return value === null || validator.isInt(value + '') | ||
123 | } | ||
124 | |||
121 | function isVideoFileSizeValid (value: string) { | 125 | function isVideoFileSizeValid (value: string) { |
122 | return exists(value) && validator.isInt(value + '', VIDEOS_CONSTRAINTS_FIELDS.FILE_SIZE) | 126 | return exists(value) && validator.isInt(value + '', VIDEOS_CONSTRAINTS_FIELDS.FILE_SIZE) |
123 | } | 127 | } |
@@ -182,6 +186,7 @@ export { | |||
182 | isVideoFileInfoHashValid, | 186 | isVideoFileInfoHashValid, |
183 | isVideoNameValid, | 187 | isVideoNameValid, |
184 | isVideoTagsValid, | 188 | isVideoTagsValid, |
189 | isVideoFPSResolutionValid, | ||
185 | isScheduleVideoUpdatePrivacyValid, | 190 | isScheduleVideoUpdatePrivacyValid, |
186 | isVideoAbuseReasonValid, | 191 | isVideoAbuseReasonValid, |
187 | isVideoFile, | 192 | isVideoFile, |
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts index bfc942fa3..4086335d7 100644 --- a/server/helpers/ffmpeg-utils.ts +++ b/server/helpers/ffmpeg-utils.ts | |||
@@ -26,7 +26,7 @@ async function getVideoFileFPS (path: string) { | |||
26 | if (!frames || !seconds) continue | 26 | if (!frames || !seconds) continue |
27 | 27 | ||
28 | const result = parseInt(frames, 10) / parseInt(seconds, 10) | 28 | const result = parseInt(frames, 10) / parseInt(seconds, 10) |
29 | if (result > 0) return result | 29 | if (result > 0) return Math.round(result) |
30 | } | 30 | } |
31 | 31 | ||
32 | return 0 | 32 | return 0 |
@@ -83,8 +83,6 @@ type TranscodeOptions = { | |||
83 | 83 | ||
84 | function transcode (options: TranscodeOptions) { | 84 | function transcode (options: TranscodeOptions) { |
85 | return new Promise<void>(async (res, rej) => { | 85 | return new Promise<void>(async (res, rej) => { |
86 | const fps = await getVideoFileFPS(options.inputPath) | ||
87 | |||
88 | let command = ffmpeg(options.inputPath) | 86 | let command = ffmpeg(options.inputPath) |
89 | .output(options.outputPath) | 87 | .output(options.outputPath) |
90 | .videoCodec('libx264') | 88 | .videoCodec('libx264') |
@@ -92,14 +90,27 @@ function transcode (options: TranscodeOptions) { | |||
92 | .outputOption('-movflags faststart') | 90 | .outputOption('-movflags faststart') |
93 | // .outputOption('-crf 18') | 91 | // .outputOption('-crf 18') |
94 | 92 | ||
95 | // Our player has some FPS limits | 93 | let fps = await getVideoFileFPS(options.inputPath) |
96 | if (fps > VIDEO_TRANSCODING_FPS.MAX) command = command.withFPS(VIDEO_TRANSCODING_FPS.MAX) | ||
97 | else if (fps < VIDEO_TRANSCODING_FPS.MIN) command = command.withFPS(VIDEO_TRANSCODING_FPS.MIN) | ||
98 | |||
99 | if (options.resolution !== undefined) { | 94 | if (options.resolution !== undefined) { |
100 | // '?x720' or '720x?' for example | 95 | // '?x720' or '720x?' for example |
101 | const size = options.isPortraitMode === true ? `${options.resolution}x?` : `?x${options.resolution}` | 96 | const size = options.isPortraitMode === true ? `${options.resolution}x?` : `?x${options.resolution}` |
102 | command = command.size(size) | 97 | command = command.size(size) |
98 | |||
99 | // On small/medium resolutions, limit FPS | ||
100 | if ( | ||
101 | options.resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN && | ||
102 | fps > VIDEO_TRANSCODING_FPS.AVERAGE | ||
103 | ) { | ||
104 | fps = VIDEO_TRANSCODING_FPS.AVERAGE | ||
105 | } | ||
106 | } | ||
107 | |||
108 | if (fps) { | ||
109 | // Hard FPS limits | ||
110 | if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = VIDEO_TRANSCODING_FPS.MAX | ||
111 | else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN | ||
112 | |||
113 | command = command.withFPS(fps) | ||
103 | } | 114 | } |
104 | 115 | ||
105 | command | 116 | command |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 67df3df80..24b7e2655 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -14,7 +14,7 @@ let config: IConfig = require('config') | |||
14 | 14 | ||
15 | // --------------------------------------------------------------------------- | 15 | // --------------------------------------------------------------------------- |
16 | 16 | ||
17 | const LAST_MIGRATION_VERSION = 220 | 17 | const LAST_MIGRATION_VERSION = 225 |
18 | 18 | ||
19 | // --------------------------------------------------------------------------- | 19 | // --------------------------------------------------------------------------- |
20 | 20 | ||
@@ -282,7 +282,9 @@ const RATES_LIMIT = { | |||
282 | let VIDEO_VIEW_LIFETIME = 60000 * 60 // 1 hour | 282 | let VIDEO_VIEW_LIFETIME = 60000 * 60 // 1 hour |
283 | const VIDEO_TRANSCODING_FPS = { | 283 | const VIDEO_TRANSCODING_FPS = { |
284 | MIN: 10, | 284 | MIN: 10, |
285 | MAX: 30 | 285 | AVERAGE: 30, |
286 | MAX: 60, | ||
287 | KEEP_ORIGIN_FPS_RESOLUTION_MIN: 720 // We keep the original FPS on high resolutions (720 minimum) | ||
286 | } | 288 | } |
287 | 289 | ||
288 | const VIDEO_RATE_TYPES: { [ id: string ]: VideoRateType } = { | 290 | const VIDEO_RATE_TYPES: { [ id: string ]: VideoRateType } = { |
diff --git a/server/initializers/migrations/0225-video-fps.ts b/server/initializers/migrations/0225-video-fps.ts new file mode 100644 index 000000000..733676845 --- /dev/null +++ b/server/initializers/migrations/0225-video-fps.ts | |||
@@ -0,0 +1,22 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async function up (utils: { | ||
4 | transaction: Sequelize.Transaction | ||
5 | queryInterface: Sequelize.QueryInterface | ||
6 | sequelize: Sequelize.Sequelize | ||
7 | }): Promise<void> { | ||
8 | { | ||
9 | const data = { | ||
10 | type: Sequelize.INTEGER, | ||
11 | allowNull: true, | ||
12 | defaultValue: null | ||
13 | } | ||
14 | await utils.queryInterface.addColumn('videoFile', 'fps', data) | ||
15 | } | ||
16 | } | ||
17 | |||
18 | function down (options) { | ||
19 | throw new Error('Not implemented.') | ||
20 | } | ||
21 | |||
22 | export { up, down } | ||
diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts index df4067a4e..372d18d69 100644 --- a/server/models/video/video-file.ts +++ b/server/models/video/video-file.ts | |||
@@ -1,6 +1,11 @@ | |||
1 | import { values } from 'lodash' | 1 | import { values } from 'lodash' |
2 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' | 2 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' |
3 | import { isVideoFileInfoHashValid, isVideoFileResolutionValid, isVideoFileSizeValid } from '../../helpers/custom-validators/videos' | 3 | import { |
4 | isVideoFileInfoHashValid, | ||
5 | isVideoFileResolutionValid, | ||
6 | isVideoFileSizeValid, | ||
7 | isVideoFPSResolutionValid | ||
8 | } from '../../helpers/custom-validators/videos' | ||
4 | import { CONSTRAINTS_FIELDS } from '../../initializers' | 9 | import { CONSTRAINTS_FIELDS } from '../../initializers' |
5 | import { throwIfNotValid } from '../utils' | 10 | import { throwIfNotValid } from '../utils' |
6 | import { VideoModel } from './video' | 11 | import { VideoModel } from './video' |
@@ -42,6 +47,12 @@ export class VideoFileModel extends Model<VideoFileModel> { | |||
42 | @Column | 47 | @Column |
43 | infoHash: string | 48 | infoHash: string |
44 | 49 | ||
50 | @AllowNull(true) | ||
51 | @Default(null) | ||
52 | @Is('VideoFileFPS', value => throwIfNotValid(value, isVideoFPSResolutionValid, 'fps')) | ||
53 | @Column | ||
54 | fps: number | ||
55 | |||
45 | @ForeignKey(() => VideoModel) | 56 | @ForeignKey(() => VideoModel) |
46 | @Column | 57 | @Column |
47 | videoId: number | 58 | videoId: number |
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 5d8089328..ab33b7c99 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -52,7 +52,7 @@ import { | |||
52 | isVideoStateValid, | 52 | isVideoStateValid, |
53 | isVideoSupportValid | 53 | isVideoSupportValid |
54 | } from '../../helpers/custom-validators/videos' | 54 | } from '../../helpers/custom-validators/videos' |
55 | import { generateImageFromVideoFile, getVideoFileResolution, transcode } from '../../helpers/ffmpeg-utils' | 55 | import { generateImageFromVideoFile, getVideoFileFPS, getVideoFileResolution, transcode } from '../../helpers/ffmpeg-utils' |
56 | import { logger } from '../../helpers/logger' | 56 | import { logger } from '../../helpers/logger' |
57 | import { getServerActor } from '../../helpers/utils' | 57 | import { getServerActor } from '../../helpers/utils' |
58 | import { | 58 | import { |
@@ -1168,6 +1168,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1168 | }, | 1168 | }, |
1169 | magnetUri: this.generateMagnetUri(videoFile, baseUrlHttp, baseUrlWs), | 1169 | magnetUri: this.generateMagnetUri(videoFile, baseUrlHttp, baseUrlWs), |
1170 | size: videoFile.size, | 1170 | size: videoFile.size, |
1171 | fps: videoFile.fps, | ||
1171 | torrentUrl: this.getTorrentUrl(videoFile, baseUrlHttp), | 1172 | torrentUrl: this.getTorrentUrl(videoFile, baseUrlHttp), |
1172 | torrentDownloadUrl: this.getTorrentDownloadUrl(videoFile, baseUrlHttp), | 1173 | torrentDownloadUrl: this.getTorrentDownloadUrl(videoFile, baseUrlHttp), |
1173 | fileUrl: this.getVideoFileUrl(videoFile, baseUrlHttp), | 1174 | fileUrl: this.getVideoFileUrl(videoFile, baseUrlHttp), |
@@ -1303,11 +1304,11 @@ export class VideoModel extends Model<VideoModel> { | |||
1303 | const newExtname = '.mp4' | 1304 | const newExtname = '.mp4' |
1304 | const inputVideoFile = this.getOriginalFile() | 1305 | const inputVideoFile = this.getOriginalFile() |
1305 | const videoInputPath = join(videosDirectory, this.getVideoFilename(inputVideoFile)) | 1306 | const videoInputPath = join(videosDirectory, this.getVideoFilename(inputVideoFile)) |
1306 | const videoOutputPath = join(videosDirectory, this.id + '-transcoded' + newExtname) | 1307 | const videoTranscodedPath = join(videosDirectory, this.id + '-transcoded' + newExtname) |
1307 | 1308 | ||
1308 | const transcodeOptions = { | 1309 | const transcodeOptions = { |
1309 | inputPath: videoInputPath, | 1310 | inputPath: videoInputPath, |
1310 | outputPath: videoOutputPath | 1311 | outputPath: videoTranscodedPath |
1311 | } | 1312 | } |
1312 | 1313 | ||
1313 | // Could be very long! | 1314 | // Could be very long! |
@@ -1319,10 +1320,13 @@ export class VideoModel extends Model<VideoModel> { | |||
1319 | // Important to do this before getVideoFilename() to take in account the new file extension | 1320 | // Important to do this before getVideoFilename() to take in account the new file extension |
1320 | inputVideoFile.set('extname', newExtname) | 1321 | inputVideoFile.set('extname', newExtname) |
1321 | 1322 | ||
1322 | await renamePromise(videoOutputPath, this.getVideoFilePath(inputVideoFile)) | 1323 | const videoOutputPath = this.getVideoFilePath(inputVideoFile) |
1323 | const stats = await statPromise(this.getVideoFilePath(inputVideoFile)) | 1324 | await renamePromise(videoTranscodedPath, videoOutputPath) |
1325 | const stats = await statPromise(videoOutputPath) | ||
1326 | const fps = await getVideoFileFPS(videoOutputPath) | ||
1324 | 1327 | ||
1325 | inputVideoFile.set('size', stats.size) | 1328 | inputVideoFile.set('size', stats.size) |
1329 | inputVideoFile.set('fps', fps) | ||
1326 | 1330 | ||
1327 | await this.createTorrentAndSetInfoHash(inputVideoFile) | 1331 | await this.createTorrentAndSetInfoHash(inputVideoFile) |
1328 | await inputVideoFile.save() | 1332 | await inputVideoFile.save() |
@@ -1360,8 +1364,10 @@ export class VideoModel extends Model<VideoModel> { | |||
1360 | await transcode(transcodeOptions) | 1364 | await transcode(transcodeOptions) |
1361 | 1365 | ||
1362 | const stats = await statPromise(videoOutputPath) | 1366 | const stats = await statPromise(videoOutputPath) |
1367 | const fps = await getVideoFileFPS(videoOutputPath) | ||
1363 | 1368 | ||
1364 | newVideoFile.set('size', stats.size) | 1369 | newVideoFile.set('size', stats.size) |
1370 | newVideoFile.set('fps', fps) | ||
1365 | 1371 | ||
1366 | await this.createTorrentAndSetInfoHash(newVideoFile) | 1372 | await this.createTorrentAndSetInfoHash(newVideoFile) |
1367 | 1373 | ||
@@ -1371,10 +1377,15 @@ export class VideoModel extends Model<VideoModel> { | |||
1371 | } | 1377 | } |
1372 | 1378 | ||
1373 | async importVideoFile (inputFilePath: string) { | 1379 | async importVideoFile (inputFilePath: string) { |
1380 | const { videoFileResolution } = await getVideoFileResolution(inputFilePath) | ||
1381 | const { size } = await statPromise(inputFilePath) | ||
1382 | const fps = await getVideoFileFPS(inputFilePath) | ||
1383 | |||
1374 | let updatedVideoFile = new VideoFileModel({ | 1384 | let updatedVideoFile = new VideoFileModel({ |
1375 | resolution: (await getVideoFileResolution(inputFilePath)).videoFileResolution, | 1385 | resolution: videoFileResolution, |
1376 | extname: extname(inputFilePath), | 1386 | extname: extname(inputFilePath), |
1377 | size: (await statPromise(inputFilePath)).size, | 1387 | size, |
1388 | fps, | ||
1378 | videoId: this.id | 1389 | videoId: this.id |
1379 | }) | 1390 | }) |
1380 | 1391 | ||
@@ -1390,6 +1401,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1390 | // Update the database | 1401 | // Update the database |
1391 | currentVideoFile.set('extname', updatedVideoFile.extname) | 1402 | currentVideoFile.set('extname', updatedVideoFile.extname) |
1392 | currentVideoFile.set('size', updatedVideoFile.size) | 1403 | currentVideoFile.set('size', updatedVideoFile.size) |
1404 | currentVideoFile.set('fps', updatedVideoFile.fps) | ||
1393 | 1405 | ||
1394 | updatedVideoFile = currentVideoFile | 1406 | updatedVideoFile = currentVideoFile |
1395 | } | 1407 | } |
diff --git a/server/tests/api/videos/video-transcoder.ts b/server/tests/api/videos/video-transcoder.ts index 2b203c26b..fe750253e 100644 --- a/server/tests/api/videos/video-transcoder.ts +++ b/server/tests/api/videos/video-transcoder.ts | |||
@@ -91,13 +91,13 @@ describe('Test video transcoding', function () { | |||
91 | expect(torrent.files[0].path).match(/\.mp4$/) | 91 | expect(torrent.files[0].path).match(/\.mp4$/) |
92 | }) | 92 | }) |
93 | 93 | ||
94 | it('Should transcode to 30 FPS', async function () { | 94 | it('Should transcode a 60 FPS video', async function () { |
95 | this.timeout(60000) | 95 | this.timeout(60000) |
96 | 96 | ||
97 | const videoAttributes = { | 97 | const videoAttributes = { |
98 | name: 'my super 30fps name for server 2', | 98 | name: 'my super 30fps name for server 2', |
99 | description: 'my super 30fps description for server 2', | 99 | description: 'my super 30fps description for server 2', |
100 | fixture: 'video_60fps_short.mp4' | 100 | fixture: '60fps_720p_small.mp4' |
101 | } | 101 | } |
102 | await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes) | 102 | await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes) |
103 | 103 | ||
@@ -109,14 +109,23 @@ describe('Test video transcoding', function () { | |||
109 | const res2 = await getVideo(servers[1].url, video.id) | 109 | const res2 = await getVideo(servers[1].url, video.id) |
110 | const videoDetails: VideoDetails = res2.body | 110 | const videoDetails: VideoDetails = res2.body |
111 | 111 | ||
112 | expect(videoDetails.files).to.have.lengthOf(1) | 112 | expect(videoDetails.files).to.have.lengthOf(4) |
113 | expect(videoDetails.files[0].fps).to.be.above(58).and.below(62) | ||
114 | expect(videoDetails.files[1].fps).to.be.below(31) | ||
115 | expect(videoDetails.files[2].fps).to.be.below(31) | ||
116 | expect(videoDetails.files[3].fps).to.be.below(31) | ||
113 | 117 | ||
114 | for (const resolution of [ '240' ]) { | 118 | for (const resolution of [ '240', '360', '480' ]) { |
115 | const path = join(root(), 'test2', 'videos', video.uuid + '-' + resolution + '.mp4') | 119 | const path = join(root(), 'test2', 'videos', video.uuid + '-' + resolution + '.mp4') |
116 | const fps = await getVideoFileFPS(path) | 120 | const fps = await getVideoFileFPS(path) |
117 | 121 | ||
118 | expect(fps).to.be.below(31) | 122 | expect(fps).to.be.below(31) |
119 | } | 123 | } |
124 | |||
125 | const path = join(root(), 'test2', 'videos', video.uuid + '-720.mp4') | ||
126 | const fps = await getVideoFileFPS(path) | ||
127 | |||
128 | expect(fps).to.be.above(58).and.below(62) | ||
120 | }) | 129 | }) |
121 | 130 | ||
122 | it('Should wait transcoding before publishing the video', async function () { | 131 | it('Should wait transcoding before publishing the video', async function () { |
diff --git a/server/tests/fixtures/60fps_720p_small.mp4 b/server/tests/fixtures/60fps_720p_small.mp4 new file mode 100644 index 000000000..74bf968a4 --- /dev/null +++ b/server/tests/fixtures/60fps_720p_small.mp4 | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/video_60fps_short.mp4 b/server/tests/fixtures/video_60fps_short.mp4 deleted file mode 100644 index ff0593cf3..000000000 --- a/server/tests/fixtures/video_60fps_short.mp4 +++ /dev/null | |||
Binary files differ | |||