app.html 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="utf-8">
  5. <title>Screen Cast</title>
  6. <style>
  7. * { box-sizing: border-box; margin: 0; padding: 0; }
  8. html, body {
  9. width: 100%; height: 100%;
  10. background: #000;
  11. overflow: hidden;
  12. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  13. }
  14. video {
  15. width: 100%; height: 100%;
  16. object-fit: contain;
  17. display: block;
  18. }
  19. /* ── Controls bar ── */
  20. #controls {
  21. position: fixed;
  22. top: 16px;
  23. left: 50%;
  24. transform: translateX(-50%);
  25. display: flex;
  26. align-items: center;
  27. gap: 4px;
  28. padding: 6px;
  29. background: rgba(18, 18, 18, 0.82);
  30. backdrop-filter: blur(24px);
  31. -webkit-backdrop-filter: blur(24px);
  32. border: 1px solid rgba(255,255,255,0.09);
  33. border-radius: 12px;
  34. z-index: 100;
  35. transition: opacity 0.25s ease;
  36. white-space: nowrap;
  37. cursor: grab;
  38. user-select: none;
  39. }
  40. #controls.dragging { cursor: grabbing; transition: none; }
  41. #controls.hidden {
  42. opacity: 0;
  43. pointer-events: none;
  44. }
  45. .sep {
  46. width: 1px;
  47. height: 18px;
  48. background: rgba(255,255,255,0.1);
  49. margin: 0 2px;
  50. flex-shrink: 0;
  51. }
  52. button {
  53. background: transparent;
  54. color: rgba(255,255,255,0.78);
  55. border: none;
  56. padding: 5px 13px;
  57. font-size: 13px;
  58. font-weight: 500;
  59. border-radius: 7px;
  60. cursor: pointer;
  61. transition: background 0.12s, color 0.12s;
  62. letter-spacing: 0.01em;
  63. line-height: 1.4;
  64. }
  65. button:hover { background: rgba(255,255,255,0.1); color: #fff; }
  66. button:active { background: rgba(255,255,255,0.18); color: #fff; }
  67. button:disabled { opacity: 0.3; cursor: default; pointer-events: none; }
  68. button.on { background: rgba(255,255,255,0.14); color: #fff; }
  69. /* ── Placeholder ── */
  70. #placeholder {
  71. position: fixed;
  72. inset: 0;
  73. display: flex;
  74. flex-direction: column;
  75. align-items: center;
  76. justify-content: center;
  77. gap: 14px;
  78. color: rgba(255,255,255,0.35);
  79. z-index: 50;
  80. transition: opacity 0.3s;
  81. user-select: none;
  82. }
  83. #placeholder.hidden { opacity: 0; pointer-events: none; }
  84. #placeholder svg { opacity: 0.25; }
  85. #placeholder .label {
  86. font-size: 15px;
  87. font-weight: 400;
  88. letter-spacing: 0.01em;
  89. }
  90. #placeholder .hint {
  91. font-size: 12px;
  92. opacity: 0.6;
  93. }
  94. kbd {
  95. font-family: inherit;
  96. font-size: 11px;
  97. background: rgba(255,255,255,0.08);
  98. border: 1px solid rgba(255,255,255,0.14);
  99. border-radius: 4px;
  100. padding: 1px 6px;
  101. }
  102. /* ── Black overlay ── */
  103. #overlay {
  104. position: fixed;
  105. inset: 0;
  106. background: #000;
  107. z-index: 80;
  108. opacity: 0;
  109. pointer-events: none;
  110. transition: opacity 0.2s;
  111. }
  112. #overlay.on { opacity: 1; pointer-events: auto; }
  113. /* ── Keyboard shortcut hint (fades after a few seconds) ── */
  114. #kbd-hint {
  115. position: fixed;
  116. bottom: 20px;
  117. left: 50%;
  118. transform: translateX(-50%);
  119. font-size: 11px;
  120. color: rgba(255,255,255,0.22);
  121. z-index: 100;
  122. letter-spacing: 0.04em;
  123. white-space: nowrap;
  124. transition: opacity 0.6s;
  125. pointer-events: none;
  126. }
  127. #kbd-hint.hidden { opacity: 0; }
  128. /* ── Connecting pulse ── */
  129. .pulse { display: inline-flex; gap: 4px; align-items: center; }
  130. .pulse i {
  131. display: block;
  132. width: 5px; height: 5px;
  133. border-radius: 50%;
  134. background: rgba(255,255,255,0.6);
  135. animation: p 1.1s ease-in-out infinite;
  136. font-style: normal;
  137. }
  138. .pulse i:nth-child(2) { animation-delay: 0.18s; }
  139. .pulse i:nth-child(3) { animation-delay: 0.36s; }
  140. @keyframes p {
  141. 0%,80%,100% { transform: scale(0.65); opacity: 0.35; }
  142. 40% { transform: scale(1); opacity: 1; }
  143. }
  144. </style>
  145. </head>
  146. <body>
  147. <div id="controls">
  148. <button id="btn-select">Select Stream</button>
  149. <button id="btn-reconnect" disabled>Reconnect</button>
  150. <div class="sep"></div>
  151. <button id="btn-black">Black</button>
  152. <button id="btn-fs">Fullscreen</button>
  153. </div>
  154. <div id="placeholder">
  155. <svg width="56" height="56" viewBox="0 0 24 24" fill="none" stroke="currentColor"
  156. stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">
  157. <rect x="2" y="3" width="20" height="14" rx="2.5"/>
  158. <line x1="8" y1="21" x2="16" y2="21"/>
  159. <line x1="12" y1="17" x2="12" y2="21"/>
  160. </svg>
  161. <p class="label">No stream selected</p>
  162. <p class="hint">Press <kbd>S</kbd> to pick a screen or window</p>
  163. </div>
  164. <video id="video" autoplay playsinline></video>
  165. <div id="overlay"></div>
  166. <div id="kbd-hint">S · select &nbsp;·&nbsp; R · reconnect &nbsp;·&nbsp; B · black &nbsp;·&nbsp; F · fullscreen &nbsp;·&nbsp; Esc · un-black</div>
  167. <script>
  168. const video = document.getElementById("video")
  169. const overlay = document.getElementById("overlay")
  170. const controls = document.getElementById("controls")
  171. const ph = document.getElementById("placeholder")
  172. const kbdHint = document.getElementById("kbd-hint")
  173. const btnSel = document.getElementById("btn-select")
  174. const btnRec = document.getElementById("btn-reconnect")
  175. const btnBlack = document.getElementById("btn-black")
  176. const btnFs = document.getElementById("btn-fs")
  177. let stream = null
  178. let isBlack = false
  179. let connecting = false
  180. let hideTimer = null
  181. // ── Auto-hide controls + cursor ────────────────────────────────
  182. function showControls() {
  183. controls.classList.remove("hidden")
  184. document.body.style.cursor = ""
  185. clearTimeout(hideTimer)
  186. if (stream) hideTimer = setTimeout(hideControls, 2500)
  187. }
  188. function hideControls() {
  189. controls.classList.add("hidden")
  190. document.body.style.cursor = "none"
  191. }
  192. document.addEventListener("mousemove", showControls)
  193. document.addEventListener("mousedown", showControls)
  194. setTimeout(() => kbdHint.classList.add("hidden"), 4000)
  195. // ── Stream ─────────────────────────────────────────────────────
  196. async function connect() {
  197. if (connecting) return
  198. connecting = true
  199. btnSel.innerHTML = `<span class="pulse"><i></i><i></i><i></i></span>`
  200. btnSel.disabled = true
  201. const saved = localStorage.getItem("displaySurface")
  202. const constraints = { video: saved ? { displaySurface: saved } : true, audio: false }
  203. try {
  204. const s = await navigator.mediaDevices.getDisplayMedia(constraints)
  205. const surface = s.getVideoTracks()[0].getSettings().displaySurface
  206. if (surface) localStorage.setItem("displaySurface", surface)
  207. attach(s)
  208. } catch(e) {
  209. console.error(e)
  210. reset()
  211. }
  212. }
  213. function attach(s) {
  214. stream = s
  215. video.srcObject = s
  216. ph.classList.add("hidden")
  217. btnRec.disabled = false
  218. reset()
  219. s.getVideoTracks()[0].onended = () => {
  220. stream = null
  221. ph.classList.remove("hidden")
  222. btnRec.disabled = true
  223. showControls()
  224. clearTimeout(hideTimer)
  225. setTimeout(connect, 1000)
  226. }
  227. showControls()
  228. }
  229. function reset() {
  230. connecting = false
  231. btnSel.disabled = false
  232. btnSel.textContent = "Select Stream"
  233. }
  234. function reconnect() {
  235. if (stream) { stream.getTracks().forEach(t => t.stop()); stream = null }
  236. connect()
  237. }
  238. function toggleBlack() {
  239. isBlack = !isBlack
  240. overlay.classList.toggle("on", isBlack)
  241. btnBlack.classList.toggle("on", isBlack)
  242. }
  243. function toggleFs() {
  244. if (!document.fullscreenElement) document.documentElement.requestFullscreen()
  245. else document.exitFullscreen()
  246. }
  247. // ── Buttons ────────────────────────────────────────────────────
  248. btnSel.onclick = connect
  249. btnRec.onclick = reconnect
  250. btnBlack.onclick = toggleBlack
  251. btnFs.onclick = toggleFs
  252. // ── Keyboard ───────────────────────────────────────────────────
  253. document.addEventListener("keydown", e => {
  254. if (e.target.tagName === "INPUT") return
  255. const k = e.key.toLowerCase()
  256. if (k === "s") connect()
  257. if (k === "r") reconnect()
  258. if (k === "b") toggleBlack()
  259. if (k === "f") toggleFs()
  260. if (k === "escape" && isBlack) toggleBlack()
  261. })
  262. document.addEventListener("fullscreenchange", () => {
  263. btnFs.textContent = document.fullscreenElement ? "Exit Fullscreen" : "Fullscreen"
  264. })
  265. // ── Drag to reposition controls ────────────────────────────────
  266. ;(function () {
  267. let dragging = false, ox = 0, oy = 0
  268. function applyPos(x, y) {
  269. // Clamp within viewport
  270. const r = controls.getBoundingClientRect()
  271. x = Math.max(0, Math.min(window.innerWidth - r.width, x))
  272. y = Math.max(0, Math.min(window.innerHeight - r.height, y))
  273. controls.style.left = x + "px"
  274. controls.style.top = y + "px"
  275. controls.style.transform = "none"
  276. }
  277. controls.addEventListener("mousedown", e => {
  278. if (e.target.tagName === "BUTTON") return
  279. dragging = true
  280. controls.classList.add("dragging")
  281. const r = controls.getBoundingClientRect()
  282. ox = e.clientX - r.left
  283. oy = e.clientY - r.top
  284. e.preventDefault()
  285. })
  286. document.addEventListener("mousemove", e => {
  287. if (!dragging) return
  288. applyPos(e.clientX - ox, e.clientY - oy)
  289. })
  290. document.addEventListener("mouseup", () => {
  291. if (!dragging) return
  292. dragging = false
  293. controls.classList.remove("dragging")
  294. })
  295. })()
  296. </script>
  297. </body>
  298. </html>