diff --git a/client/src/app/+admin/system/jobs/jobs.component.html b/client/src/app/+admin/system/jobs/jobs.component.html
index 2d60e7b9e..b6457a005 100644
--- a/client/src/app/+admin/system/jobs/jobs.component.html
+++ b/client/src/app/+admin/system/jobs/jobs.component.html
@@ -40,7 +40,8 @@
|
ID |
Type |
- State |
+ State |
+ Progress |
Created |
@@ -55,9 +56,15 @@
{{ job.id }} |
{{ job.type }} |
-
+
+ |
{{ job.state }}
|
+
+
+ {{ getProgress(job) }}
+ |
+
{{ job.createdAt | date: 'short' }} |
@@ -94,7 +101,7 @@
No jobs found.
No {{ jobType }}
jobs found.
-
+
No {{ jobState }} jobs found.
No {{ jobType }}
jobs found that are {{ jobState }}.
diff --git a/client/src/app/+admin/system/jobs/jobs.component.scss b/client/src/app/+admin/system/jobs/jobs.component.scss
index 784ec4572..9c6ae73e1 100644
--- a/client/src/app/+admin/system/jobs/jobs.component.scss
+++ b/client/src/app/+admin/system/jobs/jobs.component.scss
@@ -9,7 +9,8 @@
max-width: 30vw !important;
}
-.job-type {
+.job-type,
+.job-state {
width: 150px !important;
}
diff --git a/client/src/app/+admin/system/jobs/jobs.component.ts b/client/src/app/+admin/system/jobs/jobs.component.ts
index b1940b0d3..6ab17b3c1 100644
--- a/client/src/app/+admin/system/jobs/jobs.component.ts
+++ b/client/src/app/+admin/system/jobs/jobs.component.ts
@@ -83,6 +83,16 @@ export class JobsComponent extends RestTable implements OnInit {
this.saveJobStateAndType()
}
+ hasProgress () {
+ return this.jobType === 'all' || this.jobType === 'video-transcoding'
+ }
+
+ getProgress (job: Job) {
+ if (job.state === 'active') return job.progress + '%'
+
+ return ''
+ }
+
protected loadData () {
let jobState = this.jobState as JobState
if (this.jobState === 'all') jobState = null
diff --git a/server/controllers/api/jobs.ts b/server/controllers/api/jobs.ts
index e14ea2575..929140140 100644
--- a/server/controllers/api/jobs.ts
+++ b/server/controllers/api/jobs.ts
@@ -52,28 +52,23 @@ async function listJobs (req: express.Request, res: express.Response) {
const result: ResultList = {
total,
- data: state
- ? jobs.map(j => formatJob(j, state))
- : await Promise.all(jobs.map(j => formatJobWithUnknownState(j)))
+ data: await Promise.all(jobs.map(j => formatJob(j, state)))
}
return res.json(result)
}
-async function formatJobWithUnknownState (job: any) {
- return formatJob(job, await job.getState())
-}
-
-function formatJob (job: any, state: JobState): Job {
+async function formatJob (job: any, state?: JobState): Promise {
const error = isArray(job.stacktrace) && job.stacktrace.length !== 0
? job.stacktrace[0]
: null
return {
id: job.id,
- state: state,
+ state: state || await job.getState(),
type: job.queue.name as JobType,
data: job.data,
+ progress: await job.progress(),
error,
createdAt: new Date(job.timestamp),
finishedOn: new Date(job.finishedOn),
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts
index 6f7c186d9..a4d02908d 100644
--- a/server/helpers/ffmpeg-utils.ts
+++ b/server/helpers/ffmpeg-utils.ts
@@ -1,3 +1,4 @@
+import { Job } from 'bull'
import * as ffmpeg from 'fluent-ffmpeg'
import { readFile, remove, writeFile } from 'fs-extra'
import { dirname, join } from 'path'
@@ -124,6 +125,8 @@ interface BaseTranscodeOptions {
resolution: VideoResolution
isPortraitMode?: boolean
+
+ job?: Job
}
interface HLSTranscodeOptions extends BaseTranscodeOptions {
@@ -188,7 +191,7 @@ async function transcode (options: TranscodeOptions) {
command = await builders[options.type](command, options)
- await runCommand(command)
+ await runCommand(command, options.job)
await fixHLSPlaylistIfNeeded(options)
}
@@ -611,11 +614,9 @@ function getFFmpeg (input: string, type: 'live' | 'vod') {
return command
}
-async function runCommand (command: ffmpeg.FfmpegCommand, onEnd?: Function) {
+async function runCommand (command: ffmpeg.FfmpegCommand, job?: Job) {
return new Promise((res, rej) => {
command.on('error', (err, stdout, stderr) => {
- if (onEnd) onEnd()
-
logger.error('Error in transcoding job.', { stdout, stderr })
rej(err)
})
@@ -623,11 +624,18 @@ async function runCommand (command: ffmpeg.FfmpegCommand, onEnd?: Function) {
command.on('end', (stdout, stderr) => {
logger.debug('FFmpeg command ended.', { stdout, stderr })
- if (onEnd) onEnd()
-
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()
})
}
diff --git a/server/lib/job-queue/handlers/video-transcoding.ts b/server/lib/job-queue/handlers/video-transcoding.ts
index 20f8c3f50..083cec11a 100644
--- a/server/lib/job-queue/handlers/video-transcoding.ts
+++ b/server/lib/job-queue/handlers/video-transcoding.ts
@@ -44,20 +44,21 @@ async function processVideoTranscoding (job: Bull.Job) {
videoInputPath,
resolution: payload.resolution,
copyCodecs: payload.copyCodecs,
- isPortraitMode: payload.isPortraitMode || false
+ isPortraitMode: payload.isPortraitMode || false,
+ job
})
await retryTransactionWrapper(onHlsPlaylistGenerationSuccess, video)
} 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)
} else if (payload.type === 'merge-audio') {
- await mergeAudioVideofile(video, payload.resolution)
+ await mergeAudioVideofile(video, payload.resolution, job)
await retryTransactionWrapper(publishNewResolutionIfNeeded, video, payload)
} else {
- const transcodeType = await optimizeOriginalVideofile(video)
+ const transcodeType = await optimizeOriginalVideofile(video, video.getMaxQualityFile(), job)
await retryTransactionWrapper(onVideoFileOptimizerSuccess, video, payload, transcodeType)
}
diff --git a/server/lib/video-transcoding.ts b/server/lib/video-transcoding.ts
index a6b79eaea..beef78b44 100644
--- a/server/lib/video-transcoding.ts
+++ b/server/lib/video-transcoding.ts
@@ -1,3 +1,4 @@
+import { Job } from 'bull'
import { copyFile, ensureDir, move, remove, stat } from 'fs-extra'
import { basename, extname as extnameUtil, join } from 'path'
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.
-async function optimizeOriginalVideofile (video: MVideoWithFile, inputVideoFileArg?: MVideoFile) {
+async function optimizeOriginalVideofile (video: MVideoWithFile, inputVideoFile: MVideoFile, job?: Job) {
const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
const newExtname = '.mp4'
- const inputVideoFile = inputVideoFileArg || video.getMaxQualityFile()
const videoInputPath = getVideoFilePath(video, inputVideoFile)
const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
@@ -44,7 +44,9 @@ async function optimizeOriginalVideofile (video: MVideoWithFile, inputVideoFileA
availableEncoders,
profile: 'default',
- resolution: inputVideoFile.resolution
+ resolution: inputVideoFile.resolution,
+
+ job
}
// Could be very long!
@@ -70,7 +72,7 @@ async function optimizeOriginalVideofile (video: MVideoWithFile, inputVideoFileA
}
// 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 extname = '.mp4'
@@ -96,7 +98,9 @@ async function transcodeNewResolution (video: MVideoWithFile, resolution: VideoR
availableEncoders,
profile: 'default',
- resolution
+ resolution,
+
+ job
}
: {
type: 'video' as 'video',
@@ -107,7 +111,9 @@ async function transcodeNewResolution (video: MVideoWithFile, resolution: VideoR
profile: 'default',
resolution,
- isPortraitMode: isPortrait
+ isPortraitMode: isPortrait,
+
+ job
}
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
-async function mergeAudioVideofile (video: MVideoWithAllFiles, resolution: VideoResolution) {
+async function mergeAudioVideofile (video: MVideoWithAllFiles, resolution: VideoResolution, job: Job) {
const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
const newExtname = '.mp4'
@@ -140,7 +146,9 @@ async function mergeAudioVideofile (video: MVideoWithAllFiles, resolution: Video
profile: 'default',
audioPath: audioInputPath,
- resolution
+ resolution,
+
+ job
}
try {
@@ -190,6 +198,7 @@ function generateHlsPlaylist (options: {
resolution: VideoResolution
copyCodecs: boolean
isPortraitMode: boolean
+ job?: Job
}) {
return generateHlsPlaylistCommon({
video: options.video,
@@ -197,7 +206,8 @@ function generateHlsPlaylist (options: {
copyCodecs: options.copyCodecs,
isPortraitMode: options.isPortraitMode,
inputPath: options.videoInputPath,
- type: 'hls' as 'hls'
+ type: 'hls' as 'hls',
+ job: options.job
})
}
@@ -251,8 +261,10 @@ async function generateHlsPlaylistCommon (options: {
copyCodecs?: boolean
isAAC?: 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)
await ensureDir(join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid))
@@ -277,7 +289,9 @@ async function generateHlsPlaylistCommon (options: {
hlsPlaylist: {
videoFilename
- }
+ },
+
+ job
}
await transcode(transcodeOptions)
diff --git a/shared/models/server/job.model.ts b/shared/models/server/job.model.ts
index f9a6250c9..11d90c32f 100644
--- a/shared/models/server/job.model.ts
+++ b/shared/models/server/job.model.ts
@@ -23,6 +23,7 @@ export interface Job {
state: JobState
type: JobType
data: any
+ progress: number
error: any
createdAt: Date | string
finishedOn: Date | string