Introduce streaming playlists command
This commit is contained in:
parent
6910f20f11
commit
57f879a540
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
|
|
@ -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,21 +147,32 @@ abstract class AbstractCommand {
|
|||
}
|
||||
|
||||
private buildCommonRequestOptions (options: InternalCommonCommandOptions) {
|
||||
const { token, expectedStatus, defaultExpectedStatus, path } = options
|
||||
|
||||
const fallbackToken = options.implicitToken
|
||||
? this.server.accessToken
|
||||
: undefined
|
||||
const { path } = options
|
||||
|
||||
return {
|
||||
url: this.server.url,
|
||||
path,
|
||||
|
||||
token: token !== undefined ? token : fallbackToken,
|
||||
|
||||
statusCodeExpected: expectedStatus ?? this.expectedStatus ?? defaultExpectedStatus
|
||||
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 token !== undefined ? token : fallbackToken
|
||||
}
|
||||
|
||||
private buildStatusCodeExpected (options: Pick<InternalCommonCommandOptions, 'expectedStatus' | 'defaultExpectedStatus'>) {
|
||||
const { expectedStatus, defaultExpectedStatus } = options
|
||||
|
||||
return expectedStatus ?? this.expectedStatus ?? defaultExpectedStatus
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
}))
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue