From 57f879a540551c3b958b0991c8e1e3657a4481d8 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 9 Jul 2021 10:21:10 +0200 Subject: Introduce streaming playlists command --- shared/extra-utils/server/servers.ts | 5 +- shared/extra-utils/shared/abstract-command.ts | 45 ++++++++++-- shared/extra-utils/videos/index.ts | 3 +- .../videos/streaming-playlists-command.ts | 45 ++++++++++++ shared/extra-utils/videos/streaming-playlists.ts | 76 ++++++++++++++++++++ .../videos/video-streaming-playlists.ts | 82 ---------------------- 6 files changed, 165 insertions(+), 91 deletions(-) create mode 100644 shared/extra-utils/videos/streaming-playlists-command.ts create mode 100644 shared/extra-utils/videos/streaming-playlists.ts delete mode 100644 shared/extra-utils/videos/video-streaming-playlists.ts (limited to 'shared') 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 { 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) }) 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 { } 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) { + 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) { + 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) { + const { expectedStatus, defaultExpectedStatus } = options - statusCodeExpected: expectedStatus ?? this.expectedStatus ?? defaultExpectedStatus - } + return expectedStatus ?? this.expectedStatus ?? defaultExpectedStatus } } 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' 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 index 000000000..4caec7137 --- /dev/null +++ b/shared/extra-utils/videos/streaming-playlists-command.ts @@ -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 index 000000000..0324c739a --- /dev/null +++ b/shared/extra-utils/videos/streaming-playlists.ts @@ -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 index 99c2e1880..000000000 --- a/shared/extra-utils/videos/video-streaming-playlists.ts +++ /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 -} -- cgit v1.2.3