Init commit

This commit is contained in:
ShreejitPanchal
2026-05-19 19:56:02 +08:00
commit 6601501eff
4047 changed files with 2185372 additions and 0 deletions

View File

@@ -0,0 +1,364 @@
import { VideoFileStreamType, VideoResolution } from '@peertube/peertube-models'
import { computeOutputFPS } from '@server/helpers/ffmpeg/framerate.js'
import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
import { CONFIG } from '@server/initializers/config.js'
import { DEFAULT_AUDIO_MERGE_RESOLUTION, DEFAULT_AUDIO_RESOLUTION } from '@server/initializers/constants.js'
import { Hooks } from '@server/lib/plugins/hooks.js'
import { MUserId, MVideoFile, MVideoFullLight } from '@server/types/models/index.js'
import { buildOriginalFileResolution, computeResolutionsToTranscode } from '../../transcoding-resolutions.js'
const lTags = loggerTagsFactory('transcoding')
export type TranscodingPriorityType = 'required' | 'optional'
export abstract class AbstractJobBuilder<P extends { transcodingPriority: TranscodingPriorityType }> {
async createOptimizeOrMergeAudioJobs (options: {
video: MVideoFullLight
videoFile: MVideoFile
isNewVideo: boolean
user: MUserId
}) {
const { video, videoFile, isNewVideo, user } = options
let mergeOrOptimizePayload: P
let children: P[][] = []
const inputStreams = video.getStreamTypes()
const transcodingRequestAt = new Date().toISOString()
let maxFPS: number
let maxResolution: number
let hlsAudioAlreadyGenerated = false
if (videoFile.isAudio()) {
// The first transcoding job will transcode to this FPS value
maxFPS = Math.min(DEFAULT_AUDIO_MERGE_RESOLUTION, CONFIG.TRANSCODING.FPS.MAX)
maxResolution = DEFAULT_AUDIO_RESOLUTION
mergeOrOptimizePayload = this.buildMergeAudioPayload({
video,
isNewVideo,
inputFile: videoFile,
resolution: maxResolution,
fps: maxFPS,
transcodingPriority: 'required',
canMoveVideoState: null // Will be set below
})
} else {
const inputFPS = videoFile.fps
maxResolution = buildOriginalFileResolution(videoFile.resolution)
maxFPS = computeOutputFPS({ inputFPS, resolution: maxResolution, isOriginResolution: true, type: 'vod' })
mergeOrOptimizePayload = this.buildOptimizePayload({
video,
isNewVideo,
inputFile: videoFile,
resolution: maxResolution,
fps: maxFPS,
transcodingPriority: 'required',
canMoveVideoState: null // Will be set below
})
}
// HLS version of max resolution
if (CONFIG.TRANSCODING.HLS.ENABLED === true) {
const hasSplitAudioTranscoding = CONFIG.TRANSCODING.HLS.SPLIT_AUDIO_AND_VIDEO && videoFile.hasAudio()
children.push([
this.buildHLSJobPayload({
deleteWebVideoFiles: !CONFIG.TRANSCODING.WEB_VIDEOS.ENABLED,
separatedAudio: hasSplitAudioTranscoding,
resolution: maxResolution,
fps: maxFPS,
video,
isNewVideo,
inputStreams,
transcodingRequestAt,
transcodingPriority: 'required',
canMoveVideoState: true
})
])
if (hasSplitAudioTranscoding) {
hlsAudioAlreadyGenerated = true
children.push([
this.buildHLSJobPayload({
deleteWebVideoFiles: !CONFIG.TRANSCODING.WEB_VIDEOS.ENABLED,
separatedAudio: hasSplitAudioTranscoding,
resolution: 0,
fps: 0,
video,
isNewVideo,
inputStreams,
transcodingRequestAt,
transcodingPriority: 'required',
canMoveVideoState: true
})
])
}
}
const lowerResolutionJobPayloads = await this.buildLowerResolutionJobPayloads({
video,
inputStreams,
transcodingRequestAt,
inputVideoResolution: maxResolution,
inputVideoFPS: maxFPS,
hasAudio: videoFile.hasAudio(),
isNewVideo,
hlsAudioAlreadyGenerated
})
children = children.concat(lowerResolutionJobPayloads)
this.reassignCanMoveVideoState(mergeOrOptimizePayload, children.length === 0)
await this.createJobs({
payloads: {
parent: mergeOrOptimizePayload,
children
},
user,
video
})
}
async createTranscodingJobs (options: {
transcodingType: 'hls' | 'web-video'
video: MVideoFullLight
resolutions: number[]
isNewVideo: boolean
user: MUserId | null
}) {
const { video, transcodingType, resolutions, isNewVideo } = options
const separatedAudio = CONFIG.TRANSCODING.HLS.SPLIT_AUDIO_AND_VIDEO
const transcodingRequestAt = new Date().toISOString()
const inputStreams = video.getStreamTypes()
const maxResolution = Math.max(...resolutions)
logger.info(`Manually creating transcoding jobs for ${transcodingType}`, { resolutions, maxResolution, ...lTags(video.uuid) })
const inputFPS = video.getMaxFPS()
const children = resolutions
.map(resolution => {
const fps = computeOutputFPS({ inputFPS, resolution, isOriginResolution: maxResolution === resolution, type: 'vod' })
if (transcodingType === 'hls') {
// We'll generate audio resolution in a parent job
if (resolution === VideoResolution.H_NOVIDEO && separatedAudio) return undefined
return [
this.buildHLSJobPayload({
video,
resolution,
fps,
isNewVideo,
separatedAudio,
canMoveVideoState: true,
inputStreams,
transcodingRequestAt,
transcodingPriority: 'optional'
})
]
}
if (transcodingType === 'web-video') {
return [
this.buildWebVideoJobPayload({
video,
resolution,
fps,
isNewVideo,
canMoveVideoState: true,
transcodingPriority: 'optional'
})
]
}
throw new Error('Unknown transcoding type')
})
.filter(r => !!r)
const fps = computeOutputFPS({ inputFPS, resolution: maxResolution, isOriginResolution: true, type: 'vod' })
// Process audio first to not override the max resolution where the audio stream will be removed
const parent = transcodingType === 'hls' && separatedAudio
? this.buildHLSJobPayload({
video,
resolution: VideoResolution.H_NOVIDEO,
fps,
isNewVideo,
separatedAudio,
canMoveVideoState: true,
transcodingRequestAt,
inputStreams,
transcodingPriority: 'optional'
})
: undefined
await this.createJobs({ video, payloads: { parent, children }, user: null })
}
private async buildLowerResolutionJobPayloads (options: {
video: MVideoFullLight
inputVideoResolution: number
inputVideoFPS: number
inputStreams: VideoFileStreamType[]
hasAudio: boolean
isNewVideo: boolean
hlsAudioAlreadyGenerated: boolean
transcodingRequestAt: string
}) {
const {
video,
inputVideoResolution,
inputVideoFPS,
inputStreams,
isNewVideo,
hlsAudioAlreadyGenerated,
hasAudio,
transcodingRequestAt
} = options
// Create transcoding jobs if there are enabled resolutions
const resolutionsEnabled = await Hooks.wrapObject(
computeResolutionsToTranscode({ input: inputVideoResolution, type: 'vod', includeInput: false, strictLower: true, hasAudio }),
'filter:transcoding.auto.resolutions-to-transcode.result',
options
)
logger.debug('Lower resolutions built for %s.', video.uuid, { resolutionsEnabled, ...lTags(video.uuid) })
const sequentialPayloads: P[][] = []
for (const resolution of resolutionsEnabled) {
const fps = computeOutputFPS({
inputFPS: inputVideoFPS,
resolution,
isOriginResolution: resolution === inputVideoResolution,
type: 'vod'
})
let generateHLS = CONFIG.TRANSCODING.HLS.ENABLED
if (resolution === VideoResolution.H_NOVIDEO && hlsAudioAlreadyGenerated) generateHLS = false
const parallelPayloads: P[] = []
if (CONFIG.TRANSCODING.WEB_VIDEOS.ENABLED) {
parallelPayloads.push(
this.buildWebVideoJobPayload({
video,
resolution,
fps,
isNewVideo,
canMoveVideoState: true,
transcodingPriority: 'optional'
})
)
}
// Create a subsequent job to create HLS resolution that will just copy web video codecs
if (generateHLS) {
parallelPayloads.push(
this.buildHLSJobPayload({
video,
resolution,
fps,
isNewVideo,
separatedAudio: hasAudio && CONFIG.TRANSCODING.HLS.SPLIT_AUDIO_AND_VIDEO,
canMoveVideoState: true,
transcodingPriority: 'optional',
transcodingRequestAt,
inputStreams
})
)
}
if (parallelPayloads.length !== 0) {
sequentialPayloads.push(parallelPayloads)
}
}
return sequentialPayloads
}
// ---------------------------------------------------------------------------
protected abstract createJobs (options: {
video: MVideoFullLight
payloads: {
parent: P
// Parallel of sequential jobs to execute
// [ [ Parallel1 ], [ Parallel2 ], ... ]
// [ [ Seq1, Seq2, ... ], [ Seq1, ... ]
children: P[][]
}
user: MUserId | null
}): Promise<void>
protected abstract buildMergeAudioPayload (options: {
video: MVideoFullLight
inputFile: MVideoFile
isNewVideo: boolean
resolution: number
fps: number
transcodingPriority: TranscodingPriorityType
canMoveVideoState: boolean
}): P
protected abstract buildOptimizePayload (options: {
video: MVideoFullLight
isNewVideo: boolean
inputFile: MVideoFile
resolution: number
fps: number
transcodingPriority: TranscodingPriorityType
canMoveVideoState: boolean
}): P
protected abstract buildHLSJobPayload (options: {
video: MVideoFullLight
resolution: number
fps: number
isNewVideo: boolean
separatedAudio: boolean
deleteWebVideoFiles?: boolean // default false
inputStreams: VideoFileStreamType[]
canMoveVideoState: boolean
transcodingRequestAt: string
transcodingPriority: TranscodingPriorityType
}): P
protected abstract buildWebVideoJobPayload (options: {
video: MVideoFullLight
resolution: number
fps: number
isNewVideo: boolean
transcodingPriority: TranscodingPriorityType
canMoveVideoState: boolean
}): P
// ---------------------------------------------------------------------------
protected abstract reassignCanMoveVideoState (payload: P, canMoveVideoState: boolean): void
}

