diff options
Diffstat (limited to 'server')
-rw-r--r-- | server/helpers/ffmpeg-utils.ts | 15 | ||||
-rw-r--r-- | server/helpers/video.ts | 10 | ||||
-rw-r--r-- | server/initializers/constants.ts | 5 | ||||
-rw-r--r-- | server/lib/activitypub/videos.ts | 6 | ||||
-rw-r--r-- | server/lib/job-queue/handlers/video-live-ending.ts | 33 | ||||
-rw-r--r-- | server/lib/live-manager.ts | 42 | ||||
-rw-r--r-- | server/models/video/video.ts | 8 | ||||
-rw-r--r-- | server/tests/api/check-params/live.ts | 7 | ||||
-rw-r--r-- | server/tests/api/live/index.ts | 2 | ||||
-rw-r--r-- | server/tests/api/live/live-constraints.ts | 199 | ||||
-rw-r--r-- | server/tests/api/live/live-save-replay.ts | 307 | ||||
-rw-r--r-- | server/tests/api/live/live.ts | 213 |
12 files changed, 627 insertions, 220 deletions
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts index 268ed7624..3b794b8a2 100644 --- a/server/helpers/ffmpeg-utils.ts +++ b/server/helpers/ffmpeg-utils.ts | |||
@@ -353,7 +353,7 @@ function convertWebPToJPG (path: string, destination: string): Promise<void> { | |||
353 | }) | 353 | }) |
354 | } | 354 | } |
355 | 355 | ||
356 | function runLiveTranscoding (rtmpUrl: string, outPath: string, resolutions: number[], deleteSegments: boolean) { | 356 | function runLiveTranscoding (rtmpUrl: string, outPath: string, resolutions: number[], fps, deleteSegments: boolean) { |
357 | const command = getFFmpeg(rtmpUrl) | 357 | const command = getFFmpeg(rtmpUrl) |
358 | command.inputOption('-fflags nobuffer') | 358 | command.inputOption('-fflags nobuffer') |
359 | 359 | ||
@@ -375,10 +375,6 @@ function runLiveTranscoding (rtmpUrl: string, outPath: string, resolutions: numb | |||
375 | })) | 375 | })) |
376 | ]) | 376 | ]) |
377 | 377 | ||
378 | const liveFPS = VIDEO_TRANSCODING_FPS.AVERAGE | ||
379 | |||
380 | command.withFps(liveFPS) | ||
381 | |||
382 | command.outputOption('-b_strategy 1') | 378 | command.outputOption('-b_strategy 1') |
383 | command.outputOption('-bf 16') | 379 | command.outputOption('-bf 16') |
384 | command.outputOption('-preset superfast') | 380 | command.outputOption('-preset superfast') |
@@ -386,13 +382,14 @@ function runLiveTranscoding (rtmpUrl: string, outPath: string, resolutions: numb | |||
386 | command.outputOption('-map_metadata -1') | 382 | command.outputOption('-map_metadata -1') |
387 | command.outputOption('-pix_fmt yuv420p') | 383 | command.outputOption('-pix_fmt yuv420p') |
388 | command.outputOption('-max_muxing_queue_size 1024') | 384 | command.outputOption('-max_muxing_queue_size 1024') |
385 | command.outputOption('-g ' + (fps * 2)) | ||
389 | 386 | ||
390 | for (let i = 0; i < resolutions.length; i++) { | 387 | for (let i = 0; i < resolutions.length; i++) { |
391 | const resolution = resolutions[i] | 388 | const resolution = resolutions[i] |
392 | 389 | ||
393 | command.outputOption(`-map [vout${resolution}]`) | 390 | command.outputOption(`-map [vout${resolution}]`) |
394 | command.outputOption(`-c:v:${i} libx264`) | 391 | command.outputOption(`-c:v:${i} libx264`) |
395 | command.outputOption(`-b:v:${i} ${getTargetBitrate(resolution, liveFPS, VIDEO_TRANSCODING_FPS)}`) | 392 | command.outputOption(`-b:v:${i} ${getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS)}`) |
396 | 393 | ||
397 | command.outputOption(`-map a:0`) | 394 | command.outputOption(`-map a:0`) |
398 | command.outputOption(`-c:a:${i} aac`) | 395 | command.outputOption(`-c:a:${i} aac`) |
@@ -443,8 +440,8 @@ async function hlsPlaylistToFragmentedMP4 (hlsDirectory: string, segmentFiles: s | |||
443 | command.run() | 440 | command.run() |
444 | 441 | ||
445 | function cleaner () { | 442 | function cleaner () { |
446 | remove(concatFile) | 443 | remove(concatFilePath) |
447 | .catch(err => logger.error('Cannot remove concat file in %s.', hlsDirectory, { err })) | 444 | .catch(err => logger.error('Cannot remove concat file in %s.', hlsDirectory, { err })) |
448 | } | 445 | } |
449 | 446 | ||
450 | return new Promise<string>((res, rej) => { | 447 | return new Promise<string>((res, rej) => { |
@@ -497,7 +494,7 @@ function addDefaultX264Params (command: ffmpeg.FfmpegCommand) { | |||
497 | } | 494 | } |
498 | 495 | ||
499 | function addDefaultLiveHLSParams (command: ffmpeg.FfmpegCommand, outPath: string, deleteSegments: boolean) { | 496 | function addDefaultLiveHLSParams (command: ffmpeg.FfmpegCommand, outPath: string, deleteSegments: boolean) { |
500 | command.outputOption('-hls_time ' + VIDEO_LIVE.SEGMENT_TIME) | 497 | command.outputOption('-hls_time ' + VIDEO_LIVE.SEGMENT_TIME_SECONDS) |
501 | command.outputOption('-hls_list_size ' + VIDEO_LIVE.SEGMENTS_LIST_SIZE) | 498 | command.outputOption('-hls_list_size ' + VIDEO_LIVE.SEGMENTS_LIST_SIZE) |
502 | 499 | ||
503 | if (deleteSegments === true) { | 500 | if (deleteSegments === true) { |
diff --git a/server/helpers/video.ts b/server/helpers/video.ts index 488b4da17..999137c6d 100644 --- a/server/helpers/video.ts +++ b/server/helpers/video.ts | |||
@@ -15,7 +15,7 @@ import { | |||
15 | MVideoThumbnail, | 15 | MVideoThumbnail, |
16 | MVideoWithRights | 16 | MVideoWithRights |
17 | } from '@server/types/models' | 17 | } from '@server/types/models' |
18 | import { VideoPrivacy, VideoTranscodingPayload } from '@shared/models' | 18 | import { VideoPrivacy, VideoState, VideoTranscodingPayload } from '@shared/models' |
19 | import { VideoModel } from '../models/video/video' | 19 | import { VideoModel } from '../models/video/video' |
20 | 20 | ||
21 | type VideoFetchType = 'all' | 'only-video' | 'only-video-with-rights' | 'id' | 'none' | 'only-immutable-attributes' | 21 | type VideoFetchType = 'all' | 'only-video' | 'only-video-with-rights' | 'id' | 'none' | 'only-immutable-attributes' |
@@ -104,6 +104,13 @@ function isPrivacyForFederation (privacy: VideoPrivacy) { | |||
104 | (CONFIG.FEDERATION.VIDEOS.FEDERATE_UNLISTED === true && castedPrivacy === VideoPrivacy.UNLISTED) | 104 | (CONFIG.FEDERATION.VIDEOS.FEDERATE_UNLISTED === true && castedPrivacy === VideoPrivacy.UNLISTED) |
105 | } | 105 | } |
106 | 106 | ||
107 | function isStateForFederation (state: VideoState) { | ||
108 | const castedState = parseInt(state + '', 10) | ||
109 | |||
110 | return castedState === VideoState.PUBLISHED || castedState === VideoState.WAITING_FOR_LIVE || castedState === VideoState.LIVE_ENDED | ||
111 | |||
112 | } | ||
113 | |||
107 | function getPrivaciesForFederation () { | 114 | function getPrivaciesForFederation () { |
108 | return (CONFIG.FEDERATION.VIDEOS.FEDERATE_UNLISTED === true) | 115 | return (CONFIG.FEDERATION.VIDEOS.FEDERATE_UNLISTED === true) |
109 | ? [ { privacy: VideoPrivacy.PUBLIC }, { privacy: VideoPrivacy.UNLISTED } ] | 116 | ? [ { privacy: VideoPrivacy.PUBLIC }, { privacy: VideoPrivacy.UNLISTED } ] |
@@ -127,6 +134,7 @@ export { | |||
127 | addOptimizeOrMergeAudioJob, | 134 | addOptimizeOrMergeAudioJob, |
128 | extractVideo, | 135 | extractVideo, |
129 | getExtFromMimetype, | 136 | getExtFromMimetype, |
137 | isStateForFederation, | ||
130 | isPrivacyForFederation, | 138 | isPrivacyForFederation, |
131 | getPrivaciesForFederation | 139 | getPrivaciesForFederation |
132 | } | 140 | } |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index f8380eaa0..d1f94e6e6 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -609,7 +609,7 @@ const HLS_REDUNDANCY_DIRECTORY = join(CONFIG.STORAGE.REDUNDANCY_DIR, 'hls') | |||
609 | const VIDEO_LIVE = { | 609 | const VIDEO_LIVE = { |
610 | EXTENSION: '.ts', | 610 | EXTENSION: '.ts', |
611 | CLEANUP_DELAY: 1000 * 60 * 5, // 5 minutes | 611 | CLEANUP_DELAY: 1000 * 60 * 5, // 5 minutes |
612 | SEGMENT_TIME: 4, // 4 seconds | 612 | SEGMENT_TIME_SECONDS: 4, // 4 seconds |
613 | SEGMENTS_LIST_SIZE: 15, // 15 maximum segments in live playlist | 613 | SEGMENTS_LIST_SIZE: 15, // 15 maximum segments in live playlist |
614 | RTMP: { | 614 | RTMP: { |
615 | CHUNK_SIZE: 60000, | 615 | CHUNK_SIZE: 60000, |
@@ -738,7 +738,8 @@ if (isTestInstance() === true) { | |||
738 | 738 | ||
739 | PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME = 5000 | 739 | PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME = 5000 |
740 | 740 | ||
741 | VIDEO_LIVE.CLEANUP_DELAY = 10000 | 741 | VIDEO_LIVE.CLEANUP_DELAY = 5000 |
742 | VIDEO_LIVE.SEGMENT_TIME_SECONDS = 2 | ||
742 | } | 743 | } |
743 | 744 | ||
744 | updateWebserverUrls() | 745 | updateWebserverUrls() |
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts index ea1e6a38f..ab4aac0a1 100644 --- a/server/lib/activitypub/videos.ts +++ b/server/lib/activitypub/videos.ts | |||
@@ -85,7 +85,7 @@ async function federateVideoIfNeeded (videoArg: MVideoAPWithoutCaption, isNewVid | |||
85 | // Check this is not a blacklisted video, or unfederated blacklisted video | 85 | // Check this is not a blacklisted video, or unfederated blacklisted video |
86 | (video.isBlacklisted() === false || (isNewVideo === false && video.VideoBlacklist.unfederated === false)) && | 86 | (video.isBlacklisted() === false || (isNewVideo === false && video.VideoBlacklist.unfederated === false)) && |
87 | // Check the video is public/unlisted and published | 87 | // Check the video is public/unlisted and published |
88 | video.hasPrivacyForFederation() && (video.state === VideoState.PUBLISHED || video.state === VideoState.WAITING_FOR_LIVE) | 88 | video.hasPrivacyForFederation() && video.hasStateForFederation() |
89 | ) { | 89 | ) { |
90 | // Fetch more attributes that we will need to serialize in AP object | 90 | // Fetch more attributes that we will need to serialize in AP object |
91 | if (isArray(video.VideoCaptions) === false) { | 91 | if (isArray(video.VideoCaptions) === false) { |
@@ -302,7 +302,7 @@ async function updateVideoFromAP (options: { | |||
302 | }) { | 302 | }) { |
303 | const { video, videoObject, account, channel, overrideTo } = options | 303 | const { video, videoObject, account, channel, overrideTo } = options |
304 | 304 | ||
305 | logger.debug('Updating remote video "%s".', options.videoObject.uuid, { account, channel }) | 305 | logger.debug('Updating remote video "%s".', options.videoObject.uuid, { videoObject: options.videoObject, account, channel }) |
306 | 306 | ||
307 | let videoFieldsSave: any | 307 | let videoFieldsSave: any |
308 | const wasPrivateVideo = video.privacy === VideoPrivacy.PRIVATE | 308 | const wasPrivateVideo = video.privacy === VideoPrivacy.PRIVATE |
@@ -562,6 +562,8 @@ function isAPHashTagObject (url: any): url is ActivityHashTagObject { | |||
562 | return url && url.type === 'Hashtag' | 562 | return url && url.type === 'Hashtag' |
563 | } | 563 | } |
564 | 564 | ||
565 | |||
566 | |||
565 | async function createVideo (videoObject: VideoObject, channel: MChannelAccountLight, waitThumbnail = false) { | 567 | async function createVideo (videoObject: VideoObject, channel: MChannelAccountLight, waitThumbnail = false) { |
566 | logger.debug('Adding remote video %s.', videoObject.id) | 568 | logger.debug('Adding remote video %s.', videoObject.id) |
567 | 569 | ||
diff --git a/server/lib/job-queue/handlers/video-live-ending.ts b/server/lib/job-queue/handlers/video-live-ending.ts index 1e964726e..2b900998a 100644 --- a/server/lib/job-queue/handlers/video-live-ending.ts +++ b/server/lib/job-queue/handlers/video-live-ending.ts | |||
@@ -6,22 +6,31 @@ import { publishAndFederateIfNeeded } from '@server/lib/video' | |||
6 | import { getHLSDirectory } from '@server/lib/video-paths' | 6 | import { getHLSDirectory } from '@server/lib/video-paths' |
7 | import { generateHlsPlaylist } from '@server/lib/video-transcoding' | 7 | import { generateHlsPlaylist } from '@server/lib/video-transcoding' |
8 | import { VideoModel } from '@server/models/video/video' | 8 | import { VideoModel } from '@server/models/video/video' |
9 | import { VideoFileModel } from '@server/models/video/video-file' | ||
9 | import { VideoLiveModel } from '@server/models/video/video-live' | 10 | import { VideoLiveModel } from '@server/models/video/video-live' |
10 | import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' | 11 | import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' |
11 | import { MStreamingPlaylist, MVideo, MVideoLive, MVideoWithFile } from '@server/types/models' | 12 | import { MStreamingPlaylist, MVideo, MVideoLive } from '@server/types/models' |
12 | import { VideoLiveEndingPayload, VideoState } from '@shared/models' | 13 | import { VideoLiveEndingPayload, VideoState } from '@shared/models' |
13 | import { logger } from '../../../helpers/logger' | 14 | import { logger } from '../../../helpers/logger' |
14 | import { VideoFileModel } from '@server/models/video/video-file' | ||
15 | 15 | ||
16 | async function processVideoLiveEnding (job: Bull.Job) { | 16 | async function processVideoLiveEnding (job: Bull.Job) { |
17 | const payload = job.data as VideoLiveEndingPayload | 17 | const payload = job.data as VideoLiveEndingPayload |
18 | 18 | ||
19 | function logError () { | ||
20 | logger.warn('Video live %d does not exist anymore. Cannot process live ending.', payload.videoId) | ||
21 | } | ||
22 | |||
19 | const video = await VideoModel.load(payload.videoId) | 23 | const video = await VideoModel.load(payload.videoId) |
20 | const live = await VideoLiveModel.loadByVideoId(payload.videoId) | 24 | const live = await VideoLiveModel.loadByVideoId(payload.videoId) |
21 | 25 | ||
26 | if (!video || !live) { | ||
27 | logError() | ||
28 | return | ||
29 | } | ||
30 | |||
22 | const streamingPlaylist = await VideoStreamingPlaylistModel.loadHLSPlaylistByVideo(video.id) | 31 | const streamingPlaylist = await VideoStreamingPlaylistModel.loadHLSPlaylistByVideo(video.id) |
23 | if (!video || !streamingPlaylist || !live) { | 32 | if (!streamingPlaylist) { |
24 | logger.warn('Video live %d does not exist anymore. Cannot process live ending.', payload.videoId) | 33 | logError() |
25 | return | 34 | return |
26 | } | 35 | } |
27 | 36 | ||
@@ -52,21 +61,21 @@ async function saveLive (video: MVideo, live: MVideoLive) { | |||
52 | const playlistPath = join(hlsDirectory, playlistFile) | 61 | const playlistPath = join(hlsDirectory, playlistFile) |
53 | const { videoFileResolution } = await getVideoFileResolution(playlistPath) | 62 | const { videoFileResolution } = await getVideoFileResolution(playlistPath) |
54 | 63 | ||
55 | const mp4TmpName = buildMP4TmpName(videoFileResolution) | 64 | const mp4TmpPath = buildMP4TmpPath(hlsDirectory, videoFileResolution) |
56 | 65 | ||
57 | // Playlist name is for example 3.m3u8 | 66 | // Playlist name is for example 3.m3u8 |
58 | // Segments names are 3-0.ts 3-1.ts etc | 67 | // Segments names are 3-0.ts 3-1.ts etc |
59 | const shouldStartWith = playlistFile.replace(/\.m3u8$/, '') + '-' | 68 | const shouldStartWith = playlistFile.replace(/\.m3u8$/, '') + '-' |
60 | 69 | ||
61 | const segmentFiles = files.filter(f => f.startsWith(shouldStartWith) && f.endsWith('.ts')) | 70 | const segmentFiles = files.filter(f => f.startsWith(shouldStartWith) && f.endsWith('.ts')) |
62 | await hlsPlaylistToFragmentedMP4(hlsDirectory, segmentFiles, mp4TmpName) | 71 | await hlsPlaylistToFragmentedMP4(hlsDirectory, segmentFiles, mp4TmpPath) |
63 | 72 | ||
64 | for (const file of segmentFiles) { | 73 | for (const file of segmentFiles) { |
65 | await remove(join(hlsDirectory, file)) | 74 | await remove(join(hlsDirectory, file)) |
66 | } | 75 | } |
67 | 76 | ||
68 | if (!duration) { | 77 | if (!duration) { |
69 | duration = await getDurationFromVideoFile(mp4TmpName) | 78 | duration = await getDurationFromVideoFile(mp4TmpPath) |
70 | } | 79 | } |
71 | 80 | ||
72 | resolutions.push(videoFileResolution) | 81 | resolutions.push(videoFileResolution) |
@@ -90,7 +99,7 @@ async function saveLive (video: MVideo, live: MVideoLive) { | |||
90 | hlsPlaylist.VideoFiles = [] | 99 | hlsPlaylist.VideoFiles = [] |
91 | 100 | ||
92 | for (const resolution of resolutions) { | 101 | for (const resolution of resolutions) { |
93 | const videoInputPath = buildMP4TmpName(resolution) | 102 | const videoInputPath = buildMP4TmpPath(hlsDirectory, resolution) |
94 | const { isPortraitMode } = await getVideoFileResolution(videoInputPath) | 103 | const { isPortraitMode } = await getVideoFileResolution(videoInputPath) |
95 | 104 | ||
96 | await generateHlsPlaylist({ | 105 | await generateHlsPlaylist({ |
@@ -101,7 +110,7 @@ async function saveLive (video: MVideo, live: MVideoLive) { | |||
101 | isPortraitMode | 110 | isPortraitMode |
102 | }) | 111 | }) |
103 | 112 | ||
104 | await remove(join(hlsDirectory, videoInputPath)) | 113 | await remove(videoInputPath) |
105 | } | 114 | } |
106 | 115 | ||
107 | await publishAndFederateIfNeeded(video, true) | 116 | await publishAndFederateIfNeeded(video, true) |
@@ -110,7 +119,7 @@ async function saveLive (video: MVideo, live: MVideoLive) { | |||
110 | async function cleanupLive (video: MVideo, streamingPlaylist: MStreamingPlaylist) { | 119 | async function cleanupLive (video: MVideo, streamingPlaylist: MStreamingPlaylist) { |
111 | const hlsDirectory = getHLSDirectory(video, false) | 120 | const hlsDirectory = getHLSDirectory(video, false) |
112 | 121 | ||
113 | await cleanupLiveFiles(hlsDirectory) | 122 | await remove(hlsDirectory) |
114 | 123 | ||
115 | streamingPlaylist.destroy() | 124 | streamingPlaylist.destroy() |
116 | .catch(err => logger.error('Cannot remove live streaming playlist.', { err })) | 125 | .catch(err => logger.error('Cannot remove live streaming playlist.', { err })) |
@@ -135,6 +144,6 @@ async function cleanupLiveFiles (hlsDirectory: string) { | |||
135 | } | 144 | } |
136 | } | 145 | } |
137 | 146 | ||
138 | function buildMP4TmpName (resolution: number) { | 147 | function buildMP4TmpPath (basePath: string, resolution: number) { |
139 | return resolution + '-tmp.mp4' | 148 | return join(basePath, resolution + '-tmp.mp4') |
140 | } | 149 | } |
diff --git a/server/lib/live-manager.ts b/server/lib/live-manager.ts index 2d8f906e9..6eb05c9d6 100644 --- a/server/lib/live-manager.ts +++ b/server/lib/live-manager.ts | |||
@@ -4,7 +4,7 @@ import * as chokidar from 'chokidar' | |||
4 | import { FfmpegCommand } from 'fluent-ffmpeg' | 4 | import { FfmpegCommand } from 'fluent-ffmpeg' |
5 | import { ensureDir, stat } from 'fs-extra' | 5 | import { ensureDir, stat } from 'fs-extra' |
6 | import { basename } from 'path' | 6 | import { basename } from 'path' |
7 | import { computeResolutionsToTranscode, runLiveMuxing, runLiveTranscoding } from '@server/helpers/ffmpeg-utils' | 7 | import { computeResolutionsToTranscode, getVideoFileFPS, getVideoFileResolution, getVideoStreamCodec, getVideoStreamSize, runLiveMuxing, runLiveTranscoding } from '@server/helpers/ffmpeg-utils' |
8 | import { logger } from '@server/helpers/logger' | 8 | import { logger } from '@server/helpers/logger' |
9 | import { CONFIG, registerConfigChangedHandler } from '@server/initializers/config' | 9 | import { CONFIG, registerConfigChangedHandler } from '@server/initializers/config' |
10 | import { MEMOIZE_TTL, P2P_MEDIA_LOADER_PEER_VERSION, VIDEO_LIVE, WEBSERVER } from '@server/initializers/constants' | 10 | import { MEMOIZE_TTL, P2P_MEDIA_LOADER_PEER_VERSION, VIDEO_LIVE, WEBSERVER } from '@server/initializers/constants' |
@@ -137,6 +137,13 @@ class LiveManager { | |||
137 | this.abortSession(sessionId) | 137 | this.abortSession(sessionId) |
138 | } | 138 | } |
139 | 139 | ||
140 | getLiveQuotaUsedByUser (userId: number) { | ||
141 | const currentLives = this.livesPerUser.get(userId) | ||
142 | if (!currentLives) return 0 | ||
143 | |||
144 | return currentLives.reduce((sum, obj) => sum + obj.size, 0) | ||
145 | } | ||
146 | |||
140 | private getContext () { | 147 | private getContext () { |
141 | return context | 148 | return context |
142 | } | 149 | } |
@@ -173,8 +180,15 @@ class LiveManager { | |||
173 | const playlistUrl = WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsMasterPlaylistStaticPath(video.uuid) | 180 | const playlistUrl = WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsMasterPlaylistStaticPath(video.uuid) |
174 | 181 | ||
175 | const session = this.getContext().sessions.get(sessionId) | 182 | const session = this.getContext().sessions.get(sessionId) |
183 | const rtmpUrl = 'rtmp://127.0.0.1:' + config.rtmp.port + streamPath | ||
184 | |||
185 | const [ resolutionResult, fps ] = await Promise.all([ | ||
186 | getVideoFileResolution(rtmpUrl), | ||
187 | getVideoFileFPS(rtmpUrl) | ||
188 | ]) | ||
189 | |||
176 | const resolutionsEnabled = CONFIG.LIVE.TRANSCODING.ENABLED | 190 | const resolutionsEnabled = CONFIG.LIVE.TRANSCODING.ENABLED |
177 | ? computeResolutionsToTranscode(session.videoHeight, 'live') | 191 | ? computeResolutionsToTranscode(resolutionResult.videoFileResolution, 'live') |
178 | : [] | 192 | : [] |
179 | 193 | ||
180 | logger.info('Will mux/transcode live video of original resolution %d.', session.videoHeight, { resolutionsEnabled }) | 194 | logger.info('Will mux/transcode live video of original resolution %d.', session.videoHeight, { resolutionsEnabled }) |
@@ -193,8 +207,9 @@ class LiveManager { | |||
193 | sessionId, | 207 | sessionId, |
194 | videoLive, | 208 | videoLive, |
195 | playlist: videoStreamingPlaylist, | 209 | playlist: videoStreamingPlaylist, |
196 | streamPath, | ||
197 | originalResolution: session.videoHeight, | 210 | originalResolution: session.videoHeight, |
211 | rtmpUrl, | ||
212 | fps, | ||
198 | resolutionsEnabled | 213 | resolutionsEnabled |
199 | }) | 214 | }) |
200 | } | 215 | } |
@@ -203,11 +218,12 @@ class LiveManager { | |||
203 | sessionId: string | 218 | sessionId: string |
204 | videoLive: MVideoLiveVideo | 219 | videoLive: MVideoLiveVideo |
205 | playlist: MStreamingPlaylist | 220 | playlist: MStreamingPlaylist |
206 | streamPath: string | 221 | rtmpUrl: string |
222 | fps: number | ||
207 | resolutionsEnabled: number[] | 223 | resolutionsEnabled: number[] |
208 | originalResolution: number | 224 | originalResolution: number |
209 | }) { | 225 | }) { |
210 | const { sessionId, videoLive, playlist, streamPath, resolutionsEnabled, originalResolution } = options | 226 | const { sessionId, videoLive, playlist, resolutionsEnabled, originalResolution, fps, rtmpUrl } = options |
211 | const startStreamDateTime = new Date().getTime() | 227 | const startStreamDateTime = new Date().getTime() |
212 | const allResolutions = resolutionsEnabled.concat([ originalResolution ]) | 228 | const allResolutions = resolutionsEnabled.concat([ originalResolution ]) |
213 | 229 | ||
@@ -238,17 +254,16 @@ class LiveManager { | |||
238 | const outPath = getHLSDirectory(videoLive.Video) | 254 | const outPath = getHLSDirectory(videoLive.Video) |
239 | await ensureDir(outPath) | 255 | await ensureDir(outPath) |
240 | 256 | ||
257 | const videoUUID = videoLive.Video.uuid | ||
241 | const deleteSegments = videoLive.saveReplay === false | 258 | const deleteSegments = videoLive.saveReplay === false |
242 | 259 | ||
243 | const rtmpUrl = 'rtmp://127.0.0.1:' + config.rtmp.port + streamPath | ||
244 | const ffmpegExec = CONFIG.LIVE.TRANSCODING.ENABLED | 260 | const ffmpegExec = CONFIG.LIVE.TRANSCODING.ENABLED |
245 | ? runLiveTranscoding(rtmpUrl, outPath, allResolutions, deleteSegments) | 261 | ? runLiveTranscoding(rtmpUrl, outPath, allResolutions, fps, deleteSegments) |
246 | : runLiveMuxing(rtmpUrl, outPath, deleteSegments) | 262 | : runLiveMuxing(rtmpUrl, outPath, deleteSegments) |
247 | 263 | ||
248 | logger.info('Running live muxing/transcoding.') | 264 | logger.info('Running live muxing/transcoding for %s.', videoUUID) |
249 | this.transSessions.set(sessionId, ffmpegExec) | 265 | this.transSessions.set(sessionId, ffmpegExec) |
250 | 266 | ||
251 | const videoUUID = videoLive.Video.uuid | ||
252 | const tsWatcher = chokidar.watch(outPath + '/*.ts') | 267 | const tsWatcher = chokidar.watch(outPath + '/*.ts') |
253 | 268 | ||
254 | const updateSegment = segmentPath => this.segmentsSha256Queue.push({ operation: 'update', segmentPath, videoUUID }) | 269 | const updateSegment = segmentPath => this.segmentsSha256Queue.push({ operation: 'update', segmentPath, videoUUID }) |
@@ -307,7 +322,7 @@ class LiveManager { | |||
307 | }) | 322 | }) |
308 | 323 | ||
309 | const onFFmpegEnded = () => { | 324 | const onFFmpegEnded = () => { |
310 | logger.info('RTMP transmuxing for video %s ended. Scheduling cleanup', streamPath) | 325 | logger.info('RTMP transmuxing for video %s ended. Scheduling cleanup', rtmpUrl) |
311 | 326 | ||
312 | this.transSessions.delete(sessionId) | 327 | this.transSessions.delete(sessionId) |
313 | 328 | ||
@@ -332,13 +347,6 @@ class LiveManager { | |||
332 | ffmpegExec.on('end', () => onFFmpegEnded()) | 347 | ffmpegExec.on('end', () => onFFmpegEnded()) |
333 | } | 348 | } |
334 | 349 | ||
335 | getLiveQuotaUsedByUser (userId: number) { | ||
336 | const currentLives = this.livesPerUser.get(userId) | ||
337 | if (!currentLives) return 0 | ||
338 | |||
339 | return currentLives.reduce((sum, obj) => sum + obj.size, 0) | ||
340 | } | ||
341 | |||
342 | private async onEndTransmuxing (videoId: number, cleanupNow = false) { | 350 | private async onEndTransmuxing (videoId: number, cleanupNow = false) { |
343 | try { | 351 | try { |
344 | const fullVideo = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId) | 352 | const fullVideo = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId) |
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 7e008f7ea..8e71f8c32 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -25,7 +25,7 @@ import { | |||
25 | UpdatedAt | 25 | UpdatedAt |
26 | } from 'sequelize-typescript' | 26 | } from 'sequelize-typescript' |
27 | import { buildNSFWFilter } from '@server/helpers/express-utils' | 27 | import { buildNSFWFilter } from '@server/helpers/express-utils' |
28 | import { getPrivaciesForFederation, isPrivacyForFederation } from '@server/helpers/video' | 28 | import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video' |
29 | import { LiveManager } from '@server/lib/live-manager' | 29 | import { LiveManager } from '@server/lib/live-manager' |
30 | import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths' | 30 | import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths' |
31 | import { getServerActor } from '@server/models/application/application' | 31 | import { getServerActor } from '@server/models/application/application' |
@@ -823,6 +823,8 @@ export class VideoModel extends Model<VideoModel> { | |||
823 | static stopLiveIfNeeded (instance: VideoModel) { | 823 | static stopLiveIfNeeded (instance: VideoModel) { |
824 | if (!instance.isLive) return | 824 | if (!instance.isLive) return |
825 | 825 | ||
826 | logger.info('Stopping live of video %s after video deletion.', instance.uuid) | ||
827 | |||
826 | return LiveManager.Instance.stopSessionOf(instance.id) | 828 | return LiveManager.Instance.stopSessionOf(instance.id) |
827 | } | 829 | } |
828 | 830 | ||
@@ -1921,6 +1923,10 @@ export class VideoModel extends Model<VideoModel> { | |||
1921 | return isPrivacyForFederation(this.privacy) | 1923 | return isPrivacyForFederation(this.privacy) |
1922 | } | 1924 | } |
1923 | 1925 | ||
1926 | hasStateForFederation () { | ||
1927 | return isStateForFederation(this.state) | ||
1928 | } | ||
1929 | |||
1924 | isNewVideo (newPrivacy: VideoPrivacy) { | 1930 | isNewVideo (newPrivacy: VideoPrivacy) { |
1925 | return this.hasPrivacyForFederation() === false && isPrivacyForFederation(newPrivacy) === true | 1931 | return this.hasPrivacyForFederation() === false && isPrivacyForFederation(newPrivacy) === true |
1926 | } | 1932 | } |
diff --git a/server/tests/api/check-params/live.ts b/server/tests/api/check-params/live.ts index 3e97dffdc..2b2d1beec 100644 --- a/server/tests/api/check-params/live.ts +++ b/server/tests/api/check-params/live.ts | |||
@@ -1,7 +1,6 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | 1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ |
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | ||
5 | import { omit } from 'lodash' | 4 | import { omit } from 'lodash' |
6 | import { join } from 'path' | 5 | import { join } from 'path' |
7 | import { LiveVideo, VideoPrivacy } from '@shared/models' | 6 | import { LiveVideo, VideoPrivacy } from '@shared/models' |
@@ -14,11 +13,11 @@ import { | |||
14 | immutableAssign, | 13 | immutableAssign, |
15 | makePostBodyRequest, | 14 | makePostBodyRequest, |
16 | makeUploadRequest, | 15 | makeUploadRequest, |
16 | runAndTestFfmpegStreamError, | ||
17 | sendRTMPStream, | 17 | sendRTMPStream, |
18 | ServerInfo, | 18 | ServerInfo, |
19 | setAccessTokensToServers, | 19 | setAccessTokensToServers, |
20 | stopFfmpeg, | 20 | stopFfmpeg, |
21 | testFfmpegStreamError, | ||
22 | updateCustomSubConfig, | 21 | updateCustomSubConfig, |
23 | updateLive, | 22 | updateLive, |
24 | uploadVideoAndGetId, | 23 | uploadVideoAndGetId, |
@@ -30,9 +29,7 @@ describe('Test video lives API validator', function () { | |||
30 | const path = '/api/v1/videos/live' | 29 | const path = '/api/v1/videos/live' |
31 | let server: ServerInfo | 30 | let server: ServerInfo |
32 | let userAccessToken = '' | 31 | let userAccessToken = '' |
33 | let accountName: string | ||
34 | let channelId: number | 32 | let channelId: number |
35 | let channelName: string | ||
36 | let videoId: number | 33 | let videoId: number |
37 | let videoIdNotLive: number | 34 | let videoIdNotLive: number |
38 | 35 | ||
@@ -414,7 +411,7 @@ describe('Test video lives API validator', function () { | |||
414 | 411 | ||
415 | await waitUntilLiveStarts(server.url, server.accessToken, videoId) | 412 | await waitUntilLiveStarts(server.url, server.accessToken, videoId) |
416 | 413 | ||
417 | await testFfmpegStreamError(server.url, server.accessToken, videoId, true) | 414 | await runAndTestFfmpegStreamError(server.url, server.accessToken, videoId, true) |
418 | 415 | ||
419 | await stopFfmpeg(command) | 416 | await stopFfmpeg(command) |
420 | }) | 417 | }) |
diff --git a/server/tests/api/live/index.ts b/server/tests/api/live/index.ts index 280daf423..ee77af286 100644 --- a/server/tests/api/live/index.ts +++ b/server/tests/api/live/index.ts | |||
@@ -1 +1,3 @@ | |||
1 | export * from './live-constraints' | ||
2 | export * from './live-save-replay' | ||
1 | export * from './live' | 3 | export * from './live' |
diff --git a/server/tests/api/live/live-constraints.ts b/server/tests/api/live/live-constraints.ts new file mode 100644 index 000000000..23c8e3b0a --- /dev/null +++ b/server/tests/api/live/live-constraints.ts | |||
@@ -0,0 +1,199 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import 'mocha' | ||
4 | import * as chai from 'chai' | ||
5 | import { User, VideoDetails, VideoPrivacy } from '@shared/models' | ||
6 | import { | ||
7 | checkLiveCleanup, | ||
8 | cleanupTests, | ||
9 | createLive, | ||
10 | createUser, | ||
11 | doubleFollow, | ||
12 | flushAndRunMultipleServers, | ||
13 | getMyUserInformation, | ||
14 | getVideo, | ||
15 | runAndTestFfmpegStreamError, | ||
16 | ServerInfo, | ||
17 | setAccessTokensToServers, | ||
18 | setDefaultVideoChannel, | ||
19 | updateCustomSubConfig, | ||
20 | updateUser, | ||
21 | userLogin, | ||
22 | wait, | ||
23 | waitJobs | ||
24 | } from '../../../../shared/extra-utils' | ||
25 | |||
26 | const expect = chai.expect | ||
27 | |||
28 | describe('Test live constraints', function () { | ||
29 | let servers: ServerInfo[] = [] | ||
30 | let userId: number | ||
31 | let userAccessToken: string | ||
32 | let userChannelId: number | ||
33 | |||
34 | async function createLiveWrapper (saveReplay: boolean) { | ||
35 | const liveAttributes = { | ||
36 | name: 'user live', | ||
37 | channelId: userChannelId, | ||
38 | privacy: VideoPrivacy.PUBLIC, | ||
39 | saveReplay | ||
40 | } | ||
41 | |||
42 | const res = await createLive(servers[0].url, userAccessToken, liveAttributes) | ||
43 | return res.body.video.uuid as string | ||
44 | } | ||
45 | |||
46 | async function checkSaveReplay (videoId: string, resolutions = [ 720 ]) { | ||
47 | for (const server of servers) { | ||
48 | const res = await getVideo(server.url, videoId) | ||
49 | |||
50 | const video: VideoDetails = res.body | ||
51 | expect(video.isLive).to.be.false | ||
52 | expect(video.duration).to.be.greaterThan(0) | ||
53 | } | ||
54 | |||
55 | await checkLiveCleanup(servers[0], videoId, resolutions) | ||
56 | } | ||
57 | |||
58 | before(async function () { | ||
59 | this.timeout(120000) | ||
60 | |||
61 | servers = await flushAndRunMultipleServers(2) | ||
62 | |||
63 | // Get the access tokens | ||
64 | await setAccessTokensToServers(servers) | ||
65 | await setDefaultVideoChannel(servers) | ||
66 | |||
67 | await updateCustomSubConfig(servers[0].url, servers[0].accessToken, { | ||
68 | live: { | ||
69 | enabled: true, | ||
70 | allowReplay: true, | ||
71 | transcoding: { | ||
72 | enabled: false | ||
73 | } | ||
74 | } | ||
75 | }) | ||
76 | |||
77 | { | ||
78 | const user = { username: 'user1', password: 'superpassword' } | ||
79 | const res = await createUser({ | ||
80 | url: servers[0].url, | ||
81 | accessToken: servers[0].accessToken, | ||
82 | username: user.username, | ||
83 | password: user.password | ||
84 | }) | ||
85 | userId = res.body.user.id | ||
86 | |||
87 | userAccessToken = await userLogin(servers[0], user) | ||
88 | |||
89 | const resMe = await getMyUserInformation(servers[0].url, userAccessToken) | ||
90 | userChannelId = (resMe.body as User).videoChannels[0].id | ||
91 | |||
92 | await updateUser({ | ||
93 | url: servers[0].url, | ||
94 | userId, | ||
95 | accessToken: servers[0].accessToken, | ||
96 | videoQuota: 1, | ||
97 | videoQuotaDaily: -1 | ||
98 | }) | ||
99 | } | ||
100 | |||
101 | // Server 1 and server 2 follow each other | ||
102 | await doubleFollow(servers[0], servers[1]) | ||
103 | }) | ||
104 | |||
105 | it('Should not have size limit if save replay is disabled', async function () { | ||
106 | this.timeout(60000) | ||
107 | |||
108 | const userVideoLiveoId = await createLiveWrapper(false) | ||
109 | await runAndTestFfmpegStreamError(servers[0].url, userAccessToken, userVideoLiveoId, false) | ||
110 | }) | ||
111 | |||
112 | it('Should have size limit depending on user global quota if save replay is enabled', async function () { | ||
113 | this.timeout(60000) | ||
114 | |||
115 | // Wait for user quota memoize cache invalidation | ||
116 | await wait(5000) | ||
117 | |||
118 | const userVideoLiveoId = await createLiveWrapper(true) | ||
119 | await runAndTestFfmpegStreamError(servers[0].url, userAccessToken, userVideoLiveoId, true) | ||
120 | |||
121 | await waitJobs(servers) | ||
122 | |||
123 | await checkSaveReplay(userVideoLiveoId) | ||
124 | }) | ||
125 | |||
126 | it('Should have size limit depending on user daily quota if save replay is enabled', async function () { | ||
127 | this.timeout(60000) | ||
128 | |||
129 | // Wait for user quota memoize cache invalidation | ||
130 | await wait(5000) | ||
131 | |||
132 | await updateUser({ | ||
133 | url: servers[0].url, | ||
134 | userId, | ||
135 | accessToken: servers[0].accessToken, | ||
136 | videoQuota: -1, | ||
137 | videoQuotaDaily: 1 | ||
138 | }) | ||
139 | |||
140 | const userVideoLiveoId = await createLiveWrapper(true) | ||
141 | await runAndTestFfmpegStreamError(servers[0].url, userAccessToken, userVideoLiveoId, true) | ||
142 | |||
143 | await waitJobs(servers) | ||
144 | |||
145 | await checkSaveReplay(userVideoLiveoId) | ||
146 | }) | ||
147 | |||
148 | it('Should succeed without quota limit', async function () { | ||
149 | this.timeout(60000) | ||
150 | |||
151 | // Wait for user quota memoize cache invalidation | ||
152 | await wait(5000) | ||
153 | |||
154 | await updateUser({ | ||
155 | url: servers[0].url, | ||
156 | userId, | ||
157 | accessToken: servers[0].accessToken, | ||
158 | videoQuota: 10 * 1000 * 1000, | ||
159 | videoQuotaDaily: -1 | ||
160 | }) | ||
161 | |||
162 | const userVideoLiveoId = await createLiveWrapper(true) | ||
163 | await runAndTestFfmpegStreamError(servers[0].url, userAccessToken, userVideoLiveoId, false) | ||
164 | }) | ||
165 | |||
166 | it('Should have max duration limit', async function () { | ||
167 | this.timeout(30000) | ||
168 | |||
169 | await updateCustomSubConfig(servers[0].url, servers[0].accessToken, { | ||
170 | live: { | ||
171 | enabled: true, | ||
172 | allowReplay: true, | ||
173 | maxDuration: 1, | ||
174 | transcoding: { | ||
175 | enabled: true, | ||
176 | resolutions: { | ||
177 | '240p': true, | ||
178 | '360p': true, | ||
179 | '480p': true, | ||
180 | '720p': true, | ||
181 | '1080p': true, | ||
182 | '2160p': true | ||
183 | } | ||
184 | } | ||
185 | } | ||
186 | }) | ||
187 | |||
188 | const userVideoLiveoId = await createLiveWrapper(true) | ||
189 | await runAndTestFfmpegStreamError(servers[0].url, userAccessToken, userVideoLiveoId, true) | ||
190 | |||
191 | await waitJobs(servers) | ||
192 | |||
193 | await checkSaveReplay(userVideoLiveoId, [ 720, 480, 360, 240 ]) | ||
194 | }) | ||
195 | |||
196 | after(async function () { | ||
197 | await cleanupTests(servers) | ||
198 | }) | ||
199 | }) | ||
diff --git a/server/tests/api/live/live-save-replay.ts b/server/tests/api/live/live-save-replay.ts new file mode 100644 index 000000000..3ffa0c093 --- /dev/null +++ b/server/tests/api/live/live-save-replay.ts | |||
@@ -0,0 +1,307 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import 'mocha' | ||
4 | import * as chai from 'chai' | ||
5 | import { FfmpegCommand } from 'fluent-ffmpeg' | ||
6 | import { LiveVideoCreate, VideoDetails, VideoPrivacy, VideoState } from '@shared/models' | ||
7 | import { | ||
8 | addVideoToBlacklist, | ||
9 | checkLiveCleanup, | ||
10 | cleanupTests, | ||
11 | createLive, | ||
12 | doubleFollow, | ||
13 | flushAndRunMultipleServers, | ||
14 | getVideo, | ||
15 | getVideosList, | ||
16 | removeVideo, | ||
17 | sendRTMPStreamInVideo, | ||
18 | ServerInfo, | ||
19 | setAccessTokensToServers, | ||
20 | setDefaultVideoChannel, | ||
21 | stopFfmpeg, | ||
22 | testFfmpegStreamError, | ||
23 | updateCustomSubConfig, | ||
24 | updateVideo, | ||
25 | waitJobs, | ||
26 | waitUntilLiveStarts | ||
27 | } from '../../../../shared/extra-utils' | ||
28 | |||
29 | const expect = chai.expect | ||
30 | |||
31 | describe('Save replay setting', function () { | ||
32 | let servers: ServerInfo[] = [] | ||
33 | let liveVideoUUID: string | ||
34 | let ffmpegCommand: FfmpegCommand | ||
35 | |||
36 | async function createLiveWrapper (saveReplay: boolean) { | ||
37 | if (liveVideoUUID) { | ||
38 | try { | ||
39 | await removeVideo(servers[0].url, servers[0].accessToken, liveVideoUUID) | ||
40 | await waitJobs(servers) | ||
41 | } catch {} | ||
42 | } | ||
43 | |||
44 | const attributes: LiveVideoCreate = { | ||
45 | channelId: servers[0].videoChannel.id, | ||
46 | privacy: VideoPrivacy.PUBLIC, | ||
47 | name: 'my super live', | ||
48 | saveReplay | ||
49 | } | ||
50 | |||
51 | const res = await createLive(servers[0].url, servers[0].accessToken, attributes) | ||
52 | return res.body.video.uuid | ||
53 | } | ||
54 | |||
55 | async function checkVideosExist (videoId: string, existsInList: boolean, getStatus?: number) { | ||
56 | for (const server of servers) { | ||
57 | const length = existsInList ? 1 : 0 | ||
58 | |||
59 | const resVideos = await getVideosList(server.url) | ||
60 | expect(resVideos.body.data).to.have.lengthOf(length) | ||
61 | expect(resVideos.body.total).to.equal(length) | ||
62 | |||
63 | if (getStatus) { | ||
64 | await getVideo(server.url, videoId, getStatus) | ||
65 | } | ||
66 | } | ||
67 | } | ||
68 | |||
69 | async function checkVideoState (videoId: string, state: VideoState) { | ||
70 | for (const server of servers) { | ||
71 | const res = await getVideo(server.url, videoId) | ||
72 | expect((res.body as VideoDetails).state.id).to.equal(state) | ||
73 | } | ||
74 | } | ||
75 | |||
76 | before(async function () { | ||
77 | this.timeout(120000) | ||
78 | |||
79 | servers = await flushAndRunMultipleServers(2) | ||
80 | |||
81 | // Get the access tokens | ||
82 | await setAccessTokensToServers(servers) | ||
83 | await setDefaultVideoChannel(servers) | ||
84 | |||
85 | // Server 1 and server 2 follow each other | ||
86 | await doubleFollow(servers[0], servers[1]) | ||
87 | |||
88 | await updateCustomSubConfig(servers[0].url, servers[0].accessToken, { | ||
89 | live: { | ||
90 | enabled: true, | ||
91 | allowReplay: true, | ||
92 | maxDuration: null, | ||
93 | transcoding: { | ||
94 | enabled: false, | ||
95 | resolutions: { | ||
96 | '240p': true, | ||
97 | '360p': true, | ||
98 | '480p': true, | ||
99 | '720p': true, | ||
100 | '1080p': true, | ||
101 | '2160p': true | ||
102 | } | ||
103 | } | ||
104 | } | ||
105 | }) | ||
106 | }) | ||
107 | |||
108 | describe('With save replay disabled', function () { | ||
109 | |||
110 | before(async function () { | ||
111 | this.timeout(10000) | ||
112 | }) | ||
113 | |||
114 | it('Should correctly create and federate the "waiting for stream" live', async function () { | ||
115 | this.timeout(20000) | ||
116 | |||
117 | liveVideoUUID = await createLiveWrapper(false) | ||
118 | |||
119 | await waitJobs(servers) | ||
120 | |||
121 | await checkVideosExist(liveVideoUUID, false, 200) | ||
122 | await checkVideoState(liveVideoUUID, VideoState.WAITING_FOR_LIVE) | ||
123 | }) | ||
124 | |||
125 | it('Should correctly have updated the live and federated it when streaming in the live', async function () { | ||
126 | this.timeout(20000) | ||
127 | |||
128 | ffmpegCommand = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoUUID) | ||
129 | await waitUntilLiveStarts(servers[0].url, servers[0].accessToken, liveVideoUUID) | ||
130 | |||
131 | await waitJobs(servers) | ||
132 | |||
133 | await checkVideosExist(liveVideoUUID, true, 200) | ||
134 | await checkVideoState(liveVideoUUID, VideoState.PUBLISHED) | ||
135 | }) | ||
136 | |||
137 | it('Should correctly delete the video files after the stream ended', async function () { | ||
138 | this.timeout(30000) | ||
139 | |||
140 | await stopFfmpeg(ffmpegCommand) | ||
141 | |||
142 | await waitJobs(servers) | ||
143 | |||
144 | // Live still exist, but cannot be played anymore | ||
145 | await checkVideosExist(liveVideoUUID, false, 200) | ||
146 | await checkVideoState(liveVideoUUID, VideoState.LIVE_ENDED) | ||
147 | |||
148 | // No resolutions saved since we did not save replay | ||
149 | await checkLiveCleanup(servers[0], liveVideoUUID, []) | ||
150 | }) | ||
151 | |||
152 | it('Should correctly terminate the stream on blacklist and delete the live', async function () { | ||
153 | this.timeout(40000) | ||
154 | |||
155 | liveVideoUUID = await createLiveWrapper(false) | ||
156 | |||
157 | ffmpegCommand = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoUUID) | ||
158 | await waitUntilLiveStarts(servers[0].url, servers[0].accessToken, liveVideoUUID) | ||
159 | |||
160 | await waitJobs(servers) | ||
161 | await checkVideosExist(liveVideoUUID, true, 200) | ||
162 | |||
163 | await Promise.all([ | ||
164 | addVideoToBlacklist(servers[0].url, servers[0].accessToken, liveVideoUUID, 'bad live', true), | ||
165 | testFfmpegStreamError(ffmpegCommand, true) | ||
166 | ]) | ||
167 | |||
168 | await waitJobs(servers) | ||
169 | |||
170 | await checkVideosExist(liveVideoUUID, false) | ||
171 | |||
172 | await getVideo(servers[0].url, liveVideoUUID, 401) | ||
173 | await getVideo(servers[1].url, liveVideoUUID, 404) | ||
174 | |||
175 | await checkLiveCleanup(servers[0], liveVideoUUID, []) | ||
176 | }) | ||
177 | |||
178 | it('Should correctly terminate the stream on delete and delete the video', async function () { | ||
179 | this.timeout(40000) | ||
180 | |||
181 | liveVideoUUID = await createLiveWrapper(false) | ||
182 | |||
183 | ffmpegCommand = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoUUID) | ||
184 | await waitUntilLiveStarts(servers[0].url, servers[0].accessToken, liveVideoUUID) | ||
185 | |||
186 | await waitJobs(servers) | ||
187 | await checkVideosExist(liveVideoUUID, true, 200) | ||
188 | |||
189 | await Promise.all([ | ||
190 | testFfmpegStreamError(ffmpegCommand, true), | ||
191 | removeVideo(servers[0].url, servers[0].accessToken, liveVideoUUID) | ||
192 | ]) | ||
193 | |||
194 | await waitJobs(servers) | ||
195 | |||
196 | await checkVideosExist(liveVideoUUID, false, 404) | ||
197 | await checkLiveCleanup(servers[0], liveVideoUUID, []) | ||
198 | }) | ||
199 | }) | ||
200 | |||
201 | describe('With save replay enabled', function () { | ||
202 | |||
203 | it('Should correctly create and federate the "waiting for stream" live', async function () { | ||
204 | this.timeout(20000) | ||
205 | |||
206 | liveVideoUUID = await createLiveWrapper(true) | ||
207 | |||
208 | await waitJobs(servers) | ||
209 | |||
210 | await checkVideosExist(liveVideoUUID, false, 200) | ||
211 | await checkVideoState(liveVideoUUID, VideoState.WAITING_FOR_LIVE) | ||
212 | }) | ||
213 | |||
214 | it('Should correctly have updated the live and federated it when streaming in the live', async function () { | ||
215 | this.timeout(20000) | ||
216 | |||
217 | ffmpegCommand = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoUUID) | ||
218 | await waitUntilLiveStarts(servers[0].url, servers[0].accessToken, liveVideoUUID) | ||
219 | |||
220 | await waitJobs(servers) | ||
221 | |||
222 | await checkVideosExist(liveVideoUUID, true, 200) | ||
223 | await checkVideoState(liveVideoUUID, VideoState.PUBLISHED) | ||
224 | }) | ||
225 | |||
226 | it('Should correctly have saved the live and federated it after the streaming', async function () { | ||
227 | this.timeout(30000) | ||
228 | |||
229 | await stopFfmpeg(ffmpegCommand) | ||
230 | |||
231 | await waitJobs(servers) | ||
232 | |||
233 | // Live has been transcoded | ||
234 | await checkVideosExist(liveVideoUUID, true, 200) | ||
235 | await checkVideoState(liveVideoUUID, VideoState.PUBLISHED) | ||
236 | }) | ||
237 | |||
238 | it('Should update the saved live and correctly federate the updated attributes', async function () { | ||
239 | this.timeout(30000) | ||
240 | |||
241 | await updateVideo(servers[0].url, servers[0].accessToken, liveVideoUUID, { name: 'video updated' }) | ||
242 | await waitJobs(servers) | ||
243 | |||
244 | for (const server of servers) { | ||
245 | const res = await getVideo(server.url, liveVideoUUID) | ||
246 | expect(res.body.name).to.equal('video updated') | ||
247 | expect(res.body.isLive).to.be.false | ||
248 | } | ||
249 | }) | ||
250 | |||
251 | it('Should have cleaned up the live files', async function () { | ||
252 | await checkLiveCleanup(servers[0], liveVideoUUID, [ 720 ]) | ||
253 | }) | ||
254 | |||
255 | it('Should correctly terminate the stream on blacklist and blacklist the saved replay video', async function () { | ||
256 | this.timeout(40000) | ||
257 | |||
258 | liveVideoUUID = await createLiveWrapper(true) | ||
259 | |||
260 | ffmpegCommand = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoUUID) | ||
261 | await waitUntilLiveStarts(servers[0].url, servers[0].accessToken, liveVideoUUID) | ||
262 | |||
263 | await waitJobs(servers) | ||
264 | await checkVideosExist(liveVideoUUID, true, 200) | ||
265 | |||
266 | await Promise.all([ | ||
267 | addVideoToBlacklist(servers[0].url, servers[0].accessToken, liveVideoUUID, 'bad live', true), | ||
268 | testFfmpegStreamError(ffmpegCommand, true) | ||
269 | ]) | ||
270 | |||
271 | await waitJobs(servers) | ||
272 | |||
273 | await checkVideosExist(liveVideoUUID, false) | ||
274 | |||
275 | await getVideo(servers[0].url, liveVideoUUID, 401) | ||
276 | await getVideo(servers[1].url, liveVideoUUID, 404) | ||
277 | |||
278 | await checkLiveCleanup(servers[0], liveVideoUUID, [ 720 ]) | ||
279 | }) | ||
280 | |||
281 | it('Should correctly terminate the stream on delete and delete the video', async function () { | ||
282 | this.timeout(40000) | ||
283 | |||
284 | liveVideoUUID = await createLiveWrapper(true) | ||
285 | |||
286 | ffmpegCommand = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoUUID) | ||
287 | await waitUntilLiveStarts(servers[0].url, servers[0].accessToken, liveVideoUUID) | ||
288 | |||
289 | await waitJobs(servers) | ||
290 | await checkVideosExist(liveVideoUUID, true, 200) | ||
291 | |||
292 | await Promise.all([ | ||
293 | removeVideo(servers[0].url, servers[0].accessToken, liveVideoUUID), | ||
294 | testFfmpegStreamError(ffmpegCommand, true) | ||
295 | ]) | ||
296 | |||
297 | await waitJobs(servers) | ||
298 | |||
299 | await checkVideosExist(liveVideoUUID, false, 404) | ||
300 | await checkLiveCleanup(servers[0], liveVideoUUID, []) | ||
301 | }) | ||
302 | }) | ||
303 | |||
304 | after(async function () { | ||
305 | await cleanupTests(servers) | ||
306 | }) | ||
307 | }) | ||
diff --git a/server/tests/api/live/live.ts b/server/tests/api/live/live.ts index f351e9650..f7ccb453d 100644 --- a/server/tests/api/live/live.ts +++ b/server/tests/api/live/live.ts | |||
@@ -4,6 +4,7 @@ import 'mocha' | |||
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { LiveVideo, LiveVideoCreate, User, VideoDetails, VideoPrivacy } from '@shared/models' | 5 | import { LiveVideo, LiveVideoCreate, User, VideoDetails, VideoPrivacy } from '@shared/models' |
6 | import { | 6 | import { |
7 | addVideoToBlacklist, | ||
7 | cleanupTests, | 8 | cleanupTests, |
8 | createLive, | 9 | createLive, |
9 | createUser, | 10 | createUser, |
@@ -15,6 +16,7 @@ import { | |||
15 | getVideosList, | 16 | getVideosList, |
16 | makeRawRequest, | 17 | makeRawRequest, |
17 | removeVideo, | 18 | removeVideo, |
19 | sendRTMPStream, | ||
18 | ServerInfo, | 20 | ServerInfo, |
19 | setAccessTokensToServers, | 21 | setAccessTokensToServers, |
20 | setDefaultVideoChannel, | 22 | setDefaultVideoChannel, |
@@ -22,9 +24,7 @@ import { | |||
22 | testImage, | 24 | testImage, |
23 | updateCustomSubConfig, | 25 | updateCustomSubConfig, |
24 | updateLive, | 26 | updateLive, |
25 | updateUser, | ||
26 | userLogin, | 27 | userLogin, |
27 | wait, | ||
28 | waitJobs | 28 | waitJobs |
29 | } from '../../../../shared/extra-utils' | 29 | } from '../../../../shared/extra-utils' |
30 | 30 | ||
@@ -32,7 +32,6 @@ const expect = chai.expect | |||
32 | 32 | ||
33 | describe('Test live', function () { | 33 | describe('Test live', function () { |
34 | let servers: ServerInfo[] = [] | 34 | let servers: ServerInfo[] = [] |
35 | let liveVideoUUID: string | ||
36 | let userId: number | 35 | let userId: number |
37 | let userAccessToken: string | 36 | let userAccessToken: string |
38 | let userChannelId: number | 37 | let userChannelId: number |
@@ -49,7 +48,10 @@ describe('Test live', function () { | |||
49 | await updateCustomSubConfig(servers[0].url, servers[0].accessToken, { | 48 | await updateCustomSubConfig(servers[0].url, servers[0].accessToken, { |
50 | live: { | 49 | live: { |
51 | enabled: true, | 50 | enabled: true, |
52 | allowReplay: true | 51 | allowReplay: true, |
52 | transcoding: { | ||
53 | enabled: false | ||
54 | } | ||
53 | } | 55 | } |
54 | }) | 56 | }) |
55 | 57 | ||
@@ -74,6 +76,7 @@ describe('Test live', function () { | |||
74 | }) | 76 | }) |
75 | 77 | ||
76 | describe('Live creation, update and delete', function () { | 78 | describe('Live creation, update and delete', function () { |
79 | let liveVideoUUID: string | ||
77 | 80 | ||
78 | it('Should create a live with the appropriate parameters', async function () { | 81 | it('Should create a live with the appropriate parameters', async function () { |
79 | this.timeout(20000) | 82 | this.timeout(20000) |
@@ -220,206 +223,74 @@ describe('Test live', function () { | |||
220 | }) | 223 | }) |
221 | }) | 224 | }) |
222 | 225 | ||
223 | describe('Test live constraints', function () { | 226 | describe('Stream checks', function () { |
227 | let liveVideo: LiveVideo & VideoDetails | ||
228 | let rtmpUrl: string | ||
229 | |||
230 | before(function () { | ||
231 | rtmpUrl = 'rtmp://' + servers[0].hostname + ':1936' | ||
232 | }) | ||
224 | 233 | ||
225 | async function createLiveWrapper (saveReplay: boolean) { | 234 | async function createLiveWrapper () { |
226 | const liveAttributes = { | 235 | const liveAttributes = { |
227 | name: 'user live', | 236 | name: 'user live', |
228 | channelId: userChannelId, | 237 | channelId: userChannelId, |
229 | privacy: VideoPrivacy.PUBLIC, | 238 | privacy: VideoPrivacy.PUBLIC, |
230 | saveReplay | 239 | saveReplay: false |
231 | } | 240 | } |
232 | 241 | ||
233 | const res = await createLive(servers[0].url, userAccessToken, liveAttributes) | 242 | const res = await createLive(servers[0].url, userAccessToken, liveAttributes) |
234 | return res.body.video.uuid as string | 243 | const uuid = res.body.video.uuid |
235 | } | ||
236 | |||
237 | before(async function () { | ||
238 | await updateCustomSubConfig(servers[0].url, servers[0].accessToken, { | ||
239 | live: { | ||
240 | enabled: true, | ||
241 | allowReplay: true | ||
242 | } | ||
243 | }) | ||
244 | |||
245 | await updateUser({ | ||
246 | url: servers[0].url, | ||
247 | userId, | ||
248 | accessToken: servers[0].accessToken, | ||
249 | videoQuota: 1, | ||
250 | videoQuotaDaily: -1 | ||
251 | }) | ||
252 | }) | ||
253 | 244 | ||
254 | it('Should not have size limit if save replay is disabled', async function () { | 245 | const resLive = await getLive(servers[0].url, servers[0].accessToken, uuid) |
255 | this.timeout(30000) | 246 | const resVideo = await getVideo(servers[0].url, uuid) |
256 | 247 | ||
257 | const userVideoLiveoId = await createLiveWrapper(false) | 248 | return Object.assign(resVideo.body, resLive.body) as LiveVideo & VideoDetails |
258 | await testFfmpegStreamError(servers[0].url, userAccessToken, userVideoLiveoId, false) | 249 | } |
259 | }) | ||
260 | 250 | ||
261 | it('Should have size limit depending on user global quota if save replay is enabled', async function () { | 251 | it('Should not allow a stream without the appropriate path', async function () { |
262 | this.timeout(30000) | 252 | this.timeout(30000) |
263 | 253 | ||
264 | const userVideoLiveoId = await createLiveWrapper(true) | 254 | liveVideo = await createLiveWrapper() |
265 | await testFfmpegStreamError(servers[0].url, userAccessToken, userVideoLiveoId, true) | ||
266 | |||
267 | await waitJobs(servers) | ||
268 | |||
269 | for (const server of servers) { | ||
270 | const res = await getVideo(server.url, userVideoLiveoId) | ||
271 | 255 | ||
272 | const video: VideoDetails = res.body | 256 | const command = sendRTMPStream(rtmpUrl + '/bad-live', liveVideo.streamKey) |
273 | expect(video.isLive).to.be.false | 257 | await testFfmpegStreamError(command, true) |
274 | expect(video.duration).to.be.greaterThan(0) | ||
275 | } | ||
276 | |||
277 | // TODO: check stream correctly saved + cleaned | ||
278 | }) | 258 | }) |
279 | 259 | ||
280 | it('Should have size limit depending on user daily quota if save replay is enabled', async function () { | 260 | it('Should not allow a stream without the appropriate stream key', async function () { |
281 | this.timeout(30000) | 261 | this.timeout(30000) |
282 | 262 | ||
283 | await updateUser({ | 263 | const command = sendRTMPStream(rtmpUrl + '/live', 'bad-stream-key') |
284 | url: servers[0].url, | 264 | await testFfmpegStreamError(command, true) |
285 | userId, | ||
286 | accessToken: servers[0].accessToken, | ||
287 | videoQuota: -1, | ||
288 | videoQuotaDaily: 1 | ||
289 | }) | ||
290 | |||
291 | const userVideoLiveoId = await createLiveWrapper(true) | ||
292 | await testFfmpegStreamError(servers[0].url, userAccessToken, userVideoLiveoId, true) | ||
293 | |||
294 | // TODO: check stream correctly saved + cleaned | ||
295 | }) | 265 | }) |
296 | 266 | ||
297 | it('Should succeed without quota limit', async function () { | 267 | it('Should succeed with the correct params', async function () { |
298 | this.timeout(30000) | 268 | this.timeout(30000) |
299 | 269 | ||
300 | // Wait for user quota memoize cache invalidation | 270 | const command = sendRTMPStream(rtmpUrl + '/live', liveVideo.streamKey) |
301 | await wait(5000) | 271 | await testFfmpegStreamError(command, false) |
302 | |||
303 | await updateUser({ | ||
304 | url: servers[0].url, | ||
305 | userId, | ||
306 | accessToken: servers[0].accessToken, | ||
307 | videoQuota: 10 * 1000 * 1000, | ||
308 | videoQuotaDaily: -1 | ||
309 | }) | ||
310 | |||
311 | const userVideoLiveoId = await createLiveWrapper(true) | ||
312 | await testFfmpegStreamError(servers[0].url, userAccessToken, userVideoLiveoId, false) | ||
313 | }) | 272 | }) |
314 | 273 | ||
315 | it('Should have max duration limit', async function () { | 274 | it('Should not allow a stream on a live that was blacklisted', async function () { |
316 | this.timeout(30000) | 275 | this.timeout(30000) |
317 | 276 | ||
318 | await updateCustomSubConfig(servers[0].url, servers[0].accessToken, { | 277 | liveVideo = await createLiveWrapper() |
319 | live: { | ||
320 | enabled: true, | ||
321 | allowReplay: true, | ||
322 | maxDuration: 1 | ||
323 | } | ||
324 | }) | ||
325 | |||
326 | const userVideoLiveoId = await createLiveWrapper(true) | ||
327 | await testFfmpegStreamError(servers[0].url, userAccessToken, userVideoLiveoId, true) | ||
328 | |||
329 | // TODO: check stream correctly saved + cleaned | ||
330 | }) | ||
331 | }) | ||
332 | |||
333 | describe('With save replay disabled', function () { | ||
334 | 278 | ||
335 | it('Should correctly create and federate the "waiting for stream" live', async function () { | 279 | await addVideoToBlacklist(servers[0].url, servers[0].accessToken, liveVideo.uuid) |
336 | 280 | ||
281 | const command = sendRTMPStream(rtmpUrl + '/live', liveVideo.streamKey) | ||
282 | await testFfmpegStreamError(command, true) | ||
337 | }) | 283 | }) |
338 | 284 | ||
339 | it('Should correctly have updated the live and federated it when streaming in the live', async function () { | 285 | it('Should not allow a stream on a live that was deleted', async function () { |
340 | 286 | this.timeout(30000) | |
341 | }) | ||
342 | |||
343 | it('Should correctly delete the video and the live after the stream ended', async function () { | ||
344 | // Wait 10 seconds | ||
345 | // get video 404 | ||
346 | // get video federation 404 | ||
347 | |||
348 | // check cleanup | ||
349 | }) | ||
350 | |||
351 | it('Should correctly terminate the stream on blacklist and delete the live', async function () { | ||
352 | // Wait 10 seconds | ||
353 | // get video 404 | ||
354 | // get video federation 404 | ||
355 | |||
356 | // check cleanup | ||
357 | }) | ||
358 | |||
359 | it('Should correctly terminate the stream on delete and delete the video', async function () { | ||
360 | // Wait 10 seconds | ||
361 | // get video 404 | ||
362 | // get video federation 404 | ||
363 | |||
364 | // check cleanup | ||
365 | }) | ||
366 | }) | ||
367 | |||
368 | describe('With save replay enabled', function () { | ||
369 | |||
370 | it('Should correctly create and federate the "waiting for stream" live', async function () { | ||
371 | |||
372 | }) | ||
373 | |||
374 | it('Should correctly have updated the live and federated it when streaming in the live', async function () { | ||
375 | |||
376 | }) | ||
377 | |||
378 | it('Should correctly have saved the live and federated it after the streaming', async function () { | ||
379 | |||
380 | }) | ||
381 | |||
382 | it('Should update the saved live and correctly federate the updated attributes', async function () { | ||
383 | |||
384 | }) | ||
385 | |||
386 | it('Should have cleaned up the live files', async function () { | ||
387 | |||
388 | }) | ||
389 | |||
390 | it('Should correctly terminate the stream on blacklist and blacklist the saved replay video', async function () { | ||
391 | // Wait 10 seconds | ||
392 | // get video -> blacklisted | ||
393 | // get video federation -> blacklisted | ||
394 | |||
395 | // check cleanup live files quand meme | ||
396 | }) | ||
397 | |||
398 | it('Should correctly terminate the stream on delete and delete the video', async function () { | ||
399 | // Wait 10 seconds | ||
400 | // get video 404 | ||
401 | // get video federation 404 | ||
402 | |||
403 | // check cleanup | ||
404 | }) | ||
405 | }) | ||
406 | |||
407 | describe('Stream checks', function () { | ||
408 | |||
409 | it('Should not allow a stream without the appropriate path', async function () { | ||
410 | |||
411 | }) | ||
412 | |||
413 | it('Should not allow a stream without the appropriate stream key', async function () { | ||
414 | |||
415 | }) | ||
416 | |||
417 | it('Should not allow a stream on a live that was blacklisted', async function () { | ||
418 | 287 | ||
419 | }) | 288 | liveVideo = await createLiveWrapper() |
420 | 289 | ||
421 | it('Should not allow a stream on a live that was deleted', async function () { | 290 | await removeVideo(servers[0].url, servers[0].accessToken, liveVideo.uuid) |
422 | 291 | ||
292 | const command = sendRTMPStream(rtmpUrl + '/live', liveVideo.streamKey) | ||
293 | await testFfmpegStreamError(command, true) | ||
423 | }) | 294 | }) |
424 | }) | 295 | }) |
425 | 296 | ||