mirror of https://github.com/Chocobozzz/PeerTube
Support progress for ffmpeg tasks
parent
d44cdcd766
commit
3b01f4c0ac
|
@ -40,7 +40,8 @@
|
||||||
<th style="width: 40px"></th>
|
<th style="width: 40px"></th>
|
||||||
<th style="width: calc(100% - 390px)" class="job-id" i18n>ID</th>
|
<th style="width: calc(100% - 390px)" class="job-id" i18n>ID</th>
|
||||||
<th style="width: 200px" class="job-type" i18n>Type</th>
|
<th style="width: 200px" class="job-type" i18n>Type</th>
|
||||||
<th style="width: 200px" class="job-type" i18n *ngIf="jobState === 'all'">State</th>
|
<th style="width: 200px" class="job-state" i18n *ngIf="jobState === 'all'">State</th>
|
||||||
|
<th style="width: 100px" class="job-progress" i18n *ngIf="hasProgress()">Progress</th>
|
||||||
<th style="width: 150px" class="job-date" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
|
<th style="width: 150px" class="job-date" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
@ -55,9 +56,15 @@
|
||||||
|
|
||||||
<td class="job-id c-hand" [pRowToggler]="job" [title]="job.id">{{ job.id }}</td>
|
<td class="job-id c-hand" [pRowToggler]="job" [title]="job.id">{{ job.id }}</td>
|
||||||
<td class="job-type c-hand" [pRowToggler]="job">{{ job.type }}</td>
|
<td class="job-type c-hand" [pRowToggler]="job">{{ job.type }}</td>
|
||||||
<td class="job-type c-hand" [pRowToggler]="job" *ngIf="jobState === 'all'">
|
|
||||||
|
<td class="job-state c-hand" [pRowToggler]="job" *ngIf="jobState === 'all'">
|
||||||
<span class="badge" [ngClass]="getJobStateClass(job.state)">{{ job.state }}</span>
|
<span class="badge" [ngClass]="getJobStateClass(job.state)">{{ job.state }}</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
<td class="job-state" [pRowToggler]="job" *ngIf="hasProgress()">
|
||||||
|
{{ getProgress(job) }}
|
||||||
|
</td>
|
||||||
|
|
||||||
<td class="job-date c-hand" [pRowToggler]="job">{{ job.createdAt | date: 'short' }}</td>
|
<td class="job-date c-hand" [pRowToggler]="job">{{ job.createdAt | date: 'short' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
|
@ -9,7 +9,8 @@
|
||||||
max-width: 30vw !important;
|
max-width: 30vw !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.job-type {
|
.job-type,
|
||||||
|
.job-state {
|
||||||
width: 150px !important;
|
width: 150px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -83,6 +83,16 @@ export class JobsComponent extends RestTable implements OnInit {
|
||||||
this.saveJobStateAndType()
|
this.saveJobStateAndType()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasProgress () {
|
||||||
|
return this.jobType === 'all' || this.jobType === 'video-transcoding'
|
||||||
|
}
|
||||||
|
|
||||||
|
getProgress (job: Job) {
|
||||||
|
if (job.state === 'active') return job.progress + '%'
|
||||||
|
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
protected loadData () {
|
protected loadData () {
|
||||||
let jobState = this.jobState as JobState
|
let jobState = this.jobState as JobState
|
||||||
if (this.jobState === 'all') jobState = null
|
if (this.jobState === 'all') jobState = null
|
||||||
|
|
|
@ -52,28 +52,23 @@ async function listJobs (req: express.Request, res: express.Response) {
|
||||||
|
|
||||||
const result: ResultList<Job> = {
|
const result: ResultList<Job> = {
|
||||||
total,
|
total,
|
||||||
data: state
|
data: await Promise.all(jobs.map(j => formatJob(j, state)))
|
||||||
? jobs.map(j => formatJob(j, state))
|
|
||||||
: await Promise.all(jobs.map(j => formatJobWithUnknownState(j)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.json(result)
|
return res.json(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function formatJobWithUnknownState (job: any) {
|
async function formatJob (job: any, state?: JobState): Promise<Job> {
|
||||||
return formatJob(job, await job.getState())
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatJob (job: any, state: JobState): Job {
|
|
||||||
const error = isArray(job.stacktrace) && job.stacktrace.length !== 0
|
const error = isArray(job.stacktrace) && job.stacktrace.length !== 0
|
||||||
? job.stacktrace[0]
|
? job.stacktrace[0]
|
||||||
: null
|
: null
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: job.id,
|
id: job.id,
|
||||||
state: state,
|
state: state || await job.getState(),
|
||||||
type: job.queue.name as JobType,
|
type: job.queue.name as JobType,
|
||||||
data: job.data,
|
data: job.data,
|
||||||
|
progress: await job.progress(),
|
||||||
error,
|
error,
|
||||||
createdAt: new Date(job.timestamp),
|
createdAt: new Date(job.timestamp),
|
||||||
finishedOn: new Date(job.finishedOn),
|
finishedOn: new Date(job.finishedOn),
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { Job } from 'bull'
|
||||||
import * as ffmpeg from 'fluent-ffmpeg'
|
import * as ffmpeg from 'fluent-ffmpeg'
|
||||||
import { readFile, remove, writeFile } from 'fs-extra'
|
import { readFile, remove, writeFile } from 'fs-extra'
|
||||||
import { dirname, join } from 'path'
|
import { dirname, join } from 'path'
|
||||||
|
@ -124,6 +125,8 @@ interface BaseTranscodeOptions {
|
||||||
resolution: VideoResolution
|
resolution: VideoResolution
|
||||||
|
|
||||||
isPortraitMode?: boolean
|
isPortraitMode?: boolean
|
||||||
|
|
||||||
|
job?: Job
|
||||||
}
|
}
|
||||||
|
|
||||||
interface HLSTranscodeOptions extends BaseTranscodeOptions {
|
interface HLSTranscodeOptions extends BaseTranscodeOptions {
|
||||||
|
@ -188,7 +191,7 @@ async function transcode (options: TranscodeOptions) {
|
||||||
|
|
||||||
command = await builders[options.type](command, options)
|
command = await builders[options.type](command, options)
|
||||||
|
|
||||||
await runCommand(command)
|
await runCommand(command, options.job)
|
||||||
|
|
||||||
await fixHLSPlaylistIfNeeded(options)
|
await fixHLSPlaylistIfNeeded(options)
|
||||||
}
|
}
|
||||||
|
@ -611,11 +614,9 @@ function getFFmpeg (input: string, type: 'live' | 'vod') {
|
||||||
return command
|
return command
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runCommand (command: ffmpeg.FfmpegCommand, onEnd?: Function) {
|
async function runCommand (command: ffmpeg.FfmpegCommand, job?: Job) {
|
||||||
return new Promise<void>((res, rej) => {
|
return new Promise<void>((res, rej) => {
|
||||||
command.on('error', (err, stdout, stderr) => {
|
command.on('error', (err, stdout, stderr) => {
|
||||||
if (onEnd) onEnd()
|
|
||||||
|
|
||||||
logger.error('Error in transcoding job.', { stdout, stderr })
|
logger.error('Error in transcoding job.', { stdout, stderr })
|
||||||
rej(err)
|
rej(err)
|
||||||
})
|
})
|
||||||
|
@ -623,11 +624,18 @@ async function runCommand (command: ffmpeg.FfmpegCommand, onEnd?: Function) {
|
||||||
command.on('end', (stdout, stderr) => {
|
command.on('end', (stdout, stderr) => {
|
||||||
logger.debug('FFmpeg command ended.', { stdout, stderr })
|
logger.debug('FFmpeg command ended.', { stdout, stderr })
|
||||||
|
|
||||||
if (onEnd) onEnd()
|
|
||||||
|
|
||||||
res()
|
res()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (job) {
|
||||||
|
command.on('progress', progress => {
|
||||||
|
if (!progress.percent) return
|
||||||
|
|
||||||
|
job.progress(Math.round(progress.percent))
|
||||||
|
.catch(err => logger.warn('Cannot set ffmpeg job progress.', { err }))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
command.run()
|
command.run()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,20 +44,21 @@ async function processVideoTranscoding (job: Bull.Job) {
|
||||||
videoInputPath,
|
videoInputPath,
|
||||||
resolution: payload.resolution,
|
resolution: payload.resolution,
|
||||||
copyCodecs: payload.copyCodecs,
|
copyCodecs: payload.copyCodecs,
|
||||||
isPortraitMode: payload.isPortraitMode || false
|
isPortraitMode: payload.isPortraitMode || false,
|
||||||
|
job
|
||||||
})
|
})
|
||||||
|
|
||||||
await retryTransactionWrapper(onHlsPlaylistGenerationSuccess, video)
|
await retryTransactionWrapper(onHlsPlaylistGenerationSuccess, video)
|
||||||
} else if (payload.type === 'new-resolution') {
|
} else if (payload.type === 'new-resolution') {
|
||||||
await transcodeNewResolution(video, payload.resolution, payload.isPortraitMode || false)
|
await transcodeNewResolution(video, payload.resolution, payload.isPortraitMode || false, job)
|
||||||
|
|
||||||
await retryTransactionWrapper(publishNewResolutionIfNeeded, video, payload)
|
await retryTransactionWrapper(publishNewResolutionIfNeeded, video, payload)
|
||||||
} else if (payload.type === 'merge-audio') {
|
} else if (payload.type === 'merge-audio') {
|
||||||
await mergeAudioVideofile(video, payload.resolution)
|
await mergeAudioVideofile(video, payload.resolution, job)
|
||||||
|
|
||||||
await retryTransactionWrapper(publishNewResolutionIfNeeded, video, payload)
|
await retryTransactionWrapper(publishNewResolutionIfNeeded, video, payload)
|
||||||
} else {
|
} else {
|
||||||
const transcodeType = await optimizeOriginalVideofile(video)
|
const transcodeType = await optimizeOriginalVideofile(video, video.getMaxQualityFile(), job)
|
||||||
|
|
||||||
await retryTransactionWrapper(onVideoFileOptimizerSuccess, video, payload, transcodeType)
|
await retryTransactionWrapper(onVideoFileOptimizerSuccess, video, payload, transcodeType)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { Job } from 'bull'
|
||||||
import { copyFile, ensureDir, move, remove, stat } from 'fs-extra'
|
import { copyFile, ensureDir, move, remove, stat } from 'fs-extra'
|
||||||
import { basename, extname as extnameUtil, join } from 'path'
|
import { basename, extname as extnameUtil, join } from 'path'
|
||||||
import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
|
import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
|
||||||
|
@ -23,11 +24,10 @@ import { availableEncoders } from './video-transcoding-profiles'
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Optimize the original video file and replace it. The resolution is not changed.
|
// Optimize the original video file and replace it. The resolution is not changed.
|
||||||
async function optimizeOriginalVideofile (video: MVideoWithFile, inputVideoFileArg?: MVideoFile) {
|
async function optimizeOriginalVideofile (video: MVideoWithFile, inputVideoFile: MVideoFile, job?: Job) {
|
||||||
const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
|
const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
|
||||||
const newExtname = '.mp4'
|
const newExtname = '.mp4'
|
||||||
|
|
||||||
const inputVideoFile = inputVideoFileArg || video.getMaxQualityFile()
|
|
||||||
const videoInputPath = getVideoFilePath(video, inputVideoFile)
|
const videoInputPath = getVideoFilePath(video, inputVideoFile)
|
||||||
const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
|
const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
|
||||||
|
|
||||||
|
@ -44,7 +44,9 @@ async function optimizeOriginalVideofile (video: MVideoWithFile, inputVideoFileA
|
||||||
availableEncoders,
|
availableEncoders,
|
||||||
profile: 'default',
|
profile: 'default',
|
||||||
|
|
||||||
resolution: inputVideoFile.resolution
|
resolution: inputVideoFile.resolution,
|
||||||
|
|
||||||
|
job
|
||||||
}
|
}
|
||||||
|
|
||||||
// Could be very long!
|
// Could be very long!
|
||||||
|
@ -70,7 +72,7 @@ async function optimizeOriginalVideofile (video: MVideoWithFile, inputVideoFileA
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transcode the original video file to a lower resolution.
|
// Transcode the original video file to a lower resolution.
|
||||||
async function transcodeNewResolution (video: MVideoWithFile, resolution: VideoResolution, isPortrait: boolean) {
|
async function transcodeNewResolution (video: MVideoWithFile, resolution: VideoResolution, isPortrait: boolean, job: Job) {
|
||||||
const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
|
const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
|
||||||
const extname = '.mp4'
|
const extname = '.mp4'
|
||||||
|
|
||||||
|
@ -96,7 +98,9 @@ async function transcodeNewResolution (video: MVideoWithFile, resolution: VideoR
|
||||||
availableEncoders,
|
availableEncoders,
|
||||||
profile: 'default',
|
profile: 'default',
|
||||||
|
|
||||||
resolution
|
resolution,
|
||||||
|
|
||||||
|
job
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
type: 'video' as 'video',
|
type: 'video' as 'video',
|
||||||
|
@ -107,7 +111,9 @@ async function transcodeNewResolution (video: MVideoWithFile, resolution: VideoR
|
||||||
profile: 'default',
|
profile: 'default',
|
||||||
|
|
||||||
resolution,
|
resolution,
|
||||||
isPortraitMode: isPortrait
|
isPortraitMode: isPortrait,
|
||||||
|
|
||||||
|
job
|
||||||
}
|
}
|
||||||
|
|
||||||
await transcode(transcodeOptions)
|
await transcode(transcodeOptions)
|
||||||
|
@ -116,7 +122,7 @@ async function transcodeNewResolution (video: MVideoWithFile, resolution: VideoR
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge an image with an audio file to create a video
|
// Merge an image with an audio file to create a video
|
||||||
async function mergeAudioVideofile (video: MVideoWithAllFiles, resolution: VideoResolution) {
|
async function mergeAudioVideofile (video: MVideoWithAllFiles, resolution: VideoResolution, job: Job) {
|
||||||
const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
|
const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
|
||||||
const newExtname = '.mp4'
|
const newExtname = '.mp4'
|
||||||
|
|
||||||
|
@ -140,7 +146,9 @@ async function mergeAudioVideofile (video: MVideoWithAllFiles, resolution: Video
|
||||||
profile: 'default',
|
profile: 'default',
|
||||||
|
|
||||||
audioPath: audioInputPath,
|
audioPath: audioInputPath,
|
||||||
resolution
|
resolution,
|
||||||
|
|
||||||
|
job
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -190,6 +198,7 @@ function generateHlsPlaylist (options: {
|
||||||
resolution: VideoResolution
|
resolution: VideoResolution
|
||||||
copyCodecs: boolean
|
copyCodecs: boolean
|
||||||
isPortraitMode: boolean
|
isPortraitMode: boolean
|
||||||
|
job?: Job
|
||||||
}) {
|
}) {
|
||||||
return generateHlsPlaylistCommon({
|
return generateHlsPlaylistCommon({
|
||||||
video: options.video,
|
video: options.video,
|
||||||
|
@ -197,7 +206,8 @@ function generateHlsPlaylist (options: {
|
||||||
copyCodecs: options.copyCodecs,
|
copyCodecs: options.copyCodecs,
|
||||||
isPortraitMode: options.isPortraitMode,
|
isPortraitMode: options.isPortraitMode,
|
||||||
inputPath: options.videoInputPath,
|
inputPath: options.videoInputPath,
|
||||||
type: 'hls' as 'hls'
|
type: 'hls' as 'hls',
|
||||||
|
job: options.job
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -251,8 +261,10 @@ async function generateHlsPlaylistCommon (options: {
|
||||||
copyCodecs?: boolean
|
copyCodecs?: boolean
|
||||||
isAAC?: boolean
|
isAAC?: boolean
|
||||||
isPortraitMode: boolean
|
isPortraitMode: boolean
|
||||||
|
|
||||||
|
job?: Job
|
||||||
}) {
|
}) {
|
||||||
const { type, video, inputPath, resolution, copyCodecs, isPortraitMode, isAAC } = options
|
const { type, video, inputPath, resolution, copyCodecs, isPortraitMode, isAAC, job } = options
|
||||||
|
|
||||||
const baseHlsDirectory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid)
|
const baseHlsDirectory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid)
|
||||||
await ensureDir(join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid))
|
await ensureDir(join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid))
|
||||||
|
@ -277,7 +289,9 @@ async function generateHlsPlaylistCommon (options: {
|
||||||
|
|
||||||
hlsPlaylist: {
|
hlsPlaylist: {
|
||||||
videoFilename
|
videoFilename
|
||||||
}
|
},
|
||||||
|
|
||||||
|
job
|
||||||
}
|
}
|
||||||
|
|
||||||
await transcode(transcodeOptions)
|
await transcode(transcodeOptions)
|
||||||
|
|
|
@ -23,6 +23,7 @@ export interface Job {
|
||||||
state: JobState
|
state: JobState
|
||||||
type: JobType
|
type: JobType
|
||||||
data: any
|
data: any
|
||||||
|
progress: number
|
||||||
error: any
|
error: any
|
||||||
createdAt: Date | string
|
createdAt: Date | string
|
||||||
finishedOn: Date | string
|
finishedOn: Date | string
|
||||||
|
|
Loading…
Reference in New Issue