diff options
author | Chocobozzz <me@florianbigard.com> | 2021-01-21 14:42:43 +0100 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2021-01-21 14:42:43 +0100 |
commit | 3b01f4c0ac764ecb70efaadfd939ca868c28769c (patch) | |
tree | 99c0cdef6dac0d43dd16c02b8bae3c132037cda6 /server | |
parent | d44cdcd766fbccbe4b96f34c11a64f0e2168c3b9 (diff) | |
download | PeerTube-3b01f4c0ac764ecb70efaadfd939ca868c28769c.tar.gz PeerTube-3b01f4c0ac764ecb70efaadfd939ca868c28769c.tar.zst PeerTube-3b01f4c0ac764ecb70efaadfd939ca868c28769c.zip |
Support progress for ffmpeg tasks
Diffstat (limited to 'server')
-rw-r--r-- | server/controllers/api/jobs.ts | 13 | ||||
-rw-r--r-- | server/helpers/ffmpeg-utils.ts | 20 | ||||
-rw-r--r-- | server/lib/job-queue/handlers/video-transcoding.ts | 9 | ||||
-rw-r--r-- | server/lib/video-transcoding.ts | 36 |
4 files changed, 48 insertions, 30 deletions
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) { | |||
52 | 52 | ||
53 | const result: ResultList<Job> = { | 53 | const result: ResultList<Job> = { |
54 | total, | 54 | total, |
55 | data: state | 55 | data: await Promise.all(jobs.map(j => formatJob(j, state))) |
56 | ? jobs.map(j => formatJob(j, state)) | ||
57 | : await Promise.all(jobs.map(j => formatJobWithUnknownState(j))) | ||
58 | } | 56 | } |
59 | 57 | ||
60 | return res.json(result) | 58 | return res.json(result) |
61 | } | 59 | } |
62 | 60 | ||
63 | async function formatJobWithUnknownState (job: any) { | 61 | async function formatJob (job: any, state?: JobState): Promise<Job> { |
64 | return formatJob(job, await job.getState()) | ||
65 | } | ||
66 | |||
67 | function formatJob (job: any, state: JobState): Job { | ||
68 | const error = isArray(job.stacktrace) && job.stacktrace.length !== 0 | 62 | const error = isArray(job.stacktrace) && job.stacktrace.length !== 0 |
69 | ? job.stacktrace[0] | 63 | ? job.stacktrace[0] |
70 | : null | 64 | : null |
71 | 65 | ||
72 | return { | 66 | return { |
73 | id: job.id, | 67 | id: job.id, |
74 | state: state, | 68 | state: state || await job.getState(), |
75 | type: job.queue.name as JobType, | 69 | type: job.queue.name as JobType, |
76 | data: job.data, | 70 | data: job.data, |
71 | progress: await job.progress(), | ||
77 | error, | 72 | error, |
78 | createdAt: new Date(job.timestamp), | 73 | createdAt: new Date(job.timestamp), |
79 | finishedOn: new Date(job.finishedOn), | 74 | 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 @@ | |||
1 | import { Job } from 'bull' | ||
1 | import * as ffmpeg from 'fluent-ffmpeg' | 2 | import * as ffmpeg from 'fluent-ffmpeg' |
2 | import { readFile, remove, writeFile } from 'fs-extra' | 3 | import { readFile, remove, writeFile } from 'fs-extra' |
3 | import { dirname, join } from 'path' | 4 | import { dirname, join } from 'path' |
@@ -124,6 +125,8 @@ interface BaseTranscodeOptions { | |||
124 | resolution: VideoResolution | 125 | resolution: VideoResolution |
125 | 126 | ||
126 | isPortraitMode?: boolean | 127 | isPortraitMode?: boolean |
128 | |||
129 | job?: Job | ||
127 | } | 130 | } |
128 | 131 | ||
129 | interface HLSTranscodeOptions extends BaseTranscodeOptions { | 132 | interface HLSTranscodeOptions extends BaseTranscodeOptions { |
@@ -188,7 +191,7 @@ async function transcode (options: TranscodeOptions) { | |||
188 | 191 | ||
189 | command = await builders[options.type](command, options) | 192 | command = await builders[options.type](command, options) |
190 | 193 | ||
191 | await runCommand(command) | 194 | await runCommand(command, options.job) |
192 | 195 | ||
193 | await fixHLSPlaylistIfNeeded(options) | 196 | await fixHLSPlaylistIfNeeded(options) |
194 | } | 197 | } |
@@ -611,11 +614,9 @@ function getFFmpeg (input: string, type: 'live' | 'vod') { | |||
611 | return command | 614 | return command |
612 | } | 615 | } |
613 | 616 | ||
614 | async function runCommand (command: ffmpeg.FfmpegCommand, onEnd?: Function) { | 617 | async function runCommand (command: ffmpeg.FfmpegCommand, job?: Job) { |
615 | return new Promise<void>((res, rej) => { | 618 | return new Promise<void>((res, rej) => { |
616 | command.on('error', (err, stdout, stderr) => { | 619 | command.on('error', (err, stdout, stderr) => { |
617 | if (onEnd) onEnd() | ||
618 | |||
619 | logger.error('Error in transcoding job.', { stdout, stderr }) | 620 | logger.error('Error in transcoding job.', { stdout, stderr }) |
620 | rej(err) | 621 | rej(err) |
621 | }) | 622 | }) |
@@ -623,11 +624,18 @@ async function runCommand (command: ffmpeg.FfmpegCommand, onEnd?: Function) { | |||
623 | command.on('end', (stdout, stderr) => { | 624 | command.on('end', (stdout, stderr) => { |
624 | logger.debug('FFmpeg command ended.', { stdout, stderr }) | 625 | logger.debug('FFmpeg command ended.', { stdout, stderr }) |
625 | 626 | ||
626 | if (onEnd) onEnd() | ||
627 | |||
628 | res() | 627 | res() |
629 | }) | 628 | }) |
630 | 629 | ||
630 | if (job) { | ||
631 | command.on('progress', progress => { | ||
632 | if (!progress.percent) return | ||
633 | |||
634 | job.progress(Math.round(progress.percent)) | ||
635 | .catch(err => logger.warn('Cannot set ffmpeg job progress.', { err })) | ||
636 | }) | ||
637 | } | ||
638 | |||
631 | command.run() | 639 | command.run() |
632 | }) | 640 | }) |
633 | } | 641 | } |
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) { | |||
44 | videoInputPath, | 44 | videoInputPath, |
45 | resolution: payload.resolution, | 45 | resolution: payload.resolution, |
46 | copyCodecs: payload.copyCodecs, | 46 | copyCodecs: payload.copyCodecs, |
47 | isPortraitMode: payload.isPortraitMode || false | 47 | isPortraitMode: payload.isPortraitMode || false, |
48 | job | ||
48 | }) | 49 | }) |
49 | 50 | ||
50 | await retryTransactionWrapper(onHlsPlaylistGenerationSuccess, video) | 51 | await retryTransactionWrapper(onHlsPlaylistGenerationSuccess, video) |
51 | } else if (payload.type === 'new-resolution') { | 52 | } else if (payload.type === 'new-resolution') { |
52 | await transcodeNewResolution(video, payload.resolution, payload.isPortraitMode || false) | 53 | await transcodeNewResolution(video, payload.resolution, payload.isPortraitMode || false, job) |
53 | 54 | ||
54 | await retryTransactionWrapper(publishNewResolutionIfNeeded, video, payload) | 55 | await retryTransactionWrapper(publishNewResolutionIfNeeded, video, payload) |
55 | } else if (payload.type === 'merge-audio') { | 56 | } else if (payload.type === 'merge-audio') { |
56 | await mergeAudioVideofile(video, payload.resolution) | 57 | await mergeAudioVideofile(video, payload.resolution, job) |
57 | 58 | ||
58 | await retryTransactionWrapper(publishNewResolutionIfNeeded, video, payload) | 59 | await retryTransactionWrapper(publishNewResolutionIfNeeded, video, payload) |
59 | } else { | 60 | } else { |
60 | const transcodeType = await optimizeOriginalVideofile(video) | 61 | const transcodeType = await optimizeOriginalVideofile(video, video.getMaxQualityFile(), job) |
61 | 62 | ||
62 | await retryTransactionWrapper(onVideoFileOptimizerSuccess, video, payload, transcodeType) | 63 | await retryTransactionWrapper(onVideoFileOptimizerSuccess, video, payload, transcodeType) |
63 | } | 64 | } |
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 @@ | |||
1 | import { Job } from 'bull' | ||
1 | import { copyFile, ensureDir, move, remove, stat } from 'fs-extra' | 2 | import { copyFile, ensureDir, move, remove, stat } from 'fs-extra' |
2 | import { basename, extname as extnameUtil, join } from 'path' | 3 | import { basename, extname as extnameUtil, join } from 'path' |
3 | import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' | 4 | import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' |
@@ -23,11 +24,10 @@ import { availableEncoders } from './video-transcoding-profiles' | |||
23 | */ | 24 | */ |
24 | 25 | ||
25 | // Optimize the original video file and replace it. The resolution is not changed. | 26 | // Optimize the original video file and replace it. The resolution is not changed. |
26 | async function optimizeOriginalVideofile (video: MVideoWithFile, inputVideoFileArg?: MVideoFile) { | 27 | async function optimizeOriginalVideofile (video: MVideoWithFile, inputVideoFile: MVideoFile, job?: Job) { |
27 | const transcodeDirectory = CONFIG.STORAGE.TMP_DIR | 28 | const transcodeDirectory = CONFIG.STORAGE.TMP_DIR |
28 | const newExtname = '.mp4' | 29 | const newExtname = '.mp4' |
29 | 30 | ||
30 | const inputVideoFile = inputVideoFileArg || video.getMaxQualityFile() | ||
31 | const videoInputPath = getVideoFilePath(video, inputVideoFile) | 31 | const videoInputPath = getVideoFilePath(video, inputVideoFile) |
32 | const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname) | 32 | const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname) |
33 | 33 | ||
@@ -44,7 +44,9 @@ async function optimizeOriginalVideofile (video: MVideoWithFile, inputVideoFileA | |||
44 | availableEncoders, | 44 | availableEncoders, |
45 | profile: 'default', | 45 | profile: 'default', |
46 | 46 | ||
47 | resolution: inputVideoFile.resolution | 47 | resolution: inputVideoFile.resolution, |
48 | |||
49 | job | ||
48 | } | 50 | } |
49 | 51 | ||
50 | // Could be very long! | 52 | // Could be very long! |
@@ -70,7 +72,7 @@ async function optimizeOriginalVideofile (video: MVideoWithFile, inputVideoFileA | |||
70 | } | 72 | } |
71 | 73 | ||
72 | // Transcode the original video file to a lower resolution. | 74 | // Transcode the original video file to a lower resolution. |
73 | async function transcodeNewResolution (video: MVideoWithFile, resolution: VideoResolution, isPortrait: boolean) { | 75 | async function transcodeNewResolution (video: MVideoWithFile, resolution: VideoResolution, isPortrait: boolean, job: Job) { |
74 | const transcodeDirectory = CONFIG.STORAGE.TMP_DIR | 76 | const transcodeDirectory = CONFIG.STORAGE.TMP_DIR |
75 | const extname = '.mp4' | 77 | const extname = '.mp4' |
76 | 78 | ||
@@ -96,7 +98,9 @@ async function transcodeNewResolution (video: MVideoWithFile, resolution: VideoR | |||
96 | availableEncoders, | 98 | availableEncoders, |
97 | profile: 'default', | 99 | profile: 'default', |
98 | 100 | ||
99 | resolution | 101 | resolution, |
102 | |||
103 | job | ||
100 | } | 104 | } |
101 | : { | 105 | : { |
102 | type: 'video' as 'video', | 106 | type: 'video' as 'video', |
@@ -107,7 +111,9 @@ async function transcodeNewResolution (video: MVideoWithFile, resolution: VideoR | |||
107 | profile: 'default', | 111 | profile: 'default', |
108 | 112 | ||
109 | resolution, | 113 | resolution, |
110 | isPortraitMode: isPortrait | 114 | isPortraitMode: isPortrait, |
115 | |||
116 | job | ||
111 | } | 117 | } |
112 | 118 | ||
113 | await transcode(transcodeOptions) | 119 | await transcode(transcodeOptions) |
@@ -116,7 +122,7 @@ async function transcodeNewResolution (video: MVideoWithFile, resolution: VideoR | |||
116 | } | 122 | } |
117 | 123 | ||
118 | // Merge an image with an audio file to create a video | 124 | // Merge an image with an audio file to create a video |
119 | async function mergeAudioVideofile (video: MVideoWithAllFiles, resolution: VideoResolution) { | 125 | async function mergeAudioVideofile (video: MVideoWithAllFiles, resolution: VideoResolution, job: Job) { |
120 | const transcodeDirectory = CONFIG.STORAGE.TMP_DIR | 126 | const transcodeDirectory = CONFIG.STORAGE.TMP_DIR |
121 | const newExtname = '.mp4' | 127 | const newExtname = '.mp4' |
122 | 128 | ||
@@ -140,7 +146,9 @@ async function mergeAudioVideofile (video: MVideoWithAllFiles, resolution: Video | |||
140 | profile: 'default', | 146 | profile: 'default', |
141 | 147 | ||
142 | audioPath: audioInputPath, | 148 | audioPath: audioInputPath, |
143 | resolution | 149 | resolution, |
150 | |||
151 | job | ||
144 | } | 152 | } |
145 | 153 | ||
146 | try { | 154 | try { |
@@ -190,6 +198,7 @@ function generateHlsPlaylist (options: { | |||
190 | resolution: VideoResolution | 198 | resolution: VideoResolution |
191 | copyCodecs: boolean | 199 | copyCodecs: boolean |
192 | isPortraitMode: boolean | 200 | isPortraitMode: boolean |
201 | job?: Job | ||
193 | }) { | 202 | }) { |
194 | return generateHlsPlaylistCommon({ | 203 | return generateHlsPlaylistCommon({ |
195 | video: options.video, | 204 | video: options.video, |
@@ -197,7 +206,8 @@ function generateHlsPlaylist (options: { | |||
197 | copyCodecs: options.copyCodecs, | 206 | copyCodecs: options.copyCodecs, |
198 | isPortraitMode: options.isPortraitMode, | 207 | isPortraitMode: options.isPortraitMode, |
199 | inputPath: options.videoInputPath, | 208 | inputPath: options.videoInputPath, |
200 | type: 'hls' as 'hls' | 209 | type: 'hls' as 'hls', |
210 | job: options.job | ||
201 | }) | 211 | }) |
202 | } | 212 | } |
203 | 213 | ||
@@ -251,8 +261,10 @@ async function generateHlsPlaylistCommon (options: { | |||
251 | copyCodecs?: boolean | 261 | copyCodecs?: boolean |
252 | isAAC?: boolean | 262 | isAAC?: boolean |
253 | isPortraitMode: boolean | 263 | isPortraitMode: boolean |
264 | |||
265 | job?: Job | ||
254 | }) { | 266 | }) { |
255 | const { type, video, inputPath, resolution, copyCodecs, isPortraitMode, isAAC } = options | 267 | const { type, video, inputPath, resolution, copyCodecs, isPortraitMode, isAAC, job } = options |
256 | 268 | ||
257 | const baseHlsDirectory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid) | 269 | const baseHlsDirectory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid) |
258 | await ensureDir(join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid)) | 270 | await ensureDir(join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid)) |
@@ -277,7 +289,9 @@ async function generateHlsPlaylistCommon (options: { | |||
277 | 289 | ||
278 | hlsPlaylist: { | 290 | hlsPlaylist: { |
279 | videoFilename | 291 | videoFilename |
280 | } | 292 | }, |
293 | |||
294 | job | ||
281 | } | 295 | } |
282 | 296 | ||
283 | await transcode(transcodeOptions) | 297 | await transcode(transcodeOptions) |