initttt
This commit is contained in:
290
apps/front/view.html
Normal file
290
apps/front/view.html
Normal file
@@ -0,0 +1,290 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user