aboutsummaryrefslogtreecommitdiffhomepage
path: root/packages/tests/src/shared/streaming-playlists.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/tests/src/shared/streaming-playlists.ts')
-rw-r--r--packages/tests/src/shared/streaming-playlists.ts302
1 files changed, 302 insertions, 0 deletions
diff --git a/packages/tests/src/shared/streaming-playlists.ts b/packages/tests/src/shared/streaming-playlists.ts
new file mode 100644
index 000000000..f2f0fbe85
--- /dev/null
+++ b/packages/tests/src/shared/streaming-playlists.ts
@@ -0,0 +1,302 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { basename, dirname, join } from 'path'
5import { removeFragmentedMP4Ext, uuidRegex } from '@peertube/peertube-core-utils'
6import {
7 HttpStatusCode,
8 VideoPrivacy,
9 VideoResolution,
10 VideoStreamingPlaylist,
11 VideoStreamingPlaylistType
12} from '@peertube/peertube-models'
13import { sha256 } from '@peertube/peertube-node-utils'
14import { makeRawRequest, PeerTubeServer } from '@peertube/peertube-server-commands'
15import { expectStartWith } from './checks.js'
16import { hlsInfohashExist } from './tracker.js'
17import { checkWebTorrentWorks } from './webtorrent.js'
18
19async function checkSegmentHash (options: {
20 server: PeerTubeServer
21 baseUrlPlaylist: string
22 baseUrlSegment: string
23 resolution: number
24 hlsPlaylist: VideoStreamingPlaylist
25 token?: string
26}) {
27 const { server, baseUrlPlaylist, baseUrlSegment, resolution, hlsPlaylist, token } = options
28 const command = server.streamingPlaylists
29
30 const file = hlsPlaylist.files.find(f => f.resolution.id === resolution)
31 const videoName = basename(file.fileUrl)
32
33 const playlist = await command.get({ url: `${baseUrlPlaylist}/${removeFragmentedMP4Ext(videoName)}.m3u8`, token })
34
35 const matches = /#EXT-X-BYTERANGE:(\d+)@(\d+)/.exec(playlist)
36
37 const length = parseInt(matches[1], 10)
38 const offset = parseInt(matches[2], 10)
39 const range = `${offset}-${offset + length - 1}`
40
41 const segmentBody = await command.getFragmentedSegment({
42 url: `${baseUrlSegment}/${videoName}`,
43 expectedStatus: HttpStatusCode.PARTIAL_CONTENT_206,
44 range: `bytes=${range}`,
45 token
46 })
47
48 const shaBody = await command.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url, token })
49 expect(sha256(segmentBody)).to.equal(shaBody[videoName][range], `Invalid sha256 result for ${videoName} range ${range}`)
50}
51
52// ---------------------------------------------------------------------------
53
54async function checkLiveSegmentHash (options: {
55 server: PeerTubeServer
56 baseUrlSegment: string
57 videoUUID: string
58 segmentName: string
59 hlsPlaylist: VideoStreamingPlaylist
60 withRetry?: boolean
61}) {
62 const { server, baseUrlSegment, videoUUID, segmentName, hlsPlaylist, withRetry = false } = options
63 const command = server.streamingPlaylists
64
65 const segmentBody = await command.getFragmentedSegment({ url: `${baseUrlSegment}/${videoUUID}/${segmentName}`, withRetry })
66 const shaBody = await command.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url, withRetry })
67
68 expect(sha256(segmentBody)).to.equal(shaBody[segmentName])
69}
70
71// ---------------------------------------------------------------------------
72
73async function checkResolutionsInMasterPlaylist (options: {
74 server: PeerTubeServer
75 playlistUrl: string
76 resolutions: number[]
77 token?: string
78 transcoded?: boolean // default true
79 withRetry?: boolean // default false
80}) {
81 const { server, playlistUrl, resolutions, token, withRetry = false, transcoded = true } = options
82
83 const masterPlaylist = await server.streamingPlaylists.get({ url: playlistUrl, token, withRetry })
84
85 for (const resolution of resolutions) {
86 const base = '#EXT-X-STREAM-INF:BANDWIDTH=\\d+,RESOLUTION=\\d+x' + resolution
87
88 if (resolution === VideoResolution.H_NOVIDEO) {
89 expect(masterPlaylist).to.match(new RegExp(`${base},CODECS="mp4a.40.2"`))
90 } else if (transcoded) {
91 expect(masterPlaylist).to.match(new RegExp(`${base},(FRAME-RATE=\\d+,)?CODECS="avc1.64001f,mp4a.40.2"`))
92 } else {
93 expect(masterPlaylist).to.match(new RegExp(`${base}`))
94 }
95 }
96
97 const playlistsLength = masterPlaylist.split('\n').filter(line => line.startsWith('#EXT-X-STREAM-INF:BANDWIDTH='))
98 expect(playlistsLength).to.have.lengthOf(resolutions.length)
99}
100
101async function completeCheckHlsPlaylist (options: {
102 servers: PeerTubeServer[]
103 videoUUID: string
104 hlsOnly: boolean
105
106 resolutions?: number[]
107 objectStorageBaseUrl?: string
108}) {
109 const { videoUUID, hlsOnly, objectStorageBaseUrl } = options
110
111 const resolutions = options.resolutions ?? [ 240, 360, 480, 720 ]
112
113 for (const server of options.servers) {
114 const videoDetails = await server.videos.getWithToken({ id: videoUUID })
115 const requiresAuth = videoDetails.privacy.id === VideoPrivacy.PRIVATE || videoDetails.privacy.id === VideoPrivacy.INTERNAL
116
117 const privatePath = requiresAuth
118 ? 'private/'
119 : ''
120 const token = requiresAuth
121 ? server.accessToken
122 : undefined
123
124 const baseUrl = `http://${videoDetails.account.host}`
125
126 expect(videoDetails.streamingPlaylists).to.have.lengthOf(1)
127
128 const hlsPlaylist = videoDetails.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
129 expect(hlsPlaylist).to.not.be.undefined
130
131 const hlsFiles = hlsPlaylist.files
132 expect(hlsFiles).to.have.lengthOf(resolutions.length)
133
134 if (hlsOnly) expect(videoDetails.files).to.have.lengthOf(0)
135 else expect(videoDetails.files).to.have.lengthOf(resolutions.length)
136
137 // Check JSON files
138 for (const resolution of resolutions) {
139 const file = hlsFiles.find(f => f.resolution.id === resolution)
140 expect(file).to.not.be.undefined
141
142 if (file.resolution.id === VideoResolution.H_NOVIDEO) {
143 expect(file.resolution.label).to.equal('Audio')
144 } else {
145 expect(file.resolution.label).to.equal(resolution + 'p')
146 }
147
148 expect(file.magnetUri).to.have.lengthOf.above(2)
149 await checkWebTorrentWorks(file.magnetUri)
150
151 {
152 const nameReg = `${uuidRegex}-${file.resolution.id}`
153
154 expect(file.torrentUrl).to.match(new RegExp(`${server.url}/lazy-static/torrents/${nameReg}-hls.torrent`))
155
156 if (objectStorageBaseUrl && requiresAuth) {
157 // eslint-disable-next-line max-len
158 expect(file.fileUrl).to.match(new RegExp(`${server.url}/object-storage-proxy/streaming-playlists/hls/${privatePath}${videoDetails.uuid}/${nameReg}-fragmented.mp4`))
159 } else if (objectStorageBaseUrl) {
160 expectStartWith(file.fileUrl, objectStorageBaseUrl)
161 } else {
162 expect(file.fileUrl).to.match(
163 new RegExp(`${baseUrl}/static/streaming-playlists/hls/${privatePath}${videoDetails.uuid}/${nameReg}-fragmented.mp4`)
164 )
165 }
166 }
167
168 {
169 await Promise.all([
170 makeRawRequest({ url: file.torrentUrl, token, expectedStatus: HttpStatusCode.OK_200 }),
171 makeRawRequest({ url: file.torrentDownloadUrl, token, expectedStatus: HttpStatusCode.OK_200 }),
172 makeRawRequest({ url: file.metadataUrl, token, expectedStatus: HttpStatusCode.OK_200 }),
173 makeRawRequest({ url: file.fileUrl, token, expectedStatus: HttpStatusCode.OK_200 }),
174
175 makeRawRequest({
176 url: file.fileDownloadUrl,
177 token,
178 expectedStatus: objectStorageBaseUrl
179 ? HttpStatusCode.FOUND_302
180 : HttpStatusCode.OK_200
181 })
182 ])
183 }
184 }
185
186 // Check master playlist
187 {
188 await checkResolutionsInMasterPlaylist({ server, token, playlistUrl: hlsPlaylist.playlistUrl, resolutions })
189
190 const masterPlaylist = await server.streamingPlaylists.get({ url: hlsPlaylist.playlistUrl, token })
191
192 let i = 0
193 for (const resolution of resolutions) {
194 expect(masterPlaylist).to.contain(`${resolution}.m3u8`)
195 expect(masterPlaylist).to.contain(`${resolution}.m3u8`)
196
197 const url = 'http://' + videoDetails.account.host
198 await hlsInfohashExist(url, hlsPlaylist.playlistUrl, i)
199
200 i++
201 }
202 }
203
204 // Check resolution playlists
205 {
206 for (const resolution of resolutions) {
207 const file = hlsFiles.find(f => f.resolution.id === resolution)
208 const playlistName = removeFragmentedMP4Ext(basename(file.fileUrl)) + '.m3u8'
209
210 let url: string
211 if (objectStorageBaseUrl && requiresAuth) {
212 url = `${baseUrl}/object-storage-proxy/streaming-playlists/hls/${privatePath}${videoUUID}/${playlistName}`
213 } else if (objectStorageBaseUrl) {
214 url = `${objectStorageBaseUrl}hls/${videoUUID}/${playlistName}`
215 } else {
216 url = `${baseUrl}/static/streaming-playlists/hls/${privatePath}${videoUUID}/${playlistName}`
217 }
218
219 const subPlaylist = await server.streamingPlaylists.get({ url, token })
220
221 expect(subPlaylist).to.match(new RegExp(`${uuidRegex}-${resolution}-fragmented.mp4`))
222 expect(subPlaylist).to.contain(basename(file.fileUrl))
223 }
224 }
225
226 {
227 let baseUrlAndPath: string
228 if (objectStorageBaseUrl && requiresAuth) {
229 baseUrlAndPath = `${baseUrl}/object-storage-proxy/streaming-playlists/hls/${privatePath}${videoUUID}`
230 } else if (objectStorageBaseUrl) {
231 baseUrlAndPath = `${objectStorageBaseUrl}hls/${videoUUID}`
232 } else {
233 baseUrlAndPath = `${baseUrl}/static/streaming-playlists/hls/${privatePath}${videoUUID}`
234 }
235
236 for (const resolution of resolutions) {
237 await checkSegmentHash({
238 server,
239 token,
240 baseUrlPlaylist: baseUrlAndPath,
241 baseUrlSegment: baseUrlAndPath,
242 resolution,
243 hlsPlaylist
244 })
245 }
246 }
247 }
248}
249
250async function checkVideoFileTokenReinjection (options: {
251 server: PeerTubeServer
252 videoUUID: string
253 videoFileToken: string
254 resolutions: number[]
255 isLive: boolean
256}) {
257 const { server, resolutions, videoFileToken, videoUUID, isLive } = options
258
259 const video = await server.videos.getWithToken({ id: videoUUID })
260 const hls = video.streamingPlaylists[0]
261
262 const query = { videoFileToken, reinjectVideoFileToken: 'true' }
263 const { text } = await makeRawRequest({ url: hls.playlistUrl, query, expectedStatus: HttpStatusCode.OK_200 })
264
265 for (let i = 0; i < resolutions.length; i++) {
266 const resolution = resolutions[i]
267
268 const suffix = isLive
269 ? i
270 : `-${resolution}`
271
272 expect(text).to.contain(`${suffix}.m3u8?videoFileToken=${videoFileToken}&reinjectVideoFileToken=true`)
273 }
274
275 const resolutionPlaylists = extractResolutionPlaylistUrls(hls.playlistUrl, text)
276 expect(resolutionPlaylists).to.have.lengthOf(resolutions.length)
277
278 for (const url of resolutionPlaylists) {
279 const { text } = await makeRawRequest({ url, query, expectedStatus: HttpStatusCode.OK_200 })
280
281 const extension = isLive
282 ? '.ts'
283 : '.mp4'
284
285 expect(text).to.contain(`${extension}?videoFileToken=${videoFileToken}`)
286 expect(text).not.to.contain(`reinjectVideoFileToken=true`)
287 }
288}
289
290function extractResolutionPlaylistUrls (masterPath: string, masterContent: string) {
291 return masterContent.match(/^([^.]+\.m3u8.*)/mg)
292 .map(filename => join(dirname(masterPath), filename))
293}
294
295export {
296 checkSegmentHash,
297 checkLiveSegmentHash,
298 checkResolutionsInMasterPlaylist,
299 completeCheckHlsPlaylist,
300 extractResolutionPlaylistUrls,
301 checkVideoFileTokenReinjection
302}