diff options
author | Chocobozzz <me@florianbigard.com> | 2020-11-03 15:33:30 +0100 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2020-11-09 15:33:04 +0100 |
commit | 97969c4edf51b37eee691adba43368bb0fbb729b (patch) | |
tree | c1089f898fb936d75651630afcf406995eeb9fba | |
parent | af4ae64f6faf38f8179f2e07d3cd4ad60006be92 (diff) | |
download | PeerTube-97969c4edf51b37eee691adba43368bb0fbb729b.tar.gz PeerTube-97969c4edf51b37eee691adba43368bb0fbb729b.tar.zst PeerTube-97969c4edf51b37eee691adba43368bb0fbb729b.zip |
Add check constraints live tests
-rw-r--r-- | client/src/app/+admin/system/jobs/jobs.component.ts | 3 | ||||
-rw-r--r-- | server/initializers/constants.ts | 1 | ||||
-rw-r--r-- | server/lib/job-queue/handlers/video-live-ending.ts | 19 | ||||
-rw-r--r-- | server/lib/live-manager.ts | 14 | ||||
-rw-r--r-- | server/lib/user.ts | 6 | ||||
-rw-r--r-- | server/lib/video.ts | 15 | ||||
-rw-r--r-- | server/models/account/user.ts | 3 | ||||
-rw-r--r-- | server/models/video/video-file.ts | 8 | ||||
-rw-r--r-- | server/models/video/video.ts | 2 | ||||
-rw-r--r-- | server/tests/api/check-params/live.ts | 16 | ||||
-rw-r--r-- | server/tests/api/live/live.ts | 126 | ||||
-rw-r--r-- | shared/extra-utils/videos/live.ts | 46 | ||||
-rw-r--r-- | shared/models/videos/video-create.model.ts | 5 |
13 files changed, 228 insertions, 36 deletions
diff --git a/client/src/app/+admin/system/jobs/jobs.component.ts b/client/src/app/+admin/system/jobs/jobs.component.ts index 602362fe9..f8e12d1b6 100644 --- a/client/src/app/+admin/system/jobs/jobs.component.ts +++ b/client/src/app/+admin/system/jobs/jobs.component.ts | |||
@@ -33,7 +33,8 @@ export class JobsComponent extends RestTable implements OnInit { | |||
33 | 'videos-views', | 33 | 'videos-views', |
34 | 'activitypub-refresher', | 34 | 'activitypub-refresher', |
35 | 'video-live-ending', | 35 | 'video-live-ending', |
36 | 'video-redundancy' | 36 | 'video-redundancy', |
37 | 'video-live-ending' | ||
37 | ] | 38 | ] |
38 | 39 | ||
39 | jobs: Job[] = [] | 40 | jobs: Job[] = [] |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index f0d614112..f8380eaa0 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -733,6 +733,7 @@ if (isTestInstance() === true) { | |||
733 | 733 | ||
734 | FILES_CACHE.VIDEO_CAPTIONS.MAX_AGE = 3000 | 734 | FILES_CACHE.VIDEO_CAPTIONS.MAX_AGE = 3000 |
735 | MEMOIZE_TTL.OVERVIEWS_SAMPLE = 3000 | 735 | MEMOIZE_TTL.OVERVIEWS_SAMPLE = 3000 |
736 | MEMOIZE_TTL.LIVE_ABLE_TO_UPLOAD = 3000 | ||
736 | OVERVIEWS.VIDEOS.SAMPLE_THRESHOLD = 2 | 737 | OVERVIEWS.VIDEOS.SAMPLE_THRESHOLD = 2 |
737 | 738 | ||
738 | PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME = 5000 | 739 | PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME = 5000 |
diff --git a/server/lib/job-queue/handlers/video-live-ending.ts b/server/lib/job-queue/handlers/video-live-ending.ts index 32eeff4d1..1e964726e 100644 --- a/server/lib/job-queue/handlers/video-live-ending.ts +++ b/server/lib/job-queue/handlers/video-live-ending.ts | |||
@@ -8,9 +8,10 @@ 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 { VideoLiveModel } from '@server/models/video/video-live' | 9 | import { VideoLiveModel } from '@server/models/video/video-live' |
10 | import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' | 10 | import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' |
11 | import { MStreamingPlaylist, MVideo, MVideoLive } from '@server/types/models' | 11 | import { MStreamingPlaylist, MVideo, MVideoLive, MVideoWithFile } from '@server/types/models' |
12 | import { VideoLiveEndingPayload, VideoState } from '@shared/models' | 12 | import { VideoLiveEndingPayload, VideoState } from '@shared/models' |
13 | import { logger } from '../../../helpers/logger' | 13 | import { logger } from '../../../helpers/logger' |
14 | import { VideoFileModel } from '@server/models/video/video-file' | ||
14 | 15 | ||
15 | async function processVideoLiveEnding (job: Bull.Job) { | 16 | async function processVideoLiveEnding (job: Bull.Job) { |
16 | const payload = job.data as VideoLiveEndingPayload | 17 | const payload = job.data as VideoLiveEndingPayload |
@@ -60,6 +61,10 @@ async function saveLive (video: MVideo, live: MVideoLive) { | |||
60 | const segmentFiles = files.filter(f => f.startsWith(shouldStartWith) && f.endsWith('.ts')) | 61 | const segmentFiles = files.filter(f => f.startsWith(shouldStartWith) && f.endsWith('.ts')) |
61 | await hlsPlaylistToFragmentedMP4(hlsDirectory, segmentFiles, mp4TmpName) | 62 | await hlsPlaylistToFragmentedMP4(hlsDirectory, segmentFiles, mp4TmpName) |
62 | 63 | ||
64 | for (const file of segmentFiles) { | ||
65 | await remove(join(hlsDirectory, file)) | ||
66 | } | ||
67 | |||
63 | if (!duration) { | 68 | if (!duration) { |
64 | duration = await getDurationFromVideoFile(mp4TmpName) | 69 | duration = await getDurationFromVideoFile(mp4TmpName) |
65 | } | 70 | } |
@@ -77,8 +82,13 @@ async function saveLive (video: MVideo, live: MVideoLive) { | |||
77 | 82 | ||
78 | await video.save() | 83 | await video.save() |
79 | 84 | ||
85 | // Remove old HLS playlist video files | ||
80 | const videoWithFiles = await VideoModel.loadWithFiles(video.id) | 86 | const videoWithFiles = await VideoModel.loadWithFiles(video.id) |
81 | 87 | ||
88 | const hlsPlaylist = videoWithFiles.getHLSPlaylist() | ||
89 | await VideoFileModel.removeHLSFilesOfVideoId(hlsPlaylist.id) | ||
90 | hlsPlaylist.VideoFiles = [] | ||
91 | |||
82 | for (const resolution of resolutions) { | 92 | for (const resolution of resolutions) { |
83 | const videoInputPath = buildMP4TmpName(resolution) | 93 | const videoInputPath = buildMP4TmpName(resolution) |
84 | const { isPortraitMode } = await getVideoFileResolution(videoInputPath) | 94 | const { isPortraitMode } = await getVideoFileResolution(videoInputPath) |
@@ -90,12 +100,11 @@ async function saveLive (video: MVideo, live: MVideoLive) { | |||
90 | copyCodecs: true, | 100 | copyCodecs: true, |
91 | isPortraitMode | 101 | isPortraitMode |
92 | }) | 102 | }) |
93 | } | ||
94 | 103 | ||
95 | video.state = VideoState.PUBLISHED | 104 | await remove(join(hlsDirectory, videoInputPath)) |
96 | await video.save() | 105 | } |
97 | 106 | ||
98 | await publishAndFederateIfNeeded(video) | 107 | await publishAndFederateIfNeeded(video, true) |
99 | } | 108 | } |
100 | 109 | ||
101 | async function cleanupLive (video: MVideo, streamingPlaylist: MStreamingPlaylist) { | 110 | async function cleanupLive (video: MVideo, streamingPlaylist: MStreamingPlaylist) { |
diff --git a/server/lib/live-manager.ts b/server/lib/live-manager.ts index e115d2d50..2d8f906e9 100644 --- a/server/lib/live-manager.ts +++ b/server/lib/live-manager.ts | |||
@@ -133,10 +133,8 @@ class LiveManager { | |||
133 | const sessionId = this.videoSessions.get(videoId) | 133 | const sessionId = this.videoSessions.get(videoId) |
134 | if (!sessionId) return | 134 | if (!sessionId) return |
135 | 135 | ||
136 | this.videoSessions.delete(videoId) | ||
136 | this.abortSession(sessionId) | 137 | this.abortSession(sessionId) |
137 | |||
138 | this.onEndTransmuxing(videoId, true) | ||
139 | .catch(err => logger.error('Cannot end transmuxing of video %d.', videoId, { err })) | ||
140 | } | 138 | } |
141 | 139 | ||
142 | private getContext () { | 140 | private getContext () { |
@@ -259,9 +257,12 @@ class LiveManager { | |||
259 | updateSegment(segmentPath) | 257 | updateSegment(segmentPath) |
260 | 258 | ||
261 | if (this.isDurationConstraintValid(startStreamDateTime) !== true) { | 259 | if (this.isDurationConstraintValid(startStreamDateTime) !== true) { |
260 | logger.info('Stopping session of %s: max duration exceeded.', videoUUID) | ||
261 | |||
262 | this.stopSessionOf(videoLive.videoId) | 262 | this.stopSessionOf(videoLive.videoId) |
263 | } | 263 | } |
264 | 264 | ||
265 | // Check user quota if the user enabled replay saving | ||
265 | if (videoLive.saveReplay === true) { | 266 | if (videoLive.saveReplay === true) { |
266 | stat(segmentPath) | 267 | stat(segmentPath) |
267 | .then(segmentStat => { | 268 | .then(segmentStat => { |
@@ -270,6 +271,8 @@ class LiveManager { | |||
270 | .then(() => this.isQuotaConstraintValid(user, videoLive)) | 271 | .then(() => this.isQuotaConstraintValid(user, videoLive)) |
271 | .then(quotaValid => { | 272 | .then(quotaValid => { |
272 | if (quotaValid !== true) { | 273 | if (quotaValid !== true) { |
274 | logger.info('Stopping session of %s: user quota exceeded.', videoUUID) | ||
275 | |||
273 | this.stopSessionOf(videoLive.videoId) | 276 | this.stopSessionOf(videoLive.videoId) |
274 | } | 277 | } |
275 | }) | 278 | }) |
@@ -319,7 +322,7 @@ class LiveManager { | |||
319 | onFFmpegEnded() | 322 | onFFmpegEnded() |
320 | 323 | ||
321 | // Don't care that we killed the ffmpeg process | 324 | // Don't care that we killed the ffmpeg process |
322 | if (err?.message?.includes('SIGINT')) return | 325 | if (err?.message?.includes('Exiting normally')) return |
323 | 326 | ||
324 | logger.error('Live transcoding error.', { err, stdout, stderr }) | 327 | logger.error('Live transcoding error.', { err, stdout, stderr }) |
325 | 328 | ||
@@ -348,8 +351,7 @@ class LiveManager { | |||
348 | } | 351 | } |
349 | }, { delay: cleanupNow ? 0 : VIDEO_LIVE.CLEANUP_DELAY }) | 352 | }, { delay: cleanupNow ? 0 : VIDEO_LIVE.CLEANUP_DELAY }) |
350 | 353 | ||
351 | // FIXME: use end | 354 | fullVideo.state = VideoState.LIVE_ENDED |
352 | fullVideo.state = VideoState.WAITING_FOR_LIVE | ||
353 | await fullVideo.save() | 355 | await fullVideo.save() |
354 | 356 | ||
355 | PeerTubeSocket.Instance.sendVideoLiveNewState(fullVideo) | 357 | PeerTubeSocket.Instance.sendVideoLiveNewState(fullVideo) |
diff --git a/server/lib/user.ts b/server/lib/user.ts index d3338f329..7d6497302 100644 --- a/server/lib/user.ts +++ b/server/lib/user.ts | |||
@@ -18,8 +18,6 @@ import { Redis } from './redis' | |||
18 | import { createLocalVideoChannel } from './video-channel' | 18 | import { createLocalVideoChannel } from './video-channel' |
19 | import { createWatchLaterPlaylist } from './video-playlist' | 19 | import { createWatchLaterPlaylist } from './video-playlist' |
20 | 20 | ||
21 | import memoizee = require('memoizee') | ||
22 | |||
23 | type ChannelNames = { name: string, displayName: string } | 21 | type ChannelNames = { name: string, displayName: string } |
24 | 22 | ||
25 | async function createUserAccountAndChannelAndPlaylist (parameters: { | 23 | async function createUserAccountAndChannelAndPlaylist (parameters: { |
@@ -152,8 +150,8 @@ async function isAbleToUploadVideo (userId: number, size: number) { | |||
152 | if (user.videoQuota === -1 && user.videoQuotaDaily === -1) return Promise.resolve(true) | 150 | if (user.videoQuota === -1 && user.videoQuotaDaily === -1) return Promise.resolve(true) |
153 | 151 | ||
154 | const [ totalBytes, totalBytesDaily ] = await Promise.all([ | 152 | const [ totalBytes, totalBytesDaily ] = await Promise.all([ |
155 | getOriginalVideoFileTotalFromUser(user.id), | 153 | getOriginalVideoFileTotalFromUser(user), |
156 | getOriginalVideoFileTotalDailyFromUser(user.id) | 154 | getOriginalVideoFileTotalDailyFromUser(user) |
157 | ]) | 155 | ]) |
158 | 156 | ||
159 | const uploadedTotal = size + totalBytes | 157 | const uploadedTotal = size + totalBytes |
diff --git a/server/lib/video.ts b/server/lib/video.ts index 81b7c4159..8d9918b2d 100644 --- a/server/lib/video.ts +++ b/server/lib/video.ts | |||
@@ -4,7 +4,7 @@ import { TagModel } from '@server/models/video/tag' | |||
4 | import { VideoModel } from '@server/models/video/video' | 4 | import { VideoModel } from '@server/models/video/video' |
5 | import { FilteredModelAttributes } from '@server/types' | 5 | import { FilteredModelAttributes } from '@server/types' |
6 | import { MTag, MThumbnail, MVideoTag, MVideoThumbnail, MVideoUUID } from '@server/types/models' | 6 | import { MTag, MThumbnail, MVideoTag, MVideoThumbnail, MVideoUUID } from '@server/types/models' |
7 | import { ThumbnailType, VideoCreate, VideoPrivacy } from '@shared/models' | 7 | import { ThumbnailType, VideoCreate, VideoPrivacy, VideoState } from '@shared/models' |
8 | import { federateVideoIfNeeded } from './activitypub/videos' | 8 | import { federateVideoIfNeeded } from './activitypub/videos' |
9 | import { Notifier } from './notifier' | 9 | import { Notifier } from './notifier' |
10 | import { createVideoMiniatureFromExisting } from './thumbnail' | 10 | import { createVideoMiniatureFromExisting } from './thumbnail' |
@@ -81,8 +81,8 @@ async function setVideoTags (options: { | |||
81 | } | 81 | } |
82 | } | 82 | } |
83 | 83 | ||
84 | async function publishAndFederateIfNeeded (video: MVideoUUID) { | 84 | async function publishAndFederateIfNeeded (video: MVideoUUID, wasLive = false) { |
85 | const { videoDatabase, videoPublished } = await sequelizeTypescript.transaction(async t => { | 85 | const result = await sequelizeTypescript.transaction(async t => { |
86 | // Maybe the video changed in database, refresh it | 86 | // Maybe the video changed in database, refresh it |
87 | const videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t) | 87 | const videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t) |
88 | // Video does not exist anymore | 88 | // Video does not exist anymore |
@@ -92,14 +92,15 @@ async function publishAndFederateIfNeeded (video: MVideoUUID) { | |||
92 | const videoPublished = await videoDatabase.publishIfNeededAndSave(t) | 92 | const videoPublished = await videoDatabase.publishIfNeededAndSave(t) |
93 | 93 | ||
94 | // If the video was not published, we consider it is a new one for other instances | 94 | // If the video was not published, we consider it is a new one for other instances |
95 | await federateVideoIfNeeded(videoDatabase, videoPublished, t) | 95 | // Live videos are always federated, so it's not a new video |
96 | await federateVideoIfNeeded(videoDatabase, !wasLive && videoPublished, t) | ||
96 | 97 | ||
97 | return { videoDatabase, videoPublished } | 98 | return { videoDatabase, videoPublished } |
98 | }) | 99 | }) |
99 | 100 | ||
100 | if (videoPublished) { | 101 | if (result?.videoPublished) { |
101 | Notifier.Instance.notifyOnNewVideoIfNeeded(videoDatabase) | 102 | Notifier.Instance.notifyOnNewVideoIfNeeded(result.videoDatabase) |
102 | Notifier.Instance.notifyOnVideoPublishedAfterTranscoding(videoDatabase) | 103 | Notifier.Instance.notifyOnVideoPublishedAfterTranscoding(result.videoDatabase) |
103 | } | 104 | } |
104 | } | 105 | } |
105 | 106 | ||
diff --git a/server/models/account/user.ts b/server/models/account/user.ts index f64568c54..2aa6469fb 100644 --- a/server/models/account/user.ts +++ b/server/models/account/user.ts | |||
@@ -26,7 +26,6 @@ import { | |||
26 | MUser, | 26 | MUser, |
27 | MUserDefault, | 27 | MUserDefault, |
28 | MUserFormattable, | 28 | MUserFormattable, |
29 | MUserId, | ||
30 | MUserNotifSettingChannelDefault, | 29 | MUserNotifSettingChannelDefault, |
31 | MUserWithNotificationSetting, | 30 | MUserWithNotificationSetting, |
32 | MVideoFullLight | 31 | MVideoFullLight |
@@ -68,10 +67,10 @@ import { getSort, throwIfNotValid } from '../utils' | |||
68 | import { VideoModel } from '../video/video' | 67 | import { VideoModel } from '../video/video' |
69 | import { VideoChannelModel } from '../video/video-channel' | 68 | import { VideoChannelModel } from '../video/video-channel' |
70 | import { VideoImportModel } from '../video/video-import' | 69 | import { VideoImportModel } from '../video/video-import' |
70 | import { VideoLiveModel } from '../video/video-live' | ||
71 | import { VideoPlaylistModel } from '../video/video-playlist' | 71 | import { VideoPlaylistModel } from '../video/video-playlist' |
72 | import { AccountModel } from './account' | 72 | import { AccountModel } from './account' |
73 | import { UserNotificationSettingModel } from './user-notification-setting' | 73 | import { UserNotificationSettingModel } from './user-notification-setting' |
74 | import { VideoLiveModel } from '../video/video-live' | ||
75 | 74 | ||
76 | enum ScopeNames { | 75 | enum ScopeNames { |
77 | FOR_ME_API = 'FOR_ME_API', | 76 | FOR_ME_API = 'FOR_ME_API', |
diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts index 6a321917c..8c8fc0b51 100644 --- a/server/models/video/video-file.ts +++ b/server/models/video/video-file.ts | |||
@@ -311,6 +311,14 @@ export class VideoFileModel extends Model<VideoFileModel> { | |||
311 | return element.save({ transaction }) | 311 | return element.save({ transaction }) |
312 | } | 312 | } |
313 | 313 | ||
314 | static removeHLSFilesOfVideoId (videoStreamingPlaylistId: number) { | ||
315 | const options = { | ||
316 | where: { videoStreamingPlaylistId } | ||
317 | } | ||
318 | |||
319 | return VideoFileModel.destroy(options) | ||
320 | } | ||
321 | |||
314 | getVideoOrStreamingPlaylist (this: MVideoFileVideo | MVideoFileStreamingPlaylistVideo): MVideo | MStreamingPlaylistVideo { | 322 | getVideoOrStreamingPlaylist (this: MVideoFileVideo | MVideoFileStreamingPlaylistVideo): MVideo | MStreamingPlaylistVideo { |
315 | if (this.videoId) return (this as MVideoFileVideo).Video | 323 | if (this.videoId) return (this as MVideoFileVideo).Video |
316 | 324 | ||
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index aba8c8cf4..7e008f7ea 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -249,7 +249,7 @@ export type AvailableForListIDsOptions = { | |||
249 | [ScopeNames.WITH_LIVE]: { | 249 | [ScopeNames.WITH_LIVE]: { |
250 | include: [ | 250 | include: [ |
251 | { | 251 | { |
252 | model: VideoLiveModel, | 252 | model: VideoLiveModel.unscoped(), |
253 | required: false | 253 | required: false |
254 | } | 254 | } |
255 | ] | 255 | ] |
diff --git a/server/tests/api/check-params/live.ts b/server/tests/api/check-params/live.ts index 4134fca0c..3e97dffdc 100644 --- a/server/tests/api/check-params/live.ts +++ b/server/tests/api/check-params/live.ts | |||
@@ -18,6 +18,7 @@ import { | |||
18 | ServerInfo, | 18 | ServerInfo, |
19 | setAccessTokensToServers, | 19 | setAccessTokensToServers, |
20 | stopFfmpeg, | 20 | stopFfmpeg, |
21 | testFfmpegStreamError, | ||
21 | updateCustomSubConfig, | 22 | updateCustomSubConfig, |
22 | updateLive, | 23 | updateLive, |
23 | uploadVideoAndGetId, | 24 | uploadVideoAndGetId, |
@@ -402,6 +403,21 @@ describe('Test video lives API validator', function () { | |||
402 | 403 | ||
403 | await stopFfmpeg(command) | 404 | await stopFfmpeg(command) |
404 | }) | 405 | }) |
406 | |||
407 | it('Should fail to stream twice in the save live', async function () { | ||
408 | this.timeout(30000) | ||
409 | |||
410 | const resLive = await getLive(server.url, server.accessToken, videoId) | ||
411 | const live: LiveVideo = resLive.body | ||
412 | |||
413 | const command = sendRTMPStream(live.rtmpUrl, live.streamKey) | ||
414 | |||
415 | await waitUntilLiveStarts(server.url, server.accessToken, videoId) | ||
416 | |||
417 | await testFfmpegStreamError(server.url, server.accessToken, videoId, true) | ||
418 | |||
419 | await stopFfmpeg(command) | ||
420 | }) | ||
405 | }) | 421 | }) |
406 | 422 | ||
407 | after(async function () { | 423 | after(async function () { |
diff --git a/server/tests/api/live/live.ts b/server/tests/api/live/live.ts index e66c0cb26..f351e9650 100644 --- a/server/tests/api/live/live.ts +++ b/server/tests/api/live/live.ts | |||
@@ -2,14 +2,15 @@ | |||
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { LiveVideo, LiveVideoCreate, VideoDetails, VideoPrivacy } from '@shared/models' | 5 | import { LiveVideo, LiveVideoCreate, User, VideoDetails, VideoPrivacy } from '@shared/models' |
6 | import { | 6 | import { |
7 | acceptChangeOwnership, | ||
8 | cleanupTests, | 7 | cleanupTests, |
9 | createLive, | 8 | createLive, |
9 | createUser, | ||
10 | doubleFollow, | 10 | doubleFollow, |
11 | flushAndRunMultipleServers, | 11 | flushAndRunMultipleServers, |
12 | getLive, | 12 | getLive, |
13 | getMyUserInformation, | ||
13 | getVideo, | 14 | getVideo, |
14 | getVideosList, | 15 | getVideosList, |
15 | makeRawRequest, | 16 | makeRawRequest, |
@@ -17,9 +18,13 @@ import { | |||
17 | ServerInfo, | 18 | ServerInfo, |
18 | setAccessTokensToServers, | 19 | setAccessTokensToServers, |
19 | setDefaultVideoChannel, | 20 | setDefaultVideoChannel, |
21 | testFfmpegStreamError, | ||
20 | testImage, | 22 | testImage, |
21 | updateCustomSubConfig, | 23 | updateCustomSubConfig, |
22 | updateLive, | 24 | updateLive, |
25 | updateUser, | ||
26 | userLogin, | ||
27 | wait, | ||
23 | waitJobs | 28 | waitJobs |
24 | } from '../../../../shared/extra-utils' | 29 | } from '../../../../shared/extra-utils' |
25 | 30 | ||
@@ -28,6 +33,9 @@ const expect = chai.expect | |||
28 | describe('Test live', function () { | 33 | describe('Test live', function () { |
29 | let servers: ServerInfo[] = [] | 34 | let servers: ServerInfo[] = [] |
30 | let liveVideoUUID: string | 35 | let liveVideoUUID: string |
36 | let userId: number | ||
37 | let userAccessToken: string | ||
38 | let userChannelId: number | ||
31 | 39 | ||
32 | before(async function () { | 40 | before(async function () { |
33 | this.timeout(120000) | 41 | this.timeout(120000) |
@@ -45,6 +53,22 @@ describe('Test live', function () { | |||
45 | } | 53 | } |
46 | }) | 54 | }) |
47 | 55 | ||
56 | { | ||
57 | const user = { username: 'user1', password: 'superpassword' } | ||
58 | const res = await createUser({ | ||
59 | url: servers[0].url, | ||
60 | accessToken: servers[0].accessToken, | ||
61 | username: user.username, | ||
62 | password: user.password | ||
63 | }) | ||
64 | userId = res.body.user.id | ||
65 | |||
66 | userAccessToken = await userLogin(servers[0], user) | ||
67 | |||
68 | const resMe = await getMyUserInformation(servers[0].url, userAccessToken) | ||
69 | userChannelId = (resMe.body as User).videoChannels[0].id | ||
70 | } | ||
71 | |||
48 | // Server 1 and server 2 follow each other | 72 | // Server 1 and server 2 follow each other |
49 | await doubleFollow(servers[0], servers[1]) | 73 | await doubleFollow(servers[0], servers[1]) |
50 | }) | 74 | }) |
@@ -198,17 +222,111 @@ describe('Test live', function () { | |||
198 | 222 | ||
199 | describe('Test live constraints', function () { | 223 | describe('Test live constraints', function () { |
200 | 224 | ||
225 | async function createLiveWrapper (saveReplay: boolean) { | ||
226 | const liveAttributes = { | ||
227 | name: 'user live', | ||
228 | channelId: userChannelId, | ||
229 | privacy: VideoPrivacy.PUBLIC, | ||
230 | saveReplay | ||
231 | } | ||
232 | |||
233 | const res = await createLive(servers[0].url, userAccessToken, liveAttributes) | ||
234 | return res.body.video.uuid as string | ||
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 | |||
201 | it('Should not have size limit if save replay is disabled', async function () { | 254 | it('Should not have size limit if save replay is disabled', async function () { |
255 | this.timeout(30000) | ||
202 | 256 | ||
257 | const userVideoLiveoId = await createLiveWrapper(false) | ||
258 | await testFfmpegStreamError(servers[0].url, userAccessToken, userVideoLiveoId, false) | ||
203 | }) | 259 | }) |
204 | 260 | ||
205 | it('Should have size limit if save replay is enabled', async function () { | 261 | it('Should have size limit depending on user global quota if save replay is enabled', async function () { |
206 | // daily quota + total quota | 262 | this.timeout(30000) |
263 | |||
264 | const userVideoLiveoId = await createLiveWrapper(true) | ||
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) | ||
207 | 271 | ||
272 | const video: VideoDetails = res.body | ||
273 | expect(video.isLive).to.be.false | ||
274 | expect(video.duration).to.be.greaterThan(0) | ||
275 | } | ||
276 | |||
277 | // TODO: check stream correctly saved + cleaned | ||
278 | }) | ||
279 | |||
280 | it('Should have size limit depending on user daily quota if save replay is enabled', async function () { | ||
281 | this.timeout(30000) | ||
282 | |||
283 | await updateUser({ | ||
284 | url: servers[0].url, | ||
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 | }) | ||
296 | |||
297 | it('Should succeed without quota limit', async function () { | ||
298 | this.timeout(30000) | ||
299 | |||
300 | // Wait for user quota memoize cache invalidation | ||
301 | await wait(5000) | ||
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) | ||
208 | }) | 313 | }) |
209 | 314 | ||
210 | it('Should have max duration limit', async function () { | 315 | it('Should have max duration limit', async function () { |
316 | this.timeout(30000) | ||
317 | |||
318 | await updateCustomSubConfig(servers[0].url, servers[0].accessToken, { | ||
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) | ||
211 | 328 | ||
329 | // TODO: check stream correctly saved + cleaned | ||
212 | }) | 330 | }) |
213 | }) | 331 | }) |
214 | 332 | ||
diff --git a/shared/extra-utils/videos/live.ts b/shared/extra-utils/videos/live.ts index 65942db0a..a391565a4 100644 --- a/shared/extra-utils/videos/live.ts +++ b/shared/extra-utils/videos/live.ts | |||
@@ -1,9 +1,9 @@ | |||
1 | import * as ffmpeg from 'fluent-ffmpeg' | 1 | import * as ffmpeg from 'fluent-ffmpeg' |
2 | import { LiveVideoCreate, LiveVideoUpdate, VideoDetails, VideoState } from '@shared/models' | 2 | import { omit } from 'lodash' |
3 | import { LiveVideo, LiveVideoCreate, LiveVideoUpdate, VideoDetails, VideoState } from '@shared/models' | ||
3 | import { buildAbsoluteFixturePath, wait } from '../miscs/miscs' | 4 | import { buildAbsoluteFixturePath, wait } from '../miscs/miscs' |
4 | import { makeGetRequest, makePutBodyRequest, makeUploadRequest } from '../requests/requests' | 5 | import { makeGetRequest, makePutBodyRequest, makeUploadRequest } from '../requests/requests' |
5 | import { getVideoWithToken } from './videos' | 6 | import { getVideoWithToken } from './videos' |
6 | import { omit } from 'lodash' | ||
7 | 7 | ||
8 | function getLive (url: string, token: string, videoId: number | string, statusCodeExpected = 200) { | 8 | function getLive (url: string, token: string, videoId: number | string, statusCodeExpected = 200) { |
9 | const path = '/api/v1/videos/live' | 9 | const path = '/api/v1/videos/live' |
@@ -47,7 +47,14 @@ function createLive (url: string, token: string, fields: LiveVideoCreate, status | |||
47 | }) | 47 | }) |
48 | } | 48 | } |
49 | 49 | ||
50 | function sendRTMPStream (rtmpBaseUrl: string, streamKey: string) { | 50 | async function sendRTMPStreamInVideo (url: string, token: string, videoId: number | string, onErrorCb?: Function) { |
51 | const res = await getLive(url, token, videoId) | ||
52 | const videoLive = res.body as LiveVideo | ||
53 | |||
54 | return sendRTMPStream(videoLive.rtmpUrl, videoLive.streamKey, onErrorCb) | ||
55 | } | ||
56 | |||
57 | function sendRTMPStream (rtmpBaseUrl: string, streamKey: string, onErrorCb?: Function) { | ||
51 | const fixture = buildAbsoluteFixturePath('video_short.mp4') | 58 | const fixture = buildAbsoluteFixturePath('video_short.mp4') |
52 | 59 | ||
53 | const command = ffmpeg(fixture) | 60 | const command = ffmpeg(fixture) |
@@ -63,7 +70,7 @@ function sendRTMPStream (rtmpBaseUrl: string, streamKey: string) { | |||
63 | command.on('error', err => { | 70 | command.on('error', err => { |
64 | if (err?.message?.includes('Exiting normally')) return | 71 | if (err?.message?.includes('Exiting normally')) return |
65 | 72 | ||
66 | console.error('Cannot send RTMP stream.', { err }) | 73 | if (onErrorCb) onErrorCb(err) |
67 | }) | 74 | }) |
68 | 75 | ||
69 | if (process.env.DEBUG) { | 76 | if (process.env.DEBUG) { |
@@ -75,6 +82,34 @@ function sendRTMPStream (rtmpBaseUrl: string, streamKey: string) { | |||
75 | return command | 82 | return command |
76 | } | 83 | } |
77 | 84 | ||
85 | function waitFfmpegUntilError (command: ffmpeg.FfmpegCommand, successAfterMS = 10000) { | ||
86 | return new Promise((res, rej) => { | ||
87 | command.on('error', err => { | ||
88 | return rej(err) | ||
89 | }) | ||
90 | |||
91 | setTimeout(() => { | ||
92 | res() | ||
93 | }, successAfterMS) | ||
94 | }) | ||
95 | } | ||
96 | |||
97 | async function testFfmpegStreamError (url: string, token: string, videoId: number | string, shouldHaveError: boolean) { | ||
98 | const command = await sendRTMPStreamInVideo(url, token, videoId) | ||
99 | let error: Error | ||
100 | |||
101 | try { | ||
102 | await waitFfmpegUntilError(command, 10000) | ||
103 | } catch (err) { | ||
104 | error = err | ||
105 | } | ||
106 | |||
107 | await stopFfmpeg(command) | ||
108 | |||
109 | if (shouldHaveError && !error) throw new Error('Ffmpeg did not have an error') | ||
110 | if (!shouldHaveError && error) throw error | ||
111 | } | ||
112 | |||
78 | async function stopFfmpeg (command: ffmpeg.FfmpegCommand) { | 113 | async function stopFfmpeg (command: ffmpeg.FfmpegCommand) { |
79 | command.kill('SIGINT') | 114 | command.kill('SIGINT') |
80 | 115 | ||
@@ -99,6 +134,9 @@ export { | |||
99 | updateLive, | 134 | updateLive, |
100 | waitUntilLiveStarts, | 135 | waitUntilLiveStarts, |
101 | createLive, | 136 | createLive, |
137 | testFfmpegStreamError, | ||
102 | stopFfmpeg, | 138 | stopFfmpeg, |
139 | sendRTMPStreamInVideo, | ||
140 | waitFfmpegUntilError, | ||
103 | sendRTMPStream | 141 | sendRTMPStream |
104 | } | 142 | } |
diff --git a/shared/models/videos/video-create.model.ts b/shared/models/videos/video-create.model.ts index 9e980529d..732d508d1 100644 --- a/shared/models/videos/video-create.model.ts +++ b/shared/models/videos/video-create.model.ts | |||
@@ -2,15 +2,16 @@ import { VideoPrivacy } from './video-privacy.enum' | |||
2 | import { VideoScheduleUpdate } from './video-schedule-update.model' | 2 | import { VideoScheduleUpdate } from './video-schedule-update.model' |
3 | 3 | ||
4 | export interface VideoCreate { | 4 | export interface VideoCreate { |
5 | name: string | ||
6 | channelId: number | ||
7 | |||
5 | category?: number | 8 | category?: number |
6 | licence?: number | 9 | licence?: number |
7 | language?: string | 10 | language?: string |
8 | description?: string | 11 | description?: string |
9 | support?: string | 12 | support?: string |
10 | channelId: number | ||
11 | nsfw?: boolean | 13 | nsfw?: boolean |
12 | waitTranscoding?: boolean | 14 | waitTranscoding?: boolean |
13 | name: string | ||
14 | tags?: string[] | 15 | tags?: string[] |
15 | commentsEnabled?: boolean | 16 | commentsEnabled?: boolean |
16 | downloadEnabled?: boolean | 17 | downloadEnabled?: boolean |