aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/assets/player/resolution-menu-button.ts7
-rw-r--r--server/controllers/api/videos/index.ts7
-rw-r--r--server/helpers/custom-validators/videos.ts5
-rw-r--r--server/helpers/ffmpeg-utils.ts25
-rw-r--r--server/initializers/constants.ts6
-rw-r--r--server/initializers/migrations/0225-video-fps.ts22
-rw-r--r--server/models/video/video-file.ts15
-rw-r--r--server/models/video/video.ts26
-rw-r--r--server/tests/api/videos/video-transcoder.ts17
-rw-r--r--server/tests/fixtures/60fps_720p_small.mp4bin0 -> 276786 bytes
-rw-r--r--server/tests/fixtures/video_60fps_short.mp4bin33968 -> 0 bytes
-rw-r--r--shared/models/videos/video.model.ts1
12 files changed, 106 insertions, 25 deletions
diff --git a/client/src/assets/player/resolution-menu-button.ts b/client/src/assets/player/resolution-menu-button.ts
index e30074173..d53a24151 100644
--- a/client/src/assets/player/resolution-menu-button.ts
+++ b/client/src/assets/player/resolution-menu-button.ts
@@ -35,11 +35,16 @@ class ResolutionMenuButton extends MenuButton {
35 createMenu () { 35 createMenu () {
36 const menu = new Menu(this.player_) 36 const menu = new Menu(this.player_)
37 for (const videoFile of this.player_.peertube().videoFiles) { 37 for (const videoFile of this.player_.peertube().videoFiles) {
38 let label = videoFile.resolution.label
39 if (videoFile.fps && videoFile.fps >= 50) {
40 label += videoFile.fps
41 }
42
38 menu.addChild(new ResolutionMenuItem( 43 menu.addChild(new ResolutionMenuItem(
39 this.player_, 44 this.player_,
40 { 45 {
41 id: videoFile.resolution.id, 46 id: videoFile.resolution.id,
42 label: videoFile.resolution.label, 47 label,
43 src: videoFile.magnetUri 48 src: videoFile.magnetUri
44 }) 49 })
45 ) 50 )
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'
2import { extname, join } from 'path' 2import { extname, join } from 'path'
3import { VideoCreate, VideoPrivacy, VideoState, VideoUpdate } from '../../../../shared' 3import { VideoCreate, VideoPrivacy, VideoState, VideoUpdate } from '../../../../shared'
4import { renamePromise } from '../../../helpers/core-utils' 4import { renamePromise } from '../../../helpers/core-utils'
5import { getVideoFileResolution } from '../../../helpers/ffmpeg-utils' 5import { getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils'
6import { processImage } from '../../../helpers/image-utils' 6import { processImage } from '../../../helpers/image-utils'
7import { logger } from '../../../helpers/logger' 7import { logger } from '../../../helpers/logger'
8import { getFormattedObjects, getServerActor, resetSequelizeInstance } from '../../../helpers/utils' 8import { 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
121function isVideoFPSResolutionValid (value: string) {
122 return value === null || validator.isInt(value + '')
123}
124
121function isVideoFileSizeValid (value: string) { 125function 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
84function transcode (options: TranscodeOptions) { 84function 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
17const LAST_MIGRATION_VERSION = 220 17const LAST_MIGRATION_VERSION = 225
18 18
19// --------------------------------------------------------------------------- 19// ---------------------------------------------------------------------------
20 20
@@ -282,7 +282,9 @@ const RATES_LIMIT = {
282let VIDEO_VIEW_LIFETIME = 60000 * 60 // 1 hour 282let VIDEO_VIEW_LIFETIME = 60000 * 60 // 1 hour
283const VIDEO_TRANSCODING_FPS = { 283const 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
288const VIDEO_RATE_TYPES: { [ id: string ]: VideoRateType } = { 290const 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 @@
1import * as Sequelize from 'sequelize'
2
3async 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
18function down (options) {
19 throw new Error('Not implemented.')
20}
21
22export { 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 @@
1import { values } from 'lodash' 1import { values } from 'lodash'
2import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' 2import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
3import { isVideoFileInfoHashValid, isVideoFileResolutionValid, isVideoFileSizeValid } from '../../helpers/custom-validators/videos' 3import {
4 isVideoFileInfoHashValid,
5 isVideoFileResolutionValid,
6 isVideoFileSizeValid,
7 isVideoFPSResolutionValid
8} from '../../helpers/custom-validators/videos'
4import { CONSTRAINTS_FIELDS } from '../../initializers' 9import { CONSTRAINTS_FIELDS } from '../../initializers'
5import { throwIfNotValid } from '../utils' 10import { throwIfNotValid } from '../utils'
6import { VideoModel } from './video' 11import { 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'
55import { generateImageFromVideoFile, getVideoFileResolution, transcode } from '../../helpers/ffmpeg-utils' 55import { generateImageFromVideoFile, getVideoFileFPS, getVideoFileResolution, transcode } from '../../helpers/ffmpeg-utils'
56import { logger } from '../../helpers/logger' 56import { logger } from '../../helpers/logger'
57import { getServerActor } from '../../helpers/utils' 57import { getServerActor } from '../../helpers/utils'
58import { 58import {
@@ -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
diff --git a/shared/models/videos/video.model.ts b/shared/models/videos/video.model.ts
index f88f381cb..4e1f15ee3 100644
--- a/shared/models/videos/video.model.ts
+++ b/shared/models/videos/video.model.ts
@@ -18,6 +18,7 @@ export interface VideoFile {
18 torrentDownloadUrl: string 18 torrentDownloadUrl: string
19 fileUrl: string 19 fileUrl: string
20 fileDownloadUrl: string 20 fileDownloadUrl: string
21 fps: number
21} 22}
22 23
23export interface Video { 24export interface Video {