diff options
Diffstat (limited to 'server')
-rw-r--r-- | server/helpers/ffmpeg-utils.ts | 66 | ||||
-rw-r--r-- | server/tests/fixtures/peertube-plugin-test-transcoding-one/main.js | 97 | ||||
-rw-r--r-- | server/tests/plugins/plugin-transcoding.ts | 34 |
3 files changed, 126 insertions, 71 deletions
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts index 685a35886..aa4223cda 100644 --- a/server/helpers/ffmpeg-utils.ts +++ b/server/helpers/ffmpeg-utils.ts | |||
@@ -9,6 +9,7 @@ import { execPromise, promisify0 } from './core-utils' | |||
9 | import { computeFPS, getAudioStream, getVideoFileFPS } from './ffprobe-utils' | 9 | import { computeFPS, getAudioStream, getVideoFileFPS } from './ffprobe-utils' |
10 | import { processImage } from './image-utils' | 10 | import { processImage } from './image-utils' |
11 | import { logger } from './logger' | 11 | import { logger } from './logger' |
12 | import { FilterSpecification } from 'fluent-ffmpeg' | ||
12 | 13 | ||
13 | /** | 14 | /** |
14 | * | 15 | * |
@@ -226,21 +227,14 @@ async function getLiveTranscodingCommand (options: { | |||
226 | 227 | ||
227 | const varStreamMap: string[] = [] | 228 | const varStreamMap: string[] = [] |
228 | 229 | ||
229 | command.complexFilter([ | 230 | const complexFilter: FilterSpecification[] = [ |
230 | { | 231 | { |
231 | inputs: '[v:0]', | 232 | inputs: '[v:0]', |
232 | filter: 'split', | 233 | filter: 'split', |
233 | options: resolutions.length, | 234 | options: resolutions.length, |
234 | outputs: resolutions.map(r => `vtemp${r}`) | 235 | outputs: resolutions.map(r => `vtemp${r}`) |
235 | }, | 236 | } |
236 | 237 | ] | |
237 | ...resolutions.map(r => ({ | ||
238 | inputs: `vtemp${r}`, | ||
239 | filter: 'scale', | ||
240 | options: `w=-2:h=${r}`, | ||
241 | outputs: `vout${r}` | ||
242 | })) | ||
243 | ]) | ||
244 | 238 | ||
245 | command.outputOption('-preset superfast') | 239 | command.outputOption('-preset superfast') |
246 | command.outputOption('-sc_threshold 0') | 240 | command.outputOption('-sc_threshold 0') |
@@ -278,6 +272,13 @@ async function getLiveTranscodingCommand (options: { | |||
278 | 272 | ||
279 | command.outputOption(`${buildStreamSuffix('-c:v', i)} ${builderResult.encoder}`) | 273 | command.outputOption(`${buildStreamSuffix('-c:v', i)} ${builderResult.encoder}`) |
280 | applyEncoderOptions(command, builderResult.result) | 274 | applyEncoderOptions(command, builderResult.result) |
275 | |||
276 | complexFilter.push({ | ||
277 | inputs: `vtemp${resolution}`, | ||
278 | filter: getScaleFilter(builderResult.result), | ||
279 | options: `w=-2:h=${resolution}`, | ||
280 | outputs: `vout${resolution}` | ||
281 | }) | ||
281 | } | 282 | } |
282 | 283 | ||
283 | { | 284 | { |
@@ -300,6 +301,8 @@ async function getLiveTranscodingCommand (options: { | |||
300 | varStreamMap.push(`v:${i},a:${i}`) | 301 | varStreamMap.push(`v:${i},a:${i}`) |
301 | } | 302 | } |
302 | 303 | ||
304 | command.complexFilter(complexFilter) | ||
305 | |||
303 | addDefaultLiveHLSParams(command, outPath) | 306 | addDefaultLiveHLSParams(command, outPath) |
304 | 307 | ||
305 | command.outputOption('-var_stream_map', varStreamMap.join(' ')) | 308 | command.outputOption('-var_stream_map', varStreamMap.join(' ')) |
@@ -389,29 +392,29 @@ async function buildx264VODCommand (command: ffmpeg.FfmpegCommand, options: Tran | |||
389 | let fps = await getVideoFileFPS(options.inputPath) | 392 | let fps = await getVideoFileFPS(options.inputPath) |
390 | fps = computeFPS(fps, options.resolution) | 393 | fps = computeFPS(fps, options.resolution) |
391 | 394 | ||
392 | command = await presetVideo(command, options.inputPath, options, fps) | 395 | let scaleFilterValue: string |
393 | 396 | ||
394 | if (options.resolution !== undefined) { | 397 | if (options.resolution !== undefined) { |
395 | // '?x720' or '720x?' for example | 398 | scaleFilterValue = options.isPortraitMode === true |
396 | const size = options.isPortraitMode === true | 399 | ? `${options.resolution}:-2` |
397 | ? `${options.resolution}x?` | 400 | : `-2:${options.resolution}` |
398 | : `?x${options.resolution}` | ||
399 | |||
400 | command = command.size(size) | ||
401 | } | 401 | } |
402 | 402 | ||
403 | command = await presetVideo({ command, input: options.inputPath, transcodeOptions: options, fps, scaleFilterValue }) | ||
404 | |||
403 | return command | 405 | return command |
404 | } | 406 | } |
405 | 407 | ||
406 | async function buildAudioMergeCommand (command: ffmpeg.FfmpegCommand, options: MergeAudioTranscodeOptions) { | 408 | async function buildAudioMergeCommand (command: ffmpeg.FfmpegCommand, options: MergeAudioTranscodeOptions) { |
407 | command = command.loop(undefined) | 409 | command = command.loop(undefined) |
408 | 410 | ||
409 | command = await presetVideo(command, options.audioPath, options) | 411 | // Avoid "height not divisible by 2" error |
412 | const scaleFilterValue = 'trunc(iw/2)*2:trunc(ih/2)*2' | ||
413 | command = await presetVideo({ command, input: options.audioPath, transcodeOptions: options, scaleFilterValue }) | ||
410 | 414 | ||
411 | command.outputOption('-preset:v veryfast') | 415 | command.outputOption('-preset:v veryfast') |
412 | 416 | ||
413 | command = command.input(options.audioPath) | 417 | command = command.input(options.audioPath) |
414 | .videoFilter('scale=trunc(iw/2)*2:trunc(ih/2)*2') // Avoid "height not divisible by 2" error | ||
415 | .outputOption('-tune stillimage') | 418 | .outputOption('-tune stillimage') |
416 | .outputOption('-shortest') | 419 | .outputOption('-shortest') |
417 | 420 | ||
@@ -555,12 +558,15 @@ async function getEncoderBuilderResult (options: { | |||
555 | return null | 558 | return null |
556 | } | 559 | } |
557 | 560 | ||
558 | async function presetVideo ( | 561 | async function presetVideo (options: { |
559 | command: ffmpeg.FfmpegCommand, | 562 | command: ffmpeg.FfmpegCommand |
560 | input: string, | 563 | input: string |
561 | transcodeOptions: TranscodeOptions, | 564 | transcodeOptions: TranscodeOptions |
562 | fps?: number | 565 | fps?: number |
563 | ) { | 566 | scaleFilterValue?: string |
567 | }) { | ||
568 | const { command, input, transcodeOptions, fps, scaleFilterValue } = options | ||
569 | |||
564 | let localCommand = command | 570 | let localCommand = command |
565 | .format('mp4') | 571 | .format('mp4') |
566 | .outputOption('-movflags faststart') | 572 | .outputOption('-movflags faststart') |
@@ -601,9 +607,14 @@ async function presetVideo ( | |||
601 | 607 | ||
602 | if (streamType === 'video') { | 608 | if (streamType === 'video') { |
603 | localCommand.videoCodec(builderResult.encoder) | 609 | localCommand.videoCodec(builderResult.encoder) |
610 | |||
611 | if (scaleFilterValue) { | ||
612 | localCommand.outputOption(`-vf ${getScaleFilter(builderResult.result)}=${scaleFilterValue}`) | ||
613 | } | ||
604 | } else if (streamType === 'audio') { | 614 | } else if (streamType === 'audio') { |
605 | localCommand.audioCodec(builderResult.encoder) | 615 | localCommand.audioCodec(builderResult.encoder) |
606 | } | 616 | } |
617 | |||
607 | applyEncoderOptions(localCommand, builderResult.result) | 618 | applyEncoderOptions(localCommand, builderResult.result) |
608 | addDefaultEncoderParams({ command: localCommand, encoder: builderResult.encoder, fps }) | 619 | addDefaultEncoderParams({ command: localCommand, encoder: builderResult.encoder, fps }) |
609 | } | 620 | } |
@@ -628,10 +639,15 @@ function presetOnlyAudio (command: ffmpeg.FfmpegCommand): ffmpeg.FfmpegCommand { | |||
628 | function applyEncoderOptions (command: ffmpeg.FfmpegCommand, options: EncoderOptions): ffmpeg.FfmpegCommand { | 639 | function applyEncoderOptions (command: ffmpeg.FfmpegCommand, options: EncoderOptions): ffmpeg.FfmpegCommand { |
629 | return command | 640 | return command |
630 | .inputOptions(options.inputOptions ?? []) | 641 | .inputOptions(options.inputOptions ?? []) |
631 | .videoFilters(options.videoFilters ?? []) | ||
632 | .outputOptions(options.outputOptions ?? []) | 642 | .outputOptions(options.outputOptions ?? []) |
633 | } | 643 | } |
634 | 644 | ||
645 | function getScaleFilter (options: EncoderOptions): string { | ||
646 | if (options.scaleFilter) return options.scaleFilter.name | ||
647 | |||
648 | return 'scale' | ||
649 | } | ||
650 | |||
635 | // --------------------------------------------------------------------------- | 651 | // --------------------------------------------------------------------------- |
636 | // Utils | 652 | // Utils |
637 | // --------------------------------------------------------------------------- | 653 | // --------------------------------------------------------------------------- |
diff --git a/server/tests/fixtures/peertube-plugin-test-transcoding-one/main.js b/server/tests/fixtures/peertube-plugin-test-transcoding-one/main.js index 366b827a9..59b136947 100644 --- a/server/tests/fixtures/peertube-plugin-test-transcoding-one/main.js +++ b/server/tests/fixtures/peertube-plugin-test-transcoding-one/main.js | |||
@@ -1,63 +1,84 @@ | |||
1 | async function register ({ transcodingManager }) { | 1 | async function register ({ transcodingManager }) { |
2 | 2 | ||
3 | // Output options | ||
3 | { | 4 | { |
4 | const builder = () => { | 5 | { |
5 | return { | 6 | const builder = () => { |
6 | outputOptions: [ | 7 | return { |
7 | '-r 10' | 8 | outputOptions: [ |
8 | ] | 9 | '-r 10' |
10 | ] | ||
11 | } | ||
9 | } | 12 | } |
10 | } | ||
11 | 13 | ||
12 | transcodingManager.addVODProfile('libx264', 'low-vod', builder) | 14 | transcodingManager.addVODProfile('libx264', 'low-vod', builder) |
13 | } | 15 | } |
14 | 16 | ||
15 | { | 17 | { |
16 | const builder = () => { | 18 | const builder = (options) => { |
17 | return { | 19 | return { |
18 | videoFilters: [ | 20 | outputOptions: [ |
19 | 'fps=10' | 21 | '-r:' + options.streamNum + ' 5' |
20 | ] | 22 | ] |
23 | } | ||
21 | } | 24 | } |
22 | } | ||
23 | 25 | ||
24 | transcodingManager.addVODProfile('libx264', 'video-filters-vod', builder) | 26 | transcodingManager.addLiveProfile('libx264', 'low-live', builder) |
27 | } | ||
25 | } | 28 | } |
26 | 29 | ||
30 | // Input options | ||
27 | { | 31 | { |
28 | const builder = () => { | 32 | { |
29 | return { | 33 | const builder = () => { |
30 | inputOptions: [ | 34 | return { |
31 | '-r 5' | 35 | inputOptions: [ |
32 | ] | 36 | '-r 5' |
37 | ] | ||
38 | } | ||
33 | } | 39 | } |
34 | } | ||
35 | 40 | ||
36 | transcodingManager.addVODProfile('libx264', 'input-options-vod', builder) | 41 | transcodingManager.addVODProfile('libx264', 'input-options-vod', builder) |
37 | } | 42 | } |
38 | 43 | ||
39 | { | 44 | { |
40 | const builder = (options) => { | 45 | const builder = () => { |
41 | return { | 46 | return { |
42 | outputOptions: [ | 47 | inputOptions: [ |
43 | '-r:' + options.streamNum + ' 5' | 48 | '-r 5' |
44 | ] | 49 | ] |
50 | } | ||
45 | } | 51 | } |
46 | } | ||
47 | 52 | ||
48 | transcodingManager.addLiveProfile('libx264', 'low-live', builder) | 53 | transcodingManager.addLiveProfile('libx264', 'input-options-live', builder) |
54 | } | ||
49 | } | 55 | } |
50 | 56 | ||
57 | // Scale filters | ||
51 | { | 58 | { |
52 | const builder = () => { | 59 | { |
53 | return { | 60 | const builder = () => { |
54 | inputOptions: [ | 61 | return { |
55 | '-r 5' | 62 | scaleFilter: { |
56 | ] | 63 | name: 'Glomgold' |
64 | } | ||
65 | } | ||
57 | } | 66 | } |
67 | |||
68 | transcodingManager.addVODProfile('libx264', 'bad-scale-vod', builder) | ||
58 | } | 69 | } |
59 | 70 | ||
60 | transcodingManager.addLiveProfile('libx264', 'input-options-live', builder) | 71 | { |
72 | const builder = () => { | ||
73 | return { | ||
74 | scaleFilter: { | ||
75 | name: 'Flintheart' | ||
76 | } | ||
77 | } | ||
78 | } | ||
79 | |||
80 | transcodingManager.addLiveProfile('libx264', 'bad-scale-live', builder) | ||
81 | } | ||
61 | } | 82 | } |
62 | } | 83 | } |
63 | 84 | ||
diff --git a/server/tests/plugins/plugin-transcoding.ts b/server/tests/plugins/plugin-transcoding.ts index 415705ca1..b6dff930e 100644 --- a/server/tests/plugins/plugin-transcoding.ts +++ b/server/tests/plugins/plugin-transcoding.ts | |||
@@ -15,9 +15,11 @@ import { | |||
15 | sendRTMPStreamInVideo, | 15 | sendRTMPStreamInVideo, |
16 | setAccessTokensToServers, | 16 | setAccessTokensToServers, |
17 | setDefaultVideoChannel, | 17 | setDefaultVideoChannel, |
18 | testFfmpegStreamError, | ||
18 | uninstallPlugin, | 19 | uninstallPlugin, |
19 | updateCustomSubConfig, | 20 | updateCustomSubConfig, |
20 | uploadVideoAndGetId, | 21 | uploadVideoAndGetId, |
22 | waitFfmpegUntilError, | ||
21 | waitJobs, | 23 | waitJobs, |
22 | waitUntilLivePublished | 24 | waitUntilLivePublished |
23 | } from '../../../shared/extra-utils' | 25 | } from '../../../shared/extra-utils' |
@@ -119,8 +121,8 @@ describe('Test transcoding plugins', function () { | |||
119 | const res = await getConfig(server.url) | 121 | const res = await getConfig(server.url) |
120 | const config = res.body as ServerConfig | 122 | const config = res.body as ServerConfig |
121 | 123 | ||
122 | expect(config.transcoding.availableProfiles).to.have.members([ 'default', 'low-vod', 'video-filters-vod', 'input-options-vod' ]) | 124 | expect(config.transcoding.availableProfiles).to.have.members([ 'default', 'low-vod', 'input-options-vod', 'bad-scale-vod' ]) |
123 | expect(config.live.transcoding.availableProfiles).to.have.members([ 'default', 'low-live', 'input-options-live' ]) | 125 | expect(config.live.transcoding.availableProfiles).to.have.members([ 'default', 'low-live', 'input-options-live', 'bad-scale-live' ]) |
124 | }) | 126 | }) |
125 | 127 | ||
126 | it('Should not use the plugin profile if not chosen by the admin', async function () { | 128 | it('Should not use the plugin profile if not chosen by the admin', async function () { |
@@ -143,26 +145,31 @@ describe('Test transcoding plugins', function () { | |||
143 | await checkVideoFPS(videoUUID, 'below', 12) | 145 | await checkVideoFPS(videoUUID, 'below', 12) |
144 | }) | 146 | }) |
145 | 147 | ||
146 | it('Should apply video filters in vod profile', async function () { | 148 | it('Should apply input options in vod profile', async function () { |
147 | this.timeout(120000) | 149 | this.timeout(120000) |
148 | 150 | ||
149 | await updateConf(server, 'video-filters-vod', 'default') | 151 | await updateConf(server, 'input-options-vod', 'default') |
150 | 152 | ||
151 | const videoUUID = (await uploadVideoAndGetId({ server, videoName: 'video' })).uuid | 153 | const videoUUID = (await uploadVideoAndGetId({ server, videoName: 'video' })).uuid |
152 | await waitJobs([ server ]) | 154 | await waitJobs([ server ]) |
153 | 155 | ||
154 | await checkVideoFPS(videoUUID, 'below', 12) | 156 | await checkVideoFPS(videoUUID, 'below', 6) |
155 | }) | 157 | }) |
156 | 158 | ||
157 | it('Should apply input options in vod profile', async function () { | 159 | it('Should apply the scale filter in vod profile', async function () { |
158 | this.timeout(120000) | 160 | this.timeout(120000) |
159 | 161 | ||
160 | await updateConf(server, 'input-options-vod', 'default') | 162 | await updateConf(server, 'bad-scale-vod', 'default') |
161 | 163 | ||
162 | const videoUUID = (await uploadVideoAndGetId({ server, videoName: 'video' })).uuid | 164 | const videoUUID = (await uploadVideoAndGetId({ server, videoName: 'video' })).uuid |
163 | await waitJobs([ server ]) | 165 | await waitJobs([ server ]) |
164 | 166 | ||
165 | await checkVideoFPS(videoUUID, 'below', 6) | 167 | // Transcoding failed |
168 | const res = await getVideo(server.url, videoUUID) | ||
169 | const video: VideoDetails = res.body | ||
170 | |||
171 | expect(video.files).to.have.lengthOf(1) | ||
172 | expect(video.streamingPlaylists).to.have.lengthOf(0) | ||
166 | }) | 173 | }) |
167 | 174 | ||
168 | it('Should not use the plugin profile if not chosen by the admin', async function () { | 175 | it('Should not use the plugin profile if not chosen by the admin', async function () { |
@@ -205,6 +212,17 @@ describe('Test transcoding plugins', function () { | |||
205 | await checkLiveFPS(liveVideoId, 'below', 6) | 212 | await checkLiveFPS(liveVideoId, 'below', 6) |
206 | }) | 213 | }) |
207 | 214 | ||
215 | it('Should apply the scale filter name on live profile', async function () { | ||
216 | this.timeout(120000) | ||
217 | |||
218 | await updateConf(server, 'low-vod', 'bad-scale-live') | ||
219 | |||
220 | const liveVideoId = await createLiveWrapper(server) | ||
221 | |||
222 | const command = await sendRTMPStreamInVideo(server.url, server.accessToken, liveVideoId, 'video_short2.webm') | ||
223 | await testFfmpegStreamError(command, true) | ||
224 | }) | ||
225 | |||
208 | it('Should default to the default profile if the specified profile does not exist', async function () { | 226 | it('Should default to the default profile if the specified profile does not exist', async function () { |
209 | this.timeout(120000) | 227 | this.timeout(120000) |
210 | 228 | ||