Support studio transcoding in peertube runner
This commit is contained in:
parent
6a49056026
commit
5e47f6ab98
|
@ -272,6 +272,8 @@ export class AdminComponent implements OnInit {
|
||||||
private isRemoteRunnersEnabled () {
|
private isRemoteRunnersEnabled () {
|
||||||
const config = this.server.getHTMLConfig()
|
const config = this.server.getHTMLConfig()
|
||||||
|
|
||||||
return config.transcoding.remoteRunners.enabled || config.live.transcoding.remoteRunners.enabled
|
return config.transcoding.remoteRunners.enabled ||
|
||||||
|
config.live.transcoding.remoteRunners.enabled ||
|
||||||
|
config.videoStudio.remoteRunners.enabled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,6 +61,10 @@ export class EditConfigurationService {
|
||||||
return form.value['transcoding']['enabled'] === true
|
return form.value['transcoding']['enabled'] === true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isStudioEnabled (form: FormGroup) {
|
||||||
|
return form.value['videoStudio']['enabled'] === true
|
||||||
|
}
|
||||||
|
|
||||||
isLiveEnabled (form: FormGroup) {
|
isLiveEnabled (form: FormGroup) {
|
||||||
return form.value['live']['enabled'] === true
|
return form.value['live']['enabled'] === true
|
||||||
}
|
}
|
||||||
|
|
|
@ -218,7 +218,10 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
videoStudio: {
|
videoStudio: {
|
||||||
|
enabled: null,
|
||||||
|
remoteRunners: {
|
||||||
enabled: null
|
enabled: null
|
||||||
|
}
|
||||||
},
|
},
|
||||||
autoBlacklist: {
|
autoBlacklist: {
|
||||||
videos: {
|
videos: {
|
||||||
|
|
|
@ -230,6 +230,20 @@
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</my-peertube-checkbox>
|
</my-peertube-checkbox>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" formGroupName="remoteRunners" [ngClass]="getStudioDisabledClass()">
|
||||||
|
<my-peertube-checkbox
|
||||||
|
inputName="videoStudioRemoteRunnersEnabled" formControlName="enabled"
|
||||||
|
i18n-labelText labelText="Enable remote runners"
|
||||||
|
>
|
||||||
|
<ng-container ngProjectAs="description">
|
||||||
|
<span i18n>
|
||||||
|
Use <a routerLink="/admin/system/runners/runners-list">remote runners</a> to process studio transcoding tasks.
|
||||||
|
Remote runners has to register on your instance first.
|
||||||
|
</span>
|
||||||
|
</ng-container>
|
||||||
|
</my-peertube-checkbox>
|
||||||
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -62,10 +62,18 @@ export class EditVODTranscodingComponent implements OnInit, OnChanges {
|
||||||
return this.editConfigurationService.isTranscodingEnabled(this.form)
|
return this.editConfigurationService.isTranscodingEnabled(this.form)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isStudioEnabled () {
|
||||||
|
return this.editConfigurationService.isStudioEnabled(this.form)
|
||||||
|
}
|
||||||
|
|
||||||
getTranscodingDisabledClass () {
|
getTranscodingDisabledClass () {
|
||||||
return { 'disabled-checkbox-extra': !this.isTranscodingEnabled() }
|
return { 'disabled-checkbox-extra': !this.isTranscodingEnabled() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getStudioDisabledClass () {
|
||||||
|
return { 'disabled-checkbox-extra': !this.isStudioEnabled() }
|
||||||
|
}
|
||||||
|
|
||||||
getTotalTranscodingThreads () {
|
getTotalTranscodingThreads () {
|
||||||
return this.editConfigurationService.getTotalTranscodingThreads(this.form)
|
return this.editConfigurationService.getTotalTranscodingThreads(this.form)
|
||||||
}
|
}
|
||||||
|
|
|
@ -579,6 +579,12 @@ video_studio:
|
||||||
# If enabled, users can create transcoding tasks as they wish
|
# If enabled, users can create transcoding tasks as they wish
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|
||||||
|
# Enable remote runners to transcode studio tasks
|
||||||
|
# If enabled, your instance won't transcode the videos itself
|
||||||
|
# At least 1 remote runner must be configured to transcode your videos
|
||||||
|
remote_runners:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
import:
|
import:
|
||||||
# Add ability for your users to import remote videos (from YouTube, torrent...)
|
# Add ability for your users to import remote videos (from YouTube, torrent...)
|
||||||
videos:
|
videos:
|
||||||
|
|
|
@ -589,6 +589,13 @@ video_studio:
|
||||||
# If enabled, users can create transcoding tasks as they wish
|
# If enabled, users can create transcoding tasks as they wish
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|
||||||
|
|
||||||
|
# Enable remote runners to transcode studio tasks
|
||||||
|
# If enabled, your instance won't transcode the videos itself
|
||||||
|
# At least 1 remote runner must be configured to transcode your videos
|
||||||
|
remote_runners:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
import:
|
import:
|
||||||
# Add ability for your users to import remote videos (from YouTube, torrent...)
|
# Add ability for your users to import remote videos (from YouTube, torrent...)
|
||||||
videos:
|
videos:
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
import { logger } from 'packages/peertube-runner/shared/logger'
|
import { logger } from 'packages/peertube-runner/shared/logger'
|
||||||
import {
|
import {
|
||||||
RunnerJobLiveRTMPHLSTranscodingPayload,
|
RunnerJobLiveRTMPHLSTranscodingPayload,
|
||||||
|
RunnerJobVideoEditionTranscodingPayload,
|
||||||
RunnerJobVODAudioMergeTranscodingPayload,
|
RunnerJobVODAudioMergeTranscodingPayload,
|
||||||
RunnerJobVODHLSTranscodingPayload,
|
RunnerJobVODHLSTranscodingPayload,
|
||||||
RunnerJobVODWebVideoTranscodingPayload
|
RunnerJobVODWebVideoTranscodingPayload
|
||||||
} from '@shared/models'
|
} from '@shared/models'
|
||||||
import { processAudioMergeTranscoding, processHLSTranscoding, ProcessOptions, processWebVideoTranscoding } from './shared'
|
import { processAudioMergeTranscoding, processHLSTranscoding, ProcessOptions, processWebVideoTranscoding } from './shared'
|
||||||
import { ProcessLiveRTMPHLSTranscoding } from './shared/process-live'
|
import { ProcessLiveRTMPHLSTranscoding } from './shared/process-live'
|
||||||
|
import { processStudioTranscoding } from './shared/process-studio'
|
||||||
|
|
||||||
export async function processJob (options: ProcessOptions) {
|
export async function processJob (options: ProcessOptions) {
|
||||||
const { server, job } = options
|
const { server, job } = options
|
||||||
|
@ -21,6 +23,8 @@ export async function processJob (options: ProcessOptions) {
|
||||||
await processHLSTranscoding(options as ProcessOptions<RunnerJobVODHLSTranscodingPayload>)
|
await processHLSTranscoding(options as ProcessOptions<RunnerJobVODHLSTranscodingPayload>)
|
||||||
} else if (job.type === 'live-rtmp-hls-transcoding') {
|
} else if (job.type === 'live-rtmp-hls-transcoding') {
|
||||||
await new ProcessLiveRTMPHLSTranscoding(options as ProcessOptions<RunnerJobLiveRTMPHLSTranscodingPayload>).process()
|
await new ProcessLiveRTMPHLSTranscoding(options as ProcessOptions<RunnerJobLiveRTMPHLSTranscodingPayload>).process()
|
||||||
|
} else if (job.type === 'video-edition-transcoding') {
|
||||||
|
await processStudioTranscoding(options as ProcessOptions<RunnerJobVideoEditionTranscodingPayload>)
|
||||||
} else {
|
} else {
|
||||||
logger.error(`Unknown job ${job.type} to process`)
|
logger.error(`Unknown job ${job.type} to process`)
|
||||||
return
|
return
|
||||||
|
|
|
@ -2,11 +2,12 @@ import { throttle } from 'lodash'
|
||||||
import { ConfigManager, downloadFile, logger } from 'packages/peertube-runner/shared'
|
import { ConfigManager, downloadFile, logger } from 'packages/peertube-runner/shared'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { buildUUID } from '@shared/extra-utils'
|
import { buildUUID } from '@shared/extra-utils'
|
||||||
import { FFmpegLive, FFmpegVOD } from '@shared/ffmpeg'
|
import { FFmpegEdition, FFmpegLive, FFmpegVOD } from '@shared/ffmpeg'
|
||||||
import { RunnerJob, RunnerJobPayload } from '@shared/models'
|
import { RunnerJob, RunnerJobPayload } from '@shared/models'
|
||||||
import { PeerTubeServer } from '@shared/server-commands'
|
import { PeerTubeServer } from '@shared/server-commands'
|
||||||
import { getTranscodingLogger } from './transcoding-logger'
|
import { getTranscodingLogger } from './transcoding-logger'
|
||||||
import { getAvailableEncoders, getEncodersToTry } from './transcoding-profiles'
|
import { getAvailableEncoders, getEncodersToTry } from './transcoding-profiles'
|
||||||
|
import { remove } from 'fs-extra'
|
||||||
|
|
||||||
export type JobWithToken <T extends RunnerJobPayload = RunnerJobPayload> = RunnerJob<T> & { jobToken: string }
|
export type JobWithToken <T extends RunnerJobPayload = RunnerJobPayload> = RunnerJob<T> & { jobToken: string }
|
||||||
|
|
||||||
|
@ -24,7 +25,14 @@ export async function downloadInputFile (options: {
|
||||||
const { url, job, runnerToken } = options
|
const { url, job, runnerToken } = options
|
||||||
const destination = join(ConfigManager.Instance.getTranscodingDirectory(), buildUUID())
|
const destination = join(ConfigManager.Instance.getTranscodingDirectory(), buildUUID())
|
||||||
|
|
||||||
|
try {
|
||||||
await downloadFile({ url, jobToken: job.jobToken, runnerToken, destination })
|
await downloadFile({ url, jobToken: job.jobToken, runnerToken, destination })
|
||||||
|
} catch (err) {
|
||||||
|
remove(destination)
|
||||||
|
.catch(err => logger.error({ err }, `Cannot remove ${destination}`))
|
||||||
|
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
|
||||||
return destination
|
return destination
|
||||||
}
|
}
|
||||||
|
@ -40,6 +48,8 @@ export async function updateTranscodingProgress (options: {
|
||||||
return server.runnerJobs.update({ jobToken: job.jobToken, jobUUID: job.uuid, runnerToken, progress })
|
return server.runnerJobs.update({ jobToken: job.jobToken, jobUUID: job.uuid, runnerToken, progress })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export function buildFFmpegVOD (options: {
|
export function buildFFmpegVOD (options: {
|
||||||
server: PeerTubeServer
|
server: PeerTubeServer
|
||||||
runnerToken: string
|
runnerToken: string
|
||||||
|
@ -58,26 +68,25 @@ export function buildFFmpegVOD (options: {
|
||||||
.catch(err => logger.error({ err }, 'Cannot send job progress'))
|
.catch(err => logger.error({ err }, 'Cannot send job progress'))
|
||||||
}, updateInterval, { trailing: false })
|
}, updateInterval, { trailing: false })
|
||||||
|
|
||||||
const config = ConfigManager.Instance.getConfig()
|
|
||||||
|
|
||||||
return new FFmpegVOD({
|
return new FFmpegVOD({
|
||||||
niceness: config.ffmpeg.nice,
|
...getCommonFFmpegOptions(),
|
||||||
threads: config.ffmpeg.threads,
|
|
||||||
tmpDirectory: ConfigManager.Instance.getTranscodingDirectory(),
|
|
||||||
profile: 'default',
|
|
||||||
availableEncoders: {
|
|
||||||
available: getAvailableEncoders(),
|
|
||||||
encodersToTry: getEncodersToTry()
|
|
||||||
},
|
|
||||||
logger: getTranscodingLogger(),
|
|
||||||
updateJobProgress
|
updateJobProgress
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildFFmpegLive () {
|
export function buildFFmpegLive () {
|
||||||
|
return new FFmpegLive(getCommonFFmpegOptions())
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildFFmpegEdition () {
|
||||||
|
return new FFmpegEdition(getCommonFFmpegOptions())
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCommonFFmpegOptions () {
|
||||||
const config = ConfigManager.Instance.getConfig()
|
const config = ConfigManager.Instance.getConfig()
|
||||||
|
|
||||||
return new FFmpegLive({
|
return {
|
||||||
niceness: config.ffmpeg.nice,
|
niceness: config.ffmpeg.nice,
|
||||||
threads: config.ffmpeg.threads,
|
threads: config.ffmpeg.threads,
|
||||||
tmpDirectory: ConfigManager.Instance.getTranscodingDirectory(),
|
tmpDirectory: ConfigManager.Instance.getTranscodingDirectory(),
|
||||||
|
@ -87,5 +96,5 @@ export function buildFFmpegLive () {
|
||||||
encodersToTry: getEncodersToTry()
|
encodersToTry: getEncodersToTry()
|
||||||
},
|
},
|
||||||
logger: getTranscodingLogger()
|
logger: getTranscodingLogger()
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,138 @@
|
||||||
|
import { remove } from 'fs-extra'
|
||||||
|
import { pick } from 'lodash'
|
||||||
|
import { logger } from 'packages/peertube-runner/shared'
|
||||||
|
import { extname, join } from 'path'
|
||||||
|
import { buildUUID } from '@shared/extra-utils'
|
||||||
|
import {
|
||||||
|
RunnerJobVideoEditionTranscodingPayload,
|
||||||
|
VideoEditionTranscodingSuccess,
|
||||||
|
VideoStudioTask,
|
||||||
|
VideoStudioTaskCutPayload,
|
||||||
|
VideoStudioTaskIntroPayload,
|
||||||
|
VideoStudioTaskOutroPayload,
|
||||||
|
VideoStudioTaskPayload,
|
||||||
|
VideoStudioTaskWatermarkPayload
|
||||||
|
} from '@shared/models'
|
||||||
|
import { ConfigManager } from '../../../shared/config-manager'
|
||||||
|
import { buildFFmpegEdition, downloadInputFile, JobWithToken, ProcessOptions } from './common'
|
||||||
|
|
||||||
|
export async function processStudioTranscoding (options: ProcessOptions<RunnerJobVideoEditionTranscodingPayload>) {
|
||||||
|
const { server, job, runnerToken } = options
|
||||||
|
const payload = job.payload
|
||||||
|
|
||||||
|
let outputPath: string
|
||||||
|
const inputPath = await downloadInputFile({ url: payload.input.videoFileUrl, runnerToken, job })
|
||||||
|
let tmpInputFilePath = inputPath
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const task of payload.tasks) {
|
||||||
|
const outputFilename = 'output-edition-' + buildUUID() + '.mp4'
|
||||||
|
outputPath = join(ConfigManager.Instance.getTranscodingDirectory(), outputFilename)
|
||||||
|
|
||||||
|
await processTask({
|
||||||
|
inputPath: tmpInputFilePath,
|
||||||
|
outputPath,
|
||||||
|
task,
|
||||||
|
job,
|
||||||
|
runnerToken
|
||||||
|
})
|
||||||
|
|
||||||
|
if (tmpInputFilePath) await remove(tmpInputFilePath)
|
||||||
|
|
||||||
|
// For the next iteration
|
||||||
|
tmpInputFilePath = outputPath
|
||||||
|
}
|
||||||
|
|
||||||
|
const successBody: VideoEditionTranscodingSuccess = {
|
||||||
|
videoFile: outputPath
|
||||||
|
}
|
||||||
|
|
||||||
|
await server.runnerJobs.success({
|
||||||
|
jobToken: job.jobToken,
|
||||||
|
jobUUID: job.uuid,
|
||||||
|
runnerToken,
|
||||||
|
payload: successBody
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
await remove(tmpInputFilePath)
|
||||||
|
await remove(outputPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Private
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type TaskProcessorOptions <T extends VideoStudioTaskPayload = VideoStudioTaskPayload> = {
|
||||||
|
inputPath: string
|
||||||
|
outputPath: string
|
||||||
|
task: T
|
||||||
|
runnerToken: string
|
||||||
|
job: JobWithToken
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskProcessors: { [id in VideoStudioTask['name']]: (options: TaskProcessorOptions) => Promise<any> } = {
|
||||||
|
'add-intro': processAddIntroOutro,
|
||||||
|
'add-outro': processAddIntroOutro,
|
||||||
|
'cut': processCut,
|
||||||
|
'add-watermark': processAddWatermark
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processTask (options: TaskProcessorOptions) {
|
||||||
|
const { task } = options
|
||||||
|
|
||||||
|
const processor = taskProcessors[options.task.name]
|
||||||
|
if (!process) throw new Error('Unknown task ' + task.name)
|
||||||
|
|
||||||
|
return processor(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processAddIntroOutro (options: TaskProcessorOptions<VideoStudioTaskIntroPayload | VideoStudioTaskOutroPayload>) {
|
||||||
|
const { inputPath, task, runnerToken, job } = options
|
||||||
|
|
||||||
|
logger.debug('Adding intro/outro to ' + inputPath)
|
||||||
|
|
||||||
|
const introOutroPath = await downloadInputFile({ url: task.options.file, runnerToken, job })
|
||||||
|
|
||||||
|
return buildFFmpegEdition().addIntroOutro({
|
||||||
|
...pick(options, [ 'inputPath', 'outputPath' ]),
|
||||||
|
|
||||||
|
introOutroPath,
|
||||||
|
type: task.name === 'add-intro'
|
||||||
|
? 'intro'
|
||||||
|
: 'outro'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function processCut (options: TaskProcessorOptions<VideoStudioTaskCutPayload>) {
|
||||||
|
const { inputPath, task } = options
|
||||||
|
|
||||||
|
logger.debug(`Cutting ${inputPath}`)
|
||||||
|
|
||||||
|
return buildFFmpegEdition().cutVideo({
|
||||||
|
...pick(options, [ 'inputPath', 'outputPath' ]),
|
||||||
|
|
||||||
|
start: task.options.start,
|
||||||
|
end: task.options.end
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processAddWatermark (options: TaskProcessorOptions<VideoStudioTaskWatermarkPayload>) {
|
||||||
|
const { inputPath, task, runnerToken, job } = options
|
||||||
|
|
||||||
|
logger.debug('Adding watermark to ' + inputPath)
|
||||||
|
|
||||||
|
const watermarkPath = await downloadInputFile({ url: task.options.file, runnerToken, job })
|
||||||
|
|
||||||
|
return buildFFmpegEdition().addWatermark({
|
||||||
|
...pick(options, [ 'inputPath', 'outputPath' ]),
|
||||||
|
|
||||||
|
watermarkPath,
|
||||||
|
|
||||||
|
videoFilters: {
|
||||||
|
watermarkSizeRatio: task.options.watermarkSizeRatio,
|
||||||
|
horitonzalMarginRatio: task.options.horitonzalMarginRatio,
|
||||||
|
verticalMarginRatio: task.options.verticalMarginRatio
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -62,6 +62,7 @@ export async function processHLSTranscoding (options: ProcessOptions<RunnerJobVO
|
||||||
|
|
||||||
const ffmpegVod = buildFFmpegVOD({ job, server, runnerToken })
|
const ffmpegVod = buildFFmpegVOD({ job, server, runnerToken })
|
||||||
|
|
||||||
|
try {
|
||||||
await ffmpegVod.transcode({
|
await ffmpegVod.transcode({
|
||||||
type: 'hls',
|
type: 'hls',
|
||||||
copyCodecs: false,
|
copyCodecs: false,
|
||||||
|
@ -86,9 +87,11 @@ export async function processHLSTranscoding (options: ProcessOptions<RunnerJobVO
|
||||||
runnerToken,
|
runnerToken,
|
||||||
payload: successBody
|
payload: successBody
|
||||||
})
|
})
|
||||||
|
} finally {
|
||||||
|
await remove(inputPath)
|
||||||
await remove(outputPath)
|
await remove(outputPath)
|
||||||
await remove(videoPath)
|
await remove(videoPath)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function processAudioMergeTranscoding (options: ProcessOptions<RunnerJobVODAudioMergeTranscodingPayload>) {
|
export async function processAudioMergeTranscoding (options: ProcessOptions<RunnerJobVODAudioMergeTranscodingPayload>) {
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { ConfigManager } from '../shared'
|
||||||
import { IPCServer } from '../shared/ipc'
|
import { IPCServer } from '../shared/ipc'
|
||||||
import { logger } from '../shared/logger'
|
import { logger } from '../shared/logger'
|
||||||
import { JobWithToken, processJob } from './process'
|
import { JobWithToken, processJob } from './process'
|
||||||
|
import { isJobSupported } from './shared'
|
||||||
|
|
||||||
type PeerTubeServer = PeerTubeServerCommand & {
|
type PeerTubeServer = PeerTubeServerCommand & {
|
||||||
runnerToken: string
|
runnerToken: string
|
||||||
|
@ -199,12 +200,14 @@ export class RunnerServer {
|
||||||
|
|
||||||
const { availableJobs } = await server.runnerJobs.request({ runnerToken: server.runnerToken })
|
const { availableJobs } = await server.runnerJobs.request({ runnerToken: server.runnerToken })
|
||||||
|
|
||||||
if (availableJobs.length === 0) {
|
const filtered = availableJobs.filter(j => isJobSupported(j))
|
||||||
|
|
||||||
|
if (filtered.length === 0) {
|
||||||
logger.debug(`No job available on ${server.url} for runner ${server.runnerName}`)
|
logger.debug(`No job available on ${server.url} for runner ${server.runnerName}`)
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
return availableJobs[0]
|
return filtered[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
private async tryToExecuteJobAsync (server: PeerTubeServer, jobToAccept: { uuid: string }) {
|
private async tryToExecuteJobAsync (server: PeerTubeServer, jobToAccept: { uuid: string }) {
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './supported-job'
|
|
@ -0,0 +1,43 @@
|
||||||
|
import {
|
||||||
|
RunnerJobLiveRTMPHLSTranscodingPayload,
|
||||||
|
RunnerJobPayload,
|
||||||
|
RunnerJobType,
|
||||||
|
RunnerJobVideoEditionTranscodingPayload,
|
||||||
|
RunnerJobVODAudioMergeTranscodingPayload,
|
||||||
|
RunnerJobVODHLSTranscodingPayload,
|
||||||
|
RunnerJobVODWebVideoTranscodingPayload,
|
||||||
|
VideoStudioTaskPayload
|
||||||
|
} from '@shared/models'
|
||||||
|
|
||||||
|
const supportedMatrix = {
|
||||||
|
'vod-web-video-transcoding': (_payload: RunnerJobVODWebVideoTranscodingPayload) => {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
'vod-hls-transcoding': (_payload: RunnerJobVODHLSTranscodingPayload) => {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
'vod-audio-merge-transcoding': (_payload: RunnerJobVODAudioMergeTranscodingPayload) => {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
'live-rtmp-hls-transcoding': (_payload: RunnerJobLiveRTMPHLSTranscodingPayload) => {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
'video-edition-transcoding': (payload: RunnerJobVideoEditionTranscodingPayload) => {
|
||||||
|
const tasks = payload?.tasks
|
||||||
|
const supported = new Set<VideoStudioTaskPayload['name']>([ 'add-intro', 'add-outro', 'add-watermark', 'cut' ])
|
||||||
|
|
||||||
|
if (!Array.isArray(tasks)) return false
|
||||||
|
|
||||||
|
return tasks.every(t => t && supported.has(t.name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isJobSupported (job: {
|
||||||
|
type: RunnerJobType
|
||||||
|
payload: RunnerJobPayload
|
||||||
|
}) {
|
||||||
|
const fn = supportedMatrix[job.type]
|
||||||
|
if (!fn) return false
|
||||||
|
|
||||||
|
return fn(job.payload as any)
|
||||||
|
}
|
|
@ -274,7 +274,10 @@ function customConfig (): CustomConfig {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
videoStudio: {
|
videoStudio: {
|
||||||
enabled: CONFIG.VIDEO_STUDIO.ENABLED
|
enabled: CONFIG.VIDEO_STUDIO.ENABLED,
|
||||||
|
remoteRunners: {
|
||||||
|
enabled: CONFIG.VIDEO_STUDIO.REMOTE_RUNNERS.ENABLED
|
||||||
|
}
|
||||||
},
|
},
|
||||||
import: {
|
import: {
|
||||||
videos: {
|
videos: {
|
||||||
|
|
|
@ -2,9 +2,13 @@ import express from 'express'
|
||||||
import { logger, loggerTagsFactory } from '@server/helpers/logger'
|
import { logger, loggerTagsFactory } from '@server/helpers/logger'
|
||||||
import { proxifyHLS, proxifyWebTorrentFile } from '@server/lib/object-storage'
|
import { proxifyHLS, proxifyWebTorrentFile } from '@server/lib/object-storage'
|
||||||
import { VideoPathManager } from '@server/lib/video-path-manager'
|
import { VideoPathManager } from '@server/lib/video-path-manager'
|
||||||
|
import { getStudioTaskFilePath } from '@server/lib/video-studio'
|
||||||
import { asyncMiddleware } from '@server/middlewares'
|
import { asyncMiddleware } from '@server/middlewares'
|
||||||
import { jobOfRunnerGetValidator } from '@server/middlewares/validators/runners'
|
import { jobOfRunnerGetValidator } from '@server/middlewares/validators/runners'
|
||||||
import { runnerJobGetVideoTranscodingFileValidator } from '@server/middlewares/validators/runners/job-files'
|
import {
|
||||||
|
runnerJobGetVideoStudioTaskFileValidator,
|
||||||
|
runnerJobGetVideoTranscodingFileValidator
|
||||||
|
} from '@server/middlewares/validators/runners/job-files'
|
||||||
import { VideoStorage } from '@shared/models'
|
import { VideoStorage } from '@shared/models'
|
||||||
|
|
||||||
const lTags = loggerTagsFactory('api', 'runner')
|
const lTags = loggerTagsFactory('api', 'runner')
|
||||||
|
@ -23,6 +27,13 @@ runnerJobFilesRouter.post('/jobs/:jobUUID/files/videos/:videoId/previews/max-qua
|
||||||
getMaxQualityVideoPreview
|
getMaxQualityVideoPreview
|
||||||
)
|
)
|
||||||
|
|
||||||
|
runnerJobFilesRouter.post('/jobs/:jobUUID/files/videos/:videoId/studio/task-files/:filename',
|
||||||
|
asyncMiddleware(jobOfRunnerGetValidator),
|
||||||
|
asyncMiddleware(runnerJobGetVideoTranscodingFileValidator),
|
||||||
|
runnerJobGetVideoStudioTaskFileValidator,
|
||||||
|
getVideoEditionTaskFile
|
||||||
|
)
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
@ -82,3 +93,17 @@ function getMaxQualityVideoPreview (req: express.Request, res: express.Response)
|
||||||
|
|
||||||
return res.sendFile(file.getPath())
|
return res.sendFile(file.getPath())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getVideoEditionTaskFile (req: express.Request, res: express.Response) {
|
||||||
|
const runnerJob = res.locals.runnerJob
|
||||||
|
const runner = runnerJob.Runner
|
||||||
|
const video = res.locals.videoAll
|
||||||
|
const filename = req.params.filename
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
'Get video edition task file %s of video %s of job %s for runner %s', filename, video.uuid, runnerJob.uuid, runner.name,
|
||||||
|
lTags(runner.name, runnerJob.id, runnerJob.type)
|
||||||
|
)
|
||||||
|
|
||||||
|
return res.sendFile(getStudioTaskFilePath(filename))
|
||||||
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ import {
|
||||||
import {
|
import {
|
||||||
abortRunnerJobValidator,
|
abortRunnerJobValidator,
|
||||||
acceptRunnerJobValidator,
|
acceptRunnerJobValidator,
|
||||||
|
cancelRunnerJobValidator,
|
||||||
errorRunnerJobValidator,
|
errorRunnerJobValidator,
|
||||||
getRunnerFromTokenValidator,
|
getRunnerFromTokenValidator,
|
||||||
jobOfRunnerGetValidator,
|
jobOfRunnerGetValidator,
|
||||||
|
@ -41,6 +42,7 @@ import {
|
||||||
RunnerJobUpdateBody,
|
RunnerJobUpdateBody,
|
||||||
RunnerJobUpdatePayload,
|
RunnerJobUpdatePayload,
|
||||||
UserRight,
|
UserRight,
|
||||||
|
VideoEditionTranscodingSuccess,
|
||||||
VODAudioMergeTranscodingSuccess,
|
VODAudioMergeTranscodingSuccess,
|
||||||
VODHLSTranscodingSuccess,
|
VODHLSTranscodingSuccess,
|
||||||
VODWebVideoTranscodingSuccess
|
VODWebVideoTranscodingSuccess
|
||||||
|
@ -110,6 +112,7 @@ runnerJobsRouter.post('/jobs/:jobUUID/cancel',
|
||||||
authenticate,
|
authenticate,
|
||||||
ensureUserHasRight(UserRight.MANAGE_RUNNERS),
|
ensureUserHasRight(UserRight.MANAGE_RUNNERS),
|
||||||
asyncMiddleware(runnerJobGetValidator),
|
asyncMiddleware(runnerJobGetValidator),
|
||||||
|
cancelRunnerJobValidator,
|
||||||
asyncMiddleware(cancelRunnerJob)
|
asyncMiddleware(cancelRunnerJob)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -297,6 +300,14 @@ const jobSuccessPayloadBuilders: {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'video-edition-transcoding': (payload: VideoEditionTranscodingSuccess, files) => {
|
||||||
|
return {
|
||||||
|
...payload,
|
||||||
|
|
||||||
|
videoFile: files['payload[videoFile]'][0].path
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
'live-rtmp-hls-transcoding': () => ({})
|
'live-rtmp-hls-transcoding': () => ({})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -327,7 +338,7 @@ async function postRunnerJobSuccess (req: express.Request, res: express.Response
|
||||||
async function cancelRunnerJob (req: express.Request, res: express.Response) {
|
async function cancelRunnerJob (req: express.Request, res: express.Response) {
|
||||||
const runnerJob = res.locals.runnerJob
|
const runnerJob = res.locals.runnerJob
|
||||||
|
|
||||||
logger.info('Cancelling job %s (%s)', runnerJob.type, lTags(runnerJob.uuid, runnerJob.type))
|
logger.info('Cancelling job %s (%s)', runnerJob.uuid, runnerJob.type, lTags(runnerJob.uuid, runnerJob.type))
|
||||||
|
|
||||||
const RunnerJobHandler = getRunnerJobHandlerClass(runnerJob)
|
const RunnerJobHandler = getRunnerJobHandlerClass(runnerJob)
|
||||||
await new RunnerJobHandler().cancel({ runnerJob })
|
await new RunnerJobHandler().cancel({ runnerJob })
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
import Bluebird from 'bluebird'
|
import Bluebird from 'bluebird'
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { move } from 'fs-extra'
|
import { move } from 'fs-extra'
|
||||||
import { basename, join } from 'path'
|
import { basename } from 'path'
|
||||||
import { createAnyReqFiles } from '@server/helpers/express-utils'
|
import { createAnyReqFiles } from '@server/helpers/express-utils'
|
||||||
import { CONFIG } from '@server/initializers/config'
|
import { MIMETYPES, VIDEO_FILTERS } from '@server/initializers/constants'
|
||||||
import { MIMETYPES } from '@server/initializers/constants'
|
import { buildTaskFileFieldname, createVideoStudioJob, getStudioTaskFilePath, getTaskFileFromReq } from '@server/lib/video-studio'
|
||||||
import { JobQueue } from '@server/lib/job-queue'
|
|
||||||
import { buildTaskFileFieldname, getTaskFileFromReq } from '@server/lib/video-studio'
|
|
||||||
import {
|
import {
|
||||||
HttpStatusCode,
|
HttpStatusCode,
|
||||||
VideoState,
|
VideoState,
|
||||||
|
@ -75,7 +73,11 @@ async function createEditionTasks (req: express.Request, res: express.Response)
|
||||||
tasks: await Bluebird.mapSeries(body.tasks, (t, i) => buildTaskPayload(t, i, files))
|
tasks: await Bluebird.mapSeries(body.tasks, (t, i) => buildTaskPayload(t, i, files))
|
||||||
}
|
}
|
||||||
|
|
||||||
JobQueue.Instance.createJobAsync({ type: 'video-studio-edition', payload })
|
await createVideoStudioJob({
|
||||||
|
user: res.locals.oauth.token.User,
|
||||||
|
payload,
|
||||||
|
video
|
||||||
|
})
|
||||||
|
|
||||||
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||||
}
|
}
|
||||||
|
@ -124,13 +126,16 @@ async function buildWatermarkTask (task: VideoStudioTaskWatermark, indice: numbe
|
||||||
return {
|
return {
|
||||||
name: task.name,
|
name: task.name,
|
||||||
options: {
|
options: {
|
||||||
file: destination
|
file: destination,
|
||||||
|
watermarkSizeRatio: VIDEO_FILTERS.WATERMARK.SIZE_RATIO,
|
||||||
|
horitonzalMarginRatio: VIDEO_FILTERS.WATERMARK.HORIZONTAL_MARGIN_RATIO,
|
||||||
|
verticalMarginRatio: VIDEO_FILTERS.WATERMARK.VERTICAL_MARGIN_RATIO
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function moveStudioFileToPersistentTMP (file: string) {
|
async function moveStudioFileToPersistentTMP (file: string) {
|
||||||
const destination = join(CONFIG.STORAGE.TMP_PERSISTENT_DIR, basename(file))
|
const destination = getStudioTaskFilePath(basename(file))
|
||||||
|
|
||||||
await move(file, destination)
|
await move(file, destination)
|
||||||
|
|
||||||
|
|
|
@ -15,8 +15,12 @@ function isSafePath (p: string) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSafeFilename (filename: string, extension: string) {
|
function isSafeFilename (filename: string, extension?: string) {
|
||||||
return typeof filename === 'string' && !!filename.match(new RegExp(`^[a-z0-9-]+\\.${extension}$`))
|
const regex = extension
|
||||||
|
? new RegExp(`^[a-z0-9-]+\\.${extension}$`)
|
||||||
|
: new RegExp(`^[a-z0-9-]+\\.[a-z0-9]{1,8}$`)
|
||||||
|
|
||||||
|
return typeof filename === 'string' && !!filename.match(regex)
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSafePeerTubeFilenameWithoutExtension (filename: string) {
|
function isSafePeerTubeFilenameWithoutExtension (filename: string) {
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {
|
||||||
RunnerJobSuccessPayload,
|
RunnerJobSuccessPayload,
|
||||||
RunnerJobType,
|
RunnerJobType,
|
||||||
RunnerJobUpdatePayload,
|
RunnerJobUpdatePayload,
|
||||||
|
VideoEditionTranscodingSuccess,
|
||||||
VODAudioMergeTranscodingSuccess,
|
VODAudioMergeTranscodingSuccess,
|
||||||
VODHLSTranscodingSuccess,
|
VODHLSTranscodingSuccess,
|
||||||
VODWebVideoTranscodingSuccess
|
VODWebVideoTranscodingSuccess
|
||||||
|
@ -23,7 +24,8 @@ function isRunnerJobSuccessPayloadValid (value: RunnerJobSuccessPayload, type: R
|
||||||
return isRunnerJobVODWebVideoResultPayloadValid(value as VODWebVideoTranscodingSuccess, type, files) ||
|
return isRunnerJobVODWebVideoResultPayloadValid(value as VODWebVideoTranscodingSuccess, type, files) ||
|
||||||
isRunnerJobVODHLSResultPayloadValid(value as VODHLSTranscodingSuccess, type, files) ||
|
isRunnerJobVODHLSResultPayloadValid(value as VODHLSTranscodingSuccess, type, files) ||
|
||||||
isRunnerJobVODAudioMergeResultPayloadValid(value as VODHLSTranscodingSuccess, type, files) ||
|
isRunnerJobVODAudioMergeResultPayloadValid(value as VODHLSTranscodingSuccess, type, files) ||
|
||||||
isRunnerJobLiveRTMPHLSResultPayloadValid(value as LiveRTMPHLSTranscodingSuccess, type)
|
isRunnerJobLiveRTMPHLSResultPayloadValid(value as LiveRTMPHLSTranscodingSuccess, type) ||
|
||||||
|
isRunnerJobVideoEditionResultPayloadValid(value as VideoEditionTranscodingSuccess, type, files)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
@ -35,6 +37,7 @@ function isRunnerJobProgressValid (value: string) {
|
||||||
function isRunnerJobUpdatePayloadValid (value: RunnerJobUpdatePayload, type: RunnerJobType, files: UploadFilesForCheck) {
|
function isRunnerJobUpdatePayloadValid (value: RunnerJobUpdatePayload, type: RunnerJobType, files: UploadFilesForCheck) {
|
||||||
return isRunnerJobVODWebVideoUpdatePayloadValid(value, type, files) ||
|
return isRunnerJobVODWebVideoUpdatePayloadValid(value, type, files) ||
|
||||||
isRunnerJobVODHLSUpdatePayloadValid(value, type, files) ||
|
isRunnerJobVODHLSUpdatePayloadValid(value, type, files) ||
|
||||||
|
isRunnerJobVideoEditionUpdatePayloadValid(value, type, files) ||
|
||||||
isRunnerJobVODAudioMergeUpdatePayloadValid(value, type, files) ||
|
isRunnerJobVODAudioMergeUpdatePayloadValid(value, type, files) ||
|
||||||
isRunnerJobLiveRTMPHLSUpdatePayloadValid(value, type, files)
|
isRunnerJobLiveRTMPHLSUpdatePayloadValid(value, type, files)
|
||||||
}
|
}
|
||||||
|
@ -102,6 +105,15 @@ function isRunnerJobLiveRTMPHLSResultPayloadValid (
|
||||||
return type === 'live-rtmp-hls-transcoding' && (!value || (typeof value === 'object' && Object.keys(value).length === 0))
|
return type === 'live-rtmp-hls-transcoding' && (!value || (typeof value === 'object' && Object.keys(value).length === 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isRunnerJobVideoEditionResultPayloadValid (
|
||||||
|
_value: VideoEditionTranscodingSuccess,
|
||||||
|
type: RunnerJobType,
|
||||||
|
files: UploadFilesForCheck
|
||||||
|
) {
|
||||||
|
return type === 'video-edition-transcoding' &&
|
||||||
|
isFileValid({ files, field: 'payload[videoFile]', mimeTypeRegex: null, maxSize: null })
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function isRunnerJobVODWebVideoUpdatePayloadValid (
|
function isRunnerJobVODWebVideoUpdatePayloadValid (
|
||||||
|
@ -164,3 +176,12 @@ function isRunnerJobLiveRTMPHLSUpdatePayloadValid (
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isRunnerJobVideoEditionUpdatePayloadValid (
|
||||||
|
value: RunnerJobUpdatePayload,
|
||||||
|
type: RunnerJobType,
|
||||||
|
_files: UploadFilesForCheck
|
||||||
|
) {
|
||||||
|
return type === 'video-edition-transcoding' &&
|
||||||
|
(!value || (typeof value === 'object' && Object.keys(value).length === 0))
|
||||||
|
}
|
||||||
|
|
|
@ -38,7 +38,7 @@ function checkMissedConfig () {
|
||||||
'transcoding.resolutions.0p', 'transcoding.resolutions.144p', 'transcoding.resolutions.240p', 'transcoding.resolutions.360p',
|
'transcoding.resolutions.0p', 'transcoding.resolutions.144p', 'transcoding.resolutions.240p', 'transcoding.resolutions.360p',
|
||||||
'transcoding.resolutions.480p', 'transcoding.resolutions.720p', 'transcoding.resolutions.1080p', 'transcoding.resolutions.1440p',
|
'transcoding.resolutions.480p', 'transcoding.resolutions.720p', 'transcoding.resolutions.1080p', 'transcoding.resolutions.1440p',
|
||||||
'transcoding.resolutions.2160p', 'transcoding.always_transcode_original_resolution', 'transcoding.remote_runners.enabled',
|
'transcoding.resolutions.2160p', 'transcoding.always_transcode_original_resolution', 'transcoding.remote_runners.enabled',
|
||||||
'video_studio.enabled',
|
'video_studio.enabled', 'video_studio.remote_runners.enabled',
|
||||||
'remote_runners.stalled_jobs.vod', 'remote_runners.stalled_jobs.live',
|
'remote_runners.stalled_jobs.vod', 'remote_runners.stalled_jobs.live',
|
||||||
'import.videos.http.enabled', 'import.videos.torrent.enabled', 'import.videos.concurrency', 'import.videos.timeout',
|
'import.videos.http.enabled', 'import.videos.torrent.enabled', 'import.videos.concurrency', 'import.videos.timeout',
|
||||||
'import.video_channel_synchronization.enabled', 'import.video_channel_synchronization.max_per_user',
|
'import.video_channel_synchronization.enabled', 'import.video_channel_synchronization.max_per_user',
|
||||||
|
|
|
@ -423,7 +423,10 @@ const CONFIG = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
VIDEO_STUDIO: {
|
VIDEO_STUDIO: {
|
||||||
get ENABLED () { return config.get<boolean>('video_studio.enabled') }
|
get ENABLED () { return config.get<boolean>('video_studio.enabled') },
|
||||||
|
REMOTE_RUNNERS: {
|
||||||
|
get ENABLED () { return config.get<boolean>('video_studio.remote_runners.enabled') }
|
||||||
|
}
|
||||||
},
|
},
|
||||||
IMPORT: {
|
IMPORT: {
|
||||||
VIDEOS: {
|
VIDEOS: {
|
||||||
|
|
|
@ -229,7 +229,8 @@ const REPEAT_JOBS: { [ id in JobType ]?: RepeatOptions } = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const JOB_PRIORITY = {
|
const JOB_PRIORITY = {
|
||||||
TRANSCODING: 100
|
TRANSCODING: 100,
|
||||||
|
VIDEO_STUDIO: 150
|
||||||
}
|
}
|
||||||
|
|
||||||
const JOB_REMOVAL_OPTIONS = {
|
const JOB_REMOVAL_OPTIONS = {
|
||||||
|
|
|
@ -1,25 +1,18 @@
|
||||||
import { Job } from 'bullmq'
|
import { Job } from 'bullmq'
|
||||||
import { move, remove } from 'fs-extra'
|
import { remove } from 'fs-extra'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { getFFmpegCommandWrapperOptions } from '@server/helpers/ffmpeg'
|
import { getFFmpegCommandWrapperOptions } from '@server/helpers/ffmpeg'
|
||||||
import { createTorrentAndSetInfoHashFromPath } from '@server/helpers/webtorrent'
|
|
||||||
import { CONFIG } from '@server/initializers/config'
|
import { CONFIG } from '@server/initializers/config'
|
||||||
import { VIDEO_FILTERS } from '@server/initializers/constants'
|
|
||||||
import { federateVideoIfNeeded } from '@server/lib/activitypub/videos'
|
|
||||||
import { generateWebTorrentVideoFilename } from '@server/lib/paths'
|
|
||||||
import { createOptimizeOrMergeAudioJobs } from '@server/lib/transcoding/create-transcoding-job'
|
|
||||||
import { VideoTranscodingProfilesManager } from '@server/lib/transcoding/default-transcoding-profiles'
|
import { VideoTranscodingProfilesManager } from '@server/lib/transcoding/default-transcoding-profiles'
|
||||||
import { isAbleToUploadVideo } from '@server/lib/user'
|
import { isAbleToUploadVideo } from '@server/lib/user'
|
||||||
import { buildFileMetadata, removeHLSPlaylist, removeWebTorrentFile } from '@server/lib/video-file'
|
|
||||||
import { VideoPathManager } from '@server/lib/video-path-manager'
|
import { VideoPathManager } from '@server/lib/video-path-manager'
|
||||||
import { approximateIntroOutroAdditionalSize, safeCleanupStudioTMPFiles } from '@server/lib/video-studio'
|
import { approximateIntroOutroAdditionalSize, onVideoEditionEnded, safeCleanupStudioTMPFiles } from '@server/lib/video-studio'
|
||||||
import { UserModel } from '@server/models/user/user'
|
import { UserModel } from '@server/models/user/user'
|
||||||
import { VideoModel } from '@server/models/video/video'
|
import { VideoModel } from '@server/models/video/video'
|
||||||
import { VideoFileModel } from '@server/models/video/video-file'
|
import { MVideo, MVideoFullLight } from '@server/types/models'
|
||||||
import { MVideo, MVideoFile, MVideoFullLight, MVideoId, MVideoWithAllFiles } from '@server/types/models'
|
import { pick } from '@shared/core-utils'
|
||||||
import { getLowercaseExtension, pick } from '@shared/core-utils'
|
import { buildUUID } from '@shared/extra-utils'
|
||||||
import { buildUUID, getFileSize } from '@shared/extra-utils'
|
import { FFmpegEdition } from '@shared/ffmpeg'
|
||||||
import { FFmpegEdition, ffprobePromise, getVideoStreamDimensionsInfo, getVideoStreamDuration, getVideoStreamFPS } from '@shared/ffmpeg'
|
|
||||||
import {
|
import {
|
||||||
VideoStudioEditionPayload,
|
VideoStudioEditionPayload,
|
||||||
VideoStudioTask,
|
VideoStudioTask,
|
||||||
|
@ -46,7 +39,7 @@ async function processVideoStudioEdition (job: Job) {
|
||||||
if (!video) {
|
if (!video) {
|
||||||
logger.info('Can\'t process job %d, video does not exist.', job.id, lTags)
|
logger.info('Can\'t process job %d, video does not exist.', job.id, lTags)
|
||||||
|
|
||||||
await safeCleanupStudioTMPFiles(payload)
|
await safeCleanupStudioTMPFiles(payload.tasks)
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,28 +74,9 @@ async function processVideoStudioEdition (job: Job) {
|
||||||
|
|
||||||
logger.info('Video edition ended for video %s.', video.uuid, lTags)
|
logger.info('Video edition ended for video %s.', video.uuid, lTags)
|
||||||
|
|
||||||
const newFile = await buildNewFile(video, editionResultPath)
|
await onVideoEditionEnded({ video, editionResultPath, tasks: payload.tasks })
|
||||||
|
|
||||||
const outputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, newFile)
|
|
||||||
await move(editionResultPath, outputPath)
|
|
||||||
|
|
||||||
await safeCleanupStudioTMPFiles(payload)
|
|
||||||
|
|
||||||
await createTorrentAndSetInfoHashFromPath(video, newFile, outputPath)
|
|
||||||
await removeAllFiles(video, newFile)
|
|
||||||
|
|
||||||
await newFile.save()
|
|
||||||
|
|
||||||
video.duration = await getVideoStreamDuration(outputPath)
|
|
||||||
await video.save()
|
|
||||||
|
|
||||||
await federateVideoIfNeeded(video, false, undefined)
|
|
||||||
|
|
||||||
const user = await UserModel.loadByVideoId(video.id)
|
|
||||||
|
|
||||||
await createOptimizeOrMergeAudioJobs({ video, videoFile: newFile, isNewVideo: false, user, videoFileAlreadyLocked: false })
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
await safeCleanupStudioTMPFiles(payload)
|
await safeCleanupStudioTMPFiles(payload.tasks)
|
||||||
|
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
|
@ -181,44 +155,15 @@ function processAddWatermark (options: TaskProcessorOptions<VideoStudioTaskWater
|
||||||
watermarkPath: task.options.file,
|
watermarkPath: task.options.file,
|
||||||
|
|
||||||
videoFilters: {
|
videoFilters: {
|
||||||
watermarkSizeRatio: VIDEO_FILTERS.WATERMARK.SIZE_RATIO,
|
watermarkSizeRatio: task.options.watermarkSizeRatio,
|
||||||
horitonzalMarginRatio: VIDEO_FILTERS.WATERMARK.HORIZONTAL_MARGIN_RATIO,
|
horitonzalMarginRatio: task.options.horitonzalMarginRatio,
|
||||||
verticalMarginRatio: VIDEO_FILTERS.WATERMARK.VERTICAL_MARGIN_RATIO
|
verticalMarginRatio: task.options.verticalMarginRatio
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async function buildNewFile (video: MVideoId, path: string) {
|
|
||||||
const videoFile = new VideoFileModel({
|
|
||||||
extname: getLowercaseExtension(path),
|
|
||||||
size: await getFileSize(path),
|
|
||||||
metadata: await buildFileMetadata(path),
|
|
||||||
videoStreamingPlaylistId: null,
|
|
||||||
videoId: video.id
|
|
||||||
})
|
|
||||||
|
|
||||||
const probe = await ffprobePromise(path)
|
|
||||||
|
|
||||||
videoFile.fps = await getVideoStreamFPS(path, probe)
|
|
||||||
videoFile.resolution = (await getVideoStreamDimensionsInfo(path, probe)).resolution
|
|
||||||
|
|
||||||
videoFile.filename = generateWebTorrentVideoFilename(videoFile.resolution, videoFile.extname)
|
|
||||||
|
|
||||||
return videoFile
|
|
||||||
}
|
|
||||||
|
|
||||||
async function removeAllFiles (video: MVideoWithAllFiles, webTorrentFileException: MVideoFile) {
|
|
||||||
await removeHLSPlaylist(video)
|
|
||||||
|
|
||||||
for (const file of video.VideoFiles) {
|
|
||||||
if (file.id === webTorrentFileException.id) continue
|
|
||||||
|
|
||||||
await removeWebTorrentFile(video, file.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkUserQuotaOrThrow (video: MVideoFullLight, payload: VideoStudioEditionPayload) {
|
async function checkUserQuotaOrThrow (video: MVideoFullLight, payload: VideoStudioEditionPayload) {
|
||||||
const user = await UserModel.loadByVideoId(video.id)
|
const user = await UserModel.loadByVideoId(video.id)
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { throttle } from 'lodash'
|
||||||
import { retryTransactionWrapper } from '@server/helpers/database-utils'
|
import { retryTransactionWrapper } from '@server/helpers/database-utils'
|
||||||
import { logger, loggerTagsFactory } from '@server/helpers/logger'
|
import { logger, loggerTagsFactory } from '@server/helpers/logger'
|
||||||
import { RUNNER_JOBS } from '@server/initializers/constants'
|
import { RUNNER_JOBS } from '@server/initializers/constants'
|
||||||
|
@ -14,6 +15,8 @@ import {
|
||||||
RunnerJobSuccessPayload,
|
RunnerJobSuccessPayload,
|
||||||
RunnerJobType,
|
RunnerJobType,
|
||||||
RunnerJobUpdatePayload,
|
RunnerJobUpdatePayload,
|
||||||
|
RunnerJobVideoEditionTranscodingPayload,
|
||||||
|
RunnerJobVideoEditionTranscodingPrivatePayload,
|
||||||
RunnerJobVODAudioMergeTranscodingPayload,
|
RunnerJobVODAudioMergeTranscodingPayload,
|
||||||
RunnerJobVODAudioMergeTranscodingPrivatePayload,
|
RunnerJobVODAudioMergeTranscodingPrivatePayload,
|
||||||
RunnerJobVODHLSTranscodingPayload,
|
RunnerJobVODHLSTranscodingPayload,
|
||||||
|
@ -21,7 +24,6 @@ import {
|
||||||
RunnerJobVODWebVideoTranscodingPayload,
|
RunnerJobVODWebVideoTranscodingPayload,
|
||||||
RunnerJobVODWebVideoTranscodingPrivatePayload
|
RunnerJobVODWebVideoTranscodingPrivatePayload
|
||||||
} from '@shared/models'
|
} from '@shared/models'
|
||||||
import { throttle } from 'lodash'
|
|
||||||
|
|
||||||
type CreateRunnerJobArg =
|
type CreateRunnerJobArg =
|
||||||
{
|
{
|
||||||
|
@ -43,6 +45,11 @@ type CreateRunnerJobArg =
|
||||||
type: Extract<RunnerJobType, 'live-rtmp-hls-transcoding'>
|
type: Extract<RunnerJobType, 'live-rtmp-hls-transcoding'>
|
||||||
payload: RunnerJobLiveRTMPHLSTranscodingPayload
|
payload: RunnerJobLiveRTMPHLSTranscodingPayload
|
||||||
privatePayload: RunnerJobLiveRTMPHLSTranscodingPrivatePayload
|
privatePayload: RunnerJobLiveRTMPHLSTranscodingPrivatePayload
|
||||||
|
} |
|
||||||
|
{
|
||||||
|
type: Extract<RunnerJobType, 'video-edition-transcoding'>
|
||||||
|
payload: RunnerJobVideoEditionTranscodingPayload
|
||||||
|
privatePayload: RunnerJobVideoEditionTranscodingPrivatePayload
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class AbstractJobHandler <C, U extends RunnerJobUpdatePayload, S extends RunnerJobSuccessPayload> {
|
export abstract class AbstractJobHandler <C, U extends RunnerJobUpdatePayload, S extends RunnerJobSuccessPayload> {
|
||||||
|
@ -62,6 +69,8 @@ export abstract class AbstractJobHandler <C, U extends RunnerJobUpdatePayload, S
|
||||||
}): Promise<MRunnerJob> {
|
}): Promise<MRunnerJob> {
|
||||||
const { priority, dependsOnRunnerJob } = options
|
const { priority, dependsOnRunnerJob } = options
|
||||||
|
|
||||||
|
logger.debug('Creating runner job', { options, ...this.lTags(options.type) })
|
||||||
|
|
||||||
const runnerJob = new RunnerJobModel({
|
const runnerJob = new RunnerJobModel({
|
||||||
...pick(options, [ 'type', 'payload', 'privatePayload' ]),
|
...pick(options, [ 'type', 'payload', 'privatePayload' ]),
|
||||||
|
|
||||||
|
|
|
@ -4,27 +4,19 @@ import { logger } from '@server/helpers/logger'
|
||||||
import { moveToFailedTranscodingState, moveToNextState } from '@server/lib/video-state'
|
import { moveToFailedTranscodingState, moveToNextState } from '@server/lib/video-state'
|
||||||
import { VideoJobInfoModel } from '@server/models/video/video-job-info'
|
import { VideoJobInfoModel } from '@server/models/video/video-job-info'
|
||||||
import { MRunnerJob } from '@server/types/models/runners'
|
import { MRunnerJob } from '@server/types/models/runners'
|
||||||
import {
|
import { RunnerJobSuccessPayload, RunnerJobUpdatePayload, RunnerJobVODPrivatePayload } from '@shared/models'
|
||||||
LiveRTMPHLSTranscodingUpdatePayload,
|
|
||||||
RunnerJobSuccessPayload,
|
|
||||||
RunnerJobUpdatePayload,
|
|
||||||
RunnerJobVODPrivatePayload
|
|
||||||
} from '@shared/models'
|
|
||||||
import { AbstractJobHandler } from './abstract-job-handler'
|
import { AbstractJobHandler } from './abstract-job-handler'
|
||||||
import { loadTranscodingRunnerVideo } from './shared'
|
import { loadTranscodingRunnerVideo } from './shared'
|
||||||
|
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
export abstract class AbstractVODTranscodingJobHandler <C, U extends RunnerJobUpdatePayload, S extends RunnerJobSuccessPayload> extends AbstractJobHandler<C, U, S> {
|
export abstract class AbstractVODTranscodingJobHandler <C, U extends RunnerJobUpdatePayload, S extends RunnerJobSuccessPayload> extends AbstractJobHandler<C, U, S> {
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
protected isAbortSupported () {
|
protected isAbortSupported () {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
protected specificUpdate (_options: {
|
protected specificUpdate (_options: {
|
||||||
runnerJob: MRunnerJob
|
runnerJob: MRunnerJob
|
||||||
updatePayload?: LiveRTMPHLSTranscodingUpdatePayload
|
|
||||||
}) {
|
}) {
|
||||||
// empty
|
// empty
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
export * from './abstract-job-handler'
|
export * from './abstract-job-handler'
|
||||||
export * from './live-rtmp-hls-transcoding-job-handler'
|
export * from './live-rtmp-hls-transcoding-job-handler'
|
||||||
|
export * from './runner-job-handlers'
|
||||||
|
export * from './video-edition-transcoding-job-handler'
|
||||||
export * from './vod-audio-merge-transcoding-job-handler'
|
export * from './vod-audio-merge-transcoding-job-handler'
|
||||||
export * from './vod-hls-transcoding-job-handler'
|
export * from './vod-hls-transcoding-job-handler'
|
||||||
export * from './vod-web-video-transcoding-job-handler'
|
export * from './vod-web-video-transcoding-job-handler'
|
||||||
export * from './runner-job-handlers'
|
|
||||||
|
|
|
@ -70,7 +70,7 @@ export class LiveRTMPHLSTranscodingJobHandler extends AbstractJobHandler<CreateO
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async specificUpdate (options: {
|
protected async specificUpdate (options: {
|
||||||
runnerJob: MRunnerJob
|
runnerJob: MRunnerJob
|
||||||
updatePayload: LiveRTMPHLSTranscodingUpdatePayload
|
updatePayload: LiveRTMPHLSTranscodingUpdatePayload
|
||||||
}) {
|
}) {
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { MRunnerJob } from '@server/types/models/runners'
|
||||||
import { RunnerJobSuccessPayload, RunnerJobType, RunnerJobUpdatePayload } from '@shared/models'
|
import { RunnerJobSuccessPayload, RunnerJobType, RunnerJobUpdatePayload } from '@shared/models'
|
||||||
import { AbstractJobHandler } from './abstract-job-handler'
|
import { AbstractJobHandler } from './abstract-job-handler'
|
||||||
import { LiveRTMPHLSTranscodingJobHandler } from './live-rtmp-hls-transcoding-job-handler'
|
import { LiveRTMPHLSTranscodingJobHandler } from './live-rtmp-hls-transcoding-job-handler'
|
||||||
|
import { VideoEditionTranscodingJobHandler } from './video-edition-transcoding-job-handler'
|
||||||
import { VODAudioMergeTranscodingJobHandler } from './vod-audio-merge-transcoding-job-handler'
|
import { VODAudioMergeTranscodingJobHandler } from './vod-audio-merge-transcoding-job-handler'
|
||||||
import { VODHLSTranscodingJobHandler } from './vod-hls-transcoding-job-handler'
|
import { VODHLSTranscodingJobHandler } from './vod-hls-transcoding-job-handler'
|
||||||
import { VODWebVideoTranscodingJobHandler } from './vod-web-video-transcoding-job-handler'
|
import { VODWebVideoTranscodingJobHandler } from './vod-web-video-transcoding-job-handler'
|
||||||
|
@ -10,7 +11,8 @@ const processors: Record<RunnerJobType, new() => AbstractJobHandler<unknown, Run
|
||||||
'vod-web-video-transcoding': VODWebVideoTranscodingJobHandler,
|
'vod-web-video-transcoding': VODWebVideoTranscodingJobHandler,
|
||||||
'vod-hls-transcoding': VODHLSTranscodingJobHandler,
|
'vod-hls-transcoding': VODHLSTranscodingJobHandler,
|
||||||
'vod-audio-merge-transcoding': VODAudioMergeTranscodingJobHandler,
|
'vod-audio-merge-transcoding': VODAudioMergeTranscodingJobHandler,
|
||||||
'live-rtmp-hls-transcoding': LiveRTMPHLSTranscodingJobHandler
|
'live-rtmp-hls-transcoding': LiveRTMPHLSTranscodingJobHandler,
|
||||||
|
'video-edition-transcoding': VideoEditionTranscodingJobHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRunnerJobHandlerClass (job: MRunnerJob) {
|
export function getRunnerJobHandlerClass (job: MRunnerJob) {
|
||||||
|
|
|
@ -0,0 +1,157 @@
|
||||||
|
|
||||||
|
import { basename } from 'path'
|
||||||
|
import { logger } from '@server/helpers/logger'
|
||||||
|
import { onVideoEditionEnded, safeCleanupStudioTMPFiles } from '@server/lib/video-studio'
|
||||||
|
import { MVideo } from '@server/types/models'
|
||||||
|
import { MRunnerJob } from '@server/types/models/runners'
|
||||||
|
import { buildUUID } from '@shared/extra-utils'
|
||||||
|
import {
|
||||||
|
isVideoStudioTaskIntro,
|
||||||
|
isVideoStudioTaskOutro,
|
||||||
|
isVideoStudioTaskWatermark,
|
||||||
|
RunnerJobState,
|
||||||
|
RunnerJobUpdatePayload,
|
||||||
|
RunnerJobVideoEditionTranscodingPayload,
|
||||||
|
RunnerJobVideoEditionTranscodingPrivatePayload,
|
||||||
|
VideoEditionTranscodingSuccess,
|
||||||
|
VideoState,
|
||||||
|
VideoStudioTaskPayload
|
||||||
|
} from '@shared/models'
|
||||||
|
import { generateRunnerEditionTranscodingVideoInputFileUrl, generateRunnerTranscodingVideoInputFileUrl } from '../runner-urls'
|
||||||
|
import { AbstractJobHandler } from './abstract-job-handler'
|
||||||
|
import { loadTranscodingRunnerVideo } from './shared'
|
||||||
|
|
||||||
|
type CreateOptions = {
|
||||||
|
video: MVideo
|
||||||
|
tasks: VideoStudioTaskPayload[]
|
||||||
|
priority: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
export class VideoEditionTranscodingJobHandler extends AbstractJobHandler<CreateOptions, RunnerJobUpdatePayload, VideoEditionTranscodingSuccess> {
|
||||||
|
|
||||||
|
async create (options: CreateOptions) {
|
||||||
|
const { video, priority, tasks } = options
|
||||||
|
|
||||||
|
const jobUUID = buildUUID()
|
||||||
|
const payload: RunnerJobVideoEditionTranscodingPayload = {
|
||||||
|
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: RunnerJobVideoEditionTranscodingPrivatePayload = {
|
||||||
|
videoUUID: video.uuid,
|
||||||
|
originalTasks: tasks
|
||||||
|
}
|
||||||
|
|
||||||
|
const job = await this.createRunnerJob({
|
||||||
|
type: 'video-edition-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: VideoEditionTranscodingSuccess
|
||||||
|
}) {
|
||||||
|
const { runnerJob, resultPayload } = options
|
||||||
|
const privatePayload = runnerJob.privatePayload as RunnerJobVideoEditionTranscodingPrivatePayload
|
||||||
|
|
||||||
|
const video = await loadTranscodingRunnerVideo(runnerJob, this.lTags)
|
||||||
|
if (!video) {
|
||||||
|
await safeCleanupStudioTMPFiles(privatePayload.originalTasks)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const videoFilePath = resultPayload.videoFile as string
|
||||||
|
|
||||||
|
await onVideoEditionEnded({ 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: RunnerJobState
|
||||||
|
}) {
|
||||||
|
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 RunnerJobVideoEditionTranscodingPrivatePayload
|
||||||
|
await safeCleanupStudioTMPFiles(payload.originalTasks)
|
||||||
|
|
||||||
|
const video = await loadTranscodingRunnerVideo(options.runnerJob, this.lTags)
|
||||||
|
if (!video) return
|
||||||
|
|
||||||
|
return video.setNewState(VideoState.PUBLISHED, false, undefined)
|
||||||
|
}
|
||||||
|
}
|
|
@ -64,7 +64,7 @@ export class VODAudioMergeTranscodingJobHandler extends AbstractVODTranscodingJo
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async specificComplete (options: {
|
protected async specificComplete (options: {
|
||||||
runnerJob: MRunnerJob
|
runnerJob: MRunnerJob
|
||||||
resultPayload: VODAudioMergeTranscodingSuccess
|
resultPayload: VODAudioMergeTranscodingSuccess
|
||||||
}) {
|
}) {
|
||||||
|
|
|
@ -71,7 +71,7 @@ export class VODHLSTranscodingJobHandler extends AbstractVODTranscodingJobHandle
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async specificComplete (options: {
|
protected async specificComplete (options: {
|
||||||
runnerJob: MRunnerJob
|
runnerJob: MRunnerJob
|
||||||
resultPayload: VODHLSTranscodingSuccess
|
resultPayload: VODHLSTranscodingSuccess
|
||||||
}) {
|
}) {
|
||||||
|
|
|
@ -62,7 +62,7 @@ export class VODWebVideoTranscodingJobHandler extends AbstractVODTranscodingJobH
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async specificComplete (options: {
|
protected async specificComplete (options: {
|
||||||
runnerJob: MRunnerJob
|
runnerJob: MRunnerJob
|
||||||
resultPayload: VODWebVideoTranscodingSuccess
|
resultPayload: VODWebVideoTranscodingSuccess
|
||||||
}) {
|
}) {
|
||||||
|
|
|
@ -7,3 +7,7 @@ export function generateRunnerTranscodingVideoInputFileUrl (jobUUID: string, vid
|
||||||
export function generateRunnerTranscodingVideoPreviewFileUrl (jobUUID: string, videoUUID: string) {
|
export function generateRunnerTranscodingVideoPreviewFileUrl (jobUUID: string, videoUUID: string) {
|
||||||
return WEBSERVER.URL + '/api/v1/runners/jobs/' + jobUUID + '/files/videos/' + videoUUID + '/previews/max-quality'
|
return WEBSERVER.URL + '/api/v1/runners/jobs/' + jobUUID + '/files/videos/' + videoUUID + '/previews/max-quality'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function generateRunnerEditionTranscodingVideoInputFileUrl (jobUUID: string, videoUUID: string, filename: string) {
|
||||||
|
return WEBSERVER.URL + '/api/v1/runners/jobs/' + jobUUID + '/files/videos/' + videoUUID + '/studio/task-files/' + filename
|
||||||
|
}
|
||||||
|
|
|
@ -166,7 +166,10 @@ class ServerConfigManager {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
videoStudio: {
|
videoStudio: {
|
||||||
enabled: CONFIG.VIDEO_STUDIO.ENABLED
|
enabled: CONFIG.VIDEO_STUDIO.ENABLED,
|
||||||
|
remoteRunners: {
|
||||||
|
enabled: CONFIG.VIDEO_STUDIO.REMOTE_RUNNERS.ENABLED
|
||||||
|
}
|
||||||
},
|
},
|
||||||
import: {
|
import: {
|
||||||
videos: {
|
videos: {
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
|
|
||||||
import { JOB_PRIORITY } from '@server/initializers/constants'
|
|
||||||
import { VideoModel } from '@server/models/video/video'
|
|
||||||
import { MUserId, MVideoFile, MVideoFullLight } from '@server/types/models'
|
import { MUserId, MVideoFile, MVideoFullLight } from '@server/types/models'
|
||||||
|
|
||||||
export abstract class AbstractJobBuilder {
|
export abstract class AbstractJobBuilder {
|
||||||
|
@ -20,20 +18,4 @@ export abstract class AbstractJobBuilder {
|
||||||
isNewVideo: boolean
|
isNewVideo: boolean
|
||||||
user: MUserId | null
|
user: MUserId | null
|
||||||
}): Promise<any>
|
}): Promise<any>
|
||||||
|
|
||||||
protected async getTranscodingJobPriority (options: {
|
|
||||||
user: MUserId
|
|
||||||
fallback: number
|
|
||||||
}) {
|
|
||||||
const { user, fallback } = options
|
|
||||||
|
|
||||||
if (!user) return fallback
|
|
||||||
|
|
||||||
const now = new Date()
|
|
||||||
const lastWeek = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 7)
|
|
||||||
|
|
||||||
const videoUploadedByUser = await VideoModel.countVideosUploadedByUserSince(user.id, lastWeek)
|
|
||||||
|
|
||||||
return JOB_PRIORITY.TRANSCODING + videoUploadedByUser
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ import {
|
||||||
OptimizeTranscodingPayload,
|
OptimizeTranscodingPayload,
|
||||||
VideoTranscodingPayload
|
VideoTranscodingPayload
|
||||||
} from '@shared/models'
|
} from '@shared/models'
|
||||||
|
import { getTranscodingJobPriority } from '../../transcoding-priority'
|
||||||
import { canDoQuickTranscode } from '../../transcoding-quick-transcode'
|
import { canDoQuickTranscode } from '../../transcoding-quick-transcode'
|
||||||
import { computeResolutionsToTranscode } from '../../transcoding-resolutions'
|
import { computeResolutionsToTranscode } from '../../transcoding-resolutions'
|
||||||
import { AbstractJobBuilder } from './abstract-job-builder'
|
import { AbstractJobBuilder } from './abstract-job-builder'
|
||||||
|
@ -178,7 +179,7 @@ export class TranscodingJobQueueBuilder extends AbstractJobBuilder {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'video-transcoding' as 'video-transcoding',
|
type: 'video-transcoding' as 'video-transcoding',
|
||||||
priority: await this.getTranscodingJobPriority({ user, fallback: undefined }),
|
priority: await getTranscodingJobPriority({ user, type: 'vod', fallback: undefined }),
|
||||||
payload
|
payload
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { VideoPathManager } from '@server/lib/video-path-manager'
|
||||||
import { MUserId, MVideoFile, MVideoFullLight, MVideoWithFileThumbnail } from '@server/types/models'
|
import { MUserId, MVideoFile, MVideoFullLight, MVideoWithFileThumbnail } from '@server/types/models'
|
||||||
import { MRunnerJob } from '@server/types/models/runners'
|
import { MRunnerJob } from '@server/types/models/runners'
|
||||||
import { ffprobePromise, getVideoStreamDimensionsInfo, getVideoStreamFPS, hasAudioStream, isAudioFile } from '@shared/ffmpeg'
|
import { ffprobePromise, getVideoStreamDimensionsInfo, getVideoStreamFPS, hasAudioStream, isAudioFile } from '@shared/ffmpeg'
|
||||||
|
import { getTranscodingJobPriority } from '../../transcoding-priority'
|
||||||
import { computeResolutionsToTranscode } from '../../transcoding-resolutions'
|
import { computeResolutionsToTranscode } from '../../transcoding-resolutions'
|
||||||
import { AbstractJobBuilder } from './abstract-job-builder'
|
import { AbstractJobBuilder } from './abstract-job-builder'
|
||||||
|
|
||||||
|
@ -49,7 +50,7 @@ export class TranscodingRunnerJobBuilder extends AbstractJobBuilder {
|
||||||
: resolution
|
: resolution
|
||||||
|
|
||||||
const fps = computeOutputFPS({ inputFPS, resolution: maxResolution })
|
const fps = computeOutputFPS({ inputFPS, resolution: maxResolution })
|
||||||
const priority = await this.getTranscodingJobPriority({ user, fallback: 0 })
|
const priority = await getTranscodingJobPriority({ user, type: 'vod', fallback: 0 })
|
||||||
|
|
||||||
const mainRunnerJob = videoFile.isAudio()
|
const mainRunnerJob = videoFile.isAudio()
|
||||||
? await new VODAudioMergeTranscodingJobHandler().create({ video, resolution: maxResolution, fps, isNewVideo, priority })
|
? await new VODAudioMergeTranscodingJobHandler().create({ video, resolution: maxResolution, fps, isNewVideo, priority })
|
||||||
|
@ -63,7 +64,7 @@ export class TranscodingRunnerJobBuilder extends AbstractJobBuilder {
|
||||||
fps,
|
fps,
|
||||||
isNewVideo,
|
isNewVideo,
|
||||||
dependsOnRunnerJob: mainRunnerJob,
|
dependsOnRunnerJob: mainRunnerJob,
|
||||||
priority: await this.getTranscodingJobPriority({ user, fallback: 0 })
|
priority: await getTranscodingJobPriority({ user, type: 'vod', fallback: 0 })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,7 +97,7 @@ export class TranscodingRunnerJobBuilder extends AbstractJobBuilder {
|
||||||
const maxResolution = Math.max(...resolutions)
|
const maxResolution = Math.max(...resolutions)
|
||||||
const { fps: inputFPS } = await video.probeMaxQualityFile()
|
const { fps: inputFPS } = await video.probeMaxQualityFile()
|
||||||
const maxFPS = computeOutputFPS({ inputFPS, resolution: maxResolution })
|
const maxFPS = computeOutputFPS({ inputFPS, resolution: maxResolution })
|
||||||
const priority = await this.getTranscodingJobPriority({ user, fallback: 0 })
|
const priority = await getTranscodingJobPriority({ user, type: 'vod', fallback: 0 })
|
||||||
|
|
||||||
const childrenResolutions = resolutions.filter(r => r !== maxResolution)
|
const childrenResolutions = resolutions.filter(r => r !== maxResolution)
|
||||||
|
|
||||||
|
@ -121,7 +122,7 @@ export class TranscodingRunnerJobBuilder extends AbstractJobBuilder {
|
||||||
isNewVideo,
|
isNewVideo,
|
||||||
deleteWebVideoFiles: false,
|
deleteWebVideoFiles: false,
|
||||||
dependsOnRunnerJob,
|
dependsOnRunnerJob,
|
||||||
priority: await this.getTranscodingJobPriority({ user, fallback: 0 })
|
priority: await getTranscodingJobPriority({ user, type: 'vod', fallback: 0 })
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -133,7 +134,7 @@ export class TranscodingRunnerJobBuilder extends AbstractJobBuilder {
|
||||||
fps,
|
fps,
|
||||||
isNewVideo,
|
isNewVideo,
|
||||||
dependsOnRunnerJob,
|
dependsOnRunnerJob,
|
||||||
priority: await this.getTranscodingJobPriority({ user, fallback: 0 })
|
priority: await getTranscodingJobPriority({ user, type: 'vod', fallback: 0 })
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -172,7 +173,7 @@ export class TranscodingRunnerJobBuilder extends AbstractJobBuilder {
|
||||||
fps,
|
fps,
|
||||||
isNewVideo,
|
isNewVideo,
|
||||||
dependsOnRunnerJob: mainRunnerJob,
|
dependsOnRunnerJob: mainRunnerJob,
|
||||||
priority: await this.getTranscodingJobPriority({ user, fallback: 0 })
|
priority: await getTranscodingJobPriority({ user, type: 'vod', fallback: 0 })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -184,7 +185,7 @@ export class TranscodingRunnerJobBuilder extends AbstractJobBuilder {
|
||||||
isNewVideo,
|
isNewVideo,
|
||||||
deleteWebVideoFiles: false,
|
deleteWebVideoFiles: false,
|
||||||
dependsOnRunnerJob: mainRunnerJob,
|
dependsOnRunnerJob: mainRunnerJob,
|
||||||
priority: await this.getTranscodingJobPriority({ user, fallback: 0 })
|
priority: await getTranscodingJobPriority({ user, type: 'vod', fallback: 0 })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { JOB_PRIORITY } from '@server/initializers/constants'
|
||||||
|
import { VideoModel } from '@server/models/video/video'
|
||||||
|
import { MUserId } from '@server/types/models'
|
||||||
|
|
||||||
|
export async function getTranscodingJobPriority (options: {
|
||||||
|
user: MUserId
|
||||||
|
fallback: number
|
||||||
|
type: 'vod' | 'studio'
|
||||||
|
}) {
|
||||||
|
const { user, fallback, type } = options
|
||||||
|
|
||||||
|
if (!user) return fallback
|
||||||
|
|
||||||
|
const now = new Date()
|
||||||
|
const lastWeek = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 7)
|
||||||
|
|
||||||
|
const videoUploadedByUser = await VideoModel.countVideosUploadedByUserSince(user.id, lastWeek)
|
||||||
|
|
||||||
|
const base = type === 'vod'
|
||||||
|
? JOB_PRIORITY.TRANSCODING
|
||||||
|
: JOB_PRIORITY.VIDEO_STUDIO
|
||||||
|
|
||||||
|
return base + videoUploadedByUser
|
||||||
|
}
|
|
@ -1,19 +1,38 @@
|
||||||
import { logger } from '@server/helpers/logger'
|
import { move, remove } from 'fs-extra'
|
||||||
import { MVideoFullLight } from '@server/types/models'
|
import { join } from 'path'
|
||||||
|
import { logger, loggerTagsFactory } from '@server/helpers/logger'
|
||||||
|
import { createTorrentAndSetInfoHashFromPath } from '@server/helpers/webtorrent'
|
||||||
|
import { CONFIG } from '@server/initializers/config'
|
||||||
|
import { UserModel } from '@server/models/user/user'
|
||||||
|
import { MUser, MVideo, MVideoFile, MVideoFullLight, MVideoWithAllFiles } from '@server/types/models'
|
||||||
import { getVideoStreamDuration } from '@shared/ffmpeg'
|
import { getVideoStreamDuration } from '@shared/ffmpeg'
|
||||||
import { VideoStudioEditionPayload, VideoStudioTask } from '@shared/models'
|
import { VideoStudioEditionPayload, VideoStudioTask, VideoStudioTaskPayload } from '@shared/models'
|
||||||
import { remove } from 'fs-extra'
|
import { federateVideoIfNeeded } from './activitypub/videos'
|
||||||
|
import { JobQueue } from './job-queue'
|
||||||
|
import { VideoEditionTranscodingJobHandler } from './runners'
|
||||||
|
import { createOptimizeOrMergeAudioJobs } from './transcoding/create-transcoding-job'
|
||||||
|
import { getTranscodingJobPriority } from './transcoding/transcoding-priority'
|
||||||
|
import { buildNewFile, removeHLSPlaylist, removeWebTorrentFile } from './video-file'
|
||||||
|
import { VideoPathManager } from './video-path-manager'
|
||||||
|
|
||||||
function buildTaskFileFieldname (indice: number, fieldName = 'file') {
|
const lTags = loggerTagsFactory('video-edition')
|
||||||
|
|
||||||
|
export function buildTaskFileFieldname (indice: number, fieldName = 'file') {
|
||||||
return `tasks[${indice}][options][${fieldName}]`
|
return `tasks[${indice}][options][${fieldName}]`
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTaskFileFromReq (files: Express.Multer.File[], indice: number, fieldName = 'file') {
|
export function getTaskFileFromReq (files: Express.Multer.File[], indice: number, fieldName = 'file') {
|
||||||
return files.find(f => f.fieldname === buildTaskFileFieldname(indice, fieldName))
|
return files.find(f => f.fieldname === buildTaskFileFieldname(indice, fieldName))
|
||||||
}
|
}
|
||||||
|
|
||||||
async function safeCleanupStudioTMPFiles (payload: VideoStudioEditionPayload) {
|
export function getStudioTaskFilePath (filename: string) {
|
||||||
for (const task of payload.tasks) {
|
return join(CONFIG.STORAGE.TMP_PERSISTENT_DIR, filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function safeCleanupStudioTMPFiles (tasks: VideoStudioTaskPayload[]) {
|
||||||
|
logger.info('Removing studio task files', { tasks, ...lTags() })
|
||||||
|
|
||||||
|
for (const task of tasks) {
|
||||||
try {
|
try {
|
||||||
if (task.name === 'add-intro' || task.name === 'add-outro') {
|
if (task.name === 'add-intro' || task.name === 'add-outro') {
|
||||||
await remove(task.options.file)
|
await remove(task.options.file)
|
||||||
|
@ -26,7 +45,13 @@ async function safeCleanupStudioTMPFiles (payload: VideoStudioEditionPayload) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function approximateIntroOutroAdditionalSize (video: MVideoFullLight, tasks: VideoStudioTask[], fileFinder: (i: number) => string) {
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export async function approximateIntroOutroAdditionalSize (
|
||||||
|
video: MVideoFullLight,
|
||||||
|
tasks: VideoStudioTask[],
|
||||||
|
fileFinder: (i: number) => string
|
||||||
|
) {
|
||||||
let additionalDuration = 0
|
let additionalDuration = 0
|
||||||
|
|
||||||
for (let i = 0; i < tasks.length; i++) {
|
for (let i = 0; i < tasks.length; i++) {
|
||||||
|
@ -41,9 +66,65 @@ async function approximateIntroOutroAdditionalSize (video: MVideoFullLight, task
|
||||||
return (video.getMaxQualityFile().size / video.duration) * additionalDuration
|
return (video.getMaxQualityFile().size / video.duration) * additionalDuration
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
// ---------------------------------------------------------------------------
|
||||||
approximateIntroOutroAdditionalSize,
|
|
||||||
buildTaskFileFieldname,
|
export async function createVideoStudioJob (options: {
|
||||||
getTaskFileFromReq,
|
video: MVideo
|
||||||
safeCleanupStudioTMPFiles
|
user: MUser
|
||||||
|
payload: VideoStudioEditionPayload
|
||||||
|
}) {
|
||||||
|
const { video, user, payload } = options
|
||||||
|
|
||||||
|
const priority = await getTranscodingJobPriority({ user, type: 'studio', fallback: 0 })
|
||||||
|
|
||||||
|
if (CONFIG.VIDEO_STUDIO.REMOTE_RUNNERS.ENABLED) {
|
||||||
|
await new VideoEditionTranscodingJobHandler().create({ video, tasks: payload.tasks, priority })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await JobQueue.Instance.createJob({ type: 'video-studio-edition', payload, priority })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function onVideoEditionEnded (options: {
|
||||||
|
editionResultPath: string
|
||||||
|
tasks: VideoStudioTaskPayload[]
|
||||||
|
video: MVideoFullLight
|
||||||
|
}) {
|
||||||
|
const { video, tasks, editionResultPath } = options
|
||||||
|
|
||||||
|
const newFile = await buildNewFile({ path: editionResultPath, mode: 'web-video' })
|
||||||
|
newFile.videoId = video.id
|
||||||
|
|
||||||
|
const outputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, newFile)
|
||||||
|
await move(editionResultPath, outputPath)
|
||||||
|
|
||||||
|
await safeCleanupStudioTMPFiles(tasks)
|
||||||
|
|
||||||
|
await createTorrentAndSetInfoHashFromPath(video, newFile, outputPath)
|
||||||
|
await removeAllFiles(video, newFile)
|
||||||
|
|
||||||
|
await newFile.save()
|
||||||
|
|
||||||
|
video.duration = await getVideoStreamDuration(outputPath)
|
||||||
|
await video.save()
|
||||||
|
|
||||||
|
await federateVideoIfNeeded(video, false, undefined)
|
||||||
|
|
||||||
|
const user = await UserModel.loadByVideoId(video.id)
|
||||||
|
|
||||||
|
await createOptimizeOrMergeAudioJobs({ video, videoFile: newFile, isNewVideo: false, user, videoFileAlreadyLocked: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Private
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async function removeAllFiles (video: MVideoWithAllFiles, webTorrentFileException: MVideoFile) {
|
||||||
|
await removeHLSPlaylist(video)
|
||||||
|
|
||||||
|
for (const file of video.VideoFiles) {
|
||||||
|
if (file.id === webTorrentFileException.id) continue
|
||||||
|
|
||||||
|
await removeWebTorrentFile(video, file.id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,6 +62,7 @@ const customConfigUpdateValidator = [
|
||||||
body('transcoding.hls.enabled').isBoolean(),
|
body('transcoding.hls.enabled').isBoolean(),
|
||||||
|
|
||||||
body('videoStudio.enabled').isBoolean(),
|
body('videoStudio.enabled').isBoolean(),
|
||||||
|
body('videoStudio.remoteRunners.enabled').isBoolean(),
|
||||||
|
|
||||||
body('import.videos.concurrency').isInt({ min: 0 }),
|
body('import.videos.concurrency').isInt({ min: 0 }),
|
||||||
body('import.videos.http.enabled').isBoolean(),
|
body('import.videos.http.enabled').isBoolean(),
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { HttpStatusCode } from '@shared/models'
|
import { param } from 'express-validator'
|
||||||
|
import { basename } from 'path'
|
||||||
|
import { isSafeFilename } from '@server/helpers/custom-validators/misc'
|
||||||
|
import { hasVideoStudioTaskFile, HttpStatusCode, RunnerJobVideoEditionTranscodingPayload } from '@shared/models'
|
||||||
import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared'
|
import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared'
|
||||||
|
|
||||||
const tags = [ 'runner' ]
|
const tags = [ 'runner' ]
|
||||||
|
@ -25,3 +28,33 @@ export const runnerJobGetVideoTranscodingFileValidator = [
|
||||||
return next()
|
return next()
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export const runnerJobGetVideoStudioTaskFileValidator = [
|
||||||
|
param('filename').custom(v => isSafeFilename(v)),
|
||||||
|
|
||||||
|
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
|
if (areValidationErrors(req, res)) return
|
||||||
|
|
||||||
|
const filename = req.params.filename
|
||||||
|
|
||||||
|
const payload = res.locals.runnerJob.payload as RunnerJobVideoEditionTranscodingPayload
|
||||||
|
|
||||||
|
const found = Array.isArray(payload?.tasks) && payload.tasks.some(t => {
|
||||||
|
if (hasVideoStudioTaskFile(t)) {
|
||||||
|
return basename(t.options.file) === filename
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
return res.fail({
|
||||||
|
status: HttpStatusCode.BAD_REQUEST_400,
|
||||||
|
message: 'File is not associated to this edition task',
|
||||||
|
tags: [ ...tags, res.locals.videoAll.uuid ]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
|
@ -91,6 +91,28 @@ export const successRunnerJobValidator = [
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export const cancelRunnerJobValidator = [
|
||||||
|
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
|
const runnerJob = res.locals.runnerJob
|
||||||
|
|
||||||
|
const allowedStates = new Set<RunnerJobState>([
|
||||||
|
RunnerJobState.PENDING,
|
||||||
|
RunnerJobState.PROCESSING,
|
||||||
|
RunnerJobState.WAITING_FOR_PARENT_JOB
|
||||||
|
])
|
||||||
|
|
||||||
|
if (allowedStates.has(runnerJob.state) !== true) {
|
||||||
|
return res.fail({
|
||||||
|
status: HttpStatusCode.BAD_REQUEST_400,
|
||||||
|
message: 'Cannot cancel this job that is not in "pending", "processing" or "waiting for parent job" state',
|
||||||
|
tags
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
export const runnerJobGetValidator = [
|
export const runnerJobGetValidator = [
|
||||||
param('jobUUID').custom(isUUIDValid),
|
param('jobUUID').custom(isUUIDValid),
|
||||||
|
|
||||||
|
|
|
@ -162,7 +162,10 @@ describe('Test config API validators', function () {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
videoStudio: {
|
videoStudio: {
|
||||||
|
enabled: true,
|
||||||
|
remoteRunners: {
|
||||||
enabled: true
|
enabled: true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
import: {
|
import: {
|
||||||
videos: {
|
videos: {
|
||||||
|
|
|
@ -1,6 +1,17 @@
|
||||||
|
import { basename } from 'path'
|
||||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||||
import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@server/tests/shared'
|
import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@server/tests/shared'
|
||||||
import { HttpStatusCode, RunnerJob, RunnerJobState, RunnerJobSuccessPayload, RunnerJobUpdatePayload, VideoPrivacy } from '@shared/models'
|
import {
|
||||||
|
HttpStatusCode,
|
||||||
|
isVideoStudioTaskIntro,
|
||||||
|
RunnerJob,
|
||||||
|
RunnerJobState,
|
||||||
|
RunnerJobSuccessPayload,
|
||||||
|
RunnerJobUpdatePayload,
|
||||||
|
RunnerJobVideoEditionTranscodingPayload,
|
||||||
|
VideoPrivacy,
|
||||||
|
VideoStudioTaskIntro
|
||||||
|
} from '@shared/models'
|
||||||
import {
|
import {
|
||||||
cleanupTests,
|
cleanupTests,
|
||||||
createSingleServer,
|
createSingleServer,
|
||||||
|
@ -10,6 +21,7 @@ import {
|
||||||
setAccessTokensToServers,
|
setAccessTokensToServers,
|
||||||
setDefaultVideoChannel,
|
setDefaultVideoChannel,
|
||||||
stopFfmpeg,
|
stopFfmpeg,
|
||||||
|
VideoStudioCommand,
|
||||||
waitJobs
|
waitJobs
|
||||||
} from '@shared/server-commands'
|
} from '@shared/server-commands'
|
||||||
|
|
||||||
|
@ -53,7 +65,10 @@ describe('Test managing runners', function () {
|
||||||
registrationTokenId = data[0].id
|
registrationTokenId = data[0].id
|
||||||
|
|
||||||
await server.config.enableTranscoding(true, true)
|
await server.config.enableTranscoding(true, true)
|
||||||
|
await server.config.enableStudio()
|
||||||
await server.config.enableRemoteTranscoding()
|
await server.config.enableRemoteTranscoding()
|
||||||
|
await server.config.enableRemoteStudio()
|
||||||
|
|
||||||
runnerToken = await server.runners.autoRegisterRunner()
|
runnerToken = await server.runners.autoRegisterRunner()
|
||||||
runnerToken2 = await server.runners.autoRegisterRunner()
|
runnerToken2 = await server.runners.autoRegisterRunner()
|
||||||
|
|
||||||
|
@ -249,6 +264,10 @@ describe('Test managing runners', function () {
|
||||||
await server.runnerJobs.cancelByAdmin({ jobUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
await server.runnerJobs.cancelByAdmin({ jobUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('Should fail with an already cancelled job', async function () {
|
||||||
|
await server.runnerJobs.cancelByAdmin({ jobUUID: cancelledJobUUID, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||||
|
})
|
||||||
|
|
||||||
it('Should succeed with the correct params', async function () {
|
it('Should succeed with the correct params', async function () {
|
||||||
await server.runnerJobs.cancelByAdmin({ jobUUID })
|
await server.runnerJobs.cancelByAdmin({ jobUUID })
|
||||||
})
|
})
|
||||||
|
@ -296,9 +315,13 @@ describe('Test managing runners', function () {
|
||||||
|
|
||||||
let pendingUUID: string
|
let pendingUUID: string
|
||||||
|
|
||||||
let liveAcceptedJob: RunnerJob & { jobToken: string }
|
let videoStudioUUID: string
|
||||||
|
let studioFile: string
|
||||||
|
|
||||||
async function fetchFiles (options: {
|
let liveAcceptedJob: RunnerJob & { jobToken: string }
|
||||||
|
let studioAcceptedJob: RunnerJob & { jobToken: string }
|
||||||
|
|
||||||
|
async function fetchVideoInputFiles (options: {
|
||||||
jobUUID: string
|
jobUUID: string
|
||||||
videoUUID: string
|
videoUUID: string
|
||||||
runnerToken: string
|
runnerToken: string
|
||||||
|
@ -315,6 +338,21 @@ describe('Test managing runners', function () {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchStudioFiles (options: {
|
||||||
|
jobUUID: string
|
||||||
|
videoUUID: string
|
||||||
|
runnerToken: string
|
||||||
|
jobToken: string
|
||||||
|
studioFile?: string
|
||||||
|
expectedStatus: HttpStatusCode
|
||||||
|
}) {
|
||||||
|
const { jobUUID, expectedStatus, videoUUID, runnerToken, jobToken, studioFile } = options
|
||||||
|
|
||||||
|
const path = `/api/v1/runners/jobs/${jobUUID}/files/videos/${videoUUID}/studio/task-files/${studioFile}`
|
||||||
|
|
||||||
|
await makePostBodyRequest({ url: server.url, path, fields: { runnerToken, jobToken }, expectedStatus })
|
||||||
|
}
|
||||||
|
|
||||||
before(async function () {
|
before(async function () {
|
||||||
this.timeout(120000)
|
this.timeout(120000)
|
||||||
|
|
||||||
|
@ -352,6 +390,28 @@ describe('Test managing runners', function () {
|
||||||
pendingUUID = availableJobs[0].uuid
|
pendingUUID = availableJobs[0].uuid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
await server.config.disableTranscoding()
|
||||||
|
|
||||||
|
const { uuid } = await server.videos.quickUpload({ name: 'video studio' })
|
||||||
|
videoStudioUUID = uuid
|
||||||
|
|
||||||
|
await server.config.enableTranscoding(true, true)
|
||||||
|
await server.config.enableStudio()
|
||||||
|
|
||||||
|
await server.videoStudio.createEditionTasks({
|
||||||
|
videoId: videoStudioUUID,
|
||||||
|
tasks: VideoStudioCommand.getComplexTask()
|
||||||
|
})
|
||||||
|
|
||||||
|
const { job } = await server.runnerJobs.autoAccept({ runnerToken, type: 'video-edition-transcoding' })
|
||||||
|
studioAcceptedJob = job
|
||||||
|
|
||||||
|
const tasks = (job.payload as RunnerJobVideoEditionTranscodingPayload).tasks
|
||||||
|
const fileUrl = (tasks.find(t => isVideoStudioTaskIntro(t)) as VideoStudioTaskIntro).options.file as string
|
||||||
|
studioFile = basename(fileUrl)
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
await server.config.enableLive({
|
await server.config.enableLive({
|
||||||
allowReplay: false,
|
allowReplay: false,
|
||||||
|
@ -381,8 +441,6 @@ describe('Test managing runners', function () {
|
||||||
jobToken: string
|
jobToken: string
|
||||||
expectedStatus: HttpStatusCode
|
expectedStatus: HttpStatusCode
|
||||||
}) {
|
}) {
|
||||||
await fetchFiles({ ...options, videoUUID })
|
|
||||||
|
|
||||||
await server.runnerJobs.abort({ ...options, reason: 'reason' })
|
await server.runnerJobs.abort({ ...options, reason: 'reason' })
|
||||||
await server.runnerJobs.update({ ...options })
|
await server.runnerJobs.update({ ...options })
|
||||||
await server.runnerJobs.error({ ...options, message: 'message' })
|
await server.runnerJobs.error({ ...options, message: 'message' })
|
||||||
|
@ -390,39 +448,95 @@ describe('Test managing runners', function () {
|
||||||
}
|
}
|
||||||
|
|
||||||
it('Should fail with an invalid job uuid', async function () {
|
it('Should fail with an invalid job uuid', async function () {
|
||||||
await testEndpoints({ jobUUID: 'a', runnerToken, jobToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
const options = { jobUUID: 'a', runnerToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }
|
||||||
|
|
||||||
|
await testEndpoints({ ...options, jobToken })
|
||||||
|
await fetchVideoInputFiles({ ...options, videoUUID, jobToken })
|
||||||
|
await fetchStudioFiles({ ...options, videoUUID, jobToken: studioAcceptedJob.jobToken, studioFile })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should fail with an unknown job uuid', async function () {
|
it('Should fail with an unknown job uuid', async function () {
|
||||||
const jobUUID = badUUID
|
const options = { jobUUID: badUUID, runnerToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 }
|
||||||
await testEndpoints({ jobUUID, runnerToken, jobToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
|
||||||
|
await testEndpoints({ ...options, jobToken })
|
||||||
|
await fetchVideoInputFiles({ ...options, videoUUID, jobToken })
|
||||||
|
await fetchStudioFiles({ ...options, jobToken: studioAcceptedJob.jobToken, videoUUID, studioFile })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should fail with an invalid runner token', async function () {
|
it('Should fail with an invalid runner token', async function () {
|
||||||
await testEndpoints({ jobUUID, runnerToken: '', jobToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
const options = { runnerToken: '', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }
|
||||||
|
|
||||||
|
await testEndpoints({ ...options, jobUUID, jobToken })
|
||||||
|
await fetchVideoInputFiles({ ...options, jobUUID, videoUUID, jobToken })
|
||||||
|
await fetchStudioFiles({
|
||||||
|
...options,
|
||||||
|
jobToken: studioAcceptedJob.jobToken,
|
||||||
|
jobUUID: studioAcceptedJob.uuid,
|
||||||
|
videoUUID: videoStudioUUID,
|
||||||
|
studioFile
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should fail with an unknown runner token', async function () {
|
it('Should fail with an unknown runner token', async function () {
|
||||||
const runnerToken = badUUID
|
const options = { runnerToken: badUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 }
|
||||||
await testEndpoints({ jobUUID, runnerToken, jobToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
|
||||||
|
await testEndpoints({ ...options, jobUUID, jobToken })
|
||||||
|
await fetchVideoInputFiles({ ...options, jobUUID, videoUUID, jobToken })
|
||||||
|
await fetchStudioFiles({
|
||||||
|
...options,
|
||||||
|
jobToken: studioAcceptedJob.jobToken,
|
||||||
|
jobUUID: studioAcceptedJob.uuid,
|
||||||
|
videoUUID: videoStudioUUID,
|
||||||
|
studioFile
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should fail with an invalid job token job uuid', async function () {
|
it('Should fail with an invalid job token job uuid', async function () {
|
||||||
await testEndpoints({ jobUUID, runnerToken, jobToken: '', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
const options = { runnerToken, jobToken: '', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }
|
||||||
|
|
||||||
|
await testEndpoints({ ...options, jobUUID })
|
||||||
|
await fetchVideoInputFiles({ ...options, jobUUID, videoUUID })
|
||||||
|
await fetchStudioFiles({ ...options, jobUUID: studioAcceptedJob.uuid, videoUUID: videoStudioUUID, studioFile })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should fail with an unknown job token job uuid', async function () {
|
it('Should fail with an unknown job token job uuid', async function () {
|
||||||
const jobToken = badUUID
|
const options = { runnerToken, jobToken: badUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 }
|
||||||
await testEndpoints({ jobUUID, runnerToken, jobToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
|
||||||
|
await testEndpoints({ ...options, jobUUID })
|
||||||
|
await fetchVideoInputFiles({ ...options, jobUUID, videoUUID })
|
||||||
|
await fetchStudioFiles({ ...options, jobUUID: studioAcceptedJob.uuid, videoUUID: videoStudioUUID, studioFile })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should fail with a runner token not associated to this job', async function () {
|
it('Should fail with a runner token not associated to this job', async function () {
|
||||||
await testEndpoints({ jobUUID, runnerToken: runnerToken2, jobToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
const options = { runnerToken: runnerToken2, expectedStatus: HttpStatusCode.NOT_FOUND_404 }
|
||||||
|
|
||||||
|
await testEndpoints({ ...options, jobUUID, jobToken })
|
||||||
|
await fetchVideoInputFiles({ ...options, jobUUID, videoUUID, jobToken })
|
||||||
|
await fetchStudioFiles({
|
||||||
|
...options,
|
||||||
|
jobToken: studioAcceptedJob.jobToken,
|
||||||
|
jobUUID: studioAcceptedJob.uuid,
|
||||||
|
videoUUID: videoStudioUUID,
|
||||||
|
studioFile
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should fail with a job uuid not associated to the job token', async function () {
|
it('Should fail with a job uuid not associated to the job token', async function () {
|
||||||
await testEndpoints({ jobUUID: jobUUID2, runnerToken, jobToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
{
|
||||||
await testEndpoints({ jobUUID, runnerToken, jobToken: jobToken2, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
const options = { jobUUID: jobUUID2, runnerToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 }
|
||||||
|
|
||||||
|
await testEndpoints({ ...options, jobToken })
|
||||||
|
await fetchVideoInputFiles({ ...options, jobToken, videoUUID })
|
||||||
|
await fetchStudioFiles({ ...options, jobToken: studioAcceptedJob.jobToken, videoUUID: videoStudioUUID, studioFile })
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const options = { runnerToken, jobToken: jobToken2, expectedStatus: HttpStatusCode.NOT_FOUND_404 }
|
||||||
|
|
||||||
|
await testEndpoints({ ...options, jobUUID })
|
||||||
|
await fetchVideoInputFiles({ ...options, jobUUID, videoUUID })
|
||||||
|
await fetchStudioFiles({ ...options, jobUUID: studioAcceptedJob.uuid, videoUUID: videoStudioUUID, studioFile })
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -670,27 +784,82 @@ describe('Test managing runners', function () {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Video studio', function () {
|
||||||
|
|
||||||
|
it('Should fail with an invalid video edition transcoding payload', async function () {
|
||||||
|
await server.runnerJobs.success({
|
||||||
|
jobUUID: studioAcceptedJob.uuid,
|
||||||
|
jobToken: studioAcceptedJob.jobToken,
|
||||||
|
payload: { hello: 'video_short.mp4' } as any,
|
||||||
|
runnerToken,
|
||||||
|
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Job files', function () {
|
describe('Job files', function () {
|
||||||
|
|
||||||
describe('Video files', function () {
|
describe('Check video param for common job file routes', function () {
|
||||||
|
|
||||||
|
async function fetchFiles (options: {
|
||||||
|
videoUUID?: string
|
||||||
|
expectedStatus: HttpStatusCode
|
||||||
|
}) {
|
||||||
|
await fetchVideoInputFiles({ videoUUID, ...options, jobToken, jobUUID, runnerToken })
|
||||||
|
|
||||||
|
await fetchStudioFiles({
|
||||||
|
videoUUID: videoStudioUUID,
|
||||||
|
|
||||||
|
...options,
|
||||||
|
|
||||||
|
jobToken: studioAcceptedJob.jobToken,
|
||||||
|
jobUUID: studioAcceptedJob.uuid,
|
||||||
|
runnerToken,
|
||||||
|
studioFile
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
it('Should fail with an invalid video id', async function () {
|
it('Should fail with an invalid video id', async function () {
|
||||||
await fetchFiles({ videoUUID: 'a', jobUUID, runnerToken, jobToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
await fetchFiles({
|
||||||
|
videoUUID: 'a',
|
||||||
|
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should fail with an unknown video id', async function () {
|
it('Should fail with an unknown video id', async function () {
|
||||||
const videoUUID = '910ec12a-d9e6-458b-a274-0abb655f9464'
|
const videoUUID = '910ec12a-d9e6-458b-a274-0abb655f9464'
|
||||||
await fetchFiles({ videoUUID, jobUUID, runnerToken, jobToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
|
||||||
|
await fetchFiles({
|
||||||
|
videoUUID,
|
||||||
|
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should fail with a video id not associated to this job', async function () {
|
it('Should fail with a video id not associated to this job', async function () {
|
||||||
await fetchFiles({ videoUUID: videoUUID2, jobUUID, runnerToken, jobToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
await fetchFiles({
|
||||||
|
videoUUID: videoUUID2,
|
||||||
|
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should succeed with the correct params', async function () {
|
it('Should succeed with the correct params', async function () {
|
||||||
await fetchFiles({ videoUUID, jobUUID, runnerToken, jobToken, expectedStatus: HttpStatusCode.OK_200 })
|
await fetchFiles({ expectedStatus: HttpStatusCode.OK_200 })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Video edition tasks file routes', function () {
|
||||||
|
|
||||||
|
it('Should fail with an invalid studio filename', async function () {
|
||||||
|
await fetchStudioFiles({
|
||||||
|
videoUUID: videoStudioUUID,
|
||||||
|
jobUUID: studioAcceptedJob.uuid,
|
||||||
|
runnerToken,
|
||||||
|
jobToken: studioAcceptedJob.jobToken,
|
||||||
|
studioFile: 'toto',
|
||||||
|
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
export * from './runner-common'
|
export * from './runner-common'
|
||||||
export * from './runner-live-transcoding'
|
export * from './runner-live-transcoding'
|
||||||
export * from './runner-socket'
|
export * from './runner-socket'
|
||||||
|
export * from './runner-studio-transcoding'
|
||||||
export * from './runner-vod-transcoding'
|
export * from './runner-vod-transcoding'
|
||||||
|
|
|
@ -2,7 +2,15 @@
|
||||||
|
|
||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import { wait } from '@shared/core-utils'
|
import { wait } from '@shared/core-utils'
|
||||||
import { HttpStatusCode, Runner, RunnerJob, RunnerJobAdmin, RunnerJobState, RunnerRegistrationToken } from '@shared/models'
|
import {
|
||||||
|
HttpStatusCode,
|
||||||
|
Runner,
|
||||||
|
RunnerJob,
|
||||||
|
RunnerJobAdmin,
|
||||||
|
RunnerJobState,
|
||||||
|
RunnerJobVODWebVideoTranscodingPayload,
|
||||||
|
RunnerRegistrationToken
|
||||||
|
} from '@shared/models'
|
||||||
import {
|
import {
|
||||||
cleanupTests,
|
cleanupTests,
|
||||||
createSingleServer,
|
createSingleServer,
|
||||||
|
@ -349,7 +357,7 @@ describe('Test runner common actions', function () {
|
||||||
for (const job of availableJobs) {
|
for (const job of availableJobs) {
|
||||||
expect(job.uuid).to.exist
|
expect(job.uuid).to.exist
|
||||||
expect(job.payload.input).to.exist
|
expect(job.payload.input).to.exist
|
||||||
expect(job.payload.output).to.exist
|
expect((job.payload as RunnerJobVODWebVideoTranscodingPayload).output).to.exist
|
||||||
|
|
||||||
expect((job as RunnerJobAdmin).privatePayload).to.not.exist
|
expect((job as RunnerJobAdmin).privatePayload).to.not.exist
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,168 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||||
|
|
||||||
|
import { expect } from 'chai'
|
||||||
|
import { readFile } from 'fs-extra'
|
||||||
|
import { checkPersistentTmpIsEmpty, checkVideoDuration } from '@server/tests/shared'
|
||||||
|
import { buildAbsoluteFixturePath } from '@shared/core-utils'
|
||||||
|
import {
|
||||||
|
RunnerJobVideoEditionTranscodingPayload,
|
||||||
|
VideoEditionTranscodingSuccess,
|
||||||
|
VideoState,
|
||||||
|
VideoStudioTask,
|
||||||
|
VideoStudioTaskIntro
|
||||||
|
} from '@shared/models'
|
||||||
|
import {
|
||||||
|
cleanupTests,
|
||||||
|
createMultipleServers,
|
||||||
|
doubleFollow,
|
||||||
|
PeerTubeServer,
|
||||||
|
setAccessTokensToServers,
|
||||||
|
setDefaultVideoChannel,
|
||||||
|
VideoStudioCommand,
|
||||||
|
waitJobs
|
||||||
|
} from '@shared/server-commands'
|
||||||
|
|
||||||
|
describe('Test runner video studio transcoding', function () {
|
||||||
|
let servers: PeerTubeServer[] = []
|
||||||
|
let runnerToken: string
|
||||||
|
let videoUUID: string
|
||||||
|
let jobUUID: string
|
||||||
|
|
||||||
|
async function renewStudio (tasks: VideoStudioTask[] = VideoStudioCommand.getComplexTask()) {
|
||||||
|
const { uuid } = await servers[0].videos.quickUpload({ name: 'video' })
|
||||||
|
videoUUID = uuid
|
||||||
|
|
||||||
|
await waitJobs(servers)
|
||||||
|
|
||||||
|
await servers[0].videoStudio.createEditionTasks({ videoId: uuid, tasks })
|
||||||
|
await waitJobs(servers)
|
||||||
|
|
||||||
|
const { availableJobs } = await servers[0].runnerJobs.request({ runnerToken })
|
||||||
|
expect(availableJobs).to.have.lengthOf(1)
|
||||||
|
|
||||||
|
jobUUID = availableJobs[0].uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
this.timeout(120_000)
|
||||||
|
|
||||||
|
servers = await createMultipleServers(2)
|
||||||
|
|
||||||
|
await setAccessTokensToServers(servers)
|
||||||
|
await setDefaultVideoChannel(servers)
|
||||||
|
|
||||||
|
await doubleFollow(servers[0], servers[1])
|
||||||
|
|
||||||
|
await servers[0].config.enableTranscoding(true, true)
|
||||||
|
await servers[0].config.enableStudio()
|
||||||
|
await servers[0].config.enableRemoteStudio()
|
||||||
|
|
||||||
|
runnerToken = await servers[0].runners.autoRegisterRunner()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should error a studio transcoding job', async function () {
|
||||||
|
this.timeout(60000)
|
||||||
|
|
||||||
|
await renewStudio()
|
||||||
|
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
const { job } = await servers[0].runnerJobs.accept({ runnerToken, jobUUID })
|
||||||
|
const jobToken = job.jobToken
|
||||||
|
|
||||||
|
await servers[0].runnerJobs.error({ runnerToken, jobUUID, jobToken, message: 'Error' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const video = await servers[0].videos.get({ id: videoUUID })
|
||||||
|
expect(video.state.id).to.equal(VideoState.PUBLISHED)
|
||||||
|
|
||||||
|
await checkPersistentTmpIsEmpty(servers[0])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should cancel a transcoding job', async function () {
|
||||||
|
this.timeout(60000)
|
||||||
|
|
||||||
|
await renewStudio()
|
||||||
|
|
||||||
|
await servers[0].runnerJobs.cancelByAdmin({ jobUUID })
|
||||||
|
|
||||||
|
const video = await servers[0].videos.get({ id: videoUUID })
|
||||||
|
expect(video.state.id).to.equal(VideoState.PUBLISHED)
|
||||||
|
|
||||||
|
await checkPersistentTmpIsEmpty(servers[0])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should execute a remote studio job', async function () {
|
||||||
|
this.timeout(240_000)
|
||||||
|
|
||||||
|
const tasks = [
|
||||||
|
{
|
||||||
|
name: 'add-outro' as 'add-outro',
|
||||||
|
options: {
|
||||||
|
file: 'video_short.webm'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'add-watermark' as 'add-watermark',
|
||||||
|
options: {
|
||||||
|
file: 'thumbnail.png'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'add-intro' as 'add-intro',
|
||||||
|
options: {
|
||||||
|
file: 'video_very_short_240p.mp4'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
await renewStudio(tasks)
|
||||||
|
|
||||||
|
for (const server of servers) {
|
||||||
|
await checkVideoDuration(server, videoUUID, 5)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { job } = await servers[0].runnerJobs.accept<RunnerJobVideoEditionTranscodingPayload>({ runnerToken, jobUUID })
|
||||||
|
const jobToken = job.jobToken
|
||||||
|
|
||||||
|
expect(job.type === 'video-edition-transcoding')
|
||||||
|
expect(job.payload.input.videoFileUrl).to.exist
|
||||||
|
|
||||||
|
// Check video input file
|
||||||
|
{
|
||||||
|
await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check task files
|
||||||
|
for (let i = 0; i < tasks.length; i++) {
|
||||||
|
const task = tasks[i]
|
||||||
|
const payloadTask = job.payload.tasks[i]
|
||||||
|
|
||||||
|
expect(payloadTask.name).to.equal(task.name)
|
||||||
|
|
||||||
|
const inputFile = await readFile(buildAbsoluteFixturePath(task.options.file))
|
||||||
|
|
||||||
|
const { body } = await servers[0].runnerJobs.getJobFile({
|
||||||
|
url: (payloadTask as VideoStudioTaskIntro).options.file as string,
|
||||||
|
jobToken,
|
||||||
|
runnerToken
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(body).to.deep.equal(inputFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload: VideoEditionTranscodingSuccess = { videoFile: 'video_very_short_240p.mp4' }
|
||||||
|
await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
|
||||||
|
|
||||||
|
await waitJobs(servers)
|
||||||
|
|
||||||
|
for (const server of servers) {
|
||||||
|
await checkVideoDuration(server, videoUUID, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
await checkPersistentTmpIsEmpty(servers[0])
|
||||||
|
})
|
||||||
|
|
||||||
|
after(async function () {
|
||||||
|
await cleanupTests(servers)
|
||||||
|
})
|
||||||
|
})
|
|
@ -155,7 +155,7 @@ describe('Test runner VOD transcoding', function () {
|
||||||
expect(job.payload.output.resolution).to.equal(720)
|
expect(job.payload.output.resolution).to.equal(720)
|
||||||
expect(job.payload.output.fps).to.equal(25)
|
expect(job.payload.output.fps).to.equal(25)
|
||||||
|
|
||||||
const { body } = await servers[0].runnerJobs.getInputFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
|
const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
|
||||||
const inputFile = await readFile(buildAbsoluteFixturePath('video_short.webm'))
|
const inputFile = await readFile(buildAbsoluteFixturePath('video_short.webm'))
|
||||||
|
|
||||||
expect(body).to.deep.equal(inputFile)
|
expect(body).to.deep.equal(inputFile)
|
||||||
|
@ -200,7 +200,7 @@ describe('Test runner VOD transcoding', function () {
|
||||||
const { job } = await servers[0].runnerJobs.accept<RunnerJobVODWebVideoTranscodingPayload>({ runnerToken, jobUUID })
|
const { job } = await servers[0].runnerJobs.accept<RunnerJobVODWebVideoTranscodingPayload>({ runnerToken, jobUUID })
|
||||||
jobToken = job.jobToken
|
jobToken = job.jobToken
|
||||||
|
|
||||||
const { body } = await servers[0].runnerJobs.getInputFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
|
const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
|
||||||
const inputFile = await readFile(buildAbsoluteFixturePath('video_short.mp4'))
|
const inputFile = await readFile(buildAbsoluteFixturePath('video_short.mp4'))
|
||||||
|
|
||||||
expect(body).to.deep.equal(inputFile)
|
expect(body).to.deep.equal(inputFile)
|
||||||
|
@ -221,7 +221,7 @@ describe('Test runner VOD transcoding', function () {
|
||||||
const { job } = await servers[0].runnerJobs.accept<RunnerJobVODWebVideoTranscodingPayload>({ runnerToken, jobUUID })
|
const { job } = await servers[0].runnerJobs.accept<RunnerJobVODWebVideoTranscodingPayload>({ runnerToken, jobUUID })
|
||||||
jobToken = job.jobToken
|
jobToken = job.jobToken
|
||||||
|
|
||||||
const { body } = await servers[0].runnerJobs.getInputFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
|
const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
|
||||||
const inputFile = await readFile(buildAbsoluteFixturePath('video_short.mp4'))
|
const inputFile = await readFile(buildAbsoluteFixturePath('video_short.mp4'))
|
||||||
expect(body).to.deep.equal(inputFile)
|
expect(body).to.deep.equal(inputFile)
|
||||||
|
|
||||||
|
@ -293,7 +293,7 @@ describe('Test runner VOD transcoding', function () {
|
||||||
const { job } = await servers[0].runnerJobs.accept<RunnerJobVODHLSTranscodingPayload>({ runnerToken, jobUUID })
|
const { job } = await servers[0].runnerJobs.accept<RunnerJobVODHLSTranscodingPayload>({ runnerToken, jobUUID })
|
||||||
jobToken = job.jobToken
|
jobToken = job.jobToken
|
||||||
|
|
||||||
const { body } = await servers[0].runnerJobs.getInputFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
|
const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
|
||||||
const inputFile = await readFile(buildAbsoluteFixturePath('video_short.mp4'))
|
const inputFile = await readFile(buildAbsoluteFixturePath('video_short.mp4'))
|
||||||
|
|
||||||
expect(body).to.deep.equal(inputFile)
|
expect(body).to.deep.equal(inputFile)
|
||||||
|
@ -337,7 +337,7 @@ describe('Test runner VOD transcoding', function () {
|
||||||
const { job } = await servers[0].runnerJobs.accept<RunnerJobVODHLSTranscodingPayload>({ runnerToken, jobUUID })
|
const { job } = await servers[0].runnerJobs.accept<RunnerJobVODHLSTranscodingPayload>({ runnerToken, jobUUID })
|
||||||
jobToken = job.jobToken
|
jobToken = job.jobToken
|
||||||
|
|
||||||
const { body } = await servers[0].runnerJobs.getInputFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
|
const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
|
||||||
const inputFile = await readFile(buildAbsoluteFixturePath(maxQualityFile))
|
const inputFile = await readFile(buildAbsoluteFixturePath(maxQualityFile))
|
||||||
expect(body).to.deep.equal(inputFile)
|
expect(body).to.deep.equal(inputFile)
|
||||||
|
|
||||||
|
@ -446,13 +446,13 @@ describe('Test runner VOD transcoding', function () {
|
||||||
expect(job.payload.output.resolution).to.equal(480)
|
expect(job.payload.output.resolution).to.equal(480)
|
||||||
|
|
||||||
{
|
{
|
||||||
const { body } = await servers[0].runnerJobs.getInputFile({ url: job.payload.input.audioFileUrl, jobToken, runnerToken })
|
const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.audioFileUrl, jobToken, runnerToken })
|
||||||
const inputFile = await readFile(buildAbsoluteFixturePath('sample.ogg'))
|
const inputFile = await readFile(buildAbsoluteFixturePath('sample.ogg'))
|
||||||
expect(body).to.deep.equal(inputFile)
|
expect(body).to.deep.equal(inputFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const { body } = await servers[0].runnerJobs.getInputFile({ url: job.payload.input.previewFileUrl, jobToken, runnerToken })
|
const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.previewFileUrl, jobToken, runnerToken })
|
||||||
|
|
||||||
const video = await servers[0].videos.get({ id: videoUUID })
|
const video = await servers[0].videos.get({ id: videoUUID })
|
||||||
const { body: inputFile } = await makeGetRequest({
|
const { body: inputFile } = await makeGetRequest({
|
||||||
|
@ -503,7 +503,7 @@ describe('Test runner VOD transcoding', function () {
|
||||||
const { job } = await servers[0].runnerJobs.accept<RunnerJobVODHLSTranscodingPayload>({ runnerToken, jobUUID })
|
const { job } = await servers[0].runnerJobs.accept<RunnerJobVODHLSTranscodingPayload>({ runnerToken, jobUUID })
|
||||||
jobToken = job.jobToken
|
jobToken = job.jobToken
|
||||||
|
|
||||||
const { body } = await servers[0].runnerJobs.getInputFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
|
const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
|
||||||
const inputFile = await readFile(buildAbsoluteFixturePath('video_short_480p.mp4'))
|
const inputFile = await readFile(buildAbsoluteFixturePath('video_short_480p.mp4'))
|
||||||
expect(body).to.deep.equal(inputFile)
|
expect(body).to.deep.equal(inputFile)
|
||||||
|
|
||||||
|
|
|
@ -102,6 +102,7 @@ function checkInitialConfig (server: PeerTubeServer, data: CustomConfig) {
|
||||||
expect(data.live.transcoding.alwaysTranscodeOriginalResolution).to.be.true
|
expect(data.live.transcoding.alwaysTranscodeOriginalResolution).to.be.true
|
||||||
|
|
||||||
expect(data.videoStudio.enabled).to.be.false
|
expect(data.videoStudio.enabled).to.be.false
|
||||||
|
expect(data.videoStudio.remoteRunners.enabled).to.be.false
|
||||||
|
|
||||||
expect(data.import.videos.concurrency).to.equal(2)
|
expect(data.import.videos.concurrency).to.equal(2)
|
||||||
expect(data.import.videos.http.enabled).to.be.true
|
expect(data.import.videos.http.enabled).to.be.true
|
||||||
|
@ -211,6 +212,7 @@ function checkUpdatedConfig (data: CustomConfig) {
|
||||||
expect(data.live.transcoding.alwaysTranscodeOriginalResolution).to.be.false
|
expect(data.live.transcoding.alwaysTranscodeOriginalResolution).to.be.false
|
||||||
|
|
||||||
expect(data.videoStudio.enabled).to.be.true
|
expect(data.videoStudio.enabled).to.be.true
|
||||||
|
expect(data.videoStudio.remoteRunners.enabled).to.be.true
|
||||||
|
|
||||||
expect(data.import.videos.concurrency).to.equal(4)
|
expect(data.import.videos.concurrency).to.equal(4)
|
||||||
expect(data.import.videos.http.enabled).to.be.false
|
expect(data.import.videos.http.enabled).to.be.false
|
||||||
|
@ -374,7 +376,10 @@ const newCustomConfig: CustomConfig = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
videoStudio: {
|
videoStudio: {
|
||||||
|
enabled: true,
|
||||||
|
remoteRunners: {
|
||||||
enabled: true
|
enabled: true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
import: {
|
import: {
|
||||||
videos: {
|
videos: {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import { checkPersistentTmpIsEmpty, expectStartWith } from '@server/tests/shared'
|
import { checkPersistentTmpIsEmpty, checkVideoDuration, expectStartWith } from '@server/tests/shared'
|
||||||
import { areMockObjectStorageTestsDisabled, getAllFiles } from '@shared/core-utils'
|
import { areMockObjectStorageTestsDisabled, getAllFiles } from '@shared/core-utils'
|
||||||
import { VideoStudioTask } from '@shared/models'
|
import { VideoStudioTask } from '@shared/models'
|
||||||
import {
|
import {
|
||||||
|
@ -18,20 +18,6 @@ describe('Test video studio', function () {
|
||||||
let servers: PeerTubeServer[] = []
|
let servers: PeerTubeServer[] = []
|
||||||
let videoUUID: string
|
let videoUUID: string
|
||||||
|
|
||||||
async function checkDuration (server: PeerTubeServer, duration: number) {
|
|
||||||
const video = await server.videos.get({ id: videoUUID })
|
|
||||||
|
|
||||||
expect(video.duration).to.be.approximately(duration, 1)
|
|
||||||
|
|
||||||
for (const file of video.files) {
|
|
||||||
const metadata = await server.videos.getFileMetadata({ url: file.metadataUrl })
|
|
||||||
|
|
||||||
for (const stream of metadata.streams) {
|
|
||||||
expect(Math.round(stream.duration)).to.be.approximately(duration, 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function renewVideo (fixture = 'video_short.webm') {
|
async function renewVideo (fixture = 'video_short.webm') {
|
||||||
const video = await servers[0].videos.quickUpload({ name: 'video', fixture })
|
const video = await servers[0].videos.quickUpload({ name: 'video', fixture })
|
||||||
videoUUID = video.uuid
|
videoUUID = video.uuid
|
||||||
|
@ -79,7 +65,7 @@ describe('Test video studio', function () {
|
||||||
])
|
])
|
||||||
|
|
||||||
for (const server of servers) {
|
for (const server of servers) {
|
||||||
await checkDuration(server, 3)
|
await checkVideoDuration(server, videoUUID, 3)
|
||||||
|
|
||||||
const video = await server.videos.get({ id: videoUUID })
|
const video = await server.videos.get({ id: videoUUID })
|
||||||
expect(new Date(video.publishedAt)).to.be.below(beforeTasks)
|
expect(new Date(video.publishedAt)).to.be.below(beforeTasks)
|
||||||
|
@ -100,7 +86,7 @@ describe('Test video studio', function () {
|
||||||
])
|
])
|
||||||
|
|
||||||
for (const server of servers) {
|
for (const server of servers) {
|
||||||
await checkDuration(server, 2)
|
await checkVideoDuration(server, videoUUID, 2)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -119,7 +105,7 @@ describe('Test video studio', function () {
|
||||||
])
|
])
|
||||||
|
|
||||||
for (const server of servers) {
|
for (const server of servers) {
|
||||||
await checkDuration(server, 4)
|
await checkVideoDuration(server, videoUUID, 4)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -140,7 +126,7 @@ describe('Test video studio', function () {
|
||||||
])
|
])
|
||||||
|
|
||||||
for (const server of servers) {
|
for (const server of servers) {
|
||||||
await checkDuration(server, 10)
|
await checkVideoDuration(server, videoUUID, 10)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -158,7 +144,7 @@ describe('Test video studio', function () {
|
||||||
])
|
])
|
||||||
|
|
||||||
for (const server of servers) {
|
for (const server of servers) {
|
||||||
await checkDuration(server, 7)
|
await checkVideoDuration(server, videoUUID, 7)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -183,7 +169,7 @@ describe('Test video studio', function () {
|
||||||
])
|
])
|
||||||
|
|
||||||
for (const server of servers) {
|
for (const server of servers) {
|
||||||
await checkDuration(server, 12)
|
await checkVideoDuration(server, videoUUID, 12)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -201,7 +187,7 @@ describe('Test video studio', function () {
|
||||||
])
|
])
|
||||||
|
|
||||||
for (const server of servers) {
|
for (const server of servers) {
|
||||||
await checkDuration(server, 7)
|
await checkVideoDuration(server, videoUUID, 7)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -219,7 +205,7 @@ describe('Test video studio', function () {
|
||||||
])
|
])
|
||||||
|
|
||||||
for (const server of servers) {
|
for (const server of servers) {
|
||||||
await checkDuration(server, 10)
|
await checkVideoDuration(server, videoUUID, 10)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -237,7 +223,7 @@ describe('Test video studio', function () {
|
||||||
])
|
])
|
||||||
|
|
||||||
for (const server of servers) {
|
for (const server of servers) {
|
||||||
await checkDuration(server, 10)
|
await checkVideoDuration(server, videoUUID, 10)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -279,7 +265,7 @@ describe('Test video studio', function () {
|
||||||
await createTasks(VideoStudioCommand.getComplexTask())
|
await createTasks(VideoStudioCommand.getComplexTask())
|
||||||
|
|
||||||
for (const server of servers) {
|
for (const server of servers) {
|
||||||
await checkDuration(server, 9)
|
await checkVideoDuration(server, videoUUID, 9)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -309,7 +295,7 @@ describe('Test video studio', function () {
|
||||||
const video = await server.videos.get({ id: videoUUID })
|
const video = await server.videos.get({ id: videoUUID })
|
||||||
expect(video.files).to.have.lengthOf(0)
|
expect(video.files).to.have.lengthOf(0)
|
||||||
|
|
||||||
await checkDuration(server, 9)
|
await checkVideoDuration(server, videoUUID, 9)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -351,7 +337,7 @@ describe('Test video studio', function () {
|
||||||
expectStartWith(hlsFile.fileUrl, ObjectStorageCommand.getMockPlaylistBaseUrl())
|
expectStartWith(hlsFile.fileUrl, ObjectStorageCommand.getMockPlaylistBaseUrl())
|
||||||
}
|
}
|
||||||
|
|
||||||
await checkDuration(server, 9)
|
await checkVideoDuration(server, videoUUID, 9)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -370,7 +356,7 @@ describe('Test video studio', function () {
|
||||||
await waitJobs(servers)
|
await waitJobs(servers)
|
||||||
|
|
||||||
for (const server of servers) {
|
for (const server of servers) {
|
||||||
await checkDuration(server, 9)
|
await checkVideoDuration(server, videoUUID, 9)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
export * from './client-cli'
|
export * from './client-cli'
|
||||||
export * from './live-transcoding'
|
export * from './live-transcoding'
|
||||||
|
export * from './studio-transcoding'
|
||||||
export * from './vod-transcoding'
|
export * from './vod-transcoding'
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||||
import { expectStartWith, PeerTubeRunnerProcess, SQLCommand, testLiveVideoResolutions } from '@server/tests/shared'
|
import {
|
||||||
|
checkPeerTubeRunnerCacheIsEmpty,
|
||||||
|
expectStartWith,
|
||||||
|
PeerTubeRunnerProcess,
|
||||||
|
SQLCommand,
|
||||||
|
testLiveVideoResolutions
|
||||||
|
} from '@server/tests/shared'
|
||||||
import { areMockObjectStorageTestsDisabled, wait } from '@shared/core-utils'
|
import { areMockObjectStorageTestsDisabled, wait } from '@shared/core-utils'
|
||||||
import { HttpStatusCode, VideoPrivacy } from '@shared/models'
|
import { HttpStatusCode, VideoPrivacy } from '@shared/models'
|
||||||
import {
|
import {
|
||||||
|
@ -169,6 +175,13 @@ describe('Test Live transcoding in peertube-runner program', function () {
|
||||||
runSuite({ objectStorage: true })
|
runSuite({ objectStorage: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Check cleanup', function () {
|
||||||
|
|
||||||
|
it('Should have an empty cache directory', async function () {
|
||||||
|
await checkPeerTubeRunnerCacheIsEmpty()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
after(async function () {
|
after(async function () {
|
||||||
await peertubeRunner.unregisterPeerTubeInstance({ server: servers[0] })
|
await peertubeRunner.unregisterPeerTubeInstance({ server: servers[0] })
|
||||||
peertubeRunner.kill()
|
peertubeRunner.kill()
|
||||||
|
|
|
@ -0,0 +1,116 @@
|
||||||
|
|
||||||
|
import { expect } from 'chai'
|
||||||
|
import { checkPeerTubeRunnerCacheIsEmpty, checkVideoDuration, expectStartWith, PeerTubeRunnerProcess } from '@server/tests/shared'
|
||||||
|
import { areMockObjectStorageTestsDisabled, getAllFiles, wait } from '@shared/core-utils'
|
||||||
|
import {
|
||||||
|
cleanupTests,
|
||||||
|
createMultipleServers,
|
||||||
|
doubleFollow,
|
||||||
|
ObjectStorageCommand,
|
||||||
|
PeerTubeServer,
|
||||||
|
setAccessTokensToServers,
|
||||||
|
setDefaultVideoChannel,
|
||||||
|
VideoStudioCommand,
|
||||||
|
waitJobs
|
||||||
|
} from '@shared/server-commands'
|
||||||
|
|
||||||
|
describe('Test studio transcoding in peertube-runner program', function () {
|
||||||
|
let servers: PeerTubeServer[] = []
|
||||||
|
let peertubeRunner: PeerTubeRunnerProcess
|
||||||
|
|
||||||
|
function runSuite (options: {
|
||||||
|
objectStorage: boolean
|
||||||
|
}) {
|
||||||
|
const { objectStorage } = options
|
||||||
|
|
||||||
|
it('Should run a complex studio transcoding', async function () {
|
||||||
|
this.timeout(120000)
|
||||||
|
|
||||||
|
const { uuid } = await servers[0].videos.quickUpload({ name: 'mp4', fixture: 'video_short.mp4' })
|
||||||
|
await waitJobs(servers)
|
||||||
|
|
||||||
|
const video = await servers[0].videos.get({ id: uuid })
|
||||||
|
const oldFileUrls = getAllFiles(video).map(f => f.fileUrl)
|
||||||
|
|
||||||
|
await servers[0].videoStudio.createEditionTasks({ videoId: uuid, tasks: VideoStudioCommand.getComplexTask() })
|
||||||
|
await waitJobs(servers, { runnerJobs: true })
|
||||||
|
|
||||||
|
for (const server of servers) {
|
||||||
|
const video = await server.videos.get({ id: uuid })
|
||||||
|
const files = getAllFiles(video)
|
||||||
|
|
||||||
|
for (const f of files) {
|
||||||
|
expect(oldFileUrls).to.not.include(f.fileUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (objectStorage) {
|
||||||
|
for (const webtorrentFile of video.files) {
|
||||||
|
expectStartWith(webtorrentFile.fileUrl, ObjectStorageCommand.getMockWebTorrentBaseUrl())
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const hlsFile of video.streamingPlaylists[0].files) {
|
||||||
|
expectStartWith(hlsFile.fileUrl, ObjectStorageCommand.getMockPlaylistBaseUrl())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await checkVideoDuration(server, uuid, 9)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
this.timeout(120_000)
|
||||||
|
|
||||||
|
servers = await createMultipleServers(2)
|
||||||
|
|
||||||
|
await setAccessTokensToServers(servers)
|
||||||
|
await setDefaultVideoChannel(servers)
|
||||||
|
|
||||||
|
await doubleFollow(servers[0], servers[1])
|
||||||
|
|
||||||
|
await servers[0].config.enableTranscoding(true, true)
|
||||||
|
await servers[0].config.enableStudio()
|
||||||
|
await servers[0].config.enableRemoteStudio()
|
||||||
|
|
||||||
|
const registrationToken = await servers[0].runnerRegistrationTokens.getFirstRegistrationToken()
|
||||||
|
|
||||||
|
peertubeRunner = new PeerTubeRunnerProcess()
|
||||||
|
await peertubeRunner.runServer({ hideLogs: false })
|
||||||
|
await peertubeRunner.registerPeerTubeInstance({ server: servers[0], registrationToken, runnerName: 'runner' })
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('With videos on local filesystem storage', function () {
|
||||||
|
runSuite({ objectStorage: false })
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('With videos on object storage', function () {
|
||||||
|
if (areMockObjectStorageTestsDisabled()) return
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
await ObjectStorageCommand.prepareDefaultMockBuckets()
|
||||||
|
|
||||||
|
await servers[0].kill()
|
||||||
|
|
||||||
|
await servers[0].run(ObjectStorageCommand.getDefaultMockConfig())
|
||||||
|
|
||||||
|
// Wait for peertube runner socket reconnection
|
||||||
|
await wait(1500)
|
||||||
|
})
|
||||||
|
|
||||||
|
runSuite({ objectStorage: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Check cleanup', function () {
|
||||||
|
|
||||||
|
it('Should have an empty cache directory', async function () {
|
||||||
|
await checkPeerTubeRunnerCacheIsEmpty()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
after(async function () {
|
||||||
|
await peertubeRunner.unregisterPeerTubeInstance({ server: servers[0] })
|
||||||
|
peertubeRunner.kill()
|
||||||
|
|
||||||
|
await cleanupTests(servers)
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,6 +1,11 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import { completeCheckHlsPlaylist, completeWebVideoFilesCheck, PeerTubeRunnerProcess } from '@server/tests/shared'
|
import {
|
||||||
|
checkPeerTubeRunnerCacheIsEmpty,
|
||||||
|
completeCheckHlsPlaylist,
|
||||||
|
completeWebVideoFilesCheck,
|
||||||
|
PeerTubeRunnerProcess
|
||||||
|
} from '@server/tests/shared'
|
||||||
import { areMockObjectStorageTestsDisabled, getAllFiles, wait } from '@shared/core-utils'
|
import { areMockObjectStorageTestsDisabled, getAllFiles, wait } from '@shared/core-utils'
|
||||||
import { VideoPrivacy } from '@shared/models'
|
import { VideoPrivacy } from '@shared/models'
|
||||||
import {
|
import {
|
||||||
|
@ -321,6 +326,13 @@ describe('Test VOD transcoding in peertube-runner program', function () {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Check cleanup', function () {
|
||||||
|
|
||||||
|
it('Should have an empty cache directory', async function () {
|
||||||
|
await checkPeerTubeRunnerCacheIsEmpty()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
after(async function () {
|
after(async function () {
|
||||||
await peertubeRunner.unregisterPeerTubeInstance({ server: servers[0] })
|
await peertubeRunner.unregisterPeerTubeInstance({ server: servers[0] })
|
||||||
peertubeRunner.kill()
|
peertubeRunner.kill()
|
||||||
|
|
|
@ -130,6 +130,22 @@ function checkBadSortPagination (url: string, path: string, token?: string, quer
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async function checkVideoDuration (server: PeerTubeServer, videoUUID: string, duration: number) {
|
||||||
|
const video = await server.videos.get({ id: videoUUID })
|
||||||
|
|
||||||
|
expect(video.duration).to.be.approximately(duration, 1)
|
||||||
|
|
||||||
|
for (const file of video.files) {
|
||||||
|
const metadata = await server.videos.getFileMetadata({ url: file.metadataUrl })
|
||||||
|
|
||||||
|
for (const stream of metadata.streams) {
|
||||||
|
expect(Math.round(stream.duration)).to.be.approximately(duration, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
dateIsValid,
|
dateIsValid,
|
||||||
testImageSize,
|
testImageSize,
|
||||||
|
@ -142,5 +158,6 @@ export {
|
||||||
checkBadStartPagination,
|
checkBadStartPagination,
|
||||||
checkBadCountPagination,
|
checkBadCountPagination,
|
||||||
checkBadSortPagination,
|
checkBadSortPagination,
|
||||||
|
checkVideoDuration,
|
||||||
expectLogContain
|
expectLogContain
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,11 @@
|
||||||
|
|
||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import { pathExists, readdir } from 'fs-extra'
|
import { pathExists, readdir } from 'fs-extra'
|
||||||
|
import { homedir } from 'os'
|
||||||
|
import { join } from 'path'
|
||||||
import { PeerTubeServer } from '@shared/server-commands'
|
import { PeerTubeServer } from '@shared/server-commands'
|
||||||
|
|
||||||
async function checkTmpIsEmpty (server: PeerTubeServer) {
|
export async function checkTmpIsEmpty (server: PeerTubeServer) {
|
||||||
await checkDirectoryIsEmpty(server, 'tmp', [ 'plugins-global.css', 'hls', 'resumable-uploads' ])
|
await checkDirectoryIsEmpty(server, 'tmp', [ 'plugins-global.css', 'hls', 'resumable-uploads' ])
|
||||||
|
|
||||||
if (await pathExists(server.getDirectoryPath('tmp/hls'))) {
|
if (await pathExists(server.getDirectoryPath('tmp/hls'))) {
|
||||||
|
@ -12,11 +14,11 @@ async function checkTmpIsEmpty (server: PeerTubeServer) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkPersistentTmpIsEmpty (server: PeerTubeServer) {
|
export async function checkPersistentTmpIsEmpty (server: PeerTubeServer) {
|
||||||
await checkDirectoryIsEmpty(server, 'tmp-persistent')
|
await checkDirectoryIsEmpty(server, 'tmp-persistent')
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkDirectoryIsEmpty (server: PeerTubeServer, directory: string, exceptions: string[] = []) {
|
export async function checkDirectoryIsEmpty (server: PeerTubeServer, directory: string, exceptions: string[] = []) {
|
||||||
const directoryPath = server.getDirectoryPath(directory)
|
const directoryPath = server.getDirectoryPath(directory)
|
||||||
|
|
||||||
const directoryExists = await pathExists(directoryPath)
|
const directoryExists = await pathExists(directoryPath)
|
||||||
|
@ -28,8 +30,13 @@ async function checkDirectoryIsEmpty (server: PeerTubeServer, directory: string,
|
||||||
expect(filtered).to.have.lengthOf(0)
|
expect(filtered).to.have.lengthOf(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export async function checkPeerTubeRunnerCacheIsEmpty () {
|
||||||
checkTmpIsEmpty,
|
const directoryPath = join(homedir(), '.cache', 'peertube-runner-nodejs', 'test', 'transcoding')
|
||||||
checkPersistentTmpIsEmpty,
|
|
||||||
checkDirectoryIsEmpty
|
const directoryExists = await pathExists(directoryPath)
|
||||||
|
expect(directoryExists).to.be.true
|
||||||
|
|
||||||
|
const files = await readdir(directoryPath)
|
||||||
|
|
||||||
|
expect(files).to.have.lengthOf(0)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { VideoStudioTaskPayload } from '../server'
|
||||||
|
|
||||||
export type RunnerJobVODPayload =
|
export type RunnerJobVODPayload =
|
||||||
RunnerJobVODWebVideoTranscodingPayload |
|
RunnerJobVODWebVideoTranscodingPayload |
|
||||||
RunnerJobVODHLSTranscodingPayload |
|
RunnerJobVODHLSTranscodingPayload |
|
||||||
|
@ -5,7 +7,8 @@ export type RunnerJobVODPayload =
|
||||||
|
|
||||||
export type RunnerJobPayload =
|
export type RunnerJobPayload =
|
||||||
RunnerJobVODPayload |
|
RunnerJobVODPayload |
|
||||||
RunnerJobLiveRTMPHLSTranscodingPayload
|
RunnerJobLiveRTMPHLSTranscodingPayload |
|
||||||
|
RunnerJobVideoEditionTranscodingPayload
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@ -43,6 +46,14 @@ export interface RunnerJobVODAudioMergeTranscodingPayload {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RunnerJobVideoEditionTranscodingPayload {
|
||||||
|
input: {
|
||||||
|
videoFileUrl: string
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks: VideoStudioTaskPayload[]
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export function isAudioMergeTranscodingPayload (payload: RunnerJobPayload): payload is RunnerJobVODAudioMergeTranscodingPayload {
|
export function isAudioMergeTranscodingPayload (payload: RunnerJobPayload): payload is RunnerJobVODAudioMergeTranscodingPayload {
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { VideoStudioTaskPayload } from '../server'
|
||||||
|
|
||||||
export type RunnerJobVODPrivatePayload =
|
export type RunnerJobVODPrivatePayload =
|
||||||
RunnerJobVODWebVideoTranscodingPrivatePayload |
|
RunnerJobVODWebVideoTranscodingPrivatePayload |
|
||||||
RunnerJobVODAudioMergeTranscodingPrivatePayload |
|
RunnerJobVODAudioMergeTranscodingPrivatePayload |
|
||||||
|
@ -5,7 +7,8 @@ export type RunnerJobVODPrivatePayload =
|
||||||
|
|
||||||
export type RunnerJobPrivatePayload =
|
export type RunnerJobPrivatePayload =
|
||||||
RunnerJobVODPrivatePayload |
|
RunnerJobVODPrivatePayload |
|
||||||
RunnerJobLiveRTMPHLSTranscodingPrivatePayload
|
RunnerJobLiveRTMPHLSTranscodingPrivatePayload |
|
||||||
|
RunnerJobVideoEditionTranscodingPrivatePayload
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@ -32,3 +35,10 @@ export interface RunnerJobLiveRTMPHLSTranscodingPrivatePayload {
|
||||||
masterPlaylistName: string
|
masterPlaylistName: string
|
||||||
outputDirectory: string
|
outputDirectory: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export interface RunnerJobVideoEditionTranscodingPrivatePayload {
|
||||||
|
videoUUID: string
|
||||||
|
originalTasks: VideoStudioTaskPayload[]
|
||||||
|
}
|
||||||
|
|
|
@ -11,7 +11,8 @@ export type RunnerJobSuccessPayload =
|
||||||
VODWebVideoTranscodingSuccess |
|
VODWebVideoTranscodingSuccess |
|
||||||
VODHLSTranscodingSuccess |
|
VODHLSTranscodingSuccess |
|
||||||
VODAudioMergeTranscodingSuccess |
|
VODAudioMergeTranscodingSuccess |
|
||||||
LiveRTMPHLSTranscodingSuccess
|
LiveRTMPHLSTranscodingSuccess |
|
||||||
|
VideoEditionTranscodingSuccess
|
||||||
|
|
||||||
export interface VODWebVideoTranscodingSuccess {
|
export interface VODWebVideoTranscodingSuccess {
|
||||||
videoFile: Blob | string
|
videoFile: Blob | string
|
||||||
|
@ -30,6 +31,10 @@ export interface LiveRTMPHLSTranscodingSuccess {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface VideoEditionTranscodingSuccess {
|
||||||
|
videoFile: Blob | string
|
||||||
|
}
|
||||||
|
|
||||||
export function isWebVideoOrAudioMergeTranscodingPayloadSuccess (
|
export function isWebVideoOrAudioMergeTranscodingPayloadSuccess (
|
||||||
payload: RunnerJobSuccessPayload
|
payload: RunnerJobSuccessPayload
|
||||||
): payload is VODHLSTranscodingSuccess | VODAudioMergeTranscodingSuccess {
|
): payload is VODHLSTranscodingSuccess | VODAudioMergeTranscodingSuccess {
|
||||||
|
|
|
@ -2,4 +2,5 @@ export type RunnerJobType =
|
||||||
'vod-web-video-transcoding' |
|
'vod-web-video-transcoding' |
|
||||||
'vod-hls-transcoding' |
|
'vod-hls-transcoding' |
|
||||||
'vod-audio-merge-transcoding' |
|
'vod-audio-merge-transcoding' |
|
||||||
'live-rtmp-hls-transcoding'
|
'live-rtmp-hls-transcoding' |
|
||||||
|
'video-edition-transcoding'
|
||||||
|
|
|
@ -165,6 +165,10 @@ export interface CustomConfig {
|
||||||
|
|
||||||
videoStudio: {
|
videoStudio: {
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
|
|
||||||
|
remoteRunners: {
|
||||||
|
enabled: boolean
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
import: {
|
import: {
|
||||||
|
|
|
@ -225,6 +225,10 @@ export type VideoStudioTaskWatermarkPayload = {
|
||||||
|
|
||||||
options: {
|
options: {
|
||||||
file: string
|
file: string
|
||||||
|
|
||||||
|
watermarkSizeRatio: number
|
||||||
|
horitonzalMarginRatio: number
|
||||||
|
verticalMarginRatio: number
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { VideoPrivacy } from '../videos/video-privacy.enum'
|
|
||||||
import { ClientScriptJSON } from '../plugins/plugin-package-json.model'
|
import { ClientScriptJSON } from '../plugins/plugin-package-json.model'
|
||||||
import { NSFWPolicyType } from '../videos/nsfw-policy.type'
|
import { NSFWPolicyType } from '../videos/nsfw-policy.type'
|
||||||
|
import { VideoPrivacy } from '../videos/video-privacy.enum'
|
||||||
import { BroadcastMessageLevel } from './broadcast-message-level.type'
|
import { BroadcastMessageLevel } from './broadcast-message-level.type'
|
||||||
|
|
||||||
export interface ServerConfigPlugin {
|
export interface ServerConfigPlugin {
|
||||||
|
@ -186,6 +186,10 @@ export interface ServerConfig {
|
||||||
|
|
||||||
videoStudio: {
|
videoStudio: {
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
|
|
||||||
|
remoteRunners: {
|
||||||
|
enabled: boolean
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
import: {
|
import: {
|
||||||
|
|
|
@ -40,3 +40,21 @@ export interface VideoStudioTaskWatermark {
|
||||||
file: Blob | string
|
file: Blob | string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export function isVideoStudioTaskIntro (v: VideoStudioTask): v is VideoStudioTaskIntro {
|
||||||
|
return v.name === 'add-intro'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isVideoStudioTaskOutro (v: VideoStudioTask): v is VideoStudioTaskOutro {
|
||||||
|
return v.name === 'add-outro'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isVideoStudioTaskWatermark (v: VideoStudioTask): v is VideoStudioTaskWatermark {
|
||||||
|
return v.name === 'add-watermark'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasVideoStudioTaskFile (v: VideoStudioTask): v is VideoStudioTaskIntro | VideoStudioTaskOutro | VideoStudioTaskWatermark {
|
||||||
|
return isVideoStudioTaskIntro(v) || isVideoStudioTaskOutro(v) || isVideoStudioTaskWatermark(v)
|
||||||
|
}
|
||||||
|
|
|
@ -200,7 +200,7 @@ export class RunnerJobsCommand extends AbstractCommand {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
getInputFile (options: OverrideCommandOptions & { url: string, jobToken: string, runnerToken: string }) {
|
getJobFile (options: OverrideCommandOptions & { url: string, jobToken: string, runnerToken: string }) {
|
||||||
const { host, protocol, pathname } = new URL(options.url)
|
const { host, protocol, pathname } = new URL(options.url)
|
||||||
|
|
||||||
return this.postBodyRequest({
|
return this.postBodyRequest({
|
||||||
|
@ -249,8 +249,15 @@ export class RunnerJobsCommand extends AbstractCommand {
|
||||||
|
|
||||||
const { data } = await this.list({ count: 100 })
|
const { data } = await this.list({ count: 100 })
|
||||||
|
|
||||||
|
const allowedStates = new Set<RunnerJobState>([
|
||||||
|
RunnerJobState.PENDING,
|
||||||
|
RunnerJobState.PROCESSING,
|
||||||
|
RunnerJobState.WAITING_FOR_PARENT_JOB
|
||||||
|
])
|
||||||
|
|
||||||
for (const job of data) {
|
for (const job of data) {
|
||||||
if (state && job.state.id !== state) continue
|
if (state && job.state.id !== state) continue
|
||||||
|
else if (allowedStates.has(job.state.id) !== true) continue
|
||||||
|
|
||||||
await this.cancelByAdmin({ jobUUID: job.uuid })
|
await this.cancelByAdmin({ jobUUID: job.uuid })
|
||||||
}
|
}
|
||||||
|
|
|
@ -195,6 +195,18 @@ export class ConfigCommand extends AbstractCommand {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enableRemoteStudio () {
|
||||||
|
return this.updateExistingSubConfig({
|
||||||
|
newConfig: {
|
||||||
|
videoStudio: {
|
||||||
|
remoteRunners: {
|
||||||
|
enabled: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
enableStudio () {
|
enableStudio () {
|
||||||
|
@ -442,7 +454,10 @@ export class ConfigCommand extends AbstractCommand {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
videoStudio: {
|
videoStudio: {
|
||||||
|
enabled: false,
|
||||||
|
remoteRunners: {
|
||||||
enabled: false
|
enabled: false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
import: {
|
import: {
|
||||||
videos: {
|
videos: {
|
||||||
|
|
Loading…
Reference in New Issue