|
|
@@ -0,0 +1,349 @@
|
|
|
+<!DOCTYPE html>
|
|
|
+<html>
|
|
|
+<head>
|
|
|
+<meta charset="utf-8">
|
|
|
+<title>Screen Cast</title>
|
|
|
+<style>
|
|
|
+* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
+
|
|
|
+html, body {
|
|
|
+ width: 100%; height: 100%;
|
|
|
+ background: #000;
|
|
|
+ overflow: hidden;
|
|
|
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
|
+}
|
|
|
+
|
|
|
+video {
|
|
|
+ width: 100%; height: 100%;
|
|
|
+ object-fit: contain;
|
|
|
+ display: block;
|
|
|
+}
|
|
|
+
|
|
|
+/* ── Controls bar ── */
|
|
|
+#controls {
|
|
|
+ position: fixed;
|
|
|
+ top: 16px;
|
|
|
+ left: 50%;
|
|
|
+ transform: translateX(-50%);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 4px;
|
|
|
+ padding: 6px;
|
|
|
+ background: rgba(18, 18, 18, 0.82);
|
|
|
+ backdrop-filter: blur(24px);
|
|
|
+ -webkit-backdrop-filter: blur(24px);
|
|
|
+ border: 1px solid rgba(255,255,255,0.09);
|
|
|
+ border-radius: 12px;
|
|
|
+ z-index: 100;
|
|
|
+ transition: opacity 0.25s ease;
|
|
|
+ white-space: nowrap;
|
|
|
+ cursor: grab;
|
|
|
+ user-select: none;
|
|
|
+}
|
|
|
+
|
|
|
+#controls.dragging { cursor: grabbing; transition: none; }
|
|
|
+
|
|
|
+#controls.hidden {
|
|
|
+ opacity: 0;
|
|
|
+ pointer-events: none;
|
|
|
+}
|
|
|
+
|
|
|
+.sep {
|
|
|
+ width: 1px;
|
|
|
+ height: 18px;
|
|
|
+ background: rgba(255,255,255,0.1);
|
|
|
+ margin: 0 2px;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+button {
|
|
|
+ background: transparent;
|
|
|
+ color: rgba(255,255,255,0.78);
|
|
|
+ border: none;
|
|
|
+ padding: 5px 13px;
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 500;
|
|
|
+ border-radius: 7px;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: background 0.12s, color 0.12s;
|
|
|
+ letter-spacing: 0.01em;
|
|
|
+ line-height: 1.4;
|
|
|
+}
|
|
|
+
|
|
|
+button:hover { background: rgba(255,255,255,0.1); color: #fff; }
|
|
|
+button:active { background: rgba(255,255,255,0.18); color: #fff; }
|
|
|
+button:disabled { opacity: 0.3; cursor: default; pointer-events: none; }
|
|
|
+button.on { background: rgba(255,255,255,0.14); color: #fff; }
|
|
|
+
|
|
|
+/* ── Placeholder ── */
|
|
|
+#placeholder {
|
|
|
+ position: fixed;
|
|
|
+ inset: 0;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 14px;
|
|
|
+ color: rgba(255,255,255,0.35);
|
|
|
+ z-index: 50;
|
|
|
+ transition: opacity 0.3s;
|
|
|
+ user-select: none;
|
|
|
+}
|
|
|
+
|
|
|
+#placeholder.hidden { opacity: 0; pointer-events: none; }
|
|
|
+
|
|
|
+#placeholder svg { opacity: 0.25; }
|
|
|
+
|
|
|
+#placeholder .label {
|
|
|
+ font-size: 15px;
|
|
|
+ font-weight: 400;
|
|
|
+ letter-spacing: 0.01em;
|
|
|
+}
|
|
|
+
|
|
|
+#placeholder .hint {
|
|
|
+ font-size: 12px;
|
|
|
+ opacity: 0.6;
|
|
|
+}
|
|
|
+
|
|
|
+kbd {
|
|
|
+ font-family: inherit;
|
|
|
+ font-size: 11px;
|
|
|
+ background: rgba(255,255,255,0.08);
|
|
|
+ border: 1px solid rgba(255,255,255,0.14);
|
|
|
+ border-radius: 4px;
|
|
|
+ padding: 1px 6px;
|
|
|
+}
|
|
|
+
|
|
|
+/* ── Black overlay ── */
|
|
|
+#overlay {
|
|
|
+ position: fixed;
|
|
|
+ inset: 0;
|
|
|
+ background: #000;
|
|
|
+ z-index: 80;
|
|
|
+ opacity: 0;
|
|
|
+ pointer-events: none;
|
|
|
+ transition: opacity 0.2s;
|
|
|
+}
|
|
|
+#overlay.on { opacity: 1; pointer-events: auto; }
|
|
|
+
|
|
|
+/* ── Keyboard shortcut hint (fades after a few seconds) ── */
|
|
|
+#kbd-hint {
|
|
|
+ position: fixed;
|
|
|
+ bottom: 20px;
|
|
|
+ left: 50%;
|
|
|
+ transform: translateX(-50%);
|
|
|
+ font-size: 11px;
|
|
|
+ color: rgba(255,255,255,0.22);
|
|
|
+ z-index: 100;
|
|
|
+ letter-spacing: 0.04em;
|
|
|
+ white-space: nowrap;
|
|
|
+ transition: opacity 0.6s;
|
|
|
+ pointer-events: none;
|
|
|
+}
|
|
|
+#kbd-hint.hidden { opacity: 0; }
|
|
|
+
|
|
|
+/* ── Connecting pulse ── */
|
|
|
+.pulse { display: inline-flex; gap: 4px; align-items: center; }
|
|
|
+.pulse i {
|
|
|
+ display: block;
|
|
|
+ width: 5px; height: 5px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: rgba(255,255,255,0.6);
|
|
|
+ animation: p 1.1s ease-in-out infinite;
|
|
|
+ font-style: normal;
|
|
|
+}
|
|
|
+.pulse i:nth-child(2) { animation-delay: 0.18s; }
|
|
|
+.pulse i:nth-child(3) { animation-delay: 0.36s; }
|
|
|
+@keyframes p {
|
|
|
+ 0%,80%,100% { transform: scale(0.65); opacity: 0.35; }
|
|
|
+ 40% { transform: scale(1); opacity: 1; }
|
|
|
+}
|
|
|
+</style>
|
|
|
+</head>
|
|
|
+<body>
|
|
|
+
|
|
|
+<div id="controls">
|
|
|
+ <button id="btn-select">Select Stream</button>
|
|
|
+ <button id="btn-reconnect" disabled>Reconnect</button>
|
|
|
+ <div class="sep"></div>
|
|
|
+ <button id="btn-black">Black</button>
|
|
|
+ <button id="btn-fs">Fullscreen</button>
|
|
|
+</div>
|
|
|
+
|
|
|
+<div id="placeholder">
|
|
|
+ <svg width="56" height="56" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
|
+ stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">
|
|
|
+ <rect x="2" y="3" width="20" height="14" rx="2.5"/>
|
|
|
+ <line x1="8" y1="21" x2="16" y2="21"/>
|
|
|
+ <line x1="12" y1="17" x2="12" y2="21"/>
|
|
|
+ </svg>
|
|
|
+ <p class="label">No stream selected</p>
|
|
|
+ <p class="hint">Press <kbd>S</kbd> to pick a screen or window</p>
|
|
|
+</div>
|
|
|
+
|
|
|
+<video id="video" autoplay playsinline></video>
|
|
|
+
|
|
|
+<div id="overlay"></div>
|
|
|
+
|
|
|
+<div id="kbd-hint">S · select · R · reconnect · B · black · F · fullscreen · Esc · un-black</div>
|
|
|
+
|
|
|
+<script>
|
|
|
+const video = document.getElementById("video")
|
|
|
+const overlay = document.getElementById("overlay")
|
|
|
+const controls = document.getElementById("controls")
|
|
|
+const ph = document.getElementById("placeholder")
|
|
|
+const kbdHint = document.getElementById("kbd-hint")
|
|
|
+const btnSel = document.getElementById("btn-select")
|
|
|
+const btnRec = document.getElementById("btn-reconnect")
|
|
|
+const btnBlack = document.getElementById("btn-black")
|
|
|
+const btnFs = document.getElementById("btn-fs")
|
|
|
+
|
|
|
+let stream = null
|
|
|
+let isBlack = false
|
|
|
+let connecting = false
|
|
|
+let hideTimer = null
|
|
|
+
|
|
|
+// ── Auto-hide controls + cursor ────────────────────────────────
|
|
|
+
|
|
|
+function showControls() {
|
|
|
+ controls.classList.remove("hidden")
|
|
|
+ document.body.style.cursor = ""
|
|
|
+ clearTimeout(hideTimer)
|
|
|
+ if (stream) hideTimer = setTimeout(hideControls, 2500)
|
|
|
+}
|
|
|
+
|
|
|
+function hideControls() {
|
|
|
+ controls.classList.add("hidden")
|
|
|
+ document.body.style.cursor = "none"
|
|
|
+}
|
|
|
+
|
|
|
+document.addEventListener("mousemove", showControls)
|
|
|
+document.addEventListener("mousedown", showControls)
|
|
|
+
|
|
|
+setTimeout(() => kbdHint.classList.add("hidden"), 4000)
|
|
|
+
|
|
|
+// ── Stream ─────────────────────────────────────────────────────
|
|
|
+
|
|
|
+async function connect() {
|
|
|
+ if (connecting) return
|
|
|
+ connecting = true
|
|
|
+ btnSel.innerHTML = `<span class="pulse"><i></i><i></i><i></i></span>`
|
|
|
+ btnSel.disabled = true
|
|
|
+
|
|
|
+ const saved = localStorage.getItem("displaySurface")
|
|
|
+ const constraints = { video: saved ? { displaySurface: saved } : true, audio: false }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const s = await navigator.mediaDevices.getDisplayMedia(constraints)
|
|
|
+ const surface = s.getVideoTracks()[0].getSettings().displaySurface
|
|
|
+ if (surface) localStorage.setItem("displaySurface", surface)
|
|
|
+ attach(s)
|
|
|
+ } catch(e) {
|
|
|
+ console.error(e)
|
|
|
+ reset()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function attach(s) {
|
|
|
+ stream = s
|
|
|
+ video.srcObject = s
|
|
|
+ ph.classList.add("hidden")
|
|
|
+ btnRec.disabled = false
|
|
|
+ reset()
|
|
|
+
|
|
|
+ s.getVideoTracks()[0].onended = () => {
|
|
|
+ stream = null
|
|
|
+ ph.classList.remove("hidden")
|
|
|
+ btnRec.disabled = true
|
|
|
+ showControls()
|
|
|
+ clearTimeout(hideTimer)
|
|
|
+ setTimeout(connect, 1000)
|
|
|
+ }
|
|
|
+
|
|
|
+ showControls()
|
|
|
+}
|
|
|
+
|
|
|
+function reset() {
|
|
|
+ connecting = false
|
|
|
+ btnSel.disabled = false
|
|
|
+ btnSel.textContent = "Select Stream"
|
|
|
+}
|
|
|
+
|
|
|
+function reconnect() {
|
|
|
+ if (stream) { stream.getTracks().forEach(t => t.stop()); stream = null }
|
|
|
+ connect()
|
|
|
+}
|
|
|
+
|
|
|
+function toggleBlack() {
|
|
|
+ isBlack = !isBlack
|
|
|
+ overlay.classList.toggle("on", isBlack)
|
|
|
+ btnBlack.classList.toggle("on", isBlack)
|
|
|
+}
|
|
|
+
|
|
|
+function toggleFs() {
|
|
|
+ if (!document.fullscreenElement) document.documentElement.requestFullscreen()
|
|
|
+ else document.exitFullscreen()
|
|
|
+}
|
|
|
+
|
|
|
+// ── Buttons ────────────────────────────────────────────────────
|
|
|
+
|
|
|
+btnSel.onclick = connect
|
|
|
+btnRec.onclick = reconnect
|
|
|
+btnBlack.onclick = toggleBlack
|
|
|
+btnFs.onclick = toggleFs
|
|
|
+
|
|
|
+// ── Keyboard ───────────────────────────────────────────────────
|
|
|
+
|
|
|
+document.addEventListener("keydown", e => {
|
|
|
+ if (e.target.tagName === "INPUT") return
|
|
|
+ const k = e.key.toLowerCase()
|
|
|
+ if (k === "s") connect()
|
|
|
+ if (k === "r") reconnect()
|
|
|
+ if (k === "b") toggleBlack()
|
|
|
+ if (k === "f") toggleFs()
|
|
|
+ if (k === "escape" && isBlack) toggleBlack()
|
|
|
+})
|
|
|
+
|
|
|
+document.addEventListener("fullscreenchange", () => {
|
|
|
+ btnFs.textContent = document.fullscreenElement ? "Exit Fullscreen" : "Fullscreen"
|
|
|
+})
|
|
|
+
|
|
|
+// ── Drag to reposition controls ────────────────────────────────
|
|
|
+
|
|
|
+;(function () {
|
|
|
+ let dragging = false, ox = 0, oy = 0
|
|
|
+
|
|
|
+ function applyPos(x, y) {
|
|
|
+ // Clamp within viewport
|
|
|
+ const r = controls.getBoundingClientRect()
|
|
|
+ x = Math.max(0, Math.min(window.innerWidth - r.width, x))
|
|
|
+ y = Math.max(0, Math.min(window.innerHeight - r.height, y))
|
|
|
+ controls.style.left = x + "px"
|
|
|
+ controls.style.top = y + "px"
|
|
|
+ controls.style.transform = "none"
|
|
|
+ }
|
|
|
+
|
|
|
+ controls.addEventListener("mousedown", e => {
|
|
|
+ if (e.target.tagName === "BUTTON") return
|
|
|
+ dragging = true
|
|
|
+ controls.classList.add("dragging")
|
|
|
+ const r = controls.getBoundingClientRect()
|
|
|
+ ox = e.clientX - r.left
|
|
|
+ oy = e.clientY - r.top
|
|
|
+ e.preventDefault()
|
|
|
+ })
|
|
|
+
|
|
|
+ document.addEventListener("mousemove", e => {
|
|
|
+ if (!dragging) return
|
|
|
+ applyPos(e.clientX - ox, e.clientY - oy)
|
|
|
+ })
|
|
|
+
|
|
|
+ document.addEventListener("mouseup", () => {
|
|
|
+ if (!dragging) return
|
|
|
+ dragging = false
|
|
|
+ controls.classList.remove("dragging")
|
|
|
+ })
|
|
|
+})()
|
|
|
+</script>
|
|
|
+</body>
|
|
|
+</html>
|