View File

@@ -0,0 +1,2 @@
export * from './transcoding-job-queue-builder.js'
export * from './transcoding-runner-job-builder.js'

View File

@@ -0,0 +1,204 @@
import { pick } from '@peertube/peertube-core-utils'
import {
HLSTranscodingPayload,
MergeAudioTranscodingPayload,
NewWebVideoResolutionTranscodingPayload,
OptimizeTranscodingPayload,
VideoFileStreamType,
VideoTranscodingPayload
} from '@peertube/peertube-models'
import { CreateJobArgument, JobQueue } from '@server/lib/job-queue/index.js'
import { VideoJobInfoModel } from '@server/models/video/video-job-info.js'
import { MUserId, MVideo } from '@server/types/models/index.js'
import { getTranscodingJobPriority } from '../../transcoding-priority.js'
import { AbstractJobBuilder, TranscodingPriorityType } from './abstract-job-builder.js'
type BasePayload =
| MergeAudioTranscodingPayload
| OptimizeTranscodingPayload
| NewWebVideoResolutionTranscodingPayload
| HLSTranscodingPayload
type FullPayload = BasePayload & { transcodingPriority: TranscodingPriorityType }
export class TranscodingJobQueueBuilder extends AbstractJobBuilder<FullPayload> {
protected async createJobs (options: {
video: MVideo
payloads: {
parent: FullPayload | null
children: FullPayload[][]
}
user: MUserId | null
}): Promise<void> {
const { video, payloads: { parent, children }, user } = options
const requiredPriority = await getTranscodingJobPriority({ user, type: 'vod-required' })
const optionalPriority = await getTranscodingJobPriority({ user, type: 'vod-optional' })
const nextTranscodingSequentialJobs = children.map(p => {
return p.map(payload => {
return this.buildTranscodingJob({
payload,
priority: payload.transcodingPriority === 'required'
? requiredPriority
: optionalPriority
})
})
})
const transcodingJobBuilderJob: CreateJobArgument = {
type: 'transcoding-job-builder',
payload: {
videoUUID: video.uuid,
sequentialJobs: nextTranscodingSequentialJobs
}
}
const parentJob = parent
? this.buildTranscodingJob({
payload: parent,
priority: parent.transcodingPriority === 'required'
? requiredPriority
: optionalPriority
})
: undefined
await JobQueue.Instance.createSequentialJobFlow(parentJob, transcodingJobBuilderJob)
// transcoding-job-builder job will increase pendingTranscode
if (parentJob) {
await VideoJobInfoModel.increaseOrCreate(video.uuid, 'pendingTranscode')
}
}
private buildTranscodingJob (options: {
payload: VideoTranscodingPayload
priority: number
}) {
const { priority, payload } = options
return {
type: 'video-transcoding' as 'video-transcoding',
priority,
payload
}
}
// ---------------------------------------------------------------------------
protected buildHLSJobPayload (options: {
video: MVideo
resolution: number
fps: number
isNewVideo: boolean
separatedAudio: boolean
transcodingPriority: TranscodingPriorityType
transcodingRequestAt: string
canMoveVideoState: boolean
inputStreams: VideoFileStreamType[]
deleteWebVideoFiles?: boolean // default false
}): HLSTranscodingPayload & { transcodingPriority: TranscodingPriorityType } {
const { video, deleteWebVideoFiles = false } = options
return {
type: 'new-resolution-to-hls',
videoUUID: video.uuid,
...pick(options, [
'resolution',
'fps',
'isNewVideo',
'separatedAudio',
'canMoveVideoState',
'transcodingPriority',
'transcodingRequestAt',
'inputStreams'
]),
deleteWebVideoFiles
}
}
protected buildWebVideoJobPayload (options: {
video: MVideo
resolution: number
fps: number
isNewVideo: boolean
transcodingPriority: TranscodingPriorityType
canMoveVideoState: boolean
}): NewWebVideoResolutionTranscodingPayload & { transcodingPriority: TranscodingPriorityType } {
const { video } = options
return {
type: 'new-resolution-to-web-video',
videoUUID: video.uuid,
...pick(options, [
'isNewVideo',
'resolution',
'fps',
'transcodingPriority',
'canMoveVideoState'
])
}
}
protected buildMergeAudioPayload (options: {
video: MVideo
isNewVideo: boolean
fps: number
resolution: number
transcodingPriority: TranscodingPriorityType
canMoveVideoState: boolean
}): MergeAudioTranscodingPayload & { transcodingPriority: TranscodingPriorityType } {
const { video } = options
return {
type: 'merge-audio-to-web-video',
videoUUID: video.uuid,
...pick(options, [
'resolution',
'fps',
'isNewVideo',
'transcodingPriority',
'canMoveVideoState'
])
}
}
protected buildOptimizePayload (options: {
video: MVideo
isNewVideo: boolean
transcodingPriority: TranscodingPriorityType
canMoveVideoState: boolean
}): OptimizeTranscodingPayload & { transcodingPriority: TranscodingPriorityType } {
const { video } = options
return {
type: 'optimize-to-web-video',
videoUUID: video.uuid,
...pick(options, [
'isNewVideo',
'transcodingPriority',
'canMoveVideoState'
])
}
}
// ---------------------------------------------------------------------------
protected reassignCanMoveVideoState (payload: FullPayload, canMoveVideoState: boolean): void {
payload.canMoveVideoState = canMoveVideoState
}
}

