diff options
-rw-r--r-- | packages/ffmpeg/src/ffmpeg-vod.ts | 4 | ||||
-rw-r--r-- | packages/tests/src/api/live/index.ts | 1 | ||||
-rw-r--r-- | packages/tests/src/api/live/live-privacy-update.ts | 83 | ||||
-rw-r--r-- | server/server/lib/job-queue/handlers/video-live-ending.ts | 59 | ||||
-rw-r--r-- | server/server/lib/transcoding/hls-transcoding.ts | 17 |
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 @@ | |||
1 | import './live-constraints.js' | 1 | import './live-constraints.js' |
2 | import './live-fast-restream.js' | 2 | import './live-fast-restream.js' |
3 | import './live-socket-messages.js' | 3 | import './live-socket-messages.js' |
4 | import './live-privacy-update.js' | ||
4 | import './live-permanent.js' | 5 | import './live-permanent.js' |
5 | import './live-rtmps.js' | 6 | import './live-rtmps.js' |
6 | import './live-save-replay.js' | 7 | import './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 | |||
3 | import { HttpStatusCode, LiveVideoCreate, VideoPrivacy } from '@peertube/peertube-models' | ||
4 | import { | ||
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 | |||
15 | async 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 | |||
24 | describe('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' | |||
8 | import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url.js' | 8 | import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url.js' |
9 | import { federateVideoIfNeeded } from '@server/lib/activitypub/videos/index.js' | 9 | import { federateVideoIfNeeded } from '@server/lib/activitypub/videos/index.js' |
10 | import { cleanupAndDestroyPermanentLive, cleanupTMPLiveFiles, cleanupUnsavedNormalLive } from '@server/lib/live/index.js' | 10 | import { cleanupAndDestroyPermanentLive, cleanupTMPLiveFiles, cleanupUnsavedNormalLive } from '@server/lib/live/index.js' |
11 | import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename, getLiveReplayBaseDirectory } from '@server/lib/paths.js' | 11 | import { |
12 | generateHLSMasterPlaylistFilename, | ||
13 | generateHlsSha256SegmentsFilename, | ||
14 | getHLSDirectory, | ||
15 | getLiveReplayBaseDirectory | ||
16 | } from '@server/lib/paths.js' | ||
12 | import { generateLocalVideoMiniature, regenerateMiniaturesIfNeeded } from '@server/lib/thumbnail.js' | 17 | import { generateLocalVideoMiniature, regenerateMiniaturesIfNeeded } from '@server/lib/thumbnail.js' |
13 | import { generateHlsPlaylistResolutionFromTS } from '@server/lib/transcoding/hls-transcoding.js' | 18 | import { generateHlsPlaylistResolutionFromTS } from '@server/lib/transcoding/hls-transcoding.js' |
14 | import { VideoPathManager } from '@server/lib/video-path-manager.js' | 19 | import { VideoPathManager } from '@server/lib/video-path-manager.js' |
@@ -24,6 +29,7 @@ import { MVideo, MVideoLive, MVideoLiveSession, MVideoWithAllFiles } from '@serv | |||
24 | import { ffprobePromise, getAudioStream, getVideoStreamDimensionsInfo, getVideoStreamFPS } from '@peertube/peertube-ffmpeg' | 29 | import { ffprobePromise, getAudioStream, getVideoStreamDimensionsInfo, getVideoStreamFPS } from '@peertube/peertube-ffmpeg' |
25 | import { logger, loggerTagsFactory } from '../../../helpers/logger.js' | 30 | import { logger, loggerTagsFactory } from '../../../helpers/logger.js' |
26 | import { JobQueue } from '../job-queue.js' | 31 | import { JobQueue } from '../job-queue.js' |
32 | import { isVideoInPublicDirectory } from '@server/lib/video-privacy.js' | ||
27 | 33 | ||
28 | const lTags = loggerTagsFactory('live', 'job') | 34 | const 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 | } |