Skip to content

Commit 187f19e

Browse files
committed
demo: add dual pip demo
1 parent 85c23e7 commit 187f19e

3 files changed

Lines changed: 251 additions & 0 deletions

File tree

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<title>Cobalt Dual Picture-in-Picture Demo</title>
6+
<meta name="viewport" content="width=device-width, initial-scale=1">
7+
<style>
8+
body {
9+
background-color: #121212;
10+
color: #fff;
11+
font-family: sans-serif;
12+
margin: 0;
13+
overflow: hidden;
14+
width: 100vw;
15+
height: 100vh;
16+
}
17+
18+
.ui-layer {
19+
display: none;
20+
}
21+
22+
.video-container {
23+
position: absolute;
24+
top: 0;
25+
left: 0;
26+
width: 100%;
27+
height: 100%;
28+
}
29+
30+
.video-container {
31+
display: flex;
32+
gap: 40px; /* Precise visual gap cleanly maintained */
33+
align-items: center;
34+
justify-content: center;
35+
}
36+
37+
.video-wrapper {
38+
position: relative; /* Layout naturally via flexbox */
39+
width: 533px; /* Slightly larger nice 16:9 533x300 format */
40+
height: 300px;
41+
transition: all 0.5s ease;
42+
}
43+
44+
/* V1 is Texture mode so it natively supports Web overflow clipping beautifully */
45+
#v1-wrap {
46+
border-radius: 16px;
47+
overflow: hidden;
48+
}
49+
50+
/* V2 is Overlay mode so it must use stencil masking rather than overflow:hidden */
51+
#v2-wrap {
52+
border-radius: 16px;
53+
/* No overflow hidden here intentionally */
54+
}
55+
56+
.video-wrapper video {
57+
position: absolute;
58+
top: 0; left: 0;
59+
width: 100%;
60+
height: 100%;
61+
background: transparent;
62+
border: none;
63+
object-fit: cover;
64+
transition: all 0.5s ease;
65+
}
66+
67+
/* For V2 Hardware Overlay: Precise pixel overscan hiding 4.6px letterboxing safely */
68+
#video2 {
69+
top: -10px; left: -10px;
70+
width: calc(100% + 20px);
71+
height: calc(100% + 20px);
72+
}
73+
74+
/* The visual boundary stencil for V2 perfectly sealing the overscan */
75+
.overlay-mask {
76+
position: absolute;
77+
top: -24px; left: -24px;
78+
width: calc(100% + 48px);
79+
height: calc(100% + 48px);
80+
border: 24px solid #121212;
81+
border-radius: 40px; /* 40px outer minus 24px border = 16px exact inner hole radius */
82+
box-sizing: border-box;
83+
pointer-events: none;
84+
z-index: 2;
85+
transition: all 0.5s ease;
86+
}
87+
88+
.pip-corner {
89+
position: fixed;
90+
width: 427px;
91+
height: 240px;
92+
right: 50px;
93+
bottom: 50px;
94+
z-index: 10;
95+
transition: all 0.5s ease;
96+
}
97+
.fullscreen {
98+
border-radius: 0;
99+
position: fixed;
100+
top: 0; left: 0; bottom: 0; right: 0;
101+
width: 100vw; height: 100vh;
102+
margin: 0;
103+
z-index: 5;
104+
}
105+
.fullscreen .overlay-mask {
106+
opacity: 0;
107+
}
108+
.fullscreen #video2 {
109+
top: 0; left: 0; width: 100%; height: 100%;
110+
}
111+
</style>
112+
</head>
113+
<body>
114+
<!-- Keyboard UI Driven: No buttons -->
115+
116+
<!-- Container for videos -->
117+
<div class="video-container">
118+
<div id="v1-wrap" class="video-wrapper"><video id="video1" autoplay muted loop playsinline></video></div>
119+
<div id="v2-wrap" class="video-wrapper"><video id="video2" autoplay muted loop playsinline></video><div class="overlay-mask"></div></div>
120+
</div>
121+
122+
<script src="dual-pip-demo.js"></script>
123+
</body>
124+
</html>
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// Copyright 2026 The Cobalt Authors. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
const v1 = document.getElementById('video1');
16+
const v2 = document.getElementById('video2');
17+
const v1Wrapper = document.getElementById('v1-wrap');
18+
const v2Wrapper = document.getElementById('v2-wrap');
19+
20+
21+
let currentStage = 1;
22+
23+
function log(msg) {
24+
console.log('JS: ' + msg);
25+
}
26+
27+
// Use local file for PiP texture to ensure EGL stability, and Sintel for the overlay!
28+
const url1 = 'vp9-720p.webm';
29+
const url2 = 'https://storage.googleapis.com/ytlr-cert.appspot.com/test-materials/media/big-buck-bunny-vp9-480p-30fps.webm';
30+
31+
async function bootstrapVideo(videoObj, url, useTexture = false) {
32+
return new Promise(async (resolve) => {
33+
if (typeof MediaSource === 'undefined') { log('MSE not supported'); return resolve(); }
34+
const mediaSource = new MediaSource();
35+
mediaSource.addEventListener('sourceopen', async () => {
36+
try {
37+
const isMp4 = url.toLowerCase().includes('.mp4');
38+
const mimeType = isMp4 ? 'video/mp4; codecs="avc1.640028"' : 'video/webm; codecs="vp9"';
39+
const codecString = mimeType + (useTexture ? '; decode-to-texture=true' : '');
40+
const sourceBuffer = mediaSource.addSourceBuffer(codecString);
41+
// Safely fetch only the first 5MB of media to prevent JS Heap OOM
42+
// crashes when testing massive external videos on constrained TV memory!
43+
const response = await fetch(url, { headers: { 'Range': 'bytes=0-5000000' } });
44+
const data = await response.arrayBuffer();
45+
sourceBuffer.addEventListener('updateend', () => {
46+
if (!sourceBuffer.updating && mediaSource.readyState === 'open') {
47+
mediaSource.endOfStream();
48+
videoObj.play().catch(e => log('Play prevented: ' + e));
49+
resolve();
50+
}
51+
}, { once: true });
52+
sourceBuffer.appendBuffer(data);
53+
} catch (error) { log('Error loading MSE: ' + error); resolve(); }
54+
}, { once: true });
55+
56+
videoObj.src = URL.createObjectURL(mediaSource);
57+
videoObj.loop = true;
58+
});
59+
}
60+
61+
let isStage3 = false;
62+
let isTransitioning = false;
63+
64+
document.addEventListener('keydown', async (e) => {
65+
if (isTransitioning) return;
66+
isTransitioning = true;
67+
68+
try {
69+
if (!isStage3) {
70+
log('Stage 3: Native PiP for V1, Fullscreen for V2');
71+
72+
try {
73+
const pipPromise = v1.requestPictureInPicture();
74+
75+
setTimeout(() => {
76+
v2Wrapper.style.transition = 'none';
77+
v2Wrapper.classList.add('fullscreen');
78+
79+
// Restore transition shortly after jump is completed
80+
setTimeout(() => {
81+
v2Wrapper.style.transition = '';
82+
}, 100);
83+
}, 300);
84+
85+
await pipPromise;
86+
} catch (err) { log('Native PiP Error: ' + err.message); }
87+
88+
isStage3 = true;
89+
} else {
90+
log('Stage 2: Side-by-Side (Exiting PiP)');
91+
92+
v2Wrapper.classList.remove('fullscreen');
93+
try { await document.exitPictureInPicture(); } catch (err) {}
94+
95+
isStage3 = false;
96+
}
97+
} catch(e) { log(e.message); }
98+
99+
// Wait for the 500ms CSS transition to mathematically conclude before allowing another toggle
100+
setTimeout(() => { isTransitioning = false; }, 500);
101+
});
102+
103+
log('Bootstrapping decoders...');
104+
Promise.all([
105+
bootstrapVideo(v1, url1, true), // V1 Texture
106+
bootstrapVideo(v2, url2, false) // V2 Overlay
107+
]).then(() => {
108+
// Both decoders are now initialized.
109+
// Wait until V1 (the later one) actually fires its first frame.
110+
// When V1 starts rendering to its WebGL texture, Cobalt's compositor
111+
// does a deep structural redraw that historically wipes V2's overlay hole!
112+
const fixV2Hole = () => {
113+
// Force a minor width change to recalculate Cobalt's compositor safely.
114+
// (Do NOT use `transform`, as it breaks `position: fixed` inside WebKit/Cobalt!)
115+
v2.style.width = '105%';
116+
setTimeout(() => {
117+
v2.style.width = '104%';
118+
log('Ready!');
119+
}, 50);
120+
};
121+
122+
if (v1.readyState >= 3) {
123+
fixV2Hole();
124+
} else {
125+
v1.addEventListener('playing', fixV2Hole, { once: true });
126+
}
127+
});
363 KB
Binary file not shown.

0 commit comments

Comments
 (0)