Fix broken replay on live privacy change

This commit is contained in:
Chocobozzz 2025-05-14 16:20:35 +02:00
parent 7b06f37b22
commit 6f3b827d6c
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
5 changed files with 113 additions and 65 deletions

View File

@ -1,7 +1,7 @@
import { LoginPage } from '../po/login.po'
import { VideoPublishPage } from '../po/video-publish.po'
import { VideoWatchPage } from '../po/video-watch.po'
import { browserSleep, getScreenshotPath, isMobileDevice, isSafari, waitServerUp } from '../utils'
import { getScreenshotPath, isMobileDevice, isSafari, waitServerUp } from '../utils'
describe('Publish video', () => {
let videoPublishPage: VideoPublishPage

View File

@ -2,23 +2,42 @@
import { HttpStatusCode, LiveVideoCreate, VideoPrivacy } from '@peertube/peertube-models'
import {
cleanupTests, createSingleServer, makeRawRequest,
cleanupTests,
createSingleServer,
findExternalSavedVideo,
makeRawRequest,
PeerTubeServer,
setAccessTokensToServers,
setDefaultVideoChannel,
stopFfmpeg,
waitJobs,
waitUntilLivePublishedOnAllServers,
waitUntilLiveReplacedByReplayOnAllServers
waitUntilLiveReplacedByReplayOnAllServers,
waitUntilLiveWaitingOnAllServers
} from '@peertube/peertube-server-commands'
import { expect } from 'chai'
async function testVideoFiles (options: {
server: PeerTubeServer
uuid: string
isPrivate: boolean
}) {
const { server, uuid, isPrivate } = options
async function testVideoFiles (server: PeerTubeServer, uuid: string) {
const video = await server.videos.getWithToken({ id: uuid })
const playlist = video.streamingPlaylists[0]
const expectedStatus = HttpStatusCode.OK_200
const urls = [ playlist.playlistUrl, playlist.segmentsSha256Url ]
await makeRawRequest({ url: video.streamingPlaylists[0].playlistUrl, token: server.accessToken, expectedStatus })
await makeRawRequest({ url: video.streamingPlaylists[0].segmentsSha256Url, token: server.accessToken, expectedStatus })
for (const url of urls) {
await makeRawRequest({ url, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
if (isPrivate) {
expect(url).to.not.include('/private/')
} else {
expect(url).to.include('/private/')
}
}
}
describe('Live privacy update', function () {
@ -43,7 +62,7 @@ describe('Live privacy update', function () {
this.timeout(120000)
const fields: LiveVideoCreate = {
name: 'live',
name: 'normal live',
privacy: VideoPrivacy.PUBLIC,
permanentLive: false,
replaySettings: { privacy: VideoPrivacy.PRIVATE },
@ -61,7 +80,7 @@ describe('Live privacy update', function () {
await waitUntilLiveReplacedByReplayOnAllServers([ server ], uuid)
await waitJobs([ server ])
await testVideoFiles(server, uuid)
await testVideoFiles({ server, uuid, isPrivate: false })
})
it('Should update the replay to public and re-update it to private', async function () {
@ -69,11 +88,44 @@ describe('Live privacy update', function () {
await server.videos.update({ id: uuid, attributes: { privacy: VideoPrivacy.PUBLIC } })
await waitJobs([ server ])
await testVideoFiles(server, uuid)
await testVideoFiles({ server, uuid, isPrivate: true })
await server.videos.update({ id: uuid, attributes: { privacy: VideoPrivacy.PRIVATE } })
await waitJobs([ server ])
await testVideoFiles(server, uuid)
await testVideoFiles({ server, uuid, isPrivate: false })
})
})
describe('Permanent live', function () {
let liveUUID: string
it('Should update the permanent live privacy but still process the replay', async function () {
this.timeout(120000)
const fields: LiveVideoCreate = {
name: 'permanent live',
privacy: VideoPrivacy.PUBLIC,
permanentLive: true,
replaySettings: { privacy: VideoPrivacy.PUBLIC },
saveReplay: true,
channelId: server.store.channel.id
}
const video = await server.live.create({ fields })
liveUUID = video.uuid
const ffmpegCommand = await server.live.sendRTMPStreamInVideo({ videoId: liveUUID })
await waitUntilLivePublishedOnAllServers([ server ], liveUUID)
await stopFfmpeg(ffmpegCommand)
await waitUntilLiveWaitingOnAllServers([ server ], liveUUID)
await server.videos.update({ id: liveUUID, attributes: { privacy: VideoPrivacy.PRIVATE } })
await waitJobs([ server ])
const replay = await findExternalSavedVideo(server, liveUUID)
expect(replay).to.exist
await testVideoFiles({ server, uuid: replay.uuid, isPrivate: true })
})
})

View File

@ -102,7 +102,7 @@ describe('Test videos files', function () {
await waitJobs(servers)
})
it('Shoulde delete a web video file', async function () {
it('Should delete a web video file', async function () {
this.timeout(30_000)
const video = await servers[0].videos.get({ id: webVideoId })

View File

@ -39,13 +39,13 @@ import {
import { Job } from 'bullmq'
import { pathExists, remove } from 'fs-extra/esm'
import { readdir } from 'fs/promises'
import { join } from 'path'
import { isAbsolute, join } from 'path'
import { logger, loggerTagsFactory } from '../../../helpers/logger.js'
import { JobQueue } from '../job-queue.js'
const lTags = loggerTagsFactory('live', 'job')
async function processVideoLiveEnding (job: Job) {
export async function processVideoLiveEnding (job: Job) {
const payload = job.data as VideoLiveEndingPayload
logger.info('Processing video live ending for %s.', payload.videoId, { payload, ...lTags() })
@ -72,38 +72,47 @@ async function processVideoLiveEnding (job: Job) {
return cleanupLiveAndFederate({ permanentLive, video, streamingPlaylistId: payload.streamingPlaylistId })
}
if (await hasReplayFiles(payload.replayDirectory) !== true) {
logger.info(`No replay files found for live ${video.uuid}, skipping video replay creation.`, { ...lTags(video.uuid) })
let replayDirectory = payload.replayDirectory
return cleanupLiveAndFederate({ permanentLive, video, streamingPlaylistId: payload.streamingPlaylistId })
// Introduced in PeerTube 7.2, allow to use the appropriate base directory even if the live privacy changed
if (!isAbsolute(replayDirectory)) {
replayDirectory = join(getLiveReplayBaseDirectory(video), replayDirectory)
}
if (permanentLive) {
await saveReplayToExternalVideo({
liveVideo: video,
liveSession,
publishedAt: payload.publishedAt,
replayDirectory: payload.replayDirectory
})
const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid)
return cleanupLiveAndFederate({ permanentLive, video, streamingPlaylistId: payload.streamingPlaylistId })
try {
await video.reload()
if (await hasReplayFiles(replayDirectory) !== true) {
logger.info(`No replay files found for live ${video.uuid}, skipping video replay creation.`, { ...lTags(video.uuid) })
await cleanupLiveAndFederate({ permanentLive, video, streamingPlaylistId: payload.streamingPlaylistId })
} else if (permanentLive) {
await saveReplayToExternalVideo({
liveVideo: video,
liveSession,
publishedAt: payload.publishedAt,
replayDirectory
})
await cleanupLiveAndFederate({ permanentLive, video, streamingPlaylistId: payload.streamingPlaylistId })
} else {
await replaceLiveByReplay({
video,
liveSession,
live,
permanentLive,
replayDirectory
})
}
} finally {
inputFileMutexReleaser()
}
return replaceLiveByReplay({
video,
liveSession,
live,
permanentLive,
replayDirectory: payload.replayDirectory
})
}
// ---------------------------------------------------------------------------
export {
processVideoLiveEnding
}
// Private
// ---------------------------------------------------------------------------
async function saveReplayToExternalVideo (options: {
@ -167,16 +176,10 @@ async function saveReplayToExternalVideo (options: {
})
}
const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(liveVideo.uuid)
await assignReplayFilesToVideo({ video: replayVideo, replayDirectory })
try {
await assignReplayFilesToVideo({ video: replayVideo, replayDirectory })
logger.info(`Removing replay directory ${replayDirectory}`, lTags(liveVideo.uuid))
await remove(replayDirectory)
} finally {
inputFileMutexReleaser()
}
logger.info(`Removing replay directory ${replayDirectory}`, lTags(liveVideo.uuid))
await remove(replayDirectory)
try {
await copyOrRegenerateThumbnails({ liveVideo, replayVideo })
@ -266,25 +269,19 @@ async function replaceLiveByReplay (options: {
hlsPlaylist.segmentsSha256Filename = generateHlsSha256SegmentsFilename()
await hlsPlaylist.save()
const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(videoWithFiles.uuid)
await assignReplayFilesToVideo({ video: videoWithFiles, replayDirectory })
try {
await assignReplayFilesToVideo({ video: videoWithFiles, replayDirectory })
// Should not happen in this function, but we keep the code if in the future we can replace the permanent live by a replay
if (permanentLive) { // Remove session replay
await remove(replayDirectory)
} else {
// We won't stream again in this live, we can delete the base replay directory
await remove(getLiveReplayBaseDirectory(liveVideo))
// Should not happen in this function, but we keep the code if in the future we can replace the permanent live by a replay
if (permanentLive) { // Remove session replay
await remove(replayDirectory)
} else {
// We won't stream again in this live, we can delete the base replay directory
await remove(getLiveReplayBaseDirectory(liveVideo))
// If the live was in another base directory, also delete it
if (replayInAnotherDirectory) {
await remove(getHLSDirectory(liveVideo))
}
// If the live was in another base directory, also delete it
if (replayInAnotherDirectory) {
await remove(getHLSDirectory(liveVideo))
}
} finally {
inputFileMutexReleaser()
}
// Regenerate the thumbnail & preview?

View File

@ -27,7 +27,6 @@ import { Server, createServer } from 'net'
import context from 'node-media-server/src/node_core_ctx.js'
import nodeMediaServerLogger from 'node-media-server/src/node_core_logger.js'
import NodeRtmpSession from 'node-media-server/src/node_rtmp_session.js'
import { join } from 'path'
import { Server as ServerTLS, createServer as createServerTLS } from 'tls'
import { federateVideoIfNeeded } from '../activitypub/videos/index.js'
import { JobQueue } from '../job-queue/index.js'
@ -621,7 +620,7 @@ class LiveManager {
if (files.length === 0) return undefined
return join(directory, files.sort().reverse()[0])
return files.sort().reverse()[0]
}
private buildAllResolutionsToTranscode (originResolution: number, hasAudio: boolean) {