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
|
# Big fixtures generated/downloaded on-demand
|
||||||
/packages/tests/fixtures/video_high_bitrate_1080p.mp4
|
/packages/tests/fixtures/video_high_bitrate_1080p.mp4
|
||||||
/packages/tests/fixtures/video_59fps.mp4
|
/packages/tests/fixtures/video_59fps.mp4
|
||||||
|
/packages/tests/fixtures/video_50fps.mp4
|
||||||
/packages/tests/fixtures/transcription/models-v1/
|
/packages/tests/fixtures/transcription/models-v1/
|
||||||
|
|
||||||
# PeerTube
|
# PeerTube
|
||||||
|
|
|
@ -606,6 +606,11 @@ transcoding:
|
||||||
# Transcode and keep original resolution, even if it's above your maximum enabled resolution
|
# Transcode and keep original resolution, even if it's above your maximum enabled resolution
|
||||||
always_transcode_original_resolution: true
|
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
|
# Generate videos in a web compatible format
|
||||||
# If you also enabled the hls format, it will multiply videos storage by 2
|
# If you also enabled the hls format, it will multiply videos storage by 2
|
||||||
# If disabled, breaks federation with PeerTube instances < 2.1
|
# 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
|
# Also transcode original resolution, even if it's above your maximum enabled resolution
|
||||||
always_transcode_original_resolution: true
|
always_transcode_original_resolution: true
|
||||||
|
|
||||||
|
fps:
|
||||||
|
# Cap transcoded live FPS
|
||||||
|
# Max resolution stream still keeps the original FPS
|
||||||
|
max: 60
|
||||||
|
|
||||||
video_studio:
|
video_studio:
|
||||||
# Enable video edition by users (cut, add intro/outro, add watermark etc)
|
# Enable video edition by users (cut, add intro/outro, add watermark etc)
|
||||||
# If enabled, users can create transcoding tasks as they wish
|
# 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
|
# Transcode and keep original resolution, even if it's above your maximum enabled resolution
|
||||||
always_transcode_original_resolution: true
|
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
|
# Generate videos in a web compatible format
|
||||||
# If you also enabled the hls format, it will multiply videos storage by 2
|
# If you also enabled the hls format, it will multiply videos storage by 2
|
||||||
# If disabled, breaks federation with PeerTube instances < 2.1
|
# 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
|
# Also transcode original resolution, even if it's above your maximum enabled resolution
|
||||||
always_transcode_original_resolution: true
|
always_transcode_original_resolution: true
|
||||||
|
|
||||||
|
fps:
|
||||||
|
# Cap transcoded live FPS
|
||||||
|
# Max resolution stream still keeps the original FPS
|
||||||
|
max: 60
|
||||||
|
|
||||||
video_studio:
|
video_studio:
|
||||||
# Enable video edition by users (cut, add intro/outro, add watermark etc)
|
# Enable video edition by users (cut, add intro/outro, add watermark etc)
|
||||||
# If enabled, users can create transcoding tasks as they wish
|
# If enabled, users can create transcoding tasks as they wish
|
||||||
|
|
|
@ -126,7 +126,7 @@ export async function canDoQuickAudioTranscode (path: string, probe?: FfprobeDat
|
||||||
return true
|
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 videoStream = await getVideoStream(path, probe)
|
||||||
const fps = await getVideoStreamFPS(path, probe)
|
const fps = await getVideoStreamFPS(path, probe)
|
||||||
const bitRate = await getVideoStreamBitrate(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) return false
|
||||||
if (videoStream['codec_name'] !== 'h264') return false
|
if (videoStream['codec_name'] !== 'h264') return false
|
||||||
if (videoStream['pix_fmt'] !== 'yuv420p') 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
|
if (bitRate > getMaxTheoreticalBitrate({ ...resolutionData, fps })) return false
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|
|
@ -138,6 +138,10 @@ export interface CustomConfig {
|
||||||
|
|
||||||
alwaysTranscodeOriginalResolution: boolean
|
alwaysTranscodeOriginalResolution: boolean
|
||||||
|
|
||||||
|
fps: {
|
||||||
|
max: number
|
||||||
|
}
|
||||||
|
|
||||||
webVideos: {
|
webVideos: {
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
}
|
}
|
||||||
|
@ -168,8 +172,13 @@ export interface CustomConfig {
|
||||||
}
|
}
|
||||||
threads: number
|
threads: number
|
||||||
profile: string
|
profile: string
|
||||||
|
|
||||||
resolutions: ConfigResolutions
|
resolutions: ConfigResolutions
|
||||||
alwaysTranscodeOriginalResolution: boolean
|
alwaysTranscodeOriginalResolution: boolean
|
||||||
|
|
||||||
|
fps: {
|
||||||
|
max: number
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,2 @@
|
||||||
export * from './video-transcoding-create.model.js'
|
export * from './video-transcoding-create.model.js'
|
||||||
export * from './video-transcoding-fps.model.js'
|
|
||||||
export * from './video-transcoding.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: {
|
async enableTranscoding (options: {
|
||||||
webVideo?: boolean // default true
|
webVideo?: boolean
|
||||||
hls?: boolean // default true
|
hls?: boolean
|
||||||
keepOriginal?: boolean // default false
|
keepOriginal?: boolean
|
||||||
splitAudioAndVideo?: boolean // default false
|
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({
|
return this.updateExistingConfig({
|
||||||
newConfig: {
|
newConfig: {
|
||||||
|
@ -288,9 +313,9 @@ export class ConfigCommand extends AbstractCommand {
|
||||||
allowAudioFiles: true,
|
allowAudioFiles: true,
|
||||||
allowAdditionalExtensions: true,
|
allowAdditionalExtensions: true,
|
||||||
|
|
||||||
resolutions: Array.isArray(resolutions)
|
resolutions,
|
||||||
? ConfigCommand.getCustomConfigResolutions(resolutions)
|
|
||||||
: ConfigCommand.getConfigResolutions(resolutions === 'max', with0p),
|
alwaysTranscodeOriginalResolution,
|
||||||
|
|
||||||
webVideos: {
|
webVideos: {
|
||||||
enabled: webVideo
|
enabled: webVideo
|
||||||
|
@ -298,6 +323,9 @@ export class ConfigCommand extends AbstractCommand {
|
||||||
hls: {
|
hls: {
|
||||||
enabled: hls,
|
enabled: hls,
|
||||||
splitAudioAndVideo
|
splitAudioAndVideo
|
||||||
|
},
|
||||||
|
fps: {
|
||||||
|
max: maxFPS
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,7 +75,7 @@ describe('Object storage for video import', function () {
|
||||||
describe('With transcoding', async function () {
|
describe('With transcoding', async function () {
|
||||||
|
|
||||||
before(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 () {
|
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 setAccessTokensToServers([ server ])
|
||||||
await setDefaultVideoChannel([ 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()
|
await server.config.enableRemoteTranscoding()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -85,7 +85,7 @@ describe('Test runner VOD transcoding', function () {
|
||||||
before(async function () {
|
before(async function () {
|
||||||
this.timeout(60000)
|
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 () {
|
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['1440p']).to.be.true
|
||||||
expect(data.transcoding.resolutions['2160p']).to.be.true
|
expect(data.transcoding.resolutions['2160p']).to.be.true
|
||||||
expect(data.transcoding.alwaysTranscodeOriginalResolution).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.webVideos.enabled).to.be.true
|
||||||
expect(data.transcoding.hls.enabled).to.be.true
|
expect(data.transcoding.hls.enabled).to.be.true
|
||||||
expect(data.transcoding.hls.splitAudioAndVideo).to.be.false
|
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['1440p']).to.be.false
|
||||||
expect(data.live.transcoding.resolutions['2160p']).to.be.false
|
expect(data.live.transcoding.resolutions['2160p']).to.be.false
|
||||||
expect(data.live.transcoding.alwaysTranscodeOriginalResolution).to.be.true
|
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.enabled).to.be.false
|
||||||
expect(data.videoStudio.remoteRunners.enabled).to.be.false
|
expect(data.videoStudio.remoteRunners.enabled).to.be.false
|
||||||
|
@ -255,6 +257,9 @@ function buildNewCustomConfig (server: PeerTubeServer): CustomConfig {
|
||||||
'2160p': false
|
'2160p': false
|
||||||
},
|
},
|
||||||
alwaysTranscodeOriginalResolution: false,
|
alwaysTranscodeOriginalResolution: false,
|
||||||
|
fps: {
|
||||||
|
max: 120
|
||||||
|
},
|
||||||
webVideos: {
|
webVideos: {
|
||||||
enabled: true
|
enabled: true
|
||||||
},
|
},
|
||||||
|
@ -290,7 +295,10 @@ function buildNewCustomConfig (server: PeerTubeServer): CustomConfig {
|
||||||
'1440p': true,
|
'1440p': true,
|
||||||
'2160p': true
|
'2160p': true
|
||||||
},
|
},
|
||||||
alwaysTranscodeOriginalResolution: false
|
alwaysTranscodeOriginalResolution: false,
|
||||||
|
fps: {
|
||||||
|
max: 144
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
videoStudio: {
|
videoStudio: {
|
||||||
|
|
|
@ -77,7 +77,7 @@ function runTests (options: {
|
||||||
const video = await servers[0].videos.get({ id: videoUUID })
|
const video = await servers[0].videos.get({ id: videoUUID })
|
||||||
publishedAt = video.publishedAt as string
|
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)
|
await servers[0].config.setTranscodingConcurrency(concurrency)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ export * from './audio-only.js'
|
||||||
export * from './create-transcoding.js'
|
export * from './create-transcoding.js'
|
||||||
export * from './hls.js'
|
export * from './hls.js'
|
||||||
export * from './split-audio-and-video.js'
|
export * from './split-audio-and-video.js'
|
||||||
|
export * from './transcoder-limits.js'
|
||||||
export * from './transcoder.js'
|
export * from './transcoder.js'
|
||||||
export * from './update-while-transcoding.js'
|
export * from './update-while-transcoding.js'
|
||||||
export * from './video-studio.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 */
|
/* 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 {
|
import {
|
||||||
ffprobePromise,
|
ffprobePromise,
|
||||||
getAudioStream,
|
getAudioStream,
|
||||||
getVideoStreamBitrate,
|
|
||||||
getVideoStreamDimensionsInfo,
|
|
||||||
getVideoStreamFPS,
|
|
||||||
hasAudioStream
|
hasAudioStream
|
||||||
} from '@peertube/peertube-ffmpeg'
|
} from '@peertube/peertube-ffmpeg'
|
||||||
import { HttpStatusCode, VideoFileMetadata, VideoState } from '@peertube/peertube-models'
|
import { HttpStatusCode, VideoFileMetadata, VideoState } from '@peertube/peertube-models'
|
||||||
|
@ -21,35 +18,9 @@ import {
|
||||||
waitJobs
|
waitJobs
|
||||||
} from '@peertube/peertube-server-commands'
|
} from '@peertube/peertube-server-commands'
|
||||||
import { canDoQuickTranscode } from '@peertube/peertube-server/core/lib/transcoding/transcoding-quick-transcode.js'
|
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 { checkWebTorrentWorks } from '@tests/shared/webtorrent.js'
|
||||||
import { expect } from 'chai'
|
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 () {
|
describe('Test video transcoding', function () {
|
||||||
let servers: PeerTubeServer[] = []
|
let servers: PeerTubeServer[] = []
|
||||||
let video4k: string
|
let video4k: string
|
||||||
|
@ -64,10 +35,16 @@ describe('Test video transcoding', function () {
|
||||||
|
|
||||||
await doubleFollow(servers[0], servers[1])
|
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 () {
|
it('Should not transcode video on server 1', async function () {
|
||||||
this.timeout(60_000)
|
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 () {
|
describe('FFprobe', function () {
|
||||||
|
|
||||||
it('Should provide valid ffprobe data', async function () {
|
it('Should provide valid ffprobe data', async function () {
|
||||||
this.timeout(160_000)
|
this.timeout(160_000)
|
||||||
|
|
||||||
|
await servers[1].config.enableTranscoding({ resolutions: 'max' })
|
||||||
|
|
||||||
const videoUUID = (await servers[1].videos.quickUpload({ name: 'ffprobe data' })).uuid
|
const videoUUID = (await servers[1].videos.quickUpload({ name: 'ffprobe data' })).uuid
|
||||||
await waitJobs(servers)
|
await waitJobs(servers)
|
||||||
|
|
||||||
|
@ -667,8 +469,8 @@ describe('Test video transcoding', function () {
|
||||||
it('Should correctly detect if quick transcode is possible', async function () {
|
it('Should correctly detect if quick transcode is possible', async function () {
|
||||||
this.timeout(10_000)
|
this.timeout(10_000)
|
||||||
|
|
||||||
expect(await canDoQuickTranscode(buildAbsoluteFixturePath('video_short.mp4'))).to.be.true
|
expect(await canDoQuickTranscode(buildAbsoluteFixturePath('video_short.mp4'), 60)).to.be.true
|
||||||
expect(await canDoQuickTranscode(buildAbsoluteFixturePath('video_short.webm'))).to.be.false
|
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 () {
|
after(async function () {
|
||||||
await cleanupTests(servers)
|
await cleanupTests(servers)
|
||||||
})
|
})
|
||||||
|
|
|
@ -51,7 +51,7 @@ describe('Test generate download', function () {
|
||||||
const resolutions = [ VideoResolution.H_NOVIDEO, VideoResolution.H_144P ]
|
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 server.videos.quickUpload({ name: 'common-' + seed })
|
||||||
await waitJobs(servers)
|
await waitJobs(servers)
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@ describe('Test videos files', function () {
|
||||||
|
|
||||||
await doubleFollow(servers[0], servers[1])
|
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 () {
|
describe('When deleting all files', function () {
|
||||||
|
|
|
@ -247,7 +247,7 @@ describe('Test video source management', function () {
|
||||||
|
|
||||||
const previousPaths: string[] = []
|
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 uploadFixture = 'video_short_720p.mp4'
|
||||||
const { uuid: videoUUID } = await servers[0].videos.quickUpload({ name: 'fs with transcoding', fixture: uploadFixture })
|
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[] = []
|
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 fixture1 = 'video_short_360p.mp4'
|
||||||
const { uuid: videoUUID } = await servers[0].videos.quickUpload({
|
const { uuid: videoUUID } = await servers[0].videos.quickUpload({
|
||||||
|
|
|
@ -56,7 +56,7 @@ describe('Test syndication feeds', () => {
|
||||||
await doubleFollow(servers[0], servers[1])
|
await doubleFollow(servers[0], servers[1])
|
||||||
|
|
||||||
await servers[0].config.enableLive({ allowReplay: false, transcoding: false })
|
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()
|
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 })
|
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 () {
|
it('Should transcode audio only RTMP stream', async function () {
|
||||||
this.timeout(120000)
|
this.timeout(120000)
|
||||||
|
|
||||||
|
|
|
@ -255,6 +255,7 @@ describe('Test VOD transcoding in peertube-runner program', function () {
|
||||||
|
|
||||||
await doubleFollow(servers[0], servers[1])
|
await doubleFollow(servers[0], servers[1])
|
||||||
|
|
||||||
|
await servers[0].config.enableTranscoding({ resolutions: 'max' })
|
||||||
await servers[0].config.enableRemoteTranscoding()
|
await servers[0].config.enableRemoteTranscoding()
|
||||||
|
|
||||||
const registrationToken = await servers[0].runnerRegistrationTokens.getFirstRegistrationToken()
|
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 () {
|
describe('Web video & HLS enabled', function () {
|
||||||
|
|
||||||
before(async 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 })
|
runSpecificSuite({ webVideoEnabled: true, hlsEnabled: true, objectStorage })
|
||||||
|
@ -317,29 +318,33 @@ describe('Test VOD transcoding in peertube-runner program', function () {
|
||||||
|
|
||||||
describe('Common', 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 () {
|
it('Should not generate an upper resolution than original file', async function () {
|
||||||
this.timeout(120_000)
|
this.timeout(120_000)
|
||||||
|
|
||||||
await servers[0].config.updateExistingConfig({
|
await servers[0].config.enableTranscoding({
|
||||||
newConfig: {
|
maxFPS: 60,
|
||||||
transcoding: {
|
resolutions: [ 240, 480 ],
|
||||||
enabled: true,
|
alwaysTranscodeOriginalResolution: false
|
||||||
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' })
|
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(video.files).to.have.lengthOf(2)
|
||||||
expect(hlsFiles).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)
|
||||||
const resolutions = getAllFiles(video).map(f => f.resolution.id).sort()
|
expect(resolutions).to.have.members([ 240, 240, 480, 480 ])
|
||||||
expect(resolutions).to.deep.equal([ 240, 240, 480, 480 ])
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -285,7 +285,7 @@ describe('Test plugin helpers', function () {
|
||||||
before(async function () {
|
before(async function () {
|
||||||
this.timeout(240000)
|
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' })
|
const res = await servers[0].videos.quickUpload({ name: 'video1' })
|
||||||
videoUUID = res.uuid
|
videoUUID = res.uuid
|
||||||
|
|
|
@ -47,7 +47,7 @@ async function generateHighBitrateVideo () {
|
||||||
return tempFixturePath
|
return tempFixturePath
|
||||||
}
|
}
|
||||||
|
|
||||||
async function generateVideoWithFramerate (fps = 60) {
|
async function generateVideoWithFramerate (fps = 120, size = '1280x720') {
|
||||||
const tempFixturePath = buildAbsoluteFixturePath(`video_${fps}fps.mp4`, true)
|
const tempFixturePath = buildAbsoluteFixturePath(`video_${fps}fps.mp4`, true)
|
||||||
|
|
||||||
await ensureDir(dirname(tempFixturePath))
|
await ensureDir(dirname(tempFixturePath))
|
||||||
|
@ -60,8 +60,8 @@ async function generateVideoWithFramerate (fps = 60) {
|
||||||
|
|
||||||
return new Promise<string>((res, rej) => {
|
return new Promise<string>((res, rej) => {
|
||||||
ffmpeg()
|
ffmpeg()
|
||||||
.outputOptions([ '-f rawvideo', '-video_size 1280x720', '-i /dev/urandom' ])
|
.outputOptions([ '-f rawvideo', '-video_size ' + size, '-i /dev/urandom' ])
|
||||||
.outputOptions([ '-ac 2', '-f s16le', '-i /dev/urandom', '-t 10' ])
|
.outputOptions([ '-ac 2', '-f s16le', '-i /dev/urandom', '-t 5' ])
|
||||||
.outputOptions([ `-r ${fps}` ])
|
.outputOptions([ `-r ${fps}` ])
|
||||||
.output(tempFixturePath)
|
.output(tempFixturePath)
|
||||||
.on('error', rej)
|
.on('error', rej)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
/* 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 { LiveVideo, VideoResolution, VideoStreamingPlaylistType } from '@peertube/peertube-models'
|
||||||
import { sha1 } from '@peertube/peertube-node-utils'
|
import { sha1 } from '@peertube/peertube-node-utils'
|
||||||
import { ObjectStorageCommand, PeerTubeServer } from '@peertube/peertube-server-commands'
|
import { ObjectStorageCommand, PeerTubeServer } from '@peertube/peertube-server-commands'
|
||||||
|
@ -50,7 +51,10 @@ async function testLiveVideoResolutions (options: {
|
||||||
|
|
||||||
servers: PeerTubeServer[]
|
servers: PeerTubeServer[]
|
||||||
liveVideoId: string
|
liveVideoId: string
|
||||||
|
|
||||||
resolutions: number[]
|
resolutions: number[]
|
||||||
|
framerates?: { [id: number]: number }
|
||||||
|
|
||||||
transcoded: boolean
|
transcoded: boolean
|
||||||
|
|
||||||
hasAudio?: boolean
|
hasAudio?: boolean
|
||||||
|
@ -65,6 +69,7 @@ async function testLiveVideoResolutions (options: {
|
||||||
servers,
|
servers,
|
||||||
liveVideoId,
|
liveVideoId,
|
||||||
transcoded,
|
transcoded,
|
||||||
|
framerates,
|
||||||
objectStorage,
|
objectStorage,
|
||||||
hasAudio = true,
|
hasAudio = true,
|
||||||
hasVideo = true,
|
hasVideo = true,
|
||||||
|
@ -102,6 +107,7 @@ async function testLiveVideoResolutions (options: {
|
||||||
server,
|
server,
|
||||||
playlistUrl: hlsPlaylist.playlistUrl,
|
playlistUrl: hlsPlaylist.playlistUrl,
|
||||||
resolutions,
|
resolutions,
|
||||||
|
framerates,
|
||||||
transcoded,
|
transcoded,
|
||||||
splittedAudio,
|
splittedAudio,
|
||||||
hasAudio,
|
hasAudio,
|
||||||
|
@ -125,6 +131,16 @@ async function testLiveVideoResolutions (options: {
|
||||||
objectStorageBaseUrl
|
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
|
const baseUrl = objectStorage
|
||||||
? join(objectStorageBaseUrl, 'hls')
|
? join(objectStorageBaseUrl, 'hls')
|
||||||
: originServer.url + '/static/streaming-playlists/hls'
|
: originServer.url + '/static/streaming-playlists/hls'
|
||||||
|
|
|
@ -90,6 +90,7 @@ export async function checkResolutionsInMasterPlaylist (options: {
|
||||||
server: PeerTubeServer
|
server: PeerTubeServer
|
||||||
playlistUrl: string
|
playlistUrl: string
|
||||||
resolutions: number[]
|
resolutions: number[]
|
||||||
|
framerates?: { [id: number]: number }
|
||||||
token?: string
|
token?: string
|
||||||
transcoded?: boolean // default true
|
transcoded?: boolean // default true
|
||||||
withRetry?: boolean // default false
|
withRetry?: boolean // default false
|
||||||
|
@ -101,6 +102,7 @@ export async function checkResolutionsInMasterPlaylist (options: {
|
||||||
server,
|
server,
|
||||||
playlistUrl,
|
playlistUrl,
|
||||||
resolutions,
|
resolutions,
|
||||||
|
framerates,
|
||||||
token,
|
token,
|
||||||
hasAudio = true,
|
hasAudio = true,
|
||||||
hasVideo = true,
|
hasVideo = true,
|
||||||
|
@ -136,7 +138,13 @@ export async function checkResolutionsInMasterPlaylist (options: {
|
||||||
: ''
|
: ''
|
||||||
|
|
||||||
if (transcoded) {
|
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}`))
|
expect(masterPlaylist).to.match(new RegExp(`${regexp}`))
|
||||||
|
|
|
@ -343,6 +343,9 @@ function customConfig (): CustomConfig {
|
||||||
'2160p': CONFIG.TRANSCODING.RESOLUTIONS['2160p']
|
'2160p': CONFIG.TRANSCODING.RESOLUTIONS['2160p']
|
||||||
},
|
},
|
||||||
alwaysTranscodeOriginalResolution: CONFIG.TRANSCODING.ALWAYS_TRANSCODE_ORIGINAL_RESOLUTION,
|
alwaysTranscodeOriginalResolution: CONFIG.TRANSCODING.ALWAYS_TRANSCODE_ORIGINAL_RESOLUTION,
|
||||||
|
fps: {
|
||||||
|
max: CONFIG.TRANSCODING.FPS.MAX
|
||||||
|
},
|
||||||
webVideos: {
|
webVideos: {
|
||||||
enabled: CONFIG.TRANSCODING.WEB_VIDEOS.ENABLED
|
enabled: CONFIG.TRANSCODING.WEB_VIDEOS.ENABLED
|
||||||
},
|
},
|
||||||
|
@ -378,7 +381,10 @@ function customConfig (): CustomConfig {
|
||||||
'1440p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['1440p'],
|
'1440p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['1440p'],
|
||||||
'2160p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['2160p']
|
'2160p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['2160p']
|
||||||
},
|
},
|
||||||
alwaysTranscodeOriginalResolution: CONFIG.LIVE.TRANSCODING.ALWAYS_TRANSCODE_ORIGINAL_RESOLUTION
|
alwaysTranscodeOriginalResolution: CONFIG.LIVE.TRANSCODING.ALWAYS_TRANSCODE_ORIGINAL_RESOLUTION,
|
||||||
|
fps: {
|
||||||
|
max: CONFIG.LIVE.TRANSCODING.FPS.MAX
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
videoStudio: {
|
videoStudio: {
|
||||||
|
|
|
@ -1,31 +1,44 @@
|
||||||
import { VIDEO_TRANSCODING_FPS } from '@server/initializers/constants.js'
|
import { CONFIG } from '@server/initializers/config.js'
|
||||||
|
import { logger } from '../logger.js'
|
||||||
|
|
||||||
export function computeOutputFPS (options: {
|
export function computeOutputFPS (options: {
|
||||||
inputFPS: number
|
inputFPS: number
|
||||||
|
isOriginResolution: boolean
|
||||||
resolution: number
|
resolution: number
|
||||||
|
type: 'vod' | 'live'
|
||||||
}) {
|
}) {
|
||||||
const { resolution } = options
|
const { resolution, isOriginResolution, type } = options
|
||||||
|
|
||||||
|
const settings = type === 'vod'
|
||||||
|
? buildTranscodingFPSOptions(CONFIG.TRANSCODING.FPS.MAX)
|
||||||
|
: buildTranscodingFPSOptions(CONFIG.LIVE.TRANSCODING.FPS.MAX)
|
||||||
|
|
||||||
let fps = options.inputFPS
|
let fps = options.inputFPS
|
||||||
|
|
||||||
if (
|
if (
|
||||||
// On small/medium resolutions, limit FPS
|
// On small/medium transcoded resolutions, limit FPS
|
||||||
|
!isOriginResolution &&
|
||||||
resolution !== undefined &&
|
resolution !== undefined &&
|
||||||
resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN &&
|
resolution < settings.KEEP_ORIGIN_FPS_RESOLUTION_MIN &&
|
||||||
fps > VIDEO_TRANSCODING_FPS.AVERAGE
|
fps > settings.AVERAGE
|
||||||
) {
|
) {
|
||||||
// Get closest standard framerate by modulo: downsampling has to be done to a divisor of the nominal fps value
|
// Get closest standard framerate by modulo: downsampling has to be done to a divisor of the nominal fps value
|
||||||
fps = getClosestFramerateStandard({ fps, type: 'STANDARD' })
|
fps = getClosestFramerate({ fps, settings, type: 'STANDARD' })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fps < VIDEO_TRANSCODING_FPS.HARD_MIN) {
|
if (fps < settings.HARD_MIN) {
|
||||||
throw new Error(`Cannot compute FPS because ${fps} is lower than our minimum value ${VIDEO_TRANSCODING_FPS.HARD_MIN}`)
|
throw new Error(`Cannot compute FPS because ${fps} is lower than our minimum value ${settings.HARD_MIN}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cap min FPS
|
// Cap min FPS
|
||||||
if (fps < VIDEO_TRANSCODING_FPS.SOFT_MIN) fps = VIDEO_TRANSCODING_FPS.SOFT_MIN
|
fps = Math.max(fps, settings.TRANSCODED_MIN)
|
||||||
|
|
||||||
// Cap max FPS
|
// Cap max FPS
|
||||||
if (fps > VIDEO_TRANSCODING_FPS.SOFT_MAX) fps = getClosestFramerateStandard({ fps, type: 'HD_STANDARD' })
|
if (fps > settings.TRANSCODED_MAX) {
|
||||||
|
fps = getClosestFramerate({ fps, settings, type: 'HD_STANDARD' })
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`Computed output FPS ${fps} for resolution ${resolution}p`, { options, settings })
|
||||||
|
|
||||||
return fps
|
return fps
|
||||||
}
|
}
|
||||||
|
@ -34,12 +47,44 @@ export function computeOutputFPS (options: {
|
||||||
// Private
|
// Private
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function getClosestFramerateStandard (options: {
|
function buildTranscodingFPSOptions (maxFPS: number) {
|
||||||
fps: number
|
const STANDARD = [ 24, 25, 30 ].filter(v => v <= maxFPS)
|
||||||
type: 'HD_STANDARD' | 'STANDARD'
|
if (STANDARD.length === 0) STANDARD.push(maxFPS)
|
||||||
}) {
|
|
||||||
const { fps, type } = options
|
|
||||||
|
|
||||||
return VIDEO_TRANSCODING_FPS[type].slice(0)
|
const HD_STANDARD = [ 50, 60, maxFPS ].filter(v => v <= maxFPS)
|
||||||
.sort((a, b) => fps % a - fps % b)[0]
|
|
||||||
|
return {
|
||||||
|
HARD_MIN: 0.1,
|
||||||
|
|
||||||
|
TRANSCODED_MIN: 1,
|
||||||
|
|
||||||
|
TRANSCODED_MAX: maxFPS,
|
||||||
|
|
||||||
|
STANDARD,
|
||||||
|
HD_STANDARD,
|
||||||
|
|
||||||
|
AVERAGE: Math.min(30, maxFPS),
|
||||||
|
|
||||||
|
KEEP_ORIGIN_FPS_RESOLUTION_MIN: 720 // We keep the original FPS on high resolutions (720 minimum)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getClosestFramerate (options: {
|
||||||
|
fps: number
|
||||||
|
settings: ReturnType<typeof buildTranscodingFPSOptions>
|
||||||
|
type: Extract<keyof ReturnType<typeof buildTranscodingFPSOptions>, 'HD_STANDARD' | 'STANDARD'>
|
||||||
|
}) {
|
||||||
|
const { fps, settings, type } = options
|
||||||
|
|
||||||
|
const copy = [ ...settings[type] ]
|
||||||
|
|
||||||
|
// Biggest FPS first
|
||||||
|
const descSorted = copy.sort((a, b) => b - a)
|
||||||
|
// Find biggest FPS that can be divided by input FPS
|
||||||
|
const found = descSorted.find(e => fps % e === 0)
|
||||||
|
|
||||||
|
if (found) return found
|
||||||
|
|
||||||
|
// Approximation to the best result
|
||||||
|
return copy.sort((a, b) => fps % a - fps % b)[0]
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,7 @@ function checkMissedConfig () {
|
||||||
'transcoding.resolutions.0p', 'transcoding.resolutions.144p', 'transcoding.resolutions.240p', 'transcoding.resolutions.360p',
|
'transcoding.resolutions.0p', 'transcoding.resolutions.144p', 'transcoding.resolutions.240p', 'transcoding.resolutions.360p',
|
||||||
'transcoding.resolutions.480p', 'transcoding.resolutions.720p', 'transcoding.resolutions.1080p', 'transcoding.resolutions.1440p',
|
'transcoding.resolutions.480p', 'transcoding.resolutions.720p', 'transcoding.resolutions.1080p', 'transcoding.resolutions.1440p',
|
||||||
'transcoding.resolutions.2160p', 'transcoding.always_transcode_original_resolution', 'transcoding.remote_runners.enabled',
|
'transcoding.resolutions.2160p', 'transcoding.always_transcode_original_resolution', 'transcoding.remote_runners.enabled',
|
||||||
|
'transcoding.fps.max',
|
||||||
'video_studio.enabled', 'video_studio.remote_runners.enabled',
|
'video_studio.enabled', 'video_studio.remote_runners.enabled',
|
||||||
'video_file.update.enabled',
|
'video_file.update.enabled',
|
||||||
'remote_runners.stalled_jobs.vod', 'remote_runners.stalled_jobs.live',
|
'remote_runners.stalled_jobs.vod', 'remote_runners.stalled_jobs.live',
|
||||||
|
@ -85,7 +86,7 @@ function checkMissedConfig () {
|
||||||
'live.transcoding.resolutions.144p', 'live.transcoding.resolutions.240p', 'live.transcoding.resolutions.360p',
|
'live.transcoding.resolutions.144p', 'live.transcoding.resolutions.240p', 'live.transcoding.resolutions.360p',
|
||||||
'live.transcoding.resolutions.480p', 'live.transcoding.resolutions.720p', 'live.transcoding.resolutions.1080p',
|
'live.transcoding.resolutions.480p', 'live.transcoding.resolutions.720p', 'live.transcoding.resolutions.1080p',
|
||||||
'live.transcoding.resolutions.1440p', 'live.transcoding.resolutions.2160p', 'live.transcoding.always_transcode_original_resolution',
|
'live.transcoding.resolutions.1440p', 'live.transcoding.resolutions.2160p', 'live.transcoding.always_transcode_original_resolution',
|
||||||
'live.transcoding.remote_runners.enabled',
|
'live.transcoding.fps.max', 'live.transcoding.remote_runners.enabled',
|
||||||
'storyboards.enabled'
|
'storyboards.enabled'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -448,6 +448,9 @@ const CONFIG = {
|
||||||
get '1440p' () { return config.get<boolean>('transcoding.resolutions.1440p') },
|
get '1440p' () { return config.get<boolean>('transcoding.resolutions.1440p') },
|
||||||
get '2160p' () { return config.get<boolean>('transcoding.resolutions.2160p') }
|
get '2160p' () { return config.get<boolean>('transcoding.resolutions.2160p') }
|
||||||
},
|
},
|
||||||
|
FPS: {
|
||||||
|
get MAX () { return config.get<number>('transcoding.fps.max') }
|
||||||
|
},
|
||||||
HLS: {
|
HLS: {
|
||||||
get ENABLED () { return config.get<boolean>('transcoding.hls.enabled') },
|
get ENABLED () { return config.get<boolean>('transcoding.hls.enabled') },
|
||||||
get SPLIT_AUDIO_AND_VIDEO () { return config.get<boolean>('transcoding.hls.split_audio_and_video') }
|
get SPLIT_AUDIO_AND_VIDEO () { return config.get<boolean>('transcoding.hls.split_audio_and_video') }
|
||||||
|
@ -506,6 +509,11 @@ const CONFIG = {
|
||||||
get '1440p' () { return config.get<boolean>('live.transcoding.resolutions.1440p') },
|
get '1440p' () { return config.get<boolean>('live.transcoding.resolutions.1440p') },
|
||||||
get '2160p' () { return config.get<boolean>('live.transcoding.resolutions.2160p') }
|
get '2160p' () { return config.get<boolean>('live.transcoding.resolutions.2160p') }
|
||||||
},
|
},
|
||||||
|
|
||||||
|
FPS: {
|
||||||
|
get MAX () { return config.get<number>('live.transcoding.fps.max') }
|
||||||
|
},
|
||||||
|
|
||||||
REMOTE_RUNNERS: {
|
REMOTE_RUNNERS: {
|
||||||
get ENABLED () { return config.get<boolean>('live.transcoding.remote_runners.enabled') }
|
get ENABLED () { return config.get<boolean>('live.transcoding.remote_runners.enabled') }
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,8 +31,7 @@ import {
|
||||||
VideoRateType,
|
VideoRateType,
|
||||||
VideoResolution,
|
VideoResolution,
|
||||||
VideoState,
|
VideoState,
|
||||||
VideoStateType,
|
VideoStateType
|
||||||
VideoTranscodingFPS
|
|
||||||
} from '@peertube/peertube-models'
|
} from '@peertube/peertube-models'
|
||||||
import { isTestInstance, isTestOrDevInstance, root } from '@peertube/peertube-node-utils'
|
import { isTestInstance, isTestOrDevInstance, root } from '@peertube/peertube-node-utils'
|
||||||
import { RepeatOptions } from 'bullmq'
|
import { RepeatOptions } from 'bullmq'
|
||||||
|
@ -41,20 +40,20 @@ import { readJsonSync } from 'fs-extra/esm'
|
||||||
import invert from 'lodash-es/invert.js'
|
import invert from 'lodash-es/invert.js'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
// Do not use barrels, remain constants as independent as possible
|
// Do not use barrels, remain constants as independent as possible
|
||||||
|
import { cpus } from 'os'
|
||||||
import { parseDurationToMs, sanitizeHost, sanitizeUrl } from '../helpers/core-utils.js'
|
import { parseDurationToMs, sanitizeHost, sanitizeUrl } from '../helpers/core-utils.js'
|
||||||
import { CONFIG, registerConfigChangedHandler } from './config.js'
|
import { CONFIG, registerConfigChangedHandler } from './config.js'
|
||||||
import { cpus } from 'os'
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const LAST_MIGRATION_VERSION = 865
|
export const LAST_MIGRATION_VERSION = 865
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const API_VERSION = 'v1'
|
export const API_VERSION = 'v1'
|
||||||
const PEERTUBE_VERSION: string = readJsonSync(join(root(), 'package.json')).version
|
export const PEERTUBE_VERSION: string = readJsonSync(join(root(), 'package.json')).version
|
||||||
|
|
||||||
const PAGINATION = {
|
export const PAGINATION = {
|
||||||
GLOBAL: {
|
GLOBAL: {
|
||||||
COUNT: {
|
COUNT: {
|
||||||
DEFAULT: 15,
|
DEFAULT: 15,
|
||||||
|
@ -68,7 +67,7 @@ const PAGINATION = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const WEBSERVER = {
|
export const WEBSERVER = {
|
||||||
URL: '',
|
URL: '',
|
||||||
HOST: '',
|
HOST: '',
|
||||||
SCHEME: '',
|
SCHEME: '',
|
||||||
|
@ -84,7 +83,7 @@ const WEBSERVER = {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sortable columns per schema
|
// Sortable columns per schema
|
||||||
const SORTABLE_COLUMNS = {
|
export const SORTABLE_COLUMNS = {
|
||||||
ADMIN_USERS: [ 'id', 'username', 'videoQuotaUsed', 'createdAt', 'lastLoginDate', 'role' ],
|
ADMIN_USERS: [ 'id', 'username', 'videoQuotaUsed', 'createdAt', 'lastLoginDate', 'role' ],
|
||||||
USER_SUBSCRIPTIONS: [ 'id', 'createdAt' ],
|
USER_SUBSCRIPTIONS: [ 'id', 'createdAt' ],
|
||||||
ACCOUNTS: [ 'createdAt' ],
|
ACCOUNTS: [ 'createdAt' ],
|
||||||
|
@ -149,7 +148,7 @@ const SORTABLE_COLUMNS = {
|
||||||
VIDEO_REDUNDANCIES: [ 'name' ]
|
VIDEO_REDUNDANCIES: [ 'name' ]
|
||||||
}
|
}
|
||||||
|
|
||||||
const ROUTE_CACHE_LIFETIME = {
|
export const ROUTE_CACHE_LIFETIME = {
|
||||||
FEEDS: '15 minutes',
|
FEEDS: '15 minutes',
|
||||||
ROBOTS: '2 hours',
|
ROBOTS: '2 hours',
|
||||||
SITEMAP: '1 day',
|
SITEMAP: '1 day',
|
||||||
|
@ -166,27 +165,27 @@ const ROUTE_CACHE_LIFETIME = {
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
// Number of points we add/remove after a successful/bad request
|
// Number of points we add/remove after a successful/bad request
|
||||||
const ACTOR_FOLLOW_SCORE = {
|
export const ACTOR_FOLLOW_SCORE = {
|
||||||
PENALTY: -10,
|
PENALTY: -10,
|
||||||
BONUS: 10,
|
BONUS: 10,
|
||||||
BASE: 1000,
|
BASE: 1000,
|
||||||
MAX: 10000
|
MAX: 10000
|
||||||
}
|
}
|
||||||
|
|
||||||
const FOLLOW_STATES: { [ id: string ]: FollowState } = {
|
export const FOLLOW_STATES: { [ id: string ]: FollowState } = {
|
||||||
PENDING: 'pending',
|
PENDING: 'pending',
|
||||||
ACCEPTED: 'accepted',
|
ACCEPTED: 'accepted',
|
||||||
REJECTED: 'rejected'
|
REJECTED: 'rejected'
|
||||||
}
|
}
|
||||||
|
|
||||||
const REMOTE_SCHEME = {
|
export const REMOTE_SCHEME = {
|
||||||
HTTP: 'https',
|
HTTP: 'https',
|
||||||
WS: 'wss'
|
WS: 'wss'
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const JOB_ATTEMPTS: { [id in JobType]: number } = {
|
export const JOB_ATTEMPTS: { [id in JobType]: number } = {
|
||||||
'activitypub-http-broadcast': 1,
|
'activitypub-http-broadcast': 1,
|
||||||
'activitypub-http-broadcast-parallel': 1,
|
'activitypub-http-broadcast-parallel': 1,
|
||||||
'activitypub-http-unicast': 1,
|
'activitypub-http-unicast': 1,
|
||||||
|
@ -217,7 +216,7 @@ const JOB_ATTEMPTS: { [id in JobType]: number } = {
|
||||||
'video-transcription': 2
|
'video-transcription': 2
|
||||||
}
|
}
|
||||||
// Excluded keys are jobs that can be configured by admins
|
// Excluded keys are jobs that can be configured by admins
|
||||||
const JOB_CONCURRENCY: { [id in Exclude<JobType, 'video-transcoding' | 'video-import'>]: number } = {
|
export const JOB_CONCURRENCY: { [id in Exclude<JobType, 'video-transcoding' | 'video-import'>]: number } = {
|
||||||
'activitypub-http-broadcast': 1,
|
'activitypub-http-broadcast': 1,
|
||||||
'activitypub-http-broadcast-parallel': 30,
|
'activitypub-http-broadcast-parallel': 30,
|
||||||
'activitypub-http-unicast': 30,
|
'activitypub-http-unicast': 30,
|
||||||
|
@ -245,7 +244,7 @@ const JOB_CONCURRENCY: { [id in Exclude<JobType, 'video-transcoding' | 'video-im
|
||||||
'import-user-archive': 1,
|
'import-user-archive': 1,
|
||||||
'video-transcription': 1
|
'video-transcription': 1
|
||||||
}
|
}
|
||||||
const JOB_TTL: { [id in JobType]: number } = {
|
export const JOB_TTL: { [id in JobType]: number } = {
|
||||||
'activitypub-http-broadcast': 60000 * 10, // 10 minutes
|
'activitypub-http-broadcast': 60000 * 10, // 10 minutes
|
||||||
'activitypub-http-broadcast-parallel': 60000 * 10, // 10 minutes
|
'activitypub-http-broadcast-parallel': 60000 * 10, // 10 minutes
|
||||||
'activitypub-http-unicast': 60000 * 10, // 10 minutes
|
'activitypub-http-unicast': 60000 * 10, // 10 minutes
|
||||||
|
@ -275,7 +274,7 @@ const JOB_TTL: { [id in JobType]: number } = {
|
||||||
'import-user-archive': 60000 * 60 * 24, // 24 hours
|
'import-user-archive': 60000 * 60 * 24, // 24 hours
|
||||||
'video-transcription': 1000 * 3600 * 6 // 6 hours
|
'video-transcription': 1000 * 3600 * 6 // 6 hours
|
||||||
}
|
}
|
||||||
const REPEAT_JOBS: { [ id in JobType ]?: RepeatOptions } = {
|
export const REPEAT_JOBS: { [ id in JobType ]?: RepeatOptions } = {
|
||||||
'videos-views-stats': {
|
'videos-views-stats': {
|
||||||
pattern: randomInt(1, 20) + ' * * * *' // Between 1-20 minutes past the hour
|
pattern: randomInt(1, 20) + ' * * * *' // Between 1-20 minutes past the hour
|
||||||
},
|
},
|
||||||
|
@ -283,13 +282,13 @@ const REPEAT_JOBS: { [ id in JobType ]?: RepeatOptions } = {
|
||||||
pattern: '30 5 * * ' + randomInt(0, 7) // 1 time per week (random day) at 5:30 AM
|
pattern: '30 5 * * ' + randomInt(0, 7) // 1 time per week (random day) at 5:30 AM
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const JOB_PRIORITY = {
|
export const JOB_PRIORITY = {
|
||||||
TRANSCODING: 100,
|
TRANSCODING: 100,
|
||||||
VIDEO_STUDIO: 150,
|
VIDEO_STUDIO: 150,
|
||||||
TRANSCRIPTION: 200
|
TRANSCRIPTION: 200
|
||||||
}
|
}
|
||||||
|
|
||||||
const JOB_REMOVAL_OPTIONS = {
|
export const JOB_REMOVAL_OPTIONS = {
|
||||||
COUNT: 10000, // Max jobs to store
|
COUNT: 10000, // Max jobs to store
|
||||||
|
|
||||||
SUCCESS: { // Success jobs
|
SUCCESS: { // Success jobs
|
||||||
|
@ -306,32 +305,32 @@ const JOB_REMOVAL_OPTIONS = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const VIDEO_IMPORT_TIMEOUT = Math.floor(JOB_TTL['video-import'] * 0.9)
|
export const VIDEO_IMPORT_TIMEOUT = Math.floor(JOB_TTL['video-import'] * 0.9)
|
||||||
|
|
||||||
const RUNNER_JOBS = {
|
export const RUNNER_JOBS = {
|
||||||
MAX_FAILURES: 5,
|
MAX_FAILURES: 5,
|
||||||
LAST_CONTACT_UPDATE_INTERVAL: 30000
|
LAST_CONTACT_UPDATE_INTERVAL: 30000
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const BROADCAST_CONCURRENCY = 30 // How many requests in parallel we do in activitypub-http-broadcast job
|
export const BROADCAST_CONCURRENCY = 30 // How many requests in parallel we do in activitypub-http-broadcast job
|
||||||
const CRAWL_REQUEST_CONCURRENCY = 1 // How many requests in parallel to fetch remote data (likes, shares...)
|
export const CRAWL_REQUEST_CONCURRENCY = 1 // How many requests in parallel to fetch remote data (likes, shares...)
|
||||||
|
|
||||||
const AP_CLEANER = {
|
export const AP_CLEANER = {
|
||||||
CONCURRENCY: 10, // How many requests in parallel we do in activitypub-cleaner job
|
CONCURRENCY: 10, // How many requests in parallel we do in activitypub-cleaner job
|
||||||
UNAVAILABLE_TRESHOLD: 3, // How many attempts we do before removing an unavailable remote resource
|
UNAVAILABLE_TRESHOLD: 3, // How many attempts we do before removing an unavailable remote resource
|
||||||
PERIOD: parseDurationToMs('1 week') // /!\ Has to be sync with REPEAT_JOBS
|
PERIOD: parseDurationToMs('1 week') // /!\ Has to be sync with REPEAT_JOBS
|
||||||
}
|
}
|
||||||
|
|
||||||
const REQUEST_TIMEOUTS = {
|
export const REQUEST_TIMEOUTS = {
|
||||||
DEFAULT: 7000, // 7 seconds
|
DEFAULT: 7000, // 7 seconds
|
||||||
FILE: 30000, // 30 seconds
|
FILE: 30000, // 30 seconds
|
||||||
VIDEO_FILE: 60000, // 1 minute
|
VIDEO_FILE: 60000, // 1 minute
|
||||||
REDUNDANCY: JOB_TTL['video-redundancy']
|
REDUNDANCY: JOB_TTL['video-redundancy']
|
||||||
}
|
}
|
||||||
|
|
||||||
const SCHEDULER_INTERVALS_MS = {
|
export const SCHEDULER_INTERVALS_MS = {
|
||||||
RUNNER_JOB_WATCH_DOG: Math.min(CONFIG.REMOTE_RUNNERS.STALLED_JOBS.VOD, CONFIG.REMOTE_RUNNERS.STALLED_JOBS.LIVE),
|
RUNNER_JOB_WATCH_DOG: Math.min(CONFIG.REMOTE_RUNNERS.STALLED_JOBS.VOD, CONFIG.REMOTE_RUNNERS.STALLED_JOBS.LIVE),
|
||||||
ACTOR_FOLLOW_SCORES: 60000 * 60, // 1 hour
|
ACTOR_FOLLOW_SCORES: 60000 * 60, // 1 hour
|
||||||
REMOVE_OLD_JOBS: 60000 * 60, // 1 hour
|
REMOVE_OLD_JOBS: 60000 * 60, // 1 hour
|
||||||
|
@ -352,7 +351,7 @@ const SCHEDULER_INTERVALS_MS = {
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const CONSTRAINTS_FIELDS = {
|
export const CONSTRAINTS_FIELDS = {
|
||||||
USERS: {
|
USERS: {
|
||||||
NAME: { min: 1, max: 120 }, // Length
|
NAME: { min: 1, max: 120 }, // Length
|
||||||
DESCRIPTION: { min: 3, max: 1000 }, // Length
|
DESCRIPTION: { min: 3, max: 1000 }, // Length
|
||||||
|
@ -515,40 +514,30 @@ const CONSTRAINTS_FIELDS = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const VIEW_LIFETIME = {
|
export const VIEW_LIFETIME = {
|
||||||
VIEW: CONFIG.VIEWS.VIDEOS.VIEW_EXPIRATION,
|
VIEW: CONFIG.VIEWS.VIDEOS.VIEW_EXPIRATION,
|
||||||
VIEWER_COUNTER: 60000 * 2, // 2 minutes
|
VIEWER_COUNTER: 60000 * 2, // 2 minutes
|
||||||
VIEWER_STATS: 60000 * 60 // 1 hour
|
VIEWER_STATS: 60000 * 60 // 1 hour
|
||||||
}
|
}
|
||||||
let VIEWER_SYNC_REDIS = 30000 // Sync viewer into redis
|
export let VIEWER_SYNC_REDIS = 30000 // Sync viewer into redis
|
||||||
|
|
||||||
const MAX_LOCAL_VIEWER_WATCH_SECTIONS = 100
|
export const MAX_LOCAL_VIEWER_WATCH_SECTIONS = 100
|
||||||
|
|
||||||
let CONTACT_FORM_LIFETIME = 60000 * 60 // 1 hour
|
export let CONTACT_FORM_LIFETIME = 60000 * 60 // 1 hour
|
||||||
|
|
||||||
const VIDEO_TRANSCODING_FPS: VideoTranscodingFPS = {
|
export const DEFAULT_AUDIO_RESOLUTION = VideoResolution.H_480P
|
||||||
HARD_MIN: 0.1,
|
export const DEFAULT_AUDIO_MERGE_RESOLUTION = 25
|
||||||
SOFT_MIN: 1,
|
|
||||||
STANDARD: [ 24, 25, 30 ],
|
|
||||||
HD_STANDARD: [ 50, 60 ],
|
|
||||||
AUDIO_MERGE: 25,
|
|
||||||
AVERAGE: 30,
|
|
||||||
SOFT_MAX: 60,
|
|
||||||
KEEP_ORIGIN_FPS_RESOLUTION_MIN: 720 // We keep the original FPS on high resolutions (720 minimum)
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEFAULT_AUDIO_RESOLUTION = VideoResolution.H_480P
|
export const VIDEO_RATE_TYPES: { [ id: string ]: VideoRateType } = {
|
||||||
|
|
||||||
const VIDEO_RATE_TYPES: { [ id: string ]: VideoRateType } = {
|
|
||||||
LIKE: 'like',
|
LIKE: 'like',
|
||||||
DISLIKE: 'dislike'
|
DISLIKE: 'dislike'
|
||||||
}
|
}
|
||||||
|
|
||||||
const USER_IMPORT = {
|
export const USER_IMPORT = {
|
||||||
MAX_PLAYLIST_ELEMENTS: 1000
|
MAX_PLAYLIST_ELEMENTS: 1000
|
||||||
}
|
}
|
||||||
|
|
||||||
const FFMPEG_NICE = {
|
export const FFMPEG_NICE = {
|
||||||
// parent process defaults to niceness = 0
|
// parent process defaults to niceness = 0
|
||||||
// reminder: lower = higher priority, max value is 19, lowest is -20
|
// reminder: lower = higher priority, max value is 19, lowest is -20
|
||||||
LIVE: 5, // prioritize over VOD and THUMBNAIL
|
LIVE: 5, // prioritize over VOD and THUMBNAIL
|
||||||
|
@ -556,7 +545,7 @@ const FFMPEG_NICE = {
|
||||||
VOD: 15
|
VOD: 15
|
||||||
}
|
}
|
||||||
|
|
||||||
const VIDEO_CATEGORIES = {
|
export const VIDEO_CATEGORIES = {
|
||||||
1: 'Music',
|
1: 'Music',
|
||||||
2: 'Films',
|
2: 'Films',
|
||||||
3: 'Vehicles',
|
3: 'Vehicles',
|
||||||
|
@ -578,7 +567,7 @@ const VIDEO_CATEGORIES = {
|
||||||
}
|
}
|
||||||
|
|
||||||
// See https://creativecommons.org/licenses/?lang=en
|
// See https://creativecommons.org/licenses/?lang=en
|
||||||
const VIDEO_LICENCES = {
|
export const VIDEO_LICENCES = {
|
||||||
1: 'Attribution',
|
1: 'Attribution',
|
||||||
2: 'Attribution - Share Alike',
|
2: 'Attribution - Share Alike',
|
||||||
3: 'Attribution - No Derivatives',
|
3: 'Attribution - No Derivatives',
|
||||||
|
@ -588,9 +577,9 @@ const VIDEO_LICENCES = {
|
||||||
7: 'Public Domain Dedication'
|
7: 'Public Domain Dedication'
|
||||||
}
|
}
|
||||||
|
|
||||||
const VIDEO_LANGUAGES: { [id: string]: string } = {}
|
export const VIDEO_LANGUAGES: { [id: string]: string } = {}
|
||||||
|
|
||||||
const VIDEO_PRIVACIES: { [ id in VideoPrivacyType ]: string } = {
|
export const VIDEO_PRIVACIES: { [ id in VideoPrivacyType ]: string } = {
|
||||||
[VideoPrivacy.PUBLIC]: 'Public',
|
[VideoPrivacy.PUBLIC]: 'Public',
|
||||||
[VideoPrivacy.UNLISTED]: 'Unlisted',
|
[VideoPrivacy.UNLISTED]: 'Unlisted',
|
||||||
[VideoPrivacy.PRIVATE]: 'Private',
|
[VideoPrivacy.PRIVATE]: 'Private',
|
||||||
|
@ -598,7 +587,7 @@ const VIDEO_PRIVACIES: { [ id in VideoPrivacyType ]: string } = {
|
||||||
[VideoPrivacy.PASSWORD_PROTECTED]: 'Password protected'
|
[VideoPrivacy.PASSWORD_PROTECTED]: 'Password protected'
|
||||||
}
|
}
|
||||||
|
|
||||||
const VIDEO_STATES: { [ id in VideoStateType ]: string } = {
|
export const VIDEO_STATES: { [ id in VideoStateType ]: string } = {
|
||||||
[VideoState.PUBLISHED]: 'Published',
|
[VideoState.PUBLISHED]: 'Published',
|
||||||
[VideoState.TO_TRANSCODE]: 'To transcode',
|
[VideoState.TO_TRANSCODE]: 'To transcode',
|
||||||
[VideoState.TO_IMPORT]: 'To import',
|
[VideoState.TO_IMPORT]: 'To import',
|
||||||
|
@ -612,7 +601,7 @@ const VIDEO_STATES: { [ id in VideoStateType ]: string } = {
|
||||||
[VideoState.TO_MOVE_TO_FILE_SYSTEM_FAILED]: 'Move to file system failed'
|
[VideoState.TO_MOVE_TO_FILE_SYSTEM_FAILED]: 'Move to file system failed'
|
||||||
}
|
}
|
||||||
|
|
||||||
const VIDEO_IMPORT_STATES: { [ id in VideoImportStateType ]: string } = {
|
export const VIDEO_IMPORT_STATES: { [ id in VideoImportStateType ]: string } = {
|
||||||
[VideoImportState.FAILED]: 'Failed',
|
[VideoImportState.FAILED]: 'Failed',
|
||||||
[VideoImportState.PENDING]: 'Pending',
|
[VideoImportState.PENDING]: 'Pending',
|
||||||
[VideoImportState.SUCCESS]: 'Success',
|
[VideoImportState.SUCCESS]: 'Success',
|
||||||
|
@ -621,37 +610,37 @@ const VIDEO_IMPORT_STATES: { [ id in VideoImportStateType ]: string } = {
|
||||||
[VideoImportState.PROCESSING]: 'Processing'
|
[VideoImportState.PROCESSING]: 'Processing'
|
||||||
}
|
}
|
||||||
|
|
||||||
const VIDEO_CHANNEL_SYNC_STATE: { [ id in VideoChannelSyncStateType ]: string } = {
|
export const VIDEO_CHANNEL_SYNC_STATE: { [ id in VideoChannelSyncStateType ]: string } = {
|
||||||
[VideoChannelSyncState.FAILED]: 'Failed',
|
[VideoChannelSyncState.FAILED]: 'Failed',
|
||||||
[VideoChannelSyncState.SYNCED]: 'Synchronized',
|
[VideoChannelSyncState.SYNCED]: 'Synchronized',
|
||||||
[VideoChannelSyncState.PROCESSING]: 'Processing',
|
[VideoChannelSyncState.PROCESSING]: 'Processing',
|
||||||
[VideoChannelSyncState.WAITING_FIRST_RUN]: 'Waiting first run'
|
[VideoChannelSyncState.WAITING_FIRST_RUN]: 'Waiting first run'
|
||||||
}
|
}
|
||||||
|
|
||||||
const ABUSE_STATES: { [ id in AbuseStateType ]: string } = {
|
export const ABUSE_STATES: { [ id in AbuseStateType ]: string } = {
|
||||||
[AbuseState.PENDING]: 'Pending',
|
[AbuseState.PENDING]: 'Pending',
|
||||||
[AbuseState.REJECTED]: 'Rejected',
|
[AbuseState.REJECTED]: 'Rejected',
|
||||||
[AbuseState.ACCEPTED]: 'Accepted'
|
[AbuseState.ACCEPTED]: 'Accepted'
|
||||||
}
|
}
|
||||||
|
|
||||||
const USER_REGISTRATION_STATES: { [ id in UserRegistrationStateType ]: string } = {
|
export const USER_REGISTRATION_STATES: { [ id in UserRegistrationStateType ]: string } = {
|
||||||
[UserRegistrationState.PENDING]: 'Pending',
|
[UserRegistrationState.PENDING]: 'Pending',
|
||||||
[UserRegistrationState.REJECTED]: 'Rejected',
|
[UserRegistrationState.REJECTED]: 'Rejected',
|
||||||
[UserRegistrationState.ACCEPTED]: 'Accepted'
|
[UserRegistrationState.ACCEPTED]: 'Accepted'
|
||||||
}
|
}
|
||||||
|
|
||||||
const VIDEO_PLAYLIST_PRIVACIES: { [ id in VideoPlaylistPrivacyType ]: string } = {
|
export const VIDEO_PLAYLIST_PRIVACIES: { [ id in VideoPlaylistPrivacyType ]: string } = {
|
||||||
[VideoPlaylistPrivacy.PUBLIC]: 'Public',
|
[VideoPlaylistPrivacy.PUBLIC]: 'Public',
|
||||||
[VideoPlaylistPrivacy.UNLISTED]: 'Unlisted',
|
[VideoPlaylistPrivacy.UNLISTED]: 'Unlisted',
|
||||||
[VideoPlaylistPrivacy.PRIVATE]: 'Private'
|
[VideoPlaylistPrivacy.PRIVATE]: 'Private'
|
||||||
}
|
}
|
||||||
|
|
||||||
const VIDEO_PLAYLIST_TYPES: { [ id in VideoPlaylistType_Type ]: string } = {
|
export const VIDEO_PLAYLIST_TYPES: { [ id in VideoPlaylistType_Type ]: string } = {
|
||||||
[VideoPlaylistType.REGULAR]: 'Regular',
|
[VideoPlaylistType.REGULAR]: 'Regular',
|
||||||
[VideoPlaylistType.WATCH_LATER]: 'Watch later'
|
[VideoPlaylistType.WATCH_LATER]: 'Watch later'
|
||||||
}
|
}
|
||||||
|
|
||||||
const RUNNER_JOB_STATES: { [ id in RunnerJobStateType ]: string } = {
|
export const RUNNER_JOB_STATES: { [ id in RunnerJobStateType ]: string } = {
|
||||||
[RunnerJobState.PROCESSING]: 'Processing',
|
[RunnerJobState.PROCESSING]: 'Processing',
|
||||||
[RunnerJobState.COMPLETED]: 'Completed',
|
[RunnerJobState.COMPLETED]: 'Completed',
|
||||||
[RunnerJobState.COMPLETING]: 'Completing',
|
[RunnerJobState.COMPLETING]: 'Completing',
|
||||||
|
@ -663,27 +652,27 @@ const RUNNER_JOB_STATES: { [ id in RunnerJobStateType ]: string } = {
|
||||||
[RunnerJobState.PARENT_CANCELLED]: 'Parent job cancelled'
|
[RunnerJobState.PARENT_CANCELLED]: 'Parent job cancelled'
|
||||||
}
|
}
|
||||||
|
|
||||||
const USER_EXPORT_STATES: { [ id in UserExportStateType ]: string } = {
|
export const USER_EXPORT_STATES: { [ id in UserExportStateType ]: string } = {
|
||||||
[UserExportState.PENDING]: 'Pending',
|
[UserExportState.PENDING]: 'Pending',
|
||||||
[UserExportState.PROCESSING]: 'Processing',
|
[UserExportState.PROCESSING]: 'Processing',
|
||||||
[UserExportState.COMPLETED]: 'Completed',
|
[UserExportState.COMPLETED]: 'Completed',
|
||||||
[UserExportState.ERRORED]: 'Failed'
|
[UserExportState.ERRORED]: 'Failed'
|
||||||
}
|
}
|
||||||
|
|
||||||
const USER_IMPORT_STATES: { [ id in UserImportStateType ]: string } = {
|
export const USER_IMPORT_STATES: { [ id in UserImportStateType ]: string } = {
|
||||||
[UserImportState.PENDING]: 'Pending',
|
[UserImportState.PENDING]: 'Pending',
|
||||||
[UserImportState.PROCESSING]: 'Processing',
|
[UserImportState.PROCESSING]: 'Processing',
|
||||||
[UserImportState.COMPLETED]: 'Completed',
|
[UserImportState.COMPLETED]: 'Completed',
|
||||||
[UserImportState.ERRORED]: 'Failed'
|
[UserImportState.ERRORED]: 'Failed'
|
||||||
}
|
}
|
||||||
|
|
||||||
const VIDEO_COMMENTS_POLICY: { [ id in VideoCommentPolicyType ]: string } = {
|
export const VIDEO_COMMENTS_POLICY: { [ id in VideoCommentPolicyType ]: string } = {
|
||||||
[VideoCommentPolicy.DISABLED]: 'Disabled',
|
[VideoCommentPolicy.DISABLED]: 'Disabled',
|
||||||
[VideoCommentPolicy.ENABLED]: 'Enabled',
|
[VideoCommentPolicy.ENABLED]: 'Enabled',
|
||||||
[VideoCommentPolicy.REQUIRES_APPROVAL]: 'Requires approval'
|
[VideoCommentPolicy.REQUIRES_APPROVAL]: 'Requires approval'
|
||||||
}
|
}
|
||||||
|
|
||||||
const MIMETYPES = {
|
export const MIMETYPES = {
|
||||||
AUDIO: {
|
AUDIO: {
|
||||||
MIMETYPE_EXT: {
|
MIMETYPE_EXT: {
|
||||||
'audio/mpeg': '.mp3',
|
'audio/mpeg': '.mp3',
|
||||||
|
@ -769,7 +758,7 @@ MIMETYPES.AUDIO.EXT_MIMETYPE = invert(MIMETYPES.AUDIO.MIMETYPE_EXT)
|
||||||
MIMETYPES.IMAGE.EXT_MIMETYPE = invert(MIMETYPES.IMAGE.MIMETYPE_EXT)
|
MIMETYPES.IMAGE.EXT_MIMETYPE = invert(MIMETYPES.IMAGE.MIMETYPE_EXT)
|
||||||
MIMETYPES.VIDEO_CAPTIONS.EXT_MIMETYPE = invert(MIMETYPES.VIDEO_CAPTIONS.MIMETYPE_EXT)
|
MIMETYPES.VIDEO_CAPTIONS.EXT_MIMETYPE = invert(MIMETYPES.VIDEO_CAPTIONS.MIMETYPE_EXT)
|
||||||
|
|
||||||
const BINARY_CONTENT_TYPES = new Set([
|
export const BINARY_CONTENT_TYPES = new Set([
|
||||||
'binary/octet-stream',
|
'binary/octet-stream',
|
||||||
'application/octet-stream',
|
'application/octet-stream',
|
||||||
'application/x-binary'
|
'application/x-binary'
|
||||||
|
@ -777,7 +766,7 @@ const BINARY_CONTENT_TYPES = new Set([
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const OVERVIEWS = {
|
export const OVERVIEWS = {
|
||||||
VIDEOS: {
|
VIDEOS: {
|
||||||
SAMPLE_THRESHOLD: 6,
|
SAMPLE_THRESHOLD: 6,
|
||||||
SAMPLES_COUNT: 20
|
SAMPLES_COUNT: 20
|
||||||
|
@ -786,9 +775,9 @@ const OVERVIEWS = {
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const SERVER_ACTOR_NAME = 'peertube'
|
export const SERVER_ACTOR_NAME = 'peertube'
|
||||||
|
|
||||||
const ACTIVITY_PUB = {
|
export const ACTIVITY_PUB = {
|
||||||
POTENTIAL_ACCEPT_HEADERS: [
|
POTENTIAL_ACCEPT_HEADERS: [
|
||||||
'application/activity+json',
|
'application/activity+json',
|
||||||
'application/ld+json',
|
'application/ld+json',
|
||||||
|
@ -803,7 +792,7 @@ const ACTIVITY_PUB = {
|
||||||
VIDEO_PLAYLIST_REFRESH_INTERVAL: 3600 * 24 * 1000 * 2 // 2 days
|
VIDEO_PLAYLIST_REFRESH_INTERVAL: 3600 * 24 * 1000 * 2 // 2 days
|
||||||
}
|
}
|
||||||
|
|
||||||
const ACTIVITY_PUB_ACTOR_TYPES: { [ id: string ]: ActivityPubActorType } = {
|
export const ACTIVITY_PUB_ACTOR_TYPES: { [ id: string ]: ActivityPubActorType } = {
|
||||||
GROUP: 'Group',
|
GROUP: 'Group',
|
||||||
PERSON: 'Person',
|
PERSON: 'Person',
|
||||||
APPLICATION: 'Application',
|
APPLICATION: 'Application',
|
||||||
|
@ -811,7 +800,7 @@ const ACTIVITY_PUB_ACTOR_TYPES: { [ id: string ]: ActivityPubActorType } = {
|
||||||
SERVICE: 'Service'
|
SERVICE: 'Service'
|
||||||
}
|
}
|
||||||
|
|
||||||
const HTTP_SIGNATURE = {
|
export const HTTP_SIGNATURE = {
|
||||||
HEADER_NAME: 'signature',
|
HEADER_NAME: 'signature',
|
||||||
ALGORITHM: 'rsa-sha256',
|
ALGORITHM: 'rsa-sha256',
|
||||||
HEADERS_TO_SIGN_WITH_PAYLOAD: [ '(request-target)', 'host', 'date', 'digest' ],
|
HEADERS_TO_SIGN_WITH_PAYLOAD: [ '(request-target)', 'host', 'date', 'digest' ],
|
||||||
|
@ -821,27 +810,27 @@ const HTTP_SIGNATURE = {
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
let PRIVATE_RSA_KEY_SIZE = 2048
|
export let PRIVATE_RSA_KEY_SIZE = 2048
|
||||||
|
|
||||||
// Password encryption
|
// Password encryption
|
||||||
const BCRYPT_SALT_SIZE = 10
|
export const BCRYPT_SALT_SIZE = 10
|
||||||
|
|
||||||
const ENCRYPTION = {
|
export const ENCRYPTION = {
|
||||||
ALGORITHM: 'aes-256-cbc',
|
ALGORITHM: 'aes-256-cbc',
|
||||||
IV: 16,
|
IV: 16,
|
||||||
SALT: 'peertube',
|
SALT: 'peertube',
|
||||||
ENCODING: 'hex' as Encoding
|
ENCODING: 'hex' as Encoding
|
||||||
}
|
}
|
||||||
|
|
||||||
const USER_PASSWORD_RESET_LIFETIME = 60000 * 60 // 60 minutes
|
export const USER_PASSWORD_RESET_LIFETIME = 60000 * 60 // 60 minutes
|
||||||
const USER_PASSWORD_CREATE_LIFETIME = 60000 * 60 * 24 * 7 // 7 days
|
export const USER_PASSWORD_CREATE_LIFETIME = 60000 * 60 * 24 * 7 // 7 days
|
||||||
|
|
||||||
const TWO_FACTOR_AUTH_REQUEST_TOKEN_LIFETIME = 60000 * 10 // 10 minutes
|
export const TWO_FACTOR_AUTH_REQUEST_TOKEN_LIFETIME = 60000 * 10 // 10 minutes
|
||||||
let JWT_TOKEN_USER_EXPORT_FILE_LIFETIME = '15 minutes'
|
export let JWT_TOKEN_USER_EXPORT_FILE_LIFETIME = '15 minutes'
|
||||||
|
|
||||||
const EMAIL_VERIFY_LIFETIME = 60000 * 60 // 60 minutes
|
export const EMAIL_VERIFY_LIFETIME = 60000 * 60 // 60 minutes
|
||||||
|
|
||||||
const NSFW_POLICY_TYPES: { [ id: string ]: NSFWPolicyType } = {
|
export const NSFW_POLICY_TYPES: { [ id: string ]: NSFWPolicyType } = {
|
||||||
DO_NOT_LIST: 'do_not_list',
|
DO_NOT_LIST: 'do_not_list',
|
||||||
BLUR: 'blur',
|
BLUR: 'blur',
|
||||||
DISPLAY: 'display'
|
DISPLAY: 'display'
|
||||||
|
@ -849,13 +838,13 @@ const NSFW_POLICY_TYPES: { [ id: string ]: NSFWPolicyType } = {
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const USER_EXPORT_MAX_ITEMS = 1000
|
export const USER_EXPORT_MAX_ITEMS = 1000
|
||||||
const USER_EXPORT_FILE_PREFIX = 'user-export-'
|
export const USER_EXPORT_FILE_PREFIX = 'user-export-'
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
// Express static paths (router)
|
// Express static paths (router)
|
||||||
const STATIC_PATHS = {
|
export const STATIC_PATHS = {
|
||||||
// TODO: deprecated in v6, to remove
|
// TODO: deprecated in v6, to remove
|
||||||
THUMBNAILS: '/static/thumbnails/',
|
THUMBNAILS: '/static/thumbnails/',
|
||||||
|
|
||||||
|
@ -874,7 +863,7 @@ const STATIC_PATHS = {
|
||||||
PRIVATE_HLS: '/static/streaming-playlists/hls/private/'
|
PRIVATE_HLS: '/static/streaming-playlists/hls/private/'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const DOWNLOAD_PATHS = {
|
export const DOWNLOAD_PATHS = {
|
||||||
TORRENTS: '/download/torrents/',
|
TORRENTS: '/download/torrents/',
|
||||||
GENERATE_VIDEO: '/download/videos/generate/',
|
GENERATE_VIDEO: '/download/videos/generate/',
|
||||||
WEB_VIDEOS: '/download/web-videos/',
|
WEB_VIDEOS: '/download/web-videos/',
|
||||||
|
@ -882,7 +871,7 @@ const DOWNLOAD_PATHS = {
|
||||||
USER_EXPORTS: '/download/user-exports/',
|
USER_EXPORTS: '/download/user-exports/',
|
||||||
ORIGINAL_VIDEO_FILE: '/download/original-video-files/'
|
ORIGINAL_VIDEO_FILE: '/download/original-video-files/'
|
||||||
}
|
}
|
||||||
const LAZY_STATIC_PATHS = {
|
export const LAZY_STATIC_PATHS = {
|
||||||
THUMBNAILS: '/lazy-static/thumbnails/',
|
THUMBNAILS: '/lazy-static/thumbnails/',
|
||||||
BANNERS: '/lazy-static/banners/',
|
BANNERS: '/lazy-static/banners/',
|
||||||
AVATARS: '/lazy-static/avatars/',
|
AVATARS: '/lazy-static/avatars/',
|
||||||
|
@ -891,7 +880,7 @@ const LAZY_STATIC_PATHS = {
|
||||||
TORRENTS: '/lazy-static/torrents/',
|
TORRENTS: '/lazy-static/torrents/',
|
||||||
STORYBOARDS: '/lazy-static/storyboards/'
|
STORYBOARDS: '/lazy-static/storyboards/'
|
||||||
}
|
}
|
||||||
const OBJECT_STORAGE_PROXY_PATHS = {
|
export const OBJECT_STORAGE_PROXY_PATHS = {
|
||||||
// Need to keep this legacy path for previously generated torrents
|
// Need to keep this legacy path for previously generated torrents
|
||||||
LEGACY_PRIVATE_WEB_VIDEOS: '/object-storage-proxy/webseed/private/',
|
LEGACY_PRIVATE_WEB_VIDEOS: '/object-storage-proxy/webseed/private/',
|
||||||
PRIVATE_WEB_VIDEOS: '/object-storage-proxy/web-videos/private/',
|
PRIVATE_WEB_VIDEOS: '/object-storage-proxy/web-videos/private/',
|
||||||
|
@ -902,24 +891,24 @@ const OBJECT_STORAGE_PROXY_PATHS = {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache control
|
// Cache control
|
||||||
const STATIC_MAX_AGE = {
|
export const STATIC_MAX_AGE = {
|
||||||
SERVER: '2h',
|
SERVER: '2h',
|
||||||
LAZY_SERVER: '2d',
|
LAZY_SERVER: '2d',
|
||||||
CLIENT: '30d'
|
CLIENT: '30d'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Videos thumbnail size
|
// Videos thumbnail size
|
||||||
const THUMBNAILS_SIZE = {
|
export const THUMBNAILS_SIZE = {
|
||||||
width: minBy(CONFIG.THUMBNAILS.SIZES, 'width').width,
|
width: minBy(CONFIG.THUMBNAILS.SIZES, 'width').width,
|
||||||
height: minBy(CONFIG.THUMBNAILS.SIZES, 'width').height,
|
height: minBy(CONFIG.THUMBNAILS.SIZES, 'width').height,
|
||||||
minRemoteWidth: 150
|
minRemoteWidth: 150
|
||||||
}
|
}
|
||||||
const PREVIEWS_SIZE = {
|
export const PREVIEWS_SIZE = {
|
||||||
width: maxBy(CONFIG.THUMBNAILS.SIZES, 'width').width,
|
width: maxBy(CONFIG.THUMBNAILS.SIZES, 'width').width,
|
||||||
height: maxBy(CONFIG.THUMBNAILS.SIZES, 'width').height,
|
height: maxBy(CONFIG.THUMBNAILS.SIZES, 'width').height,
|
||||||
minRemoteWidth: 400
|
minRemoteWidth: 400
|
||||||
}
|
}
|
||||||
const ACTOR_IMAGES_SIZE: { [key in ActorImageType_Type]: { width: number, height: number }[] } = {
|
export const ACTOR_IMAGES_SIZE: { [key in ActorImageType_Type]: { width: number, height: number }[] } = {
|
||||||
[ActorImageType.AVATAR]: [ // 1/1 ratio
|
[ActorImageType.AVATAR]: [ // 1/1 ratio
|
||||||
{
|
{
|
||||||
width: 1500,
|
width: 1500,
|
||||||
|
@ -950,18 +939,18 @@ const ACTOR_IMAGES_SIZE: { [key in ActorImageType_Type]: { width: number, height
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
const STORYBOARD = {
|
export const STORYBOARD = {
|
||||||
SPRITE_MAX_SIZE: 192,
|
SPRITE_MAX_SIZE: 192,
|
||||||
SPRITES_MAX_EDGE_COUNT: 10
|
SPRITES_MAX_EDGE_COUNT: 10
|
||||||
}
|
}
|
||||||
|
|
||||||
const EMBED_SIZE = {
|
export const EMBED_SIZE = {
|
||||||
width: 560,
|
width: 560,
|
||||||
height: 315
|
height: 315
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sub folders of cache directory
|
// Sub folders of cache directory
|
||||||
const FILES_CACHE = {
|
export const FILES_CACHE = {
|
||||||
PREVIEWS: {
|
PREVIEWS: {
|
||||||
DIRECTORY: join(CONFIG.STORAGE.CACHE_DIR, 'previews'),
|
DIRECTORY: join(CONFIG.STORAGE.CACHE_DIR, 'previews'),
|
||||||
MAX_AGE: 1000 * 3600 * 3 // 3 hours
|
MAX_AGE: 1000 * 3600 * 3 // 3 hours
|
||||||
|
@ -980,7 +969,7 @@ const FILES_CACHE = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const LRU_CACHE = {
|
export const LRU_CACHE = {
|
||||||
USER_TOKENS: {
|
USER_TOKENS: {
|
||||||
MAX_SIZE: 1000
|
MAX_SIZE: 1000
|
||||||
},
|
},
|
||||||
|
@ -1004,7 +993,7 @@ const LRU_CACHE = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const DIRECTORIES = {
|
export const DIRECTORIES = {
|
||||||
RESUMABLE_UPLOAD: join(CONFIG.STORAGE.TMP_DIR, 'resumable-uploads'),
|
RESUMABLE_UPLOAD: join(CONFIG.STORAGE.TMP_DIR, 'resumable-uploads'),
|
||||||
|
|
||||||
HLS_STREAMING_PLAYLIST: {
|
HLS_STREAMING_PLAYLIST: {
|
||||||
|
@ -1024,9 +1013,9 @@ const DIRECTORIES = {
|
||||||
LOCAL_PIP_DIRECTORY: join(CONFIG.STORAGE.BIN_DIR, 'pip')
|
LOCAL_PIP_DIRECTORY: join(CONFIG.STORAGE.BIN_DIR, 'pip')
|
||||||
}
|
}
|
||||||
|
|
||||||
const RESUMABLE_UPLOAD_SESSION_LIFETIME = SCHEDULER_INTERVALS_MS.REMOVE_DANGLING_RESUMABLE_UPLOADS
|
export const RESUMABLE_UPLOAD_SESSION_LIFETIME = SCHEDULER_INTERVALS_MS.REMOVE_DANGLING_RESUMABLE_UPLOADS
|
||||||
|
|
||||||
const VIDEO_LIVE = {
|
export const VIDEO_LIVE = {
|
||||||
EXTENSION: '.ts',
|
EXTENSION: '.ts',
|
||||||
CLEANUP_DELAY: 1000 * 60 * 5, // 5 minutes
|
CLEANUP_DELAY: 1000 * 60 * 5, // 5 minutes
|
||||||
SEGMENT_TIME_SECONDS: {
|
SEGMENT_TIME_SECONDS: {
|
||||||
|
@ -1046,7 +1035,7 @@ const VIDEO_LIVE = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const MEMOIZE_TTL = {
|
export const MEMOIZE_TTL = {
|
||||||
OVERVIEWS_SAMPLE: 1000 * 3600 * 4, // 4 hours
|
OVERVIEWS_SAMPLE: 1000 * 3600 * 4, // 4 hours
|
||||||
INFO_HASH_EXISTS: 1000 * 60, // 1 minute
|
INFO_HASH_EXISTS: 1000 * 60, // 1 minute
|
||||||
VIDEO_DURATION: 1000 * 10, // 10 seconds
|
VIDEO_DURATION: 1000 * 10, // 10 seconds
|
||||||
|
@ -1056,14 +1045,14 @@ const MEMOIZE_TTL = {
|
||||||
EMBED_HTML: 1000 * 10 // 10 seconds
|
EMBED_HTML: 1000 * 10 // 10 seconds
|
||||||
}
|
}
|
||||||
|
|
||||||
const MEMOIZE_LENGTH = {
|
export const MEMOIZE_LENGTH = {
|
||||||
INFO_HASH_EXISTS: 200,
|
INFO_HASH_EXISTS: 200,
|
||||||
VIDEO_DURATION: 200
|
VIDEO_DURATION: 200
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalCPUs = Math.max(cpus().length, 1)
|
export const totalCPUs = Math.max(cpus().length, 1)
|
||||||
|
|
||||||
const WORKER_THREADS = {
|
export const WORKER_THREADS = {
|
||||||
DOWNLOAD_IMAGE: {
|
DOWNLOAD_IMAGE: {
|
||||||
CONCURRENCY: 3,
|
CONCURRENCY: 3,
|
||||||
MAX_THREADS: 1
|
MAX_THREADS: 1
|
||||||
|
@ -1086,26 +1075,26 @@ const WORKER_THREADS = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const REDUNDANCY = {
|
export const REDUNDANCY = {
|
||||||
VIDEOS: {
|
VIDEOS: {
|
||||||
RANDOMIZED_FACTOR: 5
|
RANDOMIZED_FACTOR: 5
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ACCEPT_HEADERS = [ 'html', 'application/json' ].concat(ACTIVITY_PUB.POTENTIAL_ACCEPT_HEADERS)
|
export const ACCEPT_HEADERS = [ 'html', 'application/json' ].concat(ACTIVITY_PUB.POTENTIAL_ACCEPT_HEADERS)
|
||||||
const OTP = {
|
export const OTP = {
|
||||||
HEADER_NAME: 'x-peertube-otp',
|
HEADER_NAME: 'x-peertube-otp',
|
||||||
HEADER_REQUIRED_VALUE: 'required; app'
|
HEADER_REQUIRED_VALUE: 'required; app'
|
||||||
}
|
}
|
||||||
|
|
||||||
const ASSETS_PATH = {
|
export const ASSETS_PATH = {
|
||||||
DEFAULT_AUDIO_BACKGROUND: join(root(), 'dist', 'core', 'assets', 'default-audio-background.jpg'),
|
DEFAULT_AUDIO_BACKGROUND: join(root(), 'dist', 'core', 'assets', 'default-audio-background.jpg'),
|
||||||
DEFAULT_LIVE_BACKGROUND: join(root(), 'dist', 'core', 'assets', 'default-live-background.jpg')
|
DEFAULT_LIVE_BACKGROUND: join(root(), 'dist', 'core', 'assets', 'default-live-background.jpg')
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const CUSTOM_HTML_TAG_COMMENTS = {
|
export const CUSTOM_HTML_TAG_COMMENTS = {
|
||||||
TITLE: '<!-- title tag -->',
|
TITLE: '<!-- title tag -->',
|
||||||
DESCRIPTION: '<!-- description tag -->',
|
DESCRIPTION: '<!-- description tag -->',
|
||||||
CUSTOM_CSS: '<!-- custom css tag -->',
|
CUSTOM_CSS: '<!-- custom css tag -->',
|
||||||
|
@ -1113,34 +1102,34 @@ const CUSTOM_HTML_TAG_COMMENTS = {
|
||||||
SERVER_CONFIG: '<!-- server config -->'
|
SERVER_CONFIG: '<!-- server config -->'
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_LOGS_OUTPUT_CHARACTERS = 10 * 1000 * 1000
|
export const MAX_LOGS_OUTPUT_CHARACTERS = 10 * 1000 * 1000
|
||||||
const LOG_FILENAME = 'peertube.log'
|
export const LOG_FILENAME = 'peertube.log'
|
||||||
const AUDIT_LOG_FILENAME = 'peertube-audit.log'
|
export const AUDIT_LOG_FILENAME = 'peertube-audit.log'
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const TRACKER_RATE_LIMITS = {
|
export const TRACKER_RATE_LIMITS = {
|
||||||
INTERVAL: 60000 * 5, // 5 minutes
|
INTERVAL: 60000 * 5, // 5 minutes
|
||||||
ANNOUNCES_PER_IP_PER_INFOHASH: 15, // maximum announces per torrent in the interval
|
ANNOUNCES_PER_IP_PER_INFOHASH: 15, // maximum announces per torrent in the interval
|
||||||
ANNOUNCES_PER_IP: 30, // maximum announces for all our torrents in the interval
|
ANNOUNCES_PER_IP: 30, // maximum announces for all our torrents in the interval
|
||||||
BLOCK_IP_LIFETIME: parseDurationToMs('3 minutes')
|
BLOCK_IP_LIFETIME: parseDurationToMs('3 minutes')
|
||||||
}
|
}
|
||||||
|
|
||||||
const P2P_MEDIA_LOADER_PEER_VERSION = 2
|
export const P2P_MEDIA_LOADER_PEER_VERSION = 2
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const PLUGIN_GLOBAL_CSS_FILE_NAME = 'plugins-global.css'
|
export const PLUGIN_GLOBAL_CSS_FILE_NAME = 'plugins-global.css'
|
||||||
const PLUGIN_GLOBAL_CSS_PATH = join(CONFIG.STORAGE.TMP_DIR, PLUGIN_GLOBAL_CSS_FILE_NAME)
|
export const PLUGIN_GLOBAL_CSS_PATH = join(CONFIG.STORAGE.TMP_DIR, PLUGIN_GLOBAL_CSS_FILE_NAME)
|
||||||
|
|
||||||
let PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME = 1000 * 60 * 5 // 5 minutes
|
export let PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME = 1000 * 60 * 5 // 5 minutes
|
||||||
|
|
||||||
const DEFAULT_THEME_NAME = 'default'
|
export const DEFAULT_THEME_NAME = 'default'
|
||||||
const DEFAULT_USER_THEME_NAME = 'instance-default'
|
export const DEFAULT_USER_THEME_NAME = 'instance-default'
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const SEARCH_INDEX = {
|
export const SEARCH_INDEX = {
|
||||||
ROUTES: {
|
ROUTES: {
|
||||||
VIDEOS: '/api/v1/search/videos',
|
VIDEOS: '/api/v1/search/videos',
|
||||||
VIDEO_CHANNELS: '/api/v1/search/video-channels'
|
VIDEO_CHANNELS: '/api/v1/search/video-channels'
|
||||||
|
@ -1149,7 +1138,7 @@ const SEARCH_INDEX = {
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const STATS_TIMESERIE = {
|
export const STATS_TIMESERIE = {
|
||||||
MAX_DAYS: 365 * 10 // Around 10 years
|
MAX_DAYS: 365 * 10 // Around 10 years
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1231,9 +1220,15 @@ registerConfigChangedHandler(() => {
|
||||||
updateWebserverConfig()
|
updateWebserverConfig()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export async function loadLanguages () {
|
||||||
|
if (Object.keys(VIDEO_LANGUAGES).length !== 0) return
|
||||||
|
|
||||||
|
Object.assign(VIDEO_LANGUAGES, await buildLanguages())
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const FILES_CONTENT_HASH = {
|
export const FILES_CONTENT_HASH = {
|
||||||
MANIFEST: generateContentHash(),
|
MANIFEST: generateContentHash(),
|
||||||
FAVICON: generateContentHash(),
|
FAVICON: generateContentHash(),
|
||||||
LOGO: generateContentHash()
|
LOGO: generateContentHash()
|
||||||
|
@ -1241,7 +1236,7 @@ const FILES_CONTENT_HASH = {
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const VIDEO_FILTERS = {
|
export const VIDEO_FILTERS = {
|
||||||
WATERMARK: {
|
WATERMARK: {
|
||||||
SIZE_RATIO: 1 / 10,
|
SIZE_RATIO: 1 / 10,
|
||||||
HORIZONTAL_MARGIN_RATIO: 1 / 20,
|
HORIZONTAL_MARGIN_RATIO: 1 / 20,
|
||||||
|
@ -1250,116 +1245,7 @@ const VIDEO_FILTERS = {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
// Private
|
||||||
export {
|
|
||||||
WEBSERVER,
|
|
||||||
API_VERSION,
|
|
||||||
ENCRYPTION,
|
|
||||||
VIDEO_LIVE,
|
|
||||||
PEERTUBE_VERSION,
|
|
||||||
LAZY_STATIC_PATHS,
|
|
||||||
OBJECT_STORAGE_PROXY_PATHS,
|
|
||||||
SEARCH_INDEX,
|
|
||||||
DIRECTORIES,
|
|
||||||
RESUMABLE_UPLOAD_SESSION_LIFETIME,
|
|
||||||
RUNNER_JOB_STATES,
|
|
||||||
USER_EXPORT_STATES,
|
|
||||||
USER_IMPORT_STATES,
|
|
||||||
P2P_MEDIA_LOADER_PEER_VERSION,
|
|
||||||
STORYBOARD,
|
|
||||||
ACTOR_IMAGES_SIZE,
|
|
||||||
ACCEPT_HEADERS,
|
|
||||||
BCRYPT_SALT_SIZE,
|
|
||||||
TRACKER_RATE_LIMITS,
|
|
||||||
VIDEO_COMMENTS_POLICY,
|
|
||||||
FILES_CACHE,
|
|
||||||
LOG_FILENAME,
|
|
||||||
CONSTRAINTS_FIELDS,
|
|
||||||
EMBED_SIZE,
|
|
||||||
REDUNDANCY,
|
|
||||||
USER_EXPORT_FILE_PREFIX,
|
|
||||||
JOB_CONCURRENCY,
|
|
||||||
JOB_ATTEMPTS,
|
|
||||||
AP_CLEANER,
|
|
||||||
LAST_MIGRATION_VERSION,
|
|
||||||
CUSTOM_HTML_TAG_COMMENTS,
|
|
||||||
STATS_TIMESERIE,
|
|
||||||
BROADCAST_CONCURRENCY,
|
|
||||||
AUDIT_LOG_FILENAME,
|
|
||||||
USER_IMPORT,
|
|
||||||
PAGINATION,
|
|
||||||
ACTOR_FOLLOW_SCORE,
|
|
||||||
PREVIEWS_SIZE,
|
|
||||||
REMOTE_SCHEME,
|
|
||||||
FOLLOW_STATES,
|
|
||||||
DEFAULT_USER_THEME_NAME,
|
|
||||||
SERVER_ACTOR_NAME,
|
|
||||||
TWO_FACTOR_AUTH_REQUEST_TOKEN_LIFETIME,
|
|
||||||
JWT_TOKEN_USER_EXPORT_FILE_LIFETIME,
|
|
||||||
PLUGIN_GLOBAL_CSS_FILE_NAME,
|
|
||||||
PLUGIN_GLOBAL_CSS_PATH,
|
|
||||||
PRIVATE_RSA_KEY_SIZE,
|
|
||||||
VIDEO_FILTERS,
|
|
||||||
ROUTE_CACHE_LIFETIME,
|
|
||||||
SORTABLE_COLUMNS,
|
|
||||||
JOB_TTL,
|
|
||||||
DEFAULT_THEME_NAME,
|
|
||||||
NSFW_POLICY_TYPES,
|
|
||||||
STATIC_MAX_AGE,
|
|
||||||
VIEWER_SYNC_REDIS,
|
|
||||||
STATIC_PATHS,
|
|
||||||
USER_EXPORT_MAX_ITEMS,
|
|
||||||
VIDEO_IMPORT_TIMEOUT,
|
|
||||||
VIDEO_PLAYLIST_TYPES,
|
|
||||||
MAX_LOGS_OUTPUT_CHARACTERS,
|
|
||||||
ACTIVITY_PUB,
|
|
||||||
ACTIVITY_PUB_ACTOR_TYPES,
|
|
||||||
THUMBNAILS_SIZE,
|
|
||||||
VIDEO_CATEGORIES,
|
|
||||||
MEMOIZE_LENGTH,
|
|
||||||
VIDEO_LANGUAGES,
|
|
||||||
VIDEO_PRIVACIES,
|
|
||||||
VIDEO_LICENCES,
|
|
||||||
VIDEO_STATES,
|
|
||||||
WORKER_THREADS,
|
|
||||||
VIDEO_RATE_TYPES,
|
|
||||||
JOB_PRIORITY,
|
|
||||||
VIDEO_TRANSCODING_FPS,
|
|
||||||
FFMPEG_NICE,
|
|
||||||
ABUSE_STATES,
|
|
||||||
USER_REGISTRATION_STATES,
|
|
||||||
LRU_CACHE,
|
|
||||||
REQUEST_TIMEOUTS,
|
|
||||||
RUNNER_JOBS,
|
|
||||||
MAX_LOCAL_VIEWER_WATCH_SECTIONS,
|
|
||||||
USER_PASSWORD_RESET_LIFETIME,
|
|
||||||
USER_PASSWORD_CREATE_LIFETIME,
|
|
||||||
MEMOIZE_TTL,
|
|
||||||
EMAIL_VERIFY_LIFETIME,
|
|
||||||
OVERVIEWS,
|
|
||||||
SCHEDULER_INTERVALS_MS,
|
|
||||||
REPEAT_JOBS,
|
|
||||||
DOWNLOAD_PATHS,
|
|
||||||
MIMETYPES,
|
|
||||||
CRAWL_REQUEST_CONCURRENCY,
|
|
||||||
DEFAULT_AUDIO_RESOLUTION,
|
|
||||||
BINARY_CONTENT_TYPES,
|
|
||||||
JOB_REMOVAL_OPTIONS,
|
|
||||||
HTTP_SIGNATURE,
|
|
||||||
VIDEO_IMPORT_STATES,
|
|
||||||
VIDEO_CHANNEL_SYNC_STATE,
|
|
||||||
VIEW_LIFETIME,
|
|
||||||
CONTACT_FORM_LIFETIME,
|
|
||||||
VIDEO_PLAYLIST_PRIVACIES,
|
|
||||||
PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME,
|
|
||||||
ASSETS_PATH,
|
|
||||||
FILES_CONTENT_HASH,
|
|
||||||
OTP,
|
|
||||||
loadLanguages,
|
|
||||||
buildLanguages,
|
|
||||||
generateContentHash
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function buildVideoMimetypeExt () {
|
function buildVideoMimetypeExt () {
|
||||||
|
@ -1480,12 +1366,6 @@ function buildMimetypesRegex (obj: { [id: string]: string | string[] }) {
|
||||||
.join('|')
|
.join('|')
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadLanguages () {
|
|
||||||
if (Object.keys(VIDEO_LANGUAGES).length !== 0) return
|
|
||||||
|
|
||||||
Object.assign(VIDEO_LANGUAGES, await buildLanguages())
|
|
||||||
}
|
|
||||||
|
|
||||||
async function buildLanguages () {
|
async function buildLanguages () {
|
||||||
const { iso6393 } = await import('iso-639-3')
|
const { iso6393 } = await import('iso-639-3')
|
||||||
|
|
||||||
|
|
|
@ -341,10 +341,14 @@ class LiveManager {
|
||||||
|
|
||||||
inputLocalUrl,
|
inputLocalUrl,
|
||||||
inputPublicUrl,
|
inputPublicUrl,
|
||||||
|
|
||||||
fps,
|
fps,
|
||||||
bitrate,
|
bitrate,
|
||||||
ratio,
|
ratio,
|
||||||
|
|
||||||
|
inputResolution: resolution,
|
||||||
allResolutions,
|
allResolutions,
|
||||||
|
|
||||||
hasAudio,
|
hasAudio,
|
||||||
hasVideo,
|
hasVideo,
|
||||||
probe
|
probe
|
||||||
|
@ -363,7 +367,10 @@ class LiveManager {
|
||||||
fps: number
|
fps: number
|
||||||
bitrate: number
|
bitrate: number
|
||||||
ratio: number
|
ratio: number
|
||||||
|
|
||||||
|
inputResolution: number
|
||||||
allResolutions: number[]
|
allResolutions: number[]
|
||||||
|
|
||||||
hasAudio: boolean
|
hasAudio: boolean
|
||||||
hasVideo: boolean
|
hasVideo: boolean
|
||||||
probe: FfprobeData
|
probe: FfprobeData
|
||||||
|
@ -384,7 +391,18 @@ class LiveManager {
|
||||||
videoLive,
|
videoLive,
|
||||||
user,
|
user,
|
||||||
|
|
||||||
...pick(options, [ 'inputLocalUrl', 'inputPublicUrl', 'bitrate', 'ratio', 'fps', 'allResolutions', 'hasAudio', 'hasVideo', 'probe' ])
|
...pick(options, [
|
||||||
|
'inputLocalUrl',
|
||||||
|
'inputPublicUrl',
|
||||||
|
'inputResolution',
|
||||||
|
'bitrate',
|
||||||
|
'ratio',
|
||||||
|
'fps',
|
||||||
|
'allResolutions',
|
||||||
|
'hasAudio',
|
||||||
|
'hasVideo',
|
||||||
|
'probe'
|
||||||
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
muxingSession.on('live-ready', () => this.publishAndFederateLive({ live: videoLive, ratio, audioOnlyOutput, localLTags }))
|
muxingSession.on('live-ready', () => this.publishAndFederateLive({ live: videoLive, ratio, audioOnlyOutput, localLTags }))
|
||||||
|
|
|
@ -72,6 +72,8 @@ class MuxingSession extends EventEmitter {
|
||||||
private readonly inputPublicUrl: string
|
private readonly inputPublicUrl: string
|
||||||
|
|
||||||
private readonly fps: number
|
private readonly fps: number
|
||||||
|
|
||||||
|
private readonly inputResolution: number
|
||||||
private readonly allResolutions: number[]
|
private readonly allResolutions: number[]
|
||||||
|
|
||||||
private readonly bitrate: number
|
private readonly bitrate: number
|
||||||
|
@ -125,7 +127,10 @@ class MuxingSession extends EventEmitter {
|
||||||
fps: number
|
fps: number
|
||||||
bitrate: number
|
bitrate: number
|
||||||
ratio: number
|
ratio: number
|
||||||
|
|
||||||
|
inputResolution: number
|
||||||
allResolutions: number[]
|
allResolutions: number[]
|
||||||
|
|
||||||
hasAudio: boolean
|
hasAudio: boolean
|
||||||
hasVideo: boolean
|
hasVideo: boolean
|
||||||
probe: FfprobeData
|
probe: FfprobeData
|
||||||
|
@ -149,6 +154,7 @@ class MuxingSession extends EventEmitter {
|
||||||
this.hasVideo = options.hasVideo
|
this.hasVideo = options.hasVideo
|
||||||
this.hasAudio = options.hasAudio
|
this.hasAudio = options.hasAudio
|
||||||
|
|
||||||
|
this.inputResolution = options.inputResolution
|
||||||
this.allResolutions = options.allResolutions
|
this.allResolutions = options.allResolutions
|
||||||
|
|
||||||
this.videoUUID = this.videoLive.Video.uuid
|
this.videoUUID = this.videoLive.Video.uuid
|
||||||
|
@ -547,7 +553,12 @@ class MuxingSession extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
toTranscodeFPS = computeOutputFPS({ inputFPS: this.fps, resolution })
|
toTranscodeFPS = computeOutputFPS({
|
||||||
|
inputFPS: this.fps,
|
||||||
|
resolution,
|
||||||
|
isOriginResolution: resolution === this.inputResolution,
|
||||||
|
type: 'live'
|
||||||
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
err.liveVideoErrorCode = LiveVideoError.INVALID_INPUT_VIDEO_STREAM
|
err.liveVideoErrorCode = LiveVideoError.INVALID_INPUT_VIDEO_STREAM
|
||||||
throw err
|
throw err
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { VideoResolution } from '@peertube/peertube-models'
|
||||||
import { computeOutputFPS } from '@server/helpers/ffmpeg/framerate.js'
|
import { computeOutputFPS } from '@server/helpers/ffmpeg/framerate.js'
|
||||||
import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
|
import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
|
||||||
import { CONFIG } from '@server/initializers/config.js'
|
import { CONFIG } from '@server/initializers/config.js'
|
||||||
import { DEFAULT_AUDIO_RESOLUTION, VIDEO_TRANSCODING_FPS } from '@server/initializers/constants.js'
|
import { DEFAULT_AUDIO_MERGE_RESOLUTION, DEFAULT_AUDIO_RESOLUTION } from '@server/initializers/constants.js'
|
||||||
import { Hooks } from '@server/lib/plugins/hooks.js'
|
import { Hooks } from '@server/lib/plugins/hooks.js'
|
||||||
import { VideoPathManager } from '@server/lib/video-path-manager.js'
|
import { VideoPathManager } from '@server/lib/video-path-manager.js'
|
||||||
import { MUserId, MVideoFile, MVideoFullLight } from '@server/types/models/index.js'
|
import { MUserId, MVideoFile, MVideoFullLight } from '@server/types/models/index.js'
|
||||||
|
@ -36,7 +36,7 @@ export abstract class AbstractJobBuilder <P> {
|
||||||
|
|
||||||
await VideoPathManager.Instance.makeAvailableVideoFile(videoFile.withVideoOrPlaylist(video), async videoFilePath => {
|
await VideoPathManager.Instance.makeAvailableVideoFile(videoFile.withVideoOrPlaylist(video), async videoFilePath => {
|
||||||
const probe = await ffprobePromise(videoFilePath)
|
const probe = await ffprobePromise(videoFilePath)
|
||||||
const quickTranscode = await canDoQuickTranscode(videoFilePath, probe)
|
const quickTranscode = await canDoQuickTranscode(videoFilePath, CONFIG.TRANSCODING.FPS.MAX, probe)
|
||||||
|
|
||||||
let inputFPS: number
|
let inputFPS: number
|
||||||
|
|
||||||
|
@ -46,7 +46,8 @@ export abstract class AbstractJobBuilder <P> {
|
||||||
let hlsAudioAlreadyGenerated = false
|
let hlsAudioAlreadyGenerated = false
|
||||||
|
|
||||||
if (videoFile.isAudio()) {
|
if (videoFile.isAudio()) {
|
||||||
inputFPS = maxFPS = VIDEO_TRANSCODING_FPS.AUDIO_MERGE // The first transcoding job will transcode to this FPS value
|
// The first transcoding job will transcode to this FPS value
|
||||||
|
inputFPS = maxFPS = Math.min(DEFAULT_AUDIO_MERGE_RESOLUTION, CONFIG.TRANSCODING.FPS.MAX)
|
||||||
maxResolution = DEFAULT_AUDIO_RESOLUTION
|
maxResolution = DEFAULT_AUDIO_RESOLUTION
|
||||||
|
|
||||||
mergeOrOptimizePayload = this.buildMergeAudioPayload({
|
mergeOrOptimizePayload = this.buildMergeAudioPayload({
|
||||||
|
@ -59,7 +60,7 @@ export abstract class AbstractJobBuilder <P> {
|
||||||
} else {
|
} else {
|
||||||
inputFPS = videoFile.fps
|
inputFPS = videoFile.fps
|
||||||
maxResolution = buildOriginalFileResolution(videoFile.resolution)
|
maxResolution = buildOriginalFileResolution(videoFile.resolution)
|
||||||
maxFPS = computeOutputFPS({ inputFPS, resolution: maxResolution })
|
maxFPS = computeOutputFPS({ inputFPS, resolution: maxResolution, isOriginResolution: true, type: 'vod' })
|
||||||
|
|
||||||
mergeOrOptimizePayload = this.buildOptimizePayload({
|
mergeOrOptimizePayload = this.buildOptimizePayload({
|
||||||
video,
|
video,
|
||||||
|
@ -153,7 +154,7 @@ export abstract class AbstractJobBuilder <P> {
|
||||||
const inputFPS = video.getMaxFPS()
|
const inputFPS = video.getMaxFPS()
|
||||||
|
|
||||||
const children = childrenResolutions.map(resolution => {
|
const children = childrenResolutions.map(resolution => {
|
||||||
const fps = computeOutputFPS({ inputFPS, resolution })
|
const fps = computeOutputFPS({ inputFPS, resolution, isOriginResolution: maxResolution === resolution, type: 'vod' })
|
||||||
|
|
||||||
if (transcodingType === 'hls') {
|
if (transcodingType === 'hls') {
|
||||||
return this.buildHLSJobPayload({ video, resolution, fps, isNewVideo, separatedAudio })
|
return this.buildHLSJobPayload({ video, resolution, fps, isNewVideo, separatedAudio })
|
||||||
|
@ -166,7 +167,7 @@ export abstract class AbstractJobBuilder <P> {
|
||||||
throw new Error('Unknown transcoding type')
|
throw new Error('Unknown transcoding type')
|
||||||
})
|
})
|
||||||
|
|
||||||
const fps = computeOutputFPS({ inputFPS, resolution: maxResolution })
|
const fps = computeOutputFPS({ inputFPS, resolution: maxResolution, isOriginResolution: true, type: 'vod' })
|
||||||
|
|
||||||
const parent = transcodingType === 'hls'
|
const parent = transcodingType === 'hls'
|
||||||
? this.buildHLSJobPayload({ video, resolution: maxResolution, fps, isNewVideo, separatedAudio })
|
? this.buildHLSJobPayload({ video, resolution: maxResolution, fps, isNewVideo, separatedAudio })
|
||||||
|
@ -199,7 +200,12 @@ export abstract class AbstractJobBuilder <P> {
|
||||||
const sequentialPayloads: P[][] = []
|
const sequentialPayloads: P[][] = []
|
||||||
|
|
||||||
for (const resolution of resolutionsEnabled) {
|
for (const resolution of resolutionsEnabled) {
|
||||||
const fps = computeOutputFPS({ inputFPS: inputVideoFPS, resolution })
|
const fps = computeOutputFPS({
|
||||||
|
inputFPS: inputVideoFPS,
|
||||||
|
resolution,
|
||||||
|
isOriginResolution: resolution === inputVideoResolution,
|
||||||
|
type: 'vod'
|
||||||
|
})
|
||||||
|
|
||||||
let generateHLS = CONFIG.TRANSCODING.HLS.ENABLED
|
let generateHLS = CONFIG.TRANSCODING.HLS.ENABLED
|
||||||
if (resolution === VideoResolution.H_NOVIDEO && hlsAudioAlreadyGenerated) generateHLS = false
|
if (resolution === VideoResolution.H_NOVIDEO && hlsAudioAlreadyGenerated) generateHLS = false
|
||||||
|
|
|
@ -2,11 +2,11 @@ import { FfprobeData } from 'fluent-ffmpeg'
|
||||||
import { CONFIG } from '@server/initializers/config.js'
|
import { CONFIG } from '@server/initializers/config.js'
|
||||||
import { canDoQuickAudioTranscode, canDoQuickVideoTranscode, ffprobePromise } from '@peertube/peertube-ffmpeg'
|
import { canDoQuickAudioTranscode, canDoQuickVideoTranscode, ffprobePromise } from '@peertube/peertube-ffmpeg'
|
||||||
|
|
||||||
export async function canDoQuickTranscode (path: string, existingProbe?: FfprobeData): Promise<boolean> {
|
export async function canDoQuickTranscode (path: string, maxFPS: number, existingProbe?: FfprobeData): Promise<boolean> {
|
||||||
if (CONFIG.TRANSCODING.PROFILE !== 'default') return false
|
if (CONFIG.TRANSCODING.PROFILE !== 'default') return false
|
||||||
|
|
||||||
const probe = existingProbe || await ffprobePromise(path)
|
const probe = existingProbe || await ffprobePromise(path)
|
||||||
|
|
||||||
return await canDoQuickVideoTranscode(path, probe) &&
|
return await canDoQuickVideoTranscode(path, maxFPS, probe) &&
|
||||||
await canDoQuickAudioTranscode(path, probe)
|
await canDoQuickAudioTranscode(path, probe)
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,7 +50,7 @@ export async function optimizeOriginalVideofile (options: {
|
||||||
: 'video'
|
: 'video'
|
||||||
|
|
||||||
const resolution = buildOriginalFileResolution(inputVideoFile.resolution)
|
const resolution = buildOriginalFileResolution(inputVideoFile.resolution)
|
||||||
const fps = computeOutputFPS({ inputFPS: inputVideoFile.fps, resolution })
|
const fps = computeOutputFPS({ inputFPS: inputVideoFile.fps, resolution, isOriginResolution: true, type: 'vod' })
|
||||||
|
|
||||||
// Could be very long!
|
// Could be very long!
|
||||||
await buildFFmpegVOD(job).transcode({
|
await buildFFmpegVOD(job).transcode({
|
||||||
|
|
|
@ -57,6 +57,7 @@ const customConfigUpdateValidator = [
|
||||||
body('transcoding.remoteRunners.enabled').isBoolean(),
|
body('transcoding.remoteRunners.enabled').isBoolean(),
|
||||||
|
|
||||||
body('transcoding.alwaysTranscodeOriginalResolution').isBoolean(),
|
body('transcoding.alwaysTranscodeOriginalResolution').isBoolean(),
|
||||||
|
body('transcoding.fps.max').custom(isIntOrNull),
|
||||||
|
|
||||||
body('transcoding.webVideos.enabled').isBoolean(),
|
body('transcoding.webVideos.enabled').isBoolean(),
|
||||||
body('transcoding.hls.enabled').isBoolean(),
|
body('transcoding.hls.enabled').isBoolean(),
|
||||||
|
@ -106,6 +107,7 @@ const customConfigUpdateValidator = [
|
||||||
body('live.transcoding.resolutions.1440p').isBoolean(),
|
body('live.transcoding.resolutions.1440p').isBoolean(),
|
||||||
body('live.transcoding.resolutions.2160p').isBoolean(),
|
body('live.transcoding.resolutions.2160p').isBoolean(),
|
||||||
body('live.transcoding.alwaysTranscodeOriginalResolution').isBoolean(),
|
body('live.transcoding.alwaysTranscodeOriginalResolution').isBoolean(),
|
||||||
|
body('live.transcoding.fps.max').custom(isIntOrNull),
|
||||||
body('live.transcoding.remoteRunners.enabled').isBoolean(),
|
body('live.transcoding.remoteRunners.enabled').isBoolean(),
|
||||||
|
|
||||||
body('search.remoteUri.users').isBoolean(),
|
body('search.remoteUri.users').isBoolean(),
|
||||||
|
|
Loading…
Reference in New Issue