aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2021-07-09 10:21:10 +0200
committerChocobozzz <me@florianbigard.com>2021-07-20 15:27:18 +0200
commit57f879a540551c3b958b0991c8e1e3657a4481d8 (patch)
treecd9283dec9ef0b7fee116c93c36650de188ad892
parent6910f20f114b5bd020258a3a9a3f2117819a60c2 (diff)
downloadPeerTube-57f879a540551c3b958b0991c8e1e3657a4481d8.tar.gz
PeerTube-57f879a540551c3b958b0991c8e1e3657a4481d8.tar.zst
PeerTube-57f879a540551c3b958b0991c8e1e3657a4481d8.zip
Introduce streaming playlists command
-rw-r--r--server/tests/api/live/live.ts16
-rw-r--r--server/tests/api/redundancy/redundancy.ts2
-rw-r--r--server/tests/api/videos/video-hls.ts20
-rw-r--r--shared/extra-utils/server/servers.ts5
-rw-r--r--shared/extra-utils/shared/abstract-command.ts45
-rw-r--r--shared/extra-utils/videos/index.ts3
-rw-r--r--shared/extra-utils/videos/streaming-playlists-command.ts45
-rw-r--r--shared/extra-utils/videos/streaming-playlists.ts76
-rw-r--r--shared/extra-utils/videos/video-streaming-playlists.ts82
9 files changed, 190 insertions, 104 deletions
diff --git a/server/tests/api/live/live.ts b/server/tests/api/live/live.ts
index cb52e4431..0b06df44c 100644
--- a/server/tests/api/live/live.ts
+++ b/server/tests/api/live/live.ts
@@ -15,7 +15,6 @@ import {
15 doubleFollow, 15 doubleFollow,
16 flushAndRunMultipleServers, 16 flushAndRunMultipleServers,
17 getMyVideosWithFilter, 17 getMyVideosWithFilter,
18 getPlaylist,
19 getVideo, 18 getVideo,
20 getVideosList, 19 getVideosList,
21 getVideosWithFilters, 20 getVideosWithFilters,
@@ -397,20 +396,27 @@ describe('Test live', function () {
397 // Only finite files are displayed 396 // Only finite files are displayed
398 expect(hlsPlaylist.files).to.have.lengthOf(0) 397 expect(hlsPlaylist.files).to.have.lengthOf(0)
399 398
400 await checkResolutionsInMasterPlaylist(hlsPlaylist.playlistUrl, resolutions) 399 await checkResolutionsInMasterPlaylist({ server, playlistUrl: hlsPlaylist.playlistUrl, resolutions })
401 400
402 for (let i = 0; i < resolutions.length; i++) { 401 for (let i = 0; i < resolutions.length; i++) {
403 const segmentNum = 3 402 const segmentNum = 3
404 const segmentName = `${i}-00000${segmentNum}.ts` 403 const segmentName = `${i}-00000${segmentNum}.ts`
405 await commands[0].waitUntilSegmentGeneration({ videoUUID: video.uuid, resolution: i, segment: segmentNum }) 404 await commands[0].waitUntilSegmentGeneration({ videoUUID: video.uuid, resolution: i, segment: segmentNum })
406 405
407 const res = await getPlaylist(`${servers[0].url}/static/streaming-playlists/hls/${video.uuid}/${i}.m3u8`) 406 const subPlaylist = await servers[0].streamingPlaylistsCommand.get({
408 const subPlaylist = res.text 407 url: `${servers[0].url}/static/streaming-playlists/hls/${video.uuid}/${i}.m3u8`
408 })
409 409
410 expect(subPlaylist).to.contain(segmentName) 410 expect(subPlaylist).to.contain(segmentName)
411 411
412 const baseUrlAndPath = servers[0].url + '/static/streaming-playlists/hls' 412 const baseUrlAndPath = servers[0].url + '/static/streaming-playlists/hls'
413 await checkLiveSegmentHash(baseUrlAndPath, video.uuid, segmentName, hlsPlaylist) 413 await checkLiveSegmentHash({
414 server,
415 baseUrlSegment: baseUrlAndPath,
416 videoUUID: video.uuid,
417 segmentName,
418 hlsPlaylist
419 })
414 } 420 }
415 } 421 }
416 } 422 }
diff --git a/server/tests/api/redundancy/redundancy.ts b/server/tests/api/redundancy/redundancy.ts
index e4ea99de6..d20cb80f1 100644
--- a/server/tests/api/redundancy/redundancy.ts
+++ b/server/tests/api/redundancy/redundancy.ts
@@ -203,7 +203,7 @@ async function check1PlaylistRedundancies (videoUUID?: string) {
203 const hlsPlaylist = (res.body as VideoDetails).streamingPlaylists[0] 203 const hlsPlaylist = (res.body as VideoDetails).streamingPlaylists[0]
204 204
205 for (const resolution of [ 240, 360, 480, 720 ]) { 205 for (const resolution of [ 240, 360, 480, 720 ]) {
206 await checkSegmentHash(baseUrlPlaylist, baseUrlSegment, videoUUID, resolution, hlsPlaylist) 206 await checkSegmentHash({ server: servers[1], baseUrlPlaylist, baseUrlSegment, videoUUID, resolution, hlsPlaylist })
207 } 207 }
208 208
209 const directories = [ 209 const directories = [
diff --git a/server/tests/api/videos/video-hls.ts b/server/tests/api/videos/video-hls.ts
index 3821cfed0..3dd8c9066 100644
--- a/server/tests/api/videos/video-hls.ts
+++ b/server/tests/api/videos/video-hls.ts
@@ -12,7 +12,6 @@ import {
12 cleanupTests, 12 cleanupTests,
13 doubleFollow, 13 doubleFollow,
14 flushAndRunMultipleServers, 14 flushAndRunMultipleServers,
15 getPlaylist,
16 getVideo, 15 getVideo,
17 makeRawRequest, 16 makeRawRequest,
18 removeVideo, 17 removeVideo,
@@ -67,10 +66,9 @@ async function checkHlsPlaylist (servers: ServerInfo[], videoUUID: string, hlsOn
67 } 66 }
68 67
69 { 68 {
70 await checkResolutionsInMasterPlaylist(hlsPlaylist.playlistUrl, resolutions) 69 await checkResolutionsInMasterPlaylist({ server, playlistUrl: hlsPlaylist.playlistUrl, resolutions })
71 70
72 const res = await getPlaylist(hlsPlaylist.playlistUrl) 71 const masterPlaylist = await server.streamingPlaylistsCommand.get({ url: hlsPlaylist.playlistUrl })
73 const masterPlaylist = res.text
74 72
75 for (const resolution of resolutions) { 73 for (const resolution of resolutions) {
76 expect(masterPlaylist).to.contain(`${resolution}.m3u8`) 74 expect(masterPlaylist).to.contain(`${resolution}.m3u8`)
@@ -80,9 +78,10 @@ async function checkHlsPlaylist (servers: ServerInfo[], videoUUID: string, hlsOn
80 78
81 { 79 {
82 for (const resolution of resolutions) { 80 for (const resolution of resolutions) {
83 const res = await getPlaylist(`${baseUrl}/static/streaming-playlists/hls/${videoUUID}/${resolution}.m3u8`) 81 const subPlaylist = await server.streamingPlaylistsCommand.get({
82 url: `${baseUrl}/static/streaming-playlists/hls/${videoUUID}/${resolution}.m3u8`
83 })
84 84
85 const subPlaylist = res.text
86 expect(subPlaylist).to.contain(`${videoUUID}-${resolution}-fragmented.mp4`) 85 expect(subPlaylist).to.contain(`${videoUUID}-${resolution}-fragmented.mp4`)
87 } 86 }
88 } 87 }
@@ -91,7 +90,14 @@ async function checkHlsPlaylist (servers: ServerInfo[], videoUUID: string, hlsOn
91 const baseUrlAndPath = baseUrl + '/static/streaming-playlists/hls' 90 const baseUrlAndPath = baseUrl + '/static/streaming-playlists/hls'
92 91
93 for (const resolution of resolutions) { 92 for (const resolution of resolutions) {
94 await checkSegmentHash(baseUrlAndPath, baseUrlAndPath, videoUUID, resolution, hlsPlaylist) 93 await checkSegmentHash({
94 server,
95 baseUrlPlaylist: baseUrlAndPath,
96 baseUrlSegment: baseUrlAndPath,
97 videoUUID,
98 resolution,
99 hlsPlaylist
100 })
95 } 101 }
96 } 102 }
97 } 103 }
diff --git a/shared/extra-utils/server/servers.ts b/shared/extra-utils/server/servers.ts
index 95c876110..6a1dadbcc 100644
--- a/shared/extra-utils/server/servers.ts
+++ b/shared/extra-utils/server/servers.ts
@@ -26,7 +26,8 @@ import {
26 ImportsCommand, 26 ImportsCommand,
27 LiveCommand, 27 LiveCommand,
28 PlaylistsCommand, 28 PlaylistsCommand,
29 ServicesCommand 29 ServicesCommand,
30 StreamingPlaylistsCommand
30} from '../videos' 31} from '../videos'
31import { ConfigCommand } from './config-command' 32import { ConfigCommand } from './config-command'
32import { ContactFormCommand } from './contact-form-command' 33import { ContactFormCommand } from './contact-form-command'
@@ -117,6 +118,7 @@ interface ServerInfo {
117 playlistsCommand?: PlaylistsCommand 118 playlistsCommand?: PlaylistsCommand
118 historyCommand?: HistoryCommand 119 historyCommand?: HistoryCommand
119 importsCommand?: ImportsCommand 120 importsCommand?: ImportsCommand
121 streamingPlaylistsCommand?: StreamingPlaylistsCommand
120} 122}
121 123
122function parallelTests () { 124function parallelTests () {
@@ -350,6 +352,7 @@ async function runServer (server: ServerInfo, configOverrideArg?: any, args = []
350 server.playlistsCommand = new PlaylistsCommand(server) 352 server.playlistsCommand = new PlaylistsCommand(server)
351 server.historyCommand = new HistoryCommand(server) 353 server.historyCommand = new HistoryCommand(server)
352 server.importsCommand = new ImportsCommand(server) 354 server.importsCommand = new ImportsCommand(server)
355 server.streamingPlaylistsCommand = new StreamingPlaylistsCommand(server)
353 356
354 res(server) 357 res(server)
355 }) 358 })
diff --git a/shared/extra-utils/shared/abstract-command.ts b/shared/extra-utils/shared/abstract-command.ts
index 1e07e9469..be368376f 100644
--- a/shared/extra-utils/shared/abstract-command.ts
+++ b/shared/extra-utils/shared/abstract-command.ts
@@ -16,6 +16,9 @@ export interface OverrideCommandOptions {
16} 16}
17 17
18interface InternalCommonCommandOptions extends OverrideCommandOptions { 18interface InternalCommonCommandOptions extends OverrideCommandOptions {
19 // Default to server.url
20 url?: string
21
19 path: string 22 path: string
20 // If we automatically send the server token if the token is not provided 23 // If we automatically send the server token if the token is not provided
21 implicitToken: boolean 24 implicitToken: boolean
@@ -27,6 +30,7 @@ interface InternalGetCommandOptions extends InternalCommonCommandOptions {
27 contentType?: string 30 contentType?: string
28 accept?: string 31 accept?: string
29 redirects?: number 32 redirects?: number
33 range?: string
30} 34}
31 35
32abstract class AbstractCommand { 36abstract class AbstractCommand {
@@ -55,6 +59,22 @@ abstract class AbstractCommand {
55 return unwrapText(this.getRequest(options)) 59 return unwrapText(this.getRequest(options))
56 } 60 }
57 61
62 protected getRawRequest (options: Omit<InternalGetCommandOptions, 'path'>) {
63 const { url, range } = options
64 const { host, protocol, pathname } = new URL(url)
65
66 return this.getRequest({
67 ...options,
68
69 token: this.buildCommonRequestToken(options),
70 defaultExpectedStatus: this.buildStatusCodeExpected(options),
71
72 url: `${protocol}//${host}`,
73 path: pathname,
74 range
75 })
76 }
77
58 protected getRequest (options: InternalGetCommandOptions) { 78 protected getRequest (options: InternalGetCommandOptions) {
59 const { redirects, query, contentType, accept } = options 79 const { redirects, query, contentType, accept } = options
60 80
@@ -127,20 +147,31 @@ abstract class AbstractCommand {
127 } 147 }
128 148
129 private buildCommonRequestOptions (options: InternalCommonCommandOptions) { 149 private buildCommonRequestOptions (options: InternalCommonCommandOptions) {
130 const { token, expectedStatus, defaultExpectedStatus, path } = options 150 const { path } = options
151
152 return {
153 url: this.server.url,
154 path,
155
156 token: this.buildCommonRequestToken(options),
157 statusCodeExpected: this.buildStatusCodeExpected(options)
158 }
159 }
160
161 private buildCommonRequestToken (options: Pick<InternalCommonCommandOptions, 'token' | 'implicitToken'>) {
162 const { token } = options
131 163
132 const fallbackToken = options.implicitToken 164 const fallbackToken = options.implicitToken
133 ? this.server.accessToken 165 ? this.server.accessToken
134 : undefined 166 : undefined
135 167
136 return { 168 return token !== undefined ? token : fallbackToken
137 url: this.server.url, 169 }
138 path,
139 170
140 token: token !== undefined ? token : fallbackToken, 171 private buildStatusCodeExpected (options: Pick<InternalCommonCommandOptions, 'expectedStatus' | 'defaultExpectedStatus'>) {
172 const { expectedStatus, defaultExpectedStatus } = options
141 173
142 statusCodeExpected: expectedStatus ?? this.expectedStatus ?? defaultExpectedStatus 174 return expectedStatus ?? this.expectedStatus ?? defaultExpectedStatus
143 }
144 } 175 }
145} 176}
146 177
diff --git a/shared/extra-utils/videos/index.ts b/shared/extra-utils/videos/index.ts
index 372cf7a90..f87ae8eea 100644
--- a/shared/extra-utils/videos/index.ts
+++ b/shared/extra-utils/videos/index.ts
@@ -9,7 +9,8 @@ export * from './live'
9export * from './playlists-command' 9export * from './playlists-command'
10export * from './playlists' 10export * from './playlists'
11export * from './services-command' 11export * from './services-command'
12export * from './streaming-playlists-command'
13export * from './streaming-playlists'
12export * from './video-channels' 14export * from './video-channels'
13export * from './video-comments' 15export * from './video-comments'
14export * from './video-streaming-playlists'
15export * from './videos' 16export * from './videos'
diff --git a/shared/extra-utils/videos/streaming-playlists-command.ts b/shared/extra-utils/videos/streaming-playlists-command.ts
new file mode 100644
index 000000000..4caec7137
--- /dev/null
+++ b/shared/extra-utils/videos/streaming-playlists-command.ts
@@ -0,0 +1,45 @@
1
2import { HttpStatusCode } from '../../core-utils/miscs/http-error-codes'
3import { unwrapBody, unwrapText } from '../requests'
4import { AbstractCommand, OverrideCommandOptions } from '../shared'
5
6export class StreamingPlaylistsCommand extends AbstractCommand {
7
8 get (options: OverrideCommandOptions & {
9 url: string
10 }) {
11 return unwrapText(this.getRawRequest({
12 ...options,
13
14 url: options.url,
15 implicitToken: false,
16 defaultExpectedStatus: HttpStatusCode.OK_200
17 }))
18 }
19
20 getSegment (options: OverrideCommandOptions & {
21 url: string
22 range?: string
23 }) {
24 return unwrapText(this.getRawRequest({
25 ...options,
26
27 url: options.url,
28 range: options.range,
29 implicitToken: false,
30 defaultExpectedStatus: HttpStatusCode.OK_200,
31 }))
32 }
33
34 getSegmentSha256 (options: OverrideCommandOptions & {
35 url: string
36 }) {
37 return unwrapBody<{ [ id: string ]: string }>(this.getRawRequest({
38 ...options,
39
40 url: options.url,
41 implicitToken: false,
42 defaultExpectedStatus: HttpStatusCode.OK_200
43 }))
44 }
45}
diff --git a/shared/extra-utils/videos/streaming-playlists.ts b/shared/extra-utils/videos/streaming-playlists.ts
new file mode 100644
index 000000000..0324c739a
--- /dev/null
+++ b/shared/extra-utils/videos/streaming-playlists.ts
@@ -0,0 +1,76 @@
1import { expect } from 'chai'
2import { sha256 } from '@server/helpers/core-utils'
3import { HttpStatusCode } from '@shared/core-utils'
4import { VideoStreamingPlaylist } from '@shared/models'
5import { ServerInfo } from '../server'
6
7async function checkSegmentHash (options: {
8 server: ServerInfo
9 baseUrlPlaylist: string
10 baseUrlSegment: string
11 videoUUID: string
12 resolution: number
13 hlsPlaylist: VideoStreamingPlaylist
14}) {
15 const { server, baseUrlPlaylist, baseUrlSegment, videoUUID, resolution, hlsPlaylist } = options
16 const command = server.streamingPlaylistsCommand
17
18 const playlist = await command.get({ url: `${baseUrlPlaylist}/${videoUUID}/${resolution}.m3u8` })
19
20 const videoName = `${videoUUID}-${resolution}-fragmented.mp4`
21
22 const matches = /#EXT-X-BYTERANGE:(\d+)@(\d+)/.exec(playlist)
23
24 const length = parseInt(matches[1], 10)
25 const offset = parseInt(matches[2], 10)
26 const range = `${offset}-${offset + length - 1}`
27
28 const segmentBody = await command.getSegment({
29 url: `${baseUrlSegment}/${videoUUID}/${videoName}`,
30 expectedStatus: HttpStatusCode.PARTIAL_CONTENT_206,
31 range: `bytes=${range}`
32 })
33
34 const shaBody = await command.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url })
35 expect(sha256(segmentBody)).to.equal(shaBody[videoName][range])
36}
37
38async function checkLiveSegmentHash (options: {
39 server: ServerInfo
40 baseUrlSegment: string
41 videoUUID: string
42 segmentName: string
43 hlsPlaylist: VideoStreamingPlaylist
44}) {
45 const { server, baseUrlSegment, videoUUID, segmentName, hlsPlaylist } = options
46 const command = server.streamingPlaylistsCommand
47
48 const segmentBody = await command.getSegment({ url: `${baseUrlSegment}/${videoUUID}/${segmentName}` })
49 const shaBody = await command.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url })
50
51 expect(sha256(segmentBody)).to.equal(shaBody[segmentName])
52}
53
54async function checkResolutionsInMasterPlaylist (options: {
55 server: ServerInfo
56 playlistUrl: string
57 resolutions: number[]
58}) {
59 const { server, playlistUrl, resolutions } = options
60
61 const masterPlaylist = await server.streamingPlaylistsCommand.get({ url: playlistUrl })
62
63 for (const resolution of resolutions) {
64 const reg = new RegExp(
65 '#EXT-X-STREAM-INF:BANDWIDTH=\\d+,RESOLUTION=\\d+x' + resolution + ',(FRAME-RATE=\\d+,)?CODECS="avc1.64001f,mp4a.40.2"'
66 )
67
68 expect(masterPlaylist).to.match(reg)
69 }
70}
71
72export {
73 checkSegmentHash,
74 checkLiveSegmentHash,
75 checkResolutionsInMasterPlaylist
76}
diff --git a/shared/extra-utils/videos/video-streaming-playlists.ts b/shared/extra-utils/videos/video-streaming-playlists.ts
deleted file mode 100644
index 99c2e1880..000000000
--- a/shared/extra-utils/videos/video-streaming-playlists.ts
+++ /dev/null
@@ -1,82 +0,0 @@
1import { makeRawRequest } from '../requests/requests'
2import { sha256 } from '../../../server/helpers/core-utils'
3import { VideoStreamingPlaylist } from '../../models/videos/video-streaming-playlist.model'
4import { expect } from 'chai'
5import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
6
7function getPlaylist (url: string, statusCodeExpected = HttpStatusCode.OK_200) {
8 return makeRawRequest(url, statusCodeExpected)
9}
10
11function getSegment (url: string, statusCodeExpected = HttpStatusCode.OK_200, range?: string) {
12 return makeRawRequest(url, statusCodeExpected, range)
13}
14
15function getSegmentSha256 (url: string, statusCodeExpected = HttpStatusCode.OK_200) {
16 return makeRawRequest(url, statusCodeExpected)
17}
18
19async function checkSegmentHash (
20 baseUrlPlaylist: string,
21 baseUrlSegment: string,
22 videoUUID: string,
23 resolution: number,
24 hlsPlaylist: VideoStreamingPlaylist
25) {
26 const res = await getPlaylist(`${baseUrlPlaylist}/${videoUUID}/${resolution}.m3u8`)
27 const playlist = res.text
28
29 const videoName = `${videoUUID}-${resolution}-fragmented.mp4`
30
31 const matches = /#EXT-X-BYTERANGE:(\d+)@(\d+)/.exec(playlist)
32
33 const length = parseInt(matches[1], 10)
34 const offset = parseInt(matches[2], 10)
35 const range = `${offset}-${offset + length - 1}`
36
37 const res2 = await getSegment(`${baseUrlSegment}/${videoUUID}/${videoName}`, HttpStatusCode.PARTIAL_CONTENT_206, `bytes=${range}`)
38
39 const resSha = await getSegmentSha256(hlsPlaylist.segmentsSha256Url)
40
41 const sha256Server = resSha.body[videoName][range]
42 expect(sha256(res2.body)).to.equal(sha256Server)
43}
44
45async function checkLiveSegmentHash (
46 baseUrlSegment: string,
47 videoUUID: string,
48 segmentName: string,
49 hlsPlaylist: VideoStreamingPlaylist
50) {
51 const res2 = await getSegment(`${baseUrlSegment}/${videoUUID}/${segmentName}`)
52
53 const resSha = await getSegmentSha256(hlsPlaylist.segmentsSha256Url)
54
55 const sha256Server = resSha.body[segmentName]
56 expect(sha256(res2.body)).to.equal(sha256Server)
57}
58
59async function checkResolutionsInMasterPlaylist (playlistUrl: string, resolutions: number[]) {
60 const res = await getPlaylist(playlistUrl)
61
62 const masterPlaylist = res.text
63
64 for (const resolution of resolutions) {
65 const reg = new RegExp(
66 '#EXT-X-STREAM-INF:BANDWIDTH=\\d+,RESOLUTION=\\d+x' + resolution + ',(FRAME-RATE=\\d+,)?CODECS="avc1.64001f,mp4a.40.2"'
67 )
68
69 expect(masterPlaylist).to.match(reg)
70 }
71}
72
73// ---------------------------------------------------------------------------
74
75export {
76 getPlaylist,
77 getSegment,
78 checkResolutionsInMasterPlaylist,
79 getSegmentSha256,
80 checkLiveSegmentHash,
81 checkSegmentHash
82}