1
0
Fork 0
mirror of https://github.com/Chocobozzz/PeerTube.git synced 2025-10-04 02:09:37 +02:00

Separate HLS audio and video streams

Allows:
  * The HLS player to propose an "Audio only" resolution
  * The live to output an "Audio only" resolution
  * The live to ingest and output an "Audio only" stream

 This feature is under a config for VOD videos and is enabled by default for lives

 In the future we can imagine:
  * To propose multiple audio streams for a specific video
  * To ingest an audio only VOD and just output an audio only "video"
    (the player would play the audio file and PeerTube would not
    generate additional resolutions)

This commit introduce a new way to download videos:
 * Add "/download/videos/generate/:videoId" endpoint where PeerTube can
   mux an audio only and a video only file to a mp4 container
 * The download client modal introduces a new default panel where the
   user can choose resolutions it wants to download
This commit is contained in:
Chocobozzz 2024-07-23 16:38:51 +02:00 committed by Chocobozzz
parent e77ba2dfbc
commit 816f346a60
186 changed files with 5748 additions and 2807 deletions

View file

@ -1,20 +1,283 @@
import { ffprobePromise } from '@peertube/peertube-ffmpeg'
import { 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_RESOLUTION, VIDEO_TRANSCODING_FPS } from '@server/initializers/constants.js'
import { Hooks } from '@server/lib/plugins/hooks.js'
import { VideoPathManager } from '@server/lib/video-path-manager.js'
import { MUserId, MVideoFile, MVideoFullLight } from '@server/types/models/index.js'
import { canDoQuickTranscode } from '../../transcoding-quick-transcode.js'
import { buildOriginalFileResolution, computeResolutionsToTranscode } from '../../transcoding-resolutions.js'
export abstract class AbstractJobBuilder {
const lTags = loggerTagsFactory('transcoding')
abstract createOptimizeOrMergeAudioJobs (options: {
export abstract class AbstractJobBuilder <P> {
async createOptimizeOrMergeAudioJobs (options: {
video: MVideoFullLight
videoFile: MVideoFile
isNewVideo: boolean
user: MUserId
videoFileAlreadyLocked: boolean
}): Promise<any>
}) {
const { video, videoFile, isNewVideo, user, videoFileAlreadyLocked } = options
abstract createTranscodingJobs (options: {
let mergeOrOptimizePayload: P
let children: P[][] = []
const mutexReleaser = videoFileAlreadyLocked
? () => {}
: await VideoPathManager.Instance.lockFiles(video.uuid)
try {
await video.reload()
await videoFile.reload()
await VideoPathManager.Instance.makeAvailableVideoFile(videoFile.withVideoOrPlaylist(video), async videoFilePath => {
const probe = await ffprobePromise(videoFilePath)
const quickTranscode = await canDoQuickTranscode(videoFilePath, probe)
let inputFPS: number
let maxFPS: number
let maxResolution: number
let hlsAudioAlreadyGenerated = false
if (videoFile.isAudio()) {
inputFPS = maxFPS = VIDEO_TRANSCODING_FPS.AUDIO_MERGE // The first transcoding job will transcode to this FPS value
maxResolution = DEFAULT_AUDIO_RESOLUTION
mergeOrOptimizePayload = this.buildMergeAudioPayload({
video,
isNewVideo,
inputFile: videoFile,
resolution: maxResolution,
fps: maxFPS
})
} else {
inputFPS = videoFile.fps
maxResolution = buildOriginalFileResolution(videoFile.resolution)
maxFPS = computeOutputFPS({ inputFPS, resolution: maxResolution })
mergeOrOptimizePayload = this.buildOptimizePayload({
video,
isNewVideo,
quickTranscode,
inputFile: videoFile,
resolution: maxResolution,
fps: maxFPS
})
}
// HLS version of max resolution
if (CONFIG.TRANSCODING.HLS.ENABLED === true) {
// We had some issues with a web video quick transcoded while producing a HLS version of it
const copyCodecs = !quickTranscode
const hlsPayloads: P[] = []
hlsPayloads.push(
this.buildHLSJobPayload({
deleteWebVideoFiles: !CONFIG.TRANSCODING.HLS.SPLIT_AUDIO_AND_VIDEO && !CONFIG.TRANSCODING.WEB_VIDEOS.ENABLED,
separatedAudio: CONFIG.TRANSCODING.HLS.SPLIT_AUDIO_AND_VIDEO,
copyCodecs,
resolution: maxResolution,
fps: maxFPS,
video,
isNewVideo
})
)
if (CONFIG.TRANSCODING.HLS.SPLIT_AUDIO_AND_VIDEO && videoFile.hasAudio()) {
hlsAudioAlreadyGenerated = true
hlsPayloads.push(
this.buildHLSJobPayload({
deleteWebVideoFiles: !CONFIG.TRANSCODING.WEB_VIDEOS.ENABLED,
separatedAudio: CONFIG.TRANSCODING.HLS.SPLIT_AUDIO_AND_VIDEO,
copyCodecs,
resolution: 0,
fps: 0,
video,
isNewVideo
})
)
}
children.push(hlsPayloads)
}
const lowerResolutionJobPayloads = await this.buildLowerResolutionJobPayloads({
video,
inputVideoResolution: maxResolution,
inputVideoFPS: inputFPS,
hasAudio: videoFile.hasAudio(),
isNewVideo,
hlsAudioAlreadyGenerated
})
children = children.concat(lowerResolutionJobPayloads)
})
} finally {
mutexReleaser()
}
await this.createJobs({
parent: mergeOrOptimizePayload,
children,
user,
video
})
}
async createTranscodingJobs (options: {
transcodingType: 'hls' | 'webtorrent' | 'web-video' // TODO: remove webtorrent in v7
video: MVideoFullLight
resolutions: number[]
isNewVideo: boolean
user: MUserId | null
}): Promise<any>
}) {
const { video, transcodingType, resolutions, isNewVideo } = options
const separatedAudio = CONFIG.TRANSCODING.HLS.SPLIT_AUDIO_AND_VIDEO
const maxResolution = Math.max(...resolutions)
const childrenResolutions = resolutions.filter(r => r !== maxResolution)
logger.info('Manually creating transcoding jobs for %s.', transcodingType, { childrenResolutions, maxResolution, ...lTags(video.uuid) })
const inputFPS = video.getMaxFPS()
const children = childrenResolutions.map(resolution => {
const fps = computeOutputFPS({ inputFPS, resolution })
if (transcodingType === 'hls') {
return this.buildHLSJobPayload({ video, resolution, fps, isNewVideo, separatedAudio })
}
if (transcodingType === 'webtorrent' || transcodingType === 'web-video') {
return this.buildWebVideoJobPayload({ video, resolution, fps, isNewVideo })
}
throw new Error('Unknown transcoding type')
})
const fps = computeOutputFPS({ inputFPS, resolution: maxResolution })
const parent = transcodingType === 'hls'
? this.buildHLSJobPayload({ video, resolution: maxResolution, fps, isNewVideo, separatedAudio })
: this.buildWebVideoJobPayload({ video, resolution: maxResolution, fps, isNewVideo })
// Process the last resolution after the other ones to prevent concurrency issue
// Because low resolutions use the biggest one as ffmpeg input
await this.createJobs({ video, parent, children: [ children ], user: null })
}
private async buildLowerResolutionJobPayloads (options: {
video: MVideoFullLight
inputVideoResolution: number
inputVideoFPS: number
hasAudio: boolean
isNewVideo: boolean
hlsAudioAlreadyGenerated: boolean
}) {
const { video, inputVideoResolution, inputVideoFPS, isNewVideo, hlsAudioAlreadyGenerated, hasAudio } = 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 })
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
})
)
}
// 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: CONFIG.TRANSCODING.HLS.SPLIT_AUDIO_AND_VIDEO,
copyCodecs: CONFIG.TRANSCODING.WEB_VIDEOS.ENABLED
})
)
}
sequentialPayloads.push(parallelPayloads)
}
return sequentialPayloads
}
// ---------------------------------------------------------------------------
protected abstract createJobs (options: {
video: MVideoFullLight
parent: P
children: P[][]
user: MUserId | null
}): Promise<void>
protected abstract buildMergeAudioPayload (options: {
video: MVideoFullLight
inputFile: MVideoFile
isNewVideo: boolean
resolution: number
fps: number
}): P
protected abstract buildOptimizePayload (options: {
video: MVideoFullLight
isNewVideo: boolean
quickTranscode: boolean
inputFile: MVideoFile
resolution: number
fps: number
}): P
protected abstract buildHLSJobPayload (options: {
video: MVideoFullLight
resolution: number
fps: number
isNewVideo: boolean
separatedAudio: boolean
deleteWebVideoFiles?: boolean // default false
copyCodecs?: boolean // default false
}): P
protected abstract buildWebVideoJobPayload (options: {
video: MVideoFullLight
resolution: number
fps: number
isNewVideo: boolean
}): P
}

