test(ci): [CI-01] verify installed MediaBunny browser matrix

This commit is contained in:
Harvey Zhao committed 2026-09-15 03:07:20 +08:00
1 parent 8379ac7bbf
commit 25a0c6303d
22 files changed
+5228 -19

No files matched your search

+6 -1
View File
@@ -5,6 +5,11 @@ import { mbCandidate } from '../helpers/mediabunny.js'
import { expect, test } from './fixtures.js'
const implementation = await mbCandidate()
test.beforeEach(async ({ browserName }, testInfo) => {
await testInfo.attach('mediabunny-available-inputs', { contentType: 'application/json', body: JSON.stringify([implementation].map(item => ({ browserName, name: item.name, sha256: hash(item.code), provenance: item.provenance || { kind: item.name } }))) })
})
const manifest = JSON.parse(fs.readFileSync(new URL('./media/hls/manifest.json', import.meta.url)))
const hls = new Map(Object.entries(manifest.files).map(([name, expected]) => {
const bytes = fs.readFileSync(new URL(`./media/hls/${name}`, import.meta.url))
@@ -248,7 +253,7 @@ for (const scenario of ['playback', 'buffer-pause', 'buffer-rate', 'resume-pause
expect(await page.evaluate(() => window.mbCanvas.engine.audio.queuedNodes.size)).toBe(0)
}
const state = await page.evaluate(() => ({ context: window.mbContext?.state, nodes: window.mbNodes.map(record => ({ stops: record.stops, disconnects: record.disconnects, starts: record.starts })), error: window.mbCanvas.error, events: window.mbEvents }))
await testInfo.attach('mediabunny-audio', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, sha256: hash(implementation.code), scenario, capabilities, outcome, result, state, scope: 'Native AudioContext, BufferSourceNodes and SDK decode. Delayed resume/buffer delivery is controlled. AV samples compare decoded canvas timestamps to the audio clock over short 1x/2x playback; acoustic output, long-run drift and physical devices remain MB-09.' }) })
await testInfo.attach('mediabunny-audio', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, provenance: implementation.provenance, sha256: hash(implementation.code), scenario, capabilities, outcome, result, state, scope: 'Native AudioContext, BufferSourceNodes and SDK decode. Delayed resume/buffer delivery is controlled. AV samples compare decoded canvas timestamps to the audio clock over short 1x/2x playback; acoustic output, long-run drift and physical devices remain MB-09.' }) })
}
})
}
+6 -1
View File
@@ -5,6 +5,11 @@ import { mbCandidate } from '../helpers/mediabunny.js'
import { expect, test } from './fixtures.js'
const implementation = await mbCandidate()
test.beforeEach(async ({ browserName }, testInfo) => {
await testInfo.attach('mediabunny-available-inputs', { contentType: 'application/json', body: JSON.stringify([implementation].map(item => ({ browserName, name: item.name, sha256: hash(item.code), provenance: item.provenance || { kind: item.name } }))) })
})
const manifest = JSON.parse(fs.readFileSync(new URL('./media/hls/manifest.json', import.meta.url)))
const hls = new Map(Object.entries(manifest.files).map(([name, expected]) => {
const bytes = fs.readFileSync(new URL(`./media/hls/${name}`, import.meta.url))
@@ -111,7 +116,7 @@ for (const core of ['published', 'candidate']) {
}
finally {
await page.evaluate(() => window.art?.destroy(false))
await testInfo.attach('mediabunny-capability', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, sha256: hash(implementation.code), core, scenario, capabilities, outcome, result, scope: 'Actual SDK parsing, Canvas, Web Audio and two cores. canDecode=false is deliberately controlled for selected tracks; partial cases play the remaining native decoder. Windows WebKit missing APIs is a separate control, not playback.' }) })
await testInfo.attach('mediabunny-capability', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, provenance: implementation.provenance, sha256: hash(implementation.code), core, scenario, capabilities, outcome, result, scope: 'Actual SDK parsing, Canvas, Web Audio and two cores. canDecode=false is deliberately controlled for selected tracks; partial cases play the remaining native decoder. Windows WebKit missing APIs is a separate control, not playback.' }) })
}
})
}
+7 -2
View File
@@ -6,6 +6,11 @@ import { mbCandidate } from '../helpers/mediabunny.js'
import { expect, test } from './fixtures.js'
const implementation = await mbCandidate()
test.beforeEach(async ({ browserName }, testInfo) => {
await testInfo.attach('mediabunny-available-inputs', { contentType: 'application/json', body: JSON.stringify([implementation].map(item => ({ browserName, name: item.name, sha256: hash(item.code), provenance: item.provenance || { kind: item.name } }))) })
})
const dpip = await dpipCandidate()
const manifest = JSON.parse(fs.readFileSync(new URL('./media/hls/manifest.json', import.meta.url)))
const hls = new Map(Object.entries(manifest.files).map(([name, expected]) => {
@@ -25,7 +30,7 @@ for (const core of ['published', 'candidate']) {
await page.goto(`/test/player.html?core=${core}`)
const supported = await page.evaluate(() => typeof window.documentPictureInPicture?.requestWindow === 'function')
if (!supported) {
await testInfo.attach('mediabunny-dpip', { contentType: 'application/json', body: JSON.stringify({ core, media, ending, implementation: implementation.name, sha256: hash(implementation.code), dpipSha256: hash(dpip.code), outcome: 'native-document-pip-unavailable', browserName, supported }) })
await testInfo.attach('mediabunny-dpip', { contentType: 'application/json', body: JSON.stringify({ core, media, ending, implementation: implementation.name, provenance: implementation.provenance, sha256: hash(implementation.code), dpipSha256: hash(dpip.code), dpipProvenance: dpip.provenance || { kind: dpip.name, sha256: hash(dpip.code) }, outcome: 'native-document-pip-unavailable', browserName, supported }) })
return
}
for (const [code, global] of [[implementation.code, 'mbFactory'], [dpip.code, 'dpipFactory']]) {
@@ -98,7 +103,7 @@ for (const core of ['published', 'candidate']) {
}
result = await page.evaluate(() => ({ events: window.mbPipEvents, placeholders: document.querySelectorAll('.artplayer-document-pip-placeholder').length, nativeWindowClosed: !window.documentPictureInPicture.window || window.documentPictureInPicture.window.closed }))
expect(result).toEqual({ events: ending === 'destroy' ? [true] : [true, false], placeholders: 0, nativeWindowClosed: true })
await testInfo.attach('mediabunny-dpip', { contentType: 'application/json', body: JSON.stringify({ core, media, ending, implementation: implementation.name, sha256: hash(implementation.code), dpipSha256: hash(dpip.code), outcome: 'native-document-pip-canvas-playback', before, result, scope: 'Actual requestWindow from a click, no iframe or API replacement; native Canvas decoding and window lifecycle. HLS includes native audio, and close cases change quality/audio inside PiP.' }) })
await testInfo.attach('mediabunny-dpip', { contentType: 'application/json', body: JSON.stringify({ core, media, ending, implementation: implementation.name, provenance: implementation.provenance, sha256: hash(implementation.code), dpipSha256: hash(dpip.code), dpipProvenance: dpip.provenance || { kind: dpip.name, sha256: hash(dpip.code) }, outcome: 'native-document-pip-canvas-playback', before, result, scope: 'Actual requestWindow from a click, no iframe or API replacement; native Canvas decoding and window lifecycle. HLS includes native audio, and close cases change quality/audio inside PiP.' }) })
}
finally {
await page.evaluate(() => {
+6 -1
View File
@@ -3,6 +3,11 @@ import { mbCandidate } from '../helpers/mediabunny.js'
import { expect, test } from './fixtures.js'
const implementation = await mbCandidate()
test.beforeEach(async ({ browserName }, testInfo) => {
await testInfo.attach('mediabunny-available-inputs', { contentType: 'application/json', body: JSON.stringify([implementation].map(item => ({ browserName, name: item.name, sha256: hash(item.code), provenance: item.provenance || { kind: item.name } }))) })
})
for (const core of ['published', 'candidate']) {
for (const scenario of ['native-cleanup', 'replacement']) {
test(`MediaBunny ${implementation.name}: ${core} core native Canvas entry ${scenario}`, async ({ page }, testInfo) => {
@@ -66,7 +71,7 @@ for (const core of ['published', 'candidate']) {
}, scenario)
expect(result.before).toEqual({ identity: true, context: true, width: 640, clickCount: 1, source: null, attribute: 'native-only.mp4', volume: 0.35, style: '100%', maybe: 'maybe' })
expect(result.cleanup).toEqual({ released: [true, true, true], destroyed: true, styleAfter: '43px', alias: true })
await testInfo.attach('mediabunny-entry', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, sha256: hash(implementation.code), core, scenario, result, scope: 'Actual browser Canvas descriptors and published/candidate core lifecycle. Empty source deliberately excludes decoding; existing media suites cover playback separately.' }) })
await testInfo.attach('mediabunny-entry', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, provenance: implementation.provenance, sha256: hash(implementation.code), core, scenario, result, scope: 'Actual browser Canvas descriptors and published/candidate core lifecycle. Empty source deliberately excludes decoding; existing media suites cover playback separately.' }) })
})
}
}
+6 -1
View File
@@ -5,6 +5,11 @@ import { mbCandidate } from '../helpers/mediabunny.js'
import { expect, test } from './fixtures.js'
const implementation = await mbCandidate()
test.beforeEach(async ({ browserName }, testInfo) => {
await testInfo.attach('mediabunny-available-inputs', { contentType: 'application/json', body: JSON.stringify([implementation].map(item => ({ browserName, name: item.name, sha256: hash(item.code), provenance: item.provenance || { kind: item.name } }))) })
})
const manifest = JSON.parse(fs.readFileSync(new URL('./media/hls/manifest.json', import.meta.url)))
const media = new Map(Object.entries(manifest.files).map(([name, expected]) => {
const bytes = fs.readFileSync(new URL(`./media/hls/${name}`, import.meta.url))
@@ -196,7 +201,7 @@ for (const core of ['published', 'candidate']) {
return { events: window.mbEvents, errors: window.mbErrors, writes: window.mbWrites, error: window.mbCanvas.error, destroyed: window.mbCanvas.engine.destroyed }
})
await expect.poll(() => page.evaluate(() => window.mbContext?.state || 'not-created')).toMatch(/^(?:closed|not-created)$/)
await testInfo.attach('mediabunny-hls-ui', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, sha256: hash(implementation.code), core, scenario, capabilities, outcome, result, state, scope: 'Actual old/candidate core, SDK HLS parsing and native decoding; rendered control/settings clicks, topology and controlled state/query delays. Not installed-package or long-run/device acceptance.' }) })
await testInfo.attach('mediabunny-hls-ui', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, provenance: implementation.provenance, sha256: hash(implementation.code), core, scenario, capabilities, outcome, result, state, scope: 'Actual old/candidate core, SDK HLS parsing and native decoding; rendered control/settings clicks, topology and controlled state/query delays. Input provenance identifies source or installed bundle; not long-run/device acceptance.' }) })
}
})
}
+6 -1
View File
@@ -6,6 +6,11 @@ import { tracklessMp4 } from '../helpers/trackless-mp4.js'
import { expect, test } from './fixtures.js'
const implementations = await mbBrowserImplementations()
test.beforeEach(async ({ browserName }, testInfo) => {
await testInfo.attach('mediabunny-available-inputs', { contentType: 'application/json', body: JSON.stringify(implementations.map(item => ({ browserName, name: item.name, sha256: hash(item.code), provenance: item.provenance || { kind: item.name } }))) })
})
const manifest = JSON.parse(fs.readFileSync(new URL('./media/hls/manifest.json', import.meta.url)))
const hls = new Map(Object.entries(manifest.files).map(([name, expected]) => {
const bytes = fs.readFileSync(new URL(`./media/hls/${name}`, import.meta.url))
@@ -237,7 +242,7 @@ for (const implementation of implementations) {
await expect.poll(() => page.evaluate(() => window.mbContext?.state || 'not-created')).toMatch(/^(?:closed|not-created)$/)
expect(await page.evaluate(() => window.mbPendingFrames.size)).toBe(0)
const cleanup = await page.evaluate(() => ({ audioContext: window.mbContext?.state || 'not-created', pendingFrames: window.mbPendingFrames.size, streamCancelled: window.mbStreamCancelled || false }))
await testInfo.attach('mediabunny-native-input', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, sha256: hash(implementation.code), input, capabilities, outcome, state, cleanup, hlsManifest: input === 'hls' ? manifest : null, tracklessFixture: input === 'trackless' ? { parentSha256: hash(pattern), sha256: hash(trackless) } : null, scope: 'Identified proxy with native browser media and controlled ArtPlayer host using actual core utilities/config; frame timestamps sampled against the SDK audio clock, not acoustic/long-run AV-sync acceptance. Cleanup checks settled playback, not pending-operation races. Candidate trackless rejection is checked separately before decoder setup; this is not full release acceptance.' }) })
await testInfo.attach('mediabunny-native-input', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, provenance: implementation.provenance, sha256: hash(implementation.code), input, capabilities, outcome, state, cleanup, hlsManifest: input === 'hls' ? manifest : null, tracklessFixture: input === 'trackless' ? { parentSha256: hash(pattern), sha256: hash(trackless) } : null, scope: 'Identified proxy with native browser media and controlled ArtPlayer host using actual core utilities/config; frame timestamps sampled against the SDK audio clock, not acoustic/long-run AV-sync acceptance. Cleanup checks settled playback, not pending-operation races. Candidate trackless rejection is checked separately before decoder setup; this is not full release acceptance.' }) })
}
})
}
+6 -1
View File
@@ -3,6 +3,11 @@ import { mbCandidate } from '../helpers/mediabunny.js'
import { expect, test } from './fixtures.js'
const implementation = await mbCandidate()
test.beforeEach(async ({ browserName }, testInfo) => {
await testInfo.attach('mediabunny-available-inputs', { contentType: 'application/json', body: JSON.stringify([implementation].map(item => ({ browserName, name: item.name, sha256: hash(item.code), provenance: item.provenance || { kind: item.name } }))) })
})
for (const ending of ['destroy', 'source', 'timeout']) {
test(`MediaBunny ${implementation.name}: native pending stream cancelled by ${ending}`, async ({ page }, testInfo) => {
await page.goto('/test/player.html?core=published')
@@ -57,7 +62,7 @@ for (const ending of ['destroy', 'source', 'timeout']) {
await expect.poll(() => page.evaluate(() => window.mbReplacement.locked)).toBe(true)
await page.evaluate(() => window.mbHost.emit('destroy'))
await expect.poll(() => page.evaluate(() => window.mbCancelled)).toEqual(ending === 'source' ? ['old', 'new'] : ['old'])
await testInfo.attach('mediabunny-load-cancellation', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, sha256: hash(implementation.code), ending, state, cancellations: await page.evaluate(() => window.mbCancelled), scope: 'Real browser ReadableStream and actual SDK Input disposal before first media bytes; real timeout; controlled ArtPlayer host; does not assert decoder, late frame or audio-node cleanup.' }) })
await testInfo.attach('mediabunny-load-cancellation', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, provenance: implementation.provenance, sha256: hash(implementation.code), ending, state, cancellations: await page.evaluate(() => window.mbCancelled), scope: 'Real browser ReadableStream and actual SDK Input disposal before first media bytes; real timeout; controlled ArtPlayer host; does not assert decoder, late frame or audio-node cleanup.' }) })
}
finally {
await page.evaluate(() => window.mbHost.emit('destroy'))
+6 -1
View File
@@ -3,6 +3,11 @@ import { mbCandidate } from '../helpers/mediabunny.js'
import { expect, test } from './fixtures.js'
const implementation = await mbCandidate()
test.beforeEach(async ({ browserName }, testInfo) => {
await testInfo.attach('mediabunny-available-inputs', { contentType: 'application/json', body: JSON.stringify([implementation].map(item => ({ browserName, name: item.name, sha256: hash(item.code), provenance: item.provenance || { kind: item.name } }))) })
})
for (const scenario of ['callback', 'destroy', 'independent']) {
test(`MediaBunny ${implementation.name}: native synthetic RAF ${scenario}`, async ({ page }, testInfo) => {
await page.goto('/test/player.html?core=published')
@@ -44,6 +49,6 @@ for (const scenario of ['callback', 'destroy', 'independent']) {
expect(result.idType).toBe('number')
expect(result.listeners).toBe(0)
expect(result.calls).toEqual(scenario === 'callback' ? [{ owner: 'first', time: 'number', mediaTime: 0, presentedFrames: 0 }] : scenario === 'independent' ? [{ owner: 'second' }] : [])
await testInfo.attach('mediabunny-shim-frames', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, sha256: hash(implementation.code), scenario, result, scope: 'Real browser requestAnimationFrame and actual proxy instances; synthetic metadata is preserved, no decoding or video frame accuracy is claimed.' }) })
await testInfo.attach('mediabunny-shim-frames', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, provenance: implementation.provenance, sha256: hash(implementation.code), scenario, result, scope: 'Real browser requestAnimationFrame and actual proxy instances; synthetic metadata is preserved, no decoding or video frame accuracy is claimed.' }) })
})
}
+6 -1
View File
@@ -3,6 +3,11 @@ import { mbCandidate } from '../helpers/mediabunny.js'
import { expect, test } from './fixtures.js'
const implementation = await mbCandidate()
test.beforeEach(async ({ browserName }, testInfo) => {
await testInfo.attach('mediabunny-available-inputs', { contentType: 'application/json', body: JSON.stringify([implementation].map(item => ({ browserName, name: item.name, sha256: hash(item.code), provenance: item.provenance || { kind: item.name } }))) })
})
for (const scenario of ['pause-frame', 'seek-source', 'seek-newer', 'timeupdate-pause', 'seek-pool']) {
test(`MediaBunny ${implementation.name}: native video ownership ${scenario}`, async ({ page, browserName }, testInfo) => {
await page.goto('/test/player.html?core=published')
@@ -212,7 +217,7 @@ for (const scenario of ['pause-frame', 'seek-source', 'seek-newer', 'timeupdate-
expect(state.frames).toBe(0)
await expect.poll(() => page.evaluate(() => window.mbCleanupContext?.state || 'not-created')).toMatch(/^(?:closed|not-created)$/)
state.audioContext = await page.evaluate(() => window.mbCleanupContext?.state || 'not-created')
await testInfo.attach('mediabunny-video-ownership', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, sha256: hash(implementation.code), scenario, outcome, result, state, scope: 'Actual SDK MP4 decoding and browser clock/RAF; frame delivery or two raw VideoSample deliveries before CanvasSink conversion control interleaving. Source/seek ownership, pause and timeupdate reentry verified; not long-run AV sync or a mock codec.' }) })
await testInfo.attach('mediabunny-video-ownership', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, provenance: implementation.provenance, sha256: hash(implementation.code), scenario, outcome, result, state, scope: 'Actual SDK MP4 decoding and browser clock/RAF; frame delivery or two raw VideoSample deliveries before CanvasSink conversion control interleaving. Source/seek ownership, pause and timeupdate reentry verified; not long-run AV sync or a mock codec.' }) })
}
})
}
+5 -1
View File
@@ -4,6 +4,10 @@ import { expect, test } from './fixtures.js'
const implementations = await mbBrowserImplementations()
test.beforeEach(async ({ browserName }, testInfo) => {
await testInfo.attach('mediabunny-available-inputs', { contentType: 'application/json', body: JSON.stringify(implementations.map(item => ({ browserName, name: item.name, sha256: hash(item.code), provenance: item.provenance || { kind: item.name } }))) })
})
for (const implementation of implementations) {
test(`MediaBunny ${implementation.name}: native MP4 playback or exact Windows WebKit capability failure`, async ({ page, browserName }, testInfo) => {
await page.goto('/test/player.html?core=published')
@@ -65,7 +69,7 @@ for (const implementation of implementations) {
const canvas = window.art.video
return { events: window.mbEvents, actions: window.mbActions, ready: window.art.isReady, dimensions: [canvas.width, canvas.height, canvas.videoWidth, canvas.videoHeight], paused: canvas.paused, currentTime: canvas.currentTime, error: canvas.error, readyState: canvas.readyState, audioState: canvas.engine?.audio?.audioContext?.state }
})
await testInfo.attach('mediabunny-input', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, sha256: hash(implementation.code), capabilities, outcome, state, scope: 'Identified published or candidate proxy with actual core5.4.0; native MP4 pixels/clock or explicitly unsupported Windows WebKit APIs. No HLS or long-run AV-sync acceptance.' }) })
await testInfo.attach('mediabunny-input', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, provenance: implementation.provenance, sha256: hash(implementation.code), capabilities, outcome, state, scope: 'Identified published or candidate proxy with actual core5.4.0; native MP4 pixels/clock or explicitly unsupported Windows WebKit APIs. No HLS or long-run AV-sync acceptance.' }) })
await page.evaluate(() => window.art.destroy(false))
}
})