test(hls): [PKG-HLS-02] cover released contracts and real SDK playback

This commit is contained in:
Harvey Zhao committed 2026-09-11 23:53:16 +08:00
1 parent cbbdc03a7c
commit 33f6d19b77
55 files changed
+8678 -14

No files matched your search

+6
View File
@@ -15,6 +15,12 @@ no retries, a loopback server on 8084, and no existing dev server. Set `ARTPLAYE
to use another free port. The server builds core and chapter from current JS/TS entries in memory;
it does not modify dist or docs outputs. Restart the test command after source changes.
`hls-control.spec.js` separately builds HLS control and verifies a pinned Hls.js 1.5.17 archive.
Its local playlists/segments are generated fixtures with checked hashes. Windows WebKit lacks MSE:
one explicit capability/error-cleanup test covers that limitation and its 14 playback cases are
skipped with a reason. Chromium/Firefox run the full four-combination HLS matrix. This is not
Safari playback approval; see [HLS validation](../../refactor/hls-validation.md) for evidence and open gates.
`playback.spec.js` tests published/current core with current chapter, real decoded frame colors,
media time advancing, pause, seek, source change to the existing docs video, destroy and real
HTTP failure. These are initial smoke contracts, not full chapter or lifecycle coverage.
+236
View File
@@ -0,0 +1,236 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import process from 'node:process'
import { ensureArchive, hash, readMember } from '../../refactor/scripts/releases.mjs'
import { compilePackage } from '../helpers/load.js'
import { expect, test } from './fixtures.js'
let sdkCode
let publishedCode
let sourceCode
let evidence
let media
test.afterEach(async ({ page }, testInfo) => {
const state = await page.evaluate(() => {
const art = window.art
return {
supported: window.Hls?.isSupported(),
mediaSource: typeof window.MediaSource,
managedMediaSource: typeof window.ManagedMediaSource,
video: art?.video.outerHTML,
readyState: art?.video.readyState,
networkState: art?.video.networkState,
sdkErrors: window.sdkErrors,
sdkEvents: window.sdkEvents,
levels: art?.hls.levels.map(level => ({ height: level.height, codec: level.videoCodec })),
tracks: art?.hls.audioTracks.map(track => ({ id: track.id, name: track.name })),
}
}).catch(error => ({ diagnosticError: error.message }))
await testInfo.attach('hls-state', { contentType: 'application/json', body: JSON.stringify(state, null, 2) })
})
test.beforeAll(async () => {
const sdk = JSON.parse(fs.readFileSync(new URL('../../refactor/baselines/hls-sdk.json', import.meta.url)))
const plugin = JSON.parse(fs.readFileSync(new URL('../../refactor/baselines/hls-control-release.json', import.meta.url)))
async function code(release, member) {
const bytes = readMember(await ensureArchive(release), member)
assert.equal(hash(bytes), release.files[member])
return bytes.toString()
}
sdkCode = await code(sdk.release, 'package/dist/hls.min.js')
publishedCode = await code(plugin.release, 'package/dist/artplayer-plugin-hls-control.js')
// This task exercises source builds. Installed artifacts are a separate HLS-06 gate.
assert(!process.env.ARTPLAYER_BROWSER_ARTIFACTS, 'HLS source fixture cannot masquerade as an installed plugin')
sourceCode = await compilePackage('artplayer-plugin-hls-control', 'umd')
const manifest = JSON.parse(fs.readFileSync(new URL('./media/hls/manifest.json', import.meta.url)))
media = new Map()
for (const [name, expected] of Object.entries(manifest.files)) {
assert(/^[\w.-]+$/.test(name))
const bytes = fs.readFileSync(new URL(`./media/hls/${name}`, import.meta.url))
assert.equal(hash(bytes), expected.sha256)
assert.equal(bytes.length, expected.bytes)
media.set(name, bytes)
}
evidence = { sdk: sdk.release, plugin: plugin.release, sourceSHA256: hash(sourceCode), media: manifest, limitations: ['SDK worker disabled', 'local deterministic media', 'source candidate, not installed package', 'no physical device certification'] }
})
async function openHls(page, core, plugin, testInfo, manifest = 'master.m3u8') {
await testInfo.attach('hls-inputs', { contentType: 'application/json', body: JSON.stringify(evidence) })
await page.route('**/hls-fixture/**', async (route) => {
const name = new URL(route.request().url()).pathname.split('/').at(-1)
const bytes = media.get(name)
await route.fulfill({ status: bytes ? 200 : 503, body: bytes || 'Intentional HLS failure', contentType: name.endsWith('.m3u8') ? 'application/vnd.apple.mpegurl' : 'video/mp2t' })
})
await page.goto(`/test/player.html?core=${core}`)
await page.addScriptTag({ content: sdkCode })
await page.addScriptTag({ content: plugin === 'published' ? publishedCode : sourceCode })
await page.evaluate((manifest) => {
window.sdkErrors = []
window.sdkEvents = []
window.destroyedEngines = 0
window.art = new window.Artplayer({
container: '.player',
url: `/hls-fixture/${manifest}`,
muted: true,
setting: true,
plugins: [window.artplayerPluginHlsControl({ quality: { control: true, setting: true }, audio: { control: true, setting: true } })],
customType: { m3u8(video, url, art) {
if (art.hls)
art.hls.destroy()
const hls = new window.Hls({ enableWorker: false, manifestLoadingMaxRetry: 0 })
art.hls = hls
hls.on(window.Hls.Events.ERROR, (_, data) => window.sdkErrors.push({ type: data.type, details: data.details, fatal: data.fatal }))
for (const name of ['LEVEL_SWITCHED', 'AUDIO_TRACK_SWITCHED', 'MANIFEST_PARSED'])
hls.on(window.Hls.Events[name], (_, data) => window.sdkEvents.push({ name, level: data.level, id: data.id }))
hls.on(window.Hls.Events.DESTROYING, () => window.destroyedEngines++)
hls.loadSource(url)
hls.attachMedia(video)
} },
})
const art = window.art
art.on('destroy', () => art.hls.destroy())
document.querySelector('#play').onclick = () => art.play()
document.querySelector('#pause').onclick = () => art.pause()
}, manifest)
}
test('SDK MSE availability is explicit; unavailable hosts report failure and clean up', async ({ page, browserName }, testInfo) => {
await openHls(page, 'candidate', 'published', testInfo)
const capability = await page.evaluate(() => ({ supported: window.Hls.isSupported(), mse: typeof window.MediaSource, managed: typeof window.ManagedMediaSource }))
if (process.platform === 'win32' && browserName === 'webkit') {
expect(capability).toEqual({ supported: false, mse: 'undefined', managed: 'undefined' })
await expect.poll(() => page.evaluate(() => window.sdkErrors.filter(error => error.fatal))).toEqual([{ type: 'mediaError', details: 'manifestIncompatibleCodecsError', fatal: true }])
expect(await page.evaluate(() => window.art.isReady)).toBe(false)
}
else {
expect(capability.supported).toBe(true)
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
}
expect(await page.evaluate(() => {
const sdk = window.art.hls
window.art.destroy()
return { media: sdk.media, destroyed: window.destroyedEngines }
})).toEqual({ media: null, destroyed: 1 })
})
test.describe('MSE playback matrix', () => {
// Verified by the separate capability test above. Safari/MSE coverage remains HLS-05 work.
test.skip(({ browserName }) => process.platform === 'win32' && browserName === 'webkit', 'Bundled Windows WebKit exposes neither MediaSource nor ManagedMediaSource; this is an unverified playback environment, not compatibility approval')
for (const core of ['published', 'candidate']) {
for (const plugin of ['published', 'candidate']) {
test(`${core} core / ${plugin} HLS: real decoding, manual quality, Auto and audio`, async ({ page }, testInfo) => {
await openHls(page, core, plugin, testInfo)
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
await page.click('#play')
await expect.poll(() => page.evaluate(() => window.art.video.currentTime)).toBeGreaterThan(0.3)
const initial = await page.evaluate(() => ({
supported: window.Hls.isSupported(),
version: window.Hls.version,
levels: window.art.hls.levels.map(level => level.height),
tracks: window.art.hls.audioTracks.map(track => ({ id: track.id, name: track.name })),
frames: window.art.video.getVideoPlaybackQuality().totalVideoFrames,
}))
expect(initial).toMatchObject({ supported: true, version: '1.5.17', levels: [90, 180], tracks: [{ id: 0, name: 'English' }, { id: 1, name: 'French' }] })
expect(initial.frames).toBeGreaterThan(0)
await page.evaluate(() => {
window.art.controls.show = true
})
const quality = page.locator('.art-control-hls-quality')
await quality.hover()
await quality.locator('.art-selector-item').filter({ hasText: /^90P$/ }).click()
await expect.poll(() => page.evaluate(() => ({ automatic: window.art.hls.autoLevelEnabled, level: window.art.hls.currentLevel, height: window.art.video.videoHeight }))).toEqual({ automatic: false, level: 0, height: 90 })
await quality.hover()
await quality.locator('.art-selector-item').filter({ hasText: /^180P$/ }).click()
await expect.poll(() => page.evaluate(() => ({ manual: window.art.hls.autoLevelEnabled, level: window.art.hls.currentLevel, height: window.art.video.videoHeight }))).toEqual({ manual: false, level: 1, height: 180 })
await quality.hover()
await quality.locator('.art-selector-item').filter({ hasText: /^Auto$/ }).click()
await expect.poll(() => page.evaluate(() => window.art.hls.autoLevelEnabled)).toBe(true)
await expect.poll(() => page.evaluate(() => window.art.hls.currentLevel)).toBeGreaterThanOrEqual(0)
await page.evaluate(() => {
window.art.controls.show = true
})
const audio = page.locator('.art-control-hls-audio')
await audio.hover()
await audio.locator('.art-selector-item').filter({ hasText: /^French$/ }).click()
await expect.poll(() => page.evaluate(() => window.sdkEvents.some(event => event.name === 'AUDIO_TRACK_SWITCHED' && event.id === 1))).toBe(true)
expect(await page.evaluate(() => window.art.hls.audioTrack)).toBe(1)
await page.click('#pause')
await expect.poll(() => page.evaluate(() => window.art.video.paused)).toBe(true)
await page.evaluate(() => {
window.art.seek = 5
})
await expect.poll(() => page.evaluate(() => window.art.video.currentTime)).toBeCloseTo(5, 1)
await page.click('#play')
await expect.poll(() => page.evaluate(() => window.art.video.currentTime)).toBeGreaterThan(5.2)
expect(await page.evaluate(() => window.sdkErrors.filter(error => error.fatal))).toEqual([])
const result = await page.evaluate(() => {
const video = window.art.video
const canvas = document.createElement('canvas')
canvas.width = 16
canvas.height = 16
const ctx = canvas.getContext('2d')
ctx.drawImage(video, 0, 0, 16, 16)
const pixels = ctx.getImageData(0, 0, 16, 16).data
const colors = new Set()
for (let index = 0; index < pixels.length; index += 4)
colors.add(`${pixels[index]},${pixels[index + 1]},${pixels[index + 2]}`)
const sdk = window.art.hls
window.art.destroy()
return { colors: colors.size, media: sdk.media, destroyed: window.destroyedEngines, children: document.querySelector('.player').childElementCount }
})
expect(result.colors).toBeGreaterThan(8)
expect(result).toMatchObject({ media: null, destroyed: 1, children: 0 })
})
test(`${core} core / ${plugin} HLS: failed manifest and actual SDK teardown`, async ({ page }, testInfo) => {
await openHls(page, core, plugin, testInfo, 'missing.m3u8')
await expect.poll(() => page.evaluate(() => window.sdkErrors.filter(error => error.fatal))).toEqual([{ type: 'networkError', details: 'manifestLoadError', fatal: true }])
expect(await page.locator('.art-control-hls-quality').count()).toBe(0)
await page.evaluate(() => window.art.switchUrl('/hls-fixture/master.m3u8'))
await expect.poll(() => page.evaluate(() => window.art.hls.levels.length)).toBe(2)
await page.click('#play')
await expect.poll(() => page.evaluate(() => window.art.video.currentTime)).toBeGreaterThan(0.2)
expect(await page.evaluate(() => {
const sdk = window.art.hls
window.art.destroy()
return { destroyed: window.destroyedEngines, media: sdk.media }
})).toEqual({ destroyed: 2, media: null })
})
test(`${core} core / ${plugin} HLS: source switch drops SDK audio topology`, async ({ page }, testInfo) => {
await openHls(page, core, plugin, testInfo)
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
await page.click('#play')
await expect.poll(() => page.evaluate(() => window.art.video.currentTime)).toBeGreaterThan(0.2)
await page.evaluate(() => {
window.previousHls = window.art.hls
return window.art.switchUrl('/hls-fixture/video-only.m3u8')
})
await expect.poll(() => page.evaluate(() => ({ levels: window.art.hls.levels.length, tracks: window.art.hls.audioTracks.length, height: window.art.video.videoHeight }))).toEqual({ levels: 1, tracks: 0, height: 90 })
expect(await page.evaluate(() => ({ oldMedia: window.previousHls.media, replaced: window.previousHls !== window.art.hls, destroyed: window.destroyedEngines }))).toEqual({ oldMedia: null, replaced: true, destroyed: 1 })
if (plugin === 'published') {
// Real SDK corroboration of the frozen historical empty-topology defect.
await expect(page.locator('.art-control-hls-audio')).toHaveCount(1)
}
expect(await page.evaluate(() => window.sdkErrors.filter(error => error.fatal))).toEqual([])
await page.evaluate(() => window.art.destroy())
expect(await page.evaluate(() => window.destroyedEngines)).toBe(2)
})
}
test(`${core} core / published HLS: real Auto playback loses Auto label on update`, async ({ page }, testInfo) => {
await openHls(page, core, 'published', testInfo)
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
await page.click('#play')
await expect.poll(() => page.evaluate(() => window.art.video.currentTime)).toBeGreaterThan(0.2)
expect(await page.evaluate(() => window.art.hls.autoLevelEnabled)).toBe(true)
const height = await page.evaluate(() => {
const art = window.art
art.plugins.artplayerPluginHlsControl.update()
return art.hls.levels[art.hls.currentLevel].height
})
await expect(page.locator('.art-control-hls-quality .art-selector-value')).toHaveText(`${height}P`)
expect(await page.evaluate(() => window.art.hls.autoLevelEnabled)).toBe(true)
await page.evaluate(() => window.art.destroy())
})
}
})
+16
View File
@@ -0,0 +1,16 @@
# Deterministic HLS fixtures
Generated by `node scripts/generate-hls-fixture.mjs`, using FFmpeg recorded in manifest.json.
No downloaded film or audio: testsrc2 supplies moving patterns, sine supplies 440/880 Hz audio.
The 12-second master offers 160x90 and 320x180 AVC video, English/French AAC audio, and a
separate video-only master for topology changes. Each media playlist is VOD with 2-second segments.
Segments use `.mpegts` so source tools cannot mistake MPEG transport streams for TypeScript.
The HLS browser fixture serves them as video/mp2t. File contents and byte counts are checked
against manifest.json before browser use. Tests need neither FFmpeg nor a remote media service.
To change the media, generate into a new directory, for example
`node scripts/generate-hls-fixture.mjs refactor/.cache/hls-media-next`; inspect the output and
update the media, manifest and browser evidence together. The script refuses existing directories.
Set ARTPLAYER_FFMPEG to a particular FFmpeg executable if it is not on PATH. Re-encoding with
another FFmpeg version is not expected to preserve bytes; do not overwrite hashes to silence drift.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+20
View File
@@ -0,0 +1,20 @@
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:2
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:2.005333,
english-00.mpegts
#EXTINF:2.005333,
english-01.mpegts
#EXTINF:2.005333,
english-02.mpegts
#EXTINF:1.984000,
english-03.mpegts
#EXTINF:2.005333,
english-04.mpegts
#EXTINF:2.005333,
english-05.mpegts
#EXTINF:0.021333,
english-06.mpegts
#EXT-X-ENDLIST
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+20
View File
@@ -0,0 +1,20 @@
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:2
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:2.005333,
french-00.mpegts
#EXTINF:2.005333,
french-01.mpegts
#EXTINF:2.005333,
french-02.mpegts
#EXTINF:1.984000,
french-03.mpegts
#EXTINF:2.005333,
french-04.mpegts
#EXTINF:2.005333,
french-05.mpegts
#EXTINF:0.021333,
french-06.mpegts
#EXT-X-ENDLIST
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+18
View File
@@ -0,0 +1,18 @@
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:2
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:2.000000,
high-00.mpegts
#EXTINF:2.000000,
high-01.mpegts
#EXTINF:2.000000,
high-02.mpegts
#EXTINF:2.000000,
high-03.mpegts
#EXTINF:2.000000,
high-04.mpegts
#EXTINF:2.000000,
high-05.mpegts
#EXT-X-ENDLIST
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+18
View File
@@ -0,0 +1,18 @@
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:2
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:2.000000,
low-00.mpegts
#EXTINF:2.000000,
low-01.mpegts
#EXTINF:2.000000,
low-02.mpegts
#EXTINF:2.000000,
low-03.mpegts
#EXTINF:2.000000,
low-04.mpegts
#EXTINF:2.000000,
low-05.mpegts
#EXT-X-ENDLIST
+269
View File
@@ -0,0 +1,269 @@
{
"kind": "generated-hls-media",
"version": "ffmpeg version 2025-11-12-git-6cdd2cbe32-full_build-www.gyan.dev Copyright (c) 2000-2025 the FFmpeg developers",
"duration": 12,
"commands": [
[
"-hide_banner",
"-loglevel",
"error",
"-n",
"-f",
"lavfi",
"-i",
"testsrc2=size=160x90:rate=24",
"-t",
"12",
"-an",
"-c:v",
"libx264",
"-pix_fmt",
"yuv420p",
"-profile:v",
"baseline",
"-level:v",
"3.0",
"-g",
"48",
"-keyint_min",
"48",
"-sc_threshold",
"0",
"-crf",
"28",
"-f",
"hls",
"-hls_time",
"2",
"-hls_playlist_type",
"vod",
"-hls_segment_filename",
"low-%02d.mpegts",
"low.m3u8"
],
[
"-hide_banner",
"-loglevel",
"error",
"-n",
"-f",
"lavfi",
"-i",
"testsrc2=size=320x180:rate=24",
"-t",
"12",
"-an",
"-c:v",
"libx264",
"-pix_fmt",
"yuv420p",
"-profile:v",
"baseline",
"-level:v",
"3.0",
"-g",
"48",
"-keyint_min",
"48",
"-sc_threshold",
"0",
"-crf",
"28",
"-f",
"hls",
"-hls_time",
"2",
"-hls_playlist_type",
"vod",
"-hls_segment_filename",
"high-%02d.mpegts",
"high.m3u8"
],
[
"-hide_banner",
"-loglevel",
"error",
"-n",
"-f",
"lavfi",
"-i",
"sine=frequency=440:sample_rate=48000",
"-t",
"12",
"-vn",
"-c:a",
"aac",
"-b:a",
"48k",
"-ac",
"2",
"-f",
"hls",
"-hls_time",
"2",
"-hls_playlist_type",
"vod",
"-hls_segment_filename",
"english-%02d.mpegts",
"english.m3u8"
],
[
"-hide_banner",
"-loglevel",
"error",
"-n",
"-f",
"lavfi",
"-i",
"sine=frequency=880:sample_rate=48000",
"-t",
"12",
"-vn",
"-c:a",
"aac",
"-b:a",
"48k",
"-ac",
"2",
"-f",
"hls",
"-hls_time",
"2",
"-hls_playlist_type",
"vod",
"-hls_segment_filename",
"french-%02d.mpegts",
"french.m3u8"
]
],
"files": {
"english-00.mpegts": {
"sha256": "cc599f7c270385e85c28f710a34c5bd90b09d4d27adb3b1e6af3de65584b8207",
"bytes": 13912
},
"english-01.mpegts": {
"sha256": "01b17c28defc8a8ebf4978c5236eb759d80521a1cb840898095c35dac420cec5",
"bytes": 14100
},
"english-02.mpegts": {
"sha256": "90751b8b5abedd6b2f2e4eb83ccd609098ec7f2901163aaebf977cb9a11c46eb",
"bytes": 14100
},
"english-03.mpegts": {
"sha256": "9b1d86b7b261df66dbdace4ef1e846796cd710ffb72b22fc69532c1fe7d1c55e",
"bytes": 13912
},
"english-04.mpegts": {
"sha256": "dbfddd4f997942dd0b912f4c8bdbc5c11ee2a3fc51ac67df48d3aed2756715b9",
"bytes": 14100
},
"english-05.mpegts": {
"sha256": "9b53790d4f50f172852b7d4ff51bdd4bedd39e4c7a1efbfe734edf7b9a68a533",
"bytes": 14100
},
"english-06.mpegts": {
"sha256": "50baaa5e9fe7ca49b22c4d0817c44103b3edc763aee7f1b17d22f796d4189d4a",
"bytes": 752
},
"english.m3u8": {
"sha256": "5aae9d117d458f65f7febae68555ea4e48b2055b4cfb123fb919e385a1edda21",
"bytes": 365
},
"french-00.mpegts": {
"sha256": "7278acd094bc7caeeeea424a2c11ac4285fc45a8becb6c8ec55068f295ae35cd",
"bytes": 13912
},
"french-01.mpegts": {
"sha256": "e9464d109e36a53d7f151442b544d47ce79324bbaa805e0a98f171621c9c7a13",
"bytes": 14100
},
"french-02.mpegts": {
"sha256": "3ac48cab6aac942950b59f79ac6bd950dcae425f14821b83b1b5bb72d6815eb0",
"bytes": 14100
},
"french-03.mpegts": {
"sha256": "906f2d8b1f29bb7d57240b145563bab975d8f7dfff78cebd1505b6d2a3feb43b",
"bytes": 13912
},
"french-04.mpegts": {
"sha256": "cbf82876118d41af254bdef51f6cd08d842e8c6edf53250667fad2b8088e3601",
"bytes": 14100
},
"french-05.mpegts": {
"sha256": "a91e564ec54d845837217c45b8e25b08fe208f2b5f97562f513baabd515427d9",
"bytes": 14100
},
"french-06.mpegts": {
"sha256": "2c8edc11b06bacf1e62c4db67594ce187aa93e0241012011ebcf587b64ccff46",
"bytes": 752
},
"french.m3u8": {
"sha256": "6f7fb3eacb944597fa3105cb6d708eac3a8dc413e1706a11b6fb0e631214634e",
"bytes": 358
},
"high-00.mpegts": {
"sha256": "c5a8faac85b0594818255117696dce43fda6b363df40e2f3ffc453bd236cc833",
"bytes": 45308
},
"high-01.mpegts": {
"sha256": "5714640c3f17d86c56036453dc17d45f871af63b461318c43527af3482f8661c",
"bytes": 52264
},
"high-02.mpegts": {
"sha256": "fea1cbfb199bc867a322c7a646d43594a1d0d1d23fa0bcb6e770f2669b489440",
"bytes": 48128
},
"high-03.mpegts": {
"sha256": "4a74a28e7ad6bc626358e32365a0b07df1da82c92753e28032983e22404958b5",
"bytes": 54144
},
"high-04.mpegts": {
"sha256": "d04889b3b996d3e79c12bc297f9577690d3ae4f4172cdc0484b584954dab789c",
"bytes": 47376
},
"high-05.mpegts": {
"sha256": "36ee0c64b8c3637d0e2282c611e9250650ac493440095fc2efbe9de1c54add15",
"bytes": 48128
},
"high.m3u8": {
"sha256": "672c0e8b99242be05b16fc747a00715b2c25579b1ecc1623c0579fa34f0cb11d",
"bytes": 311
},
"low-00.mpegts": {
"sha256": "cbd62f5f4641cbe188a15640139bea393b5927fce600ff7513ef9ade9b33fb62",
"bytes": 28952
},
"low-01.mpegts": {
"sha256": "f7821fba38c63f5d80daef21cdd3430880bdc9fd5f5adfa939ed6481106c2507",
"bytes": 32900
},
"low-02.mpegts": {
"sha256": "3dc93f9f0fd065678b01fc6c568c12350e9ab0b324d1e0fa0e9f77787da7e7d2",
"bytes": 30268
},
"low-03.mpegts": {
"sha256": "d94c8913f7fde98161550041287529b90a58d537d6083d4382540c5fce0b430b",
"bytes": 32336
},
"low-04.mpegts": {
"sha256": "11596c59b547c2ff23bbba1f70e15f061a3e87bbdc1f4f59daa9b5279a6003af",
"bytes": 29704
},
"low-05.mpegts": {
"sha256": "5c6ec25a69fdad73a23cfbc34f0f3c6d8abe44592650c92fa9110a38d6bd9e45",
"bytes": 29516
},
"low.m3u8": {
"sha256": "404a8d868ed2af3598269aee89caba0563c2805dac223fe4c00c9a9eda73554f",
"bytes": 305
},
"master.m3u8": {
"sha256": "f226bd38f3c76714ada2b5a6866ad1cb39b041023e4a18d971d175f26f917e38",
"bytes": 470
},
"video-only.m3u8": {
"sha256": "3d7bb9069f6218191dc65745389bf1ab0ba6d5ed66cbd14e2cf30211913da3fc",
"bytes": 108
}
}
}
+8
View File
@@ -0,0 +1,8 @@
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",NAME="English",LANGUAGE="en",DEFAULT=YES,AUTOSELECT=YES,URI="english.m3u8"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",NAME="French",LANGUAGE="fr",DEFAULT=NO,AUTOSELECT=YES,URI="french.m3u8"
#EXT-X-STREAM-INF:BANDWIDTH=200000,RESOLUTION=160x90,CODECS="avc1.42c01e,mp4a.40.2",AUDIO="audio"
low.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=500000,RESOLUTION=320x180,CODECS="avc1.42c01e,mp4a.40.2",AUDIO="audio"
high.m3u8
+4
View File
@@ -0,0 +1,4 @@
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-STREAM-INF:BANDWIDTH=200000,RESOLUTION=160x90,CODECS="avc1.42c01e"
low.m3u8