View file

@ -1,14 +1,3 @@
import Bluebird from 'bluebird'
import { computeOutputFPS } from '@server/helpers/ffmpeg/index.js'
import { logger } from '@server/helpers/logger.js'
import { CONFIG } from '@server/initializers/config.js'
import { DEFAULT_AUDIO_RESOLUTION, VIDEO_TRANSCODING_FPS } from '@server/initializers/constants.js'
import { CreateJobArgument, JobQueue } from '@server/lib/job-queue/index.js'
import { Hooks } from '@server/lib/plugins/hooks.js'
import { VideoPathManager } from '@server/lib/video-path-manager.js'
import { VideoJobInfoModel } from '@server/models/video/video-job-info.js'
import { MUserId, MVideoFile, MVideoFullLight, MVideoWithFileThumbnail } from '@server/types/models/index.js'
import { ffprobePromise, getVideoStreamDimensionsInfo, getVideoStreamFPS, hasAudioStream, isAudioFile } from '@peertube/peertube-ffmpeg'
import {
HLSTranscodingPayload,
MergeAudioTranscodingPayload,
@ -16,83 +5,30 @@ import {
OptimizeTranscodingPayload,
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 Bluebird from 'bluebird'
import { getTranscodingJobPriority } from '../../transcoding-priority.js'
import { canDoQuickTranscode } from '../../transcoding-quick-transcode.js'
import { buildOriginalFileResolution, computeResolutionsToTranscode } from '../../transcoding-resolutions.js'
import { AbstractJobBuilder } from './abstract-job-builder.js'
export class TranscodingJobQueueBuilder extends AbstractJobBuilder {
type Payload =
MergeAudioTranscodingPayload |
OptimizeTranscodingPayload |
NewWebVideoResolutionTranscodingPayload |
HLSTranscodingPayload
async createOptimizeOrMergeAudioJobs (options: {
video: MVideoFullLight
videoFile: MVideoFile
isNewVideo: boolean
user: MUserId
videoFileAlreadyLocked: boolean
}) {
const { video, videoFile, isNewVideo, user, videoFileAlreadyLocked } = options
export class TranscodingJobQueueBuilder extends AbstractJobBuilder <Payload> {
let mergeOrOptimizePayload: MergeAudioTranscodingPayload | OptimizeTranscodingPayload
let nextTranscodingSequentialJobPayloads: (NewWebVideoResolutionTranscodingPayload | HLSTranscodingPayload)[][] = []
protected async createJobs (options: {
video: MVideo
parent: Payload
children: Payload[][]
user: MUserId | null
}): Promise<void> {
const { video, parent, children, user } = options
const mutexReleaser = videoFileAlreadyLocked
? () => {}
: await VideoPathManager.Instance.lockFiles(video.uuid)
try {
await video.reload()
await videoFile.reload()
await VideoPathManager.Instance.makeAvailableVideoFile(videoFile.withVideoOrPlaylist(video), async videoFilePath => {
const probe = await ffprobePromise(videoFilePath)
const { resolution } = await getVideoStreamDimensionsInfo(videoFilePath, probe)
const hasAudio = await hasAudioStream(videoFilePath, probe)
const quickTranscode = await canDoQuickTranscode(videoFilePath, probe)
const inputFPS = videoFile.isAudio()
? VIDEO_TRANSCODING_FPS.AUDIO_MERGE // The first transcoding job will transcode to this FPS value
: await getVideoStreamFPS(videoFilePath, probe)
const maxResolution = await isAudioFile(videoFilePath, probe)
? DEFAULT_AUDIO_RESOLUTION
: buildOriginalFileResolution(resolution)
if (CONFIG.TRANSCODING.HLS.ENABLED === true) {
nextTranscodingSequentialJobPayloads.push([
this.buildHLSJobPayload({
deleteWebVideoFiles: CONFIG.TRANSCODING.WEB_VIDEOS.ENABLED === false,
// We had some issues with a web video quick transcoded while producing a HLS version of it
copyCodecs: !quickTranscode,
resolution: maxResolution,
fps: computeOutputFPS({ inputFPS, resolution: maxResolution }),
videoUUID: video.uuid,
isNewVideo
})
])
}
const lowerResolutionJobPayloads = await this.buildLowerResolutionJobPayloads({
video,
inputVideoResolution: maxResolution,
inputVideoFPS: inputFPS,
hasAudio,
isNewVideo
})
nextTranscodingSequentialJobPayloads = [ ...nextTranscodingSequentialJobPayloads, ...lowerResolutionJobPayloads ]
const hasChildren = nextTranscodingSequentialJobPayloads.length !== 0
mergeOrOptimizePayload = videoFile.isAudio()
? this.buildMergeAudioPayload({ videoUUID: video.uuid, isNewVideo, hasChildren })
: this.buildOptimizePayload({ videoUUID: video.uuid, isNewVideo, quickTranscode, hasChildren })
})
} finally {
mutexReleaser()
}
const nextTranscodingSequentialJobs = await Bluebird.mapSeries(nextTranscodingSequentialJobPayloads, payloads => {
const nextTranscodingSequentialJobs = await Bluebird.mapSeries(children, payloads => {
return Bluebird.mapSeries(payloads, payload => {
return this.buildTranscodingJob({ payload, user })
})
@ -106,217 +42,109 @@ export class TranscodingJobQueueBuilder extends AbstractJobBuilder {
}
}
const mergeOrOptimizeJob = await this.buildTranscodingJob({ payload: mergeOrOptimizePayload, user })
const mergeOrOptimizeJob = await this.buildTranscodingJob({ payload: parent, user, hasChildren: !!children.length })
await JobQueue.Instance.createSequentialJobFlow(...[ mergeOrOptimizeJob, transcodingJobBuilderJob ])
await JobQueue.Instance.createSequentialJobFlow(mergeOrOptimizeJob, transcodingJobBuilderJob)
// transcoding-job-builder job will increase pendingTranscode
await VideoJobInfoModel.increaseOrCreate(video.uuid, 'pendingTranscode')
}
// ---------------------------------------------------------------------------
async createTranscodingJobs (options: {
transcodingType: 'hls' | 'webtorrent' | 'web-video' // TODO: remove webtorrent in v7
video: MVideoFullLight
resolutions: number[]
isNewVideo: boolean
user: MUserId | null
}) {
const { video, transcodingType, resolutions, isNewVideo } = options
const maxResolution = Math.max(...resolutions)
const childrenResolutions = resolutions.filter(r => r !== maxResolution)
logger.info('Manually creating transcoding jobs for %s.', transcodingType, { childrenResolutions, maxResolution })
const { fps: inputFPS } = await video.probeMaxQualityFile()
const children = childrenResolutions.map(resolution => {
const fps = computeOutputFPS({ inputFPS, resolution })
if (transcodingType === 'hls') {
return this.buildHLSJobPayload({ videoUUID: video.uuid, resolution, fps, isNewVideo })
}
if (transcodingType === 'webtorrent' || transcodingType === 'web-video') {
return this.buildWebVideoJobPayload({ videoUUID: video.uuid, resolution, fps, isNewVideo })
}
throw new Error('Unknown transcoding type')
})
const fps = computeOutputFPS({ inputFPS, resolution: maxResolution })
const parent = transcodingType === 'hls'
? this.buildHLSJobPayload({ videoUUID: video.uuid, resolution: maxResolution, fps, isNewVideo })
: this.buildWebVideoJobPayload({ videoUUID: video.uuid, resolution: maxResolution, fps, isNewVideo })
// Process the last resolution after the other ones to prevent concurrency issue
// Because low resolutions use the biggest one as ffmpeg input
await this.createTranscodingJobsWithChildren({ videoUUID: video.uuid, parent, children, user: null })
}
// ---------------------------------------------------------------------------
private async createTranscodingJobsWithChildren (options: {
videoUUID: string
parent: (HLSTranscodingPayload | NewWebVideoResolutionTranscodingPayload)
children: (HLSTranscodingPayload | NewWebVideoResolutionTranscodingPayload)[]
user: MUserId | null
}) {
const { videoUUID, parent, children, user } = options
const parentJob = await this.buildTranscodingJob({ payload: parent, user })
const childrenJobs = await Bluebird.mapSeries(children, c => this.buildTranscodingJob({ payload: c, user }))
await JobQueue.Instance.createJobWithChildren(parentJob, childrenJobs)
await VideoJobInfoModel.increaseOrCreate(videoUUID, 'pendingTranscode', 1 + children.length)
}
private async buildTranscodingJob (options: {
payload: VideoTranscodingPayload
hasChildren?: boolean
user: MUserId | null // null means we don't want priority
}) {
const { user, payload } = options
const { user, payload, hasChildren = false } = options
return {
type: 'video-transcoding' as 'video-transcoding',
priority: await getTranscodingJobPriority({ user, type: 'vod', fallback: undefined }),
payload
payload: { ...payload, hasChildren }
}
}
private async buildLowerResolutionJobPayloads (options: {
video: MVideoWithFileThumbnail
inputVideoResolution: number
inputVideoFPS: number
hasAudio: boolean
isNewVideo: boolean
}) {
const { video, inputVideoResolution, inputVideoFPS, isNewVideo, hasAudio } = 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
)
const sequentialPayloads: (NewWebVideoResolutionTranscodingPayload | HLSTranscodingPayload)[][] = []
for (const resolution of resolutionsEnabled) {
const fps = computeOutputFPS({ inputFPS: inputVideoFPS, resolution })
if (CONFIG.TRANSCODING.WEB_VIDEOS.ENABLED) {
const payloads: (NewWebVideoResolutionTranscodingPayload | HLSTranscodingPayload)[] = [
this.buildWebVideoJobPayload({
videoUUID: video.uuid,
resolution,
fps,
isNewVideo
})
]
// Create a subsequent job to create HLS resolution that will just copy web video codecs
if (CONFIG.TRANSCODING.HLS.ENABLED) {
payloads.push(
this.buildHLSJobPayload({
videoUUID: video.uuid,
resolution,
fps,
isNewVideo,
copyCodecs: true
})
)
}
sequentialPayloads.push(payloads)
} else if (CONFIG.TRANSCODING.HLS.ENABLED) {
sequentialPayloads.push([
this.buildHLSJobPayload({
videoUUID: video.uuid,
resolution,
fps,
copyCodecs: false,
isNewVideo
})
])
}
}
return sequentialPayloads
}
private buildHLSJobPayload (options: {
videoUUID: string
protected buildHLSJobPayload (options: {
video: MVideo
resolution: number
fps: number
isNewVideo: boolean
separatedAudio: boolean
deleteWebVideoFiles?: boolean // default false
copyCodecs?: boolean // default false
}): HLSTranscodingPayload {
const { videoUUID, resolution, fps, isNewVideo, deleteWebVideoFiles = false, copyCodecs = false } = options
const { video, resolution, fps, isNewVideo, separatedAudio, deleteWebVideoFiles = false, copyCodecs = false } = options
return {
type: 'new-resolution-to-hls',
videoUUID,
videoUUID: video.uuid,
resolution,
fps,
copyCodecs,
isNewVideo,
separatedAudio,
deleteWebVideoFiles
}
}
private buildWebVideoJobPayload (options: {
videoUUID: string
protected buildWebVideoJobPayload (options: {
video: MVideo
resolution: number
fps: number
isNewVideo: boolean
}): NewWebVideoResolutionTranscodingPayload {
const { videoUUID, resolution, fps, isNewVideo } = options
const { video, resolution, fps, isNewVideo } = options
return {
type: 'new-resolution-to-web-video',
videoUUID,
videoUUID: video.uuid,
isNewVideo,
resolution,
fps
}
}
private buildMergeAudioPayload (options: {
videoUUID: string
protected buildMergeAudioPayload (options: {
video: MVideo
isNewVideo: boolean
hasChildren: boolean
fps: number
resolution: number
}): MergeAudioTranscodingPayload {
const { videoUUID, isNewVideo, hasChildren } = options
const { video, isNewVideo, resolution, fps } = options
return {
type: 'merge-audio-to-web-video',
resolution: DEFAULT_AUDIO_RESOLUTION,
fps: VIDEO_TRANSCODING_FPS.AUDIO_MERGE,
videoUUID,
isNewVideo,
hasChildren
resolution,
fps,
videoUUID: video.uuid,
// Will be set later
hasChildren: undefined,
isNewVideo
}
}
private buildOptimizePayload (options: {
videoUUID: string
protected buildOptimizePayload (options: {
video: MVideo
quickTranscode: boolean
isNewVideo: boolean
hasChildren: boolean
}): OptimizeTranscodingPayload {
const { videoUUID, quickTranscode, isNewVideo, hasChildren } = options
const { video, quickTranscode, isNewVideo } = options
return {
type: 'optimize-to-web-video',
videoUUID,
videoUUID: video.uuid,
isNewVideo,
hasChildren,
// Will be set later
hasChildren: undefined,
quickTranscode
}
}
}

View file

@ -1,19 +1,11 @@
import { ffprobePromise, getVideoStreamDimensionsInfo, getVideoStreamFPS, hasAudioStream, isAudioFile } from '@peertube/peertube-ffmpeg'
import { computeOutputFPS } from '@server/helpers/ffmpeg/index.js'
import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
import { CONFIG } from '@server/initializers/config.js'
import { DEFAULT_AUDIO_RESOLUTION, VIDEO_TRANSCODING_FPS } from '@server/initializers/constants.js'
import { Hooks } from '@server/lib/plugins/hooks.js'
import {
VODAudioMergeTranscodingJobHandler,
VODHLSTranscodingJobHandler,
VODWebVideoTranscodingJobHandler
} from '@server/lib/runners/index.js'
import { VideoPathManager } from '@server/lib/video-path-manager.js'
import { MUserId, MVideoFile, MVideoFullLight, MVideoWithFileThumbnail } from '@server/types/models/index.js'
import { MRunnerJob } from '@server/types/models/runners/index.js'
} 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 { buildOriginalFileResolution, computeResolutionsToTranscode } from '../../transcoding-resolutions.js'
import { AbstractJobBuilder } from './abstract-job-builder.js'
/**
@ -22,185 +14,150 @@ import { AbstractJobBuilder } from './abstract-job-builder.js'
*
*/
const lTags = loggerTagsFactory('transcoding')
type Payload = {
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'>
}
export class TranscodingRunnerJobBuilder extends AbstractJobBuilder {
// eslint-disable-next-line max-len
export class TranscodingRunnerJobBuilder extends AbstractJobBuilder <Payload> {
async createOptimizeOrMergeAudioJobs (options: {
video: MVideoFullLight
videoFile: MVideoFile
isNewVideo: boolean
user: MUserId
videoFileAlreadyLocked: boolean
}) {
const { video, videoFile, isNewVideo, user, videoFileAlreadyLocked } = options
protected async createJobs (options: {
video: MVideo
parent: Payload
children: Payload[][] // Array of sequential jobs to create that depend on parent job
user: MUserId | null
}): Promise<void> {
const { parent, children, user } = options
const mutexReleaser = videoFileAlreadyLocked
? () => {}
: await VideoPathManager.Instance.lockFiles(video.uuid)
const parentJob = await this.createJob({ payload: parent, user })
try {
await video.reload()
await videoFile.reload()
for (const parallelPayloads of children) {
let lastJob = parentJob
await VideoPathManager.Instance.makeAvailableVideoFile(videoFile.withVideoOrPlaylist(video), async videoFilePath => {
const probe = await ffprobePromise(videoFilePath)
const { resolution } = await getVideoStreamDimensionsInfo(videoFilePath, probe)
const hasAudio = await hasAudioStream(videoFilePath, probe)
const inputFPS = videoFile.isAudio()
? VIDEO_TRANSCODING_FPS.AUDIO_MERGE // The first transcoding job will transcode to this FPS value
: await getVideoStreamFPS(videoFilePath, probe)
const isAudioInput = await isAudioFile(videoFilePath, probe)
const maxResolution = isAudioInput
? DEFAULT_AUDIO_RESOLUTION
: buildOriginalFileResolution(resolution)
const fps = computeOutputFPS({ inputFPS, resolution: maxResolution })
const priority = await getTranscodingJobPriority({ user, type: 'vod', fallback: 0 })
const jobPayload = { video, resolution: maxResolution, fps, isNewVideo, priority, deleteInputFileId: videoFile.id }
const mainRunnerJob = videoFile.isAudio()
? await new VODAudioMergeTranscodingJobHandler().create(jobPayload)
: await new VODWebVideoTranscodingJobHandler().create(jobPayload)
if (CONFIG.TRANSCODING.HLS.ENABLED === true) {
await new VODHLSTranscodingJobHandler().create({
video,
deleteWebVideoFiles: CONFIG.TRANSCODING.WEB_VIDEOS.ENABLED === false,
resolution: maxResolution,
fps,
isNewVideo,
dependsOnRunnerJob: mainRunnerJob,
priority: await getTranscodingJobPriority({ user, type: 'vod', fallback: 0 })
})
}
await this.buildLowerResolutionJobPayloads({
video,
inputVideoResolution: maxResolution,
inputVideoFPS: inputFPS,
hasAudio,
isNewVideo,
mainRunnerJob,
for (const parallelPayload of parallelPayloads) {
lastJob = await this.createJob({
payload: parallelPayload,
dependsOnRunnerJob: lastJob,
user
})
})
} finally {
mutexReleaser()
}
lastJob = undefined
}
}
private async createJob (options: {
payload: Payload
user: MUserId | null
dependsOnRunnerJob?: MRunnerJob
}) {
const { dependsOnRunnerJob, payload, user } = options
const builder = new payload.Builder()
return builder.create({
...(payload.options as any), // FIXME: typings
dependsOnRunnerJob,
priority: await getTranscodingJobPriority({ user, type: 'vod', fallback: 0 })
})
}
// ---------------------------------------------------------------------------
async createTranscodingJobs (options: {
transcodingType: 'hls' | 'webtorrent' | 'web-video' // TODO: remove webtorrent in v7
protected buildHLSJobPayload (options: {
video: MVideoFullLight
resolutions: number[]
resolution: number
fps: number
isNewVideo: boolean
user: MUserId | null
}) {
const { video, transcodingType, resolutions, isNewVideo, user } = options
separatedAudio: boolean
deleteWebVideoFiles?: boolean // default false
copyCodecs?: boolean // default false
}): Payload {
const { video, resolution, fps, isNewVideo, separatedAudio, deleteWebVideoFiles = false } = options
const maxResolution = Math.max(...resolutions)
const { fps: inputFPS } = await video.probeMaxQualityFile()
const maxFPS = computeOutputFPS({ inputFPS, resolution: maxResolution })
const priority = await getTranscodingJobPriority({ user, type: 'vod', fallback: 0 })
return {
Builder: VODHLSTranscodingJobHandler,
const childrenResolutions = resolutions.filter(r => r !== maxResolution)
logger.info('Manually creating transcoding jobs for %s.', transcodingType, { childrenResolutions, maxResolution })
const jobPayload = { video, resolution: maxResolution, fps: maxFPS, isNewVideo, priority, deleteInputFileId: null }
// Process the last resolution before the other ones to prevent concurrency issue
// Because low resolutions use the biggest one as ffmpeg input
const mainJob = transcodingType === 'hls'
// eslint-disable-next-line max-len
? await new VODHLSTranscodingJobHandler().create({ ...jobPayload, deleteWebVideoFiles: false })
: await new VODWebVideoTranscodingJobHandler().create(jobPayload)
for (const resolution of childrenResolutions) {
const dependsOnRunnerJob = mainJob
const fps = computeOutputFPS({ inputFPS, resolution })
if (transcodingType === 'hls') {
await new VODHLSTranscodingJobHandler().create({
video,
resolution,
fps,
isNewVideo,
deleteWebVideoFiles: false,
dependsOnRunnerJob,
priority: await getTranscodingJobPriority({ user, type: 'vod', fallback: 0 })
})
continue
options: {
video,
resolution,
fps,
isNewVideo,
separatedAudio,
deleteWebVideoFiles
}
if (transcodingType === 'webtorrent' || transcodingType === 'web-video') {
await new VODWebVideoTranscodingJobHandler().create({
video,
resolution,
fps,
isNewVideo,
dependsOnRunnerJob,
deleteInputFileId: null,
priority: await getTranscodingJobPriority({ user, type: 'vod', fallback: 0 })
})
continue
}
throw new Error('Unknown transcoding type')
}
}
private async buildLowerResolutionJobPayloads (options: {
mainRunnerJob: MRunnerJob
video: MVideoWithFileThumbnail
inputVideoResolution: number
inputVideoFPS: number
hasAudio: boolean
protected buildWebVideoJobPayload (options: {
video: MVideoFullLight
resolution: number
fps: number
isNewVideo: boolean
user: MUserId
}) {
const { video, inputVideoResolution, inputVideoFPS, isNewVideo, hasAudio, mainRunnerJob, user } = options
}): Payload {
const { video, resolution, fps, isNewVideo } = 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
)
return {
Builder: VODWebVideoTranscodingJobHandler,
logger.debug('Lower resolutions build for %s.', video.uuid, { resolutionsEnabled, ...lTags(video.uuid) })
for (const resolution of resolutionsEnabled) {
const fps = computeOutputFPS({ inputFPS: inputVideoFPS, resolution })
if (CONFIG.TRANSCODING.WEB_VIDEOS.ENABLED) {
await new VODWebVideoTranscodingJobHandler().create({
video,
resolution,
fps,
isNewVideo,
dependsOnRunnerJob: mainRunnerJob,
deleteInputFileId: null,
priority: await getTranscodingJobPriority({ user, type: 'vod', fallback: 0 })
})
options: {
video,
resolution,
fps,
isNewVideo,
deleteInputFileId: null
}
}
}
if (CONFIG.TRANSCODING.HLS.ENABLED) {
await new VODHLSTranscodingJobHandler().create({
video,
resolution,
fps,
isNewVideo,
deleteWebVideoFiles: false,
dependsOnRunnerJob: mainRunnerJob,
priority: await getTranscodingJobPriority({ user, type: 'vod', fallback: 0 })
})
protected buildMergeAudioPayload (options: {
video: MVideoFullLight
inputFile: MVideoFile
isNewVideo: boolean
fps: number
resolution: number
}): Payload {
const { video, isNewVideo, inputFile, resolution, fps } = options
return {
Builder: VODAudioMergeTranscodingJobHandler,
options: {
video,
resolution,
fps,
isNewVideo,
deleteInputFileId: inputFile.id
}
}
}
protected buildOptimizePayload (options: {
video: MVideoFullLight
inputFile: MVideoFile
quickTranscode: boolean
isNewVideo: boolean
fps: number
resolution: number
}): Payload {
const { video, isNewVideo, inputFile, fps, resolution } = options
return {
Builder: VODWebVideoTranscodingJobHandler,
options: {
video,
resolution,
fps,
isNewVideo,
deleteInputFileId: inputFile.id
}
}
}