diff options
-rw-r--r-- | config/test-1.yaml | 4 | ||||
-rw-r--r-- | server/helpers/ffmpeg-utils.ts | 1 | ||||
-rw-r--r-- | server/lib/live-manager.ts | 2 | ||||
-rw-r--r-- | server/tests/api/check-params/index.ts | 1 | ||||
-rw-r--r-- | server/tests/api/check-params/live.ts | 410 | ||||
-rw-r--r-- | shared/extra-utils/index.ts | 3 | ||||
-rw-r--r-- | shared/extra-utils/requests/requests.ts | 4 | ||||
-rw-r--r-- | shared/extra-utils/videos/live.ts | 102 | ||||
-rw-r--r-- | shared/models/videos/video-create.model.ts | 3 |
9 files changed, 527 insertions, 3 deletions
diff --git a/config/test-1.yaml b/config/test-1.yaml index 2ef9e6c7c..fe5b3cf44 100644 --- a/config/test-1.yaml +++ b/config/test-1.yaml | |||
@@ -35,3 +35,7 @@ signup: | |||
35 | 35 | ||
36 | transcoding: | 36 | transcoding: |
37 | enabled: false | 37 | enabled: false |
38 | |||
39 | live: | ||
40 | rtmp: | ||
41 | port: 1936 | ||
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts index b985988d3..268ed7624 100644 --- a/server/helpers/ffmpeg-utils.ts +++ b/server/helpers/ffmpeg-utils.ts | |||
@@ -385,6 +385,7 @@ function runLiveTranscoding (rtmpUrl: string, outPath: string, resolutions: numb | |||
385 | command.outputOption('-level 3.1') | 385 | command.outputOption('-level 3.1') |
386 | command.outputOption('-map_metadata -1') | 386 | command.outputOption('-map_metadata -1') |
387 | command.outputOption('-pix_fmt yuv420p') | 387 | command.outputOption('-pix_fmt yuv420p') |
388 | command.outputOption('-max_muxing_queue_size 1024') | ||
388 | 389 | ||
389 | for (let i = 0; i < resolutions.length; i++) { | 390 | for (let i = 0; i < resolutions.length; i++) { |
390 | const resolution = resolutions[i] | 391 | const resolution = resolutions[i] |
diff --git a/server/lib/live-manager.ts b/server/lib/live-manager.ts index fd9a859f9..e115d2d50 100644 --- a/server/lib/live-manager.ts +++ b/server/lib/live-manager.ts | |||
@@ -322,6 +322,8 @@ class LiveManager { | |||
322 | if (err?.message?.includes('SIGINT')) return | 322 | if (err?.message?.includes('SIGINT')) return |
323 | 323 | ||
324 | logger.error('Live transcoding error.', { err, stdout, stderr }) | 324 | logger.error('Live transcoding error.', { err, stdout, stderr }) |
325 | |||
326 | this.abortSession(sessionId) | ||
325 | }) | 327 | }) |
326 | 328 | ||
327 | ffmpegExec.on('end', () => onFFmpegEnded()) | 329 | ffmpegExec.on('end', () => onFFmpegEnded()) |
diff --git a/server/tests/api/check-params/index.ts b/server/tests/api/check-params/index.ts index 0ee1f27aa..b5f0d07be 100644 --- a/server/tests/api/check-params/index.ts +++ b/server/tests/api/check-params/index.ts | |||
@@ -8,6 +8,7 @@ import './debug' | |||
8 | import './follows' | 8 | import './follows' |
9 | import './jobs' | 9 | import './jobs' |
10 | import './logs' | 10 | import './logs' |
11 | import './live' | ||
11 | import './plugins' | 12 | import './plugins' |
12 | import './redundancy' | 13 | import './redundancy' |
13 | import './search' | 14 | import './search' |
diff --git a/server/tests/api/check-params/live.ts b/server/tests/api/check-params/live.ts new file mode 100644 index 000000000..4134fca0c --- /dev/null +++ b/server/tests/api/check-params/live.ts | |||
@@ -0,0 +1,410 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import 'mocha' | ||
4 | import * as chai from 'chai' | ||
5 | import { omit } from 'lodash' | ||
6 | import { join } from 'path' | ||
7 | import { LiveVideo, VideoPrivacy } from '@shared/models' | ||
8 | import { | ||
9 | cleanupTests, | ||
10 | createUser, | ||
11 | flushAndRunServer, | ||
12 | getLive, | ||
13 | getMyUserInformation, | ||
14 | immutableAssign, | ||
15 | makePostBodyRequest, | ||
16 | makeUploadRequest, | ||
17 | sendRTMPStream, | ||
18 | ServerInfo, | ||
19 | setAccessTokensToServers, | ||
20 | stopFfmpeg, | ||
21 | updateCustomSubConfig, | ||
22 | updateLive, | ||
23 | uploadVideoAndGetId, | ||
24 | userLogin, | ||
25 | waitUntilLiveStarts | ||
26 | } from '../../../../shared/extra-utils' | ||
27 | |||
28 | describe('Test video lives API validator', function () { | ||
29 | const path = '/api/v1/videos/live' | ||
30 | let server: ServerInfo | ||
31 | let userAccessToken = '' | ||
32 | let accountName: string | ||
33 | let channelId: number | ||
34 | let channelName: string | ||
35 | let videoId: number | ||
36 | let videoIdNotLive: number | ||
37 | |||
38 | // --------------------------------------------------------------- | ||
39 | |||
40 | before(async function () { | ||
41 | this.timeout(30000) | ||
42 | |||
43 | server = await flushAndRunServer(1) | ||
44 | |||
45 | await setAccessTokensToServers([ server ]) | ||
46 | |||
47 | await updateCustomSubConfig(server.url, server.accessToken, { | ||
48 | live: { | ||
49 | enabled: true, | ||
50 | maxInstanceLives: 20, | ||
51 | maxUserLives: 20, | ||
52 | allowReplay: true | ||
53 | } | ||
54 | }) | ||
55 | |||
56 | const username = 'user1' | ||
57 | const password = 'my super password' | ||
58 | await createUser({ url: server.url, accessToken: server.accessToken, username: username, password: password }) | ||
59 | userAccessToken = await userLogin(server, { username, password }) | ||
60 | |||
61 | { | ||
62 | const res = await getMyUserInformation(server.url, server.accessToken) | ||
63 | channelId = res.body.videoChannels[0].id | ||
64 | } | ||
65 | |||
66 | { | ||
67 | videoIdNotLive = (await uploadVideoAndGetId({ server, videoName: 'not live' })).id | ||
68 | } | ||
69 | }) | ||
70 | |||
71 | describe('When creating a live', function () { | ||
72 | let baseCorrectParams | ||
73 | |||
74 | before(function () { | ||
75 | baseCorrectParams = { | ||
76 | name: 'my super name', | ||
77 | category: 5, | ||
78 | licence: 1, | ||
79 | language: 'pt', | ||
80 | nsfw: false, | ||
81 | commentsEnabled: true, | ||
82 | downloadEnabled: true, | ||
83 | waitTranscoding: true, | ||
84 | description: 'my super description', | ||
85 | support: 'my super support text', | ||
86 | tags: [ 'tag1', 'tag2' ], | ||
87 | privacy: VideoPrivacy.PUBLIC, | ||
88 | channelId, | ||
89 | saveReplay: false | ||
90 | } | ||
91 | }) | ||
92 | |||
93 | it('Should fail with nothing', async function () { | ||
94 | const fields = {} | ||
95 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
96 | }) | ||
97 | |||
98 | it('Should fail with a long name', async function () { | ||
99 | const fields = immutableAssign(baseCorrectParams, { name: 'super'.repeat(65) }) | ||
100 | |||
101 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
102 | }) | ||
103 | |||
104 | it('Should fail with a bad category', async function () { | ||
105 | const fields = immutableAssign(baseCorrectParams, { category: 125 }) | ||
106 | |||
107 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
108 | }) | ||
109 | |||
110 | it('Should fail with a bad licence', async function () { | ||
111 | const fields = immutableAssign(baseCorrectParams, { licence: 125 }) | ||
112 | |||
113 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
114 | }) | ||
115 | |||
116 | it('Should fail with a bad language', async function () { | ||
117 | const fields = immutableAssign(baseCorrectParams, { language: 'a'.repeat(15) }) | ||
118 | |||
119 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
120 | }) | ||
121 | |||
122 | it('Should fail with a long description', async function () { | ||
123 | const fields = immutableAssign(baseCorrectParams, { description: 'super'.repeat(2500) }) | ||
124 | |||
125 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
126 | }) | ||
127 | |||
128 | it('Should fail with a long support text', async function () { | ||
129 | const fields = immutableAssign(baseCorrectParams, { support: 'super'.repeat(201) }) | ||
130 | |||
131 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
132 | }) | ||
133 | |||
134 | it('Should fail without a channel', async function () { | ||
135 | const fields = omit(baseCorrectParams, 'channelId') | ||
136 | |||
137 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
138 | }) | ||
139 | |||
140 | it('Should fail with a bad channel', async function () { | ||
141 | const fields = immutableAssign(baseCorrectParams, { channelId: 545454 }) | ||
142 | |||
143 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
144 | }) | ||
145 | |||
146 | it('Should fail with another user channel', async function () { | ||
147 | const user = { | ||
148 | username: 'fake', | ||
149 | password: 'fake_password' | ||
150 | } | ||
151 | await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password }) | ||
152 | |||
153 | const accessTokenUser = await userLogin(server, user) | ||
154 | const res = await getMyUserInformation(server.url, accessTokenUser) | ||
155 | const customChannelId = res.body.videoChannels[0].id | ||
156 | |||
157 | const fields = immutableAssign(baseCorrectParams, { channelId: customChannelId }) | ||
158 | |||
159 | await makePostBodyRequest({ url: server.url, path, token: userAccessToken, fields }) | ||
160 | }) | ||
161 | |||
162 | it('Should fail with too many tags', async function () { | ||
163 | const fields = immutableAssign(baseCorrectParams, { tags: [ 'tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6' ] }) | ||
164 | |||
165 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
166 | }) | ||
167 | |||
168 | it('Should fail with a tag length too low', async function () { | ||
169 | const fields = immutableAssign(baseCorrectParams, { tags: [ 'tag1', 't' ] }) | ||
170 | |||
171 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
172 | }) | ||
173 | |||
174 | it('Should fail with a tag length too big', async function () { | ||
175 | const fields = immutableAssign(baseCorrectParams, { tags: [ 'tag1', 'my_super_tag_too_long_long_long_long_long_long' ] }) | ||
176 | |||
177 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
178 | }) | ||
179 | |||
180 | it('Should fail with an incorrect thumbnail file', async function () { | ||
181 | const fields = baseCorrectParams | ||
182 | const attaches = { | ||
183 | thumbnailfile: join(__dirname, '..', '..', 'fixtures', 'avatar.png') | ||
184 | } | ||
185 | |||
186 | await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) | ||
187 | }) | ||
188 | |||
189 | it('Should fail with a big thumbnail file', async function () { | ||
190 | const fields = baseCorrectParams | ||
191 | const attaches = { | ||
192 | thumbnailfile: join(__dirname, '..', '..', 'fixtures', 'avatar-big.png') | ||
193 | } | ||
194 | |||
195 | await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) | ||
196 | }) | ||
197 | |||
198 | it('Should fail with an incorrect preview file', async function () { | ||
199 | const fields = baseCorrectParams | ||
200 | const attaches = { | ||
201 | previewfile: join(__dirname, '..', '..', 'fixtures', 'avatar.png') | ||
202 | } | ||
203 | |||
204 | await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) | ||
205 | }) | ||
206 | |||
207 | it('Should fail with a big preview file', async function () { | ||
208 | const fields = baseCorrectParams | ||
209 | const attaches = { | ||
210 | previewfile: join(__dirname, '..', '..', 'fixtures', 'avatar-big.png') | ||
211 | } | ||
212 | |||
213 | await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) | ||
214 | }) | ||
215 | |||
216 | it('Should succeed with the correct parameters', async function () { | ||
217 | this.timeout(30000) | ||
218 | |||
219 | const res = await makePostBodyRequest({ | ||
220 | url: server.url, | ||
221 | path, | ||
222 | token: server.accessToken, | ||
223 | fields: baseCorrectParams, | ||
224 | statusCodeExpected: 200 | ||
225 | }) | ||
226 | |||
227 | videoId = res.body.video.id | ||
228 | }) | ||
229 | |||
230 | it('Should forbid if live is disabled', async function () { | ||
231 | await updateCustomSubConfig(server.url, server.accessToken, { | ||
232 | live: { | ||
233 | enabled: false | ||
234 | } | ||
235 | }) | ||
236 | |||
237 | await makePostBodyRequest({ | ||
238 | url: server.url, | ||
239 | path, | ||
240 | token: server.accessToken, | ||
241 | fields: baseCorrectParams, | ||
242 | statusCodeExpected: 403 | ||
243 | }) | ||
244 | }) | ||
245 | |||
246 | it('Should forbid to save replay if not enabled by the admin', async function () { | ||
247 | const fields = immutableAssign(baseCorrectParams, { saveReplay: true }) | ||
248 | |||
249 | await updateCustomSubConfig(server.url, server.accessToken, { | ||
250 | live: { | ||
251 | enabled: true, | ||
252 | allowReplay: false | ||
253 | } | ||
254 | }) | ||
255 | |||
256 | await makePostBodyRequest({ | ||
257 | url: server.url, | ||
258 | path, | ||
259 | token: server.accessToken, | ||
260 | fields, | ||
261 | statusCodeExpected: 403 | ||
262 | }) | ||
263 | }) | ||
264 | |||
265 | it('Should allow to save replay if enabled by the admin', async function () { | ||
266 | const fields = immutableAssign(baseCorrectParams, { saveReplay: true }) | ||
267 | |||
268 | await updateCustomSubConfig(server.url, server.accessToken, { | ||
269 | live: { | ||
270 | enabled: true, | ||
271 | allowReplay: true | ||
272 | } | ||
273 | }) | ||
274 | |||
275 | await makePostBodyRequest({ | ||
276 | url: server.url, | ||
277 | path, | ||
278 | token: server.accessToken, | ||
279 | fields, | ||
280 | statusCodeExpected: 200 | ||
281 | }) | ||
282 | }) | ||
283 | |||
284 | it('Should not allow live if max instance lives is reached', async function () { | ||
285 | await updateCustomSubConfig(server.url, server.accessToken, { | ||
286 | live: { | ||
287 | enabled: true, | ||
288 | maxInstanceLives: 1 | ||
289 | } | ||
290 | }) | ||
291 | |||
292 | await makePostBodyRequest({ | ||
293 | url: server.url, | ||
294 | path, | ||
295 | token: server.accessToken, | ||
296 | fields: baseCorrectParams, | ||
297 | statusCodeExpected: 403 | ||
298 | }) | ||
299 | }) | ||
300 | |||
301 | it('Should not allow live if max user lives is reached', async function () { | ||
302 | await updateCustomSubConfig(server.url, server.accessToken, { | ||
303 | live: { | ||
304 | enabled: true, | ||
305 | maxInstanceLives: 20, | ||
306 | maxUserLives: 1 | ||
307 | } | ||
308 | }) | ||
309 | |||
310 | await makePostBodyRequest({ | ||
311 | url: server.url, | ||
312 | path, | ||
313 | token: server.accessToken, | ||
314 | fields: baseCorrectParams, | ||
315 | statusCodeExpected: 403 | ||
316 | }) | ||
317 | }) | ||
318 | }) | ||
319 | |||
320 | describe('When getting live information', function () { | ||
321 | |||
322 | it('Should fail without access token', async function () { | ||
323 | await getLive(server.url, '', videoId, 401) | ||
324 | }) | ||
325 | |||
326 | it('Should fail with a bad access token', async function () { | ||
327 | await getLive(server.url, 'toto', videoId, 401) | ||
328 | }) | ||
329 | |||
330 | it('Should fail with access token of another user', async function () { | ||
331 | await getLive(server.url, userAccessToken, videoId, 403) | ||
332 | }) | ||
333 | |||
334 | it('Should fail with a bad video id', async function () { | ||
335 | await getLive(server.url, server.accessToken, 'toto', 400) | ||
336 | }) | ||
337 | |||
338 | it('Should fail with an unknown video id', async function () { | ||
339 | await getLive(server.url, server.accessToken, 454555, 404) | ||
340 | }) | ||
341 | |||
342 | it('Should fail with a non live video', async function () { | ||
343 | await getLive(server.url, server.accessToken, videoIdNotLive, 404) | ||
344 | }) | ||
345 | |||
346 | it('Should succeed with the correct params', async function () { | ||
347 | await getLive(server.url, server.accessToken, videoId) | ||
348 | }) | ||
349 | }) | ||
350 | |||
351 | describe('When updating live information', async function () { | ||
352 | |||
353 | it('Should fail without access token', async function () { | ||
354 | await updateLive(server.url, '', videoId, {}, 401) | ||
355 | }) | ||
356 | |||
357 | it('Should fail with a bad access token', async function () { | ||
358 | await updateLive(server.url, 'toto', videoId, {}, 401) | ||
359 | }) | ||
360 | |||
361 | it('Should fail with access token of another user', async function () { | ||
362 | await updateLive(server.url, userAccessToken, videoId, {}, 403) | ||
363 | }) | ||
364 | |||
365 | it('Should fail with a bad video id', async function () { | ||
366 | await updateLive(server.url, server.accessToken, 'toto', {}, 400) | ||
367 | }) | ||
368 | |||
369 | it('Should fail with an unknown video id', async function () { | ||
370 | await updateLive(server.url, server.accessToken, 454555, {}, 404) | ||
371 | }) | ||
372 | |||
373 | it('Should fail with a non live video', async function () { | ||
374 | await updateLive(server.url, server.accessToken, videoIdNotLive, {}, 404) | ||
375 | }) | ||
376 | |||
377 | it('Should succeed with the correct params', async function () { | ||
378 | await updateLive(server.url, server.accessToken, videoId, { saveReplay: false }) | ||
379 | }) | ||
380 | |||
381 | it('Should fail to update replay status if replay is not allowed on the instance', async function () { | ||
382 | await updateCustomSubConfig(server.url, server.accessToken, { | ||
383 | live: { | ||
384 | enabled: true, | ||
385 | allowReplay: false | ||
386 | } | ||
387 | }) | ||
388 | |||
389 | await updateLive(server.url, server.accessToken, videoId, { saveReplay: true }, 403) | ||
390 | }) | ||
391 | |||
392 | it('Should fail to update a live if it has already started', async function () { | ||
393 | this.timeout(20000) | ||
394 | |||
395 | const resLive = await getLive(server.url, server.accessToken, videoId) | ||
396 | const live: LiveVideo = resLive.body | ||
397 | |||
398 | const command = sendRTMPStream(live.rtmpUrl, live.streamKey) | ||
399 | |||
400 | await waitUntilLiveStarts(server.url, server.accessToken, videoId) | ||
401 | await updateLive(server.url, server.accessToken, videoId, {}, 400) | ||
402 | |||
403 | await stopFfmpeg(command) | ||
404 | }) | ||
405 | }) | ||
406 | |||
407 | after(async function () { | ||
408 | await cleanupTests([ server ]) | ||
409 | }) | ||
410 | }) | ||
diff --git a/shared/extra-utils/index.ts b/shared/extra-utils/index.ts index af4d23856..d118b12d2 100644 --- a/shared/extra-utils/index.ts +++ b/shared/extra-utils/index.ts | |||
@@ -13,11 +13,12 @@ export * from './requests/requests' | |||
13 | export * from './requests/check-api-params' | 13 | export * from './requests/check-api-params' |
14 | export * from './server/servers' | 14 | export * from './server/servers' |
15 | export * from './server/plugins' | 15 | export * from './server/plugins' |
16 | export * from './videos/services' | ||
17 | export * from './videos/video-playlists' | 16 | export * from './videos/video-playlists' |
18 | export * from './users/users' | 17 | export * from './users/users' |
19 | export * from './users/accounts' | 18 | export * from './users/accounts' |
20 | export * from './moderation/abuses' | 19 | export * from './moderation/abuses' |
20 | export * from './videos/services' | ||
21 | export * from './videos/live' | ||
21 | export * from './videos/video-abuses' | 22 | export * from './videos/video-abuses' |
22 | export * from './videos/video-blacklist' | 23 | export * from './videos/video-blacklist' |
23 | export * from './videos/video-captions' | 24 | export * from './videos/video-captions' |
diff --git a/shared/extra-utils/requests/requests.ts b/shared/extra-utils/requests/requests.ts index 0e9d67f0b..6b00871e0 100644 --- a/shared/extra-utils/requests/requests.ts +++ b/shared/extra-utils/requests/requests.ts | |||
@@ -63,7 +63,7 @@ function makeUploadRequest (options: { | |||
63 | path: string | 63 | path: string |
64 | token?: string | 64 | token?: string |
65 | fields: { [ fieldName: string ]: any } | 65 | fields: { [ fieldName: string ]: any } |
66 | attaches: { [ attachName: string ]: any | any[] } | 66 | attaches?: { [ attachName: string ]: any | any[] } |
67 | statusCodeExpected?: number | 67 | statusCodeExpected?: number |
68 | }) { | 68 | }) { |
69 | if (!options.statusCodeExpected) options.statusCodeExpected = 400 | 69 | if (!options.statusCodeExpected) options.statusCodeExpected = 400 |
@@ -93,7 +93,7 @@ function makeUploadRequest (options: { | |||
93 | } | 93 | } |
94 | }) | 94 | }) |
95 | 95 | ||
96 | Object.keys(options.attaches).forEach(attach => { | 96 | Object.keys(options.attaches || {}).forEach(attach => { |
97 | const value = options.attaches[attach] | 97 | const value = options.attaches[attach] |
98 | if (Array.isArray(value)) { | 98 | if (Array.isArray(value)) { |
99 | req.attach(attach, buildAbsoluteFixturePath(value[0]), value[1]) | 99 | req.attach(attach, buildAbsoluteFixturePath(value[0]), value[1]) |
diff --git a/shared/extra-utils/videos/live.ts b/shared/extra-utils/videos/live.ts new file mode 100644 index 000000000..f500fdc3e --- /dev/null +++ b/shared/extra-utils/videos/live.ts | |||
@@ -0,0 +1,102 @@ | |||
1 | import * as ffmpeg from 'fluent-ffmpeg' | ||
2 | import { LiveVideoCreate, LiveVideoUpdate, VideoDetails, VideoState } from '@shared/models' | ||
3 | import { buildAbsoluteFixturePath, wait } from '../miscs/miscs' | ||
4 | import { makeGetRequest, makePutBodyRequest, makeUploadRequest } from '../requests/requests' | ||
5 | import { ServerInfo } from '../server/servers' | ||
6 | import { getVideo, getVideoWithToken } from './videos' | ||
7 | |||
8 | function getLive (url: string, token: string, videoId: number | string, statusCodeExpected = 200) { | ||
9 | const path = '/api/v1/videos/live' | ||
10 | |||
11 | return makeGetRequest({ | ||
12 | url, | ||
13 | token, | ||
14 | path: path + '/' + videoId, | ||
15 | statusCodeExpected | ||
16 | }) | ||
17 | } | ||
18 | |||
19 | function updateLive (url: string, token: string, videoId: number | string, fields: LiveVideoUpdate, statusCodeExpected = 204) { | ||
20 | const path = '/api/v1/videos/live' | ||
21 | |||
22 | return makePutBodyRequest({ | ||
23 | url, | ||
24 | token, | ||
25 | path: path + '/' + videoId, | ||
26 | fields, | ||
27 | statusCodeExpected | ||
28 | }) | ||
29 | } | ||
30 | |||
31 | function createLive (url: string, token: string, fields: LiveVideoCreate, statusCodeExpected = 200) { | ||
32 | const path = '/api/v1/videos/live' | ||
33 | |||
34 | let attaches: any = {} | ||
35 | if (fields.thumbnailfile) attaches = { thumbnailfile: fields.thumbnailfile } | ||
36 | if (fields.previewfile) attaches = { previewfile: fields.previewfile } | ||
37 | |||
38 | return makeUploadRequest({ | ||
39 | url, | ||
40 | path, | ||
41 | token, | ||
42 | attaches, | ||
43 | fields, | ||
44 | statusCodeExpected | ||
45 | }) | ||
46 | } | ||
47 | |||
48 | function sendRTMPStream (rtmpBaseUrl: string, streamKey: string) { | ||
49 | const fixture = buildAbsoluteFixturePath('video_short.mp4') | ||
50 | |||
51 | const command = ffmpeg(fixture) | ||
52 | command.inputOption('-stream_loop -1') | ||
53 | command.inputOption('-re') | ||
54 | |||
55 | command.outputOption('-c copy') | ||
56 | command.outputOption('-f flv') | ||
57 | |||
58 | const rtmpUrl = rtmpBaseUrl + '/' + streamKey | ||
59 | command.output(rtmpUrl) | ||
60 | |||
61 | command.on('error', err => { | ||
62 | if (err?.message?.includes('Exiting normally')) return | ||
63 | |||
64 | console.error('Cannot send RTMP stream.', { err }) | ||
65 | }) | ||
66 | |||
67 | if (process.env.DEBUG) { | ||
68 | command.on('stderr', data => console.log(data)) | ||
69 | } | ||
70 | |||
71 | command.run() | ||
72 | |||
73 | return command | ||
74 | } | ||
75 | |||
76 | async function stopFfmpeg (command: ffmpeg.FfmpegCommand) { | ||
77 | command.kill('SIGINT') | ||
78 | |||
79 | await wait(500) | ||
80 | } | ||
81 | |||
82 | async function waitUntilLiveStarts (url: string, token: string, videoId: number | string) { | ||
83 | let video: VideoDetails | ||
84 | |||
85 | do { | ||
86 | const res = await getVideoWithToken(url, token, videoId) | ||
87 | video = res.body | ||
88 | |||
89 | await wait(500) | ||
90 | } while (video.state.id === VideoState.WAITING_FOR_LIVE) | ||
91 | } | ||
92 | |||
93 | // --------------------------------------------------------------------------- | ||
94 | |||
95 | export { | ||
96 | getLive, | ||
97 | updateLive, | ||
98 | waitUntilLiveStarts, | ||
99 | createLive, | ||
100 | stopFfmpeg, | ||
101 | sendRTMPStream | ||
102 | } | ||
diff --git a/shared/models/videos/video-create.model.ts b/shared/models/videos/video-create.model.ts index 59b118567..175327afa 100644 --- a/shared/models/videos/video-create.model.ts +++ b/shared/models/videos/video-create.model.ts | |||
@@ -17,4 +17,7 @@ export interface VideoCreate { | |||
17 | privacy: VideoPrivacy | 17 | privacy: VideoPrivacy |
18 | scheduleUpdate?: VideoScheduleUpdate | 18 | scheduleUpdate?: VideoScheduleUpdate |
19 | originallyPublishedAt?: Date | string | 19 | originallyPublishedAt?: Date | string |
20 | |||
21 | thumbnailfile?: Blob | ||
22 | previewfile?: Blob | ||
20 | } | 23 | } |