diff options
author | Chocobozzz <me@florianbigard.com> | 2021-07-08 10:18:40 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2021-07-20 15:27:17 +0200 |
commit | 4f2199144e428c16460750305f737b890c1ac322 (patch) | |
tree | a19c5c0f254ab0b32d6c8838af33a1ba260e4877 /shared/extra-utils/videos | |
parent | 2c27e70471120c92e0bc8c8114141fbb31ff98ac (diff) | |
download | PeerTube-4f2199144e428c16460750305f737b890c1ac322.tar.gz PeerTube-4f2199144e428c16460750305f737b890c1ac322.tar.zst PeerTube-4f2199144e428c16460750305f737b890c1ac322.zip |
Introduce live command
Diffstat (limited to 'shared/extra-utils/videos')
-rw-r--r-- | shared/extra-utils/videos/index.ts | 13 | ||||
-rw-r--r-- | shared/extra-utils/videos/live-command.ts | 156 | ||||
-rw-r--r-- | shared/extra-utils/videos/live.ts | 139 |
3 files changed, 175 insertions, 133 deletions
diff --git a/shared/extra-utils/videos/index.ts b/shared/extra-utils/videos/index.ts new file mode 100644 index 000000000..c9c884285 --- /dev/null +++ b/shared/extra-utils/videos/index.ts | |||
@@ -0,0 +1,13 @@ | |||
1 | export * from './live-command' | ||
2 | export * from './live' | ||
3 | export * from './services' | ||
4 | export * from './video-blacklist' | ||
5 | export * from './video-captions' | ||
6 | export * from './video-change-ownership' | ||
7 | export * from './video-channels' | ||
8 | export * from './video-comments' | ||
9 | export * from './video-history' | ||
10 | export * from './video-imports' | ||
11 | export * from './video-playlists' | ||
12 | export * from './video-streaming-playlists' | ||
13 | export * from './videos' | ||
diff --git a/shared/extra-utils/videos/live-command.ts b/shared/extra-utils/videos/live-command.ts new file mode 100644 index 000000000..55811b8ba --- /dev/null +++ b/shared/extra-utils/videos/live-command.ts | |||
@@ -0,0 +1,156 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { readdir } from 'fs-extra' | ||
4 | import { omit } from 'lodash' | ||
5 | import { join } from 'path' | ||
6 | import { LiveVideo, LiveVideoCreate, LiveVideoUpdate, VideoCreateResult, VideoDetails, VideoState } from '@shared/models' | ||
7 | import { HttpStatusCode } from '../../core-utils/miscs/http-error-codes' | ||
8 | import { buildServerDirectory, wait } from '../miscs/miscs' | ||
9 | import { unwrapBody } from '../requests' | ||
10 | import { waitUntilLog } from '../server/servers' | ||
11 | import { AbstractCommand, OverrideCommandOptions } from '../shared' | ||
12 | import { sendRTMPStream, testFfmpegStreamError } from './live' | ||
13 | import { getVideoWithToken } from './videos' | ||
14 | |||
15 | export class LiveCommand extends AbstractCommand { | ||
16 | |||
17 | getLive (options: OverrideCommandOptions & { | ||
18 | videoId: number | string | ||
19 | }) { | ||
20 | const path = '/api/v1/videos/live' | ||
21 | |||
22 | return this.getRequestBody<LiveVideo>({ | ||
23 | ...options, | ||
24 | |||
25 | path: path + '/' + options.videoId, | ||
26 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
27 | }) | ||
28 | } | ||
29 | |||
30 | updateLive (options: OverrideCommandOptions & { | ||
31 | videoId: number | string | ||
32 | fields: LiveVideoUpdate | ||
33 | }) { | ||
34 | const { videoId, fields } = options | ||
35 | const path = '/api/v1/videos/live' | ||
36 | |||
37 | return this.putBodyRequest({ | ||
38 | ...options, | ||
39 | |||
40 | path: path + '/' + videoId, | ||
41 | fields, | ||
42 | defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
43 | }) | ||
44 | } | ||
45 | |||
46 | async createLive (options: OverrideCommandOptions & { | ||
47 | fields: LiveVideoCreate | ||
48 | }) { | ||
49 | const { fields } = options | ||
50 | const path = '/api/v1/videos/live' | ||
51 | |||
52 | const attaches: any = {} | ||
53 | if (fields.thumbnailfile) attaches.thumbnailfile = fields.thumbnailfile | ||
54 | if (fields.previewfile) attaches.previewfile = fields.previewfile | ||
55 | |||
56 | const body = await unwrapBody<{ video: VideoCreateResult }>(this.postUploadRequest({ | ||
57 | ...options, | ||
58 | |||
59 | path, | ||
60 | attaches, | ||
61 | fields: omit(fields, 'thumbnailfile', 'previewfile'), | ||
62 | defaultExpectedStatus: HttpStatusCode.OK_200 | ||
63 | })) | ||
64 | |||
65 | return body.video | ||
66 | } | ||
67 | |||
68 | async sendRTMPStreamInVideo (options: OverrideCommandOptions & { | ||
69 | videoId: number | string | ||
70 | fixtureName?: string | ||
71 | }) { | ||
72 | const { videoId, fixtureName } = options | ||
73 | const videoLive = await this.getLive({ videoId }) | ||
74 | |||
75 | return sendRTMPStream(videoLive.rtmpUrl, videoLive.streamKey, fixtureName) | ||
76 | } | ||
77 | |||
78 | async runAndTestFfmpegStreamError (options: OverrideCommandOptions & { | ||
79 | videoId: number | string | ||
80 | shouldHaveError: boolean | ||
81 | }) { | ||
82 | const command = await this.sendRTMPStreamInVideo(options) | ||
83 | |||
84 | return testFfmpegStreamError(command, options.shouldHaveError) | ||
85 | } | ||
86 | |||
87 | waitUntilLivePublished (options: OverrideCommandOptions & { | ||
88 | videoId: number | string | ||
89 | }) { | ||
90 | const { videoId } = options | ||
91 | return this.waitUntilLiveState({ videoId, state: VideoState.PUBLISHED }) | ||
92 | } | ||
93 | |||
94 | waitUntilLiveWaiting (options: OverrideCommandOptions & { | ||
95 | videoId: number | string | ||
96 | }) { | ||
97 | const { videoId } = options | ||
98 | return this.waitUntilLiveState({ videoId, state: VideoState.WAITING_FOR_LIVE }) | ||
99 | } | ||
100 | |||
101 | waitUntilLiveEnded (options: OverrideCommandOptions & { | ||
102 | videoId: number | string | ||
103 | }) { | ||
104 | const { videoId } = options | ||
105 | return this.waitUntilLiveState({ videoId, state: VideoState.LIVE_ENDED }) | ||
106 | } | ||
107 | |||
108 | waitUntilLiveSegmentGeneration (options: OverrideCommandOptions & { | ||
109 | videoUUID: string | ||
110 | resolution: number | ||
111 | segment: number | ||
112 | }) { | ||
113 | const { resolution, segment, videoUUID } = options | ||
114 | const segmentName = `${resolution}-00000${segment}.ts` | ||
115 | |||
116 | return waitUntilLog(this.server, `${videoUUID}/${segmentName}`, 2, false) | ||
117 | } | ||
118 | |||
119 | async waitUntilLiveSaved (options: OverrideCommandOptions & { | ||
120 | videoId: number | string | ||
121 | }) { | ||
122 | let video: VideoDetails | ||
123 | |||
124 | do { | ||
125 | const res = await getVideoWithToken(this.server.url, options.token ?? this.server.accessToken, options.videoId) | ||
126 | video = res.body | ||
127 | |||
128 | await wait(500) | ||
129 | } while (video.isLive === true && video.state.id !== VideoState.PUBLISHED) | ||
130 | } | ||
131 | |||
132 | async getPlaylistsCount (options: OverrideCommandOptions & { | ||
133 | videoUUID: string | ||
134 | }) { | ||
135 | const basePath = buildServerDirectory(this.server, 'streaming-playlists') | ||
136 | const hlsPath = join(basePath, 'hls', options.videoUUID) | ||
137 | |||
138 | const files = await readdir(hlsPath) | ||
139 | |||
140 | return files.filter(f => f.endsWith('.m3u8')).length | ||
141 | } | ||
142 | |||
143 | private async waitUntilLiveState (options: OverrideCommandOptions & { | ||
144 | videoId: number | string | ||
145 | state: VideoState | ||
146 | }) { | ||
147 | let video: VideoDetails | ||
148 | |||
149 | do { | ||
150 | const res = await getVideoWithToken(this.server.url, options.token ?? this.server.accessToken, options.videoId) | ||
151 | video = res.body | ||
152 | |||
153 | await wait(500) | ||
154 | } while (video.state.id !== options.state) | ||
155 | } | ||
156 | } | ||
diff --git a/shared/extra-utils/videos/live.ts b/shared/extra-utils/videos/live.ts index c0384769b..285a39c7e 100644 --- a/shared/extra-utils/videos/live.ts +++ b/shared/extra-utils/videos/live.ts | |||
@@ -3,69 +3,9 @@ | |||
3 | import { expect } from 'chai' | 3 | import { expect } from 'chai' |
4 | import * as ffmpeg from 'fluent-ffmpeg' | 4 | import * as ffmpeg from 'fluent-ffmpeg' |
5 | import { pathExists, readdir } from 'fs-extra' | 5 | import { pathExists, readdir } from 'fs-extra' |
6 | import { omit } from 'lodash' | ||
7 | import { join } from 'path' | 6 | import { join } from 'path' |
8 | import { LiveVideo, LiveVideoCreate, LiveVideoUpdate, VideoDetails, VideoState } from '@shared/models' | ||
9 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | ||
10 | import { buildAbsoluteFixturePath, buildServerDirectory, wait } from '../miscs/miscs' | 7 | import { buildAbsoluteFixturePath, buildServerDirectory, wait } from '../miscs/miscs' |
11 | import { makeGetRequest, makePutBodyRequest, makeUploadRequest } from '../requests/requests' | 8 | import { ServerInfo } from '../server/servers' |
12 | import { ServerInfo, waitUntilLog } from '../server/servers' | ||
13 | import { getVideoWithToken } from './videos' | ||
14 | |||
15 | function getLive (url: string, token: string, videoId: number | string, statusCodeExpected = HttpStatusCode.OK_200) { | ||
16 | const path = '/api/v1/videos/live' | ||
17 | |||
18 | return makeGetRequest({ | ||
19 | url, | ||
20 | token, | ||
21 | path: path + '/' + videoId, | ||
22 | statusCodeExpected | ||
23 | }) | ||
24 | } | ||
25 | |||
26 | function updateLive ( | ||
27 | url: string, | ||
28 | token: string, | ||
29 | videoId: number | string, | ||
30 | fields: LiveVideoUpdate, | ||
31 | statusCodeExpected = HttpStatusCode.NO_CONTENT_204 | ||
32 | ) { | ||
33 | const path = '/api/v1/videos/live' | ||
34 | |||
35 | return makePutBodyRequest({ | ||
36 | url, | ||
37 | token, | ||
38 | path: path + '/' + videoId, | ||
39 | fields, | ||
40 | statusCodeExpected | ||
41 | }) | ||
42 | } | ||
43 | |||
44 | function createLive (url: string, token: string, fields: LiveVideoCreate, statusCodeExpected = HttpStatusCode.OK_200) { | ||
45 | const path = '/api/v1/videos/live' | ||
46 | |||
47 | const attaches: any = {} | ||
48 | if (fields.thumbnailfile) attaches.thumbnailfile = fields.thumbnailfile | ||
49 | if (fields.previewfile) attaches.previewfile = fields.previewfile | ||
50 | |||
51 | const updatedFields = omit(fields, 'thumbnailfile', 'previewfile') | ||
52 | |||
53 | return makeUploadRequest({ | ||
54 | url, | ||
55 | path, | ||
56 | token, | ||
57 | attaches, | ||
58 | fields: updatedFields, | ||
59 | statusCodeExpected | ||
60 | }) | ||
61 | } | ||
62 | |||
63 | async function sendRTMPStreamInVideo (url: string, token: string, videoId: number | string, fixtureName?: string) { | ||
64 | const res = await getLive(url, token, videoId) | ||
65 | const videoLive = res.body as LiveVideo | ||
66 | |||
67 | return sendRTMPStream(videoLive.rtmpUrl, videoLive.streamKey, fixtureName) | ||
68 | } | ||
69 | 9 | ||
70 | function sendRTMPStream (rtmpBaseUrl: string, streamKey: string, fixtureName = 'video_short.mp4') { | 10 | function sendRTMPStream (rtmpBaseUrl: string, streamKey: string, fixtureName = 'video_short.mp4') { |
71 | const fixture = buildAbsoluteFixturePath(fixtureName) | 11 | const fixture = buildAbsoluteFixturePath(fixtureName) |
@@ -109,12 +49,6 @@ function waitFfmpegUntilError (command: ffmpeg.FfmpegCommand, successAfterMS = 1 | |||
109 | }) | 49 | }) |
110 | } | 50 | } |
111 | 51 | ||
112 | async function runAndTestFfmpegStreamError (url: string, token: string, videoId: number | string, shouldHaveError: boolean) { | ||
113 | const command = await sendRTMPStreamInVideo(url, token, videoId) | ||
114 | |||
115 | return testFfmpegStreamError(command, shouldHaveError) | ||
116 | } | ||
117 | |||
118 | async function testFfmpegStreamError (command: ffmpeg.FfmpegCommand, shouldHaveError: boolean) { | 52 | async function testFfmpegStreamError (command: ffmpeg.FfmpegCommand, shouldHaveError: boolean) { |
119 | let error: Error | 53 | let error: Error |
120 | 54 | ||
@@ -136,48 +70,9 @@ async function stopFfmpeg (command: ffmpeg.FfmpegCommand) { | |||
136 | await wait(500) | 70 | await wait(500) |
137 | } | 71 | } |
138 | 72 | ||
139 | function waitUntilLivePublished (url: string, token: string, videoId: number | string) { | ||
140 | return waitUntilLiveState(url, token, videoId, VideoState.PUBLISHED) | ||
141 | } | ||
142 | |||
143 | function waitUntilLiveWaiting (url: string, token: string, videoId: number | string) { | ||
144 | return waitUntilLiveState(url, token, videoId, VideoState.WAITING_FOR_LIVE) | ||
145 | } | ||
146 | |||
147 | function waitUntilLiveEnded (url: string, token: string, videoId: number | string) { | ||
148 | return waitUntilLiveState(url, token, videoId, VideoState.LIVE_ENDED) | ||
149 | } | ||
150 | |||
151 | function waitUntilLiveSegmentGeneration (server: ServerInfo, videoUUID: string, resolutionNum: number, segmentNum: number) { | ||
152 | const segmentName = `${resolutionNum}-00000${segmentNum}.ts` | ||
153 | return waitUntilLog(server, `${videoUUID}/${segmentName}`, 2, false) | ||
154 | } | ||
155 | |||
156 | async function waitUntilLiveState (url: string, token: string, videoId: number | string, state: VideoState) { | ||
157 | let video: VideoDetails | ||
158 | |||
159 | do { | ||
160 | const res = await getVideoWithToken(url, token, videoId) | ||
161 | video = res.body | ||
162 | |||
163 | await wait(500) | ||
164 | } while (video.state.id !== state) | ||
165 | } | ||
166 | |||
167 | async function waitUntilLiveSaved (url: string, token: string, videoId: number | string) { | ||
168 | let video: VideoDetails | ||
169 | |||
170 | do { | ||
171 | const res = await getVideoWithToken(url, token, videoId) | ||
172 | video = res.body | ||
173 | |||
174 | await wait(500) | ||
175 | } while (video.isLive === true && video.state.id !== VideoState.PUBLISHED) | ||
176 | } | ||
177 | |||
178 | async function waitUntilLivePublishedOnAllServers (servers: ServerInfo[], videoId: string) { | 73 | async function waitUntilLivePublishedOnAllServers (servers: ServerInfo[], videoId: string) { |
179 | for (const server of servers) { | 74 | for (const server of servers) { |
180 | await waitUntilLivePublished(server.url, server.accessToken, videoId) | 75 | await server.liveCommand.waitUntilLivePublished({ videoId }) |
181 | } | 76 | } |
182 | } | 77 | } |
183 | 78 | ||
@@ -206,33 +101,11 @@ async function checkLiveCleanup (server: ServerInfo, videoUUID: string, resoluti | |||
206 | expect(files).to.contain('segments-sha256.json') | 101 | expect(files).to.contain('segments-sha256.json') |
207 | } | 102 | } |
208 | 103 | ||
209 | async function getPlaylistsCount (server: ServerInfo, videoUUID: string) { | ||
210 | const basePath = buildServerDirectory(server, 'streaming-playlists') | ||
211 | const hlsPath = join(basePath, 'hls', videoUUID) | ||
212 | |||
213 | const files = await readdir(hlsPath) | ||
214 | |||
215 | return files.filter(f => f.endsWith('.m3u8')).length | ||
216 | } | ||
217 | |||
218 | // --------------------------------------------------------------------------- | ||
219 | |||
220 | export { | 104 | export { |
221 | getLive, | 105 | sendRTMPStream, |
222 | getPlaylistsCount, | ||
223 | waitUntilLiveSaved, | ||
224 | waitUntilLivePublished, | ||
225 | updateLive, | ||
226 | createLive, | ||
227 | runAndTestFfmpegStreamError, | ||
228 | checkLiveCleanup, | ||
229 | waitUntilLiveSegmentGeneration, | ||
230 | stopFfmpeg, | ||
231 | waitUntilLiveWaiting, | ||
232 | sendRTMPStreamInVideo, | ||
233 | waitUntilLiveEnded, | ||
234 | waitFfmpegUntilError, | 106 | waitFfmpegUntilError, |
107 | testFfmpegStreamError, | ||
108 | stopFfmpeg, | ||
235 | waitUntilLivePublishedOnAllServers, | 109 | waitUntilLivePublishedOnAllServers, |
236 | sendRTMPStream, | 110 | checkLiveCleanup |
237 | testFfmpegStreamError | ||
238 | } | 111 | } |