diff --git a/client/e2e/src/suites-local/publish.e2e-spec.ts b/client/e2e/src/suites-local/publish.e2e-spec.ts index 152511a32..18745db68 100644 --- a/client/e2e/src/suites-local/publish.e2e-spec.ts +++ b/client/e2e/src/suites-local/publish.e2e-spec.ts @@ -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 diff --git a/packages/tests/src/api/live/live-privacy-update.ts b/packages/tests/src/api/live/live-privacy-update.ts index ff12ff3e3..139e32a1c 100644 --- a/packages/tests/src/api/live/live-privacy-update.ts +++ b/packages/tests/src/api/live/live-privacy-update.ts @@ -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 }) }) }) diff --git a/packages/tests/src/api/videos/video-files.ts b/packages/tests/src/api/videos/video-files.ts index 2fc179570..242f737cb 100644 --- a/packages/tests/src/api/videos/video-files.ts +++ b/packages/tests/src/api/videos/video-files.ts @@ -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 }) diff --git a/server/core/lib/job-queue/handlers/video-live-ending.ts b/server/core/lib/job-queue/handlers/video-live-ending.ts index 55ab4cf47..3f899db3e 100644 --- a/server/core/lib/job-queue/handlers/video-live-ending.ts +++ b/server/core/lib/job-queue/handlers/video-live-ending.ts @@ -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? diff --git a/server/core/lib/live/live-manager.ts b/server/core/lib/live/live-manager.ts index 4661889cf..b9e41fa28 100644 --- a/server/core/lib/live/live-manager.ts +++ b/server/core/lib/live/live-manager.ts @@ -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) {