diff options
author | Chocobozzz <me@florianbigard.com> | 2023-07-31 14:34:36 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2023-08-11 15:02:33 +0200 |
commit | 3a4992633ee62d5edfbb484d9c6bcb3cf158489d (patch) | |
tree | e4510b39bdac9c318fdb4b47018d08f15368b8f0 /server/tests/shared/streaming-playlists.ts | |
parent | 04d1da5621d25d59bd5fa1543b725c497bf5d9a8 (diff) | |
download | PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.tar.gz PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.tar.zst PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.zip |
Migrate server to ESM
Sorry for the very big commit that may lead to git log issues and merge
conflicts, but it's a major step forward:
* Server can be faster at startup because imports() are async and we can
easily lazy import big modules
* Angular doesn't seem to support ES import (with .js extension), so we
had to correctly organize peertube into a monorepo:
* Use yarn workspace feature
* Use typescript reference projects for dependencies
* Shared projects have been moved into "packages", each one is now a
node module (with a dedicated package.json/tsconfig.json)
* server/tools have been moved into apps/ and is now a dedicated app
bundled and published on NPM so users don't have to build peertube
cli tools manually
* server/tests have been moved into packages/ so we don't compile
them every time we want to run the server
* Use isolatedModule option:
* Had to move from const enum to const
(https://www.typescriptlang.org/docs/handbook/enums.html#objects-vs-enums)
* Had to explictely specify "type" imports when used in decorators
* Prefer tsx (that uses esbuild under the hood) instead of ts-node to
load typescript files (tests with mocha or scripts):
* To reduce test complexity as esbuild doesn't support decorator
metadata, we only test server files that do not import server
models
* We still build tests files into js files for a faster CI
* Remove unmaintained peertube CLI import script
* Removed some barrels to speed up execution (less imports)
Diffstat (limited to 'server/tests/shared/streaming-playlists.ts')
-rw-r--r-- | server/tests/shared/streaming-playlists.ts | 296 |
1 files changed, 0 insertions, 296 deletions
diff --git a/server/tests/shared/streaming-playlists.ts b/server/tests/shared/streaming-playlists.ts deleted file mode 100644 index e4f88bc25..000000000 --- a/server/tests/shared/streaming-playlists.ts +++ /dev/null | |||
@@ -1,296 +0,0 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { basename, dirname, join } from 'path' | ||
5 | import { removeFragmentedMP4Ext, uuidRegex } from '@shared/core-utils' | ||
6 | import { sha256 } from '@shared/extra-utils' | ||
7 | import { HttpStatusCode, VideoPrivacy, VideoResolution, VideoStreamingPlaylist, VideoStreamingPlaylistType } from '@shared/models' | ||
8 | import { makeRawRequest, PeerTubeServer } from '@shared/server-commands' | ||
9 | import { expectStartWith } from './checks' | ||
10 | import { hlsInfohashExist } from './tracker' | ||
11 | import { checkWebTorrentWorks } from './webtorrent' | ||
12 | |||
13 | async function checkSegmentHash (options: { | ||
14 | server: PeerTubeServer | ||
15 | baseUrlPlaylist: string | ||
16 | baseUrlSegment: string | ||
17 | resolution: number | ||
18 | hlsPlaylist: VideoStreamingPlaylist | ||
19 | token?: string | ||
20 | }) { | ||
21 | const { server, baseUrlPlaylist, baseUrlSegment, resolution, hlsPlaylist, token } = options | ||
22 | const command = server.streamingPlaylists | ||
23 | |||
24 | const file = hlsPlaylist.files.find(f => f.resolution.id === resolution) | ||
25 | const videoName = basename(file.fileUrl) | ||
26 | |||
27 | const playlist = await command.get({ url: `${baseUrlPlaylist}/${removeFragmentedMP4Ext(videoName)}.m3u8`, token }) | ||
28 | |||
29 | const matches = /#EXT-X-BYTERANGE:(\d+)@(\d+)/.exec(playlist) | ||
30 | |||
31 | const length = parseInt(matches[1], 10) | ||
32 | const offset = parseInt(matches[2], 10) | ||
33 | const range = `${offset}-${offset + length - 1}` | ||
34 | |||
35 | const segmentBody = await command.getFragmentedSegment({ | ||
36 | url: `${baseUrlSegment}/${videoName}`, | ||
37 | expectedStatus: HttpStatusCode.PARTIAL_CONTENT_206, | ||
38 | range: `bytes=${range}`, | ||
39 | token | ||
40 | }) | ||
41 | |||
42 | const shaBody = await command.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url, token }) | ||
43 | expect(sha256(segmentBody)).to.equal(shaBody[videoName][range], `Invalid sha256 result for ${videoName} range ${range}`) | ||
44 | } | ||
45 | |||
46 | // --------------------------------------------------------------------------- | ||
47 | |||
48 | async function checkLiveSegmentHash (options: { | ||
49 | server: PeerTubeServer | ||
50 | baseUrlSegment: string | ||
51 | videoUUID: string | ||
52 | segmentName: string | ||
53 | hlsPlaylist: VideoStreamingPlaylist | ||
54 | withRetry?: boolean | ||
55 | }) { | ||
56 | const { server, baseUrlSegment, videoUUID, segmentName, hlsPlaylist, withRetry = false } = options | ||
57 | const command = server.streamingPlaylists | ||
58 | |||
59 | const segmentBody = await command.getFragmentedSegment({ url: `${baseUrlSegment}/${videoUUID}/${segmentName}`, withRetry }) | ||
60 | const shaBody = await command.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url, withRetry }) | ||
61 | |||
62 | expect(sha256(segmentBody)).to.equal(shaBody[segmentName]) | ||
63 | } | ||
64 | |||
65 | // --------------------------------------------------------------------------- | ||
66 | |||
67 | async function checkResolutionsInMasterPlaylist (options: { | ||
68 | server: PeerTubeServer | ||
69 | playlistUrl: string | ||
70 | resolutions: number[] | ||
71 | token?: string | ||
72 | transcoded?: boolean // default true | ||
73 | withRetry?: boolean // default false | ||
74 | }) { | ||
75 | const { server, playlistUrl, resolutions, token, withRetry = false, transcoded = true } = options | ||
76 | |||
77 | const masterPlaylist = await server.streamingPlaylists.get({ url: playlistUrl, token, withRetry }) | ||
78 | |||
79 | for (const resolution of resolutions) { | ||
80 | const base = '#EXT-X-STREAM-INF:BANDWIDTH=\\d+,RESOLUTION=\\d+x' + resolution | ||
81 | |||
82 | if (resolution === VideoResolution.H_NOVIDEO) { | ||
83 | expect(masterPlaylist).to.match(new RegExp(`${base},CODECS="mp4a.40.2"`)) | ||
84 | } else if (transcoded) { | ||
85 | expect(masterPlaylist).to.match(new RegExp(`${base},(FRAME-RATE=\\d+,)?CODECS="avc1.64001f,mp4a.40.2"`)) | ||
86 | } else { | ||
87 | expect(masterPlaylist).to.match(new RegExp(`${base}`)) | ||
88 | } | ||
89 | } | ||
90 | |||
91 | const playlistsLength = masterPlaylist.split('\n').filter(line => line.startsWith('#EXT-X-STREAM-INF:BANDWIDTH=')) | ||
92 | expect(playlistsLength).to.have.lengthOf(resolutions.length) | ||
93 | } | ||
94 | |||
95 | async function completeCheckHlsPlaylist (options: { | ||
96 | servers: PeerTubeServer[] | ||
97 | videoUUID: string | ||
98 | hlsOnly: boolean | ||
99 | |||
100 | resolutions?: number[] | ||
101 | objectStorageBaseUrl?: string | ||
102 | }) { | ||
103 | const { videoUUID, hlsOnly, objectStorageBaseUrl } = options | ||
104 | |||
105 | const resolutions = options.resolutions ?? [ 240, 360, 480, 720 ] | ||
106 | |||
107 | for (const server of options.servers) { | ||
108 | const videoDetails = await server.videos.getWithToken({ id: videoUUID }) | ||
109 | const requiresAuth = videoDetails.privacy.id === VideoPrivacy.PRIVATE || videoDetails.privacy.id === VideoPrivacy.INTERNAL | ||
110 | |||
111 | const privatePath = requiresAuth | ||
112 | ? 'private/' | ||
113 | : '' | ||
114 | const token = requiresAuth | ||
115 | ? server.accessToken | ||
116 | : undefined | ||
117 | |||
118 | const baseUrl = `http://${videoDetails.account.host}` | ||
119 | |||
120 | expect(videoDetails.streamingPlaylists).to.have.lengthOf(1) | ||
121 | |||
122 | const hlsPlaylist = videoDetails.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS) | ||
123 | expect(hlsPlaylist).to.not.be.undefined | ||
124 | |||
125 | const hlsFiles = hlsPlaylist.files | ||
126 | expect(hlsFiles).to.have.lengthOf(resolutions.length) | ||
127 | |||
128 | if (hlsOnly) expect(videoDetails.files).to.have.lengthOf(0) | ||
129 | else expect(videoDetails.files).to.have.lengthOf(resolutions.length) | ||
130 | |||
131 | // Check JSON files | ||
132 | for (const resolution of resolutions) { | ||
133 | const file = hlsFiles.find(f => f.resolution.id === resolution) | ||
134 | expect(file).to.not.be.undefined | ||
135 | |||
136 | if (file.resolution.id === VideoResolution.H_NOVIDEO) { | ||
137 | expect(file.resolution.label).to.equal('Audio') | ||
138 | } else { | ||
139 | expect(file.resolution.label).to.equal(resolution + 'p') | ||
140 | } | ||
141 | |||
142 | expect(file.magnetUri).to.have.lengthOf.above(2) | ||
143 | await checkWebTorrentWorks(file.magnetUri) | ||
144 | |||
145 | { | ||
146 | const nameReg = `${uuidRegex}-${file.resolution.id}` | ||
147 | |||
148 | expect(file.torrentUrl).to.match(new RegExp(`${server.url}/lazy-static/torrents/${nameReg}-hls.torrent`)) | ||
149 | |||
150 | if (objectStorageBaseUrl && requiresAuth) { | ||
151 | // eslint-disable-next-line max-len | ||
152 | expect(file.fileUrl).to.match(new RegExp(`${server.url}/object-storage-proxy/streaming-playlists/hls/${privatePath}${videoDetails.uuid}/${nameReg}-fragmented.mp4`)) | ||
153 | } else if (objectStorageBaseUrl) { | ||
154 | expectStartWith(file.fileUrl, objectStorageBaseUrl) | ||
155 | } else { | ||
156 | expect(file.fileUrl).to.match( | ||
157 | new RegExp(`${baseUrl}/static/streaming-playlists/hls/${privatePath}${videoDetails.uuid}/${nameReg}-fragmented.mp4`) | ||
158 | ) | ||
159 | } | ||
160 | } | ||
161 | |||
162 | { | ||
163 | await Promise.all([ | ||
164 | makeRawRequest({ url: file.torrentUrl, token, expectedStatus: HttpStatusCode.OK_200 }), | ||
165 | makeRawRequest({ url: file.torrentDownloadUrl, token, expectedStatus: HttpStatusCode.OK_200 }), | ||
166 | makeRawRequest({ url: file.metadataUrl, token, expectedStatus: HttpStatusCode.OK_200 }), | ||
167 | makeRawRequest({ url: file.fileUrl, token, expectedStatus: HttpStatusCode.OK_200 }), | ||
168 | |||
169 | makeRawRequest({ | ||
170 | url: file.fileDownloadUrl, | ||
171 | token, | ||
172 | expectedStatus: objectStorageBaseUrl | ||
173 | ? HttpStatusCode.FOUND_302 | ||
174 | : HttpStatusCode.OK_200 | ||
175 | }) | ||
176 | ]) | ||
177 | } | ||
178 | } | ||
179 | |||
180 | // Check master playlist | ||
181 | { | ||
182 | await checkResolutionsInMasterPlaylist({ server, token, playlistUrl: hlsPlaylist.playlistUrl, resolutions }) | ||
183 | |||
184 | const masterPlaylist = await server.streamingPlaylists.get({ url: hlsPlaylist.playlistUrl, token }) | ||
185 | |||
186 | let i = 0 | ||
187 | for (const resolution of resolutions) { | ||
188 | expect(masterPlaylist).to.contain(`${resolution}.m3u8`) | ||
189 | expect(masterPlaylist).to.contain(`${resolution}.m3u8`) | ||
190 | |||
191 | const url = 'http://' + videoDetails.account.host | ||
192 | await hlsInfohashExist(url, hlsPlaylist.playlistUrl, i) | ||
193 | |||
194 | i++ | ||
195 | } | ||
196 | } | ||
197 | |||
198 | // Check resolution playlists | ||
199 | { | ||
200 | for (const resolution of resolutions) { | ||
201 | const file = hlsFiles.find(f => f.resolution.id === resolution) | ||
202 | const playlistName = removeFragmentedMP4Ext(basename(file.fileUrl)) + '.m3u8' | ||
203 | |||
204 | let url: string | ||
205 | if (objectStorageBaseUrl && requiresAuth) { | ||
206 | url = `${baseUrl}/object-storage-proxy/streaming-playlists/hls/${privatePath}${videoUUID}/${playlistName}` | ||
207 | } else if (objectStorageBaseUrl) { | ||
208 | url = `${objectStorageBaseUrl}hls/${videoUUID}/${playlistName}` | ||
209 | } else { | ||
210 | url = `${baseUrl}/static/streaming-playlists/hls/${privatePath}${videoUUID}/${playlistName}` | ||
211 | } | ||
212 | |||
213 | const subPlaylist = await server.streamingPlaylists.get({ url, token }) | ||
214 | |||
215 | expect(subPlaylist).to.match(new RegExp(`${uuidRegex}-${resolution}-fragmented.mp4`)) | ||
216 | expect(subPlaylist).to.contain(basename(file.fileUrl)) | ||
217 | } | ||
218 | } | ||
219 | |||
220 | { | ||
221 | let baseUrlAndPath: string | ||
222 | if (objectStorageBaseUrl && requiresAuth) { | ||
223 | baseUrlAndPath = `${baseUrl}/object-storage-proxy/streaming-playlists/hls/${privatePath}${videoUUID}` | ||
224 | } else if (objectStorageBaseUrl) { | ||
225 | baseUrlAndPath = `${objectStorageBaseUrl}hls/${videoUUID}` | ||
226 | } else { | ||
227 | baseUrlAndPath = `${baseUrl}/static/streaming-playlists/hls/${privatePath}${videoUUID}` | ||
228 | } | ||
229 | |||
230 | for (const resolution of resolutions) { | ||
231 | await checkSegmentHash({ | ||
232 | server, | ||
233 | token, | ||
234 | baseUrlPlaylist: baseUrlAndPath, | ||
235 | baseUrlSegment: baseUrlAndPath, | ||
236 | resolution, | ||
237 | hlsPlaylist | ||
238 | }) | ||
239 | } | ||
240 | } | ||
241 | } | ||
242 | } | ||
243 | |||
244 | async function checkVideoFileTokenReinjection (options: { | ||
245 | server: PeerTubeServer | ||
246 | videoUUID: string | ||
247 | videoFileToken: string | ||
248 | resolutions: number[] | ||
249 | isLive: boolean | ||
250 | }) { | ||
251 | const { server, resolutions, videoFileToken, videoUUID, isLive } = options | ||
252 | |||
253 | const video = await server.videos.getWithToken({ id: videoUUID }) | ||
254 | const hls = video.streamingPlaylists[0] | ||
255 | |||
256 | const query = { videoFileToken, reinjectVideoFileToken: 'true' } | ||
257 | const { text } = await makeRawRequest({ url: hls.playlistUrl, query, expectedStatus: HttpStatusCode.OK_200 }) | ||
258 | |||
259 | for (let i = 0; i < resolutions.length; i++) { | ||
260 | const resolution = resolutions[i] | ||
261 | |||
262 | const suffix = isLive | ||
263 | ? i | ||
264 | : `-${resolution}` | ||
265 | |||
266 | expect(text).to.contain(`${suffix}.m3u8?videoFileToken=${videoFileToken}&reinjectVideoFileToken=true`) | ||
267 | } | ||
268 | |||
269 | const resolutionPlaylists = extractResolutionPlaylistUrls(hls.playlistUrl, text) | ||
270 | expect(resolutionPlaylists).to.have.lengthOf(resolutions.length) | ||
271 | |||
272 | for (const url of resolutionPlaylists) { | ||
273 | const { text } = await makeRawRequest({ url, query, expectedStatus: HttpStatusCode.OK_200 }) | ||
274 | |||
275 | const extension = isLive | ||
276 | ? '.ts' | ||
277 | : '.mp4' | ||
278 | |||
279 | expect(text).to.contain(`${extension}?videoFileToken=${videoFileToken}`) | ||
280 | expect(text).not.to.contain(`reinjectVideoFileToken=true`) | ||
281 | } | ||
282 | } | ||
283 | |||
284 | function extractResolutionPlaylistUrls (masterPath: string, masterContent: string) { | ||
285 | return masterContent.match(/^([^.]+\.m3u8.*)/mg) | ||
286 | .map(filename => join(dirname(masterPath), filename)) | ||
287 | } | ||
288 | |||
289 | export { | ||
290 | checkSegmentHash, | ||
291 | checkLiveSegmentHash, | ||
292 | checkResolutionsInMasterPlaylist, | ||
293 | completeCheckHlsPlaylist, | ||
294 | extractResolutionPlaylistUrls, | ||
295 | checkVideoFileTokenReinjection | ||
296 | } | ||