mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2025-10-03 17:59:37 +02:00
Fix broken replay on live privacy change
This commit is contained in:
parent
7b06f37b22
commit
6f3b827d6c
5 changed files with 113 additions and 65 deletions
|
@ -1,7 +1,7 @@
|
||||||
import { LoginPage } from '../po/login.po'
|
import { LoginPage } from '../po/login.po'
|
||||||
import { VideoPublishPage } from '../po/video-publish.po'
|
import { VideoPublishPage } from '../po/video-publish.po'
|
||||||
import { VideoWatchPage } from '../po/video-watch.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', () => {
|
describe('Publish video', () => {
|
||||||
let videoPublishPage: VideoPublishPage
|
let videoPublishPage: VideoPublishPage
|
||||||
|
|
|
@ -2,23 +2,42 @@
|
||||||
|
|
||||||
import { HttpStatusCode, LiveVideoCreate, VideoPrivacy } from '@peertube/peertube-models'
|
import { HttpStatusCode, LiveVideoCreate, VideoPrivacy } from '@peertube/peertube-models'
|
||||||
import {
|
import {
|
||||||
cleanupTests, createSingleServer, makeRawRequest,
|
cleanupTests,
|
||||||
|
createSingleServer,
|
||||||
|
findExternalSavedVideo,
|
||||||
|
makeRawRequest,
|
||||||
PeerTubeServer,
|
PeerTubeServer,
|
||||||
setAccessTokensToServers,
|
setAccessTokensToServers,
|
||||||
setDefaultVideoChannel,
|
setDefaultVideoChannel,
|
||||||
stopFfmpeg,
|
stopFfmpeg,
|
||||||
waitJobs,
|
waitJobs,
|
||||||
waitUntilLivePublishedOnAllServers,
|
waitUntilLivePublishedOnAllServers,
|
||||||
waitUntilLiveReplacedByReplayOnAllServers
|
waitUntilLiveReplacedByReplayOnAllServers,
|
||||||
|
waitUntilLiveWaitingOnAllServers
|
||||||
} from '@peertube/peertube-server-commands'
|
} 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 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 })
|
for (const url of urls) {
|
||||||
await makeRawRequest({ url: video.streamingPlaylists[0].segmentsSha256Url, token: server.accessToken, expectedStatus })
|
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 () {
|
describe('Live privacy update', function () {
|
||||||
|
@ -43,7 +62,7 @@ describe('Live privacy update', function () {
|
||||||
this.timeout(120000)
|
this.timeout(120000)
|
||||||
|
|
||||||
const fields: LiveVideoCreate = {
|
const fields: LiveVideoCreate = {
|
||||||
name: 'live',
|
name: 'normal live',
|
||||||
privacy: VideoPrivacy.PUBLIC,
|
privacy: VideoPrivacy.PUBLIC,
|
||||||
permanentLive: false,
|
permanentLive: false,
|
||||||
replaySettings: { privacy: VideoPrivacy.PRIVATE },
|
replaySettings: { privacy: VideoPrivacy.PRIVATE },
|
||||||
|
@ -61,7 +80,7 @@ describe('Live privacy update', function () {
|
||||||
await waitUntilLiveReplacedByReplayOnAllServers([ server ], uuid)
|
await waitUntilLiveReplacedByReplayOnAllServers([ server ], uuid)
|
||||||
await waitJobs([ server ])
|
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 () {
|
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 server.videos.update({ id: uuid, attributes: { privacy: VideoPrivacy.PUBLIC } })
|
||||||
await waitJobs([ server ])
|
await waitJobs([ server ])
|
||||||
await testVideoFiles(server, uuid)
|
await testVideoFiles({ server, uuid, isPrivate: true })
|
||||||
|
|
||||||
await server.videos.update({ id: uuid, attributes: { privacy: VideoPrivacy.PRIVATE } })
|
await server.videos.update({ id: uuid, attributes: { privacy: VideoPrivacy.PRIVATE } })
|
||||||
await waitJobs([ server ])
|
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 })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -102,7 +102,7 @@ describe('Test videos files', function () {
|
||||||
await waitJobs(servers)
|
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)
|
this.timeout(30_000)
|
||||||
|
|
||||||
const video = await servers[0].videos.get({ id: webVideoId })
|
const video = await servers[0].videos.get({ id: webVideoId })
|
||||||
|
|
|
@ -39,13 +39,13 @@ import {
|
||||||
import { Job } from 'bullmq'
|
import { Job } from 'bullmq'
|
||||||
import { pathExists, remove } from 'fs-extra/esm'
|
import { pathExists, remove } from 'fs-extra/esm'
|
||||||
import { readdir } from 'fs/promises'
|
import { readdir } from 'fs/promises'
|
||||||
import { join } from 'path'
|
import { isAbsolute, join } from 'path'
|
||||||
import { logger, loggerTagsFactory } from '../../../helpers/logger.js'
|
import { logger, loggerTagsFactory } from '../../../helpers/logger.js'
|
||||||
import { JobQueue } from '../job-queue.js'
|
import { JobQueue } from '../job-queue.js'
|
||||||
|
|
||||||
const lTags = loggerTagsFactory('live', 'job')
|
const lTags = loggerTagsFactory('live', 'job')
|
||||||
|
|
||||||
async function processVideoLiveEnding (job: Job) {
|
export async function processVideoLiveEnding (job: Job) {
|
||||||
const payload = job.data as VideoLiveEndingPayload
|
const payload = job.data as VideoLiveEndingPayload
|
||||||
|
|
||||||
logger.info('Processing video live ending for %s.', payload.videoId, { payload, ...lTags() })
|
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 })
|
return cleanupLiveAndFederate({ permanentLive, video, streamingPlaylistId: payload.streamingPlaylistId })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await hasReplayFiles(payload.replayDirectory) !== true) {
|
let replayDirectory = payload.replayDirectory
|
||||||
logger.info(`No replay files found for live ${video.uuid}, skipping video replay creation.`, { ...lTags(video.uuid) })
|
|
||||||
|
|
||||||
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) {
|
const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid)
|
||||||
await saveReplayToExternalVideo({
|
|
||||||
liveVideo: video,
|
|
||||||
liveSession,
|
|
||||||
publishedAt: payload.publishedAt,
|
|
||||||
replayDirectory: payload.replayDirectory
|
|
||||||
})
|
|
||||||
|
|
||||||
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
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
// Private
|
||||||
export {
|
|
||||||
processVideoLiveEnding
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async function saveReplayToExternalVideo (options: {
|
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 {
|
logger.info(`Removing replay directory ${replayDirectory}`, lTags(liveVideo.uuid))
|
||||||
await assignReplayFilesToVideo({ video: replayVideo, replayDirectory })
|
await remove(replayDirectory)
|
||||||
|
|
||||||
logger.info(`Removing replay directory ${replayDirectory}`, lTags(liveVideo.uuid))
|
|
||||||
await remove(replayDirectory)
|
|
||||||
} finally {
|
|
||||||
inputFileMutexReleaser()
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await copyOrRegenerateThumbnails({ liveVideo, replayVideo })
|
await copyOrRegenerateThumbnails({ liveVideo, replayVideo })
|
||||||
|
@ -266,25 +269,19 @@ async function replaceLiveByReplay (options: {
|
||||||
hlsPlaylist.segmentsSha256Filename = generateHlsSha256SegmentsFilename()
|
hlsPlaylist.segmentsSha256Filename = generateHlsSha256SegmentsFilename()
|
||||||
await hlsPlaylist.save()
|
await hlsPlaylist.save()
|
||||||
|
|
||||||
const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(videoWithFiles.uuid)
|
await assignReplayFilesToVideo({ video: videoWithFiles, replayDirectory })
|
||||||
|
|
||||||
try {
|
// Should not happen in this function, but we keep the code if in the future we can replace the permanent live by a replay
|
||||||
await assignReplayFilesToVideo({ video: videoWithFiles, replayDirectory })
|
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 the live was in another base directory, also delete it
|
||||||
if (permanentLive) { // Remove session replay
|
if (replayInAnotherDirectory) {
|
||||||
await remove(replayDirectory)
|
await remove(getHLSDirectory(liveVideo))
|
||||||
} 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))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
inputFileMutexReleaser()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Regenerate the thumbnail & preview?
|
// Regenerate the thumbnail & preview?
|
||||||
|
|
|
@ -27,7 +27,6 @@ import { Server, createServer } from 'net'
|
||||||
import context from 'node-media-server/src/node_core_ctx.js'
|
import context from 'node-media-server/src/node_core_ctx.js'
|
||||||
import nodeMediaServerLogger from 'node-media-server/src/node_core_logger.js'
|
import nodeMediaServerLogger from 'node-media-server/src/node_core_logger.js'
|
||||||
import NodeRtmpSession from 'node-media-server/src/node_rtmp_session.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 { Server as ServerTLS, createServer as createServerTLS } from 'tls'
|
||||||
import { federateVideoIfNeeded } from '../activitypub/videos/index.js'
|
import { federateVideoIfNeeded } from '../activitypub/videos/index.js'
|
||||||
import { JobQueue } from '../job-queue/index.js'
|
import { JobQueue } from '../job-queue/index.js'
|
||||||
|
@ -621,7 +620,7 @@ class LiveManager {
|
||||||
|
|
||||||
if (files.length === 0) return undefined
|
if (files.length === 0) return undefined
|
||||||
|
|
||||||
return join(directory, files.sort().reverse()[0])
|
return files.sort().reverse()[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildAllResolutionsToTranscode (originResolution: number, hasAudio: boolean) {
|
private buildAllResolutionsToTranscode (originResolution: number, hasAudio: boolean) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue