createMenu () {
const menu = new Menu(this.player_)
for (const videoFile of this.player_.peertube().videoFiles) {
+ let label = videoFile.resolution.label
+ if (videoFile.fps && videoFile.fps >= 50) {
+ label += videoFile.fps
+ }
+
menu.addChild(new ResolutionMenuItem(
this.player_,
{
id: videoFile.resolution.id,
- label: videoFile.resolution.label,
+ label,
src: videoFile.magnetUri
})
)
import { extname, join } from 'path'
import { VideoCreate, VideoPrivacy, VideoState, VideoUpdate } from '../../../../shared'
import { renamePromise } from '../../../helpers/core-utils'
-import { getVideoFileResolution } from '../../../helpers/ffmpeg-utils'
+import { getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils'
import { processImage } from '../../../helpers/image-utils'
import { logger } from '../../../helpers/logger'
import { getFormattedObjects, getServerActor, resetSequelizeInstance } from '../../../helpers/utils'
// Build the file object
const { videoFileResolution } = await getVideoFileResolution(videoPhysicalFile.path)
+ const fps = await getVideoFileFPS(videoPhysicalFile.path)
+
const videoFileData = {
extname: extname(videoPhysicalFile.filename),
resolution: videoFileResolution,
- size: videoPhysicalFile.size
+ size: videoPhysicalFile.size,
+ fps
}
const videoFile = new VideoFileModel(videoFileData)
return exists(value) && validator.isInt(value + '')
}
+function isVideoFPSResolutionValid (value: string) {
+ return value === null || validator.isInt(value + '')
+}
+
function isVideoFileSizeValid (value: string) {
return exists(value) && validator.isInt(value + '', VIDEOS_CONSTRAINTS_FIELDS.FILE_SIZE)
}
isVideoFileInfoHashValid,
isVideoNameValid,
isVideoTagsValid,
+ isVideoFPSResolutionValid,
isScheduleVideoUpdatePrivacyValid,
isVideoAbuseReasonValid,
isVideoFile,
if (!frames || !seconds) continue
const result = parseInt(frames, 10) / parseInt(seconds, 10)
- if (result > 0) return result
+ if (result > 0) return Math.round(result)
}
return 0
function transcode (options: TranscodeOptions) {
return new Promise<void>(async (res, rej) => {
- const fps = await getVideoFileFPS(options.inputPath)
-
let command = ffmpeg(options.inputPath)
.output(options.outputPath)
.videoCodec('libx264')
.outputOption('-movflags faststart')
// .outputOption('-crf 18')
- // Our player has some FPS limits
- if (fps > VIDEO_TRANSCODING_FPS.MAX) command = command.withFPS(VIDEO_TRANSCODING_FPS.MAX)
- else if (fps < VIDEO_TRANSCODING_FPS.MIN) command = command.withFPS(VIDEO_TRANSCODING_FPS.MIN)
-
+ let fps = await getVideoFileFPS(options.inputPath)
if (options.resolution !== undefined) {
// '?x720' or '720x?' for example
const size = options.isPortraitMode === true ? `${options.resolution}x?` : `?x${options.resolution}`
command = command.size(size)
+
+ // On small/medium resolutions, limit FPS
+ if (
+ options.resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN &&
+ fps > VIDEO_TRANSCODING_FPS.AVERAGE
+ ) {
+ fps = VIDEO_TRANSCODING_FPS.AVERAGE
+ }
+ }
+
+ if (fps) {
+ // Hard FPS limits
+ if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = VIDEO_TRANSCODING_FPS.MAX
+ else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN
+
+ command = command.withFPS(fps)
}
command
// ---------------------------------------------------------------------------
-const LAST_MIGRATION_VERSION = 220
+const LAST_MIGRATION_VERSION = 225
// ---------------------------------------------------------------------------
let VIDEO_VIEW_LIFETIME = 60000 * 60 // 1 hour
const VIDEO_TRANSCODING_FPS = {
MIN: 10,
- MAX: 30
+ AVERAGE: 30,
+ MAX: 60,
+ KEEP_ORIGIN_FPS_RESOLUTION_MIN: 720 // We keep the original FPS on high resolutions (720 minimum)
}
const VIDEO_RATE_TYPES: { [ id: string ]: VideoRateType } = {
--- /dev/null
+import * as Sequelize from 'sequelize'
+
+async function up (utils: {
+ transaction: Sequelize.Transaction
+ queryInterface: Sequelize.QueryInterface
+ sequelize: Sequelize.Sequelize
+}): Promise<void> {
+ {
+ const data = {
+ type: Sequelize.INTEGER,
+ allowNull: true,
+ defaultValue: null
+ }
+ await utils.queryInterface.addColumn('videoFile', 'fps', data)
+ }
+}
+
+function down (options) {
+ throw new Error('Not implemented.')
+}
+
+export { up, down }
import { values } from 'lodash'
-import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
-import { isVideoFileInfoHashValid, isVideoFileResolutionValid, isVideoFileSizeValid } from '../../helpers/custom-validators/videos'
+import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
+import {
+ isVideoFileInfoHashValid,
+ isVideoFileResolutionValid,
+ isVideoFileSizeValid,
+ isVideoFPSResolutionValid
+} from '../../helpers/custom-validators/videos'
import { CONSTRAINTS_FIELDS } from '../../initializers'
import { throwIfNotValid } from '../utils'
import { VideoModel } from './video'
@Column
infoHash: string
+ @AllowNull(true)
+ @Default(null)
+ @Is('VideoFileFPS', value => throwIfNotValid(value, isVideoFPSResolutionValid, 'fps'))
+ @Column
+ fps: number
+
@ForeignKey(() => VideoModel)
@Column
videoId: number
isVideoStateValid,
isVideoSupportValid
} from '../../helpers/custom-validators/videos'
-import { generateImageFromVideoFile, getVideoFileResolution, transcode } from '../../helpers/ffmpeg-utils'
+import { generateImageFromVideoFile, getVideoFileFPS, getVideoFileResolution, transcode } from '../../helpers/ffmpeg-utils'
import { logger } from '../../helpers/logger'
import { getServerActor } from '../../helpers/utils'
import {
},
magnetUri: this.generateMagnetUri(videoFile, baseUrlHttp, baseUrlWs),
size: videoFile.size,
+ fps: videoFile.fps,
torrentUrl: this.getTorrentUrl(videoFile, baseUrlHttp),
torrentDownloadUrl: this.getTorrentDownloadUrl(videoFile, baseUrlHttp),
fileUrl: this.getVideoFileUrl(videoFile, baseUrlHttp),
const newExtname = '.mp4'
const inputVideoFile = this.getOriginalFile()
const videoInputPath = join(videosDirectory, this.getVideoFilename(inputVideoFile))
- const videoOutputPath = join(videosDirectory, this.id + '-transcoded' + newExtname)
+ const videoTranscodedPath = join(videosDirectory, this.id + '-transcoded' + newExtname)
const transcodeOptions = {
inputPath: videoInputPath,
- outputPath: videoOutputPath
+ outputPath: videoTranscodedPath
}
// Could be very long!
// Important to do this before getVideoFilename() to take in account the new file extension
inputVideoFile.set('extname', newExtname)
- await renamePromise(videoOutputPath, this.getVideoFilePath(inputVideoFile))
- const stats = await statPromise(this.getVideoFilePath(inputVideoFile))
+ const videoOutputPath = this.getVideoFilePath(inputVideoFile)
+ await renamePromise(videoTranscodedPath, videoOutputPath)
+ const stats = await statPromise(videoOutputPath)
+ const fps = await getVideoFileFPS(videoOutputPath)
inputVideoFile.set('size', stats.size)
+ inputVideoFile.set('fps', fps)
await this.createTorrentAndSetInfoHash(inputVideoFile)
await inputVideoFile.save()
await transcode(transcodeOptions)
const stats = await statPromise(videoOutputPath)
+ const fps = await getVideoFileFPS(videoOutputPath)
newVideoFile.set('size', stats.size)
+ newVideoFile.set('fps', fps)
await this.createTorrentAndSetInfoHash(newVideoFile)
}
async importVideoFile (inputFilePath: string) {
+ const { videoFileResolution } = await getVideoFileResolution(inputFilePath)
+ const { size } = await statPromise(inputFilePath)
+ const fps = await getVideoFileFPS(inputFilePath)
+
let updatedVideoFile = new VideoFileModel({
- resolution: (await getVideoFileResolution(inputFilePath)).videoFileResolution,
+ resolution: videoFileResolution,
extname: extname(inputFilePath),
- size: (await statPromise(inputFilePath)).size,
+ size,
+ fps,
videoId: this.id
})
// Update the database
currentVideoFile.set('extname', updatedVideoFile.extname)
currentVideoFile.set('size', updatedVideoFile.size)
+ currentVideoFile.set('fps', updatedVideoFile.fps)
updatedVideoFile = currentVideoFile
}
expect(torrent.files[0].path).match(/\.mp4$/)
})
- it('Should transcode to 30 FPS', async function () {
+ it('Should transcode a 60 FPS video', 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'
+ fixture: '60fps_720p_small.mp4'
}
await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
const res2 = await getVideo(servers[1].url, video.id)
const videoDetails: VideoDetails = res2.body
- expect(videoDetails.files).to.have.lengthOf(1)
+ expect(videoDetails.files).to.have.lengthOf(4)
+ expect(videoDetails.files[0].fps).to.be.above(58).and.below(62)
+ expect(videoDetails.files[1].fps).to.be.below(31)
+ expect(videoDetails.files[2].fps).to.be.below(31)
+ expect(videoDetails.files[3].fps).to.be.below(31)
- for (const resolution of [ '240' ]) {
+ for (const resolution of [ '240', '360', '480' ]) {
const path = join(root(), 'test2', 'videos', video.uuid + '-' + resolution + '.mp4')
const fps = await getVideoFileFPS(path)
expect(fps).to.be.below(31)
}
+
+ const path = join(root(), 'test2', 'videos', video.uuid + '-720.mp4')
+ const fps = await getVideoFileFPS(path)
+
+ expect(fps).to.be.above(58).and.below(62)
})
it('Should wait transcoding before publishing the video', async function () {
torrentDownloadUrl: string
fileUrl: string
fileDownloadUrl: string
+ fps: number
}
export interface Video {