Support max FPS configuration

This commit is contained in:
Chocobozzz 2024-08-12 16:17:11 +02:00
parent 0bd2474fed
commit bbaf96d60d
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
37 changed files with 736 additions and 623 deletions

1
.gitignore vendored
View File

@ -16,6 +16,7 @@ yarn-error.log
# Big fixtures generated/downloaded on-demand
/packages/tests/fixtures/video_high_bitrate_1080p.mp4
/packages/tests/fixtures/video_59fps.mp4
/packages/tests/fixtures/video_50fps.mp4
/packages/tests/fixtures/transcription/models-v1/
# PeerTube

View File

@ -606,6 +606,11 @@ transcoding:
# Transcode and keep original resolution, even if it's above your maximum enabled resolution
always_transcode_original_resolution: true
fps:
# Cap transcoded video FPS
# Max resolution file still keeps the original FPS
max: 60
# Generate videos in a web compatible format
# If you also enabled the hls format, it will multiply videos storage by 2
# If disabled, breaks federation with PeerTube instances < 2.1
@ -716,6 +721,11 @@ live:
# Also transcode original resolution, even if it's above your maximum enabled resolution
always_transcode_original_resolution: true
fps:
# Cap transcoded live FPS
# Max resolution stream still keeps the original FPS
max: 60
video_studio:
# Enable video edition by users (cut, add intro/outro, add watermark etc)
# If enabled, users can create transcoding tasks as they wish

View File

@ -616,6 +616,11 @@ transcoding:
# Transcode and keep original resolution, even if it's above your maximum enabled resolution
always_transcode_original_resolution: true
fps:
# Cap transcoded video FPS
# Max resolution file still keeps the original FPS
max: 60
# Generate videos in a web compatible format
# If you also enabled the hls format, it will multiply videos storage by 2
# If disabled, breaks federation with PeerTube instances < 2.1
@ -726,6 +731,11 @@ live:
# Also transcode original resolution, even if it's above your maximum enabled resolution
always_transcode_original_resolution: true
fps:
# Cap transcoded live FPS
# Max resolution stream still keeps the original FPS
max: 60
video_studio:
# Enable video edition by users (cut, add intro/outro, add watermark etc)
# If enabled, users can create transcoding tasks as they wish

View File

