Files
illusory-iotam/apps/front/view.html
2026-03-27 20:06:38 +02:00

291 lines
11 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<!-- iOS full-screen web app -->
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta
name="apple-mobile-web-app-status-bar-style"
content="black-translucent"
/>
<meta name="mobile-web-app-capable" content="yes" />
<title>Loading...</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body,
html {
width: 100%;
height: 100%;
overflow: hidden;
background: #000;
overscroll-behavior: none;
touch-action: none;
-webkit-user-select: none;
user-select: none;
}
/* ── Loading overlay ─────────────────────────── */
#overlay {
position: fixed;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #000;
color: #999;
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-size: 14px;
z-index: 200;
transition: opacity 0.4s ease;
}
#overlay.hidden {
opacity: 0;
pointer-events: none;
}
.spinner {
width: 24px;
height: 24px;
border: 2px solid #333;
border-top-color: #999;
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin-bottom: 16px;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* ── ws-scrcpy iframe ────────────────────────── */
#scrcpy-frame {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: none;
}
</style>
</head>
<body>
<div id="overlay">
<div class="spinner"></div>
<span>Connecting...</span>
</div>
<iframe id="scrcpy-frame" src="" allow="autoplay"></iframe>
<script>
const iframe = document.getElementById("scrcpy-frame");
const overlay = document.getElementById("overlay");
// We need the device UDID to build the direct-stream URL.
// Strategy: first, load the ws-scrcpy device list page silently
// and scrape the first available device, then redirect the iframe
// to the stream URL for that device.
// Phase 1: load device list from ws-scrcpy (proxied through us)
async function getFirstDevice() {
// ws-scrcpy exposes device info via its tracker WebSocket,
// but the simplest approach is to fetch the page HTML and
// parse the device list. However, ws-scrcpy is an SPA that
// builds the list client-side. So instead we'll use a short
// poll: load ws-scrcpy in the iframe with the device list,
// wait for a device to appear, grab its UDID, then switch
// to the stream view.
// Actually, even simpler: ws-scrcpy also has a device tracker
// that sends device info over WebSocket. Let's just load the
// ws-scrcpy index in the iframe, wait for the content, then
// inject CSS + auto-click the first stream player link.
// Load ws-scrcpy's index (proxied, so same-origin)
// The ?scrcpy=1 param tells our Bun proxy to pass through
// to ws-scrcpy instead of serving view.html again.
iframe.src = "/?scrcpy=1";
}
const HIDE_CSS = `
/* Hide device list table and all non-video UI */
#devices,
.table-wrapper,
.tracker-name,
.control-buttons-list,
.control-wrapper,
.more-box {
display: none !important;
}
/* Make video fill the entire viewport */
.device-view {
position: fixed !important;
inset: 0 !important;
width: 100% !important;
height: 100% !important;
margin: 0 !important;
padding: 0 !important;
background: #000 !important;
}
.video {
width: 100% !important;
height: 100% !important;
position: absolute !important;
inset: 0 !important;
}
.video-layer,
.touch-layer {
width: 100% !important;
height: 100% !important;
max-width: 100% !important;
max-height: 100% !important;
object-fit: contain !important;
}
body {
margin: 0 !important;
padding: 0 !important;
overflow: hidden !important;
background: #000 !important;
}
`;
function injectCSS(doc) {
if (doc.querySelector("#maitm-hide-css")) return;
const style = doc.createElement("style");
style.id = "maitm-hide-css";
style.textContent = HIDE_CSS;
doc.head.appendChild(style);
}
// Phase 2: once iframe loads, inject CSS to hide the device list
// UI and auto-click the first available stream link.
iframe.addEventListener("load", function onLoad() {
try {
const doc =
iframe.contentDocument || iframe.contentWindow.document;
if (!doc) return;
injectCSS(doc);
// Re-inject CSS whenever ws-scrcpy rebuilds the DOM
// (e.g., when navigating from device list to stream via hash)
const observer = new MutationObserver(() => injectCSS(doc));
observer.observe(doc.body, {
childList: true,
subtree: true,
});
// Now we need to auto-navigate to the first device's stream.
// The device list is built async via WebSocket, so we poll
// for a device link to appear and click it.
pollForDevice(doc);
} catch (e) {
console.error("Cannot access iframe (cross-origin?):", e);
}
});
function pollForDevice(doc) {
let attempts = 0;
const maxAttempts = 60; // 30 seconds
const interval = setInterval(() => {
attempts++;
// Look for stream player links in the device list
// ws-scrcpy renders links with player names as link text
const links = doc.querySelectorAll("a");
let streamLink = null;
for (const link of links) {
const href = link.getAttribute("href") || "";
const text = (link.textContent || "").toLowerCase();
// Look for player links — they contain "action=stream"
// or player names like "mse", "webcodecs", etc.
if (
href.includes("action=stream") ||
text === "mse" ||
text === "webcodecs" ||
text === "tinyh264" ||
text === "broadway"
) {
streamLink = link;
break;
}
}
if (streamLink) {
clearInterval(interval);
console.log(
"Found device stream link, clicking:",
streamLink.href,
);
streamLink.click();
// Wait for stream to start rendering, then hide overlay
waitForVideo(doc);
return;
}
if (attempts >= maxAttempts) {
clearInterval(interval);
overlay.querySelector("span").textContent =
"No device found. Make sure your phone is connected via USB.";
}
}, 500);
}
function waitForVideo(doc) {
let checks = 0;
const interval = setInterval(() => {
checks++;
const canvas = doc.querySelector("canvas");
const video = doc.querySelector("video");
if (canvas || video) {
clearInterval(interval);
// Give a moment for first frame to render
setTimeout(() => {
overlay.classList.add("hidden");
document.title = "\u200B"; // Zero-width space — blank title
}, 500);
return;
}
if (checks > 30) {
clearInterval(interval);
overlay.classList.add("hidden");
}
}, 300);
}
// Prevent default touch behaviors that interfere
document.addEventListener("touchmove", (e) => e.preventDefault(), {
passive: false,
});
document.addEventListener(
"touchstart",
(e) => {
if (e.touches.length > 1) e.preventDefault();
},
{ passive: false },
);
// Start
getFirstDevice();
</script>
</body>
</html>