aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--server/helpers/ffmpeg-utils.ts66
-rw-r--r--server/tests/fixtures/peertube-plugin-test-transcoding-one/main.js97
-rw-r--r--server/tests/plugins/plugin-transcoding.ts34
-rw-r--r--shared/models/videos/video-transcoding.model.ts5
-rw-r--r--support/doc/plugins/guide.md7
5 files changed, 130 insertions, 79 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'
9import { computeFPS, getAudioStream, getVideoFileFPS } from './ffprobe-utils' 9import { computeFPS, getAudioStream, getVideoFileFPS } from './ffprobe-utils'
10import { processImage } from './image-utils' 10import { processImage } from './image-utils'
11import { logger } from './logger' 11import { logger } from './logger'
12import { 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
406async function buildAudioMergeCommand (command: ffmpeg.FfmpegCommand, options: MergeAudioTranscodeOptions) { 408async 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
558async function presetVideo ( 561async 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 {
628function applyEncoderOptions (command: ffmpeg.FfmpegCommand, options: EncoderOptions): ffmpeg.FfmpegCommand { 639function 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
645function 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 @@
1async function register ({ transcodingManager }) { 1async 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
diff --git a/shared/models/videos/video-transcoding.model.ts b/shared/models/videos/video-transcoding.model.ts
index ffb0115dc..3f2382ce8 100644
--- a/shared/models/videos/video-transcoding.model.ts
+++ b/shared/models/videos/video-transcoding.model.ts
@@ -12,8 +12,11 @@ export type EncoderOptionsBuilder = (params: {
12export interface EncoderOptions { 12export interface EncoderOptions {
13 copy?: boolean // Copy stream? Default to false 13 copy?: boolean // Copy stream? Default to false
14 14
15 scaleFilter?: {
16 name: string
17 }
18
15 inputOptions?: string[] 19 inputOptions?: string[]
16 videoFilters?: string[]
17 outputOptions?: string[] 20 outputOptions?: string[]
18} 21}
19 22
diff --git a/support/doc/plugins/guide.md b/support/doc/plugins/guide.md
index 331813e4f..ea33b8d9f 100644
--- a/support/doc/plugins/guide.md
+++ b/support/doc/plugins/guide.md
@@ -328,8 +328,6 @@ function register (...) {
328Adding transcoding profiles allow admins to change ffmpeg encoding parameters and/or encoders. 328Adding transcoding profiles allow admins to change ffmpeg encoding parameters and/or encoders.
329A transcoding profile has to be chosen by the admin of the instance using the admin configuration. 329A transcoding profile has to be chosen by the admin of the instance using the admin configuration.
330 330
331Transcoding profiles used for live transcoding must not provide any `videoFilters`.
332
333```js 331```js
334async function register ({ 332async function register ({
335 transcodingManager 333 transcodingManager
@@ -346,9 +344,6 @@ async function register ({
346 // All these options are optional and defaults to [] 344 // All these options are optional and defaults to []
347 return { 345 return {
348 inputOptions: [], 346 inputOptions: [],
349 videoFilters: [
350 'vflip' // flip the video vertically
351 ],
352 outputOptions: [ 347 outputOptions: [
353 // Use a custom bitrate 348 // Use a custom bitrate
354 '-b' + streamString + ' 10K' 349 '-b' + streamString + ' 10K'
@@ -364,7 +359,6 @@ async function register ({
364 359
365 // And/Or support this profile for live transcoding 360 // And/Or support this profile for live transcoding
366 transcodingManager.addLiveProfile(encoder, profileName, builder) 361 transcodingManager.addLiveProfile(encoder, profileName, builder)
367 // Note: this profile will fail for live transcode because it specifies videoFilters
368 } 362 }
369 363
370 { 364 {
@@ -401,7 +395,6 @@ async function register ({
401 const builder = () => { 395 const builder = () => {
402 return { 396 return {
403 inputOptions: [], 397 inputOptions: [],
404 videoFilters: [],
405 outputOptions: [] 398 outputOptions: []
406 } 399 }
407 } 400 }