Use a single file instead of segments for HLS
This commit is contained in:
parent
6ec0b75beb
commit
4c280004ce
|
@ -3,18 +3,25 @@ import { basename } from 'path'
|
||||||
|
|
||||||
function segmentValidatorFactory (segmentsSha256Url: string) {
|
function segmentValidatorFactory (segmentsSha256Url: string) {
|
||||||
const segmentsJSON = fetchSha256Segments(segmentsSha256Url)
|
const segmentsJSON = fetchSha256Segments(segmentsSha256Url)
|
||||||
|
const regex = /bytes=(\d+)-(\d+)/
|
||||||
|
|
||||||
return async function segmentValidator (segment: Segment) {
|
return async function segmentValidator (segment: Segment) {
|
||||||
const segmentName = basename(segment.url)
|
const filename = basename(segment.url)
|
||||||
|
const captured = regex.exec(segment.range)
|
||||||
|
|
||||||
const hashShouldBe = (await segmentsJSON)[segmentName]
|
const range = captured[1] + '-' + captured[2]
|
||||||
|
|
||||||
|
const hashShouldBe = (await segmentsJSON)[filename][range]
|
||||||
if (hashShouldBe === undefined) {
|
if (hashShouldBe === undefined) {
|
||||||
throw new Error(`Unknown segment name ${segmentName} in segment validator`)
|
throw new Error(`Unknown segment name ${filename}/${range} in segment validator`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const calculatedSha = bufferToEx(await sha256(segment.data))
|
const calculatedSha = bufferToEx(await sha256(segment.data))
|
||||||
if (calculatedSha !== hashShouldBe) {
|
if (calculatedSha !== hashShouldBe) {
|
||||||
throw new Error(`Hashes does not correspond for segment ${segmentName} (expected: ${hashShouldBe} instead of ${calculatedSha})`)
|
throw new Error(
|
||||||
|
`Hashes does not correspond for segment ${filename}/${range}` +
|
||||||
|
`(expected: ${hashShouldBe} instead of ${calculatedSha})`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -117,7 +117,6 @@
|
||||||
"fluent-ffmpeg": "^2.1.0",
|
"fluent-ffmpeg": "^2.1.0",
|
||||||
"fs-extra": "^7.0.0",
|
"fs-extra": "^7.0.0",
|
||||||
"helmet": "^3.12.1",
|
"helmet": "^3.12.1",
|
||||||
"hlsdownloader": "https://github.com/Chocobozzz/hlsdownloader#build",
|
|
||||||
"http-signature": "^1.2.0",
|
"http-signature": "^1.2.0",
|
||||||
"ip-anonymize": "^0.0.6",
|
"ip-anonymize": "^0.0.6",
|
||||||
"ipaddr.js": "1.8.1",
|
"ipaddr.js": "1.8.1",
|
||||||
|
|
|
@ -41,7 +41,7 @@ async function run () {
|
||||||
}
|
}
|
||||||
|
|
||||||
function get (url: string, headers: any = {}) {
|
function get (url: string, headers: any = {}) {
|
||||||
return doRequest({
|
return doRequest<any>({
|
||||||
uri: url,
|
uri: url,
|
||||||
json: true,
|
json: true,
|
||||||
headers: Object.assign(headers, {
|
headers: Object.assign(headers, {
|
||||||
|
|
|
@ -122,7 +122,9 @@ type TranscodeOptions = {
|
||||||
resolution: VideoResolution
|
resolution: VideoResolution
|
||||||
isPortraitMode?: boolean
|
isPortraitMode?: boolean
|
||||||
|
|
||||||
generateHlsPlaylist?: boolean
|
hlsPlaylist?: {
|
||||||
|
videoFilename: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function transcode (options: TranscodeOptions) {
|
function transcode (options: TranscodeOptions) {
|
||||||
|
@ -161,14 +163,16 @@ function transcode (options: TranscodeOptions) {
|
||||||
command = command.withFPS(fps)
|
command = command.withFPS(fps)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.generateHlsPlaylist) {
|
if (options.hlsPlaylist) {
|
||||||
const segmentFilename = `${dirname(options.outputPath)}/${options.resolution}_%03d.ts`
|
const videoPath = `${dirname(options.outputPath)}/${options.hlsPlaylist.videoFilename}`
|
||||||
|
|
||||||
command = command.outputOption('-hls_time 4')
|
command = command.outputOption('-hls_time 4')
|
||||||
.outputOption('-hls_list_size 0')
|
.outputOption('-hls_list_size 0')
|
||||||
.outputOption('-hls_playlist_type vod')
|
.outputOption('-hls_playlist_type vod')
|
||||||
.outputOption('-hls_segment_filename ' + segmentFilename)
|
.outputOption('-hls_segment_filename ' + videoPath)
|
||||||
|
.outputOption('-hls_segment_type fmp4')
|
||||||
.outputOption('-f hls')
|
.outputOption('-f hls')
|
||||||
|
.outputOption('-hls_flags single_file')
|
||||||
}
|
}
|
||||||
|
|
||||||
command
|
command
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { join } from 'path'
|
||||||
|
|
||||||
function doRequest <T> (
|
function doRequest <T> (
|
||||||
requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean }
|
requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean }
|
||||||
): Bluebird<{ response: request.RequestResponse, body: any }> {
|
): Bluebird<{ response: request.RequestResponse, body: T }> {
|
||||||
if (requestOptions.activityPub === true) {
|
if (requestOptions.activityPub === true) {
|
||||||
if (!Array.isArray(requestOptions.headers)) requestOptions.headers = {}
|
if (!Array.isArray(requestOptions.headers)) requestOptions.headers = {}
|
||||||
requestOptions.headers['accept'] = ACTIVITY_PUB.ACCEPT_HEADER
|
requestOptions.headers['accept'] = ACTIVITY_PUB.ACCEPT_HEADER
|
||||||
|
|
|
@ -7,7 +7,6 @@ import { join } from 'path'
|
||||||
import { Instance as ParseTorrent } from 'parse-torrent'
|
import { Instance as ParseTorrent } from 'parse-torrent'
|
||||||
import { remove } from 'fs-extra'
|
import { remove } from 'fs-extra'
|
||||||
import * as memoizee from 'memoizee'
|
import * as memoizee from 'memoizee'
|
||||||
import { isArray } from './custom-validators/misc'
|
|
||||||
|
|
||||||
function deleteFileAsync (path: string) {
|
function deleteFileAsync (path: string) {
|
||||||
remove(path)
|
remove(path)
|
||||||
|
|
|
@ -355,10 +355,10 @@ async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: numbe
|
||||||
|
|
||||||
logger.info('Fetching remote actor %s.', actorUrl)
|
logger.info('Fetching remote actor %s.', actorUrl)
|
||||||
|
|
||||||
const requestResult = await doRequest(options)
|
const requestResult = await doRequest<ActivityPubActor>(options)
|
||||||
normalizeActor(requestResult.body)
|
normalizeActor(requestResult.body)
|
||||||
|
|
||||||
const actorJSON: ActivityPubActor = requestResult.body
|
const actorJSON = requestResult.body
|
||||||
if (isActorObjectValid(actorJSON) === false) {
|
if (isActorObjectValid(actorJSON) === false) {
|
||||||
logger.debug('Remote actor JSON is not valid.', { actorJSON })
|
logger.debug('Remote actor JSON is not valid.', { actorJSON })
|
||||||
return { result: undefined, statusCode: requestResult.response.statusCode }
|
return { result: undefined, statusCode: requestResult.response.statusCode }
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import { VideoModel } from '../models/video/video'
|
import { VideoModel } from '../models/video/video'
|
||||||
import { basename, dirname, join } from 'path'
|
import { basename, join, dirname } from 'path'
|
||||||
import { HLS_PLAYLIST_DIRECTORY, CONFIG } from '../initializers'
|
import { CONFIG, HLS_PLAYLIST_DIRECTORY } from '../initializers'
|
||||||
import { outputJSON, pathExists, readdir, readFile, remove, writeFile, move } from 'fs-extra'
|
import { close, ensureDir, move, open, outputJSON, pathExists, read, readFile, remove, writeFile } from 'fs-extra'
|
||||||
import { getVideoFileSize } from '../helpers/ffmpeg-utils'
|
import { getVideoFileSize } from '../helpers/ffmpeg-utils'
|
||||||
import { sha256 } from '../helpers/core-utils'
|
import { sha256 } from '../helpers/core-utils'
|
||||||
import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
|
import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
|
||||||
import HLSDownloader from 'hlsdownloader'
|
|
||||||
import { logger } from '../helpers/logger'
|
import { logger } from '../helpers/logger'
|
||||||
import { parse } from 'url'
|
import { doRequest, doRequestAndSaveToFile } from '../helpers/requests'
|
||||||
|
import { generateRandomString } from '../helpers/utils'
|
||||||
|
import { flatten, uniq } from 'lodash'
|
||||||
|
|
||||||
async function updateMasterHLSPlaylist (video: VideoModel) {
|
async function updateMasterHLSPlaylist (video: VideoModel) {
|
||||||
const directory = join(HLS_PLAYLIST_DIRECTORY, video.uuid)
|
const directory = join(HLS_PLAYLIST_DIRECTORY, video.uuid)
|
||||||
|
@ -37,66 +38,119 @@ async function updateMasterHLSPlaylist (video: VideoModel) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateSha256Segments (video: VideoModel) {
|
async function updateSha256Segments (video: VideoModel) {
|
||||||
const directory = join(HLS_PLAYLIST_DIRECTORY, video.uuid)
|
const json: { [filename: string]: { [range: string]: string } } = {}
|
||||||
const files = await readdir(directory)
|
|
||||||
const json: { [filename: string]: string} = {}
|
|
||||||
|
|
||||||
for (const file of files) {
|
const playlistDirectory = join(HLS_PLAYLIST_DIRECTORY, video.uuid)
|
||||||
if (file.endsWith('.ts') === false) continue
|
|
||||||
|
|
||||||
const buffer = await readFile(join(directory, file))
|
// For all the resolutions available for this video
|
||||||
const filename = basename(file)
|
for (const file of video.VideoFiles) {
|
||||||
|
const rangeHashes: { [range: string]: string } = {}
|
||||||
|
|
||||||
json[filename] = sha256(buffer)
|
const videoPath = join(playlistDirectory, VideoStreamingPlaylistModel.getHlsVideoName(video.uuid, file.resolution))
|
||||||
|
const playlistPath = join(playlistDirectory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(file.resolution))
|
||||||
|
|
||||||
|
// Maybe the playlist is not generated for this resolution yet
|
||||||
|
if (!await pathExists(playlistPath)) continue
|
||||||
|
|
||||||
|
const playlistContent = await readFile(playlistPath)
|
||||||
|
const ranges = getRangesFromPlaylist(playlistContent.toString())
|
||||||
|
|
||||||
|
const fd = await open(videoPath, 'r')
|
||||||
|
for (const range of ranges) {
|
||||||
|
const buf = Buffer.alloc(range.length)
|
||||||
|
await read(fd, buf, 0, range.length, range.offset)
|
||||||
|
|
||||||
|
rangeHashes[`${range.offset}-${range.offset + range.length - 1}`] = sha256(buf)
|
||||||
|
}
|
||||||
|
await close(fd)
|
||||||
|
|
||||||
|
const videoFilename = VideoStreamingPlaylistModel.getHlsVideoName(video.uuid, file.resolution)
|
||||||
|
json[videoFilename] = rangeHashes
|
||||||
}
|
}
|
||||||
|
|
||||||
const outputPath = join(directory, VideoStreamingPlaylistModel.getHlsSha256SegmentsFilename())
|
const outputPath = join(playlistDirectory, VideoStreamingPlaylistModel.getHlsSha256SegmentsFilename())
|
||||||
await outputJSON(outputPath, json)
|
await outputJSON(outputPath, json)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getRangesFromPlaylist (playlistContent: string) {
|
||||||
|
const ranges: { offset: number, length: number }[] = []
|
||||||
|
const lines = playlistContent.split('\n')
|
||||||
|
const regex = /^#EXT-X-BYTERANGE:(\d+)@(\d+)$/
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const captured = regex.exec(line)
|
||||||
|
|
||||||
|
if (captured) {
|
||||||
|
ranges.push({ length: parseInt(captured[1], 10), offset: parseInt(captured[2], 10) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ranges
|
||||||
|
}
|
||||||
|
|
||||||
function downloadPlaylistSegments (playlistUrl: string, destinationDir: string, timeout: number) {
|
function downloadPlaylistSegments (playlistUrl: string, destinationDir: string, timeout: number) {
|
||||||
let timer
|
let timer
|
||||||
|
|
||||||
logger.info('Importing HLS playlist %s', playlistUrl)
|
logger.info('Importing HLS playlist %s', playlistUrl)
|
||||||
|
|
||||||
const params = {
|
|
||||||
playlistURL: playlistUrl,
|
|
||||||
destination: CONFIG.STORAGE.TMP_DIR
|
|
||||||
}
|
|
||||||
const downloader = new HLSDownloader(params)
|
|
||||||
|
|
||||||
const hlsDestinationDir = join(CONFIG.STORAGE.TMP_DIR, dirname(parse(playlistUrl).pathname))
|
|
||||||
|
|
||||||
return new Promise<string>(async (res, rej) => {
|
return new Promise<string>(async (res, rej) => {
|
||||||
downloader.startDownload(err => {
|
const tmpDirectory = join(CONFIG.STORAGE.TMP_DIR, await generateRandomString(10))
|
||||||
clearTimeout(timer)
|
|
||||||
|
|
||||||
if (err) {
|
await ensureDir(tmpDirectory)
|
||||||
deleteTmpDirectory(hlsDestinationDir)
|
|
||||||
|
|
||||||
return rej(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
move(hlsDestinationDir, destinationDir, { overwrite: true })
|
|
||||||
.then(() => res())
|
|
||||||
.catch(err => {
|
|
||||||
deleteTmpDirectory(hlsDestinationDir)
|
|
||||||
|
|
||||||
return rej(err)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
timer = setTimeout(() => {
|
timer = setTimeout(() => {
|
||||||
deleteTmpDirectory(hlsDestinationDir)
|
deleteTmpDirectory(tmpDirectory)
|
||||||
|
|
||||||
return rej(new Error('HLS download timeout.'))
|
return rej(new Error('HLS download timeout.'))
|
||||||
}, timeout)
|
}, timeout)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fetch master playlist
|
||||||
|
const subPlaylistUrls = await fetchUniqUrls(playlistUrl)
|
||||||
|
|
||||||
|
const subRequests = subPlaylistUrls.map(u => fetchUniqUrls(u))
|
||||||
|
const fileUrls = uniq(flatten(await Promise.all(subRequests)))
|
||||||
|
|
||||||
|
logger.debug('Will download %d HLS files.', fileUrls.length, { fileUrls })
|
||||||
|
|
||||||
|
for (const fileUrl of fileUrls) {
|
||||||
|
const destPath = join(tmpDirectory, basename(fileUrl))
|
||||||
|
|
||||||
|
await doRequestAndSaveToFile({ uri: fileUrl }, destPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTimeout(timer)
|
||||||
|
|
||||||
|
await move(tmpDirectory, destinationDir, { overwrite: true })
|
||||||
|
|
||||||
|
return res()
|
||||||
|
} catch (err) {
|
||||||
|
deleteTmpDirectory(tmpDirectory)
|
||||||
|
|
||||||
|
return rej(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
function deleteTmpDirectory (directory: string) {
|
function deleteTmpDirectory (directory: string) {
|
||||||
remove(directory)
|
remove(directory)
|
||||||
.catch(err => logger.error('Cannot delete path on HLS download error.', { err }))
|
.catch(err => logger.error('Cannot delete path on HLS download error.', { err }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchUniqUrls (playlistUrl: string) {
|
||||||
|
const { body } = await doRequest<string>({ uri: playlistUrl })
|
||||||
|
|
||||||
|
if (!body) return []
|
||||||
|
|
||||||
|
const urls = body.split('\n')
|
||||||
|
.filter(line => line.endsWith('.m3u8') || line.endsWith('.mp4'))
|
||||||
|
.map(url => {
|
||||||
|
if (url.startsWith('http://') || url.startsWith('https://')) return url
|
||||||
|
|
||||||
|
return `${dirname(playlistUrl)}/${url}`
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return uniq(urls)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
@ -100,7 +100,10 @@ async function generateHlsPlaylist (video: VideoModel, resolution: VideoResoluti
|
||||||
outputPath,
|
outputPath,
|
||||||
resolution,
|
resolution,
|
||||||
isPortraitMode,
|
isPortraitMode,
|
||||||
generateHlsPlaylist: true
|
|
||||||
|
hlsPlaylist: {
|
||||||
|
videoFilename: VideoStreamingPlaylistModel.getHlsVideoName(video.uuid, resolution)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await transcode(transcodeOptions)
|
await transcode(transcodeOptions)
|
||||||
|
|
|
@ -125,6 +125,10 @@ export class VideoStreamingPlaylistModel extends Model<VideoStreamingPlaylistMod
|
||||||
return 'segments-sha256.json'
|
return 'segments-sha256.json'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getHlsVideoName (uuid: string, resolution: number) {
|
||||||
|
return `${uuid}-${resolution}-fragmented.mp4`
|
||||||
|
}
|
||||||
|
|
||||||
static getHlsMasterPlaylistStaticPath (videoUUID: string) {
|
static getHlsMasterPlaylistStaticPath (videoUUID: string) {
|
||||||
return join(STATIC_PATHS.PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getMasterHlsPlaylistFilename())
|
return join(STATIC_PATHS.PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getMasterHlsPlaylistFilename())
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ import {
|
||||||
viewVideo,
|
viewVideo,
|
||||||
wait,
|
wait,
|
||||||
waitUntilLog,
|
waitUntilLog,
|
||||||
checkVideoFilesWereRemoved, removeVideo, getVideoWithToken, reRunServer
|
checkVideoFilesWereRemoved, removeVideo, getVideoWithToken, reRunServer, checkSegmentHash
|
||||||
} from '../../../../shared/utils'
|
} from '../../../../shared/utils'
|
||||||
import { waitJobs } from '../../../../shared/utils/server/jobs'
|
import { waitJobs } from '../../../../shared/utils/server/jobs'
|
||||||
|
|
||||||
|
@ -178,20 +178,24 @@ async function check1PlaylistRedundancies (videoUUID?: string) {
|
||||||
expect(redundancy.baseUrl).to.equal(servers[0].url + '/static/redundancy/hls/' + videoUUID)
|
expect(redundancy.baseUrl).to.equal(servers[0].url + '/static/redundancy/hls/' + videoUUID)
|
||||||
}
|
}
|
||||||
|
|
||||||
await makeGetRequest({
|
const baseUrlPlaylist = servers[1].url + '/static/playlists/hls'
|
||||||
url: servers[0].url,
|
const baseUrlSegment = servers[0].url + '/static/redundancy/hls'
|
||||||
statusCodeExpected: 200,
|
|
||||||
path: `/static/redundancy/hls/${videoUUID}/360_000.ts`,
|
const res = await getVideo(servers[0].url, videoUUID)
|
||||||
contentType: null
|
const hlsPlaylist = (res.body as VideoDetails).streamingPlaylists[0]
|
||||||
})
|
|
||||||
|
for (const resolution of [ 240, 360, 480, 720 ]) {
|
||||||
|
await checkSegmentHash(baseUrlPlaylist, baseUrlSegment, videoUUID, resolution, hlsPlaylist)
|
||||||
|
}
|
||||||
|
|
||||||
for (const directory of [ 'test1/redundancy/hls', 'test2/playlists/hls' ]) {
|
for (const directory of [ 'test1/redundancy/hls', 'test2/playlists/hls' ]) {
|
||||||
const files = await readdir(join(root(), directory, videoUUID))
|
const files = await readdir(join(root(), directory, videoUUID))
|
||||||
expect(files).to.have.length.at.least(4)
|
expect(files).to.have.length.at.least(4)
|
||||||
|
|
||||||
for (const resolution of [ 240, 360, 480, 720 ]) {
|
for (const resolution of [ 240, 360, 480, 720 ]) {
|
||||||
expect(files.find(f => f === `${resolution}_000.ts`)).to.not.be.undefined
|
const filename = `${videoUUID}-${resolution}-fragmented.mp4`
|
||||||
expect(files.find(f => f === `${resolution}_001.ts`)).to.not.be.undefined
|
|
||||||
|
expect(files.find(f => f === filename)).to.not.be.undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,13 +4,12 @@ import * as chai from 'chai'
|
||||||
import 'mocha'
|
import 'mocha'
|
||||||
import {
|
import {
|
||||||
checkDirectoryIsEmpty,
|
checkDirectoryIsEmpty,
|
||||||
|
checkSegmentHash,
|
||||||
checkTmpIsEmpty,
|
checkTmpIsEmpty,
|
||||||
doubleFollow,
|
doubleFollow,
|
||||||
flushAndRunMultipleServers,
|
flushAndRunMultipleServers,
|
||||||
flushTests,
|
flushTests,
|
||||||
getPlaylist,
|
getPlaylist,
|
||||||
getSegment,
|
|
||||||
getSegmentSha256,
|
|
||||||
getVideo,
|
getVideo,
|
||||||
killallServers,
|
killallServers,
|
||||||
removeVideo,
|
removeVideo,
|
||||||
|
@ -22,7 +21,6 @@ import {
|
||||||
} from '../../../../shared/utils'
|
} from '../../../../shared/utils'
|
||||||
import { VideoDetails } from '../../../../shared/models/videos'
|
import { VideoDetails } from '../../../../shared/models/videos'
|
||||||
import { VideoStreamingPlaylistType } from '../../../../shared/models/videos/video-streaming-playlist.type'
|
import { VideoStreamingPlaylistType } from '../../../../shared/models/videos/video-streaming-playlist.type'
|
||||||
import { sha256 } from '../../../helpers/core-utils'
|
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
|
|
||||||
const expect = chai.expect
|
const expect = chai.expect
|
||||||
|
@ -56,19 +54,15 @@ async function checkHlsPlaylist (servers: ServerInfo[], videoUUID: string) {
|
||||||
const res2 = await getPlaylist(`http://localhost:9001/static/playlists/hls/${videoUUID}/${resolution}.m3u8`)
|
const res2 = await getPlaylist(`http://localhost:9001/static/playlists/hls/${videoUUID}/${resolution}.m3u8`)
|
||||||
|
|
||||||
const subPlaylist = res2.text
|
const subPlaylist = res2.text
|
||||||
expect(subPlaylist).to.contain(resolution + '_000.ts')
|
expect(subPlaylist).to.contain(`${videoUUID}-${resolution}-fragmented.mp4`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
const baseUrl = 'http://localhost:9001/static/playlists/hls'
|
||||||
|
|
||||||
for (const resolution of resolutions) {
|
for (const resolution of resolutions) {
|
||||||
|
await checkSegmentHash(baseUrl, baseUrl, videoUUID, resolution, hlsPlaylist)
|
||||||
const res2 = await getSegment(`http://localhost:9001/static/playlists/hls/${videoUUID}/${resolution}_000.ts`)
|
|
||||||
|
|
||||||
const resSha = await getSegmentSha256(hlsPlaylist.segmentsSha256Url)
|
|
||||||
|
|
||||||
const sha256Server = resSha.body[ resolution + '_000.ts' ]
|
|
||||||
expect(sha256(res2.body)).to.equal(sha256Server)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,9 @@ export interface ActivityPubOrderedCollection<T> {
|
||||||
'@context': string[]
|
'@context': string[]
|
||||||
type: 'OrderedCollection' | 'OrderedCollectionPage'
|
type: 'OrderedCollection' | 'OrderedCollectionPage'
|
||||||
totalItems: number
|
totalItems: number
|
||||||
partOf?: string
|
|
||||||
orderedItems: T[]
|
orderedItems: T[]
|
||||||
|
|
||||||
|
partOf?: string
|
||||||
|
next?: string
|
||||||
|
first?: string
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,10 +3,10 @@ import { buildAbsoluteFixturePath, root } from '../miscs/miscs'
|
||||||
import { isAbsolute, join } from 'path'
|
import { isAbsolute, join } from 'path'
|
||||||
import { parse } from 'url'
|
import { parse } from 'url'
|
||||||
|
|
||||||
function makeRawRequest (url: string, statusCodeExpected?: number) {
|
function makeRawRequest (url: string, statusCodeExpected?: number, range?: string) {
|
||||||
const { host, protocol, pathname } = parse(url)
|
const { host, protocol, pathname } = parse(url)
|
||||||
|
|
||||||
return makeGetRequest({ url: `${protocol}//${host}`, path: pathname, statusCodeExpected })
|
return makeGetRequest({ url: `${protocol}//${host}`, path: pathname, statusCodeExpected, range })
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeGetRequest (options: {
|
function makeGetRequest (options: {
|
||||||
|
@ -15,7 +15,8 @@ function makeGetRequest (options: {
|
||||||
query?: any,
|
query?: any,
|
||||||
token?: string,
|
token?: string,
|
||||||
statusCodeExpected?: number,
|
statusCodeExpected?: number,
|
||||||
contentType?: string
|
contentType?: string,
|
||||||
|
range?: string
|
||||||
}) {
|
}) {
|
||||||
if (!options.statusCodeExpected) options.statusCodeExpected = 400
|
if (!options.statusCodeExpected) options.statusCodeExpected = 400
|
||||||
if (options.contentType === undefined) options.contentType = 'application/json'
|
if (options.contentType === undefined) options.contentType = 'application/json'
|
||||||
|
@ -25,6 +26,7 @@ function makeGetRequest (options: {
|
||||||
if (options.contentType) req.set('Accept', options.contentType)
|
if (options.contentType) req.set('Accept', options.contentType)
|
||||||
if (options.token) req.set('Authorization', 'Bearer ' + options.token)
|
if (options.token) req.set('Authorization', 'Bearer ' + options.token)
|
||||||
if (options.query) req.query(options.query)
|
if (options.query) req.query(options.query)
|
||||||
|
if (options.range) req.set('Range', options.range)
|
||||||
|
|
||||||
return req.expect(options.statusCodeExpected)
|
return req.expect(options.statusCodeExpected)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,51 @@
|
||||||
import { makeRawRequest } from '../requests/requests'
|
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'
|
||||||
|
|
||||||
function getPlaylist (url: string, statusCodeExpected = 200) {
|
function getPlaylist (url: string, statusCodeExpected = 200) {
|
||||||
return makeRawRequest(url, statusCodeExpected)
|
return makeRawRequest(url, statusCodeExpected)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSegment (url: string, statusCodeExpected = 200) {
|
function getSegment (url: string, statusCodeExpected = 200, range?: string) {
|
||||||
return makeRawRequest(url, statusCodeExpected)
|
return makeRawRequest(url, statusCodeExpected, range)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSegmentSha256 (url: string, statusCodeExpected = 200) {
|
function getSegmentSha256 (url: string, statusCodeExpected = 200) {
|
||||||
return makeRawRequest(url, statusCodeExpected)
|
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}`, 206, `bytes=${range}`)
|
||||||
|
|
||||||
|
const resSha = await getSegmentSha256(hlsPlaylist.segmentsSha256Url)
|
||||||
|
|
||||||
|
const sha256Server = resSha.body[ videoName ][range]
|
||||||
|
expect(sha256(res2.body)).to.equal(sha256Server)
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getPlaylist,
|
getPlaylist,
|
||||||
getSegment,
|
getSegment,
|
||||||
getSegmentSha256
|
getSegmentSha256,
|
||||||
|
checkSegmentHash
|
||||||
}
|
}
|
||||||
|
|
64
yarn.lock
64
yarn.lock
|
@ -2,14 +2,6 @@
|
||||||
# yarn lockfile v1
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
"@babel/polyfill@^7.2.5":
|
|
||||||
version "7.2.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/@babel/polyfill/-/polyfill-7.2.5.tgz#6c54b964f71ad27edddc567d065e57e87ed7fa7d"
|
|
||||||
integrity sha512-8Y/t3MWThtMLYr0YNC/Q76tqN1w30+b0uQMeFUYauG2UGTR19zyUtFrAzT23zNtBxPp+LbE5E/nwV/q/r3y6ug==
|
|
||||||
dependencies:
|
|
||||||
core-js "^2.5.7"
|
|
||||||
regenerator-runtime "^0.12.0"
|
|
||||||
|
|
||||||
"@iamstarkov/listr-update-renderer@0.4.1":
|
"@iamstarkov/listr-update-renderer@0.4.1":
|
||||||
version "0.4.1"
|
version "0.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/@iamstarkov/listr-update-renderer/-/listr-update-renderer-0.4.1.tgz#d7c48092a2dcf90fd672b6c8b458649cb350c77e"
|
resolved "https://registry.yarnpkg.com/@iamstarkov/listr-update-renderer/-/listr-update-renderer-0.4.1.tgz#d7c48092a2dcf90fd672b6c8b458649cb350c77e"
|
||||||
|
@ -3593,17 +3585,6 @@ hide-powered-by@1.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/hide-powered-by/-/hide-powered-by-1.0.0.tgz#4a85ad65881f62857fc70af7174a1184dccce32b"
|
resolved "https://registry.yarnpkg.com/hide-powered-by/-/hide-powered-by-1.0.0.tgz#4a85ad65881f62857fc70af7174a1184dccce32b"
|
||||||
integrity sha1-SoWtZYgfYoV/xwr3F0oRhNzM4ys=
|
integrity sha1-SoWtZYgfYoV/xwr3F0oRhNzM4ys=
|
||||||
|
|
||||||
"hlsdownloader@https://github.com/Chocobozzz/hlsdownloader#build":
|
|
||||||
version "0.0.0-semantic-release"
|
|
||||||
resolved "https://github.com/Chocobozzz/hlsdownloader#e19f9d803dcfe7ec25fd734b4743184f19a9b0cc"
|
|
||||||
dependencies:
|
|
||||||
"@babel/polyfill" "^7.2.5"
|
|
||||||
async "^2.6.1"
|
|
||||||
minimist "^1.2.0"
|
|
||||||
mkdirp "^0.5.1"
|
|
||||||
request "^2.88.0"
|
|
||||||
request-promise "^4.2.2"
|
|
||||||
|
|
||||||
hosted-git-info@^2.1.4, hosted-git-info@^2.6.0, hosted-git-info@^2.7.1:
|
hosted-git-info@^2.1.4, hosted-git-info@^2.6.0, hosted-git-info@^2.7.1:
|
||||||
version "2.7.1"
|
version "2.7.1"
|
||||||
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.7.1.tgz#97f236977bd6e125408930ff6de3eec6281ec047"
|
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.7.1.tgz#97f236977bd6e125408930ff6de3eec6281ec047"
|
||||||
|
@ -4870,7 +4851,7 @@ lodash@=3.10.1:
|
||||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
|
||||||
integrity sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=
|
integrity sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=
|
||||||
|
|
||||||
lodash@^4.0.0, lodash@^4.13.1, lodash@^4.17.1, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.5, lodash@^4.3.0, lodash@^4.8.2, lodash@~4.17.10:
|
lodash@^4.0.0, lodash@^4.17.1, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.5, lodash@^4.3.0, lodash@^4.8.2, lodash@~4.17.10:
|
||||||
version "4.17.11"
|
version "4.17.11"
|
||||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
|
||||||
integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==
|
integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==
|
||||||
|
@ -6651,11 +6632,6 @@ psl@^1.1.24:
|
||||||
resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.29.tgz#60f580d360170bb722a797cc704411e6da850c67"
|
resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.29.tgz#60f580d360170bb722a797cc704411e6da850c67"
|
||||||
integrity sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==
|
integrity sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==
|
||||||
|
|
||||||
psl@^1.1.28:
|
|
||||||
version "1.1.31"
|
|
||||||
resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.31.tgz#e9aa86d0101b5b105cbe93ac6b784cd547276184"
|
|
||||||
integrity sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==
|
|
||||||
|
|
||||||
pstree.remy@^1.1.2:
|
pstree.remy@^1.1.2:
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.2.tgz#4448bbeb4b2af1fed242afc8dc7416a6f504951a"
|
resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.2.tgz#4448bbeb4b2af1fed242afc8dc7416a6f504951a"
|
||||||
|
@ -6699,7 +6675,7 @@ punycode@^1.4.1:
|
||||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
|
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
|
||||||
integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
|
integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
|
||||||
|
|
||||||
punycode@^2.1.0, punycode@^2.1.1:
|
punycode@^2.1.0:
|
||||||
version "2.1.1"
|
version "2.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
|
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
|
||||||
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
|
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
|
||||||
|
@ -6982,11 +6958,6 @@ reflect-metadata@^0.1.12:
|
||||||
resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.12.tgz#311bf0c6b63cd782f228a81abe146a2bfa9c56f2"
|
resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.12.tgz#311bf0c6b63cd782f228a81abe146a2bfa9c56f2"
|
||||||
integrity sha512-n+IyV+nGz3+0q3/Yf1ra12KpCyi001bi4XFxSjbiWWjfqb52iTTtpGXmCCAOWWIAn9KEuFZKGqBERHmrtScZ3A==
|
integrity sha512-n+IyV+nGz3+0q3/Yf1ra12KpCyi001bi4XFxSjbiWWjfqb52iTTtpGXmCCAOWWIAn9KEuFZKGqBERHmrtScZ3A==
|
||||||
|
|
||||||
regenerator-runtime@^0.12.0:
|
|
||||||
version "0.12.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de"
|
|
||||||
integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==
|
|
||||||
|
|
||||||
regex-not@^1.0.0, regex-not@^1.0.2:
|
regex-not@^1.0.0, regex-not@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c"
|
resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c"
|
||||||
|
@ -7036,23 +7007,6 @@ repeat-string@^1.6.1:
|
||||||
resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
|
resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
|
||||||
integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc=
|
integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc=
|
||||||
|
|
||||||
request-promise-core@1.1.1:
|
|
||||||
version "1.1.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.1.tgz#3eee00b2c5aa83239cfb04c5700da36f81cd08b6"
|
|
||||||
integrity sha1-Pu4AssWqgyOc+wTFcA2jb4HNCLY=
|
|
||||||
dependencies:
|
|
||||||
lodash "^4.13.1"
|
|
||||||
|
|
||||||
request-promise@^4.2.2:
|
|
||||||
version "4.2.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/request-promise/-/request-promise-4.2.2.tgz#d1ea46d654a6ee4f8ee6a4fea1018c22911904b4"
|
|
||||||
integrity sha1-0epG1lSm7k+O5qT+oQGMIpEZBLQ=
|
|
||||||
dependencies:
|
|
||||||
bluebird "^3.5.0"
|
|
||||||
request-promise-core "1.1.1"
|
|
||||||
stealthy-require "^1.1.0"
|
|
||||||
tough-cookie ">=2.3.3"
|
|
||||||
|
|
||||||
request@^2.74.0, request@^2.81.0, request@^2.83.0, request@^2.87.0, request@^2.88.0:
|
request@^2.74.0, request@^2.81.0, request@^2.83.0, request@^2.87.0, request@^2.88.0:
|
||||||
version "2.88.0"
|
version "2.88.0"
|
||||||
resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
|
resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
|
||||||
|
@ -7970,11 +7924,6 @@ statuses@~1.4.0:
|
||||||
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087"
|
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087"
|
||||||
integrity sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==
|
integrity sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==
|
||||||
|
|
||||||
stealthy-require@^1.1.0:
|
|
||||||
version "1.1.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
|
|
||||||
integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=
|
|
||||||
|
|
||||||
stream-each@^1.1.0:
|
stream-each@^1.1.0:
|
||||||
version "1.2.3"
|
version "1.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.3.tgz#ebe27a0c389b04fbcc233642952e10731afa9bae"
|
resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.3.tgz#ebe27a0c389b04fbcc233642952e10731afa9bae"
|
||||||
|
@ -8467,15 +8416,6 @@ touch@^3.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
nopt "~1.0.10"
|
nopt "~1.0.10"
|
||||||
|
|
||||||
tough-cookie@>=2.3.3:
|
|
||||||
version "3.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-3.0.1.tgz#9df4f57e739c26930a018184887f4adb7dca73b2"
|
|
||||||
integrity sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==
|
|
||||||
dependencies:
|
|
||||||
ip-regex "^2.1.0"
|
|
||||||
psl "^1.1.28"
|
|
||||||
punycode "^2.1.1"
|
|
||||||
|
|
||||||
tough-cookie@~2.4.3:
|
tough-cookie@~2.4.3:
|
||||||
version "2.4.3"
|
version "2.4.3"
|
||||||
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781"
|
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781"
|
||||||
|
|
Loading…
Reference in New Issue