mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2025-10-04 18:29:27 +02:00
Implement remote runner jobs in server
Move ffmpeg functions to @shared
This commit is contained in:
parent
6bcb854cde
commit
0c9668f779
168 changed files with 6907 additions and 2803 deletions
36
server/lib/transcoding/create-transcoding-job.ts
Normal file
36
server/lib/transcoding/create-transcoding-job.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import { CONFIG } from '@server/initializers/config'
|
||||
import { MUserId, MVideoFile, MVideoFullLight } from '@server/types/models'
|
||||
import { TranscodingJobQueueBuilder, TranscodingRunnerJobBuilder } from './shared'
|
||||
|
||||
export function createOptimizeOrMergeAudioJobs (options: {
|
||||
video: MVideoFullLight
|
||||
videoFile: MVideoFile
|
||||
isNewVideo: boolean
|
||||
user: MUserId
|
||||
}) {
|
||||
return getJobBuilder().createOptimizeOrMergeAudioJobs(options)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function createTranscodingJobs (options: {
|
||||
transcodingType: 'hls' | 'webtorrent'
|
||||
video: MVideoFullLight
|
||||
resolutions: number[]
|
||||
isNewVideo: boolean
|
||||
user: MUserId
|
||||
}) {
|
||||
return getJobBuilder().createTranscodingJobs(options)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function getJobBuilder () {
|
||||
if (CONFIG.TRANSCODING.REMOTE_RUNNERS.ENABLED === true) {
|
||||
return new TranscodingRunnerJobBuilder()
|
||||
}
|
||||
|
||||
return new TranscodingJobQueueBuilder()
|
||||
}
|
|
@ -1,15 +1,9 @@
|
|||
|
||||
import { logger } from '@server/helpers/logger'
|
||||
import { getAverageBitrate, getMinLimitBitrate } from '@shared/core-utils'
|
||||
import { AvailableEncoders, EncoderOptionsBuilder, EncoderOptionsBuilderParams, VideoResolution } from '../../../shared/models/videos'
|
||||
import {
|
||||
buildStreamSuffix,
|
||||
canDoQuickAudioTranscode,
|
||||
ffprobePromise,
|
||||
getAudioStream,
|
||||
getMaxAudioBitrate,
|
||||
resetSupportedEncoders
|
||||
} from '../../helpers/ffmpeg'
|
||||
import { buildStreamSuffix, FFmpegCommandWrapper, ffprobePromise, getAudioStream, getMaxAudioBitrate } from '@shared/ffmpeg'
|
||||
import { AvailableEncoders, EncoderOptionsBuilder, EncoderOptionsBuilderParams, VideoResolution } from '@shared/models'
|
||||
import { canDoQuickAudioTranscode } from './transcoding-quick-transcode'
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -184,14 +178,14 @@ class VideoTranscodingProfilesManager {
|
|||
addEncoderPriority (type: 'vod' | 'live', streamType: 'audio' | 'video', encoder: string, priority: number) {
|
||||
this.encodersPriorities[type][streamType].push({ name: encoder, priority })
|
||||
|
||||
resetSupportedEncoders()
|
||||
FFmpegCommandWrapper.resetSupportedEncoders()
|
||||
}
|
||||
|
||||
removeEncoderPriority (type: 'vod' | 'live', streamType: 'audio' | 'video', encoder: string, priority: number) {
|
||||
this.encodersPriorities[type][streamType] = this.encodersPriorities[type][streamType]
|
||||
.filter(o => o.name !== encoder && o.priority !== priority)
|
||||
|
||||
resetSupportedEncoders()
|
||||
FFmpegCommandWrapper.resetSupportedEncoders()
|
||||
}
|
||||
|
||||
private getEncodersByPriority (type: 'vod' | 'live', streamType: 'audio' | 'video') {
|
||||
|
|
18
server/lib/transcoding/ended-transcoding.ts
Normal file
18
server/lib/transcoding/ended-transcoding.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { retryTransactionWrapper } from '@server/helpers/database-utils'
|
||||
import { VideoJobInfoModel } from '@server/models/video/video-job-info'
|
||||
import { MVideo } from '@server/types/models'
|
||||
import { moveToNextState } from '../video-state'
|
||||
|
||||
export async function onTranscodingEnded (options: {
|
||||
video: MVideo
|
||||
isNewVideo: boolean
|
||||
moveVideoToNextState: boolean
|
||||
}) {
|
||||
const { video, isNewVideo, moveVideoToNextState } = options
|
||||
|
||||
await VideoJobInfoModel.decrease(video.uuid, 'pendingTranscode')
|
||||
|
||||
if (moveVideoToNextState) {
|
||||
await retryTransactionWrapper(moveToNextState, { video, isNewVideo })
|
||||
}
|
||||
}
|
181
server/lib/transcoding/hls-transcoding.ts
Normal file
181
server/lib/transcoding/hls-transcoding.ts
Normal file
|
@ -0,0 +1,181 @@
|
|||
import { MutexInterface } from 'async-mutex'
|
||||
import { Job } from 'bullmq'
|
||||
import { ensureDir, move, stat } from 'fs-extra'
|
||||
import { basename, extname as extnameUtil, join } from 'path'
|
||||
import { retryTransactionWrapper } from '@server/helpers/database-utils'
|
||||
import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
|
||||
import { sequelizeTypescript } from '@server/initializers/database'
|
||||
import { MVideo, MVideoFile } from '@server/types/models'
|
||||
import { pick } from '@shared/core-utils'
|
||||
import { getVideoStreamDuration, getVideoStreamFPS } from '@shared/ffmpeg'
|
||||
import { VideoResolution } from '@shared/models'
|
||||
import { CONFIG } from '../../initializers/config'
|
||||
import { VideoFileModel } from '../../models/video/video-file'
|
||||
import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist'
|
||||
import { updatePlaylistAfterFileChange } from '../hls'
|
||||
import { generateHLSVideoFilename, getHlsResolutionPlaylistFilename } from '../paths'
|
||||
import { buildFileMetadata } from '../video-file'
|
||||
import { VideoPathManager } from '../video-path-manager'
|
||||
import { buildFFmpegVOD } from './shared'
|
||||
|
||||
// Concat TS segments from a live video to a fragmented mp4 HLS playlist
|
||||
export async function generateHlsPlaylistResolutionFromTS (options: {
|
||||
video: MVideo
|
||||
concatenatedTsFilePath: string
|
||||
resolution: VideoResolution
|
||||
fps: number
|
||||
isAAC: boolean
|
||||
inputFileMutexReleaser: MutexInterface.Releaser
|
||||
}) {
|
||||
return generateHlsPlaylistCommon({
|
||||
type: 'hls-from-ts' as 'hls-from-ts',
|
||||
inputPath: options.concatenatedTsFilePath,
|
||||
|
||||
...pick(options, [ 'video', 'resolution', 'fps', 'inputFileMutexReleaser', 'isAAC' ])
|
||||
})
|
||||
}
|
||||
|
||||
// Generate an HLS playlist from an input file, and update the master playlist
|
||||
export function generateHlsPlaylistResolution (options: {
|
||||
video: MVideo
|
||||
videoInputPath: string
|
||||
resolution: VideoResolution
|
||||
fps: number
|
||||
copyCodecs: boolean
|
||||
inputFileMutexReleaser: MutexInterface.Releaser
|
||||
job?: Job
|
||||
}) {
|
||||
return generateHlsPlaylistCommon({
|
||||
type: 'hls' as 'hls',
|
||||
inputPath: options.videoInputPath,
|
||||
|
||||
...pick(options, [ 'video', 'resolution', 'fps', 'copyCodecs', 'inputFileMutexReleaser', 'job' ])
|
||||
})
|
||||
}
|
||||
|
||||
export async function onHLSVideoFileTranscoding (options: {
|
||||
video: MVideo
|
||||
videoFile: MVideoFile
|
||||
videoOutputPath: string
|
||||
m3u8OutputPath: string
|
||||
}) {
|
||||
const { video, videoFile, videoOutputPath, m3u8OutputPath } = options
|
||||
|
||||
// Create or update the playlist
|
||||
const playlist = await retryTransactionWrapper(() => {
|
||||
return sequelizeTypescript.transaction(async transaction => {
|
||||
return VideoStreamingPlaylistModel.loadOrGenerate(video, transaction)
|
||||
})
|
||||
})
|
||||
videoFile.videoStreamingPlaylistId = playlist.id
|
||||
|
||||
const mutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid)
|
||||
|
||||
try {
|
||||
// VOD transcoding is a long task, refresh video attributes
|
||||
await video.reload()
|
||||
|
||||
const videoFilePath = VideoPathManager.Instance.getFSVideoFileOutputPath(playlist, videoFile)
|
||||
await ensureDir(VideoPathManager.Instance.getFSHLSOutputPath(video))
|
||||
|
||||
// Move playlist file
|
||||
const resolutionPlaylistPath = VideoPathManager.Instance.getFSHLSOutputPath(video, basename(m3u8OutputPath))
|
||||
await move(m3u8OutputPath, resolutionPlaylistPath, { overwrite: true })
|
||||
// Move video file
|
||||
await move(videoOutputPath, videoFilePath, { overwrite: true })
|
||||
|
||||
// Update video duration if it was not set (in case of a live for example)
|
||||
if (!video.duration) {
|
||||
video.duration = await getVideoStreamDuration(videoFilePath)
|
||||
await video.save()
|
||||
}
|
||||
|
||||
const stats = await stat(videoFilePath)
|
||||
|
||||
videoFile.size = stats.size
|
||||
videoFile.fps = await getVideoStreamFPS(videoFilePath)
|
||||
videoFile.metadata = await buildFileMetadata(videoFilePath)
|
||||
|
||||
await createTorrentAndSetInfoHash(playlist, videoFile)
|
||||
|
||||
const oldFile = await VideoFileModel.loadHLSFile({
|
||||
playlistId: playlist.id,
|
||||
fps: videoFile.fps,
|
||||
resolution: videoFile.resolution
|
||||
})
|
||||
|
||||
if (oldFile) {
|
||||
await video.removeStreamingPlaylistVideoFile(playlist, oldFile)
|
||||
await oldFile.destroy()
|
||||
}
|
||||
|
||||
const savedVideoFile = await VideoFileModel.customUpsert(videoFile, 'streaming-playlist', undefined)
|
||||
|
||||
await updatePlaylistAfterFileChange(video, playlist)
|
||||
|
||||
return { resolutionPlaylistPath, videoFile: savedVideoFile }
|
||||
} finally {
|
||||
mutexReleaser()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function generateHlsPlaylistCommon (options: {
|
||||
type: 'hls' | 'hls-from-ts'
|
||||
video: MVideo
|
||||
inputPath: string
|
||||
|
||||
resolution: VideoResolution
|
||||
fps: number
|
||||
|
||||
inputFileMutexReleaser: MutexInterface.Releaser
|
||||
|
||||
copyCodecs?: boolean
|
||||
isAAC?: boolean
|
||||
|
||||
job?: Job
|
||||
}) {
|
||||
const { type, video, inputPath, resolution, fps, copyCodecs, isAAC, job, inputFileMutexReleaser } = options
|
||||
const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
|
||||
|
||||
const videoTranscodedBasePath = join(transcodeDirectory, type)
|
||||
await ensureDir(videoTranscodedBasePath)
|
||||
|
||||
const videoFilename = generateHLSVideoFilename(resolution)
|
||||
const videoOutputPath = join(videoTranscodedBasePath, videoFilename)
|
||||
|
||||
const resolutionPlaylistFilename = getHlsResolutionPlaylistFilename(videoFilename)
|
||||
const m3u8OutputPath = join(videoTranscodedBasePath, resolutionPlaylistFilename)
|
||||
|
||||
const transcodeOptions = {
|
||||
type,
|
||||
|
||||
inputPath,
|
||||
outputPath: m3u8OutputPath,
|
||||
|
||||
resolution,
|
||||
fps,
|
||||
copyCodecs,
|
||||
|
||||
isAAC,
|
||||
|
||||
inputFileMutexReleaser,
|
||||
|
||||
hlsPlaylist: {
|
||||
videoFilename
|
||||
}
|
||||
}
|
||||
|
||||
await buildFFmpegVOD(job).transcode(transcodeOptions)
|
||||
|
||||
const newVideoFile = new VideoFileModel({
|
||||
resolution,
|
||||
extname: extnameUtil(videoFilename),
|
||||
size: 0,
|
||||
filename: videoFilename,
|
||||
fps: -1
|
||||
})
|
||||
|
||||
await onHLSVideoFileTranscoding({ video, videoFile: newVideoFile, videoOutputPath, m3u8OutputPath })
|
||||
}
|
18
server/lib/transcoding/shared/ffmpeg-builder.ts
Normal file
18
server/lib/transcoding/shared/ffmpeg-builder.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { Job } from 'bullmq'
|
||||
import { getFFmpegCommandWrapperOptions } from '@server/helpers/ffmpeg'
|
||||
import { logger } from '@server/helpers/logger'
|
||||
import { FFmpegVOD } from '@shared/ffmpeg'
|
||||
import { VideoTranscodingProfilesManager } from '../default-transcoding-profiles'
|
||||
|
||||
export function buildFFmpegVOD (job?: Job) {
|
||||
return new FFmpegVOD({
|
||||
...getFFmpegCommandWrapperOptions('vod', VideoTranscodingProfilesManager.Instance.getAvailableEncoders()),
|
||||
|
||||
updateJobProgress: progress => {
|
||||
if (!job) return
|
||||
|
||||
job.updateProgress(progress)
|
||||
.catch(err => logger.error('Cannot update ffmpeg job progress', { err }))
|
||||
}
|
||||
})
|
||||
}
|
2
server/lib/transcoding/shared/index.ts
Normal file
2
server/lib/transcoding/shared/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from './job-builders'
|
||||
export * from './ffmpeg-builder'
|
|
@ -0,0 +1,38 @@
|
|||
|
||||
import { JOB_PRIORITY } from '@server/initializers/constants'
|
||||
import { VideoModel } from '@server/models/video/video'
|
||||
import { MUserId, MVideoFile, MVideoFullLight } from '@server/types/models'
|
||||
|
||||
export abstract class AbstractJobBuilder {
|
||||
|
||||
abstract createOptimizeOrMergeAudioJobs (options: {
|
||||
video: MVideoFullLight
|
||||
videoFile: MVideoFile
|
||||
isNewVideo: boolean
|
||||
user: MUserId
|
||||
}): Promise<any>
|
||||
|
||||
abstract createTranscodingJobs (options: {
|
||||
transcodingType: 'hls' | 'webtorrent'
|
||||
video: MVideoFullLight
|
||||
resolutions: number[]
|
||||
isNewVideo: boolean
|
||||
user: MUserId | null
|
||||
}): 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
|
||||
}
|
||||
}
|
2
server/lib/transcoding/shared/job-builders/index.ts
Normal file
2
server/lib/transcoding/shared/job-builders/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from './transcoding-job-queue-builder'
|
||||
export * from './transcoding-runner-job-builder'
|
|
@ -0,0 +1,308 @@
|
|||
import Bluebird from 'bluebird'
|
||||
import { computeOutputFPS } from '@server/helpers/ffmpeg'
|
||||
import { logger } from '@server/helpers/logger'
|
||||
import { CONFIG } from '@server/initializers/config'
|
||||
import { DEFAULT_AUDIO_RESOLUTION, VIDEO_TRANSCODING_FPS } from '@server/initializers/constants'
|
||||
import { CreateJobArgument, JobQueue } from '@server/lib/job-queue'
|
||||
import { Hooks } from '@server/lib/plugins/hooks'
|
||||
import { VideoPathManager } from '@server/lib/video-path-manager'
|
||||
import { VideoJobInfoModel } from '@server/models/video/video-job-info'
|
||||
import { MUserId, MVideoFile, MVideoFullLight, MVideoWithFileThumbnail } from '@server/types/models'
|
||||
import { ffprobePromise, getVideoStreamDimensionsInfo, getVideoStreamFPS, hasAudioStream, isAudioFile } from '@shared/ffmpeg'
|
||||
import {
|
||||
HLSTranscodingPayload,
|
||||
MergeAudioTranscodingPayload,
|
||||
NewWebTorrentResolutionTranscodingPayload,
|
||||
OptimizeTranscodingPayload,
|
||||
VideoTranscodingPayload
|
||||
} from '@shared/models'
|
||||
import { canDoQuickTranscode } from '../../transcoding-quick-transcode'
|
||||
import { computeResolutionsToTranscode } from '../../transcoding-resolutions'
|
||||
import { AbstractJobBuilder } from './abstract-job-builder'
|
||||
|
||||
export class TranscodingJobQueueBuilder extends AbstractJobBuilder {
|
||||
|
||||
async createOptimizeOrMergeAudioJobs (options: {
|
||||
video: MVideoFullLight
|
||||
videoFile: MVideoFile
|
||||
isNewVideo: boolean
|
||||
user: MUserId
|
||||
}) {
|
||||
const { video, videoFile, isNewVideo, user } = options
|
||||
|
||||
let mergeOrOptimizePayload: MergeAudioTranscodingPayload | OptimizeTranscodingPayload
|
||||
let nextTranscodingSequentialJobPayloads: (NewWebTorrentResolutionTranscodingPayload | HLSTranscodingPayload)[][] = []
|
||||
|
||||
const mutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid)
|
||||
|
||||
try {
|
||||
await VideoPathManager.Instance.makeAvailableVideoFile(videoFile.withVideoOrPlaylist(video), async videoFilePath => {
|
||||
const probe = await ffprobePromise(videoFilePath)
|
||||
|
||||
const { resolution } = await getVideoStreamDimensionsInfo(videoFilePath, probe)
|
||||
const hasAudio = await hasAudioStream(videoFilePath, probe)
|
||||
const quickTranscode = await canDoQuickTranscode(videoFilePath, probe)
|
||||
const inputFPS = videoFile.isAudio()
|
||||
? VIDEO_TRANSCODING_FPS.AUDIO_MERGE // The first transcoding job will transcode to this FPS value
|
||||
: await getVideoStreamFPS(videoFilePath, probe)
|
||||
|
||||
const maxResolution = await isAudioFile(videoFilePath, probe)
|
||||
? DEFAULT_AUDIO_RESOLUTION
|
||||
: resolution
|
||||
|
||||
if (CONFIG.TRANSCODING.HLS.ENABLED === true) {
|
||||
nextTranscodingSequentialJobPayloads.push([
|
||||
this.buildHLSJobPayload({
|
||||
deleteWebTorrentFiles: CONFIG.TRANSCODING.WEBTORRENT.ENABLED === false,
|
||||
|
||||
// We had some issues with a web video quick transcoded while producing a HLS version of it
|
||||
copyCodecs: !quickTranscode,
|
||||
|
||||
resolution: maxResolution,
|
||||
fps: computeOutputFPS({ inputFPS, resolution: maxResolution }),
|
||||
videoUUID: video.uuid,
|
||||
isNewVideo
|
||||
})
|
||||
])
|
||||
}
|
||||
|
||||
const lowerResolutionJobPayloads = await this.buildLowerResolutionJobPayloads({
|
||||
video,
|
||||
inputVideoResolution: maxResolution,
|
||||
inputVideoFPS: inputFPS,
|
||||
hasAudio,
|
||||
isNewVideo
|
||||
})
|
||||
|
||||
nextTranscodingSequentialJobPayloads = [ ...nextTranscodingSequentialJobPayloads, ...lowerResolutionJobPayloads ]
|
||||
|
||||
mergeOrOptimizePayload = videoFile.isAudio()
|
||||
? this.buildMergeAudioPayload({ videoUUID: video.uuid, isNewVideo })
|
||||
: this.buildOptimizePayload({ videoUUID: video.uuid, isNewVideo, quickTranscode })
|
||||
})
|
||||
} finally {
|
||||
mutexReleaser()
|
||||
}
|
||||
|
||||
const nextTranscodingSequentialJobs = await Bluebird.mapSeries(nextTranscodingSequentialJobPayloads, payloads => {
|
||||
return Bluebird.mapSeries(payloads, payload => {
|
||||
return this.buildTranscodingJob({ payload, user })
|
||||
})
|
||||
})
|
||||
|
||||
const transcodingJobBuilderJob: CreateJobArgument = {
|
||||
type: 'transcoding-job-builder',
|
||||
payload: {
|
||||
videoUUID: video.uuid,
|
||||
sequentialJobs: nextTranscodingSequentialJobs
|
||||
}
|
||||
}
|
||||
|
||||
const mergeOrOptimizeJob = await this.buildTranscodingJob({ payload: mergeOrOptimizePayload, user })
|
||||
|
||||
return JobQueue.Instance.createSequentialJobFlow(...[ mergeOrOptimizeJob, transcodingJobBuilderJob ])
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async createTranscodingJobs (options: {
|
||||
transcodingType: 'hls' | 'webtorrent'
|
||||
video: MVideoFullLight
|
||||
resolutions: number[]
|
||||
isNewVideo: boolean
|
||||
user: MUserId | null
|
||||
}) {
|
||||
const { video, transcodingType, resolutions, isNewVideo } = options
|
||||
|
||||
const maxResolution = Math.max(...resolutions)
|
||||
const childrenResolutions = resolutions.filter(r => r !== maxResolution)
|
||||
|
||||
logger.info('Manually creating transcoding jobs for %s.', transcodingType, { childrenResolutions, maxResolution })
|
||||
|
||||
const { fps: inputFPS } = await video.probeMaxQualityFile()
|
||||
|
||||
const children = childrenResolutions.map(resolution => {
|
||||
const fps = computeOutputFPS({ inputFPS, resolution })
|
||||
|
||||
if (transcodingType === 'hls') {
|
||||
return this.buildHLSJobPayload({ videoUUID: video.uuid, resolution, fps, isNewVideo })
|
||||
}
|
||||
|
||||
if (transcodingType === 'webtorrent') {
|
||||
return this.buildWebTorrentJobPayload({ videoUUID: video.uuid, resolution, fps, isNewVideo })
|
||||
}
|
||||
|
||||
throw new Error('Unknown transcoding type')
|
||||
})
|
||||
|
||||
const fps = computeOutputFPS({ inputFPS, resolution: maxResolution })
|
||||
|
||||
const parent = transcodingType === 'hls'
|
||||
? this.buildHLSJobPayload({ videoUUID: video.uuid, resolution: maxResolution, fps, isNewVideo })
|
||||
: this.buildWebTorrentJobPayload({ videoUUID: video.uuid, resolution: maxResolution, fps, isNewVideo })
|
||||
|
||||
// Process the last resolution after the other ones to prevent concurrency issue
|
||||
// Because low resolutions use the biggest one as ffmpeg input
|
||||
await this.createTranscodingJobsWithChildren({ videoUUID: video.uuid, parent, children, user: null })
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
private async createTranscodingJobsWithChildren (options: {
|
||||
videoUUID: string
|
||||
parent: (HLSTranscodingPayload | NewWebTorrentResolutionTranscodingPayload)
|
||||
children: (HLSTranscodingPayload | NewWebTorrentResolutionTranscodingPayload)[]
|
||||
user: MUserId | null
|
||||
}) {
|
||||
const { videoUUID, parent, children, user } = options
|
||||
|
||||
const parentJob = await this.buildTranscodingJob({ payload: parent, user })
|
||||
const childrenJobs = await Bluebird.mapSeries(children, c => this.buildTranscodingJob({ payload: c, user }))
|
||||
|
||||
await JobQueue.Instance.createJobWithChildren(parentJob, childrenJobs)
|
||||
|
||||
await VideoJobInfoModel.increaseOrCreate(videoUUID, 'pendingTranscode', 1 + children.length)
|
||||
}
|
||||
|
||||
private async buildTranscodingJob (options: {
|
||||
payload: VideoTranscodingPayload
|
||||
user: MUserId | null // null means we don't want priority
|
||||
}) {
|
||||
const { user, payload } = options
|
||||
|
||||
return {
|
||||
type: 'video-transcoding' as 'video-transcoding',
|
||||
priority: await this.getTranscodingJobPriority({ user, fallback: undefined }),
|
||||
payload
|
||||
}
|
||||
}
|
||||
|
||||
private async buildLowerResolutionJobPayloads (options: {
|
||||
video: MVideoWithFileThumbnail
|
||||
inputVideoResolution: number
|
||||
inputVideoFPS: number
|
||||
hasAudio: boolean
|
||||
isNewVideo: boolean
|
||||
}) {
|
||||
const { video, inputVideoResolution, inputVideoFPS, isNewVideo, hasAudio } = options
|
||||
|
||||
// Create transcoding jobs if there are enabled resolutions
|
||||
const resolutionsEnabled = await Hooks.wrapObject(
|
||||
computeResolutionsToTranscode({ input: inputVideoResolution, type: 'vod', includeInput: false, strictLower: true, hasAudio }),
|
||||
'filter:transcoding.auto.resolutions-to-transcode.result',
|
||||
options
|
||||
)
|
||||
|
||||
const sequentialPayloads: (NewWebTorrentResolutionTranscodingPayload | HLSTranscodingPayload)[][] = []
|
||||
|
||||
for (const resolution of resolutionsEnabled) {
|
||||
const fps = computeOutputFPS({ inputFPS: inputVideoFPS, resolution })
|
||||
|
||||
if (CONFIG.TRANSCODING.WEBTORRENT.ENABLED) {
|
||||
const payloads: (NewWebTorrentResolutionTranscodingPayload | HLSTranscodingPayload)[] = [
|
||||
this.buildWebTorrentJobPayload({
|
||||
videoUUID: video.uuid,
|
||||
resolution,
|
||||
fps,
|
||||
isNewVideo
|
||||
})
|
||||
]
|
||||
|
||||
// Create a subsequent job to create HLS resolution that will just copy web video codecs
|
||||
if (CONFIG.TRANSCODING.HLS.ENABLED) {
|
||||
payloads.push(
|
||||
this.buildHLSJobPayload({
|
||||
videoUUID: video.uuid,
|
||||
resolution,
|
||||
fps,
|
||||
isNewVideo,
|
||||
copyCodecs: true
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
sequentialPayloads.push(payloads)
|
||||
} else if (CONFIG.TRANSCODING.HLS.ENABLED) {
|
||||
sequentialPayloads.push([
|
||||
this.buildHLSJobPayload({
|
||||
videoUUID: video.uuid,
|
||||
resolution,
|
||||
fps,
|
||||
copyCodecs: false,
|
||||
isNewVideo
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
return sequentialPayloads
|
||||
}
|
||||
|
||||
private buildHLSJobPayload (options: {
|
||||
videoUUID: string
|
||||
resolution: number
|
||||
fps: number
|
||||
isNewVideo: boolean
|
||||
deleteWebTorrentFiles?: boolean // default false
|
||||
copyCodecs?: boolean // default false
|
||||
}): HLSTranscodingPayload {
|
||||
const { videoUUID, resolution, fps, isNewVideo, deleteWebTorrentFiles = false, copyCodecs = false } = options
|
||||
|
||||
return {
|
||||
type: 'new-resolution-to-hls',
|
||||
videoUUID,
|
||||
resolution,
|
||||
fps,
|
||||
copyCodecs,
|
||||
isNewVideo,
|
||||
deleteWebTorrentFiles
|
||||
}
|
||||
}
|
||||
|
||||
private buildWebTorrentJobPayload (options: {
|
||||
videoUUID: string
|
||||
resolution: number
|
||||
fps: number
|
||||
isNewVideo: boolean
|
||||
}): NewWebTorrentResolutionTranscodingPayload {
|
||||
const { videoUUID, resolution, fps, isNewVideo } = options
|
||||
|
||||
return {
|
||||
type: 'new-resolution-to-webtorrent',
|
||||
videoUUID,
|
||||
isNewVideo,
|
||||
resolution,
|
||||
fps
|
||||
}
|
||||
}
|
||||
|
||||
private buildMergeAudioPayload (options: {
|
||||
videoUUID: string
|
||||
isNewVideo: boolean
|
||||
}): MergeAudioTranscodingPayload {
|
||||
const { videoUUID, isNewVideo } = options
|
||||
|
||||
return {
|
||||
type: 'merge-audio-to-webtorrent',
|
||||
resolution: DEFAULT_AUDIO_RESOLUTION,
|
||||
fps: VIDEO_TRANSCODING_FPS.AUDIO_MERGE,
|
||||
videoUUID,
|
||||
isNewVideo
|
||||
}
|
||||
}
|
||||
|
||||
private buildOptimizePayload (options: {
|
||||
videoUUID: string
|
||||
quickTranscode: boolean
|
||||
isNewVideo: boolean
|
||||
}): OptimizeTranscodingPayload {
|
||||
const { videoUUID, quickTranscode, isNewVideo } = options
|
||||
|
||||
return {
|
||||
type: 'optimize-to-webtorrent',
|
||||
videoUUID,
|
||||
isNewVideo,
|
||||
quickTranscode
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,189 @@
|
|||
import { computeOutputFPS } from '@server/helpers/ffmpeg'
|
||||
import { logger, loggerTagsFactory } from '@server/helpers/logger'
|
||||
import { CONFIG } from '@server/initializers/config'
|
||||
import { DEFAULT_AUDIO_RESOLUTION, VIDEO_TRANSCODING_FPS } from '@server/initializers/constants'
|
||||
import { Hooks } from '@server/lib/plugins/hooks'
|
||||
import { VODAudioMergeTranscodingJobHandler, VODHLSTranscodingJobHandler, VODWebVideoTranscodingJobHandler } from '@server/lib/runners'
|
||||
import { VideoPathManager } from '@server/lib/video-path-manager'
|
||||
import { MUserId, MVideoFile, MVideoFullLight, MVideoWithFileThumbnail } from '@server/types/models'
|
||||
import { MRunnerJob } from '@server/types/models/runners'
|
||||
import { ffprobePromise, getVideoStreamDimensionsInfo, getVideoStreamFPS, hasAudioStream, isAudioFile } from '@shared/ffmpeg'
|
||||
import { computeResolutionsToTranscode } from '../../transcoding-resolutions'
|
||||
import { AbstractJobBuilder } from './abstract-job-builder'
|
||||
|
||||
/**
|
||||
*
|
||||
* Class to build transcoding job in the local job queue
|
||||
*
|
||||
*/
|
||||
|
||||
const lTags = loggerTagsFactory('transcoding')
|
||||
|
||||
export class TranscodingRunnerJobBuilder extends AbstractJobBuilder {
|
||||
|
||||
async createOptimizeOrMergeAudioJobs (options: {
|
||||
video: MVideoFullLight
|
||||
videoFile: MVideoFile
|
||||
isNewVideo: boolean
|
||||
user: MUserId
|
||||
}) {
|
||||
const { video, videoFile, isNewVideo, user } = options
|
||||
|
||||
const mutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid)
|
||||
|
||||
try {
|
||||
await VideoPathManager.Instance.makeAvailableVideoFile(videoFile.withVideoOrPlaylist(video), async videoFilePath => {
|
||||
const probe = await ffprobePromise(videoFilePath)
|
||||
|
||||
const { resolution } = await getVideoStreamDimensionsInfo(videoFilePath, probe)
|
||||
const hasAudio = await hasAudioStream(videoFilePath, probe)
|
||||
const inputFPS = videoFile.isAudio()
|
||||
? VIDEO_TRANSCODING_FPS.AUDIO_MERGE // The first transcoding job will transcode to this FPS value
|
||||
: await getVideoStreamFPS(videoFilePath, probe)
|
||||
|
||||
const maxResolution = await isAudioFile(videoFilePath, probe)
|
||||
? DEFAULT_AUDIO_RESOLUTION
|
||||
: resolution
|
||||
|
||||
const fps = computeOutputFPS({ inputFPS, resolution: maxResolution })
|
||||
const priority = await this.getTranscodingJobPriority({ user, fallback: 0 })
|
||||
|
||||
const mainRunnerJob = videoFile.isAudio()
|
||||
? await new VODAudioMergeTranscodingJobHandler().create({ video, resolution: maxResolution, fps, isNewVideo, priority })
|
||||
: await new VODWebVideoTranscodingJobHandler().create({ video, resolution: maxResolution, fps, isNewVideo, priority })
|
||||
|
||||
if (CONFIG.TRANSCODING.HLS.ENABLED === true) {
|
||||
await new VODHLSTranscodingJobHandler().create({
|
||||
video,
|
||||
deleteWebVideoFiles: CONFIG.TRANSCODING.WEBTORRENT.ENABLED === false,
|
||||
resolution: maxResolution,
|
||||
fps,
|
||||
isNewVideo,
|
||||
dependsOnRunnerJob: mainRunnerJob,
|
||||
priority: await this.getTranscodingJobPriority({ user, fallback: 0 })
|
||||
})
|
||||
}
|
||||
|
||||
await this.buildLowerResolutionJobPayloads({
|
||||
video,
|
||||
inputVideoResolution: maxResolution,
|
||||
inputVideoFPS: inputFPS,
|
||||
hasAudio,
|
||||
isNewVideo,
|
||||
mainRunnerJob,
|
||||
user
|
||||
})
|
||||
})
|
||||
} finally {
|
||||
mutexReleaser()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async createTranscodingJobs (options: {
|
||||
transcodingType: 'hls' | 'webtorrent'
|
||||
video: MVideoFullLight
|
||||
resolutions: number[]
|
||||
isNewVideo: boolean
|
||||
user: MUserId | null
|
||||
}) {
|
||||
const { video, transcodingType, resolutions, isNewVideo, user } = options
|
||||
|
||||
const maxResolution = Math.max(...resolutions)
|
||||
const { fps: inputFPS } = await video.probeMaxQualityFile()
|
||||
const maxFPS = computeOutputFPS({ inputFPS, resolution: maxResolution })
|
||||
const priority = await this.getTranscodingJobPriority({ user, fallback: 0 })
|
||||
|
||||
const childrenResolutions = resolutions.filter(r => r !== maxResolution)
|
||||
|
||||
logger.info('Manually creating transcoding jobs for %s.', transcodingType, { childrenResolutions, maxResolution })
|
||||
|
||||
// Process the last resolution before the other ones to prevent concurrency issue
|
||||
// Because low resolutions use the biggest one as ffmpeg input
|
||||
const mainJob = transcodingType === 'hls'
|
||||
// eslint-disable-next-line max-len
|
||||
? await new VODHLSTranscodingJobHandler().create({ video, resolution: maxResolution, fps: maxFPS, isNewVideo, deleteWebVideoFiles: false, priority })
|
||||
: await new VODWebVideoTranscodingJobHandler().create({ video, resolution: maxResolution, fps: maxFPS, isNewVideo, priority })
|
||||
|
||||
for (const resolution of childrenResolutions) {
|
||||
const dependsOnRunnerJob = mainJob
|
||||
const fps = computeOutputFPS({ inputFPS, resolution: maxResolution })
|
||||
|
||||
if (transcodingType === 'hls') {
|
||||
await new VODHLSTranscodingJobHandler().create({
|
||||
video,
|
||||
resolution,
|
||||
fps,
|
||||
isNewVideo,
|
||||
deleteWebVideoFiles: false,
|
||||
dependsOnRunnerJob,
|
||||
priority: await this.getTranscodingJobPriority({ user, fallback: 0 })
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
if (transcodingType === 'webtorrent') {
|
||||
await new VODWebVideoTranscodingJobHandler().create({
|
||||
video,
|
||||
resolution,
|
||||
fps,
|
||||
isNewVideo,
|
||||
dependsOnRunnerJob,
|
||||
priority: await this.getTranscodingJobPriority({ user, fallback: 0 })
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
throw new Error('Unknown transcoding type')
|
||||
}
|
||||
}
|
||||
|
||||
private async buildLowerResolutionJobPayloads (options: {
|
||||
mainRunnerJob: MRunnerJob
|
||||
video: MVideoWithFileThumbnail
|
||||
inputVideoResolution: number
|
||||
inputVideoFPS: number
|
||||
hasAudio: boolean
|
||||
isNewVideo: boolean
|
||||
user: MUserId
|
||||
}) {
|
||||
const { video, inputVideoResolution, inputVideoFPS, isNewVideo, hasAudio, mainRunnerJob, user } = options
|
||||
|
||||
// Create transcoding jobs if there are enabled resolutions
|
||||
const resolutionsEnabled = await Hooks.wrapObject(
|
||||
computeResolutionsToTranscode({ input: inputVideoResolution, type: 'vod', includeInput: false, strictLower: true, hasAudio }),
|
||||
'filter:transcoding.auto.resolutions-to-transcode.result',
|
||||
options
|
||||
)
|
||||
|
||||
logger.debug('Lower resolutions build for %s.', video.uuid, { resolutionsEnabled, ...lTags(video.uuid) })
|
||||
|
||||
for (const resolution of resolutionsEnabled) {
|
||||
const fps = computeOutputFPS({ inputFPS: inputVideoFPS, resolution })
|
||||
|
||||
if (CONFIG.TRANSCODING.WEBTORRENT.ENABLED) {
|
||||
await new VODWebVideoTranscodingJobHandler().create({
|
||||
video,
|
||||
resolution,
|
||||
fps,
|
||||
isNewVideo,
|
||||
dependsOnRunnerJob: mainRunnerJob,
|
||||
priority: await this.getTranscodingJobPriority({ user, fallback: 0 })
|
||||
})
|
||||
}
|
||||
|
||||
if (CONFIG.TRANSCODING.HLS.ENABLED) {
|
||||
await new VODHLSTranscodingJobHandler().create({
|
||||
video,
|
||||
resolution,
|
||||
fps,
|
||||
isNewVideo,
|
||||
deleteWebVideoFiles: false,
|
||||
dependsOnRunnerJob: mainRunnerJob,
|
||||
priority: await this.getTranscodingJobPriority({ user, fallback: 0 })
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
61
server/lib/transcoding/transcoding-quick-transcode.ts
Normal file
61
server/lib/transcoding/transcoding-quick-transcode.ts
Normal file
|
@ -0,0 +1,61 @@
|
|||
import { FfprobeData } from 'fluent-ffmpeg'
|
||||
import { CONFIG } from '@server/initializers/config'
|
||||
import { VIDEO_TRANSCODING_FPS } from '@server/initializers/constants'
|
||||
import { getMaxBitrate } from '@shared/core-utils'
|
||||
import {
|
||||
ffprobePromise,
|
||||
getAudioStream,
|
||||
getMaxAudioBitrate,
|
||||
getVideoStream,
|
||||
getVideoStreamBitrate,
|
||||
getVideoStreamDimensionsInfo,
|
||||
getVideoStreamFPS
|
||||
} from '@shared/ffmpeg'
|
||||
|
||||
export async function canDoQuickTranscode (path: string, existingProbe?: FfprobeData): Promise<boolean> {
|
||||
if (CONFIG.TRANSCODING.PROFILE !== 'default') return false
|
||||
|
||||
const probe = existingProbe || await ffprobePromise(path)
|
||||
|
||||
return await canDoQuickVideoTranscode(path, probe) &&
|
||||
await canDoQuickAudioTranscode(path, probe)
|
||||
}
|
||||
|
||||
export async function canDoQuickAudioTranscode (path: string, probe?: FfprobeData): Promise<boolean> {
|
||||
const parsedAudio = await getAudioStream(path, probe)
|
||||
|
||||
if (!parsedAudio.audioStream) return true
|
||||
|
||||
if (parsedAudio.audioStream['codec_name'] !== 'aac') return false
|
||||
|
||||
const audioBitrate = parsedAudio.bitrate
|
||||
if (!audioBitrate) return false
|
||||
|
||||
const maxAudioBitrate = getMaxAudioBitrate('aac', audioBitrate)
|
||||
if (maxAudioBitrate !== -1 && audioBitrate > maxAudioBitrate) return false
|
||||
|
||||
const channelLayout = parsedAudio.audioStream['channel_layout']
|
||||
// Causes playback issues with Chrome
|
||||
if (!channelLayout || channelLayout === 'unknown' || channelLayout === 'quad') return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export async function canDoQuickVideoTranscode (path: string, probe?: FfprobeData): Promise<boolean> {
|
||||
const videoStream = await getVideoStream(path, probe)
|
||||
const fps = await getVideoStreamFPS(path, probe)
|
||||
const bitRate = await getVideoStreamBitrate(path, probe)
|
||||
const resolutionData = await getVideoStreamDimensionsInfo(path, probe)
|
||||
|
||||
// If ffprobe did not manage to guess the bitrate
|
||||
if (!bitRate) return false
|
||||
|
||||
// check video params
|
||||
if (!videoStream) return false
|
||||
if (videoStream['codec_name'] !== 'h264') return false
|
||||
if (videoStream['pix_fmt'] !== 'yuv420p') return false
|
||||
if (fps < VIDEO_TRANSCODING_FPS.MIN || fps > VIDEO_TRANSCODING_FPS.MAX) return false
|
||||
if (bitRate > getMaxBitrate({ ...resolutionData, fps })) return false
|
||||
|
||||
return true
|
||||
}
|
52
server/lib/transcoding/transcoding-resolutions.ts
Normal file
52
server/lib/transcoding/transcoding-resolutions.ts
Normal file
|
@ -0,0 +1,52 @@
|
|||
import { CONFIG } from '@server/initializers/config'
|
||||
import { toEven } from '@shared/core-utils'
|
||||
import { VideoResolution } from '@shared/models'
|
||||
|
||||
export function computeResolutionsToTranscode (options: {
|
||||
input: number
|
||||
type: 'vod' | 'live'
|
||||
includeInput: boolean
|
||||
strictLower: boolean
|
||||
hasAudio: boolean
|
||||
}) {
|
||||
const { input, type, includeInput, strictLower, hasAudio } = options
|
||||
|
||||
const configResolutions = type === 'vod'
|
||||
? CONFIG.TRANSCODING.RESOLUTIONS
|
||||
: CONFIG.LIVE.TRANSCODING.RESOLUTIONS
|
||||
|
||||
const resolutionsEnabled = new Set<number>()
|
||||
|
||||
// Put in the order we want to proceed jobs
|
||||
const availableResolutions: VideoResolution[] = [
|
||||
VideoResolution.H_NOVIDEO,
|
||||
VideoResolution.H_480P,
|
||||
VideoResolution.H_360P,
|
||||
VideoResolution.H_720P,
|
||||
VideoResolution.H_240P,
|
||||
VideoResolution.H_144P,
|
||||
VideoResolution.H_1080P,
|
||||
VideoResolution.H_1440P,
|
||||
VideoResolution.H_4K
|
||||
]
|
||||
|
||||
for (const resolution of availableResolutions) {
|
||||
// Resolution not enabled
|
||||
if (configResolutions[resolution + 'p'] !== true) continue
|
||||
// Too big resolution for input file
|
||||
if (input < resolution) continue
|
||||
// We only want lower resolutions than input file
|
||||
if (strictLower && input === resolution) continue
|
||||
// Audio resolutio but no audio in the video
|
||||
if (resolution === VideoResolution.H_NOVIDEO && !hasAudio) continue
|
||||
|
||||
resolutionsEnabled.add(resolution)
|
||||
}
|
||||
|
||||
if (includeInput) {
|
||||
// Always use an even resolution to avoid issues with ffmpeg
|
||||
resolutionsEnabled.add(toEven(input))
|
||||
}
|
||||
|
||||
return Array.from(resolutionsEnabled)
|
||||
}
|
|
@ -1,465 +0,0 @@
|
|||
import { MutexInterface } from 'async-mutex'
|
||||
import { Job } from 'bullmq'
|
||||
import { copyFile, ensureDir, move, remove, stat } from 'fs-extra'
|
||||
import { basename, extname as extnameUtil, join } from 'path'
|
||||
import { toEven } from '@server/helpers/core-utils'
|
||||
import { retryTransactionWrapper } from '@server/helpers/database-utils'
|
||||
import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
|
||||
import { sequelizeTypescript } from '@server/initializers/database'
|
||||
import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models'
|
||||
import { pick } from '@shared/core-utils'
|
||||
import { VideoResolution, VideoStorage } from '../../../shared/models/videos'
|
||||
import {
|
||||
buildFileMetadata,
|
||||
canDoQuickTranscode,
|
||||
computeResolutionsToTranscode,
|
||||
ffprobePromise,
|
||||
getVideoStreamDuration,
|
||||
getVideoStreamFPS,
|
||||
transcodeVOD,
|
||||
TranscodeVODOptions,
|
||||
TranscodeVODOptionsType
|
||||
} from '../../helpers/ffmpeg'
|
||||
import { CONFIG } from '../../initializers/config'
|
||||
import { VideoFileModel } from '../../models/video/video-file'
|
||||
import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist'
|
||||
import { updatePlaylistAfterFileChange } from '../hls'
|
||||
import { generateHLSVideoFilename, generateWebTorrentVideoFilename, getHlsResolutionPlaylistFilename } from '../paths'
|
||||
import { VideoPathManager } from '../video-path-manager'
|
||||
import { VideoTranscodingProfilesManager } from './default-transcoding-profiles'
|
||||
|
||||
/**
|
||||
*
|
||||
* Functions that run transcoding functions, update the database, cleanup files, create torrent files...
|
||||
* Mainly called by the job queue
|
||||
*
|
||||
*/
|
||||
|
||||
// Optimize the original video file and replace it. The resolution is not changed.
|
||||
async function optimizeOriginalVideofile (options: {
|
||||
video: MVideoFullLight
|
||||
inputVideoFile: MVideoFile
|
||||
job: Job
|
||||
}) {
|
||||
const { video, inputVideoFile, job } = options
|
||||
|
||||
const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
|
||||
const newExtname = '.mp4'
|
||||
|
||||
// Will be released by our transcodeVOD function once ffmpeg is ran
|
||||
const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid)
|
||||
|
||||
try {
|
||||
await video.reload()
|
||||
|
||||
const fileWithVideoOrPlaylist = inputVideoFile.withVideoOrPlaylist(video)
|
||||
|
||||
const result = await VideoPathManager.Instance.makeAvailableVideoFile(fileWithVideoOrPlaylist, async videoInputPath => {
|
||||
const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
|
||||
|
||||
const transcodeType: TranscodeVODOptionsType = await canDoQuickTranscode(videoInputPath)
|
||||
? 'quick-transcode'
|
||||
: 'video'
|
||||
|
||||
const resolution = buildOriginalFileResolution(inputVideoFile.resolution)
|
||||
|
||||
const transcodeOptions: TranscodeVODOptions = {
|
||||
type: transcodeType,
|
||||
|
||||
inputPath: videoInputPath,
|
||||
outputPath: videoTranscodedPath,
|
||||
|
||||
inputFileMutexReleaser,
|
||||
|
||||
availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
|
||||
profile: CONFIG.TRANSCODING.PROFILE,
|
||||
|
||||
resolution,
|
||||
|
||||
job
|
||||
}
|
||||
|
||||
// Could be very long!
|
||||
await transcodeVOD(transcodeOptions)
|
||||
|
||||
// Important to do this before getVideoFilename() to take in account the new filename
|
||||
inputVideoFile.resolution = resolution
|
||||
inputVideoFile.extname = newExtname
|
||||
inputVideoFile.filename = generateWebTorrentVideoFilename(resolution, newExtname)
|
||||
inputVideoFile.storage = VideoStorage.FILE_SYSTEM
|
||||
|
||||
const { videoFile } = await onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, inputVideoFile)
|
||||
await remove(videoInputPath)
|
||||
|
||||
return { transcodeType, videoFile }
|
||||
})
|
||||
|
||||
return result
|
||||
} finally {
|
||||
inputFileMutexReleaser()
|
||||
}
|
||||
}
|
||||
|
||||
// Transcode the original video file to a lower resolution compatible with WebTorrent
|
||||
async function transcodeNewWebTorrentResolution (options: {
|
||||
video: MVideoFullLight
|
||||
resolution: VideoResolution
|
||||
job: Job
|
||||
}) {
|
||||
const { video, resolution, job } = options
|
||||
|
||||
const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
|
||||
const newExtname = '.mp4'
|
||||
|
||||
const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid)
|
||||
|
||||
try {
|
||||
await video.reload()
|
||||
|
||||
const file = video.getMaxQualityFile().withVideoOrPlaylist(video)
|
||||
|
||||
const result = await VideoPathManager.Instance.makeAvailableVideoFile(file, async videoInputPath => {
|
||||
const newVideoFile = new VideoFileModel({
|
||||
resolution,
|
||||
extname: newExtname,
|
||||
filename: generateWebTorrentVideoFilename(resolution, newExtname),
|
||||
size: 0,
|
||||
videoId: video.id
|
||||
})
|
||||
|
||||
const videoTranscodedPath = join(transcodeDirectory, newVideoFile.filename)
|
||||
|
||||
const transcodeOptions = resolution === VideoResolution.H_NOVIDEO
|
||||
? {
|
||||
type: 'only-audio' as 'only-audio',
|
||||
|
||||
inputPath: videoInputPath,
|
||||
outputPath: videoTranscodedPath,
|
||||
|
||||
inputFileMutexReleaser,
|
||||
|
||||
availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
|
||||
profile: CONFIG.TRANSCODING.PROFILE,
|
||||
|
||||
resolution,
|
||||
|
||||
job
|
||||
}
|
||||
: {
|
||||
type: 'video' as 'video',
|
||||
inputPath: videoInputPath,
|
||||
outputPath: videoTranscodedPath,
|
||||
|
||||
inputFileMutexReleaser,
|
||||
|
||||
availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
|
||||
profile: CONFIG.TRANSCODING.PROFILE,
|
||||
|
||||
resolution,
|
||||
|
||||
job
|
||||
}
|
||||
|
||||
await transcodeVOD(transcodeOptions)
|
||||
|
||||
return onWebTorrentVideoFileTranscoding(video, newVideoFile, videoTranscodedPath, newVideoFile)
|
||||
})
|
||||
|
||||
return result
|
||||
} finally {
|
||||
inputFileMutexReleaser()
|
||||
}
|
||||
}
|
||||
|
||||
// Merge an image with an audio file to create a video
|
||||
async function mergeAudioVideofile (options: {
|
||||
video: MVideoFullLight
|
||||
resolution: VideoResolution
|
||||
job: Job
|
||||
}) {
|
||||
const { video, resolution, job } = options
|
||||
|
||||
const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
|
||||
const newExtname = '.mp4'
|
||||
|
||||
const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid)
|
||||
|
||||
try {
|
||||
await video.reload()
|
||||
|
||||
const inputVideoFile = video.getMinQualityFile()
|
||||
|
||||
const fileWithVideoOrPlaylist = inputVideoFile.withVideoOrPlaylist(video)
|
||||
|
||||
const result = await VideoPathManager.Instance.makeAvailableVideoFile(fileWithVideoOrPlaylist, async audioInputPath => {
|
||||
const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
|
||||
|
||||
// If the user updates the video preview during transcoding
|
||||
const previewPath = video.getPreview().getPath()
|
||||
const tmpPreviewPath = join(CONFIG.STORAGE.TMP_DIR, basename(previewPath))
|
||||
await copyFile(previewPath, tmpPreviewPath)
|
||||
|
||||
const transcodeOptions = {
|
||||
type: 'merge-audio' as 'merge-audio',
|
||||
|
||||
inputPath: tmpPreviewPath,
|
||||
outputPath: videoTranscodedPath,
|
||||
|
||||
inputFileMutexReleaser,
|
||||
|
||||
availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
|
||||
profile: CONFIG.TRANSCODING.PROFILE,
|
||||
|
||||
audioPath: audioInputPath,
|
||||
resolution,
|
||||
|
||||
job
|
||||
}
|
||||
|
||||
try {
|
||||
await transcodeVOD(transcodeOptions)
|
||||
|
||||
await remove(audioInputPath)
|
||||
await remove(tmpPreviewPath)
|
||||
} catch (err) {
|
||||
await remove(tmpPreviewPath)
|
||||
throw err
|
||||
}
|
||||
|
||||
// Important to do this before getVideoFilename() to take in account the new file extension
|
||||
inputVideoFile.extname = newExtname
|
||||
inputVideoFile.resolution = resolution
|
||||
inputVideoFile.filename = generateWebTorrentVideoFilename(inputVideoFile.resolution, newExtname)
|
||||
|
||||
// ffmpeg generated a new video file, so update the video duration
|
||||
// See https://trac.ffmpeg.org/ticket/5456
|
||||
video.duration = await getVideoStreamDuration(videoTranscodedPath)
|
||||
await video.save()
|
||||
|
||||
return onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, inputVideoFile)
|
||||
})
|
||||
|
||||
return result
|
||||
} finally {
|
||||
inputFileMutexReleaser()
|
||||
}
|
||||
}
|
||||
|
||||
// Concat TS segments from a live video to a fragmented mp4 HLS playlist
|
||||
async function generateHlsPlaylistResolutionFromTS (options: {
|
||||
video: MVideo
|
||||
concatenatedTsFilePath: string
|
||||
resolution: VideoResolution
|
||||
isAAC: boolean
|
||||
inputFileMutexReleaser: MutexInterface.Releaser
|
||||
}) {
|
||||
return generateHlsPlaylistCommon({
|
||||
type: 'hls-from-ts' as 'hls-from-ts',
|
||||
inputPath: options.concatenatedTsFilePath,
|
||||
|
||||
...pick(options, [ 'video', 'resolution', 'inputFileMutexReleaser', 'isAAC' ])
|
||||
})
|
||||
}
|
||||
|
||||
// Generate an HLS playlist from an input file, and update the master playlist
|
||||
function generateHlsPlaylistResolution (options: {
|
||||
video: MVideo
|
||||
videoInputPath: string
|
||||
resolution: VideoResolution
|
||||
copyCodecs: boolean
|
||||
inputFileMutexReleaser: MutexInterface.Releaser
|
||||
job?: Job
|
||||
}) {
|
||||
return generateHlsPlaylistCommon({
|
||||
type: 'hls' as 'hls',
|
||||
inputPath: options.videoInputPath,
|
||||
|
||||
...pick(options, [ 'video', 'resolution', 'copyCodecs', 'inputFileMutexReleaser', 'job' ])
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
generateHlsPlaylistResolution,
|
||||
generateHlsPlaylistResolutionFromTS,
|
||||
optimizeOriginalVideofile,
|
||||
transcodeNewWebTorrentResolution,
|
||||
mergeAudioVideofile
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function onWebTorrentVideoFileTranscoding (
|
||||
video: MVideoFullLight,
|
||||
videoFile: MVideoFile,
|
||||
transcodingPath: string,
|
||||
newVideoFile: MVideoFile
|
||||
) {
|
||||
const mutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid)
|
||||
|
||||
try {
|
||||
await video.reload()
|
||||
|
||||
const outputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, newVideoFile)
|
||||
|
||||
const stats = await stat(transcodingPath)
|
||||
|
||||
const probe = await ffprobePromise(transcodingPath)
|
||||
const fps = await getVideoStreamFPS(transcodingPath, probe)
|
||||
const metadata = await buildFileMetadata(transcodingPath, probe)
|
||||
|
||||
await move(transcodingPath, outputPath, { overwrite: true })
|
||||
|
||||
videoFile.size = stats.size
|
||||
videoFile.fps = fps
|
||||
videoFile.metadata = metadata
|
||||
|
||||
await createTorrentAndSetInfoHash(video, videoFile)
|
||||
|
||||
const oldFile = await VideoFileModel.loadWebTorrentFile({ videoId: video.id, fps: videoFile.fps, resolution: videoFile.resolution })
|
||||
if (oldFile) await video.removeWebTorrentFile(oldFile)
|
||||
|
||||
await VideoFileModel.customUpsert(videoFile, 'video', undefined)
|
||||
video.VideoFiles = await video.$get('VideoFiles')
|
||||
|
||||
return { video, videoFile }
|
||||
} finally {
|
||||
mutexReleaser()
|
||||
}
|
||||
}
|
||||
|
||||
async function generateHlsPlaylistCommon (options: {
|
||||
type: 'hls' | 'hls-from-ts'
|
||||
video: MVideo
|
||||
inputPath: string
|
||||
resolution: VideoResolution
|
||||
|
||||
inputFileMutexReleaser: MutexInterface.Releaser
|
||||
|
||||
copyCodecs?: boolean
|
||||
isAAC?: boolean
|
||||
|
||||
job?: Job
|
||||
}) {
|
||||
const { type, video, inputPath, resolution, copyCodecs, isAAC, job, inputFileMutexReleaser } = options
|
||||
const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
|
||||
|
||||
const videoTranscodedBasePath = join(transcodeDirectory, type)
|
||||
await ensureDir(videoTranscodedBasePath)
|
||||
|
||||
const videoFilename = generateHLSVideoFilename(resolution)
|
||||
const resolutionPlaylistFilename = getHlsResolutionPlaylistFilename(videoFilename)
|
||||
const resolutionPlaylistFileTranscodePath = join(videoTranscodedBasePath, resolutionPlaylistFilename)
|
||||
|
||||
const transcodeOptions = {
|
||||
type,
|
||||
|
||||
inputPath,
|
||||
outputPath: resolutionPlaylistFileTranscodePath,
|
||||
|
||||
availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
|
||||
profile: CONFIG.TRANSCODING.PROFILE,
|
||||
|
||||
resolution,
|
||||
copyCodecs,
|
||||
|
||||
isAAC,
|
||||
|
||||
inputFileMutexReleaser,
|
||||
|
||||
hlsPlaylist: {
|
||||
videoFilename
|
||||
},
|
||||
|
||||
job
|
||||
}
|
||||
|
||||
await transcodeVOD(transcodeOptions)
|
||||
|
||||
// Create or update the playlist
|
||||
const playlist = await retryTransactionWrapper(() => {
|
||||
return sequelizeTypescript.transaction(async transaction => {
|
||||
return VideoStreamingPlaylistModel.loadOrGenerate(video, transaction)
|
||||
})
|
||||
})
|
||||
|
||||
const newVideoFile = new VideoFileModel({
|
||||
resolution,
|
||||
extname: extnameUtil(videoFilename),
|
||||
size: 0,
|
||||
filename: videoFilename,
|
||||
fps: -1,
|
||||
videoStreamingPlaylistId: playlist.id
|
||||
})
|
||||
|
||||
const mutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid)
|
||||
|
||||
try {
|
||||
// VOD transcoding is a long task, refresh video attributes
|
||||
await video.reload()
|
||||
|
||||
const videoFilePath = VideoPathManager.Instance.getFSVideoFileOutputPath(playlist, newVideoFile)
|
||||
await ensureDir(VideoPathManager.Instance.getFSHLSOutputPath(video))
|
||||
|
||||
// Move playlist file
|
||||
const resolutionPlaylistPath = VideoPathManager.Instance.getFSHLSOutputPath(video, resolutionPlaylistFilename)
|
||||
await move(resolutionPlaylistFileTranscodePath, resolutionPlaylistPath, { overwrite: true })
|
||||
// Move video file
|
||||
await move(join(videoTranscodedBasePath, videoFilename), videoFilePath, { overwrite: true })
|
||||
|
||||
// Update video duration if it was not set (in case of a live for example)
|
||||
if (!video.duration) {
|
||||
video.duration = await getVideoStreamDuration(videoFilePath)
|
||||
await video.save()
|
||||
}
|
||||
|
||||
const stats = await stat(videoFilePath)
|
||||
|
||||
newVideoFile.size = stats.size
|
||||
newVideoFile.fps = await getVideoStreamFPS(videoFilePath)
|
||||
newVideoFile.metadata = await buildFileMetadata(videoFilePath)
|
||||
|
||||
await createTorrentAndSetInfoHash(playlist, newVideoFile)
|
||||
|
||||
const oldFile = await VideoFileModel.loadHLSFile({
|
||||
playlistId: playlist.id,
|
||||
fps: newVideoFile.fps,
|
||||
resolution: newVideoFile.resolution
|
||||
})
|
||||
|
||||
if (oldFile) {
|
||||
await video.removeStreamingPlaylistVideoFile(playlist, oldFile)
|
||||
await oldFile.destroy()
|
||||
}
|
||||
|
||||
const savedVideoFile = await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined)
|
||||
|
||||
await updatePlaylistAfterFileChange(video, playlist)
|
||||
|
||||
return { resolutionPlaylistPath, videoFile: savedVideoFile }
|
||||
} finally {
|
||||
mutexReleaser()
|
||||
}
|
||||
}
|
||||
|
||||
function buildOriginalFileResolution (inputResolution: number) {
|
||||
if (CONFIG.TRANSCODING.ALWAYS_TRANSCODE_ORIGINAL_RESOLUTION === true) {
|
||||
return toEven(inputResolution)
|
||||
}
|
||||
|
||||
const resolutions = computeResolutionsToTranscode({
|
||||
input: inputResolution,
|
||||
type: 'vod',
|
||||
includeInput: false,
|
||||
strictLower: false,
|
||||
// We don't really care about the audio resolution in this context
|
||||
hasAudio: true
|
||||
})
|
||||
|
||||
if (resolutions.length === 0) {
|
||||
return toEven(inputResolution)
|
||||
}
|
||||
|
||||
return Math.max(...resolutions)
|
||||
}
|
273
server/lib/transcoding/web-transcoding.ts
Normal file
273
server/lib/transcoding/web-transcoding.ts
Normal file
|
@ -0,0 +1,273 @@
|
|||
import { Job } from 'bullmq'
|
||||
import { copyFile, move, remove, stat } from 'fs-extra'
|
||||
import { basename, join } from 'path'
|
||||
import { computeOutputFPS } from '@server/helpers/ffmpeg'
|
||||
import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
|
||||
import { MVideoFile, MVideoFullLight } from '@server/types/models'
|
||||
import { toEven } from '@shared/core-utils'
|
||||
import { ffprobePromise, getVideoStreamDuration, getVideoStreamFPS, TranscodeVODOptionsType } from '@shared/ffmpeg'
|
||||
import { VideoResolution, VideoStorage } from '@shared/models'
|
||||
import { CONFIG } from '../../initializers/config'
|
||||
import { VideoFileModel } from '../../models/video/video-file'
|
||||
import { generateWebTorrentVideoFilename } from '../paths'
|
||||
import { buildFileMetadata } from '../video-file'
|
||||
import { VideoPathManager } from '../video-path-manager'
|
||||
import { buildFFmpegVOD } from './shared'
|
||||
import { computeResolutionsToTranscode } from './transcoding-resolutions'
|
||||
|
||||
// Optimize the original video file and replace it. The resolution is not changed.
|
||||
export async function optimizeOriginalVideofile (options: {
|
||||
video: MVideoFullLight
|
||||
inputVideoFile: MVideoFile
|
||||
quickTranscode: boolean
|
||||
job: Job
|
||||
}) {
|
||||
const { video, inputVideoFile, quickTranscode, job } = options
|
||||
|
||||
const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
|
||||
const newExtname = '.mp4'
|
||||
|
||||
// Will be released by our transcodeVOD function once ffmpeg is ran
|
||||
const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid)
|
||||
|
||||
try {
|
||||
await video.reload()
|
||||
|
||||
const fileWithVideoOrPlaylist = inputVideoFile.withVideoOrPlaylist(video)
|
||||
|
||||
const result = await VideoPathManager.Instance.makeAvailableVideoFile(fileWithVideoOrPlaylist, async videoInputPath => {
|
||||
const videoOutputPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
|
||||
|
||||
const transcodeType: TranscodeVODOptionsType = quickTranscode
|
||||
? 'quick-transcode'
|
||||
: 'video'
|
||||
|
||||
const resolution = buildOriginalFileResolution(inputVideoFile.resolution)
|
||||
const fps = computeOutputFPS({ inputFPS: inputVideoFile.fps, resolution })
|
||||
|
||||
// Could be very long!
|
||||
await buildFFmpegVOD(job).transcode({
|
||||
type: transcodeType,
|
||||
|
||||
inputPath: videoInputPath,
|
||||
outputPath: videoOutputPath,
|
||||
|
||||
inputFileMutexReleaser,
|
||||
|
||||
resolution,
|
||||
fps
|
||||
})
|
||||
|
||||
// Important to do this before getVideoFilename() to take in account the new filename
|
||||
inputVideoFile.resolution = resolution
|
||||
inputVideoFile.extname = newExtname
|
||||
inputVideoFile.filename = generateWebTorrentVideoFilename(resolution, newExtname)
|
||||
inputVideoFile.storage = VideoStorage.FILE_SYSTEM
|
||||
|
||||
const { videoFile } = await onWebTorrentVideoFileTranscoding({
|
||||
video,
|
||||
videoFile: inputVideoFile,
|
||||
videoOutputPath
|
||||
})
|
||||
|
||||
await remove(videoInputPath)
|
||||
|
||||
return { transcodeType, videoFile }
|
||||
})
|
||||
|
||||
return result
|
||||
} finally {
|
||||
inputFileMutexReleaser()
|
||||
}
|
||||
}
|
||||
|
||||
// Transcode the original video file to a lower resolution compatible with WebTorrent
|
||||
export async function transcodeNewWebTorrentResolution (options: {
|
||||
video: MVideoFullLight
|
||||
resolution: VideoResolution
|
||||
fps: number
|
||||
job: Job
|
||||
}) {
|
||||
const { video, resolution, fps, job } = options
|
||||
|
||||
const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
|
||||
const newExtname = '.mp4'
|
||||
|
||||
const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid)
|
||||
|
||||
try {
|
||||
await video.reload()
|
||||
|
||||
const file = video.getMaxQualityFile().withVideoOrPlaylist(video)
|
||||
|
||||
const result = await VideoPathManager.Instance.makeAvailableVideoFile(file, async videoInputPath => {
|
||||
const newVideoFile = new VideoFileModel({
|
||||
resolution,
|
||||
extname: newExtname,
|
||||
filename: generateWebTorrentVideoFilename(resolution, newExtname),
|
||||
size: 0,
|
||||
videoId: video.id
|
||||
})
|
||||
|
||||
const videoOutputPath = join(transcodeDirectory, newVideoFile.filename)
|
||||
|
||||
const transcodeOptions = {
|
||||
type: 'video' as 'video',
|
||||
|
||||
inputPath: videoInputPath,
|
||||
outputPath: videoOutputPath,
|
||||
|
||||
inputFileMutexReleaser,
|
||||
|
||||
resolution,
|
||||
fps
|
||||
}
|
||||
|
||||
await buildFFmpegVOD(job).transcode(transcodeOptions)
|
||||
|
||||
return onWebTorrentVideoFileTranscoding({ video, videoFile: newVideoFile, videoOutputPath })
|
||||
})
|
||||
|
||||
return result
|
||||
} finally {
|
||||
inputFileMutexReleaser()
|
||||
}
|
||||
}
|
||||
|
||||
// Merge an image with an audio file to create a video
|
||||
export async function mergeAudioVideofile (options: {
|
||||
video: MVideoFullLight
|
||||
resolution: VideoResolution
|
||||
fps: number
|
||||
job: Job
|
||||
}) {
|
||||
const { video, resolution, fps, job } = options
|
||||
|
||||
const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
|
||||
const newExtname = '.mp4'
|
||||
|
||||
const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid)
|
||||
|
||||
try {
|
||||
await video.reload()
|
||||
|
||||
const inputVideoFile = video.getMinQualityFile()
|
||||
|
||||
const fileWithVideoOrPlaylist = inputVideoFile.withVideoOrPlaylist(video)
|
||||
|
||||
const result = await VideoPathManager.Instance.makeAvailableVideoFile(fileWithVideoOrPlaylist, async audioInputPath => {
|
||||
const videoOutputPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
|
||||
|
||||
// If the user updates the video preview during transcoding
|
||||
const previewPath = video.getPreview().getPath()
|
||||
const tmpPreviewPath = join(CONFIG.STORAGE.TMP_DIR, basename(previewPath))
|
||||
await copyFile(previewPath, tmpPreviewPath)
|
||||
|
||||
const transcodeOptions = {
|
||||
type: 'merge-audio' as 'merge-audio',
|
||||
|
||||
inputPath: tmpPreviewPath,
|
||||
outputPath: videoOutputPath,
|
||||
|
||||
inputFileMutexReleaser,
|
||||
|
||||
audioPath: audioInputPath,
|
||||
resolution,
|
||||
fps
|
||||
}
|
||||
|
||||
try {
|
||||
await buildFFmpegVOD(job).transcode(transcodeOptions)
|
||||
|
||||
await remove(audioInputPath)
|
||||
await remove(tmpPreviewPath)
|
||||
} catch (err) {
|
||||
await remove(tmpPreviewPath)
|
||||
throw err
|
||||
}
|
||||
|
||||
// Important to do this before getVideoFilename() to take in account the new file extension
|
||||
inputVideoFile.extname = newExtname
|
||||
inputVideoFile.resolution = resolution
|
||||
inputVideoFile.filename = generateWebTorrentVideoFilename(inputVideoFile.resolution, newExtname)
|
||||
|
||||
// ffmpeg generated a new video file, so update the video duration
|
||||
// See https://trac.ffmpeg.org/ticket/5456
|
||||
video.duration = await getVideoStreamDuration(videoOutputPath)
|
||||
await video.save()
|
||||
|
||||
return onWebTorrentVideoFileTranscoding({
|
||||
video,
|
||||
videoFile: inputVideoFile,
|
||||
videoOutputPath
|
||||
})
|
||||
})
|
||||
|
||||
return result
|
||||
} finally {
|
||||
inputFileMutexReleaser()
|
||||
}
|
||||
}
|
||||
|
||||
export async function onWebTorrentVideoFileTranscoding (options: {
|
||||
video: MVideoFullLight
|
||||
videoFile: MVideoFile
|
||||
videoOutputPath: string
|
||||
}) {
|
||||
const { video, videoFile, videoOutputPath } = options
|
||||
|
||||
const mutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid)
|
||||
|
||||
try {
|
||||
await video.reload()
|
||||
|
||||
const outputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, videoFile)
|
||||
|
||||
const stats = await stat(videoOutputPath)
|
||||
|
||||
const probe = await ffprobePromise(videoOutputPath)
|
||||
const fps = await getVideoStreamFPS(videoOutputPath, probe)
|
||||
const metadata = await buildFileMetadata(videoOutputPath, probe)
|
||||
|
||||
await move(videoOutputPath, outputPath, { overwrite: true })
|
||||
|
||||
videoFile.size = stats.size
|
||||
videoFile.fps = fps
|
||||
videoFile.metadata = metadata
|
||||
|
||||
await createTorrentAndSetInfoHash(video, videoFile)
|
||||
|
||||
const oldFile = await VideoFileModel.loadWebTorrentFile({ videoId: video.id, fps: videoFile.fps, resolution: videoFile.resolution })
|
||||
if (oldFile) await video.removeWebTorrentFile(oldFile)
|
||||
|
||||
await VideoFileModel.customUpsert(videoFile, 'video', undefined)
|
||||
video.VideoFiles = await video.$get('VideoFiles')
|
||||
|
||||
return { video, videoFile }
|
||||
} finally {
|
||||
mutexReleaser()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function buildOriginalFileResolution (inputResolution: number) {
|
||||
if (CONFIG.TRANSCODING.ALWAYS_TRANSCODE_ORIGINAL_RESOLUTION === true) {
|
||||
return toEven(inputResolution)
|
||||
}
|
||||
|
||||
const resolutions = computeResolutionsToTranscode({
|
||||
input: inputResolution,
|
||||
type: 'vod',
|
||||
includeInput: false,
|
||||
strictLower: false,
|
||||
// We don't really care about the audio resolution in this context
|
||||
hasAudio: true
|
||||
})
|
||||
|
||||
if (resolutions.length === 0) {
|
||||
return toEven(inputResolution)
|
||||
}
|
||||
|
||||
return Math.max(...resolutions)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue