mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2025-10-03 17:59: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:
parent
e77ba2dfbc
commit
816f346a60
186 changed files with 5748 additions and 2807 deletions
|
@ -1,17 +1,17 @@
|
|||
import { MutexInterface } from 'async-mutex'
|
||||
import { Job } from 'bullmq'
|
||||
import { ensureDir, move } from 'fs-extra/esm'
|
||||
import { join } from 'path'
|
||||
import { pick } from '@peertube/peertube-core-utils'
|
||||
import { getVideoStreamDuration, HLSFromTSTranscodeOptions, HLSTranscodeOptions } from '@peertube/peertube-ffmpeg'
|
||||
import { retryTransactionWrapper } from '@server/helpers/database-utils.js'
|
||||
import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent.js'
|
||||
import { sequelizeTypescript } from '@server/initializers/database.js'
|
||||
import { MVideo } from '@server/types/models/index.js'
|
||||
import { getVideoStreamDuration } from '@peertube/peertube-ffmpeg'
|
||||
import { MutexInterface } from 'async-mutex'
|
||||
import { Job } from 'bullmq'
|
||||
import { ensureDir, move } from 'fs-extra/esm'
|
||||
import { join } from 'path'
|
||||
import { CONFIG } from '../../initializers/config.js'
|
||||
import { VideoFileModel } from '../../models/video/video-file.js'
|
||||
import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist.js'
|
||||
import { renameVideoFileInPlaylist, updatePlaylistAfterFileChange } from '../hls.js'
|
||||
import { renameVideoFileInPlaylist, updateM3U8AndShaPlaylist } from '../hls.js'
|
||||
import { generateHLSVideoFilename, getHlsResolutionPlaylistFilename } from '../paths.js'
|
||||
import { buildNewFile } from '../video-file.js'
|
||||
import { VideoPathManager } from '../video-path-manager.js'
|
||||
|
@ -28,7 +28,8 @@ export async function generateHlsPlaylistResolutionFromTS (options: {
|
|||
}) {
|
||||
return generateHlsPlaylistCommon({
|
||||
type: 'hls-from-ts' as 'hls-from-ts',
|
||||
inputPath: options.concatenatedTsFilePath,
|
||||
|
||||
videoInputPath: options.concatenatedTsFilePath,
|
||||
|
||||
...pick(options, [ 'video', 'resolution', 'fps', 'inputFileMutexReleaser', 'isAAC' ])
|
||||
})
|
||||
|
@ -37,18 +38,31 @@ export async function generateHlsPlaylistResolutionFromTS (options: {
|
|||
// Generate an HLS playlist from an input file, and update the master playlist
|
||||
export function generateHlsPlaylistResolution (options: {
|
||||
video: MVideo
|
||||
|
||||
videoInputPath: string
|
||||
separatedAudioInputPath: string
|
||||
|
||||
resolution: number
|
||||
fps: number
|
||||
copyCodecs: boolean
|
||||
inputFileMutexReleaser: MutexInterface.Releaser
|
||||
separatedAudio: boolean
|
||||
job?: Job
|
||||
}) {
|
||||
return generateHlsPlaylistCommon({
|
||||
type: 'hls' as 'hls',
|
||||
inputPath: options.videoInputPath,
|
||||
|
||||
...pick(options, [ 'video', 'resolution', 'fps', 'copyCodecs', 'inputFileMutexReleaser', 'job' ])
|
||||
...pick(options, [
|
||||
'videoInputPath',
|
||||
'separatedAudioInputPath',
|
||||
'video',
|
||||
'resolution',
|
||||
'fps',
|
||||
'copyCodecs',
|
||||
'separatedAudio',
|
||||
'inputFileMutexReleaser',
|
||||
'job'
|
||||
])
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -113,7 +127,7 @@ export async function onHLSVideoFileTranscoding (options: {
|
|||
|
||||
const savedVideoFile = await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined)
|
||||
|
||||
await updatePlaylistAfterFileChange(video, playlist)
|
||||
await updateM3U8AndShaPlaylist(video, playlist)
|
||||
|
||||
return { resolutionPlaylistPath, videoFile: savedVideoFile }
|
||||
} finally {
|
||||
|
@ -121,24 +135,43 @@ export async function onHLSVideoFileTranscoding (options: {
|
|||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function generateHlsPlaylistCommon (options: {
|
||||
type: 'hls' | 'hls-from-ts'
|
||||
video: MVideo
|
||||
inputPath: string
|
||||
|
||||
videoInputPath: string
|
||||
separatedAudioInputPath?: string
|
||||
|
||||
resolution: number
|
||||
fps: number
|
||||
|
||||
inputFileMutexReleaser: MutexInterface.Releaser
|
||||
|
||||
separatedAudio?: boolean
|
||||
|
||||
copyCodecs?: boolean
|
||||
isAAC?: boolean
|
||||
|
||||
job?: Job
|
||||
}) {
|
||||
const { type, video, inputPath, resolution, fps, copyCodecs, isAAC, job, inputFileMutexReleaser } = options
|
||||
const {
|
||||
type,
|
||||
video,
|
||||
videoInputPath,
|
||||
separatedAudioInputPath,
|
||||
resolution,
|
||||
fps,
|
||||
copyCodecs,
|
||||
separatedAudio,
|
||||
isAAC,
|
||||
job,
|
||||
inputFileMutexReleaser
|
||||
} = options
|
||||
|
||||
const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
|
||||
|
||||
const videoTranscodedBasePath = join(transcodeDirectory, type)
|
||||
|
@ -150,15 +183,18 @@ async function generateHlsPlaylistCommon (options: {
|
|||
const resolutionPlaylistFilename = getHlsResolutionPlaylistFilename(videoFilename)
|
||||
const m3u8OutputPath = join(videoTranscodedBasePath, resolutionPlaylistFilename)
|
||||
|
||||
const transcodeOptions = {
|
||||
const transcodeOptions: HLSTranscodeOptions | HLSFromTSTranscodeOptions = {
|
||||
type,
|
||||
|
||||
inputPath,
|
||||
videoInputPath,
|
||||
separatedAudioInputPath,
|
||||
|
||||
outputPath: m3u8OutputPath,
|
||||
|
||||
resolution,
|
||||
fps,
|
||||
copyCodecs,
|
||||
separatedAudio,
|
||||
|
||||
isAAC,
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
import { buildAspectRatio } from '@peertube/peertube-core-utils'
|
||||
import { TranscodeVODOptionsType, getVideoStreamDuration } from '@peertube/peertube-ffmpeg'
|
||||
import {
|
||||
MergeAudioTranscodeOptions,
|
||||
TranscodeVODOptionsType,
|
||||
VideoTranscodeOptions,
|
||||
getVideoStreamDuration
|
||||
} from '@peertube/peertube-ffmpeg'
|
||||
import { VideoFileStream } from '@peertube/peertube-models'
|
||||
import { computeOutputFPS } from '@server/helpers/ffmpeg/index.js'
|
||||
import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent.js'
|
||||
import { VideoModel } from '@server/models/video/video.js'
|
||||
|
@ -21,25 +27,22 @@ import { buildOriginalFileResolution } from './transcoding-resolutions.js'
|
|||
// Optimize the original video file and replace it. The resolution is not changed.
|
||||
export async function optimizeOriginalVideofile (options: {
|
||||
video: MVideoFullLight
|
||||
inputVideoFile: MVideoFile
|
||||
quickTranscode: boolean
|
||||
job: Job
|
||||
}) {
|
||||
const { video, inputVideoFile, quickTranscode, job } = options
|
||||
const { quickTranscode, job } = options
|
||||
|
||||
const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
|
||||
const newExtname = '.mp4'
|
||||
|
||||
// Will be released by our transcodeVOD function once ffmpeg is ran
|
||||
const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid)
|
||||
const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(options.video.uuid)
|
||||
|
||||
try {
|
||||
await video.reload()
|
||||
await inputVideoFile.reload()
|
||||
const video = await VideoModel.loadFull(options.video.id)
|
||||
const inputVideoFile = video.getMaxQualityFile(VideoFileStream.VIDEO)
|
||||
|
||||
const fileWithVideoOrPlaylist = inputVideoFile.withVideoOrPlaylist(video)
|
||||
|
||||
const result = await VideoPathManager.Instance.makeAvailableVideoFile(fileWithVideoOrPlaylist, async videoInputPath => {
|
||||
const result = await VideoPathManager.Instance.makeAvailableVideoFile(inputVideoFile, async videoInputPath => {
|
||||
const videoOutputPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
|
||||
|
||||
const transcodeType: TranscodeVODOptionsType = quickTranscode
|
||||
|
@ -53,7 +56,7 @@ export async function optimizeOriginalVideofile (options: {
|
|||
await buildFFmpegVOD(job).transcode({
|
||||
type: transcodeType,
|
||||
|
||||
inputPath: videoInputPath,
|
||||
videoInputPath,
|
||||
outputPath: videoOutputPath,
|
||||
|
||||
inputFileMutexReleaser,
|
||||
|
@ -89,16 +92,17 @@ export async function transcodeNewWebVideoResolution (options: {
|
|||
|
||||
try {
|
||||
const video = await VideoModel.loadFull(videoArg.uuid)
|
||||
const file = video.getMaxQualityFile().withVideoOrPlaylist(video)
|
||||
|
||||
const result = await VideoPathManager.Instance.makeAvailableVideoFile(file, async videoInputPath => {
|
||||
const result = await VideoPathManager.Instance.makeAvailableMaxQualityFiles(video, async ({ videoPath, separatedAudioPath }) => {
|
||||
const filename = generateWebVideoFilename(resolution, newExtname)
|
||||
const videoOutputPath = join(transcodeDirectory, filename)
|
||||
|
||||
const transcodeOptions = {
|
||||
type: 'video' as 'video',
|
||||
const transcodeOptions: VideoTranscodeOptions = {
|
||||
type: 'video',
|
||||
|
||||
videoInputPath: videoPath,
|
||||
separatedAudioInputPath: separatedAudioPath,
|
||||
|
||||
inputPath: videoInputPath,
|
||||
outputPath: videoOutputPath,
|
||||
|
||||
inputFileMutexReleaser,
|
||||
|
@ -134,11 +138,9 @@ export async function mergeAudioVideofile (options: {
|
|||
|
||||
try {
|
||||
const video = await VideoModel.loadFull(videoArg.uuid)
|
||||
const inputVideoFile = video.getMinQualityFile()
|
||||
const inputVideoFile = video.getMaxQualityFile(VideoFileStream.AUDIO)
|
||||
|
||||
const fileWithVideoOrPlaylist = inputVideoFile.withVideoOrPlaylist(video)
|
||||
|
||||
const result = await VideoPathManager.Instance.makeAvailableVideoFile(fileWithVideoOrPlaylist, async audioInputPath => {
|
||||
const result = await VideoPathManager.Instance.makeAvailableVideoFile(inputVideoFile, async audioInputPath => {
|
||||
const videoOutputPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
|
||||
|
||||
// If the user updates the video preview during transcoding
|
||||
|
@ -146,15 +148,16 @@ export async function mergeAudioVideofile (options: {
|
|||
const tmpPreviewPath = join(CONFIG.STORAGE.TMP_DIR, basename(previewPath))
|
||||
await copyFile(previewPath, tmpPreviewPath)
|
||||
|
||||
const transcodeOptions = {
|
||||
type: 'merge-audio' as 'merge-audio',
|
||||
const transcodeOptions: MergeAudioTranscodeOptions = {
|
||||
type: 'merge-audio',
|
||||
|
||||
videoInputPath: tmpPreviewPath,
|
||||
audioPath: audioInputPath,
|
||||
|
||||
inputPath: tmpPreviewPath,
|
||||
outputPath: videoOutputPath,
|
||||
|
||||
inputFileMutexReleaser,
|
||||
|
||||
audioPath: audioInputPath,
|
||||
resolution,
|
||||
fps
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue