Files
feature-next-arch/assets/www/webrtc/playwebrtc.html
2026-01-18 20:21:14 +08:00

340 lines
9.1 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebRTC 前后视角 + PIP + 虚化背景(Apple TV 风格)</title>
<style>
body {
margin: 0;
background: #000;
overflow: hidden;
}
canvas {
display: block;
width: 100vw;
height: 100vh;
touch-action: none;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script src="jswebrtc.min.js"></script>
<script>
/* ================= 基础 ================= */
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
let video = null;
let player = null;
let drawing = false;
let streamUrl = 'webrtc://1.95.137.212/live/livestream/111';
/* ================= 配置 ================= */
const PIP_WIDTH_RATIO = 0.25;
const PIP_RADIUS_RATIO = 0.04;
/* ===== 虚化背景配置 ===== */
const BLUR_BG_ENABLED = true;
const BLUR_RADIUS_PX = 28;
const BLUR_DARK_ALPHA = 0.12; // 降低暗色强度,让过渡更自然
/* ================= PIP 状态 ================= */
function createPip(x, y) {
return { x, y, dragging:false, offsetX:0, offsetY:0, visible:true };
}
const pipState = {
left: createPip(10, 10),
right: createPip(0, 10),
active: null
};
/* ================= 主画面状态 ================= */
let mainView = 'front'; // front / back
/* ================= 工具函数 ================= */
function calcContain16by9(region) {
const r = 16 / 9;
let w = region.dw, h = region.dh;
if (w / h > r) w = h * r;
else h = w / r;
return { dx:(region.dw-w)/2, dy:(region.dh-h)/2, dw:w, dh:h };
}
function getPipSize() {
const w = canvas.width * PIP_WIDTH_RATIO;
return { w, h: w * 9 / 16 };
}
function hitTest(x, y, pip) {
const s = getPipSize();
return x >= pip.x && x <= pip.x + s.w &&
y >= pip.y && y <= pip.y + s.h;
}
function clamp(pip) {
const s = getPipSize();
pip.x = Math.max(0, Math.min(canvas.width - s.w, pip.x));
pip.y = Math.max(0, Math.min(canvas.height - s.h, pip.y));
}
/* ================= Canvas ================= */
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const s = getPipSize();
pipState.right.x = canvas.width - s.w - 10;
}
window.addEventListener('resize', resizeCanvas);
/* ================= 拖动 ================= */
canvas.addEventListener('pointerdown', e => {
const r = canvas.getBoundingClientRect();
const x = e.clientX - r.left;
const y = e.clientY - r.top;
for (const k of ['left','right']) {
const pip = pipState[k];
if (pip.visible && hitTest(x, y, pip)) {
pipState.active = pip;
pip.dragging = true;
pip.offsetX = x - pip.x;
pip.offsetY = y - pip.y;
canvas.setPointerCapture(e.pointerId);
break;
}
}
});
canvas.addEventListener('pointermove', e => {
const pip = pipState.active;
if (!pip || !pip.dragging) return;
const r = canvas.getBoundingClientRect();
pip.x = e.clientX - r.left - pip.offsetX;
pip.y = e.clientY - r.top - pip.offsetY;
clamp(pip);
});
canvas.addEventListener('pointerup', resetDrag);
canvas.addEventListener('pointercancel', resetDrag);
function resetDrag() {
if (pipState.active) pipState.active.dragging = false;
pipState.active = null;
}
/* ================= WebRTC ================= */
function createVideoElement() {
const v = document.createElement('video');
v.style.display = 'none';
v.playsInline = true;
v.muted = true;
document.body.appendChild(v);
return v;
}
function destroyPlayer() {
drawing = false;
if (player) {
try { player.destroy(); } catch(e) {}
player = null;
}
if (video) {
video.pause();
video.srcObject = null;
video.remove();
video = null;
}
}
function initPlayer() {
destroyPlayer();
video = createVideoElement();
console.log('[WebRTC] init:', streamUrl);
player = new JSWebrtc.Player(streamUrl, {
video,
autoplay: true,
onPlay: () => {
resizeCanvas();
drawing = true;
requestAnimationFrame(draw);
},
onError: err => {
console.error('[WebRTC] error', err);
}
});
}
/* ================= 圆角 ================= */
function roundRectPath(x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x+r, y);
ctx.arcTo(x+w, y, x+w, y+h, r);
ctx.arcTo(x+w, y+h, x, y+h, r);
ctx.arcTo(x, y+h, x, y, r);
ctx.arcTo(x, y, x+w, y, r);
ctx.closePath();
}
function drawBlurBackground(video) {
if (!BLUR_BG_ENABLED) return;
const vw = video.videoWidth;
const vh = video.videoHeight;
let sx = 0, sy = 0, sw = vw / 2, sh = vh / 2;
if (mainView === 'back') sx = vw / 2;
const main = calcContain16by9({ dw: canvas.width, dh: canvas.height });
/* ========= 1. 整屏强模糊背景 ========= */
ctx.save();
ctx.filter = `blur(${BLUR_RADIUS_PX}px)`;
ctx.drawImage(
video,
sx, sy, sw, sh,
0, 0, canvas.width, canvas.height
);
ctx.restore();
ctx.fillStyle = `rgba(0,0,0,${BLUR_DARK_ALPHA})`;
ctx.fillRect(0, 0, canvas.width, canvas.height);
/* ========= 2. 盖回清晰画面(关键) ========= */
ctx.save();
const cx = main.dx + main.dw / 2;
const cy = main.dy + main.dh / 2;
const base = Math.min(main.dw, main.dh);
// ✅ 完全清晰的“安全区”
const clearRadius = base * 0.42;
// ✅ 开始渐变的边界
const fadeRadius = base * 0.62;
const mask = ctx.createRadialGradient(
cx, cy, clearRadius,
cx, cy, fadeRadius
);
/*
0.0 ~ 1.0 的意义:
0 = 不透明(画清晰)
1 = 完全透明(露出模糊)
*/
// ★ 重点:前段完全没有任何渐变
mask.addColorStop(0.0, 'rgba(0,0,0,1)');
mask.addColorStop(0.65, 'rgba(0,0,0,1)');
// ★ 从这里才“开始虚”
mask.addColorStop(0.82, 'rgba(0,0,0,0.4)');
mask.addColorStop(1.0, 'rgba(0,0,0,0)');
ctx.globalCompositeOperation = 'destination-over';
ctx.fillStyle = mask;
ctx.fillRect(
main.dx - base,
main.dy - base,
main.dw + base * 2,
main.dh + base * 2
);
ctx.restore();
}
/* ================= PIP ================= */
function drawPip(pip, sx, sy, sw, sh) {
if (!pip.visible) return;
const s = getPipSize();
const r = s.w * PIP_RADIUS_RATIO;
ctx.save();
roundRectPath(pip.x, pip.y, s.w, s.h, r);
ctx.clip();
ctx.drawImage(video, sx, sy, sw, sh, pip.x, pip.y, s.w, s.h);
ctx.restore();
}
/* ================= 绘制 ================= */
function draw() {
if (!drawing || !video || video.readyState < video.HAVE_ENOUGH_DATA) {
requestAnimationFrame(draw);
return;
}
ctx.clearRect(0, 0, canvas.width, canvas.height);
const vw = video.videoWidth;
const vh = video.videoHeight;
/* ① 虚化背景:仅基于当前主视角内容 */
drawBlurBackground(video);
/* ② 主画面(清晰) */
const main = calcContain16by9({ dw: canvas.width, dh: canvas.height });
let sx = 0, sy = 0, sw = vw / 2, sh = vh / 2;
if (mainView === 'back') sx = vw / 2;
ctx.drawImage(
video,
sx, sy, sw, sh,
main.dx, main.dy, main.dw, main.dh
);
/* ③ PIP(小窗) */
drawPip(pipState.left, 0, vh/2, vw/2, vh/2);
drawPip(pipState.right, vw/2, vh/2, vw/2, vh/2);
requestAnimationFrame(draw);
}
/* ================= JS 接口 ================= */
window.setPipVisible = function({location, flag}) {
if (location === 'left') pipState.left.visible = !!flag;
if (location === 'right') pipState.right.visible = !!flag;
};
window.setMainView = function({view}) {
if (view === 'front' || view === 'back') mainView = view;
};
window.toggleMainView = function() {
mainView = (mainView === 'front') ? 'back' : 'front';
};
window.setStreamUrl = function({url}) {
if (!url) return;
streamUrl = url;
setTimeout(initPlayer, 80);
};
window.refreshSelf = function() {
location.reload();
};
/* ================= 启动 ================= */
resizeCanvas();
initPlayer();
</script>
</body>
</html>