mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-23 22:01:20 -08:00
test(hls): [PKG-HLS-02] cover released contracts and real SDK playback
This commit is contained in:
1 parent
cbbdc03a7c
commit
33f6d19b77
55 files changed
+8678
-14
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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())
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -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.
@@ -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.
@@ -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.
@@ -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.
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,4 @@
|
||||
#EXTM3U
|
||||
#EXT-X-VERSION:3
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=200000,RESOLUTION=160x90,CODECS="avc1.42c01e"
|
||||
low.m3u8
|
||||
Reference in new issue
Block a user