]>
Commit | Line | Data |
---|---|---|
3545e72c C |
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ |
2 | ||
57f879a5 | 3 | import { expect } from 'chai' |
71e3e879 | 4 | import { basename, dirname, join } from 'path' |
3545e72c | 5 | import { removeFragmentedMP4Ext, uuidRegex } from '@shared/core-utils' |
f304a158 | 6 | import { sha256 } from '@shared/extra-utils' |
3545e72c C |
7 | import { HttpStatusCode, VideoStreamingPlaylist, VideoStreamingPlaylistType } from '@shared/models' |
8 | import { makeRawRequest, PeerTubeServer, webtorrentAdd } from '@shared/server-commands' | |
9 | import { expectStartWith } from './checks' | |
10 | import { hlsInfohashExist } from './tracker' | |
57f879a5 C |
11 | |
12 | async function checkSegmentHash (options: { | |
254d3579 | 13 | server: PeerTubeServer |
57f879a5 C |
14 | baseUrlPlaylist: string |
15 | baseUrlSegment: string | |
57f879a5 C |
16 | resolution: number |
17 | hlsPlaylist: VideoStreamingPlaylist | |
18 | }) { | |
0305db28 | 19 | const { server, baseUrlPlaylist, baseUrlSegment, resolution, hlsPlaylist } = options |
89d241a7 | 20 | const command = server.streamingPlaylists |
57f879a5 | 21 | |
83903cb6 C |
22 | const file = hlsPlaylist.files.find(f => f.resolution.id === resolution) |
23 | const videoName = basename(file.fileUrl) | |
57f879a5 | 24 | |
0305db28 | 25 | const playlist = await command.get({ url: `${baseUrlPlaylist}/${removeFragmentedMP4Ext(videoName)}.m3u8` }) |
764b1a14 | 26 | |
57f879a5 C |
27 | const matches = /#EXT-X-BYTERANGE:(\d+)@(\d+)/.exec(playlist) |
28 | ||
29 | const length = parseInt(matches[1], 10) | |
30 | const offset = parseInt(matches[2], 10) | |
31 | const range = `${offset}-${offset + length - 1}` | |
32 | ||
cfd57d2c | 33 | const segmentBody = await command.getFragmentedSegment({ |
0305db28 | 34 | url: `${baseUrlSegment}/${videoName}`, |
57f879a5 C |
35 | expectedStatus: HttpStatusCode.PARTIAL_CONTENT_206, |
36 | range: `bytes=${range}` | |
37 | }) | |
38 | ||
39 | const shaBody = await command.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url }) | |
40 | expect(sha256(segmentBody)).to.equal(shaBody[videoName][range]) | |
41 | } | |
42 | ||
54db8e3d C |
43 | // --------------------------------------------------------------------------- |
44 | ||
57f879a5 | 45 | async function checkLiveSegmentHash (options: { |
254d3579 | 46 | server: PeerTubeServer |
57f879a5 C |
47 | baseUrlSegment: string |
48 | videoUUID: string | |
49 | segmentName: string | |
50 | hlsPlaylist: VideoStreamingPlaylist | |
51 | }) { | |
52 | const { server, baseUrlSegment, videoUUID, segmentName, hlsPlaylist } = options | |
89d241a7 | 53 | const command = server.streamingPlaylists |
57f879a5 | 54 | |
cfd57d2c | 55 | const segmentBody = await command.getFragmentedSegment({ url: `${baseUrlSegment}/${videoUUID}/${segmentName}` }) |
57f879a5 C |
56 | const shaBody = await command.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url }) |
57 | ||
58 | expect(sha256(segmentBody)).to.equal(shaBody[segmentName]) | |
59 | } | |
60 | ||
54db8e3d C |
61 | // --------------------------------------------------------------------------- |
62 | ||
57f879a5 | 63 | async function checkResolutionsInMasterPlaylist (options: { |
254d3579 | 64 | server: PeerTubeServer |
57f879a5 C |
65 | playlistUrl: string |
66 | resolutions: number[] | |
cfd57d2c | 67 | transcoded?: boolean // default true |
8bd6aa04 | 68 | withRetry?: boolean // default false |
57f879a5 | 69 | }) { |
8bd6aa04 | 70 | const { server, playlistUrl, resolutions, withRetry = false, transcoded = true } = options |
57f879a5 | 71 | |
8bd6aa04 | 72 | const masterPlaylist = await server.streamingPlaylists.get({ url: playlistUrl, withRetry }) |
57f879a5 C |
73 | |
74 | for (const resolution of resolutions) { | |
cfd57d2c C |
75 | const reg = transcoded |
76 | ? new RegExp('#EXT-X-STREAM-INF:BANDWIDTH=\\d+,RESOLUTION=\\d+x' + resolution + ',(FRAME-RATE=\\d+,)?CODECS="avc1.64001f,mp4a.40.2"') | |
77 | : new RegExp('#EXT-X-STREAM-INF:BANDWIDTH=\\d+,RESOLUTION=\\d+x' + resolution + '') | |
57f879a5 C |
78 | |
79 | expect(masterPlaylist).to.match(reg) | |
80 | } | |
84cae54e C |
81 | |
82 | const playlistsLength = masterPlaylist.split('\n').filter(line => line.startsWith('#EXT-X-STREAM-INF:BANDWIDTH=')) | |
83 | expect(playlistsLength).to.have.lengthOf(resolutions.length) | |
57f879a5 C |
84 | } |
85 | ||
3545e72c C |
86 | async function completeCheckHlsPlaylist (options: { |
87 | servers: PeerTubeServer[] | |
88 | videoUUID: string | |
89 | hlsOnly: boolean | |
90 | ||
91 | resolutions?: number[] | |
92 | objectStorageBaseUrl: string | |
93 | }) { | |
94 | const { videoUUID, hlsOnly, objectStorageBaseUrl } = options | |
95 | ||
96 | const resolutions = options.resolutions ?? [ 240, 360, 480, 720 ] | |
97 | ||
98 | for (const server of options.servers) { | |
99 | const videoDetails = await server.videos.get({ id: videoUUID }) | |
100 | const baseUrl = `http://${videoDetails.account.host}` | |
101 | ||
102 | expect(videoDetails.streamingPlaylists).to.have.lengthOf(1) | |
103 | ||
104 | const hlsPlaylist = videoDetails.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS) | |
105 | expect(hlsPlaylist).to.not.be.undefined | |
106 | ||
107 | const hlsFiles = hlsPlaylist.files | |
108 | expect(hlsFiles).to.have.lengthOf(resolutions.length) | |
109 | ||
110 | if (hlsOnly) expect(videoDetails.files).to.have.lengthOf(0) | |
111 | else expect(videoDetails.files).to.have.lengthOf(resolutions.length) | |
112 | ||
113 | // Check JSON files | |
114 | for (const resolution of resolutions) { | |
115 | const file = hlsFiles.find(f => f.resolution.id === resolution) | |
116 | expect(file).to.not.be.undefined | |
117 | ||
118 | expect(file.magnetUri).to.have.lengthOf.above(2) | |
119 | expect(file.torrentUrl).to.match( | |
44df7025 | 120 | new RegExp(`${server.url}/lazy-static/torrents/${uuidRegex}-${file.resolution.id}-hls.torrent`) |
3545e72c C |
121 | ) |
122 | ||
123 | if (objectStorageBaseUrl) { | |
124 | expectStartWith(file.fileUrl, objectStorageBaseUrl) | |
125 | } else { | |
126 | expect(file.fileUrl).to.match( | |
127 | new RegExp(`${baseUrl}/static/streaming-playlists/hls/${videoDetails.uuid}/${uuidRegex}-${file.resolution.id}-fragmented.mp4`) | |
128 | ) | |
129 | } | |
130 | ||
131 | expect(file.resolution.label).to.equal(resolution + 'p') | |
132 | ||
133 | await makeRawRequest({ url: file.torrentUrl, expectedStatus: HttpStatusCode.OK_200 }) | |
134 | await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 }) | |
135 | ||
136 | const torrent = await webtorrentAdd(file.magnetUri, true) | |
137 | expect(torrent.files).to.be.an('array') | |
138 | expect(torrent.files.length).to.equal(1) | |
139 | expect(torrent.files[0].path).to.exist.and.to.not.equal('') | |
140 | } | |
141 | ||
142 | // Check master playlist | |
143 | { | |
144 | await checkResolutionsInMasterPlaylist({ server, playlistUrl: hlsPlaylist.playlistUrl, resolutions }) | |
145 | ||
146 | const masterPlaylist = await server.streamingPlaylists.get({ url: hlsPlaylist.playlistUrl }) | |
147 | ||
148 | let i = 0 | |
149 | for (const resolution of resolutions) { | |
150 | expect(masterPlaylist).to.contain(`${resolution}.m3u8`) | |
151 | expect(masterPlaylist).to.contain(`${resolution}.m3u8`) | |
152 | ||
153 | const url = 'http://' + videoDetails.account.host | |
154 | await hlsInfohashExist(url, hlsPlaylist.playlistUrl, i) | |
155 | ||
156 | i++ | |
157 | } | |
158 | } | |
159 | ||
160 | // Check resolution playlists | |
161 | { | |
162 | for (const resolution of resolutions) { | |
163 | const file = hlsFiles.find(f => f.resolution.id === resolution) | |
164 | const playlistName = removeFragmentedMP4Ext(basename(file.fileUrl)) + '.m3u8' | |
165 | ||
166 | const url = objectStorageBaseUrl | |
167 | ? `${objectStorageBaseUrl}hls/${videoUUID}/${playlistName}` | |
168 | : `${baseUrl}/static/streaming-playlists/hls/${videoUUID}/${playlistName}` | |
169 | ||
170 | const subPlaylist = await server.streamingPlaylists.get({ url }) | |
171 | ||
172 | expect(subPlaylist).to.match(new RegExp(`${uuidRegex}-${resolution}-fragmented.mp4`)) | |
173 | expect(subPlaylist).to.contain(basename(file.fileUrl)) | |
174 | } | |
175 | } | |
176 | ||
177 | { | |
178 | const baseUrlAndPath = objectStorageBaseUrl | |
179 | ? objectStorageBaseUrl + 'hls/' + videoUUID | |
180 | : baseUrl + '/static/streaming-playlists/hls/' + videoUUID | |
181 | ||
182 | for (const resolution of resolutions) { | |
183 | await checkSegmentHash({ | |
184 | server, | |
185 | baseUrlPlaylist: baseUrlAndPath, | |
186 | baseUrlSegment: baseUrlAndPath, | |
187 | resolution, | |
188 | hlsPlaylist | |
189 | }) | |
190 | } | |
191 | } | |
192 | } | |
193 | } | |
194 | ||
71e3e879 C |
195 | async function checkVideoFileTokenReinjection (options: { |
196 | server: PeerTubeServer | |
197 | videoUUID: string | |
198 | videoFileToken: string | |
199 | resolutions: number[] | |
200 | isLive: boolean | |
201 | }) { | |
202 | const { server, resolutions, videoFileToken, videoUUID, isLive } = options | |
203 | ||
204 | const video = await server.videos.getWithToken({ id: videoUUID }) | |
205 | const hls = video.streamingPlaylists[0] | |
206 | ||
207 | const query = { videoFileToken, reinjectVideoFileToken: 'true' } | |
208 | const { text } = await makeRawRequest({ url: hls.playlistUrl, query, expectedStatus: HttpStatusCode.OK_200 }) | |
209 | ||
210 | for (let i = 0; i < resolutions.length; i++) { | |
211 | const resolution = resolutions[i] | |
212 | ||
213 | const suffix = isLive | |
214 | ? i | |
215 | : `-${resolution}` | |
216 | ||
73fb3dc5 | 217 | expect(text).to.contain(`${suffix}.m3u8?videoFileToken=${videoFileToken}&reinjectVideoFileToken=true`) |
71e3e879 C |
218 | } |
219 | ||
220 | const resolutionPlaylists = extractResolutionPlaylistUrls(hls.playlistUrl, text) | |
221 | expect(resolutionPlaylists).to.have.lengthOf(resolutions.length) | |
222 | ||
223 | for (const url of resolutionPlaylists) { | |
224 | const { text } = await makeRawRequest({ url, query, expectedStatus: HttpStatusCode.OK_200 }) | |
225 | ||
226 | const extension = isLive | |
227 | ? '.ts' | |
228 | : '.mp4' | |
229 | ||
230 | expect(text).to.contain(`${extension}?videoFileToken=${videoFileToken}`) | |
73fb3dc5 | 231 | expect(text).not.to.contain(`reinjectVideoFileToken=true`) |
71e3e879 C |
232 | } |
233 | } | |
234 | ||
235 | function extractResolutionPlaylistUrls (masterPath: string, masterContent: string) { | |
236 | return masterContent.match(/^([^.]+\.m3u8.*)/mg) | |
237 | .map(filename => join(dirname(masterPath), filename)) | |
238 | } | |
239 | ||
57f879a5 C |
240 | export { |
241 | checkSegmentHash, | |
242 | checkLiveSegmentHash, | |
3545e72c | 243 | checkResolutionsInMasterPlaylist, |
71e3e879 C |
244 | completeCheckHlsPlaylist, |
245 | extractResolutionPlaylistUrls, | |
246 | checkVideoFileTokenReinjection | |
57f879a5 | 247 | } |