@ -126,7 +126,7 @@ export async function canDoQuickAudioTranscode (path: string, probe?: FfprobeDat
return true
}
export async function canDoQuickVideoTranscode (path: string, probe?: FfprobeData): Promise<boolean> {
export async function canDoQuickVideoTranscode (path: string, maxFPS: number, probe?: FfprobeData): Promise<boolean> {
const videoStream = await getVideoStream(path, probe)
const fps = await getVideoStreamFPS(path, probe)
const bitRate = await getVideoStreamBitrate(path, probe)
@ -139,7 +139,7 @@ export async function canDoQuickVideoTranscode (path: string, probe?: FfprobeDat
if (!videoStream) return false
if (videoStream['codec_name'] !== 'h264') return false
if (videoStream['pix_fmt'] !== 'yuv420p') return false
if (fps < 2 || fps > 65) return false
if (fps < 2 || fps > maxFPS) return false
if (bitRate > getMaxTheoreticalBitrate({ ...resolutionData, fps })) return false
return true

View File

@ -138,6 +138,10 @@ export interface CustomConfig {
alwaysTranscodeOriginalResolution: boolean
fps: {
max: number
}
webVideos: {
enabled: boolean
}
@ -168,8 +172,13 @@ export interface CustomConfig {
}
threads: number
profile: string
resolutions: ConfigResolutions
alwaysTranscodeOriginalResolution: boolean
fps: {
max: number
}
}
}

View File

@ -1,3 +1,2 @@
export * from './video-transcoding-create.model.js'
export * from './video-transcoding-fps.model.js'
export * from './video-transcoding.model.js'

View File

@ -1,18 +0,0 @@
export type VideoTranscodingFPS = {
// Refuse videos with FPS below this limit
HARD_MIN: number
// Cap FPS to this min value
SOFT_MIN: number
STANDARD: number[]
HD_STANDARD: number[]
AUDIO_MERGE: number
AVERAGE: number
// Cap FPS to this max value
SOFT_MAX: number
KEEP_ORIGIN_FPS_RESOLUTION_MIN: number
}

View File

@ -265,17 +265,42 @@ export class ConfigCommand extends AbstractCommand {
})
}
enableTranscoding (options: {
webVideo?: boolean // default true
hls?: boolean // default true
keepOriginal?: boolean // default false
splitAudioAndVideo?: boolean // default false
async enableTranscoding (options: {
webVideo?: boolean
hls?: boolean
keepOriginal?: boolean
splitAudioAndVideo?: boolean
resolutions?: 'min' | 'max' | number[] // default 'max'
resolutions?: 'min' | 'max' | number[]
with0p?: boolean // default false
with0p?: boolean
alwaysTranscodeOriginalResolution?: boolean
maxFPS?: number
} = {}) {
const { resolutions = 'max', webVideo = true, hls = true, with0p = false, keepOriginal = false, splitAudioAndVideo = false } = options
const {
webVideo,
hls,
with0p,
keepOriginal,
splitAudioAndVideo,
alwaysTranscodeOriginalResolution,
maxFPS
} = options
let resolutions: ReturnType<typeof ConfigCommand.getCustomConfigResolutions>
if (Array.isArray(options.resolutions)) {
resolutions = ConfigCommand.getCustomConfigResolutions(options.resolutions)
} else if (typeof options.resolutions === 'string') {
resolutions = ConfigCommand.getConfigResolutions(options.resolutions === 'max', with0p)
} else if (with0p !== undefined) {
const existing = await this.getCustomConfig({ ...options, expectedStatus: HttpStatusCode.OK_200 })
resolutions = existing.transcoding.resolutions
resolutions['0p'] = with0p === true
}
return this.updateExistingConfig({
newConfig: {
@ -288,9 +313,9 @@ export class ConfigCommand extends AbstractCommand {
allowAudioFiles: true,
allowAdditionalExtensions: true,
resolutions: Array.isArray(resolutions)
? ConfigCommand.getCustomConfigResolutions(resolutions)
: ConfigCommand.getConfigResolutions(resolutions === 'max', with0p),
resolutions,
alwaysTranscodeOriginalResolution,
webVideos: {
enabled: webVideo
@ -298,6 +323,9 @@ export class ConfigCommand extends AbstractCommand {
hls: {
enabled: hls,
splitAudioAndVideo
},
fps: {
max: maxFPS
}
}
}

View File

@ -75,7 +75,7 @@ describe('Object storage for video import', function () {
describe('With transcoding', async function () {
before(async function () {
await server.config.enableTranscoding()
await server.config.enableTranscoding({ webVideo: true, hls: true, resolutions: 'max' })
})
it('Should import a video and have sent it to object storage', async function () {

View File

@ -41,7 +41,7 @@ describe('Test runner common actions', function () {
await setAccessTokensToServers([ server ])
await setDefaultVideoChannel([ server ])
await server.config.enableTranscoding({ hls: true, webVideo: true })
await server.config.enableTranscoding({ hls: true, webVideo: true, resolutions: 'max' })
await server.config.enableRemoteTranscoding()
})

View File

@ -85,7 +85,7 @@ describe('Test runner VOD transcoding', function () {
before(async function () {
this.timeout(60000)
await servers[0].config.enableTranscoding({ hls: true, webVideo: true })
await servers[0].config.enableTranscoding({ hls: true, webVideo: true, resolutions: 'max' })
})
it('Should error a transcoding job', async function () {

View File

@ -81,6 +81,7 @@ function checkInitialConfig (server: PeerTubeServer, data: CustomConfig) {
expect(data.transcoding.resolutions['1440p']).to.be.true
expect(data.transcoding.resolutions['2160p']).to.be.true
expect(data.transcoding.alwaysTranscodeOriginalResolution).to.be.true
expect(data.transcoding.fps.max).to.equal(60)
expect(data.transcoding.webVideos.enabled).to.be.true
expect(data.transcoding.hls.enabled).to.be.true
expect(data.transcoding.hls.splitAudioAndVideo).to.be.false
@ -106,6 +107,7 @@ function checkInitialConfig (server: PeerTubeServer, data: CustomConfig) {
expect(data.live.transcoding.resolutions['1440p']).to.be.false
expect(data.live.transcoding.resolutions['2160p']).to.be.false
expect(data.live.transcoding.alwaysTranscodeOriginalResolution).to.be.true
expect(data.live.transcoding.fps.max).to.equal(60)
expect(data.videoStudio.enabled).to.be.false
expect(data.videoStudio.remoteRunners.enabled).to.be.false
@ -255,6 +257,9 @@ function buildNewCustomConfig (server: PeerTubeServer): CustomConfig {
'2160p': false
},
alwaysTranscodeOriginalResolution: false,
fps: {
max: 120
},
webVideos: {
enabled: true
},
@ -290,7 +295,10 @@ function buildNewCustomConfig (server: PeerTubeServer): CustomConfig {
'1440p': true,
'2160p': true
},
alwaysTranscodeOriginalResolution: false
alwaysTranscodeOriginalResolution: false,
fps: {
max: 144
}
}
},
videoStudio: {

View File

@ -77,7 +77,7 @@ function runTests (options: {
const video = await servers[0].videos.get({ id: videoUUID })
publishedAt = video.publishedAt as string
await servers[0].config.enableTranscoding()
await servers[0].config.enableTranscoding({ webVideo: true, hls: true, resolutions: 'max' })
await servers[0].config.setTranscodingConcurrency(concurrency)
})

View File

@ -2,6 +2,7 @@ export * from './audio-only.js'
export * from './create-transcoding.js'
export * from './hls.js'
export * from './split-audio-and-video.js'
export * from './transcoder-limits.js'
export * from './transcoder.js'
export * from './update-while-transcoding.js'
export * from './video-studio.js'

View File

@ -0,0 +1,276 @@
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
import { getAllFiles, getMaxTheoreticalBitrate, getMinTheoreticalBitrate } from '@peertube/peertube-core-utils'
import {
getVideoStreamBitrate,
getVideoStreamDimensionsInfo,
getVideoStreamFPS
} from '@peertube/peertube-ffmpeg'
import { VideoResolution } from '@peertube/peertube-models'
import {
cleanupTests,
createMultipleServers,
doubleFollow,
PeerTubeServer,
setAccessTokensToServers,
waitJobs
} from '@peertube/peertube-server-commands'
import { generateHighBitrateVideo, generateVideoWithFramerate } from '@tests/shared/generate.js'
import { expect } from 'chai'
describe('Test video transcoding limits', function () {
let servers: PeerTubeServer[] = []
before(async function () {
this.timeout(30_000)
// Run servers
servers = await createMultipleServers(2)
await setAccessTokensToServers(servers)
await doubleFollow(servers[0], servers[1])
await servers[1].config.enableTranscoding({
alwaysTranscodeOriginalResolution: true,
hls: true,
webVideo: true,
resolutions: 'max',
with0p: false
})
})
describe('Framerate limits', function () {
async function testFPS (uuid: string, originFPS: number, averageFPS: number) {
for (const server of servers) {
const video = await server.videos.get({ id: uuid })
const files = video.files
const originalFile = files[0]
expect(originalFile.fps).to.be.closeTo(originFPS, 2)
const path = servers[1].servers.buildWebVideoFilePath(originalFile.fileUrl)
expect(await getVideoStreamFPS(path)).to.be.closeTo(originFPS, 2)
files.shift()
for (const file of files) {
expect(file.fps).to.be.closeTo(averageFPS, 2)
const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl)
expect(await getVideoStreamFPS(path)).to.be.closeTo(averageFPS, 2)
}
}
}
it('Should transcode a 60 FPS video', async function () {
this.timeout(60_000)
const attributes = { name: '60fps server 2', fixture: '60fps_720p_small.mp4' }
const { uuid } = await servers[1].videos.upload({ attributes })
await waitJobs(servers)
await testFPS(uuid, 60, 30)
})
it('Should transcode origin resolution to max FPS', async function () {
this.timeout(360_000)
let tempFixturePath: string
{
tempFixturePath = await generateVideoWithFramerate(50, '480x270')
const fps = await getVideoStreamFPS(tempFixturePath)
expect(fps).to.be.equal(50)
}
{
const attributes = { name: '50fps', fixture: tempFixturePath }
const { uuid } = await servers[1].videos.upload({ attributes })
await waitJobs(servers)
await testFPS(uuid, 50, 25)
}
})
it('Should downscale to the closest divisor standard framerate', async function () {
this.timeout(360_000)
let tempFixturePath: string
{
tempFixturePath = await generateVideoWithFramerate(59)
const fps = await getVideoStreamFPS(tempFixturePath)
expect(fps).to.be.equal(59)
}
const attributes = { name: '59fps video', fixture: tempFixturePath }
const { uuid } = await servers[1].videos.upload({ attributes })
await waitJobs(servers)
await testFPS(uuid, 59, 25)
})
it('Should configure max FPS', async function () {
this.timeout(120_000)
const update = (value: number) => {
return servers[1].config.updateExistingConfig({
newConfig: {
transcoding: {
fps: { max: value }
}
}
})
}
await update(15)
const attributes = { name: 'capped 15fps', fixture: '60fps_720p_small.mp4' }
const { uuid } = await servers[1].videos.upload({ attributes })
await waitJobs(servers)
await testFPS(uuid, 15, 15)
await update(60)
})
})
describe('Bitrate control', function () {
it('Should respect maximum bitrate values', async function () {
this.timeout(160_000)
const tempFixturePath = await generateHighBitrateVideo()
const attributes = {
name: 'high bitrate video',
description: 'high bitrate video',
fixture: tempFixturePath
}
await servers[1].videos.upload({ attributes })
await waitJobs(servers)
for (const server of servers) {
const { data } = await server.videos.list()
const { id } = data.find(v => v.name === attributes.name)
const video = await server.videos.get({ id })
for (const resolution of [ 240, 360, 480, 720, 1080 ]) {
const file = video.files.find(f => f.resolution.id === resolution)
const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl)
const bitrate = await getVideoStreamBitrate(path)
const fps = await getVideoStreamFPS(path)
const dataResolution = await getVideoStreamDimensionsInfo(path)
expect(resolution).to.equal(resolution)
const maxBitrate = getMaxTheoreticalBitrate({ ...dataResolution, fps })
expect(bitrate).to.be.below(maxBitrate)
}
}
})
it('Should not transcode to an higher bitrate than the original file but above our low limit', async function () {
this.timeout(160_000)
const newConfig = {
transcoding: {
enabled: true,
resolutions: {
'144p': true,
'240p': true,
'360p': true,
'480p': true,
'720p': true,
'1080p': true,
'1440p': true,
'2160p': true
},
webVideos: { enabled: true },
hls: { enabled: true }
}
}
await servers[1].config.updateExistingConfig({ newConfig })
const attributes = {
name: 'low bitrate',
fixture: 'low-bitrate.mp4'
}
const { id } = await servers[1].videos.upload({ attributes })
await waitJobs(servers)
const video = await servers[1].videos.get({ id })
const resolutions = [ 240, 360, 480, 720, 1080 ]
for (const r of resolutions) {
const file = video.files.find(f => f.resolution.id === r)
const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl)
const bitrate = await getVideoStreamBitrate(path)
const inputBitrate = 60_000
const limit = getMinTheoreticalBitrate({ fps: 10, ratio: 1, resolution: r })
let belowValue = Math.max(inputBitrate, limit)
belowValue += belowValue * 0.20 // Apply 20% margin because bitrate control is not very precise
expect(bitrate, `${path} not below ${limit}`).to.be.below(belowValue)
}
})
})
describe('Resolution capping', function () {
it('Should not generate an upper resolution than original file', async function () {
this.timeout(120_000)
await servers[0].config.enableTranscoding({
resolutions: [ VideoResolution.H_240P, VideoResolution.H_480P ],
alwaysTranscodeOriginalResolution: false
})
const { uuid } = await servers[0].videos.quickUpload({ name: 'video', fixture: 'video_short.webm' })
await waitJobs(servers)
const video = await servers[0].videos.get({ id: uuid })
const hlsFiles = video.streamingPlaylists[0].files
expect(video.files).to.have.lengthOf(2)
expect(hlsFiles).to.have.lengthOf(2)
const resolutions = getAllFiles(video).map(f => f.resolution.id)
expect(resolutions).to.have.members([ 240, 240, 480, 480 ])
})
it('Should only keep the original resolution if all resolutions are disabled', async function () {
this.timeout(120_000)
await servers[0].config.enableTranscoding({ resolutions: [] })
const { uuid } = await servers[0].videos.quickUpload({ name: 'video', fixture: 'video_short.webm' })
await waitJobs(servers)
const video = await servers[0].videos.get({ id: uuid })
const hlsFiles = video.streamingPlaylists[0].files
expect(video.files).to.have.lengthOf(1)
expect(hlsFiles).to.have.lengthOf(1)
expect(video.files[0].resolution.id).to.equal(720)
expect(hlsFiles[0].resolution.id).to.equal(720)
})
})
after(async function () {
await cleanupTests(servers)
})
})

View File

@ -1,12 +1,9 @@
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
import { getAllFiles, getMaxTheoreticalBitrate, getMinTheoreticalBitrate, omit } from '@peertube/peertube-core-utils'
import { getAllFiles, omit } from '@peertube/peertube-core-utils'
import {
ffprobePromise,
getAudioStream,
getVideoStreamBitrate,
getVideoStreamDimensionsInfo,
getVideoStreamFPS,
hasAudioStream
} from '@peertube/peertube-ffmpeg'
import { HttpStatusCode, VideoFileMetadata, VideoState } from '@peertube/peertube-models'
@ -21,35 +18,9 @@ import {
waitJobs
} from '@peertube/peertube-server-commands'
import { canDoQuickTranscode } from '@peertube/peertube-server/core/lib/transcoding/transcoding-quick-transcode.js'
import { generateHighBitrateVideo, generateVideoWithFramerate } from '@tests/shared/generate.js'
import { checkWebTorrentWorks } from '@tests/shared/webtorrent.js'
import { expect } from 'chai'
function updateConfigForTranscoding (server: PeerTubeServer) {
return server.config.updateExistingConfig({
newConfig: {
transcoding: {
enabled: true,
allowAdditionalExtensions: true,
allowAudioFiles: true,
hls: { enabled: true },
webVideos: { enabled: true },
resolutions: {
'0p': false,
'144p': true,
'240p': true,
'360p': true,
'480p': true,
'720p': true,
'1080p': true,
'1440p': true,
'2160p': true
}
}
}
})
}
describe('Test video transcoding', function () {
let servers: PeerTubeServer[] = []
let video4k: string
@ -64,10 +35,16 @@ describe('Test video transcoding', function () {
await doubleFollow(servers[0], servers[1])
await updateConfigForTranscoding(servers[1])
await servers[1].config.enableTranscoding({
alwaysTranscodeOriginalResolution: true,
resolutions: 'max',
hls: true,
webVideo: true,
with0p: false
})
})
describe('Basic transcoding (or not)', function () {
describe('Common transcoding', function () {
it('Should not transcode video on server 1', async function () {
this.timeout(60_000)
@ -414,7 +391,7 @@ describe('Test video transcoding', function () {
}
}
await updateConfigForTranscoding(servers[1])
await servers[1].config.enableTranscoding({ alwaysTranscodeOriginalResolution: true, hls: true, webVideo: true, with0p: false })
})
}
@ -427,188 +404,13 @@ describe('Test video transcoding', function () {
})
})
describe('Framerate', function () {
it('Should transcode a 60 FPS video', async function () {
this.timeout(60_000)
const attributes = {
name: 'my super 30fps name for server 2',
description: 'my super 30fps description for server 2',
fixture: '60fps_720p_small.mp4'
}
await servers[1].videos.upload({ attributes })
await waitJobs(servers)
for (const server of servers) {
const { data } = await server.videos.list()
const video = data.find(v => v.name === attributes.name)
const videoDetails = await server.videos.get({ id: video.id })
expect(videoDetails.files).to.have.lengthOf(5)
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)
expect(videoDetails.files[4].fps).to.be.below(31)
for (const resolution of [ 144, 240, 360, 480 ]) {
const file = videoDetails.files.find(f => f.resolution.id === resolution)
const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl)
const fps = await getVideoStreamFPS(path)
expect(fps).to.be.below(31)
}
const file = videoDetails.files.find(f => f.resolution.id === 720)
const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl)
const fps = await getVideoStreamFPS(path)
expect(fps).to.be.above(58).and.below(62)
}
})
it('Should downscale to the closest divisor standard framerate', async function () {
this.timeout(360_000)
let tempFixturePath: string
{
tempFixturePath = await generateVideoWithFramerate(59)
const fps = await getVideoStreamFPS(tempFixturePath)
expect(fps).to.be.equal(59)
}
const attributes = {
name: '59fps video',
description: '59fps video',
fixture: tempFixturePath
}
await servers[1].videos.upload({ attributes })
await waitJobs(servers)
for (const server of servers) {
const { data } = await server.videos.list()
const { id } = data.find(v => v.name === attributes.name)
const video = await server.videos.get({ id })
{
const file = video.files.find(f => f.resolution.id === 240)
const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl)
const fps = await getVideoStreamFPS(path)
expect(fps).to.be.equal(25)
}
{
const file = video.files.find(f => f.resolution.id === 720)
const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl)
const fps = await getVideoStreamFPS(path)
expect(fps).to.be.equal(59)
}
}
})
})
describe('Bitrate control', function () {
it('Should respect maximum bitrate values', async function () {
this.timeout(160_000)
const tempFixturePath = await generateHighBitrateVideo()
const attributes = {
name: 'high bitrate video',
description: 'high bitrate video',
fixture: tempFixturePath
}
await servers[1].videos.upload({ attributes })
await waitJobs(servers)
for (const server of servers) {
const { data } = await server.videos.list()
const { id } = data.find(v => v.name === attributes.name)
const video = await server.videos.get({ id })
for (const resolution of [ 240, 360, 480, 720, 1080 ]) {
const file = video.files.find(f => f.resolution.id === resolution)
const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl)
const bitrate = await getVideoStreamBitrate(path)
const fps = await getVideoStreamFPS(path)
const dataResolution = await getVideoStreamDimensionsInfo(path)
expect(resolution).to.equal(resolution)
const maxBitrate = getMaxTheoreticalBitrate({ ...dataResolution, fps })
expect(bitrate).to.be.below(maxBitrate)
}
}
})
it('Should not transcode to an higher bitrate than the original file but above our low limit', async function () {
this.timeout(160_000)
const newConfig = {
transcoding: {
enabled: true,
resolutions: {
'144p': true,
'240p': true,
'360p': true,
'480p': true,
'720p': true,
'1080p': true,
'1440p': true,
'2160p': true
},
webVideos: { enabled: true },
hls: { enabled: true }
}
}
await servers[1].config.updateExistingConfig({ newConfig })
const attributes = {
name: 'low bitrate',
fixture: 'low-bitrate.mp4'
}
const { id } = await servers[1].videos.upload({ attributes })
await waitJobs(servers)
const video = await servers[1].videos.get({ id })
const resolutions = [ 240, 360, 480, 720, 1080 ]
for (const r of resolutions) {
const file = video.files.find(f => f.resolution.id === r)
const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl)
const bitrate = await getVideoStreamBitrate(path)
const inputBitrate = 60_000
const limit = getMinTheoreticalBitrate({ fps: 10, ratio: 1, resolution: r })
let belowValue = Math.max(inputBitrate, limit)
belowValue += belowValue * 0.20 // Apply 20% margin because bitrate control is not very precise
expect(bitrate, `${path} not below ${limit}`).to.be.below(belowValue)
}
})
})
describe('FFprobe', function () {
it('Should provide valid ffprobe data', async function () {
this.timeout(160_000)
await servers[1].config.enableTranscoding({ resolutions: 'max' })
const videoUUID = (await servers[1].videos.quickUpload({ name: 'ffprobe data' })).uuid
await waitJobs(servers)
@ -667,8 +469,8 @@ describe('Test video transcoding', function () {
it('Should correctly detect if quick transcode is possible', async function () {
this.timeout(10_000)
expect(await canDoQuickTranscode(buildAbsoluteFixturePath('video_short.mp4'))).to.be.true
expect(await canDoQuickTranscode(buildAbsoluteFixturePath('video_short.webm'))).to.be.false
expect(await canDoQuickTranscode(buildAbsoluteFixturePath('video_short.mp4'), 60)).to.be.true
expect(await canDoQuickTranscode(buildAbsoluteFixturePath('video_short.webm'), 60)).to.be.false
})
})
@ -702,82 +504,6 @@ describe('Test video transcoding', function () {
})
})
describe('Bounded transcoding', function () {
it('Should not generate an upper resolution than original file', async function () {
this.timeout(120_000)
await servers[0].config.updateExistingConfig({
newConfig: {
transcoding: {
enabled: true,
hls: { enabled: true },
webVideos: { enabled: true },
resolutions: {
'0p': false,
'144p': false,
'240p': true,
'360p': false,
'480p': true,
'720p': false,
'1080p': false,
'1440p': false,
'2160p': false
},
alwaysTranscodeOriginalResolution: false
}
}
})
const { uuid } = await servers[0].videos.quickUpload({ name: 'video', fixture: 'video_short.webm' })
await waitJobs(servers)
const video = await servers[0].videos.get({ id: uuid })
const hlsFiles = video.streamingPlaylists[0].files
expect(video.files).to.have.lengthOf(2)
expect(hlsFiles).to.have.lengthOf(2)
// eslint-disable-next-line @typescript-eslint/require-array-sort-compare
const resolutions = getAllFiles(video).map(f => f.resolution.id).sort()
expect(resolutions).to.deep.equal([ 240, 240, 480, 480 ])
})
it('Should only keep the original resolution if all resolutions are disabled', async function () {
this.timeout(120_000)
await servers[0].config.updateExistingConfig({
newConfig: {
transcoding: {
resolutions: {
'0p': false,
'144p': false,
'240p': false,
'360p': false,
'480p': false,
'720p': false,
'1080p': false,
'1440p': false,
'2160p': false
}
}
}
})
const { uuid } = await servers[0].videos.quickUpload({ name: 'video', fixture: 'video_short.webm' })
await waitJobs(servers)
const video = await servers[0].videos.get({ id: uuid })
const hlsFiles = video.streamingPlaylists[0].files
expect(video.files).to.have.lengthOf(1)
expect(hlsFiles).to.have.lengthOf(1)
expect(video.files[0].resolution.id).to.equal(720)
expect(hlsFiles[0].resolution.id).to.equal(720)
})
})
after(async function () {
await cleanupTests(servers)
})

View File

@ -51,7 +51,7 @@ describe('Test generate download', function () {
const resolutions = [ VideoResolution.H_NOVIDEO, VideoResolution.H_144P ]
{
await server.config.enableTranscoding({ hls: true, webVideo: true, resolutions })
await server.config.enableTranscoding({ hls: true, webVideo: true, splitAudioAndVideo: false, resolutions })
await server.videos.quickUpload({ name: 'common-' + seed })
await waitJobs(servers)
}

View File

@ -25,7 +25,7 @@ describe('Test videos files', function () {
await doubleFollow(servers[0], servers[1])
await servers[0].config.enableTranscoding({ hls: true, webVideo: true })
await servers[0].config.enableTranscoding({ hls: true, webVideo: true, resolutions: 'max' })
})
describe('When deleting all files', function () {

View File

@ -247,7 +247,7 @@ describe('Test video source management', function () {
const previousPaths: string[] = []
await servers[0].config.enableTranscoding({ hls: true, webVideo: true, with0p: true, keepOriginal: true })
await servers[0].config.enableTranscoding({ hls: true, webVideo: true, with0p: true, keepOriginal: true, resolutions: 'max' })
const uploadFixture = 'video_short_720p.mp4'
const { uuid: videoUUID } = await servers[0].videos.quickUpload({ name: 'fs with transcoding', fixture: uploadFixture })
@ -527,7 +527,7 @@ describe('Test video source management', function () {
const previousPaths: string[] = []
await servers[0].config.enableTranscoding({ hls: true, webVideo: true, with0p: true, keepOriginal: true })
await servers[0].config.enableTranscoding({ hls: true, webVideo: true, with0p: true, keepOriginal: true, resolutions: 'max' })
const fixture1 = 'video_short_360p.mp4'
const { uuid: videoUUID } = await servers[0].videos.quickUpload({

View File

@ -56,7 +56,7 @@ describe('Test syndication feeds', () => {
await doubleFollow(servers[0], servers[1])
await servers[0].config.enableLive({ allowReplay: false, transcoding: false })
await serverHLSOnly.config.enableTranscoding({ webVideo: false, hls: true, with0p: true })
await serverHLSOnly.config.enableTranscoding({ webVideo: false, hls: true, with0p: true, resolutions: 'max' })
{
const user = await servers[0].users.getMyInfo()

View File

@ -70,6 +70,64 @@ describe('Test Live transcoding in peertube-runner program', function () {
await servers[0].videos.remove({ id: video.id })
})
it('Should cap FPS', async function () {
this.timeout(120000)
await servers[0].config.updateExistingConfig({
newConfig: {
live: {
transcoding: {
fps: { max: 48 }
}
}
}
})
const { video } = await servers[0].live.quickCreate({ permanentLive: true, saveReplay: false, privacy: VideoPrivacy.PUBLIC })
const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({
videoId: video.uuid,
copyCodecs: true,
fixtureName: '60fps_720p_small.mp4'
})
await waitUntilLivePublishedOnAllServers(servers, video.uuid)
await waitJobs(servers)
await testLiveVideoResolutions({
originServer: servers[0],
sqlCommand: sqlCommandServer1,
servers,
liveVideoId: video.uuid,
resolutions: [ 720, 480, 360, 240, 144 ],
framerates: {
720: 48,
480: 30,
360: 30,
240: 30,
144: 30
},
objectStorage,
transcoded: true
})
await stopFfmpeg(ffmpegCommand)
await waitUntilLiveWaitingOnAllServers(servers, video.uuid)
const { data } = await servers[0].runnerJobs.list({ sort: '-createdAt' })
while (true) {
const liveJob = data.find(d => d.type === 'live-rtmp-hls-transcoding')
expect(liveJob).to.exist
if (liveJob.state.id === RunnerJobState.COMPLETED) break
await wait(500)
}
await servers[0].videos.remove({ id: video.id })
})
it('Should transcode audio only RTMP stream', async function () {
this.timeout(120000)

View File

@ -255,6 +255,7 @@ describe('Test VOD transcoding in peertube-runner program', function () {
await doubleFollow(servers[0], servers[1])
await servers[0].config.enableTranscoding({ resolutions: 'max' })
await servers[0].config.enableRemoteTranscoding()
const registrationToken = await servers[0].runnerRegistrationTokens.getFirstRegistrationToken()
@ -304,7 +305,7 @@ describe('Test VOD transcoding in peertube-runner program', function () {
describe('Web video & HLS enabled', function () {
before(async function () {
await servers[0].config.enableTranscoding({ hls: true, webVideo: true, with0p: true })
await servers[0].config.enableTranscoding({ hls: true, webVideo: true, with0p: true, splitAudioAndVideo: false })
})
runSpecificSuite({ webVideoEnabled: true, hlsEnabled: true, objectStorage })
@ -317,29 +318,33 @@ describe('Test VOD transcoding in peertube-runner program', function () {
describe('Common', function () {
it('Should cap max FPS', async function () {
this.timeout(120_000)
await servers[0].config.enableTranscoding({ maxFPS: 15, resolutions: [ 240, 480, 720 ], hls: true, webVideo: true })
const { uuid } = await servers[0].videos.quickUpload({ name: 'video', fixture: 'video_short.webm' })
await waitJobs(servers, { runnerJobs: true })
const video = await servers[0].videos.get({ id: uuid })
const hlsFiles = video.streamingPlaylists[0].files
expect(video.files).to.have.lengthOf(3)
expect(hlsFiles).to.have.lengthOf(3)
const fpsArray = getAllFiles(video).map(f => f.fps)
for (const fps of fpsArray) {
expect(fps).to.be.at.most(15)
}
})
it('Should not generate an upper resolution than original file', async function () {
this.timeout(120_000)
await servers[0].config.updateExistingConfig({
newConfig: {
transcoding: {
enabled: true,
hls: { enabled: true },
webVideos: { enabled: true },
resolutions: {
'0p': false,
'144p': false,
'240p': true,
'360p': false,
'480p': true,
'720p': false,
'1080p': false,
'1440p': false,
'2160p': false
},
alwaysTranscodeOriginalResolution: false
}
}
await servers[0].config.enableTranscoding({
maxFPS: 60,
resolutions: [ 240, 480 ],
alwaysTranscodeOriginalResolution: false
})
const { uuid } = await servers[0].videos.quickUpload({ name: 'video', fixture: 'video_short.webm' })
@ -351,9 +356,8 @@ describe('Test VOD transcoding in peertube-runner program', function () {
expect(video.files).to.have.lengthOf(2)
expect(hlsFiles).to.have.lengthOf(2)
// eslint-disable-next-line @typescript-eslint/require-array-sort-compare
const resolutions = getAllFiles(video).map(f => f.resolution.id).sort()
expect(resolutions).to.deep.equal([ 240, 240, 480, 480 ])
const resolutions = getAllFiles(video).map(f => f.resolution.id)
expect(resolutions).to.have.members([ 240, 240, 480, 480 ])
})
})
})

View File

@ -285,7 +285,7 @@ describe('Test plugin helpers', function () {
before(async function () {
this.timeout(240000)
await servers[0].config.enableTranscoding()
await servers[0].config.enableTranscoding({ webVideo: true, hls: true, resolutions: 'max' })
const res = await servers[0].videos.quickUpload({ name: 'video1' })
videoUUID = res.uuid

View File

@ -47,7 +47,7 @@ async function generateHighBitrateVideo () {
return tempFixturePath
}
async function generateVideoWithFramerate (fps = 60) {
async function generateVideoWithFramerate (fps = 120, size = '1280x720') {
const tempFixturePath = buildAbsoluteFixturePath(`video_${fps}fps.mp4`, true)
await ensureDir(dirname(tempFixturePath))
@ -60,8 +60,8 @@ async function generateVideoWithFramerate (fps = 60) {
return new Promise<string>((res, rej) => {
ffmpeg()
.outputOptions([ '-f rawvideo', '-video_size 1280x720', '-i /dev/urandom' ])
.outputOptions([ '-ac 2', '-f s16le', '-i /dev/urandom', '-t 10' ])
.outputOptions([ '-f rawvideo', '-video_size ' + size, '-i /dev/urandom' ])
.outputOptions([ '-ac 2', '-f s16le', '-i /dev/urandom', '-t 5' ])
.outputOptions([ `-r ${fps}` ])
.output(tempFixturePath)
.on('error', rej)

View File

@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
import { getVideoStreamDimensionsInfo, getVideoStreamFPS } from '@peertube/peertube-ffmpeg'
import { LiveVideo, VideoResolution, VideoStreamingPlaylistType } from '@peertube/peertube-models'
import { sha1 } from '@peertube/peertube-node-utils'
import { ObjectStorageCommand, PeerTubeServer } from '@peertube/peertube-server-commands'
@ -50,7 +51,10 @@ async function testLiveVideoResolutions (options: {
servers: PeerTubeServer[]
liveVideoId: string
resolutions: number[]
framerates?: { [id: number]: number }
transcoded: boolean
hasAudio?: boolean
@ -65,6 +69,7 @@ async function testLiveVideoResolutions (options: {
servers,
liveVideoId,
transcoded,
framerates,
objectStorage,
hasAudio = true,
hasVideo = true,
@ -102,6 +107,7 @@ async function testLiveVideoResolutions (options: {
server,
playlistUrl: hlsPlaylist.playlistUrl,
resolutions,
framerates,
transcoded,
splittedAudio,
hasAudio,
@ -125,6 +131,16 @@ async function testLiveVideoResolutions (options: {
objectStorageBaseUrl
})
if (framerates) {
const segmentPath = servers[0].servers.buildDirectory(join('streaming-playlists', 'hls', video.uuid, segmentName))
const { resolution } = await getVideoStreamDimensionsInfo(segmentPath)
if (resolution) {
const fps = await getVideoStreamFPS(segmentPath)
expect(fps).to.equal(framerates[resolution])
}
}
const baseUrl = objectStorage
? join(objectStorageBaseUrl, 'hls')
: originServer.url + '/static/streaming-playlists/hls'

View File

@ -90,6 +90,7 @@ export async function checkResolutionsInMasterPlaylist (options: {
server: PeerTubeServer
playlistUrl: string
resolutions: number[]
framerates?: { [id: number]: number }
token?: string
transcoded?: boolean // default true
withRetry?: boolean // default false
@ -101,6 +102,7 @@ export async function checkResolutionsInMasterPlaylist (options: {
server,
playlistUrl,
resolutions,
framerates,
token,
hasAudio = true,
hasVideo = true,
@ -136,7 +138,13 @@ export async function checkResolutionsInMasterPlaylist (options: {
: ''
if (transcoded) {
regexp += `,(FRAME-RATE=\\d+,)?CODECS="${codecs}"${audioGroup}`
const framerateRegex = framerates
? framerates[resolution]
: '\\d+'
if (!framerateRegex) throw new Error('Unknown framerate for resolution ' + resolution)
regexp += `,(FRAME-RATE=${framerateRegex},)?CODECS="${codecs}"${audioGroup}`
}
expect(masterPlaylist).to.match(new RegExp(`${regexp}`))

View File

@ -343,6 +343,9 @@ function customConfig (): CustomConfig {
'2160p': CONFIG.TRANSCODING.RESOLUTIONS['2160p']
},
alwaysTranscodeOriginalResolution: CONFIG.TRANSCODING.ALWAYS_TRANSCODE_ORIGINAL_RESOLUTION,
fps: {
max: CONFIG.TRANSCODING.FPS.MAX
},
webVideos: {
enabled: CONFIG.TRANSCODING.WEB_VIDEOS.ENABLED
},