291 lines
11 KiB
HTML
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("#iotam-hide-css")) return;
|
|
const style = doc.createElement("style");
|
|
style.id = "iotam-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>
|