aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--packages/ffmpeg/src/ffmpeg-vod.ts4
-rw-r--r--packages/tests/src/api/live/index.ts1
-rw-r--r--packages/tests/src/api/live/live-privacy-update.ts83
-rw-r--r--server/server/lib/job-queue/handlers/video-live-ending.ts59
-rw-r--r--server/server/lib/transcoding/hls-transcoding.ts17
5 files changed, 144 insertions, 20 deletions
diff --git a/packages/ffmpeg/src/ffmpeg-vod.ts b/packages/ffmpeg/src/ffmpeg-vod.ts
index 6dd272b8d..373dc6e81 100644
--- a/packages/ffmpeg/src/ffmpeg-vod.ts
+++ b/packages/ffmpeg/src/ffmpeg-vod.ts
@@ -102,7 +102,9 @@ export class FFmpegVOD {
102 102
103 command.on('start', () => { 103 command.on('start', () => {
104 setTimeout(() => { 104 setTimeout(() => {
105 options.inputFileMutexReleaser() 105 if (options.inputFileMutexReleaser) {
106 options.inputFileMutexReleaser()
107 }
106 }, 1000) 108 }, 1000)
107 }) 109 })
108 110
diff --git a/packages/tests/src/api/live/index.ts b/packages/tests/src/api/live/index.ts
index e61e6c611..bb5177d95 100644
--- a/packages/tests/src/api/live/index.ts
+++ b/packages/tests/src/api/live/index.ts
@@ -1,6 +1,7 @@
1import './live-constraints.js' 1import './live-constraints.js'
2import './live-fast-restream.js' 2import './live-fast-restream.js'
3import './live-socket-messages.js' 3import './live-socket-messages.js'
4import './live-privacy-update.js'
4import './live-permanent.js' 5import './live-permanent.js'
5import './live-rtmps.js' 6import './live-rtmps.js'
6import './live-save-replay.js' 7import './live-save-replay.js'
diff --git a/packages/tests/src/api/live/live-privacy-update.ts b/packages/tests/src/api/live/live-privacy-update.ts
new file mode 100644
index 000000000..ff12ff3e3
--- /dev/null
+++ b/packages/tests/src/api/live/live-privacy-update.ts
@@ -0,0 +1,83 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { HttpStatusCode, LiveVideoCreate, VideoPrivacy } from '@peertube/peertube-models'
4import {
5 cleanupTests, createSingleServer, makeRawRequest,
6 PeerTubeServer,
7 setAccessTokensToServers,
8 setDefaultVideoChannel,
9 stopFfmpeg,
10 waitJobs,
11 waitUntilLivePublishedOnAllServers,
12 waitUntilLiveReplacedByReplayOnAllServers
13} from '@peertube/peertube-server-commands'
14
15async function testVideoFiles (server: PeerTubeServer, uuid: string) {
16 const video = await server.videos.getWithToken({ id: uuid })
17
18 const expectedStatus = HttpStatusCode.OK_200
19
20 await makeRawRequest({ url: video.streamingPlaylists[0].playlistUrl, token: server.accessToken, expectedStatus })
21 await makeRawRequest({ url: video.streamingPlaylists[0].segmentsSha256Url, token: server.accessToken, expectedStatus })
22}
23
24describe('Live privacy update', function () {
25 let server: PeerTubeServer
26
27 before(async function () {
28 this.timeout(120000)
29
30 server = await createSingleServer(1)
31
32 await setAccessTokensToServers([ server ])
33 await setDefaultVideoChannel([ server ])
34
35 await server.config.enableMinimumTranscoding()
36 await server.config.enableLive({ allowReplay: true, transcoding: true, resolutions: 'min' })
37 })
38
39 describe('Normal live', function () {
40 let uuid: string
41
42 it('Should create a public live with private replay', async function () {
43 this.timeout(120000)
44
45 const fields: LiveVideoCreate = {
46 name: 'live',
47 privacy: VideoPrivacy.PUBLIC,
48 permanentLive: false,
49 replaySettings: { privacy: VideoPrivacy.PRIVATE },
50 saveReplay: true,
51 channelId: server.store.channel.id
52 }
53
54 const video = await server.live.create({ fields })
55 uuid = video.uuid
56
57 const ffmpegCommand = await server.live.sendRTMPStreamInVideo({ videoId: uuid })
58 await waitUntilLivePublishedOnAllServers([ server ], uuid)
59 await stopFfmpeg(ffmpegCommand)
60
61 await waitUntilLiveReplacedByReplayOnAllServers([ server ], uuid)
62 await waitJobs([ server ])
63
64 await testVideoFiles(server, uuid)
65 })
66
67 it('Should update the replay to public and re-update it to private', async function () {
68 this.timeout(120000)
69
70 await server.videos.update({ id: uuid, attributes: { privacy: VideoPrivacy.PUBLIC } })
71 await waitJobs([ server ])
72 await testVideoFiles(server, uuid)
73
74 await server.videos.update({ id: uuid, attributes: { privacy: VideoPrivacy.PRIVATE } })
75 await waitJobs([ server ])
76 await testVideoFiles(server, uuid)
77 })
78 })
79
80 after(async function () {
81 await cleanupTests([ server ])
82 })
83})
diff --git a/server/server/lib/job-queue/handlers/video-live-ending.ts b/server/server/lib/job-queue/handlers/video-live-ending.ts
index 0b4a4fd8b..f10cc763c 100644
--- a/server/server/lib/job-queue/handlers/video-live-ending.ts
+++ b/server/server/lib/job-queue/handlers/video-live-ending.ts
@@ -8,7 +8,12 @@ import { CONSTRAINTS_FIELDS } from '@server/initializers/constants.js'
8import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url.js' 8import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url.js'
9import { federateVideoIfNeeded } from '@server/lib/activitypub/videos/index.js' 9import { federateVideoIfNeeded } from '@server/lib/activitypub/videos/index.js'
10import { cleanupAndDestroyPermanentLive, cleanupTMPLiveFiles, cleanupUnsavedNormalLive } from '@server/lib/live/index.js' 10import { cleanupAndDestroyPermanentLive, cleanupTMPLiveFiles, cleanupUnsavedNormalLive } from '@server/lib/live/index.js'
11import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename, getLiveReplayBaseDirectory } from '@server/lib/paths.js' 11import {
12 generateHLSMasterPlaylistFilename,
13 generateHlsSha256SegmentsFilename,
14 getHLSDirectory,
15 getLiveReplayBaseDirectory
16} from '@server/lib/paths.js'
12import { generateLocalVideoMiniature, regenerateMiniaturesIfNeeded } from '@server/lib/thumbnail.js' 17import { generateLocalVideoMiniature, regenerateMiniaturesIfNeeded } from '@server/lib/thumbnail.js'
13import { generateHlsPlaylistResolutionFromTS } from '@server/lib/transcoding/hls-transcoding.js' 18import { generateHlsPlaylistResolutionFromTS } from '@server/lib/transcoding/hls-transcoding.js'
14import { VideoPathManager } from '@server/lib/video-path-manager.js' 19import { VideoPathManager } from '@server/lib/video-path-manager.js'
@@ -24,6 +29,7 @@ import { MVideo, MVideoLive, MVideoLiveSession, MVideoWithAllFiles } from '@serv
24import { ffprobePromise, getAudioStream, getVideoStreamDimensionsInfo, getVideoStreamFPS } from '@peertube/peertube-ffmpeg' 29import { ffprobePromise, getAudioStream, getVideoStreamDimensionsInfo, getVideoStreamFPS } from '@peertube/peertube-ffmpeg'
25import { logger, loggerTagsFactory } from '../../../helpers/logger.js' 30import { logger, loggerTagsFactory } from '../../../helpers/logger.js'
26import { JobQueue } from '../job-queue.js' 31import { JobQueue } from '../job-queue.js'
32import { isVideoInPublicDirectory } from '@server/lib/video-privacy.js'
27 33
28const lTags = loggerTagsFactory('live', 'job') 34const lTags = loggerTagsFactory('live', 'job')
29 35
@@ -139,9 +145,15 @@ async function saveReplayToExternalVideo (options: {
139 }) 145 })
140 } 146 }
141 147
142 await assignReplayFilesToVideo({ video: replayVideo, replayDirectory }) 148 const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(liveVideo.uuid)
143 149
144 await remove(replayDirectory) 150 try {
151 await assignReplayFilesToVideo({ video: replayVideo, replayDirectory })
152
153 await remove(replayDirectory)
154 } finally {
155 inputFileMutexReleaser()
156 }
145 157
146 for (const type of [ ThumbnailType.MINIATURE, ThumbnailType.PREVIEW ]) { 158 for (const type of [ ThumbnailType.MINIATURE, ThumbnailType.PREVIEW ]) {
147 const image = await generateLocalVideoMiniature({ video: replayVideo, videoFile: replayVideo.getMaxQualityFile(), type }) 159 const image = await generateLocalVideoMiniature({ video: replayVideo, videoFile: replayVideo.getMaxQualityFile(), type })
@@ -160,11 +172,14 @@ async function replaceLiveByReplay (options: {
160 permanentLive: boolean 172 permanentLive: boolean
161 replayDirectory: string 173 replayDirectory: string
162}) { 174}) {
163 const { video, liveSession, live, permanentLive, replayDirectory } = options 175 const { video: liveVideo, liveSession, live, permanentLive, replayDirectory } = options
164 176
165 const replaySettings = await VideoLiveReplaySettingModel.load(liveSession.replaySettingId) 177 const replaySettings = await VideoLiveReplaySettingModel.load(liveSession.replaySettingId)
166 const videoWithFiles = await VideoModel.loadFull(video.id) 178 const videoWithFiles = await VideoModel.loadFull(liveVideo.id)
167 const hlsPlaylist = videoWithFiles.getHLSPlaylist() 179 const hlsPlaylist = videoWithFiles.getHLSPlaylist()
180 const replayInAnotherDirectory = isVideoInPublicDirectory(liveVideo.privacy) !== isVideoInPublicDirectory(replaySettings.privacy)
181
182 logger.info(`Replacing live ${liveVideo.uuid} by replay ${replayDirectory}.`, { replayInAnotherDirectory, ...lTags(liveVideo.uuid) })
168 183
169 await cleanupTMPLiveFiles(videoWithFiles, hlsPlaylist) 184 await cleanupTMPLiveFiles(videoWithFiles, hlsPlaylist)
170 185
@@ -188,13 +203,25 @@ async function replaceLiveByReplay (options: {
188 hlsPlaylist.segmentsSha256Filename = generateHlsSha256SegmentsFilename() 203 hlsPlaylist.segmentsSha256Filename = generateHlsSha256SegmentsFilename()
189 await hlsPlaylist.save() 204 await hlsPlaylist.save()
190 205
191 await assignReplayFilesToVideo({ video: videoWithFiles, replayDirectory }) 206 const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(videoWithFiles.uuid)
192 207
193 // Should not happen in this function, but we keep the code if in the future we can replace the permanent live by a replay 208 try {
194 if (permanentLive) { // Remove session replay 209 await assignReplayFilesToVideo({ video: videoWithFiles, replayDirectory })
195 await remove(replayDirectory) 210
196 } else { // We won't stream again in this live, we can delete the base replay directory 211 // Should not happen in this function, but we keep the code if in the future we can replace the permanent live by a replay
197 await remove(getLiveReplayBaseDirectory(videoWithFiles)) 212 if (permanentLive) { // Remove session replay
213 await remove(replayDirectory)
214 } else {
215 // We won't stream again in this live, we can delete the base replay directory
216 await remove(getLiveReplayBaseDirectory(liveVideo))
217
218 // If the live was in another base directory, also delete it
219 if (replayInAnotherDirectory) {
220 await remove(getHLSDirectory(liveVideo))
221 }
222 }
223 } finally {
224 inputFileMutexReleaser()
198 } 225 }
199 226
200 // Regenerate the thumbnail & preview? 227 // Regenerate the thumbnail & preview?
@@ -214,8 +241,10 @@ async function assignReplayFilesToVideo (options: {
214 241
215 const concatenatedTsFiles = await readdir(replayDirectory) 242 const concatenatedTsFiles = await readdir(replayDirectory)
216 243
244 logger.info(`Assigning replays ${replayDirectory} to video ${video.uuid}.`, { concatenatedTsFiles, ...lTags(video.uuid) })
245
217 for (const concatenatedTsFile of concatenatedTsFiles) { 246 for (const concatenatedTsFile of concatenatedTsFiles) {
218 const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid) 247 // Generating hls playlist can be long, reload the video in this case
219 await video.reload() 248 await video.reload()
220 249
221 const concatenatedTsFilePath = join(replayDirectory, concatenatedTsFile) 250 const concatenatedTsFilePath = join(replayDirectory, concatenatedTsFile)
@@ -228,17 +257,17 @@ async function assignReplayFilesToVideo (options: {
228 try { 257 try {
229 await generateHlsPlaylistResolutionFromTS({ 258 await generateHlsPlaylistResolutionFromTS({
230 video, 259 video,
231 inputFileMutexReleaser, 260 inputFileMutexReleaser: null, // Already locked in parent
232 concatenatedTsFilePath, 261 concatenatedTsFilePath,
233 resolution, 262 resolution,
234 fps, 263 fps,
235 isAAC: audioStream?.codec_name === 'aac' 264 isAAC: audioStream?.codec_name === 'aac'
236 }) 265 })
266
267 logger.error('coucou')
237 } catch (err) { 268 } catch (err) {
238 logger.error('Cannot generate HLS playlist resolution from TS files.', { err }) 269 logger.error('Cannot generate HLS playlist resolution from TS files.', { err })
239 } 270 }
240
241 inputFileMutexReleaser()
242 } 271 }
243 272
244 return video 273 return video
diff --git a/server/server/lib/transcoding/hls-transcoding.ts b/server/server/lib/transcoding/hls-transcoding.ts
index 5f07f112a..15182f5e6 100644
--- a/server/server/lib/transcoding/hls-transcoding.ts
+++ b/server/server/lib/transcoding/hls-transcoding.ts
@@ -58,8 +58,9 @@ export async function onHLSVideoFileTranscoding (options: {
58 videoFile: MVideoFile 58 videoFile: MVideoFile
59 videoOutputPath: string 59 videoOutputPath: string
60 m3u8OutputPath: string 60 m3u8OutputPath: string
61 filesLockedInParent?: boolean // default false
61}) { 62}) {
62 const { video, videoFile, videoOutputPath, m3u8OutputPath } = options 63 const { video, videoFile, videoOutputPath, m3u8OutputPath, filesLockedInParent = false } = options
63 64
64 // Create or update the playlist 65 // Create or update the playlist
65 const playlist = await retryTransactionWrapper(() => { 66 const playlist = await retryTransactionWrapper(() => {
@@ -69,7 +70,9 @@ export async function onHLSVideoFileTranscoding (options: {
69 }) 70 })
70 videoFile.videoStreamingPlaylistId = playlist.id 71 videoFile.videoStreamingPlaylistId = playlist.id
71 72
72 const mutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid) 73 const mutexReleaser = !filesLockedInParent
74 ? await VideoPathManager.Instance.lockFiles(video.uuid)
75 : null
73 76
74 try { 77 try {
75 await video.reload() 78 await video.reload()
@@ -114,7 +117,7 @@ export async function onHLSVideoFileTranscoding (options: {
114 117
115 return { resolutionPlaylistPath, videoFile: savedVideoFile } 118 return { resolutionPlaylistPath, videoFile: savedVideoFile }
116 } finally { 119 } finally {
117 mutexReleaser() 120 if (mutexReleaser) mutexReleaser()
118 } 121 }
119} 122}
120 123
@@ -176,5 +179,11 @@ async function generateHlsPlaylistCommon (options: {
176 fps: -1 179 fps: -1
177 }) 180 })
178 181
179 await onHLSVideoFileTranscoding({ video, videoFile: newVideoFile, videoOutputPath, m3u8OutputPath }) 182 await onHLSVideoFileTranscoding({
183 video,
184 videoFile: newVideoFile,
185 videoOutputPath,
186 m3u8OutputPath,
187 filesLockedInParent: !inputFileMutexReleaser
188 })
180} 189}