aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2020-11-03 15:33:30 +0100
committerChocobozzz <chocobozzz@cpy.re>2020-11-09 15:33:04 +0100
commit97969c4edf51b37eee691adba43368bb0fbb729b (patch)
treec1089f898fb936d75651630afcf406995eeb9fba
parentaf4ae64f6faf38f8179f2e07d3cd4ad60006be92 (diff)
downloadPeerTube-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.ts3
-rw-r--r--server/initializers/constants.ts1
-rw-r--r--server/lib/job-queue/handlers/video-live-ending.ts19
-rw-r--r--server/lib/live-manager.ts14
-rw-r--r--server/lib/user.ts6
-rw-r--r--server/lib/video.ts15
-rw-r--r--server/models/account/user.ts3
-rw-r--r--server/models/video/video-file.ts8
-rw-r--r--server/models/video/video.ts2
-rw-r--r--server/tests/api/check-params/live.ts16
-rw-r--r--server/tests/api/live/live.ts126
-rw-r--r--shared/extra-utils/videos/live.ts46
-rw-r--r--shared/models/videos/video-create.model.ts5
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'
8import { VideoModel } from '@server/models/video/video' 8import { VideoModel } from '@server/models/video/video'
9import { VideoLiveModel } from '@server/models/video/video-live' 9import { VideoLiveModel } from '@server/models/video/video-live'
10import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' 10import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist'
11import { MStreamingPlaylist, MVideo, MVideoLive } from '@server/types/models' 11import { MStreamingPlaylist, MVideo, MVideoLive, MVideoWithFile } from '@server/types/models'
12import { VideoLiveEndingPayload, VideoState } from '@shared/models' 12import { VideoLiveEndingPayload, VideoState } from '@shared/models'
13import { logger } from '../../../helpers/logger' 13import { logger } from '../../../helpers/logger'
14import { VideoFileModel } from '@server/models/video/video-file'
14 15
15async function processVideoLiveEnding (job: Bull.Job) { 16async 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
101async function cleanupLive (video: MVideo, streamingPlaylist: MStreamingPlaylist) { 110async 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'
18import { createLocalVideoChannel } from './video-channel' 18import { createLocalVideoChannel } from './video-channel'
19import { createWatchLaterPlaylist } from './video-playlist' 19import { createWatchLaterPlaylist } from './video-playlist'
20 20
21import memoizee = require('memoizee')
22
23type ChannelNames = { name: string, displayName: string } 21type ChannelNames = { name: string, displayName: string }
24 22
25async function createUserAccountAndChannelAndPlaylist (parameters: { 23async 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'
4import { VideoModel } from '@server/models/video/video' 4import { VideoModel } from '@server/models/video/video'
5import { FilteredModelAttributes } from '@server/types' 5import { FilteredModelAttributes } from '@server/types'
6import { MTag, MThumbnail, MVideoTag, MVideoThumbnail, MVideoUUID } from '@server/types/models' 6import { MTag, MThumbnail, MVideoTag, MVideoThumbnail, MVideoUUID } from '@server/types/models'
7import { ThumbnailType, VideoCreate, VideoPrivacy } from '@shared/models' 7import { ThumbnailType, VideoCreate, VideoPrivacy, VideoState } from '@shared/models'
8import { federateVideoIfNeeded } from './activitypub/videos' 8import { federateVideoIfNeeded } from './activitypub/videos'
9import { Notifier } from './notifier' 9import { Notifier } from './notifier'
10import { createVideoMiniatureFromExisting } from './thumbnail' 10import { createVideoMiniatureFromExisting } from './thumbnail'
@@ -81,8 +81,8 @@ async function setVideoTags (options: {
81 } 81 }
82} 82}
83 83
84async function publishAndFederateIfNeeded (video: MVideoUUID) { 84async 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'
68import { VideoModel } from '../video/video' 67import { VideoModel } from '../video/video'
69import { VideoChannelModel } from '../video/video-channel' 68import { VideoChannelModel } from '../video/video-channel'
70import { VideoImportModel } from '../video/video-import' 69import { VideoImportModel } from '../video/video-import'
70import { VideoLiveModel } from '../video/video-live'
71import { VideoPlaylistModel } from '../video/video-playlist' 71import { VideoPlaylistModel } from '../video/video-playlist'
72import { AccountModel } from './account' 72import { AccountModel } from './account'
73import { UserNotificationSettingModel } from './user-notification-setting' 73import { UserNotificationSettingModel } from './user-notification-setting'
74import { VideoLiveModel } from '../video/video-live'
75 74
76enum ScopeNames { 75enum 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
3import 'mocha' 3import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
5import { LiveVideo, LiveVideoCreate, VideoDetails, VideoPrivacy } from '@shared/models' 5import { LiveVideo, LiveVideoCreate, User, VideoDetails, VideoPrivacy } from '@shared/models'
6import { 6import {
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
28describe('Test live', function () { 33describe('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 @@
1import * as ffmpeg from 'fluent-ffmpeg' 1import * as ffmpeg from 'fluent-ffmpeg'
2import { LiveVideoCreate, LiveVideoUpdate, VideoDetails, VideoState } from '@shared/models' 2import { omit } from 'lodash'
3import { LiveVideo, LiveVideoCreate, LiveVideoUpdate, VideoDetails, VideoState } from '@shared/models'
3import { buildAbsoluteFixturePath, wait } from '../miscs/miscs' 4import { buildAbsoluteFixturePath, wait } from '../miscs/miscs'
4import { makeGetRequest, makePutBodyRequest, makeUploadRequest } from '../requests/requests' 5import { makeGetRequest, makePutBodyRequest, makeUploadRequest } from '../requests/requests'
5import { getVideoWithToken } from './videos' 6import { getVideoWithToken } from './videos'
6import { omit } from 'lodash'
7 7
8function getLive (url: string, token: string, videoId: number | string, statusCodeExpected = 200) { 8function 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
50function sendRTMPStream (rtmpBaseUrl: string, streamKey: string) { 50async 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
57function 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
85function 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
97async 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
78async function stopFfmpeg (command: ffmpeg.FfmpegCommand) { 113async 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'
2import { VideoScheduleUpdate } from './video-schedule-update.model' 2import { VideoScheduleUpdate } from './video-schedule-update.model'
3 3
4export interface VideoCreate { 4export 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