Fix splitting audio/video of existing videos

This commit is contained in:
Chocobozzz 2024-09-25 13:49:21 +02:00
parent 7e9fba3ae5
commit 093a9bf749
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
5 changed files with 313 additions and 163 deletions

View File

@ -1,8 +1,7 @@
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
import { expect } from 'chai'
import { areMockObjectStorageTestsDisabled } from '@peertube/peertube-node-utils'
import { HttpStatusCode, VideoDetails } from '@peertube/peertube-models'
import { areMockObjectStorageTestsDisabled } from '@peertube/peertube-node-utils'
import {
cleanupTests,
ConfigCommand,
@ -16,7 +15,8 @@ import {
waitJobs
} from '@peertube/peertube-server-commands'
import { expectStartWith } from '@tests/shared/checks.js'
import { checkResolutionsInMasterPlaylist } from '@tests/shared/streaming-playlists.js'
import { checkResolutionsInMasterPlaylist, completeCheckHlsPlaylist } from '@tests/shared/streaming-playlists.js'
import { expect } from 'chai'
async function checkFilesInObjectStorage (objectStorage: ObjectStorageCommand, video: VideoDetails) {
for (const file of video.files) {
@ -81,175 +81,273 @@ function runTests (options: {
await servers[0].config.setTranscodingConcurrency(concurrency)
})
it('Should generate HLS', async function () {
this.timeout(60000)
describe('Common transcoding', function () {
await servers[0].videos.runTranscoding({
videoId: videoUUID,
transcodingType: 'hls'
it('Should generate HLS', async function () {
this.timeout(60000)
await servers[0].videos.runTranscoding({
videoId: videoUUID,
transcodingType: 'hls'
})
await waitJobs(servers)
await expectNoFailedTranscodingJob(servers[0])
for (const server of servers) {
const videoDetails = await server.videos.get({ id: videoUUID })
expect(videoDetails.files).to.have.lengthOf(1)
expect(videoDetails.streamingPlaylists).to.have.lengthOf(1)
expect(videoDetails.streamingPlaylists[0].files).to.have.lengthOf(5)
if (enableObjectStorage) await checkFilesInObjectStorage(objectStorage, videoDetails)
}
})
await waitJobs(servers)
await expectNoFailedTranscodingJob(servers[0])
it('Should generate Web Video', async function () {
this.timeout(60000)
for (const server of servers) {
const videoDetails = await server.videos.get({ id: videoUUID })
await servers[0].videos.runTranscoding({
videoId: videoUUID,
transcodingType: 'web-video'
})
expect(videoDetails.files).to.have.lengthOf(1)
expect(videoDetails.streamingPlaylists).to.have.lengthOf(1)
expect(videoDetails.streamingPlaylists[0].files).to.have.lengthOf(5)
await waitJobs(servers)
if (enableObjectStorage) await checkFilesInObjectStorage(objectStorage, videoDetails)
}
})
for (const server of servers) {
const videoDetails = await server.videos.get({ id: videoUUID })
it('Should generate Web Video', async function () {
this.timeout(60000)
expect(videoDetails.files).to.have.lengthOf(5)
expect(videoDetails.streamingPlaylists).to.have.lengthOf(1)
expect(videoDetails.streamingPlaylists[0].files).to.have.lengthOf(5)
await servers[0].videos.runTranscoding({
videoId: videoUUID,
transcodingType: 'web-video'
if (enableObjectStorage) await checkFilesInObjectStorage(objectStorage, videoDetails)
}
})
await waitJobs(servers)
it('Should generate Web Video from HLS only video', async function () {
this.timeout(60000)
for (const server of servers) {
const videoDetails = await server.videos.get({ id: videoUUID })
await servers[0].videos.removeAllWebVideoFiles({ videoId: videoUUID })
await waitJobs(servers)
expect(videoDetails.files).to.have.lengthOf(5)
expect(videoDetails.streamingPlaylists).to.have.lengthOf(1)
expect(videoDetails.streamingPlaylists[0].files).to.have.lengthOf(5)
await servers[0].videos.runTranscoding({ videoId: videoUUID, transcodingType: 'web-video' })
await waitJobs(servers)
if (enableObjectStorage) await checkFilesInObjectStorage(objectStorage, videoDetails)
}
})
for (const server of servers) {
const videoDetails = await server.videos.get({ id: videoUUID })
it('Should generate Web Video from HLS only video', async function () {
this.timeout(60000)
expect(videoDetails.files).to.have.lengthOf(5)
expect(videoDetails.streamingPlaylists).to.have.lengthOf(1)
expect(videoDetails.streamingPlaylists[0].files).to.have.lengthOf(5)
await servers[0].videos.removeAllWebVideoFiles({ videoId: videoUUID })
await waitJobs(servers)
if (enableObjectStorage) await checkFilesInObjectStorage(objectStorage, videoDetails)
}
})
await servers[0].videos.runTranscoding({ videoId: videoUUID, transcodingType: 'web-video' })
await waitJobs(servers)
it('Should only generate Web Video', async function () {
this.timeout(60000)
for (const server of servers) {
const videoDetails = await server.videos.get({ id: videoUUID })
await servers[0].videos.removeHLSPlaylist({ videoId: videoUUID })
await waitJobs(servers)
expect(videoDetails.files).to.have.lengthOf(5)
expect(videoDetails.streamingPlaylists).to.have.lengthOf(1)
expect(videoDetails.streamingPlaylists[0].files).to.have.lengthOf(5)
await servers[0].videos.runTranscoding({ videoId: videoUUID, transcodingType: 'web-video' })
await waitJobs(servers)
if (enableObjectStorage) await checkFilesInObjectStorage(objectStorage, videoDetails)
}
})
for (const server of servers) {
const videoDetails = await server.videos.get({ id: videoUUID })
it('Should only generate Web Video', async function () {
this.timeout(60000)
expect(videoDetails.files).to.have.lengthOf(5)
expect(videoDetails.streamingPlaylists).to.have.lengthOf(0)
await servers[0].videos.removeHLSPlaylist({ videoId: videoUUID })
await waitJobs(servers)
if (enableObjectStorage) await checkFilesInObjectStorage(objectStorage, videoDetails)
}
})
await servers[0].videos.runTranscoding({ videoId: videoUUID, transcodingType: 'web-video' })
await waitJobs(servers)
it('Should correctly update HLS playlist on resolution change', async function () {
this.timeout(120000)
for (const server of servers) {
const videoDetails = await server.videos.get({ id: videoUUID })
await servers[0].config.updateExistingConfig({
newConfig: {
transcoding: {
enabled: true,
resolutions: ConfigCommand.getConfigResolutions(false),
expect(videoDetails.files).to.have.lengthOf(5)
expect(videoDetails.streamingPlaylists).to.have.lengthOf(0)
if (enableObjectStorage) await checkFilesInObjectStorage(objectStorage, videoDetails)
}
})
it('Should correctly update HLS playlist on resolution change', async function () {
this.timeout(120000)
await servers[0].config.updateExistingConfig({
newConfig: {
transcoding: {
enabled: true,
resolutions: ConfigCommand.getConfigResolutions(false),
webVideos: {
enabled: true
},
hls: {
enabled: true
webVideos: {
enabled: true
},
hls: {
enabled: true
}
}
}
})
const { uuid } = await servers[0].videos.quickUpload({ name: 'quick' })
await waitJobs(servers)
for (const server of servers) {
const videoDetails = await server.videos.get({ id: uuid })
expect(videoDetails.files).to.have.lengthOf(1)
expect(videoDetails.streamingPlaylists).to.have.lengthOf(1)
expect(videoDetails.streamingPlaylists[0].files).to.have.lengthOf(1)
if (enableObjectStorage) await checkFilesInObjectStorage(objectStorage, videoDetails)
shouldBeDeleted = [
videoDetails.streamingPlaylists[0].files[0].fileUrl,
videoDetails.streamingPlaylists[0].playlistUrl,
videoDetails.streamingPlaylists[0].segmentsSha256Url
]
}
await servers[0].config.updateExistingConfig({
newConfig: {
transcoding: {
enabled: true,
resolutions: ConfigCommand.getConfigResolutions(true),
webVideos: {
enabled: true
},
hls: {
enabled: true
}
}
}
})
await servers[0].videos.runTranscoding({ videoId: uuid, transcodingType: 'hls' })
await waitJobs(servers)
for (const server of servers) {
const videoDetails = await server.videos.get({ id: uuid })
expect(videoDetails.streamingPlaylists).to.have.lengthOf(1)
expect(videoDetails.streamingPlaylists[0].files).to.have.lengthOf(5)
if (enableObjectStorage) {
await checkFilesInObjectStorage(objectStorage, videoDetails)
const hlsPlaylist = videoDetails.streamingPlaylists[0]
const resolutions = hlsPlaylist.files.map(f => f.resolution.id)
await checkResolutionsInMasterPlaylist({ server: servers[0], playlistUrl: hlsPlaylist.playlistUrl, resolutions })
const shaBody = await servers[0].streamingPlaylists.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url, withRetry: true })
expect(Object.keys(shaBody)).to.have.lengthOf(5)
}
}
})
const { uuid } = await servers[0].videos.quickUpload({ name: 'quick' })
await waitJobs(servers)
for (const server of servers) {
const videoDetails = await server.videos.get({ id: uuid })
expect(videoDetails.files).to.have.lengthOf(1)
expect(videoDetails.streamingPlaylists).to.have.lengthOf(1)
expect(videoDetails.streamingPlaylists[0].files).to.have.lengthOf(1)
if (enableObjectStorage) await checkFilesInObjectStorage(objectStorage, videoDetails)
shouldBeDeleted = [
videoDetails.streamingPlaylists[0].files[0].fileUrl,
videoDetails.streamingPlaylists[0].playlistUrl,
videoDetails.streamingPlaylists[0].segmentsSha256Url
]
}
await servers[0].config.updateExistingConfig({
newConfig: {
transcoding: {
enabled: true,
resolutions: ConfigCommand.getConfigResolutions(true),
webVideos: {
enabled: true
},
hls: {
enabled: true
}
}
it('Should have correctly deleted previous files', async function () {
for (const fileUrl of shouldBeDeleted) {
await makeRawRequest({ url: fileUrl, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
}
})
await servers[0].videos.runTranscoding({ videoId: uuid, transcodingType: 'hls' })
await waitJobs(servers)
it('Should not have updated published at attributes', async function () {
const video = await servers[0].videos.get({ id: videoUUID })
for (const server of servers) {
const videoDetails = await server.videos.get({ id: uuid })
expect(video.publishedAt).to.equal(publishedAt)
})
})
expect(videoDetails.streamingPlaylists).to.have.lengthOf(1)
expect(videoDetails.streamingPlaylists[0].files).to.have.lengthOf(5)
describe('With split audio and video', function () {
if (enableObjectStorage) {
await checkFilesInObjectStorage(objectStorage, videoDetails)
async function runTest (options: {
audio: boolean
hls: boolean
webVideo: boolean
afterWebVideo: boolean
resolutions?: number[]
}) {
let resolutions = options.resolutions
const hlsPlaylist = videoDetails.streamingPlaylists[0]
const resolutions = hlsPlaylist.files.map(f => f.resolution.id)
await checkResolutionsInMasterPlaylist({ server: servers[0], playlistUrl: hlsPlaylist.playlistUrl, resolutions })
if (!resolutions) {
resolutions = [ 720, 240 ]
const shaBody = await servers[0].streamingPlaylists.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url, withRetry: true })
expect(Object.keys(shaBody)).to.have.lengthOf(5)
if (options.audio) resolutions.push(0)
}
const objectStorageBaseUrl = enableObjectStorage
? objectStorage?.getMockPlaylistBaseUrl()
: undefined
await servers[0].config.enableTranscoding({
resolutions,
hls: options.hls,
splitAudioAndVideo: false,
webVideo: options.webVideo
})
const { uuid: videoUUID } = await servers[0].videos.quickUpload({ name: 'hls splitted' })
await waitJobs(servers)
await completeCheckHlsPlaylist({
servers,
resolutions,
videoUUID,
hlsOnly: !options.webVideo,
splittedAudio: false,
objectStorageBaseUrl
})
await servers[0].config.enableTranscoding({
resolutions,
hls: true,
splitAudioAndVideo: true,
webVideo: options.afterWebVideo
})
await servers[0].videos.runTranscoding({ videoId: videoUUID, transcodingType: 'hls' })
await waitJobs(servers)
if (options.afterWebVideo) {
await servers[0].videos.runTranscoding({ videoId: videoUUID, transcodingType: 'web-video' })
await waitJobs(servers)
}
await completeCheckHlsPlaylist({
servers,
resolutions,
videoUUID,
hlsOnly: !options.afterWebVideo,
splittedAudio: true,
objectStorageBaseUrl
})
}
})
it('Should have correctly deleted previous files', async function () {
for (const fileUrl of shouldBeDeleted) {
await makeRawRequest({ url: fileUrl, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
}
})
it('Should split audio and video from an existing Web & HLS video', async function () {
this.timeout(60000)
it('Should not have updated published at attributes', async function () {
const video = await servers[0].videos.get({ id: videoUUID })
await runTest({ webVideo: true, hls: true, afterWebVideo: true, audio: false })
})
expect(video.publishedAt).to.equal(publishedAt)
it('Should split audio and video from an existing HLS video without audio resolution', async function () {
this.timeout(60000)
await runTest({ webVideo: false, hls: true, afterWebVideo: true, audio: false })
})
it('Should split audio and video to a HLS only video from an existing HLS video without audio resolution', async function () {
this.timeout(60000)
await runTest({ webVideo: false, hls: true, afterWebVideo: false, audio: false })
})
it('Should split audio and video to a HLS only video from an existing HLS video with audio resolution', async function () {
this.timeout(60000)
await runTest({ webVideo: false, hls: true, afterWebVideo: false, audio: false })
})
it('Should split audio and video on HLS only video that only have 1 resolution', async function () {
this.timeout(60000)
await runTest({ webVideo: false, hls: true, afterWebVideo: false, audio: false, resolutions: [ 720 ] })
})
})
after(async function () {

View File

@ -243,6 +243,41 @@ describe('Test VOD transcoding in peertube-runner program', function () {
resolutions: [ 720, 480, 360, 240, 144, 0 ]
})
})
it('Should re-transcode a non splitted audio/video HLS only video', async function () {
this.timeout(240000)
const resolutions = [ 720, 240 ]
await servers[0].config.enableTranscoding({
hls: true,
webVideo: false,
resolutions,
splitAudioAndVideo: false
})
const { uuid } = await servers[0].videos.quickUpload({ name: 'manual hls only transcoding', fixture: 'video_short.mp4' })
await waitJobs(servers, { runnerJobs: true })
await servers[0].config.enableTranscoding({
hls: hlsEnabled,
webVideo: webVideoEnabled,
resolutions,
splitAudioAndVideo: splittedAudio
})
await servers[0].videos.runTranscoding({ transcodingType: 'hls', videoId: uuid })
await waitJobs(servers, { runnerJobs: true })
await completeCheckHlsPlaylist({
hlsOnly: true,
servers: [ servers[0] ],
videoUUID: uuid,
splittedAudio,
objectStorageBaseUrl: objectStorageBaseUrlHLS,
resolutions
})
})
}
before(async function () {
@ -266,10 +301,12 @@ describe('Test VOD transcoding in peertube-runner program', function () {
})
function runSuites (objectStorage?: ObjectStorageCommand) {
const resolutions = 'max'
describe('Web video only enabled', function () {
before(async function () {
await servers[0].config.enableTranscoding({ resolutions: 'max', webVideo: true, hls: false, with0p: true })
await servers[0].config.enableTranscoding({ resolutions, webVideo: true, hls: false, with0p: true })
})
runSpecificSuite({ webVideoEnabled: true, hlsEnabled: false, objectStorage })
@ -278,7 +315,7 @@ describe('Test VOD transcoding in peertube-runner program', function () {
describe('HLS videos only enabled', function () {
before(async function () {
await servers[0].config.enableTranscoding({ webVideo: false, hls: true, with0p: true })
await servers[0].config.enableTranscoding({ resolutions, webVideo: false, hls: true, with0p: true })
})
runSpecificSuite({ webVideoEnabled: false, hlsEnabled: true, objectStorage })
@ -287,7 +324,7 @@ describe('Test VOD transcoding in peertube-runner program', function () {
describe('HLS only with separated audio only enabled', function () {
before(async function () {
await servers[0].config.enableTranscoding({ webVideo: false, hls: true, splitAudioAndVideo: true, with0p: true })
await servers[0].config.enableTranscoding({ resolutions, webVideo: false, hls: true, splitAudioAndVideo: true, with0p: true })
})
runSpecificSuite({ webVideoEnabled: false, hlsEnabled: true, splittedAudio: true, objectStorage })
@ -296,7 +333,7 @@ describe('Test VOD transcoding in peertube-runner program', function () {
describe('Web video & HLS with separated audio only enabled', function () {
before(async function () {
await servers[0].config.enableTranscoding({ hls: true, webVideo: true, splitAudioAndVideo: true, with0p: true })
await servers[0].config.enableTranscoding({ resolutions, hls: true, webVideo: true, splitAudioAndVideo: true, with0p: true })
})
runSpecificSuite({ webVideoEnabled: true, hlsEnabled: true, splittedAudio: true, objectStorage })
@ -305,7 +342,7 @@ describe('Test VOD transcoding in peertube-runner program', function () {
describe('Web video & HLS enabled', function () {
before(async function () {
await servers[0].config.enableTranscoding({ hls: true, webVideo: true, with0p: true, splitAudioAndVideo: false })
await servers[0].config.enableTranscoding({ resolutions, hls: true, webVideo: true, with0p: true, splitAudioAndVideo: false })
})
runSpecificSuite({ webVideoEnabled: true, hlsEnabled: true, objectStorage })

View File

@ -127,8 +127,7 @@ export abstract class AbstractJobBuilder <P> {
}
await this.createJobs({
parent: mergeOrOptimizePayload,
children,
payloads: [ [ mergeOrOptimizePayload ], ...children ],
user,
video
})
@ -151,19 +150,24 @@ export abstract class AbstractJobBuilder <P> {
const inputFPS = video.getMaxFPS()
const children = childrenResolutions.map(resolution => {
const fps = computeOutputFPS({ inputFPS, resolution, isOriginResolution: maxResolution === resolution, type: 'vod' })
const children = childrenResolutions
.map(resolution => {
const fps = computeOutputFPS({ inputFPS, resolution, isOriginResolution: maxResolution === resolution, type: 'vod' })
if (transcodingType === 'hls') {
return this.buildHLSJobPayload({ video, resolution, fps, isNewVideo, separatedAudio })
}
if (transcodingType === 'hls') {
// We'll generate audio resolution in a parent job
if (resolution === VideoResolution.H_NOVIDEO && separatedAudio) return undefined
if (transcodingType === 'webtorrent' || transcodingType === 'web-video') {
return this.buildWebVideoJobPayload({ video, resolution, fps, isNewVideo })
}
return this.buildHLSJobPayload({ video, resolution, fps, isNewVideo, separatedAudio })
}
throw new Error('Unknown transcoding type')
})
if (transcodingType === 'webtorrent' || transcodingType === 'web-video') {
return this.buildWebVideoJobPayload({ video, resolution, fps, isNewVideo })
}
throw new Error('Unknown transcoding type')
})
.filter(r => !!r)
const fps = computeOutputFPS({ inputFPS, resolution: maxResolution, isOriginResolution: true, type: 'vod' })
@ -171,9 +175,17 @@ export abstract class AbstractJobBuilder <P> {
? this.buildHLSJobPayload({ video, resolution: maxResolution, fps, isNewVideo, separatedAudio })
: this.buildWebVideoJobPayload({ video, resolution: maxResolution, fps, isNewVideo })
// Process the last resolution after the other ones to prevent concurrency issue
// Because low resolutions use the biggest one as ffmpeg input
await this.createJobs({ video, parent, children: [ children ], user: null })
// Low resolutions use the biggest one as ffmpeg input so we need to process max resolution (with audio) independently
const payloads: [ [ P ], ...(P[][]) ] = [ [ parent ] ]
// Process audio first to not override the max resolution where the audio stream will be removed
if (transcodingType === 'hls' && separatedAudio) {
payloads.unshift([ this.buildHLSJobPayload({ video, resolution: VideoResolution.H_NOVIDEO, fps, isNewVideo, separatedAudio }) ])
}
if (children && children.length !== 0) payloads.push(children)
await this.createJobs({ video, payloads, user: null })
}
private async buildLowerResolutionJobPayloads (options: {
@ -247,8 +259,7 @@ export abstract class AbstractJobBuilder <P> {
protected abstract createJobs (options: {
video: MVideoFullLight
parent: P
children: P[][]
payloads: [ [ P ], ...(P[][]) ] // Array of sequential jobs to create that depend on parent job
user: MUserId | null
}): Promise<void>

View File

@ -22,14 +22,16 @@ export class TranscodingJobQueueBuilder extends AbstractJobBuilder <Payload> {
protected async createJobs (options: {
video: MVideo
parent: Payload
children: Payload[][]
payloads: [ [ Payload ], ...(Payload[][]) ] // Array of sequential jobs to create that depend on parent job
user: MUserId | null
}): Promise<void> {
const { video, parent, children, user } = options
const { video, payloads, user } = options
const nextTranscodingSequentialJobs = await Bluebird.mapSeries(children, payloads => {
return Bluebird.mapSeries(payloads, payload => {
const parent = payloads[0][0]
payloads.shift()
const nextTranscodingSequentialJobs = await Bluebird.mapSeries(payloads, p => {
return Bluebird.mapSeries(p, payload => {
return this.buildTranscodingJob({ payload, user })
})
})
@ -42,9 +44,9 @@ export class TranscodingJobQueueBuilder extends AbstractJobBuilder <Payload> {
}
}
const mergeOrOptimizeJob = await this.buildTranscodingJob({ payload: parent, user, hasChildren: !!children.length })
const parentJob = await this.buildTranscodingJob({ payload: parent, user, hasChildren: payloads.length !== 0 })
await JobQueue.Instance.createSequentialJobFlow(mergeOrOptimizeJob, transcodingJobBuilderJob)
await JobQueue.Instance.createSequentialJobFlow(parentJob, transcodingJobBuilderJob)
// transcoding-job-builder job will increase pendingTranscode
await VideoJobInfoModel.increaseOrCreate(video.uuid, 'pendingTranscode')

View File

@ -31,15 +31,17 @@ export class TranscodingRunnerJobBuilder extends AbstractJobBuilder <Payload> {
protected async createJobs (options: {
video: MVideo
parent: Payload
children: Payload[][] // Array of sequential jobs to create that depend on parent job
payloads: [ [ Payload ], ...(Payload[][]) ] // Array of sequential jobs to create that depend on parent job
user: MUserId | null
}): Promise<void> {
const { parent, children, user } = options
const { payloads, user } = options
const parent = payloads[0][0]
payloads.shift()
const parentJob = await this.createJob({ payload: parent, user })
for (const parallelPayloads of children) {
for (const parallelPayloads of payloads) {
let lastJob = parentJob
for (const parallelPayload of parallelPayloads) {