fix(extension): support cross-origin media frames

This commit is contained in:
KoalaDev
2026-08-17 16:49:53 +02:00
parent 77bdf21405
commit 082b69f509
31 changed files with 2858 additions and 263 deletions
+307 -6
View File
@@ -27,16 +27,51 @@ async function selectTargetTab(context, extensionId, pageUrl) {
async function sendServerCommand(context, extensionId, tabId, action, payload) {
return withExtensionPage(context, extensionId, page => page.evaluate(async ({ tabId, action, payload }) => {
return chrome.tabs.sendMessage(tabId, {
type: 'SERVER_COMMAND',
return chrome.runtime.sendMessage({
type: 'CONTENT_EVENT',
action,
payload,
actionTimestamp: Date.now(),
commandSenderId: 'e2e'
payload: payload || {},
expectedTabId: tabId
});
}, { tabId, action, payload }));
}
async function sendServerCommandBurst(context, extensionId, tabId, commands) {
return withExtensionPage(context, extensionId, page => page.evaluate(async ({ tabId, commands }) => {
return Promise.all(commands.map(({ action, payload }) => chrome.runtime.sendMessage({
type: 'CONTENT_EVENT',
action,
payload: payload || {},
expectedTabId: tabId
})));
}, { tabId, commands }));
}
async function getExtensionState(context, extensionId, message) {
return withExtensionPage(context, extensionId, page => page.evaluate(
request => chrome.runtime.sendMessage(request),
message
));
}
async function getFrameMonitorState(context, extensionId, pageUrl, frameUrlPart) {
return withExtensionPage(context, extensionId, page => page.evaluate(async ({ pageUrl, frameUrlPart }) => {
const [tab] = await chrome.tabs.query({ url: pageUrl });
if (!tab) throw new Error(`no tab matched ${pageUrl}`);
const frames = await chrome.webNavigation.getAllFrames({ tabId: tab.id });
const frame = frames.find(candidate => candidate.url.includes(frameUrlPart));
if (!frame) throw new Error(`no frame matched ${frameUrlPart}`);
const target = frame.documentId
? { tabId: tab.id, documentIds: [frame.documentId] }
: { tabId: tab.id, frameIds: [frame.frameId] };
const [result] = await chrome.scripting.executeScript({
target,
func: () => typeof window.__koalaMediaFrameMonitorCleanup
});
return result?.result;
}, { pageUrl, frameUrlPart }));
}
test('injects into the target tab and attaches to a same-origin frame player', async ({ context, extensionId, baseURL }) => {
const url = `${baseURL}/pages/iframe-player.html`;
const page = await context.newPage();
@@ -67,7 +102,8 @@ test('applies remote play, pause and seek to the framed player', async ({ contex
return video ? video.dataset.koalaAttached : null;
})).toBe('true');
await sendServerCommand(context, extensionId, tabId, 'play');
const playResponse = await sendServerCommand(context, extensionId, tabId, 'play');
expect(playResponse).toMatchObject({ status: 'ok_solo' });
await expect.poll(() => page.evaluate(FRAMED_VIDEO_PAUSED), { message: 'remote play should start playback' }).toBe(false);
await sendServerCommand(context, extensionId, tabId, 'pause');
@@ -159,6 +195,271 @@ test('re-attaches when a nested player frame swaps its document', async ({ conte
).toBe('true');
});
test('moves local event listeners after a CSS-only player switch', async ({ context, extensionId, baseURL }) => {
const url = `${baseURL}/pages/player-css-switch.html`;
const page = await context.newPage();
await page.goto(url);
await page.waitForFunction(() => window.__fixtureReady === true);
await selectTargetTab(context, extensionId, url);
await expect.poll(() => page.locator('#first').getAttribute('data-koala-attached')).toBe('true');
await page.waitForTimeout(250);
const beforeBurst = await getExtensionState(context, extensionId, { type: 'GET_STATUS' });
await page.evaluate(() => {
const first = document.getElementById('first');
first.dispatchEvent(new window.Event('play'));
first.dispatchEvent(new window.Event('pause'));
window.switchPlayer();
});
await expect.poll(async () => {
const status = await getExtensionState(context, extensionId, { type: 'GET_STATUS' });
return status.lastActionState.action === 'play'
&& status.lastActionState.timestamp > beforeBurst.lastActionState.timestamp;
}).toBe(true);
const leading = await getExtensionState(context, extensionId, { type: 'GET_STATUS' });
await expect.poll(
() => page.locator('#second').getAttribute('data-koala-attached'),
{ message: 'attribute-only visibility changes must move the active controller' }
).toBe('true');
expect(await page.locator('#first').getAttribute('data-koala-attached')).toBeNull();
const afterSwitch = await getExtensionState(context, extensionId, { type: 'GET_STATUS' });
expect(afterSwitch.lastActionState.action).toBe('play');
expect(afterSwitch.lastActionState.timestamp).toBe(leading.lastActionState.timestamp);
const before = await getExtensionState(context, extensionId, { type: 'GET_STATUS' });
await page.locator('#first').evaluate(video => video.dispatchEvent(new window.Event('play')));
await page.waitForTimeout(250);
const afterStale = await getExtensionState(context, extensionId, { type: 'GET_STATUS' });
expect(afterStale.lastActionState.timestamp).toBe(before.lastActionState.timestamp);
await page.locator('#second').evaluate(video => video.dispatchEvent(new window.Event('play')));
await expect.poll(async () => {
const status = await getExtensionState(context, extensionId, { type: 'GET_STATUS' });
return status.lastActionState.timestamp;
}).toBeGreaterThan(afterStale.lastActionState.timestamp);
});
test('re-elects after a wrapper-only change inside an unselected cross-origin frame', async ({ context, extensionId, baseURL }) => {
const url = `${baseURL}/pages/cross-origin-internal-switching.html`;
const page = await context.newPage();
await page.goto(url);
await page.waitForFunction(() => window.__fixtureReady === true);
const first = page.frames().find(frame => frame.url().includes('slot=first'));
const second = page.frames().find(frame => frame.url().includes('slot=second'));
await selectTargetTab(context, extensionId, url);
await expect.poll(() => first.locator('video').getAttribute('data-koala-attached')).toBe('true');
expect(await second.locator('video').getAttribute('data-koala-attached')).toBeNull();
await page.evaluate(() => window.switchInternalPlayer());
await expect.poll(
() => second.locator('video').getAttribute('data-koala-attached'),
{ message: 'an unselected frame must announce its internally-visible player' }
).toBe('true');
expect(await first.locator('video').getAttribute('data-koala-attached')).toBeNull();
});
test('targets a visible nested cross-origin player and keeps top-page debug context', async ({ context, extensionId, baseURL }) => {
const url = `${baseURL}/pages/cross-origin-nested.html`;
const page = await context.newPage();
await page.goto(url);
await page.waitForFunction(() => window.__fixtureReady === true);
const visiblePlayer = () => page.frames().find(frame => frame.url().includes('/frames/player-frame.html?visible=1'));
await expect.poll(async () => visiblePlayer()?.locator('video').getAttribute('src')).toContain('player-480p-12s.mp4');
const { tabId, response } = await selectTargetTab(context, extensionId, url);
expect(response).toMatchObject({ status: 'ok' });
expect(response.frameId).toBeGreaterThan(0);
await expect.poll(() => visiblePlayer()?.locator('video').getAttribute('data-koala-attached')).toBe('true');
const hiddenPlayer = page.frames().find(frame => frame.url().includes('/frames/player-frame-2.html?hidden=1'));
expect(await hiddenPlayer.locator('video').getAttribute('data-koala-attached')).toBeNull();
const playResponse = await sendServerCommand(context, extensionId, tabId, 'play');
expect(playResponse).toMatchObject({ status: 'ok_solo' });
await expect.poll(() => visiblePlayer().locator('video').evaluate(video => video.paused)).toBe(false);
await sendServerCommand(context, extensionId, tabId, 'pause');
await expect.poll(() => visiblePlayer().locator('video').evaluate(video => video.paused)).toBe(true);
await sendServerCommand(context, extensionId, tabId, 'seek', { targetTime: 6 });
await expect.poll(() => visiblePlayer().locator('video').evaluate(video => video.currentTime)).toBeGreaterThan(5);
await sendServerCommand(context, extensionId, tabId, 'pause');
await sendServerCommandBurst(context, extensionId, tabId, [
{ action: 'seek', payload: { targetTime: 8 } },
{ action: 'play', payload: { currentTime: 8 } }
]);
await expect.poll(() => visiblePlayer().locator('video').evaluate(video => ({
paused: video.paused,
currentTime: video.currentTime
}))).toMatchObject({ paused: false, currentTime: expect.any(Number) });
await expect.poll(() => visiblePlayer().locator('video').evaluate(video => video.currentTime)).toBeGreaterThan(7);
const state = await getExtensionState(context, extensionId, { type: 'GET_VIDEO_STATE', tabId });
expect(state).toMatchObject({
found: true,
url,
pageTitle: 'Nested cross-origin player',
frameOrigin: new URL(baseURL.replace('localhost', '127.0.0.1')).origin,
inIframe: true
});
});
test('re-elects the visible cross-origin player after an iframe switch', async ({ context, extensionId, baseURL }) => {
const url = `${baseURL}/pages/cross-origin-switching.html`;
const page = await context.newPage();
await page.goto(url);
await page.waitForFunction(() => window.__fixtureReady === true);
const first = page.frames().find(frame => frame.url().includes('/frames/player-frame.html?slot=first'));
const second = page.frames().find(frame => frame.url().includes('/frames/player-frame-2.html?slot=second'));
const { tabId, response } = await selectTargetTab(context, extensionId, url);
expect(response).toMatchObject({ status: 'ok' });
const firstFrameId = response.frameId;
await expect.poll(() => first.locator('video').getAttribute('data-koala-attached')).toBe('true');
await page.evaluate(() => window.switchPlayer());
await expect.poll(() => second.locator('video').getAttribute('data-koala-attached')).toBe('true');
await expect.poll(async () => {
const status = await getExtensionState(context, extensionId, { type: 'GET_STATUS' });
return status.targetFrameId;
}).not.toBe(firstFrameId);
const playResponse = await sendServerCommand(context, extensionId, tabId, 'play');
expect(playResponse).toMatchObject({ status: 'ok_solo' });
await expect.poll(() => second.locator('video').evaluate(video => video.paused)).toBe(false);
expect(await first.locator('video').evaluate(video => video.paused)).toBe(true);
});
test('keeps commands flowing during continuous player-frame geometry changes', async ({ context, extensionId, baseURL }) => {
const url = `${baseURL}/pages/cross-origin-switching.html`;
const page = await context.newPage();
await page.goto(url);
await page.waitForFunction(() => window.__fixtureReady === true);
const first = page.frames().find(frame => frame.url().includes('/frames/player-frame.html?slot=first'));
const { tabId, response } = await selectTargetTab(context, extensionId, url);
expect(response).toMatchObject({ status: 'ok' });
await expect.poll(() => first.locator('video').getAttribute('data-koala-attached')).toBe('true');
await page.evaluate(() => window.startGeometryChurn());
try {
await page.waitForTimeout(300);
await sendServerCommand(context, extensionId, tabId, 'play', { currentTime: 1 });
await expect.poll(
() => first.locator('video').evaluate(video => video.paused),
{ timeout: 3000, message: 'bounded refresh passes must not starve commands' }
).toBe(false);
} finally {
await page.evaluate(() => window.stopGeometryChurn());
}
});
test('deactivates media monitors in child frames after a target-tab switch', async ({ context, extensionId, baseURL }) => {
const firstUrl = `${baseURL}/pages/cross-origin-nested.html`;
const secondUrl = `${baseURL}/pages/simple-player.html`;
const firstPage = await context.newPage();
const secondPage = await context.newPage();
await firstPage.goto(firstUrl);
await firstPage.waitForFunction(() => window.__fixtureReady === true);
await secondPage.goto(secondUrl);
await secondPage.waitForFunction(() => window.__fixtureReady === true);
await selectTargetTab(context, extensionId, firstUrl);
await expect.poll(() => getFrameMonitorState(
context,
extensionId,
firstUrl,
'/frames/player-frame.html?visible=1'
)).toBe('function');
await selectTargetTab(context, extensionId, secondUrl);
await expect.poll(
() => getFrameMonitorState(
context,
extensionId,
firstUrl,
'/frames/player-frame.html?visible=1'
),
{ message: 'child-frame monitor should be destroyed with the old target tab' }
).toBe('undefined');
});
test('re-attaches after a selected cross-origin frame navigates', async ({ context, extensionId, baseURL }) => {
const url = `${baseURL}/pages/cross-origin-reloading.html`;
const page = await context.newPage();
await page.goto(url);
await page.waitForFunction(() => window.__fixtureReady === true);
const first = page.frames().find(frame => frame.url().includes('generation=first'));
const { tabId, response } = await selectTargetTab(context, extensionId, url);
expect(response).toMatchObject({ status: 'ok' });
await expect.poll(() => first.locator('video').getAttribute('data-koala-attached')).toBe('true');
const firstDocumentId = (await getExtensionState(context, extensionId, { type: 'GET_STATUS' })).targetDocumentId;
await page.evaluate(() => window.reloadPlayer());
await expect.poll(() => page.frames().find(frame => frame.url().includes('generation=second'))?.locator('video').getAttribute('data-koala-attached')).toBe('true');
await expect.poll(async () => {
const status = await getExtensionState(context, extensionId, { type: 'GET_STATUS' });
return status.targetDocumentId;
}).not.toBe(firstDocumentId);
const state = await getExtensionState(context, extensionId, { type: 'GET_VIDEO_STATE', tabId });
expect(state).toMatchObject({ found: true, inIframe: true });
});
test('discovers a video inserted late inside a cross-origin frame', async ({ context, extensionId, baseURL }) => {
const url = `${baseURL}/pages/cross-origin-late.html`;
const page = await context.newPage();
await page.goto(url);
await page.waitForFunction(() => window.__fixtureReady === true);
const lateFrame = page.frames().find(frame => frame.url().includes('/frames/late-player-frame.html'));
const { response } = await selectTargetTab(context, extensionId, url);
expect(response).toMatchObject({ status: 'ok', hasVideo: false });
await expect.poll(
() => lateFrame.locator('#late-player').getAttribute('data-koala-attached'),
{ timeout: 12_000, message: 'late cross-origin video should trigger target re-election' }
).toBe('true');
await expect.poll(async () => {
const status = await getExtensionState(context, extensionId, { type: 'GET_STATUS' });
return status.targetHasVideo;
}).toBe(true);
});
test('rejects a cross-origin player hidden three frame levels deep', async ({ context, extensionId, baseURL }) => {
const url = `${baseURL}/pages/deep-hidden-cross-origin.html`;
const page = await context.newPage();
await page.goto(url);
await page.waitForFunction(() => window.__fixtureReady === true);
const hiddenFrame = page.frames().find(frame => frame.url().includes('deep=hidden'));
await expect.poll(() => hiddenFrame?.locator('video').getAttribute('src')).toContain('player-1080p-30s.mp4');
const { response } = await selectTargetTab(context, extensionId, url);
expect(response).toMatchObject({ status: 'ok', frameId: 0, hasVideo: false });
expect(await hiddenFrame.locator('video').getAttribute('data-koala-attached')).toBeNull();
});
test('rejects a player inside a hidden same-origin frame', async ({ context, extensionId, baseURL }) => {
const url = `${baseURL}/pages/hidden-same-origin.html`;
const page = await context.newPage();
await page.goto(url);
await page.waitForFunction(() => window.__fixtureReady === true);
const hiddenFrame = page.frames().find(frame => frame.url().includes('hidden=same-origin'));
await expect.poll(() => hiddenFrame?.locator('video').getAttribute('src')).toContain('player-480p-12s.mp4');
const { response } = await selectTargetTab(context, extensionId, url);
expect(response).toMatchObject({ status: 'ok', frameId: 0, hasVideo: false });
expect(await hiddenFrame.locator('video').getAttribute('data-koala-attached')).toBeNull();
});
test('rejects a hidden cross-origin player after its iframe URL redirects', async ({ context, extensionId, baseURL }) => {
const url = `${baseURL}/pages/hidden-redirect-cross-origin.html`;
const page = await context.newPage();
await page.goto(url);
await page.waitForFunction(() => window.__fixtureReady === true);
const redirectedFrame = page.frames().find(frame => frame.url().includes('redirected=hidden'));
await expect.poll(() => redirectedFrame?.locator('video').getAttribute('src')).toContain('player-1080p-30s.mp4');
const { response } = await selectTargetTab(context, extensionId, url);
expect(response).toMatchObject({ status: 'ok', frameId: 0, hasVideo: false });
expect(await redirectedFrame.locator('video').getAttribute('data-koala-attached')).toBeNull();
});
function FRAMED_VIDEO_PAUSED() {
return document.querySelector('iframe').contentDocument.querySelector('video').paused;
}
+7
View File
@@ -20,6 +20,13 @@ const TYPES = {
const server = http.createServer((req, res) => {
const requested = decodeURIComponent(new URL(req.url, 'http://localhost').pathname);
if (requested === '/redirect/hidden-player') {
res.writeHead(302, {
Location: `http://127.0.0.1:${port}/pages/frames/player-frame-2.html?redirected=hidden`,
'Cache-Control': 'no-store'
}).end();
return;
}
const filePath = path.resolve(root, `.${requested}`);
if (!filePath.startsWith(root + path.sep)) {
@@ -0,0 +1,32 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Cross-origin internal wrapper switch</title>
<style>
iframe { width: 640px; height: 360px; border: 0; display: block; }
</style>
</head>
<body>
<iframe id="first" allow="autoplay; fullscreen"></iframe>
<iframe id="second" allow="autoplay; fullscreen"></iframe>
<script>
const playerOrigin = `http://127.0.0.1:${location.port}`;
const first = document.getElementById('first');
const second = document.getElementById('second');
first.src = `${playerOrigin}/pages/frames/wrapper-switch-player.html?slot=first&visible=1`;
second.src = `${playerOrigin}/pages/frames/wrapper-switch-player.html?slot=second&visible=0`;
let loaded = 0;
const ready = () => {
loaded++;
if (loaded === 2) window.__fixtureReady = true;
};
first.addEventListener('load', ready);
second.addEventListener('load', ready);
window.switchInternalPlayer = () => {
first.contentWindow.postMessage('koala-hide', playerOrigin);
second.contentWindow.postMessage('koala-show', playerOrigin);
};
</script>
</body>
</html>
@@ -0,0 +1,10 @@
<!doctype html>
<meta charset="utf-8">
<title>Late cross-origin player</title>
<style>body { margin: 0; } iframe { border: 0; }</style>
<iframe id="player" width="860" height="490" allowfullscreen></iframe>
<script>
const player = document.getElementById('player');
player.src = `http://127.0.0.1:${location.port}/pages/frames/late-player-frame.html`;
player.addEventListener('load', () => { window.__fixtureReady = true; }, { once: true });
</script>
@@ -0,0 +1,18 @@
<!doctype html>
<meta charset="utf-8">
<title>Nested cross-origin player</title>
<style>
body { margin: 0; }
iframe { border: 0; }
#hidden-player { display: none; }
</style>
<iframe id="wrapper" width="870" height="500" src="frames/cross-origin-wrapper.html" allowfullscreen></iframe>
<iframe id="hidden-player" width="870" height="500" allowfullscreen></iframe>
<script>
const hidden = document.getElementById('hidden-player');
hidden.src = `http://127.0.0.1:${location.port}/pages/frames/player-frame-2.html?hidden=1`;
Promise.all([
new Promise(resolve => document.getElementById('wrapper').addEventListener('load', resolve, { once: true })),
new Promise(resolve => hidden.addEventListener('load', resolve, { once: true }))
]).then(() => { window.__fixtureReady = true; });
</script>
@@ -0,0 +1,13 @@
<!doctype html>
<meta charset="utf-8">
<title>Reloading cross-origin player</title>
<style>body { margin: 0; } iframe { border: 0; }</style>
<iframe id="player" width="860" height="490" allowfullscreen></iframe>
<script>
const player = document.getElementById('player');
player.src = `http://127.0.0.1:${location.port}/pages/frames/player-frame.html?generation=first`;
player.addEventListener('load', () => { window.__fixtureReady = true; }, { once: true });
window.reloadPlayer = () => {
player.src = `http://127.0.0.1:${location.port}/pages/frames/player-frame-2.html?generation=second`;
};
</script>
@@ -0,0 +1,38 @@
<!doctype html>
<meta charset="utf-8">
<title>Switching cross-origin players</title>
<style>
body { margin: 0; }
iframe { border: 0; }
#second { display: none; }
</style>
<iframe id="first" width="860" height="490" allowfullscreen></iframe>
<iframe id="second" width="860" height="490" allowfullscreen></iframe>
<script>
const first = document.getElementById('first');
const second = document.getElementById('second');
first.src = `http://127.0.0.1:${location.port}/pages/frames/player-frame.html?slot=first`;
second.src = `http://127.0.0.1:${location.port}/pages/frames/player-frame-2.html?slot=second`;
Promise.all([
new Promise(resolve => first.addEventListener('load', resolve, { once: true })),
new Promise(resolve => second.addEventListener('load', resolve, { once: true }))
]).then(() => { window.__fixtureReady = true; });
window.switchPlayer = () => {
first.style.visibility = 'hidden';
first.style.position = 'absolute';
first.style.left = '-10000px';
second.style.display = 'block';
};
let geometryChurn = null;
window.startGeometryChurn = () => {
let wide = false;
geometryChurn = setInterval(() => {
wide = !wide;
first.style.width = wide ? '840px' : '800px';
}, 80);
};
window.stopGeometryChurn = () => {
clearInterval(geometryChurn);
geometryChurn = null;
};
</script>
@@ -0,0 +1,10 @@
<!doctype html>
<meta charset="utf-8">
<title>Deep hidden cross-origin player</title>
<style>body { margin: 0; } iframe { border: 0; } #hidden-wrapper { visibility: hidden; }</style>
<iframe id="hidden-wrapper" width="870" height="500" src="frames/deep-wrapper-1.html" allowfullscreen></iframe>
<script>
document.getElementById('hidden-wrapper').addEventListener('load', () => {
window.__fixtureReady = true;
}, { once: true });
</script>
@@ -0,0 +1,9 @@
<!doctype html>
<meta charset="utf-8">
<title>Cross-origin wrapper</title>
<style>body { margin: 0; } iframe { border: 0; }</style>
<iframe id="inner-player" width="860" height="490" allowfullscreen></iframe>
<script>
document.getElementById('inner-player').src =
`http://127.0.0.1:${location.port}/pages/frames/player-frame.html?visible=1`;
</script>
@@ -0,0 +1,5 @@
<!doctype html>
<meta charset="utf-8">
<title>Deep wrapper 1</title>
<style>body { margin: 0; } iframe { border: 0; }</style>
<iframe width="860" height="490" src="deep-wrapper-2.html" allowfullscreen></iframe>
@@ -0,0 +1,9 @@
<!doctype html>
<meta charset="utf-8">
<title>Deep wrapper 2</title>
<style>body { margin: 0; } iframe { border: 0; }</style>
<iframe id="deep-player" width="850" height="480" allowfullscreen></iframe>
<script>
document.getElementById('deep-player').src =
`http://127.0.0.1:${location.port}/pages/frames/player-frame-2.html?deep=hidden`;
</script>
@@ -0,0 +1,15 @@
<!doctype html>
<meta charset="utf-8">
<title>Late player frame</title>
<style>body { margin: 0; }</style>
<script>
setTimeout(() => {
const video = document.createElement('video');
video.id = 'late-player';
video.width = 854;
video.height = 480;
video.controls = true;
video.src = '../../media/player-480p-12s.mp4';
document.body.append(video);
}, 8000);
</script>
@@ -0,0 +1,30 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Wrapper-switch player</title>
<style>
html, body { margin: 0; }
#wrapper.hidden { visibility: hidden; }
video { width: 640px; height: 360px; }
</style>
</head>
<body>
<div id="wrapper">
<video id="player" controls preload="auto"></video>
</div>
<script>
const params = new URLSearchParams(location.search);
const wrapper = document.getElementById('wrapper');
const player = document.getElementById('player');
wrapper.classList.toggle('hidden', params.get('visible') !== '1');
player.src = params.get('slot') === 'second'
? '../../media/player-1080p-30s.mp4'
: '../../media/player-480p-12s.mp4';
window.addEventListener('message', event => {
if (event.data === 'koala-show') wrapper.classList.remove('hidden');
if (event.data === 'koala-hide') wrapper.classList.add('hidden');
});
</script>
</body>
</html>
@@ -0,0 +1,10 @@
<!doctype html>
<meta charset="utf-8">
<title>Hidden redirected cross-origin player</title>
<style>body { margin: 0; } iframe { border: 0; visibility: hidden; }</style>
<iframe id="player" width="860" height="490" allowfullscreen></iframe>
<script>
const player = document.getElementById('player');
player.src = `http://localhost:${location.port}/redirect/hidden-player`;
player.addEventListener('load', () => { window.__fixtureReady = true; }, { once: true });
</script>
@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Hidden same-origin frame</title>
<style>
iframe { width: 800px; height: 450px; border: 0; visibility: hidden; }
</style>
</head>
<body>
<iframe src="frames/player-frame.html?hidden=same-origin" allow="autoplay; fullscreen"></iframe>
<script>window.__fixtureReady = true;</script>
</body>
</html>
@@ -0,0 +1,22 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>CSS player switch</title>
<style>
video { width: 640px; height: 360px; }
#second { visibility: hidden; }
</style>
</head>
<body>
<video id="first" controls preload="auto" src="../media/player-480p-12s.mp4"></video>
<video id="second" controls preload="auto" src="../media/player-1080p-30s.mp4"></video>
<script>
window.switchPlayer = () => {
document.getElementById('first').style.visibility = 'hidden';
document.getElementById('second').style.visibility = 'visible';
};
window.__fixtureReady = true;
</script>
</body>
</html>