]>
Commit | Line | Data |
---|---|---|
3545e72c C |
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ |
2 | ||
57f879a5 | 3 | import { expect } from 'chai' |
83903cb6 | 4 | import { basename } 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 | ||
43 | async function checkLiveSegmentHash (options: { | |
254d3579 | 44 | server: PeerTubeServer |
57f879a5 C |
45 | baseUrlSegment: string |
46 | videoUUID: string | |
47 | segmentName: string | |
48 | hlsPlaylist: VideoStreamingPlaylist | |
49 | }) { | |
50 | const { server, baseUrlSegment, videoUUID, segmentName, hlsPlaylist } = options | |
89d241a7 | 51 | const command = server.streamingPlaylists |
57f879a5 | 52 | |
cfd57d2c | 53 | const segmentBody = await command.getFragmentedSegment({ url: `${baseUrlSegment}/${videoUUID}/${segmentName}` }) |
57f879a5 C |
54 | const shaBody = await command.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url }) |
55 | ||
56 | expect(sha256(segmentBody)).to.equal(shaBody[segmentName]) | |
57 | } | |
58 | ||
59 | async function checkResolutionsInMasterPlaylist (options: { | |
254d3579 | 60 | server: PeerTubeServer |
57f879a5 C |
61 | playlistUrl: string |
62 | resolutions: number[] | |
cfd57d2c | 63 | transcoded?: boolean // default true |
8bd6aa04 | 64 | withRetry?: boolean // default false |
57f879a5 | 65 | }) { |
8bd6aa04 | 66 | const { server, playlistUrl, resolutions, withRetry = false, transcoded = true } = options |
57f879a5 | 67 | |
8bd6aa04 | 68 | const masterPlaylist = await server.streamingPlaylists.get({ url: playlistUrl, withRetry }) |
57f879a5 C |
69 | |
70 | for (const resolution of resolutions) { | |
cfd57d2c C |
71 | const reg = transcoded |
72 | ? new RegExp('#EXT-X-STREAM-INF:BANDWIDTH=\\d+,RESOLUTION=\\d+x' + resolution + ',(FRAME-RATE=\\d+,)?CODECS="avc1.64001f,mp4a.40.2"') | |
73 | : new RegExp('#EXT-X-STREAM-INF:BANDWIDTH=\\d+,RESOLUTION=\\d+x' + resolution + '') | |
57f879a5 C |
74 | |
75 | expect(masterPlaylist).to.match(reg) | |
76 | } | |
84cae54e C |
77 | |
78 | const playlistsLength = masterPlaylist.split('\n').filter(line => line.startsWith('#EXT-X-STREAM-INF:BANDWIDTH=')) | |
79 | expect(playlistsLength).to.have.lengthOf(resolutions.length) | |
57f879a5 C |
80 | } |
81 | ||
3545e72c C |
82 | async function completeCheckHlsPlaylist (options: { |
83 | servers: PeerTubeServer[] | |
84 | videoUUID: string | |
85 | hlsOnly: boolean | |
86 | ||
87 | resolutions?: number[] | |
88 | objectStorageBaseUrl: string | |
89 | }) { | |
90 | const { videoUUID, hlsOnly, objectStorageBaseUrl } = options | |
91 | ||
92 | const resolutions = options.resolutions ?? [ 240, 360, 480, 720 ] | |
93 | ||
94 | for (const server of options.servers) { | |
95 | const videoDetails = await server.videos.get({ id: videoUUID }) | |
96 | const baseUrl = `http://${videoDetails.account.host}` | |
97 | ||
98 | expect(videoDetails.streamingPlaylists).to.have.lengthOf(1) | |
99 | ||
100 | const hlsPlaylist = videoDetails.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS) | |
101 | expect(hlsPlaylist).to.not.be.undefined | |
102 | ||
103 | const hlsFiles = hlsPlaylist.files | |
104 | expect(hlsFiles).to.have.lengthOf(resolutions.length) | |
105 | ||
106 | if (hlsOnly) expect(videoDetails.files).to.have.lengthOf(0) | |
107 | else expect(videoDetails.files).to.have.lengthOf(resolutions.length) | |
108 | ||
109 | // Check JSON files | |
110 | for (const resolution of resolutions) { | |
111 | const file = hlsFiles.find(f => f.resolution.id === resolution) | |
112 | expect(file).to.not.be.undefined | |
113 | ||
114 | expect(file.magnetUri).to.have.lengthOf.above(2) | |
115 | expect(file.torrentUrl).to.match( | |
44df7025 | 116 | new RegExp(`${server.url}/lazy-static/torrents/${uuidRegex}-${file.resolution.id}-hls.torrent`) |
3545e72c C |
117 | ) |
118 | ||
119 | if (objectStorageBaseUrl) { | |
120 | expectStartWith(file.fileUrl, objectStorageBaseUrl) | |
121 | } else { | |
122 | expect(file.fileUrl).to.match( | |
123 | new RegExp(`${baseUrl}/static/streaming-playlists/hls/${videoDetails.uuid}/${uuidRegex}-${file.resolution.id}-fragmented.mp4`) | |
124 | ) | |
125 | } | |
126 | ||
127 | expect(file.resolution.label).to.equal(resolution + 'p') | |
128 | ||
129 | await makeRawRequest({ url: file.torrentUrl, expectedStatus: HttpStatusCode.OK_200 }) | |
130 | await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 }) | |
131 | ||
132 | const torrent = await webtorrentAdd(file.magnetUri, true) | |
133 | expect(torrent.files).to.be.an('array') | |
134 | expect(torrent.files.length).to.equal(1) | |
135 | expect(torrent.files[0].path).to.exist.and.to.not.equal('') | |
136 | } | |
137 | ||
138 | // Check master playlist | |
139 | { | |
140 | await checkResolutionsInMasterPlaylist({ server, playlistUrl: hlsPlaylist.playlistUrl, resolutions }) | |
141 | ||
142 | const masterPlaylist = await server.streamingPlaylists.get({ url: hlsPlaylist.playlistUrl }) | |
143 | ||
144 | let i = 0 | |
145 | for (const resolution of resolutions) { | |
146 | expect(masterPlaylist).to.contain(`${resolution}.m3u8`) | |
147 | expect(masterPlaylist).to.contain(`${resolution}.m3u8`) | |
148 | ||
149 | const url = 'http://' + videoDetails.account.host | |
150 | await hlsInfohashExist(url, hlsPlaylist.playlistUrl, i) | |
151 | ||
152 | i++ | |
153 | } | |
154 | } | |
155 | ||
156 | // Check resolution playlists | |
157 | { | |
158 | for (const resolution of resolutions) { | |
159 | const file = hlsFiles.find(f => f.resolution.id === resolution) | |
160 | const playlistName = removeFragmentedMP4Ext(basename(file.fileUrl)) + '.m3u8' | |
161 | ||
162 | const url = objectStorageBaseUrl | |
163 | ? `${objectStorageBaseUrl}hls/${videoUUID}/${playlistName}` | |
164 | : `${baseUrl}/static/streaming-playlists/hls/${videoUUID}/${playlistName}` | |
165 | ||
166 | const subPlaylist = await server.streamingPlaylists.get({ url }) | |
167 | ||
168 | expect(subPlaylist).to.match(new RegExp(`${uuidRegex}-${resolution}-fragmented.mp4`)) | |
169 | expect(subPlaylist).to.contain(basename(file.fileUrl)) | |
170 | } | |
171 | } | |
172 | ||
173 | { | |
174 | const baseUrlAndPath = objectStorageBaseUrl | |
175 | ? objectStorageBaseUrl + 'hls/' + videoUUID | |
176 | : baseUrl + '/static/streaming-playlists/hls/' + videoUUID | |
177 | ||
178 | for (const resolution of resolutions) { | |
179 | await checkSegmentHash({ | |
180 | server, | |
181 | baseUrlPlaylist: baseUrlAndPath, | |
182 | baseUrlSegment: baseUrlAndPath, | |
183 | resolution, | |
184 | hlsPlaylist | |
185 | }) | |
186 | } | |
187 | } | |
188 | } | |
189 | } | |
190 | ||
57f879a5 C |
191 | export { |
192 | checkSegmentHash, | |
193 | checkLiveSegmentHash, | |
3545e72c C |
194 | checkResolutionsInMasterPlaylist, |
195 | completeCheckHlsPlaylist | |
57f879a5 | 196 | } |