はじまりの大地

このコミットが含まれているのは:
2024-07-15 09:14:04 +09:00
コミット 6632905f32
3501個のファイルの変更1439465行の追加0行の削除
+280
ファイルの表示
@@ -0,0 +1,280 @@
import { pick } from '@peertube/peertube-core-utils'
import {
RunnerJobLiveRTMPHLSTranscodingPayload,
RunnerJobLiveRTMPHLSTranscodingPrivatePayload,
RunnerJobState,
RunnerJobStateType,
RunnerJobStudioTranscodingPayload,
RunnerJobSuccessPayload,
RunnerJobTranscriptionPayload,
RunnerJobTranscriptionPrivatePayload,
RunnerJobType,
RunnerJobUpdatePayload,
RunnerJobVODAudioMergeTranscodingPayload,
RunnerJobVODAudioMergeTranscodingPrivatePayload,
RunnerJobVODHLSTranscodingPayload,
RunnerJobVODHLSTranscodingPrivatePayload,
RunnerJobVODWebVideoTranscodingPayload,
RunnerJobVODWebVideoTranscodingPrivatePayload,
RunnerJobVideoStudioTranscodingPrivatePayload
} from '@peertube/peertube-models'
import { saveInTransactionWithRetries } from '@server/helpers/database-utils.js'
import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
import { RUNNER_JOBS } from '@server/initializers/constants.js'
import { sequelizeTypescript } from '@server/initializers/database.js'
import { PeerTubeSocket } from '@server/lib/peertube-socket.js'
import { RunnerJobModel } from '@server/models/runner/runner-job.js'
import { setAsUpdated } from '@server/models/shared/index.js'
import { MRunnerJob } from '@server/types/models/runners/index.js'
import throttle from 'lodash-es/throttle.js'
type CreateRunnerJobArg =
{
type: Extract<RunnerJobType, 'vod-web-video-transcoding'>
payload: RunnerJobVODWebVideoTranscodingPayload
privatePayload: RunnerJobVODWebVideoTranscodingPrivatePayload
} |
{
type: Extract<RunnerJobType, 'vod-hls-transcoding'>
payload: RunnerJobVODHLSTranscodingPayload
privatePayload: RunnerJobVODHLSTranscodingPrivatePayload
} |
{
type: Extract<RunnerJobType, 'vod-audio-merge-transcoding'>
payload: RunnerJobVODAudioMergeTranscodingPayload
privatePayload: RunnerJobVODAudioMergeTranscodingPrivatePayload
} |
{
type: Extract<RunnerJobType, 'live-rtmp-hls-transcoding'>
payload: RunnerJobLiveRTMPHLSTranscodingPayload
privatePayload: RunnerJobLiveRTMPHLSTranscodingPrivatePayload
} |
{
type: Extract<RunnerJobType, 'video-studio-transcoding'>
payload: RunnerJobStudioTranscodingPayload
privatePayload: RunnerJobVideoStudioTranscodingPrivatePayload
} |
{
type: Extract<RunnerJobType, 'video-transcription'>
payload: RunnerJobTranscriptionPayload
privatePayload: RunnerJobTranscriptionPrivatePayload
}
export abstract class AbstractJobHandler <C, U extends RunnerJobUpdatePayload, S extends RunnerJobSuccessPayload> {
protected readonly lTags = loggerTagsFactory('runner')
static setJobAsUpdatedThrottled = throttle(setAsUpdated, 2000)
// ---------------------------------------------------------------------------
abstract create (options: C): Promise<MRunnerJob>
protected async createRunnerJob (options: CreateRunnerJobArg & {
jobUUID: string
priority: number
dependsOnRunnerJob?: MRunnerJob
}): Promise<MRunnerJob> {
const { priority, dependsOnRunnerJob } = options
logger.debug('Creating runner job', { options, ...this.lTags(options.type) })
const runnerJob = new RunnerJobModel({
...pick(options, [ 'type', 'payload', 'privatePayload' ]),
uuid: options.jobUUID,
state: dependsOnRunnerJob
? RunnerJobState.WAITING_FOR_PARENT_JOB
: RunnerJobState.PENDING,
dependsOnRunnerJobId: dependsOnRunnerJob?.id,
priority
})
await saveInTransactionWithRetries(runnerJob)
if (runnerJob.state === RunnerJobState.PENDING) {
PeerTubeSocket.Instance.sendAvailableJobsPingToRunners()
}
return runnerJob
}
// ---------------------------------------------------------------------------
protected abstract specificUpdate (options: {
runnerJob: MRunnerJob
updatePayload?: U
}): Promise<void> | void
async update (options: {
runnerJob: MRunnerJob
progress?: number
updatePayload?: U
}) {
const { runnerJob, progress } = options
await this.specificUpdate(options)
if (progress) runnerJob.progress = progress
if (!runnerJob.changed()) {
try {
await AbstractJobHandler.setJobAsUpdatedThrottled({ sequelize: sequelizeTypescript, table: 'runnerJob', id: runnerJob.id })
} catch (err) {
logger.warn('Cannot set remote job as updated', { err, ...this.lTags(runnerJob.id, runnerJob.type) })
}
return
}
await saveInTransactionWithRetries(runnerJob)
}
// ---------------------------------------------------------------------------
async complete (options: {
runnerJob: MRunnerJob
resultPayload: S
}) {
const { runnerJob } = options
runnerJob.state = RunnerJobState.COMPLETING
await saveInTransactionWithRetries(runnerJob)
try {
await this.specificComplete(options)
runnerJob.state = RunnerJobState.COMPLETED
} catch (err) {
logger.error('Cannot complete runner job', { err, ...this.lTags(runnerJob.id, runnerJob.type) })
runnerJob.state = RunnerJobState.ERRORED
runnerJob.error = err.message
}
runnerJob.progress = null
runnerJob.finishedAt = new Date()
await saveInTransactionWithRetries(runnerJob)
const [ affectedCount ] = await RunnerJobModel.updateDependantJobsOf(runnerJob)
if (affectedCount !== 0) PeerTubeSocket.Instance.sendAvailableJobsPingToRunners()
}
protected abstract specificComplete (options: {
runnerJob: MRunnerJob
resultPayload: S
}): Promise<void> | void
// ---------------------------------------------------------------------------
async cancel (options: {
runnerJob: MRunnerJob
fromParent?: boolean
}) {
const { runnerJob, fromParent } = options
await this.specificCancel(options)
const cancelState = fromParent
? RunnerJobState.PARENT_CANCELLED
: RunnerJobState.CANCELLED
runnerJob.setToErrorOrCancel(cancelState)
await saveInTransactionWithRetries(runnerJob)
const children = await RunnerJobModel.listChildrenOf(runnerJob)
for (const child of children) {
logger.info(`Cancelling child job ${child.uuid} of ${runnerJob.uuid} because of parent cancel`, this.lTags(child.uuid))
await this.cancel({ runnerJob: child, fromParent: true })
}
}
protected abstract specificCancel (options: {
runnerJob: MRunnerJob
}): Promise<void> | void
// ---------------------------------------------------------------------------
protected abstract isAbortSupported (): boolean
async abort (options: {
runnerJob: MRunnerJob
abortNotSupportedErrorMessage?: string
}) {
const { runnerJob, abortNotSupportedErrorMessage = 'Job has been aborted but it is not supported by this job type' } = options
if (this.isAbortSupported() !== true) {
return this.error({ runnerJob, message: abortNotSupportedErrorMessage })
}
await this.specificAbort(options)
runnerJob.resetToPending()
await saveInTransactionWithRetries(runnerJob)
PeerTubeSocket.Instance.sendAvailableJobsPingToRunners()
}
protected setAbortState (runnerJob: MRunnerJob) {
runnerJob.resetToPending()
}
protected abstract specificAbort (options: {
runnerJob: MRunnerJob
}): Promise<void> | void
// ---------------------------------------------------------------------------
async error (options: {
runnerJob: MRunnerJob
message: string
fromParent?: boolean
}) {
const { runnerJob, message, fromParent } = options
const errorState = fromParent
? RunnerJobState.PARENT_ERRORED
: RunnerJobState.ERRORED
const nextState = errorState === RunnerJobState.ERRORED && this.isAbortSupported() && runnerJob.failures < RUNNER_JOBS.MAX_FAILURES
? RunnerJobState.PENDING
: errorState
await this.specificError({ ...options, nextState })
if (nextState === errorState) {
runnerJob.setToErrorOrCancel(nextState)
runnerJob.error = message
} else {
runnerJob.resetToPending()
}
await saveInTransactionWithRetries(runnerJob)
if (runnerJob.state === errorState) {
const children = await RunnerJobModel.listChildrenOf(runnerJob)
for (const child of children) {
logger.info(`Erroring child job ${child.uuid} of ${runnerJob.uuid} because of parent error`, this.lTags(child.uuid))
await this.error({ runnerJob: child, message: 'Parent error', fromParent: true })
}
} else {
PeerTubeSocket.Instance.sendAvailableJobsPingToRunners()
}
}
protected abstract specificError (options: {
runnerJob: MRunnerJob
message: string
nextState: RunnerJobStateType
}): Promise<void> | void
}
+71
ファイルの表示
@@ -0,0 +1,71 @@
import {
RunnerJobState,
RunnerJobStateType,
RunnerJobSuccessPayload,
RunnerJobUpdatePayload,
RunnerJobVODPrivatePayload
} from '@peertube/peertube-models'
import { retryTransactionWrapper } from '@server/helpers/database-utils.js'
import { logger } from '@server/helpers/logger.js'
import { moveToFailedTranscodingState, moveToNextState } from '@server/lib/video-state.js'
import { VideoJobInfoModel } from '@server/models/video/video-job-info.js'
import { MRunnerJob } from '@server/types/models/runners/index.js'
import { AbstractJobHandler } from './abstract-job-handler.js'
import { loadRunnerVideo } from './shared/utils.js'
// eslint-disable-next-line max-len
export abstract class AbstractVODTranscodingJobHandler <C, U extends RunnerJobUpdatePayload, S extends RunnerJobSuccessPayload> extends AbstractJobHandler<C, U, S> {
protected isAbortSupported () {
return true
}
protected specificUpdate (_options: {
runnerJob: MRunnerJob
}) {
// empty
}
protected specificAbort (_options: {
runnerJob: MRunnerJob
}) {
// empty
}
protected async specificError (options: {
runnerJob: MRunnerJob
nextState: RunnerJobStateType
}) {
if (options.nextState !== RunnerJobState.ERRORED) return
const video = await loadRunnerVideo(options.runnerJob, this.lTags)
if (!video) return
await moveToFailedTranscodingState(video)
await VideoJobInfoModel.decrease(video.uuid, 'pendingTranscode')
}
protected async specificCancel (options: {
runnerJob: MRunnerJob
}) {
const { runnerJob } = options
const video = await loadRunnerVideo(options.runnerJob, this.lTags)
if (!video) return
const pending = await VideoJobInfoModel.decrease(video.uuid, 'pendingTranscode')
logger.debug(`Pending transcode decreased to ${pending} after cancel`, this.lTags(video.uuid))
if (pending === 0) {
logger.info(
`All transcoding jobs of ${video.uuid} have been processed or canceled, moving it to its next state`,
this.lTags(video.uuid)
)
const privatePayload = runnerJob.privatePayload as RunnerJobVODPrivatePayload
await retryTransactionWrapper(moveToNextState, { video, isNewVideo: privatePayload.isNewVideo })
}
}
}
+8
ファイルの表示
@@ -0,0 +1,8 @@
export * from './abstract-job-handler.js'
export * from './live-rtmp-hls-transcoding-job-handler.js'
export * from './runner-job-handlers.js'
export * from './transcription-job-handler.js'
export * from './video-studio-transcoding-job-handler.js'
export * from './vod-audio-merge-transcoding-job-handler.js'
export * from './vod-hls-transcoding-job-handler.js'
export * from './vod-web-video-transcoding-job-handler.js'
+176
ファイルの表示
@@ -0,0 +1,176 @@
import {
LiveRTMPHLSTranscodingSuccess,
LiveRTMPHLSTranscodingUpdatePayload,
LiveVideoError,
RunnerJobLiveRTMPHLSTranscodingPayload,
RunnerJobLiveRTMPHLSTranscodingPrivatePayload,
RunnerJobStateType
} from '@peertube/peertube-models'
import { buildUUID } from '@peertube/peertube-node-utils'
import { tryAtomicMove } from '@server/helpers/fs.js'
import { logger } from '@server/helpers/logger.js'
import { JOB_PRIORITY } from '@server/initializers/constants.js'
import { LiveManager } from '@server/lib/live/index.js'
import { MStreamingPlaylist, MVideo } from '@server/types/models/index.js'
import { MRunnerJob } from '@server/types/models/runners/index.js'
import { remove } from 'fs-extra/esm'
import { join } from 'path'
import { AbstractJobHandler } from './abstract-job-handler.js'
type CreateOptions = {
video: MVideo
playlist: MStreamingPlaylist
sessionId: string
rtmpUrl: string
toTranscode: {
resolution: number
fps: number
}[]
segmentListSize: number
segmentDuration: number
outputDirectory: string
}
// eslint-disable-next-line max-len
export class LiveRTMPHLSTranscodingJobHandler extends AbstractJobHandler<CreateOptions, LiveRTMPHLSTranscodingUpdatePayload, LiveRTMPHLSTranscodingSuccess> {
async create (options: CreateOptions) {
const { video, rtmpUrl, toTranscode, playlist, segmentDuration, segmentListSize, outputDirectory, sessionId } = options
const jobUUID = buildUUID()
const payload: RunnerJobLiveRTMPHLSTranscodingPayload = {
input: {
rtmpUrl
},
output: {
toTranscode,
segmentListSize,
segmentDuration
}
}
const privatePayload: RunnerJobLiveRTMPHLSTranscodingPrivatePayload = {
videoUUID: video.uuid,
masterPlaylistName: playlist.playlistFilename,
sessionId,
outputDirectory
}
const job = await this.createRunnerJob({
type: 'live-rtmp-hls-transcoding',
jobUUID,
payload,
privatePayload,
priority: JOB_PRIORITY.TRANSCODING
})
return job
}
// ---------------------------------------------------------------------------
protected async specificUpdate (options: {
runnerJob: MRunnerJob
updatePayload: LiveRTMPHLSTranscodingUpdatePayload
}) {
const { runnerJob, updatePayload } = options
const privatePayload = runnerJob.privatePayload as RunnerJobLiveRTMPHLSTranscodingPrivatePayload
const outputDirectory = privatePayload.outputDirectory
const videoUUID = privatePayload.videoUUID
// Always process the chunk first before moving m3u8 that references this chunk
if (updatePayload.type === 'add-chunk') {
await tryAtomicMove(
updatePayload.videoChunkFile as string,
join(outputDirectory, updatePayload.videoChunkFilename)
)
} else if (updatePayload.type === 'remove-chunk') {
await remove(join(outputDirectory, updatePayload.videoChunkFilename))
}
if (updatePayload.resolutionPlaylistFile && updatePayload.resolutionPlaylistFilename) {
await tryAtomicMove(
updatePayload.resolutionPlaylistFile as string,
join(outputDirectory, updatePayload.resolutionPlaylistFilename)
)
}
if (updatePayload.masterPlaylistFile) {
await tryAtomicMove(updatePayload.masterPlaylistFile as string, join(outputDirectory, privatePayload.masterPlaylistName))
}
logger.debug(
'Runner live RTMP to HLS job %s for %s updated.',
runnerJob.uuid, videoUUID, { updatePayload, ...this.lTags(videoUUID, runnerJob.uuid) }
)
}
// ---------------------------------------------------------------------------
protected specificComplete (options: {
runnerJob: MRunnerJob
}) {
return this.stopLive({
runnerJob: options.runnerJob,
type: 'ended'
})
}
// ---------------------------------------------------------------------------
protected isAbortSupported () {
return false
}
protected specificAbort () {
throw new Error('Not implemented')
}
protected specificError (options: {
runnerJob: MRunnerJob
nextState: RunnerJobStateType
}) {
return this.stopLive({
runnerJob: options.runnerJob,
type: 'errored'
})
}
protected specificCancel (options: {
runnerJob: MRunnerJob
}) {
return this.stopLive({
runnerJob: options.runnerJob,
type: 'cancelled'
})
}
private stopLive (options: {
runnerJob: MRunnerJob
type: 'ended' | 'errored' | 'cancelled'
}) {
const { runnerJob, type } = options
const privatePayload = runnerJob.privatePayload as RunnerJobLiveRTMPHLSTranscodingPrivatePayload
const videoUUID = privatePayload.videoUUID
const errorType = {
ended: null,
errored: LiveVideoError.RUNNER_JOB_ERROR,
cancelled: LiveVideoError.RUNNER_JOB_CANCEL
}
LiveManager.Instance.stopSessionOfVideo({
videoUUID: privatePayload.videoUUID,
expectedSessionId: privatePayload.sessionId,
error: errorType[type]
})
logger.info('Runner live RTMP to HLS job %s for video %s %s.', runnerJob.uuid, videoUUID, type, this.lTags(runnerJob.uuid, videoUUID))
}
}
+22
ファイルの表示
@@ -0,0 +1,22 @@
import { RunnerJobSuccessPayload, RunnerJobType, RunnerJobUpdatePayload } from '@peertube/peertube-models'
import { MRunnerJob } from '@server/types/models/runners/index.js'
import { AbstractJobHandler } from './abstract-job-handler.js'
import { LiveRTMPHLSTranscodingJobHandler } from './live-rtmp-hls-transcoding-job-handler.js'
import { TranscriptionJobHandler } from './transcription-job-handler.js'
import { VideoStudioTranscodingJobHandler } from './video-studio-transcoding-job-handler.js'
import { VODAudioMergeTranscodingJobHandler } from './vod-audio-merge-transcoding-job-handler.js'
import { VODHLSTranscodingJobHandler } from './vod-hls-transcoding-job-handler.js'
import { VODWebVideoTranscodingJobHandler } from './vod-web-video-transcoding-job-handler.js'
const processors: Record<RunnerJobType, new() => AbstractJobHandler<unknown, RunnerJobUpdatePayload, RunnerJobSuccessPayload>> = {
'vod-web-video-transcoding': VODWebVideoTranscodingJobHandler,
'vod-hls-transcoding': VODHLSTranscodingJobHandler,
'vod-audio-merge-transcoding': VODAudioMergeTranscodingJobHandler,
'live-rtmp-hls-transcoding': LiveRTMPHLSTranscodingJobHandler,
'video-studio-transcoding': VideoStudioTranscodingJobHandler,
'video-transcription': TranscriptionJobHandler
}
export function getRunnerJobHandlerClass (job: MRunnerJob) {
return processors[job.type]
}
+36
ファイルの表示
@@ -0,0 +1,36 @@
import { logger, LoggerTagsFn } from '@server/helpers/logger.js'
import { onTranscodingEnded } from '@server/lib/transcoding/ended-transcoding.js'
import { onWebVideoFileTranscoding } from '@server/lib/transcoding/web-transcoding.js'
import { VideoModel } from '@server/models/video/video.js'
import { MVideoFullLight } from '@server/types/models/index.js'
import { MRunnerJob } from '@server/types/models/runners/index.js'
import { RunnerJobVODAudioMergeTranscodingPrivatePayload, RunnerJobVODWebVideoTranscodingPrivatePayload } from '@peertube/peertube-models'
export async function onVODWebVideoOrAudioMergeTranscodingJob (options: {
video: MVideoFullLight
videoFilePath: string
privatePayload: RunnerJobVODWebVideoTranscodingPrivatePayload | RunnerJobVODAudioMergeTranscodingPrivatePayload
wasAudioFile: boolean
}) {
const { video, videoFilePath, privatePayload, wasAudioFile } = options
const deleteWebInputVideoFile = privatePayload.deleteInputFileId
? video.VideoFiles.find(f => f.id === privatePayload.deleteInputFileId)
: undefined
await onWebVideoFileTranscoding({ video, videoOutputPath: videoFilePath, deleteWebInputVideoFile, wasAudioFile })
await onTranscodingEnded({ isNewVideo: privatePayload.isNewVideo, moveVideoToNextState: true, video })
}
export async function loadRunnerVideo (runnerJob: MRunnerJob, lTags: LoggerTagsFn) {
const videoUUID = runnerJob.privatePayload.videoUUID
const video = await VideoModel.loadFull(videoUUID)
if (!video) {
logger.info('Video %s does not exist anymore after runner job.', videoUUID, lTags(videoUUID))
return undefined
}
return video
}
+99
ファイルの表示
@@ -0,0 +1,99 @@
import {
RunnerJobState,
RunnerJobStateType,
RunnerJobTranscriptionPayload,
RunnerJobTranscriptionPrivatePayload,
RunnerJobUpdatePayload,
TranscriptionSuccess
} from '@peertube/peertube-models'
import { buildUUID } from '@peertube/peertube-node-utils'
import { JOB_PRIORITY } from '@server/initializers/constants.js'
import { onTranscriptionEnded } from '@server/lib/video-captions.js'
import { VideoJobInfoModel } from '@server/models/video/video-job-info.js'
import { MVideoUUID } from '@server/types/models/index.js'
import { MRunnerJob } from '@server/types/models/runners/index.js'
import { generateRunnerTranscodingVideoInputFileUrl } from '../runner-urls.js'
import { AbstractJobHandler } from './abstract-job-handler.js'
import { loadRunnerVideo } from './shared/utils.js'
type CreateOptions = {
video: MVideoUUID
}
export class TranscriptionJobHandler extends AbstractJobHandler<CreateOptions, RunnerJobUpdatePayload, TranscriptionSuccess> {
protected isAbortSupported () {
return true
}
protected specificUpdate (_options: {
runnerJob: MRunnerJob
}) {
// empty
}
protected specificAbort (_options: {
runnerJob: MRunnerJob
}) {
// empty
}
protected async specificError (options: {
runnerJob: MRunnerJob
nextState: RunnerJobStateType
}) {
if (options.nextState !== RunnerJobState.ERRORED) return
await VideoJobInfoModel.decrease(options.runnerJob.privatePayload.videoUUID, 'pendingTranscription')
}
protected async specificCancel (options: {
runnerJob: MRunnerJob
}) {
await VideoJobInfoModel.decrease(options.runnerJob.privatePayload.videoUUID, 'pendingTranscription')
}
async create (options: CreateOptions) {
const { video } = options
const jobUUID = buildUUID()
const payload: RunnerJobTranscriptionPayload = {
input: {
videoFileUrl: generateRunnerTranscodingVideoInputFileUrl(jobUUID, video.uuid)
}
}
const privatePayload: RunnerJobTranscriptionPrivatePayload = {
videoUUID: video.uuid
}
const job = await this.createRunnerJob({
type: 'video-transcription',
jobUUID,
payload,
privatePayload,
priority: JOB_PRIORITY.TRANSCODING
})
return job
}
// ---------------------------------------------------------------------------
protected async specificComplete (options: {
runnerJob: MRunnerJob
resultPayload: TranscriptionSuccess
}) {
const { runnerJob, resultPayload } = options
const video = await loadRunnerVideo(runnerJob, this.lTags)
if (!video) return
await onTranscriptionEnded({
video,
language: resultPayload.inputLanguage,
vttPath: resultPayload.vttFile as string,
lTags: this.lTags().tags
})
}
}
+157
ファイルの表示
@@ -0,0 +1,157 @@
import {
RunnerJobState,
RunnerJobStateType,
RunnerJobStudioTranscodingPayload,
RunnerJobUpdatePayload,
RunnerJobVideoStudioTranscodingPrivatePayload,
VideoState,
VideoStudioTaskPayload,
VideoStudioTranscodingSuccess,
isVideoStudioTaskIntro,
isVideoStudioTaskOutro,
isVideoStudioTaskWatermark
} from '@peertube/peertube-models'
import { buildUUID } from '@peertube/peertube-node-utils'
import { logger } from '@server/helpers/logger.js'
import { onVideoStudioEnded, safeCleanupStudioTMPFiles } from '@server/lib/video-studio.js'
import { MVideo } from '@server/types/models/index.js'
import { MRunnerJob } from '@server/types/models/runners/index.js'
import { basename } from 'path'
import { generateRunnerEditionTranscodingVideoInputFileUrl, generateRunnerTranscodingVideoInputFileUrl } from '../runner-urls.js'
import { AbstractJobHandler } from './abstract-job-handler.js'
import { loadRunnerVideo } from './shared/utils.js'
type CreateOptions = {
video: MVideo
tasks: VideoStudioTaskPayload[]
priority: number
}
// eslint-disable-next-line max-len
export class VideoStudioTranscodingJobHandler extends AbstractJobHandler<CreateOptions, RunnerJobUpdatePayload, VideoStudioTranscodingSuccess> {
async create (options: CreateOptions) {
const { video, priority, tasks } = options
const jobUUID = buildUUID()
const payload: RunnerJobStudioTranscodingPayload = {
input: {
videoFileUrl: generateRunnerTranscodingVideoInputFileUrl(jobUUID, video.uuid)
},
tasks: tasks.map(t => {
if (isVideoStudioTaskIntro(t) || isVideoStudioTaskOutro(t)) {
return {
...t,
options: {
...t.options,
file: generateRunnerEditionTranscodingVideoInputFileUrl(jobUUID, video.uuid, basename(t.options.file))
}
}
}
if (isVideoStudioTaskWatermark(t)) {
return {
...t,
options: {
...t.options,
file: generateRunnerEditionTranscodingVideoInputFileUrl(jobUUID, video.uuid, basename(t.options.file))
}
}
}
return t
})
}
const privatePayload: RunnerJobVideoStudioTranscodingPrivatePayload = {
videoUUID: video.uuid,
originalTasks: tasks
}
const job = await this.createRunnerJob({
type: 'video-studio-transcoding',
jobUUID,
payload,
privatePayload,
priority
})
return job
}
// ---------------------------------------------------------------------------
protected isAbortSupported () {
return true
}
protected specificUpdate (_options: {
runnerJob: MRunnerJob
}) {
// empty
}
protected specificAbort (_options: {
runnerJob: MRunnerJob
}) {
// empty
}
protected async specificComplete (options: {
runnerJob: MRunnerJob
resultPayload: VideoStudioTranscodingSuccess
}) {
const { runnerJob, resultPayload } = options
const privatePayload = runnerJob.privatePayload as RunnerJobVideoStudioTranscodingPrivatePayload
const video = await loadRunnerVideo(runnerJob, this.lTags)
if (!video) {
await safeCleanupStudioTMPFiles(privatePayload.originalTasks)
}
const videoFilePath = resultPayload.videoFile as string
await onVideoStudioEnded({ video, editionResultPath: videoFilePath, tasks: privatePayload.originalTasks })
logger.info(
'Runner video edition transcoding job %s for %s ended.',
runnerJob.uuid, video.uuid, this.lTags(video.uuid, runnerJob.uuid)
)
}
protected specificError (options: {
runnerJob: MRunnerJob
nextState: RunnerJobStateType
}) {
if (options.nextState === RunnerJobState.ERRORED) {
return this.specificErrorOrCancel(options)
}
return Promise.resolve()
}
protected specificCancel (options: {
runnerJob: MRunnerJob
}) {
return this.specificErrorOrCancel(options)
}
private async specificErrorOrCancel (options: {
runnerJob: MRunnerJob
}) {
const { runnerJob } = options
const payload = runnerJob.privatePayload as RunnerJobVideoStudioTranscodingPrivatePayload
await safeCleanupStudioTMPFiles(payload.originalTasks)
const video = await loadRunnerVideo(options.runnerJob, this.lTags)
if (!video) return
return video.setNewState(VideoState.PUBLISHED, false, undefined)
}
}
+86
ファイルの表示
@@ -0,0 +1,86 @@
import { pick } from '@peertube/peertube-core-utils'
import {
RunnerJobUpdatePayload,
RunnerJobVODAudioMergeTranscodingPayload,
RunnerJobVODWebVideoTranscodingPrivatePayload,
VODAudioMergeTranscodingSuccess
} from '@peertube/peertube-models'
import { buildUUID } from '@peertube/peertube-node-utils'
import { logger } from '@server/helpers/logger.js'
import { VideoJobInfoModel } from '@server/models/video/video-job-info.js'
import { MVideo } from '@server/types/models/index.js'
import { MRunnerJob } from '@server/types/models/runners/index.js'
import { generateRunnerTranscodingVideoInputFileUrl, generateRunnerTranscodingVideoPreviewFileUrl } from '../runner-urls.js'
import { AbstractVODTranscodingJobHandler } from './abstract-vod-transcoding-job-handler.js'
import { loadRunnerVideo, onVODWebVideoOrAudioMergeTranscodingJob } from './shared/utils.js'
type CreateOptions = {
video: MVideo
isNewVideo: boolean
resolution: number
fps: number
priority: number
deleteInputFileId: number | null
dependsOnRunnerJob?: MRunnerJob
}
// eslint-disable-next-line max-len
export class VODAudioMergeTranscodingJobHandler extends AbstractVODTranscodingJobHandler<CreateOptions, RunnerJobUpdatePayload, VODAudioMergeTranscodingSuccess> {
async create (options: CreateOptions) {
const { video, resolution, fps, priority, dependsOnRunnerJob } = options
const jobUUID = buildUUID()
const payload: RunnerJobVODAudioMergeTranscodingPayload = {
input: {
audioFileUrl: generateRunnerTranscodingVideoInputFileUrl(jobUUID, video.uuid),
previewFileUrl: generateRunnerTranscodingVideoPreviewFileUrl(jobUUID, video.uuid)
},
output: {
resolution,
fps
}
}
const privatePayload: RunnerJobVODWebVideoTranscodingPrivatePayload = {
...pick(options, [ 'isNewVideo', 'deleteInputFileId' ]),
videoUUID: video.uuid
}
const job = await this.createRunnerJob({
type: 'vod-audio-merge-transcoding',
jobUUID,
payload,
privatePayload,
priority,
dependsOnRunnerJob
})
await VideoJobInfoModel.increaseOrCreate(video.uuid, 'pendingTranscode')
return job
}
// ---------------------------------------------------------------------------
protected async specificComplete (options: {
runnerJob: MRunnerJob
resultPayload: VODAudioMergeTranscodingSuccess
}) {
const { runnerJob, resultPayload } = options
const privatePayload = runnerJob.privatePayload as RunnerJobVODWebVideoTranscodingPrivatePayload
const video = await loadRunnerVideo(runnerJob, this.lTags)
if (!video) return
const videoFilePath = resultPayload.videoFile as string
await onVODWebVideoOrAudioMergeTranscodingJob({ video, videoFilePath, privatePayload, wasAudioFile: true })
logger.info(
'Runner VOD audio merge transcoding job %s for %s ended.',
runnerJob.uuid, video.uuid, this.lTags(video.uuid, runnerJob.uuid)
)
}
}
+99
ファイルの表示
@@ -0,0 +1,99 @@
import { pick } from '@peertube/peertube-core-utils'
import {
RunnerJobUpdatePayload,
RunnerJobVODHLSTranscodingPayload,
RunnerJobVODHLSTranscodingPrivatePayload,
VODHLSTranscodingSuccess
} from '@peertube/peertube-models'
import { buildUUID } from '@peertube/peertube-node-utils'
import { logger } from '@server/helpers/logger.js'
import { onTranscodingEnded } from '@server/lib/transcoding/ended-transcoding.js'
import { onHLSVideoFileTranscoding } from '@server/lib/transcoding/hls-transcoding.js'
import { removeAllWebVideoFiles } from '@server/lib/video-file.js'
import { VideoJobInfoModel } from '@server/models/video/video-job-info.js'
import { MVideo } from '@server/types/models/index.js'
import { MRunnerJob } from '@server/types/models/runners/index.js'
import { generateRunnerTranscodingVideoInputFileUrl } from '../runner-urls.js'
import { AbstractVODTranscodingJobHandler } from './abstract-vod-transcoding-job-handler.js'
import { loadRunnerVideo } from './shared/utils.js'
type CreateOptions = {
video: MVideo
isNewVideo: boolean
deleteWebVideoFiles: boolean
resolution: number
fps: number
priority: number
dependsOnRunnerJob?: MRunnerJob
}
// eslint-disable-next-line max-len
export class VODHLSTranscodingJobHandler extends AbstractVODTranscodingJobHandler<CreateOptions, RunnerJobUpdatePayload, VODHLSTranscodingSuccess> {
async create (options: CreateOptions) {
const { video, resolution, fps, dependsOnRunnerJob, priority } = options
const jobUUID = buildUUID()
const payload: RunnerJobVODHLSTranscodingPayload = {
input: {
videoFileUrl: generateRunnerTranscodingVideoInputFileUrl(jobUUID, video.uuid)
},
output: {
resolution,
fps
}
}
const privatePayload: RunnerJobVODHLSTranscodingPrivatePayload = {
...pick(options, [ 'isNewVideo', 'deleteWebVideoFiles' ]),
videoUUID: video.uuid
}
const job = await this.createRunnerJob({
type: 'vod-hls-transcoding',
jobUUID,
payload,
privatePayload,
priority,
dependsOnRunnerJob
})
await VideoJobInfoModel.increaseOrCreate(video.uuid, 'pendingTranscode')
return job
}
// ---------------------------------------------------------------------------
protected async specificComplete (options: {
runnerJob: MRunnerJob
resultPayload: VODHLSTranscodingSuccess
}) {
const { runnerJob, resultPayload } = options
const privatePayload = runnerJob.privatePayload as RunnerJobVODHLSTranscodingPrivatePayload
const video = await loadRunnerVideo(runnerJob, this.lTags)
if (!video) return
const videoFilePath = resultPayload.videoFile as string
const resolutionPlaylistFilePath = resultPayload.resolutionPlaylistFile as string
await onHLSVideoFileTranscoding({
video,
m3u8OutputPath: resolutionPlaylistFilePath,
videoOutputPath: videoFilePath
})
await onTranscodingEnded({ isNewVideo: privatePayload.isNewVideo, moveVideoToNextState: true, video })
if (privatePayload.deleteWebVideoFiles === true) {
logger.info('Removing web video files of %s now we have a HLS version of it.', video.uuid, this.lTags(video.uuid))
await removeAllWebVideoFiles(video)
}
logger.info('Runner VOD HLS job %s for %s ended.', runnerJob.uuid, video.uuid, this.lTags(runnerJob.uuid, video.uuid))
}
}
+85
ファイルの表示
@@ -0,0 +1,85 @@
import { pick } from '@peertube/peertube-core-utils'
import {
RunnerJobUpdatePayload,
RunnerJobVODWebVideoTranscodingPayload,
RunnerJobVODWebVideoTranscodingPrivatePayload,
VODWebVideoTranscodingSuccess
} from '@peertube/peertube-models'
import { buildUUID } from '@peertube/peertube-node-utils'
import { logger } from '@server/helpers/logger.js'
import { VideoJobInfoModel } from '@server/models/video/video-job-info.js'
import { MVideo } from '@server/types/models/index.js'
import { MRunnerJob } from '@server/types/models/runners/index.js'
import { generateRunnerTranscodingVideoInputFileUrl } from '../runner-urls.js'
import { AbstractVODTranscodingJobHandler } from './abstract-vod-transcoding-job-handler.js'
import { loadRunnerVideo, onVODWebVideoOrAudioMergeTranscodingJob } from './shared/utils.js'
type CreateOptions = {
video: MVideo
isNewVideo: boolean
resolution: number
fps: number
priority: number
deleteInputFileId: number | null
dependsOnRunnerJob?: MRunnerJob
}
// eslint-disable-next-line max-len
export class VODWebVideoTranscodingJobHandler extends AbstractVODTranscodingJobHandler<CreateOptions, RunnerJobUpdatePayload, VODWebVideoTranscodingSuccess> {
async create (options: CreateOptions) {
const { video, resolution, fps, priority, dependsOnRunnerJob } = options
const jobUUID = buildUUID()
const payload: RunnerJobVODWebVideoTranscodingPayload = {
input: {
videoFileUrl: generateRunnerTranscodingVideoInputFileUrl(jobUUID, video.uuid)
},
output: {
resolution,
fps
}
}
const privatePayload: RunnerJobVODWebVideoTranscodingPrivatePayload = {
...pick(options, [ 'isNewVideo', 'deleteInputFileId' ]),
videoUUID: video.uuid
}
const job = await this.createRunnerJob({
type: 'vod-web-video-transcoding',
jobUUID,
payload,
privatePayload,
dependsOnRunnerJob,
priority
})
await VideoJobInfoModel.increaseOrCreate(video.uuid, 'pendingTranscode')
return job
}
// ---------------------------------------------------------------------------
protected async specificComplete (options: {
runnerJob: MRunnerJob
resultPayload: VODWebVideoTranscodingSuccess
}) {
const { runnerJob, resultPayload } = options
const privatePayload = runnerJob.privatePayload as RunnerJobVODWebVideoTranscodingPrivatePayload
const video = await loadRunnerVideo(runnerJob, this.lTags)
if (!video) return
const videoFilePath = resultPayload.videoFile as string
await onVODWebVideoOrAudioMergeTranscodingJob({ video, videoFilePath, privatePayload, wasAudioFile: false })
logger.info(
'Runner VOD web video transcoding job %s for %s ended.',
runnerJob.uuid, video.uuid, this.lTags(video.uuid, runnerJob.uuid)
)
}
}