Support max FPS configuration
This commit is contained in:
parent
0bd2474fed
commit
bbaf96d60d
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,2 @@
|
|||
export * from './video-transcoding-create.model.js'
|
||||
export * from './video-transcoding-fps.model.js'
|
||||
export * from './video-transcoding.model.js'
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
|
@ -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)
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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 ])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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}`))
|
||||
|
|
|
@ -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
|
||||
},
|
||||
|