View File

@@ -0,0 +1,226 @@
import { pick } from '@peertube/peertube-core-utils'
import { VideoFileStreamType } from '@peertube/peertube-models'
import {
VODAudioMergeTranscodingJobHandler,
VODHLSTranscodingJobHandler,
VODWebVideoTranscodingJobHandler
} from '@server/lib/runners/job-handlers/index.js'
import { MUserId, MVideo, MVideoFile, MVideoFullLight } from '@server/types/models/index.js'
import { MRunnerJob } from '@server/types/models/runners/runner-job.js'
import { getTranscodingJobPriority } from '../../transcoding-priority.js'
import { AbstractJobBuilder, TranscodingPriorityType } from './abstract-job-builder.js'
/**
* Class to build transcoding job in the local job queue
*/
type BasePayload = {
Builder: new() => VODHLSTranscodingJobHandler
options: Omit<Parameters<VODHLSTranscodingJobHandler['create']>[0], 'priority'>
} | {
Builder: new() => VODAudioMergeTranscodingJobHandler
options: Omit<Parameters<VODAudioMergeTranscodingJobHandler['create']>[0], 'priority'>
} | {
Builder: new() => VODWebVideoTranscodingJobHandler
options: Omit<Parameters<VODWebVideoTranscodingJobHandler['create']>[0], 'priority'>
}
type FullPayload = BasePayload & { transcodingPriority: TranscodingPriorityType }
export class TranscodingRunnerJobBuilder extends AbstractJobBuilder<FullPayload> {
protected async createJobs (options: {
video: MVideo
payloads: {
parent: FullPayload | null
children: FullPayload[][]
}
user: MUserId | null
}): Promise<void> {
const { payloads: { parent, children }, user } = options
const requiredPriority = await getTranscodingJobPriority({ user, type: 'vod-required' })
const optionalPriority = await getTranscodingJobPriority({ user, type: 'vod-optional' })
const parentJob = parent
? await this.createJob({
payload: parent,
priority: parent.transcodingPriority === 'required'
? requiredPriority
: optionalPriority
})
: undefined
for (const parallelPayloads of children) {
let lastJob = parentJob
for (const sequentialPayload of parallelPayloads) {
lastJob = await this.createJob({
payload: sequentialPayload,
priority: sequentialPayload.transcodingPriority === 'required'
? requiredPriority
: optionalPriority,
dependsOnRunnerJob: lastJob
})
}
lastJob = undefined
}
}
private createJob (options: {
payload: FullPayload
priority: number
dependsOnRunnerJob?: MRunnerJob
}) {
const { dependsOnRunnerJob, payload, priority } = options
const builder = new payload.Builder()
return builder.create({
...(payload.options as any), // FIXME: typings
dependsOnRunnerJob,
priority
})
}
// ---------------------------------------------------------------------------
protected buildHLSJobPayload (options: {
video: MVideoFullLight
resolution: number
fps: number
isNewVideo: boolean
separatedAudio: boolean
transcodingPriority: TranscodingPriorityType
canMoveVideoState: boolean
inputStreams: VideoFileStreamType[]
transcodingRequestAt: string
deleteWebVideoFiles?: boolean // default false
}): FullPayload {
const { deleteWebVideoFiles = false } = options
return {
Builder: VODHLSTranscodingJobHandler,
options: {
...pick(options, [
'video',
'resolution',
'fps',
'isNewVideo',
'separatedAudio',
'canMoveVideoState',
'transcodingRequestAt',
'inputStreams'
]),
deleteWebVideoFiles
},
...pick(options, [ 'transcodingPriority' ])
}
}
protected buildWebVideoJobPayload (options: {
video: MVideoFullLight
resolution: number
fps: number
isNewVideo: boolean
transcodingPriority: TranscodingPriorityType
canMoveVideoState: boolean
}): FullPayload {
return {
Builder: VODWebVideoTranscodingJobHandler,
options: {
...pick(options, [
'video',
'resolution',
'fps',
'isNewVideo',
'canMoveVideoState',
'transcodingPriority',
'canMoveVideoState'
]),
deleteInputFileId: null
},
...pick(options, [ 'transcodingPriority' ])
}
}
protected buildMergeAudioPayload (options: {
video: MVideoFullLight
inputFile: MVideoFile
isNewVideo: boolean
fps: number
resolution: number
transcodingPriority: TranscodingPriorityType
canMoveVideoState: boolean
}): FullPayload {
const { inputFile } = options
return {
Builder: VODAudioMergeTranscodingJobHandler,
options: {
...pick(options, [
'video',
'resolution',
'fps',
'isNewVideo',
'canMoveVideoState'
]),
deleteInputFileId: inputFile.id
},
...pick(options, [ 'transcodingPriority' ])
}
}
protected buildOptimizePayload (options: {
video: MVideoFullLight
inputFile: MVideoFile
isNewVideo: boolean
fps: number
resolution: number
transcodingPriority: TranscodingPriorityType
canMoveVideoState: boolean
}): FullPayload {
const { inputFile } = options
return {
Builder: VODWebVideoTranscodingJobHandler,
options: {
...pick(options, [
'video',
'resolution',
'fps',
'isNewVideo',
'canMoveVideoState'
]),
deleteInputFileId: inputFile.id
},
...pick(options, [ 'transcodingPriority' ])
}
}
protected reassignCanMoveVideoState (payload: FullPayload, canMoveVideoState: boolean) {
payload.options.canMoveVideoState = canMoveVideoState
}
}