From 319932c1debc1722a937316b8cb7734200a5b1b7 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 31 Jan 2025 11:28:30 +0100 Subject: [PATCH] Fix adding an intro/outro with splitted HLS --- packages/ffmpeg/src/ffmpeg-edition.ts | 27 +- .../tests/src/api/transcoding/video-studio.ts | 468 +++++++++--------- 2 files changed, 255 insertions(+), 240 deletions(-) diff --git a/packages/ffmpeg/src/ffmpeg-edition.ts b/packages/ffmpeg/src/ffmpeg-edition.ts index 58d865291..510533bb4 100644 --- a/packages/ffmpeg/src/ffmpeg-edition.ts +++ b/packages/ffmpeg/src/ffmpeg-edition.ts @@ -127,7 +127,8 @@ export class FFmpegEdition { const mainProbe = await ffprobePromise(videoInputPath) const fps = await getVideoStreamFPS(videoInputPath, mainProbe) const { resolution } = await getVideoStreamDimensionsInfo(videoInputPath, mainProbe) - const mainHasAudio = await hasAudioStream(separatedAudioInputPath || videoInputPath, mainProbe) + + const mainHasAudio = await hasAudioStream(separatedAudioInputPath || videoInputPath) const introOutroProbe = await ffprobePromise(introOutroPath) const introOutroHasAudio = await hasAudioStream(introOutroPath, introOutroProbe) @@ -135,12 +136,23 @@ export class FFmpegEdition { const command = this.commandWrapper.buildCommand([ ...this.buildInputs(options), introOutroPath ], inputFileMutexReleaser) .output(outputPath) + const videoInput = 0 + + const audioInput = separatedAudioInputPath + ? videoInput + 1 + : videoInput + + const introInput = audioInput + 1 + let introAudio = introInput + if (!introOutroHasAudio && mainHasAudio) { const duration = await getVideoStreamDuration(introOutroPath, introOutroProbe) command.input('anullsrc') command.withInputFormat('lavfi') command.withInputOption('-t ' + duration) + + introAudio++ } await presetVOD({ @@ -159,7 +171,7 @@ export class FFmpegEdition { // Add black background to correctly scale intro/outro with padding const complexFilter: FilterSpecification[] = [ { - inputs: [ '1', '0' ], + inputs: [ `${introInput}`, `${videoInput}` ], filter: 'scale2ref', options: { w: 'iw', @@ -185,7 +197,7 @@ export class FFmpegEdition { outputs: [ 'to-scale-bg' ] }, { - inputs: [ '1', 'to-scale-bg' ], + inputs: [ `${introInput}`, 'to-scale-bg' ], filter: 'scale2ref', options: { w: 'iw', @@ -221,14 +233,9 @@ export class FFmpegEdition { const mainFilterInputs = [ 'main' ] if (mainHasAudio) { - mainFilterInputs.push('0:a') + mainFilterInputs.push(`${audioInput}:a`) - if (introOutroHasAudio) { - introOutroFilterInputs.push('1:a') - } else { - // Silent input - introOutroFilterInputs.push('2:a') - } + introOutroFilterInputs.push(`${introAudio}:a`) } if (type === 'intro') { diff --git a/packages/tests/src/api/transcoding/video-studio.ts b/packages/tests/src/api/transcoding/video-studio.ts index 48f514f9a..b84511bed 100644 --- a/packages/tests/src/api/transcoding/video-studio.ts +++ b/packages/tests/src/api/transcoding/video-studio.ts @@ -1,4 +1,3 @@ -import { expect } from 'chai' import { getAllFiles, getHLS } from '@peertube/peertube-core-utils' import { VideoStudioTask } from '@peertube/peertube-models' import { areMockObjectStorageTestsDisabled } from '@peertube/peertube-node-utils' @@ -16,6 +15,7 @@ import { import { checkVideoDuration, expectStartWith } from '@tests/shared/checks.js' import { checkPersistentTmpIsEmpty } from '@tests/shared/directories.js' import { completeCheckHlsPlaylist } from '@tests/shared/streaming-playlists.js' +import { expect } from 'chai' describe('Test video studio', function () { let servers: PeerTubeServer[] = [] @@ -48,254 +48,258 @@ describe('Test video studio', function () { await servers[0].config.enableStudio() }) - describe('Cutting', function () { + function runCommonTests () { + describe('Cutting', function () { - it('Should cut the beginning of the video', async function () { - this.timeout(120_000) + it('Should cut the beginning of the video', async function () { + this.timeout(120_000) - await renewVideo() - await waitJobs(servers) + await renewVideo() + await waitJobs(servers) - const beforeTasks = new Date() + const beforeTasks = new Date() - await createTasks([ - { - name: 'cut', - options: { - start: 2 + await createTasks([ + { + name: 'cut', + options: { + start: 2 + } } + ]) + + for (const server of servers) { + await checkVideoDuration(server, videoUUID, 3) + + const video = await server.videos.get({ id: videoUUID }) + expect(new Date(video.publishedAt)).to.be.below(beforeTasks) } - ]) + }) - for (const server of servers) { - await checkVideoDuration(server, videoUUID, 3) + it('Should cut the end of the video', async function () { + this.timeout(120_000) + await renewVideo() - const video = await server.videos.get({ id: videoUUID }) - expect(new Date(video.publishedAt)).to.be.below(beforeTasks) - } + await createTasks([ + { + name: 'cut', + options: { + end: 2 + } + } + ]) + + for (const server of servers) { + await checkVideoDuration(server, videoUUID, 2) + } + }) + + it('Should cut start/end of the video', async function () { + this.timeout(120_000) + await renewVideo('video_short1.webm') // 10 seconds video duration + + await createTasks([ + { + name: 'cut', + options: { + start: 2, + end: 6 + } + } + ]) + + for (const server of servers) { + await checkVideoDuration(server, videoUUID, 4) + } + }) + + it('Should cut start/end of the audio', async function () { + this.timeout(120_000) + + await servers[0].config.enableMinimumTranscoding({ splitAudioAndVideo: true }) + await renewVideo('video_short1.webm') + await servers[0].config.enableMinimumTranscoding() + + const video = await servers[0].videos.get({ id: videoUUID }) + for (const file of video.files) { + if (file.resolution.id === 0) continue + + await servers[0].videos.removeWebVideoFile({ fileId: file.id, videoId: videoUUID }) + } + + for (const file of getHLS(video).files) { + if (file.resolution.id === 0) continue + + await servers[0].videos.removeHLSFile({ fileId: file.id, videoId: videoUUID }) + } + + await createTasks([ + { + name: 'cut', + options: { + start: 2, + end: 6 + } + } + ]) + + for (const server of servers) { + await checkVideoDuration(server, videoUUID, 4) + } + }) }) - it('Should cut the end of the video', async function () { - this.timeout(120_000) - await renewVideo() + describe('Intro/Outro', function () { - await createTasks([ - { - name: 'cut', - options: { - end: 2 + it('Should add an intro', async function () { + this.timeout(120_000) + await renewVideo() + + await createTasks([ + { + name: 'add-intro', + options: { + file: 'video_short.webm' + } } - } - ]) + ]) - for (const server of servers) { - await checkVideoDuration(server, videoUUID, 2) - } + for (const server of servers) { + await checkVideoDuration(server, videoUUID, 10) + } + }) + + it('Should add an outro', async function () { + this.timeout(120_000) + await renewVideo() + + await createTasks([ + { + name: 'add-outro', + options: { + file: 'video_very_short_240p.mp4' + } + } + ]) + + for (const server of servers) { + await checkVideoDuration(server, videoUUID, 7) + } + }) + + it('Should add an intro/outro', async function () { + this.timeout(120_000) + await renewVideo() + + await createTasks([ + { + name: 'add-intro', + options: { + file: 'video_very_short_240p.mp4' + } + }, + { + name: 'add-outro', + options: { + // Different frame rate + file: 'video_short2.webm' + } + } + ]) + + for (const server of servers) { + await checkVideoDuration(server, videoUUID, 12) + } + }) + + it('Should add an intro to a video without audio', async function () { + this.timeout(120_000) + await renewVideo('video_short_no_audio.mp4') + + await createTasks([ + { + name: 'add-intro', + options: { + file: 'video_very_short_240p.mp4' + } + } + ]) + + for (const server of servers) { + await checkVideoDuration(server, videoUUID, 7) + } + }) + + it('Should add an outro without audio to a video with audio', async function () { + this.timeout(120_000) + await renewVideo() + + await createTasks([ + { + name: 'add-outro', + options: { + file: 'video_short_no_audio.mp4' + } + } + ]) + + for (const server of servers) { + await checkVideoDuration(server, videoUUID, 10) + } + }) + + it('Should add an outro without audio to a video without audio', async function () { + this.timeout(120_000) + await renewVideo('video_short_no_audio.mp4') + + await createTasks([ + { + name: 'add-outro', + options: { + file: 'video_short_no_audio.mp4' + } + } + ]) + + for (const server of servers) { + await checkVideoDuration(server, videoUUID, 10) + } + }) }) - it('Should cut start/end of the video', async function () { - this.timeout(120_000) - await renewVideo('video_short1.webm') // 10 seconds video duration + describe('Watermark', function () { - await createTasks([ - { - name: 'cut', - options: { - start: 2, - end: 6 + it('Should add a watermark to the video', async function () { + this.timeout(120_000) + await renewVideo() + + const video = await servers[0].videos.get({ id: videoUUID }) + const oldFileUrls = getAllFiles(video).map(f => f.fileUrl) + + await createTasks([ + { + name: 'add-watermark', + options: { + file: 'custom-thumbnail.png' + } + } + ]) + + for (const server of servers) { + const video = await server.videos.get({ id: videoUUID }) + const fileUrls = getAllFiles(video).map(f => f.fileUrl) + + for (const oldUrl of oldFileUrls) { + expect(fileUrls).to.not.include(oldUrl) } } - ]) - - for (const server of servers) { - await checkVideoDuration(server, videoUUID, 4) - } + }) }) + } - it('Should cut start/end of the audio', async function () { - this.timeout(120_000) + describe('Web videos enabled', function () { - await servers[0].config.enableMinimumTranscoding({ splitAudioAndVideo: true }) - await renewVideo('video_short1.webm') - await servers[0].config.enableMinimumTranscoding() - - const video = await servers[0].videos.get({ id: videoUUID }) - for (const file of video.files) { - if (file.resolution.id === 0) continue - - await servers[0].videos.removeWebVideoFile({ fileId: file.id, videoId: videoUUID }) - } - - for (const file of getHLS(video).files) { - if (file.resolution.id === 0) continue - - await servers[0].videos.removeHLSFile({ fileId: file.id, videoId: videoUUID }) - } - - await createTasks([ - { - name: 'cut', - options: { - start: 2, - end: 6 - } - } - ]) - - for (const server of servers) { - await checkVideoDuration(server, videoUUID, 4) - } - }) - }) - - describe('Intro/Outro', function () { - - it('Should add an intro', async function () { - this.timeout(120_000) - await renewVideo() - - await createTasks([ - { - name: 'add-intro', - options: { - file: 'video_short.webm' - } - } - ]) - - for (const server of servers) { - await checkVideoDuration(server, videoUUID, 10) - } - }) - - it('Should add an outro', async function () { - this.timeout(120_000) - await renewVideo() - - await createTasks([ - { - name: 'add-outro', - options: { - file: 'video_very_short_240p.mp4' - } - } - ]) - - for (const server of servers) { - await checkVideoDuration(server, videoUUID, 7) - } - }) - - it('Should add an intro/outro', async function () { - this.timeout(120_000) - await renewVideo() - - await createTasks([ - { - name: 'add-intro', - options: { - file: 'video_very_short_240p.mp4' - } - }, - { - name: 'add-outro', - options: { - // Different frame rate - file: 'video_short2.webm' - } - } - ]) - - for (const server of servers) { - await checkVideoDuration(server, videoUUID, 12) - } - }) - - it('Should add an intro to a video without audio', async function () { - this.timeout(120_000) - await renewVideo('video_short_no_audio.mp4') - - await createTasks([ - { - name: 'add-intro', - options: { - file: 'video_very_short_240p.mp4' - } - } - ]) - - for (const server of servers) { - await checkVideoDuration(server, videoUUID, 7) - } - }) - - it('Should add an outro without audio to a video with audio', async function () { - this.timeout(120_000) - await renewVideo() - - await createTasks([ - { - name: 'add-outro', - options: { - file: 'video_short_no_audio.mp4' - } - } - ]) - - for (const server of servers) { - await checkVideoDuration(server, videoUUID, 10) - } - }) - - it('Should add an outro without audio to a video with audio', async function () { - this.timeout(120_000) - await renewVideo('video_short_no_audio.mp4') - - await createTasks([ - { - name: 'add-outro', - options: { - file: 'video_short_no_audio.mp4' - } - } - ]) - - for (const server of servers) { - await checkVideoDuration(server, videoUUID, 10) - } - }) - }) - - describe('Watermark', function () { - - it('Should add a watermark to the video', async function () { - this.timeout(120_000) - await renewVideo() - - const video = await servers[0].videos.get({ id: videoUUID }) - const oldFileUrls = getAllFiles(video).map(f => f.fileUrl) - - await createTasks([ - { - name: 'add-watermark', - options: { - file: 'custom-thumbnail.png' - } - } - ]) - - for (const server of servers) { - const video = await server.videos.get({ id: videoUUID }) - const fileUrls = getAllFiles(video).map(f => f.fileUrl) - - for (const oldUrl of oldFileUrls) { - expect(fileUrls).to.not.include(oldUrl) - } - } - }) - }) - - describe('Complex tasks', function () { + runCommonTests() it('Should run a complex task', async function () { this.timeout(240_000) @@ -315,7 +319,9 @@ describe('Test video studio', function () { await servers[0].config.enableMinimumTranscoding({ webVideo: false, hls: true }) }) - it('Should run a complex task on HLS only video', async function () { + runCommonTests() + + it('Should run a complex task', async function () { this.timeout(240_000) await renewVideo() @@ -338,7 +344,9 @@ describe('Test video studio', function () { await servers[0].config.enableMinimumTranscoding({ webVideo: false, hls: true, splitAudioAndVideo: true }) }) - it('Should run a complex task on HLS only video', async function () { + runCommonTests() + + it('Should run a complex task', async function () { this.timeout(240_000) await renewVideo()