Handle higher FPS for high resolution (test)

This commit is contained in:
Chocobozzz 2018-06-29 16:41:29 +02:00
parent 34b1919290
commit 3a6f351b25
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
12 changed files with 106 additions and 25 deletions

View File

@ -35,11 +35,16 @@ class ResolutionMenuButton extends MenuButton {
createMenu () { createMenu () {
const menu = new Menu(this.player_) const menu = new Menu(this.player_)
for (const videoFile of this.player_.peertube().videoFiles) { 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( menu.addChild(new ResolutionMenuItem(
this.player_, this.player_,
{ {
id: videoFile.resolution.id, id: videoFile.resolution.id,
label: videoFile.resolution.label, label,
src: videoFile.magnetUri src: videoFile.magnetUri
}) })
) )

View File

@ -2,7 +2,7 @@ import * as express from 'express'
import { extname, join } from 'path' import { extname, join } from 'path'
import { VideoCreate, VideoPrivacy, VideoState, VideoUpdate } from '../../../../shared' import { VideoCreate, VideoPrivacy, VideoState, VideoUpdate } from '../../../../shared'
import { renamePromise } from '../../../helpers/core-utils' 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 { processImage } from '../../../helpers/image-utils'
import { logger } from '../../../helpers/logger' import { logger } from '../../../helpers/logger'
import { getFormattedObjects, getServerActor, resetSequelizeInstance } from '../../../helpers/utils' import { getFormattedObjects, getServerActor, resetSequelizeInstance } from '../../../helpers/utils'
@ -184,10 +184,13 @@ async function addVideo (req: express.Request, res: express.Response) {
// Build the file object // Build the file object
const { videoFileResolution } = await getVideoFileResolution(videoPhysicalFile.path) const { videoFileResolution } = await getVideoFileResolution(videoPhysicalFile.path)
const fps = await getVideoFileFPS(videoPhysicalFile.path)
const videoFileData = { const videoFileData = {
extname: extname(videoPhysicalFile.filename), extname: extname(videoPhysicalFile.filename),
resolution: videoFileResolution, resolution: videoFileResolution,
size: videoPhysicalFile.size size: videoPhysicalFile.size,
fps
} }
const videoFile = new VideoFileModel(videoFileData) const videoFile = new VideoFileModel(videoFileData)

View File

@ -118,6 +118,10 @@ function isVideoFileResolutionValid (value: string) {
return exists(value) && validator.isInt(value + '') return exists(value) && validator.isInt(value + '')
} }
function isVideoFPSResolutionValid (value: string) {
return value === null || validator.isInt(value + '')
}
function isVideoFileSizeValid (value: string) { function isVideoFileSizeValid (value: string) {
return exists(value) && validator.isInt(value + '', VIDEOS_CONSTRAINTS_FIELDS.FILE_SIZE) return exists(value) && validator.isInt(value + '', VIDEOS_CONSTRAINTS_FIELDS.FILE_SIZE)
} }
@ -182,6 +186,7 @@ export {
isVideoFileInfoHashValid, isVideoFileInfoHashValid,
isVideoNameValid, isVideoNameValid,
isVideoTagsValid, isVideoTagsValid,
isVideoFPSResolutionValid,
isScheduleVideoUpdatePrivacyValid, isScheduleVideoUpdatePrivacyValid,
isVideoAbuseReasonValid, isVideoAbuseReasonValid,
isVideoFile, isVideoFile,

View File

@ -26,7 +26,7 @@ async function getVideoFileFPS (path: string) {
if (!frames || !seconds) continue if (!frames || !seconds) continue
const result = parseInt(frames, 10) / parseInt(seconds, 10) const result = parseInt(frames, 10) / parseInt(seconds, 10)
if (result > 0) return result if (result > 0) return Math.round(result)
} }
return 0 return 0
@ -83,8 +83,6 @@ type TranscodeOptions = {
function transcode (options: TranscodeOptions) { function transcode (options: TranscodeOptions) {
return new Promise<void>(async (res, rej) => { return new Promise<void>(async (res, rej) => {
const fps = await getVideoFileFPS(options.inputPath)
let command = ffmpeg(options.inputPath) let command = ffmpeg(options.inputPath)
.output(options.outputPath) .output(options.outputPath)
.videoCodec('libx264') .videoCodec('libx264')
@ -92,14 +90,27 @@ function transcode (options: TranscodeOptions) {
.outputOption('-movflags faststart') .outputOption('-movflags faststart')
// .outputOption('-crf 18') // .outputOption('-crf 18')
// Our player has some FPS limits let fps = await getVideoFileFPS(options.inputPath)
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)
if (options.resolution !== undefined) { if (options.resolution !== undefined) {
// '?x720' or '720x?' for example // '?x720' or '720x?' for example
const size = options.isPortraitMode === true ? `${options.resolution}x?` : `?x${options.resolution}` const size = options.isPortraitMode === true ? `${options.resolution}x?` : `?x${options.resolution}`
command = command.size(size) 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 command

View File

@ -14,7 +14,7 @@ let config: IConfig = require('config')
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const LAST_MIGRATION_VERSION = 220 const LAST_MIGRATION_VERSION = 225
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -282,7 +282,9 @@ const RATES_LIMIT = {
let VIDEO_VIEW_LIFETIME = 60000 * 60 // 1 hour let VIDEO_VIEW_LIFETIME = 60000 * 60 // 1 hour
const VIDEO_TRANSCODING_FPS = { const VIDEO_TRANSCODING_FPS = {
MIN: 10, 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 } = { const VIDEO_RATE_TYPES: { [ id: string ]: VideoRateType } = {

View File

@ -0,0 +1,22 @@
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 }

View File

@ -1,6 +1,11 @@
import { values } from 'lodash' import { values } from 'lodash'
import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
import { isVideoFileInfoHashValid, isVideoFileResolutionValid, isVideoFileSizeValid } from '../../helpers/custom-validators/videos' import {
isVideoFileInfoHashValid,
isVideoFileResolutionValid,
isVideoFileSizeValid,
isVideoFPSResolutionValid
} from '../../helpers/custom-validators/videos'
import { CONSTRAINTS_FIELDS } from '../../initializers' import { CONSTRAINTS_FIELDS } from '../../initializers'
import { throwIfNotValid } from '../utils' import { throwIfNotValid } from '../utils'
import { VideoModel } from './video' import { VideoModel } from './video'
@ -42,6 +47,12 @@ export class VideoFileModel extends Model<VideoFileModel> {
@Column @Column
infoHash: string infoHash: string
@AllowNull(true)
@Default(null)
@Is('VideoFileFPS', value => throwIfNotValid(value, isVideoFPSResolutionValid, 'fps'))
@Column
fps: number
@ForeignKey(() => VideoModel) @ForeignKey(() => VideoModel)
@Column @Column
videoId: number videoId: number

View File

@ -52,7 +52,7 @@ import {
isVideoStateValid, isVideoStateValid,
isVideoSupportValid isVideoSupportValid
} from '../../helpers/custom-validators/videos' } 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 { logger } from '../../helpers/logger'
import { getServerActor } from '../../helpers/utils' import { getServerActor } from '../../helpers/utils'
import { import {
@ -1168,6 +1168,7 @@ export class VideoModel extends Model<VideoModel> {
}, },
magnetUri: this.generateMagnetUri(videoFile, baseUrlHttp, baseUrlWs), magnetUri: this.generateMagnetUri(videoFile, baseUrlHttp, baseUrlWs),
size: videoFile.size, size: videoFile.size,
fps: videoFile.fps,
torrentUrl: this.getTorrentUrl(videoFile, baseUrlHttp), torrentUrl: this.getTorrentUrl(videoFile, baseUrlHttp),
torrentDownloadUrl: this.getTorrentDownloadUrl(videoFile, baseUrlHttp), torrentDownloadUrl: this.getTorrentDownloadUrl(videoFile, baseUrlHttp),
fileUrl: this.getVideoFileUrl(videoFile, baseUrlHttp), fileUrl: this.getVideoFileUrl(videoFile, baseUrlHttp),
@ -1303,11 +1304,11 @@ export class VideoModel extends Model<VideoModel> {
const newExtname = '.mp4' const newExtname = '.mp4'
const inputVideoFile = this.getOriginalFile() const inputVideoFile = this.getOriginalFile()
const videoInputPath = join(videosDirectory, this.getVideoFilename(inputVideoFile)) 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 = { const transcodeOptions = {
inputPath: videoInputPath, inputPath: videoInputPath,
outputPath: videoOutputPath outputPath: videoTranscodedPath
} }
// Could be very long! // Could be very long!
@ -1319,10 +1320,13 @@ export class VideoModel extends Model<VideoModel> {
// Important to do this before getVideoFilename() to take in account the new file extension // Important to do this before getVideoFilename() to take in account the new file extension
inputVideoFile.set('extname', newExtname) inputVideoFile.set('extname', newExtname)
await renamePromise(videoOutputPath, this.getVideoFilePath(inputVideoFile)) const videoOutputPath = this.getVideoFilePath(inputVideoFile)
const stats = await statPromise(this.getVideoFilePath(inputVideoFile)) await renamePromise(videoTranscodedPath, videoOutputPath)
const stats = await statPromise(videoOutputPath)
const fps = await getVideoFileFPS(videoOutputPath)
inputVideoFile.set('size', stats.size) inputVideoFile.set('size', stats.size)
inputVideoFile.set('fps', fps)
await this.createTorrentAndSetInfoHash(inputVideoFile) await this.createTorrentAndSetInfoHash(inputVideoFile)
await inputVideoFile.save() await inputVideoFile.save()
@ -1360,8 +1364,10 @@ export class VideoModel extends Model<VideoModel> {
await transcode(transcodeOptions) await transcode(transcodeOptions)
const stats = await statPromise(videoOutputPath) const stats = await statPromise(videoOutputPath)
const fps = await getVideoFileFPS(videoOutputPath)
newVideoFile.set('size', stats.size) newVideoFile.set('size', stats.size)
newVideoFile.set('fps', fps)
await this.createTorrentAndSetInfoHash(newVideoFile) await this.createTorrentAndSetInfoHash(newVideoFile)
@ -1371,10 +1377,15 @@ export class VideoModel extends Model<VideoModel> {
} }
async importVideoFile (inputFilePath: string) { async importVideoFile (inputFilePath: string) {
const { videoFileResolution } = await getVideoFileResolution(inputFilePath)
const { size } = await statPromise(inputFilePath)
const fps = await getVideoFileFPS(inputFilePath)
let updatedVideoFile = new VideoFileModel({ let updatedVideoFile = new VideoFileModel({
resolution: (await getVideoFileResolution(inputFilePath)).videoFileResolution, resolution: videoFileResolution,
extname: extname(inputFilePath), extname: extname(inputFilePath),
size: (await statPromise(inputFilePath)).size, size,
fps,
videoId: this.id videoId: this.id
}) })
@ -1390,6 +1401,7 @@ export class VideoModel extends Model<VideoModel> {
// Update the database // Update the database
currentVideoFile.set('extname', updatedVideoFile.extname) currentVideoFile.set('extname', updatedVideoFile.extname)
currentVideoFile.set('size', updatedVideoFile.size) currentVideoFile.set('size', updatedVideoFile.size)
currentVideoFile.set('fps', updatedVideoFile.fps)
updatedVideoFile = currentVideoFile updatedVideoFile = currentVideoFile
} }

View File

@ -91,13 +91,13 @@ describe('Test video transcoding', function () {
expect(torrent.files[0].path).match(/\.mp4$/) 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) this.timeout(60000)
const videoAttributes = { const videoAttributes = {
name: 'my super 30fps name for server 2', name: 'my super 30fps name for server 2',
description: 'my super 30fps description 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) await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
@ -109,14 +109,23 @@ describe('Test video transcoding', function () {
const res2 = await getVideo(servers[1].url, video.id) const res2 = await getVideo(servers[1].url, video.id)
const videoDetails: VideoDetails = res2.body 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 path = join(root(), 'test2', 'videos', video.uuid + '-' + resolution + '.mp4')
const fps = await getVideoFileFPS(path) const fps = await getVideoFileFPS(path)
expect(fps).to.be.below(31) 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 () { it('Should wait transcoding before publishing the video', async function () {

Binary file not shown.

Binary file not shown.

View File

@ -18,6 +18,7 @@ export interface VideoFile {
torrentDownloadUrl: string torrentDownloadUrl: string
fileUrl: string fileUrl: string
fileDownloadUrl: string fileDownloadUrl: string
fps: number
} }
export interface Video { export interface Video {