Introduce streaming playlists command

This commit is contained in:
Chocobozzz 2021-07-09 10:21:10 +02:00
parent 6910f20f11
commit 57f879a540
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
9 changed files with 191 additions and 105 deletions

View File

@ -15,7 +15,6 @@ import {
doubleFollow, doubleFollow,
flushAndRunMultipleServers, flushAndRunMultipleServers,
getMyVideosWithFilter, getMyVideosWithFilter,
getPlaylist,
getVideo, getVideo,
getVideosList, getVideosList,
getVideosWithFilters, getVideosWithFilters,
@ -397,20 +396,27 @@ describe('Test live', function () {
// Only finite files are displayed // Only finite files are displayed
expect(hlsPlaylist.files).to.have.lengthOf(0) 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++) { for (let i = 0; i < resolutions.length; i++) {
const segmentNum = 3 const segmentNum = 3
const segmentName = `${i}-00000${segmentNum}.ts` const segmentName = `${i}-00000${segmentNum}.ts`
await commands[0].waitUntilSegmentGeneration({ videoUUID: video.uuid, resolution: i, segment: segmentNum }) 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 = await servers[0].streamingPlaylistsCommand.get({
const subPlaylist = res.text url: `${servers[0].url}/static/streaming-playlists/hls/${video.uuid}/${i}.m3u8`
})
expect(subPlaylist).to.contain(segmentName) expect(subPlaylist).to.contain(segmentName)
const baseUrlAndPath = servers[0].url + '/static/streaming-playlists/hls' 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
})
} }
} }
} }

View File

@ -203,7 +203,7 @@ async function check1PlaylistRedundancies (videoUUID?: string) {
const hlsPlaylist = (res.body as VideoDetails).streamingPlaylists[0] const hlsPlaylist = (res.body as VideoDetails).streamingPlaylists[0]
for (const resolution of [ 240, 360, 480, 720 ]) { 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 = [ const directories = [

View File

@ -12,7 +12,6 @@ import {
cleanupTests, cleanupTests,
doubleFollow, doubleFollow,
flushAndRunMultipleServers, flushAndRunMultipleServers,
getPlaylist,
getVideo, getVideo,
makeRawRequest, makeRawRequest,
removeVideo, 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 = await server.streamingPlaylistsCommand.get({ url: hlsPlaylist.playlistUrl })
const masterPlaylist = res.text
for (const resolution of resolutions) { for (const resolution of resolutions) {
expect(masterPlaylist).to.contain(`${resolution}.m3u8`) expect(masterPlaylist).to.contain(`${resolution}.m3u8`)
@ -80,9 +78,10 @@ async function checkHlsPlaylist (servers: ServerInfo[], videoUUID: string, hlsOn
{ {
for (const resolution of resolutions) { 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`) 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' const baseUrlAndPath = baseUrl + '/static/streaming-playlists/hls'
for (const resolution of resolutions) { for (const resolution of resolutions) {
await checkSegmentHash(baseUrlAndPath, baseUrlAndPath, videoUUID, resolution, hlsPlaylist) await checkSegmentHash({
server,
baseUrlPlaylist: baseUrlAndPath,
baseUrlSegment: baseUrlAndPath,
videoUUID,
resolution,
hlsPlaylist
})
} }
} }
} }

View File

@ -26,7 +26,8 @@ import {
ImportsCommand, ImportsCommand,
LiveCommand, LiveCommand,
PlaylistsCommand, PlaylistsCommand,
ServicesCommand ServicesCommand,
StreamingPlaylistsCommand
} from '../videos' } from '../videos'
import { ConfigCommand } from './config-command' import { ConfigCommand } from './config-command'
import { ContactFormCommand } from './contact-form-command' import { ContactFormCommand } from './contact-form-command'
@ -117,6 +118,7 @@ interface ServerInfo {
playlistsCommand?: PlaylistsCommand playlistsCommand?: PlaylistsCommand
historyCommand?: HistoryCommand historyCommand?: HistoryCommand
importsCommand?: ImportsCommand importsCommand?: ImportsCommand
streamingPlaylistsCommand?: StreamingPlaylistsCommand
} }
function parallelTests () { function parallelTests () {
@ -350,6 +352,7 @@ async function runServer (server: ServerInfo, configOverrideArg?: any, args = []
server.playlistsCommand = new PlaylistsCommand(server) server.playlistsCommand = new PlaylistsCommand(server)
server.historyCommand = new HistoryCommand(server) server.historyCommand = new HistoryCommand(server)
server.importsCommand = new ImportsCommand(server) server.importsCommand = new ImportsCommand(server)
server.streamingPlaylistsCommand = new StreamingPlaylistsCommand(server)
res(server) res(server)
}) })

View File

@ -16,6 +16,9 @@ export interface OverrideCommandOptions {
} }
interface InternalCommonCommandOptions extends OverrideCommandOptions { interface InternalCommonCommandOptions extends OverrideCommandOptions {
// Default to server.url
url?: string
path: string path: string
// If we automatically send the server token if the token is not provided // If we automatically send the server token if the token is not provided
implicitToken: boolean implicitToken: boolean
@ -27,6 +30,7 @@ interface InternalGetCommandOptions extends InternalCommonCommandOptions {
contentType?: string contentType?: string
accept?: string accept?: string
redirects?: number redirects?: number
range?: string
} }
abstract class AbstractCommand { abstract class AbstractCommand {
@ -55,6 +59,22 @@ abstract class AbstractCommand {
return unwrapText(this.getRequest(options)) 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) { protected getRequest (options: InternalGetCommandOptions) {
const { redirects, query, contentType, accept } = options const { redirects, query, contentType, accept } = options
@ -127,21 +147,32 @@ abstract class AbstractCommand {
} }
private buildCommonRequestOptions (options: InternalCommonCommandOptions) { private buildCommonRequestOptions (options: InternalCommonCommandOptions) {
const { token, expectedStatus, defaultExpectedStatus, path } = options const { path } = options
const fallbackToken = options.implicitToken
? this.server.accessToken
: undefined
return { return {
url: this.server.url, url: this.server.url,
path, path,
token: token !== undefined ? token : fallbackToken, token: this.buildCommonRequestToken(options),
statusCodeExpected: this.buildStatusCodeExpected(options)
statusCodeExpected: expectedStatus ?? this.expectedStatus ?? defaultExpectedStatus
} }
} }
private buildCommonRequestToken (options: Pick<InternalCommonCommandOptions, 'token' | 'implicitToken'>) {
const { token } = options
const fallbackToken = options.implicitToken
? this.server.accessToken
: undefined
return token !== undefined ? token : fallbackToken
}
private buildStatusCodeExpected (options: Pick<InternalCommonCommandOptions, 'expectedStatus' | 'defaultExpectedStatus'>) {
const { expectedStatus, defaultExpectedStatus } = options
return expectedStatus ?? this.expectedStatus ?? defaultExpectedStatus
}
} }
export { export {

View File

@ -9,7 +9,8 @@ export * from './live'
export * from './playlists-command' export * from './playlists-command'
export * from './playlists' export * from './playlists'
export * from './services-command' export * from './services-command'
export * from './streaming-playlists-command'
export * from './streaming-playlists'
export * from './video-channels' export * from './video-channels'
export * from './video-comments' export * from './video-comments'
export * from './video-streaming-playlists'
export * from './videos' export * from './videos'

View File

@ -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
}))
}
}

View File

@ -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
}

View File

@ -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
}