]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Introduce streaming playlists command
authorChocobozzz <me@florianbigard.com>
Fri, 9 Jul 2021 08:21:10 +0000 (10:21 +0200)
committerChocobozzz <me@florianbigard.com>
Tue, 20 Jul 2021 13:27:18 +0000 (15:27 +0200)
server/tests/api/live/live.ts
server/tests/api/redundancy/redundancy.ts
server/tests/api/videos/video-hls.ts
shared/extra-utils/server/servers.ts
shared/extra-utils/shared/abstract-command.ts
shared/extra-utils/videos/index.ts
shared/extra-utils/videos/streaming-playlists-command.ts [new file with mode: 0644]
shared/extra-utils/videos/streaming-playlists.ts [new file with mode: 0644]
shared/extra-utils/videos/video-streaming-playlists.ts [deleted file]

index cb52e443144d3dff179dc59d35ba5b969d2a6cd9..0b06df44c8d07d3a4dd5094b359b8de6c2847142 100644 (file)
@@ -15,7 +15,6 @@ import {
   doubleFollow,
   flushAndRunMultipleServers,
   getMyVideosWithFilter,
-  getPlaylist,
   getVideo,
   getVideosList,
   getVideosWithFilters,
@@ -397,20 +396,27 @@ describe('Test live', function () {
         // Only finite files are displayed
         expect(hlsPlaylist.files).to.have.lengthOf(0)
 
-        await checkResolutionsInMasterPlaylist(hlsPlaylist.playlistUrl, resolutions)
+        await checkResolutionsInMasterPlaylist({ server, playlistUrl: hlsPlaylist.playlistUrl, resolutions })
 
         for (let i = 0; i < resolutions.length; i++) {
           const segmentNum = 3
           const segmentName = `${i}-00000${segmentNum}.ts`
           await commands[0].waitUntilSegmentGeneration({ videoUUID: video.uuid, resolution: i, segment: segmentNum })
 
-          const res = await getPlaylist(`${servers[0].url}/static/streaming-playlists/hls/${video.uuid}/${i}.m3u8`)
-          const subPlaylist = res.text
+          const subPlaylist = await servers[0].streamingPlaylistsCommand.get({
+            url: `${servers[0].url}/static/streaming-playlists/hls/${video.uuid}/${i}.m3u8`
+          })
 
           expect(subPlaylist).to.contain(segmentName)
 
           const baseUrlAndPath = servers[0].url + '/static/streaming-playlists/hls'
-          await checkLiveSegmentHash(baseUrlAndPath, video.uuid, segmentName, hlsPlaylist)
+          await checkLiveSegmentHash({
+            server,
+            baseUrlSegment: baseUrlAndPath,
+            videoUUID: video.uuid,
+            segmentName,
+            hlsPlaylist
+          })
         }
       }
     }
index e4ea99de6c2223973218a09f66593446a6613f5e..d20cb80f10fd5e2bf5470fd1c8a258393f465a42 100644 (file)
@@ -203,7 +203,7 @@ async function check1PlaylistRedundancies (videoUUID?: string) {
   const hlsPlaylist = (res.body as VideoDetails).streamingPlaylists[0]
 
   for (const resolution of [ 240, 360, 480, 720 ]) {
-    await checkSegmentHash(baseUrlPlaylist, baseUrlSegment, videoUUID, resolution, hlsPlaylist)
+    await checkSegmentHash({ server: servers[1], baseUrlPlaylist, baseUrlSegment, videoUUID, resolution, hlsPlaylist })
   }
 
   const directories = [
index 3821cfed03ba2b9cd1704b04f95536c19ffbddc2..3dd8c906697f36f4feb7e485d8fda34b3d11f646 100644 (file)
@@ -12,7 +12,6 @@ import {
   cleanupTests,
   doubleFollow,
   flushAndRunMultipleServers,
-  getPlaylist,
   getVideo,
   makeRawRequest,
   removeVideo,
@@ -67,10 +66,9 @@ async function checkHlsPlaylist (servers: ServerInfo[], videoUUID: string, hlsOn
     }
 
     {
-      await checkResolutionsInMasterPlaylist(hlsPlaylist.playlistUrl, resolutions)
+      await checkResolutionsInMasterPlaylist({ server, playlistUrl: hlsPlaylist.playlistUrl, resolutions })
 
-      const res = await getPlaylist(hlsPlaylist.playlistUrl)
-      const masterPlaylist = res.text
+      const masterPlaylist = await server.streamingPlaylistsCommand.get({ url: hlsPlaylist.playlistUrl })
 
       for (const resolution of resolutions) {
         expect(masterPlaylist).to.contain(`${resolution}.m3u8`)
@@ -80,9 +78,10 @@ async function checkHlsPlaylist (servers: ServerInfo[], videoUUID: string, hlsOn
 
     {
       for (const resolution of resolutions) {
-        const res = await getPlaylist(`${baseUrl}/static/streaming-playlists/hls/${videoUUID}/${resolution}.m3u8`)
+        const subPlaylist = await server.streamingPlaylistsCommand.get({
+          url: `${baseUrl}/static/streaming-playlists/hls/${videoUUID}/${resolution}.m3u8`
+        })
 
-        const subPlaylist = res.text
         expect(subPlaylist).to.contain(`${videoUUID}-${resolution}-fragmented.mp4`)
       }
     }
@@ -91,7 +90,14 @@ async function checkHlsPlaylist (servers: ServerInfo[], videoUUID: string, hlsOn
       const baseUrlAndPath = baseUrl + '/static/streaming-playlists/hls'
 
       for (const resolution of resolutions) {
-        await checkSegmentHash(baseUrlAndPath, baseUrlAndPath, videoUUID, resolution, hlsPlaylist)
+        await checkSegmentHash({
+          server,
+          baseUrlPlaylist: baseUrlAndPath,
+          baseUrlSegment: baseUrlAndPath,
+          videoUUID,
+          resolution,
+          hlsPlaylist
+        })
       }
     }
   }
index 95c8761101c1c5b3be64029b60a3b65e651357f5..6a1dadbccc311ee4f95f64bc4b0232c329b99304 100644 (file)
@@ -26,7 +26,8 @@ import {
   ImportsCommand,
   LiveCommand,
   PlaylistsCommand,
-  ServicesCommand
+  ServicesCommand,
+  StreamingPlaylistsCommand
 } from '../videos'
 import { ConfigCommand } from './config-command'
 import { ContactFormCommand } from './contact-form-command'
@@ -117,6 +118,7 @@ interface ServerInfo {
   playlistsCommand?: PlaylistsCommand
   historyCommand?: HistoryCommand
   importsCommand?: ImportsCommand
+  streamingPlaylistsCommand?: StreamingPlaylistsCommand
 }
 
 function parallelTests () {
@@ -350,6 +352,7 @@ async function runServer (server: ServerInfo, configOverrideArg?: any, args = []
       server.playlistsCommand = new PlaylistsCommand(server)
       server.historyCommand = new HistoryCommand(server)
       server.importsCommand = new ImportsCommand(server)
+      server.streamingPlaylistsCommand = new StreamingPlaylistsCommand(server)
 
       res(server)
     })
index 1e07e946920ba34b1ac3c9a26b860c155fcdca9f..be368376f75d305560a9b95d01477d8134065a40 100644 (file)
@@ -16,6 +16,9 @@ export interface OverrideCommandOptions {
 }
 
 interface InternalCommonCommandOptions extends OverrideCommandOptions {
+  // Default to server.url
+  url?: string
+
   path: string
   // If we automatically send the server token if the token is not provided
   implicitToken: boolean
@@ -27,6 +30,7 @@ interface InternalGetCommandOptions extends InternalCommonCommandOptions {
   contentType?: string
   accept?: string
   redirects?: number
+  range?: string
 }
 
 abstract class AbstractCommand {
@@ -55,6 +59,22 @@ abstract class AbstractCommand {
     return unwrapText(this.getRequest(options))
   }
 
+  protected getRawRequest (options: Omit<InternalGetCommandOptions, 'path'>) {
+    const { url, range } = options
+    const { host, protocol, pathname } = new URL(url)
+
+    return this.getRequest({
+      ...options,
+
+      token: this.buildCommonRequestToken(options),
+      defaultExpectedStatus: this.buildStatusCodeExpected(options),
+
+      url: `${protocol}//${host}`,
+      path: pathname,
+      range
+    })
+  }
+
   protected getRequest (options: InternalGetCommandOptions) {
     const { redirects, query, contentType, accept } = options
 
@@ -127,20 +147,31 @@ abstract class AbstractCommand {
   }
 
   private buildCommonRequestOptions (options: InternalCommonCommandOptions) {
-    const { token, expectedStatus, defaultExpectedStatus, path } = options
+    const { path } = options
+
+    return {
+      url: this.server.url,
+      path,
+
+      token: this.buildCommonRequestToken(options),
+      statusCodeExpected: this.buildStatusCodeExpected(options)
+    }
+  }
+
+  private buildCommonRequestToken (options: Pick<InternalCommonCommandOptions, 'token' | 'implicitToken'>) {
+    const { token } = options
 
     const fallbackToken = options.implicitToken
       ? this.server.accessToken
       : undefined
 
-    return {
-      url: this.server.url,
-      path,
+    return token !== undefined ? token : fallbackToken
+  }
 
-      token: token !== undefined ? token : fallbackToken,
+  private buildStatusCodeExpected (options: Pick<InternalCommonCommandOptions, 'expectedStatus' | 'defaultExpectedStatus'>) {
+    const { expectedStatus, defaultExpectedStatus } = options
 
-      statusCodeExpected: expectedStatus ?? this.expectedStatus ?? defaultExpectedStatus
-    }
+    return expectedStatus ?? this.expectedStatus ?? defaultExpectedStatus
   }
 }
 
index 372cf7a9059a8b5b1ce9a9f95e7354e200481411..f87ae8eea5763c7c9af3d2ff2a9adc211c61061a 100644 (file)
@@ -9,7 +9,8 @@ export * from './live'
 export * from './playlists-command'
 export * from './playlists'
 export * from './services-command'
+export * from './streaming-playlists-command'
+export * from './streaming-playlists'
 export * from './video-channels'
 export * from './video-comments'
-export * from './video-streaming-playlists'
 export * 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 (file)
index 0000000..4caec71
--- /dev/null
@@ -0,0 +1,45 @@
+
+import { HttpStatusCode } from '../../core-utils/miscs/http-error-codes'
+import { unwrapBody, unwrapText } from '../requests'
+import { AbstractCommand, OverrideCommandOptions } from '../shared'
+
+export class StreamingPlaylistsCommand extends AbstractCommand {
+
+  get (options: OverrideCommandOptions & {
+    url: string
+  }) {
+    return unwrapText(this.getRawRequest({
+      ...options,
+
+      url: options.url,
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    }))
+  }
+
+  getSegment (options: OverrideCommandOptions & {
+    url: string
+    range?: string
+  }) {
+    return unwrapText(this.getRawRequest({
+      ...options,
+
+      url: options.url,
+      range: options.range,
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200,
+    }))
+  }
+
+  getSegmentSha256 (options: OverrideCommandOptions & {
+    url: string
+  }) {
+    return unwrapBody<{ [ id: string ]: string }>(this.getRawRequest({
+      ...options,
+
+      url: options.url,
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    }))
+  }
+}
diff --git a/shared/extra-utils/videos/streaming-playlists.ts b/shared/extra-utils/videos/streaming-playlists.ts
new file mode 100644 (file)
index 0000000..0324c73
--- /dev/null
@@ -0,0 +1,76 @@
+import { expect } from 'chai'
+import { sha256 } from '@server/helpers/core-utils'
+import { HttpStatusCode } from '@shared/core-utils'
+import { VideoStreamingPlaylist } from '@shared/models'
+import { ServerInfo } from '../server'
+
+async function checkSegmentHash (options: {
+  server: ServerInfo
+  baseUrlPlaylist: string
+  baseUrlSegment: string
+  videoUUID: string
+  resolution: number
+  hlsPlaylist: VideoStreamingPlaylist
+}) {
+  const { server, baseUrlPlaylist, baseUrlSegment, videoUUID, resolution, hlsPlaylist } = options
+  const command = server.streamingPlaylistsCommand
+
+  const playlist = await command.get({ url: `${baseUrlPlaylist}/${videoUUID}/${resolution}.m3u8` })
+
+  const videoName = `${videoUUID}-${resolution}-fragmented.mp4`
+
+  const matches = /#EXT-X-BYTERANGE:(\d+)@(\d+)/.exec(playlist)
+
+  const length = parseInt(matches[1], 10)
+  const offset = parseInt(matches[2], 10)
+  const range = `${offset}-${offset + length - 1}`
+
+  const segmentBody = await command.getSegment({
+    url: `${baseUrlSegment}/${videoUUID}/${videoName}`,
+    expectedStatus: HttpStatusCode.PARTIAL_CONTENT_206,
+    range: `bytes=${range}`
+  })
+
+  const shaBody = await command.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url })
+  expect(sha256(segmentBody)).to.equal(shaBody[videoName][range])
+}
+
+async function checkLiveSegmentHash (options: {
+  server: ServerInfo
+  baseUrlSegment: string
+  videoUUID: string
+  segmentName: string
+  hlsPlaylist: VideoStreamingPlaylist
+}) {
+  const { server, baseUrlSegment, videoUUID, segmentName, hlsPlaylist } = options
+  const command = server.streamingPlaylistsCommand
+
+  const segmentBody = await command.getSegment({ url: `${baseUrlSegment}/${videoUUID}/${segmentName}` })
+  const shaBody = await command.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url })
+
+  expect(sha256(segmentBody)).to.equal(shaBody[segmentName])
+}
+
+async function checkResolutionsInMasterPlaylist (options: {
+  server: ServerInfo
+  playlistUrl: string
+  resolutions: number[]
+}) {
+  const { server, playlistUrl, resolutions } = options
+
+  const masterPlaylist = await server.streamingPlaylistsCommand.get({ url: playlistUrl })
+
+  for (const resolution of resolutions) {
+    const reg = new RegExp(
+      '#EXT-X-STREAM-INF:BANDWIDTH=\\d+,RESOLUTION=\\d+x' + resolution + ',(FRAME-RATE=\\d+,)?CODECS="avc1.64001f,mp4a.40.2"'
+    )
+
+    expect(masterPlaylist).to.match(reg)
+  }
+}
+
+export {
+  checkSegmentHash,
+  checkLiveSegmentHash,
+  checkResolutionsInMasterPlaylist
+}
diff --git a/shared/extra-utils/videos/video-streaming-playlists.ts b/shared/extra-utils/videos/video-streaming-playlists.ts
deleted file mode 100644 (file)
index 99c2e18..0000000
+++ /dev/null
@@ -1,82 +0,0 @@
-import { makeRawRequest } from '../requests/requests'
-import { sha256 } from '../../../server/helpers/core-utils'
-import { VideoStreamingPlaylist } from '../../models/videos/video-streaming-playlist.model'
-import { expect } from 'chai'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
-
-function getPlaylist (url: string, statusCodeExpected = HttpStatusCode.OK_200) {
-  return makeRawRequest(url, statusCodeExpected)
-}
-
-function getSegment (url: string, statusCodeExpected = HttpStatusCode.OK_200, range?: string) {
-  return makeRawRequest(url, statusCodeExpected, range)
-}
-
-function getSegmentSha256 (url: string, statusCodeExpected = HttpStatusCode.OK_200) {
-  return makeRawRequest(url, statusCodeExpected)
-}
-
-async function checkSegmentHash (
-  baseUrlPlaylist: string,
-  baseUrlSegment: string,
-  videoUUID: string,
-  resolution: number,
-  hlsPlaylist: VideoStreamingPlaylist
-) {
-  const res = await getPlaylist(`${baseUrlPlaylist}/${videoUUID}/${resolution}.m3u8`)
-  const playlist = res.text
-
-  const videoName = `${videoUUID}-${resolution}-fragmented.mp4`
-
-  const matches = /#EXT-X-BYTERANGE:(\d+)@(\d+)/.exec(playlist)
-
-  const length = parseInt(matches[1], 10)
-  const offset = parseInt(matches[2], 10)
-  const range = `${offset}-${offset + length - 1}`
-
-  const res2 = await getSegment(`${baseUrlSegment}/${videoUUID}/${videoName}`, HttpStatusCode.PARTIAL_CONTENT_206, `bytes=${range}`)
-
-  const resSha = await getSegmentSha256(hlsPlaylist.segmentsSha256Url)
-
-  const sha256Server = resSha.body[videoName][range]
-  expect(sha256(res2.body)).to.equal(sha256Server)
-}
-
-async function checkLiveSegmentHash (
-  baseUrlSegment: string,
-  videoUUID: string,
-  segmentName: string,
-  hlsPlaylist: VideoStreamingPlaylist
-) {
-  const res2 = await getSegment(`${baseUrlSegment}/${videoUUID}/${segmentName}`)
-
-  const resSha = await getSegmentSha256(hlsPlaylist.segmentsSha256Url)
-
-  const sha256Server = resSha.body[segmentName]
-  expect(sha256(res2.body)).to.equal(sha256Server)
-}
-
-async function checkResolutionsInMasterPlaylist (playlistUrl: string, resolutions: number[]) {
-  const res = await getPlaylist(playlistUrl)
-
-  const masterPlaylist = res.text
-
-  for (const resolution of resolutions) {
-    const reg = new RegExp(
-      '#EXT-X-STREAM-INF:BANDWIDTH=\\d+,RESOLUTION=\\d+x' + resolution + ',(FRAME-RATE=\\d+,)?CODECS="avc1.64001f,mp4a.40.2"'
-    )
-
-    expect(masterPlaylist).to.match(reg)
-  }
-}
-
-// ---------------------------------------------------------------------------
-
-export {
-  getPlaylist,
-  getSegment,
-  checkResolutionsInMasterPlaylist,
-  getSegmentSha256,
-  checkLiveSegmentHash,
-  checkSegmentHash
-}