Initial commit: ASCILINE YouTube Streamer
ASCII-art YouTube streaming for the Tesla in-car browser. - FastAPI server on a Mac mini, no Docker. - yt-dlp resolver: ID/URL/search. - ffmpeg with -re -fps_mode cfr for source-paced video; trivial drain consumer. Separate ffmpeg for AAC/ADTS audio. - Vendored ASCILINE renderer (MIT) for the binary wire protocol; pure fillText color path, on-demand selection flush. - HMAC PIN-gated cookie; Secure flag scheme-aware so /audio works on plain http during local dev. - LOW preset (120x50 24fps) verified clean on M4: FPS 24/24, JIT ~42ms.
This commit is contained in:
285
static/pin.html
Normal file
285
static/pin.html
Normal file
@@ -0,0 +1,285 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>ASCILINE — Enter PIN</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<style>
|
||||
/* Inline minimal theme — matches style.css variables without importing it
|
||||
so this page loads before /static/ is auth-gated. */
|
||||
:root {
|
||||
--bg: #000000;
|
||||
--bg2: #0d0d0d;
|
||||
--bg3: #1a1a1a;
|
||||
--border: #2a2a2a;
|
||||
--border-hi: #3d3d3d;
|
||||
--accent: #F5A623;
|
||||
--accent-dim:#b87a18;
|
||||
--fg: #e8e8e8;
|
||||
--fg-dim: #888;
|
||||
--fg-faint: #444;
|
||||
--red: #e84040;
|
||||
--green: #3ecf6e;
|
||||
--font-ui: 'Arial Narrow', Arial, sans-serif;
|
||||
--font-mono: 'Courier New', monospace;
|
||||
}
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
html, body {
|
||||
height: 100%;
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
font-family: var(--font-ui);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
#card {
|
||||
width: 320px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
#brand {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 6px;
|
||||
color: var(--accent);
|
||||
margin-bottom: 6px;
|
||||
user-select: none;
|
||||
}
|
||||
#subtitle {
|
||||
font-size: 12px;
|
||||
letter-spacing: 3px;
|
||||
color: var(--fg-faint);
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 32px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* PIN dots */
|
||||
#pin-display {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
margin-bottom: 24px;
|
||||
min-height: 20px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.pin-dot {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--border-hi);
|
||||
background: transparent;
|
||||
transition: background 120ms, border-color 120ms;
|
||||
}
|
||||
.pin-dot.filled {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
/* Status message */
|
||||
#msg {
|
||||
height: 22px;
|
||||
font-size: 13px;
|
||||
font-family: var(--font-mono);
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--fg-dim);
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
#msg.error { color: var(--red); }
|
||||
#msg.locked { color: var(--red); }
|
||||
|
||||
/* Keypad */
|
||||
#keypad {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
.key {
|
||||
height: 72px;
|
||||
background: var(--bg3);
|
||||
border: 1.5px solid var(--border-hi);
|
||||
border-radius: 8px;
|
||||
color: var(--fg);
|
||||
font-size: 26px;
|
||||
font-weight: 600;
|
||||
font-family: var(--font-ui);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background 80ms, border-color 80ms, transform 60ms;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
.key:active:not(:disabled) {
|
||||
background: var(--accent-dim);
|
||||
border-color: var(--accent);
|
||||
transform: scale(0.94);
|
||||
}
|
||||
.key:disabled {
|
||||
opacity: 0.35;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.key-clear {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1px;
|
||||
color: var(--fg-dim);
|
||||
}
|
||||
.key-zero {
|
||||
/* 0 sits in the middle column of the last row */
|
||||
}
|
||||
.key-submit {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
color: #000;
|
||||
font-size: 20px;
|
||||
}
|
||||
.key-submit:hover:not(:disabled) {
|
||||
background: #ffb838;
|
||||
border-color: #ffb838;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="card">
|
||||
<div id="brand">ASCILINE</div>
|
||||
<div id="subtitle">Enter PIN to continue</div>
|
||||
|
||||
<div id="pin-display"></div>
|
||||
<div id="msg"></div>
|
||||
|
||||
<div id="keypad">
|
||||
<button class="key" data-digit="1">1</button>
|
||||
<button class="key" data-digit="2">2</button>
|
||||
<button class="key" data-digit="3">3</button>
|
||||
<button class="key" data-digit="4">4</button>
|
||||
<button class="key" data-digit="5">5</button>
|
||||
<button class="key" data-digit="6">6</button>
|
||||
<button class="key" data-digit="7">7</button>
|
||||
<button class="key" data-digit="8">8</button>
|
||||
<button class="key" data-digit="9">9</button>
|
||||
<button class="key key-clear" data-action="clear">CLR</button>
|
||||
<button class="key key-zero" data-digit="0">0</button>
|
||||
<button class="key key-submit" data-action="submit">✓</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const MAX_LEN = 8;
|
||||
let pin = "";
|
||||
let locked = false;
|
||||
|
||||
const display = document.getElementById("pin-display");
|
||||
const msg = document.getElementById("msg");
|
||||
const keys = document.querySelectorAll(".key");
|
||||
|
||||
function renderDots(len) {
|
||||
display.innerHTML = "";
|
||||
for (let i = 0; i < Math.max(len, 4); i++) {
|
||||
const d = document.createElement("div");
|
||||
d.className = "pin-dot" + (i < len ? " filled" : "");
|
||||
display.appendChild(d);
|
||||
}
|
||||
}
|
||||
|
||||
function setMsg(text, cls) {
|
||||
msg.textContent = text;
|
||||
msg.className = cls || "";
|
||||
}
|
||||
|
||||
function setDisabled(state) {
|
||||
keys.forEach(k => k.disabled = state);
|
||||
}
|
||||
|
||||
renderDots(0);
|
||||
|
||||
document.getElementById("keypad").addEventListener("click", function (e) {
|
||||
if (locked) return;
|
||||
const btn = e.target.closest(".key");
|
||||
if (!btn || btn.disabled) return;
|
||||
|
||||
const digit = btn.dataset.digit;
|
||||
const action = btn.dataset.action;
|
||||
|
||||
if (digit !== undefined) {
|
||||
if (pin.length < MAX_LEN) {
|
||||
pin += digit;
|
||||
renderDots(pin.length);
|
||||
setMsg("");
|
||||
}
|
||||
} else if (action === "clear") {
|
||||
pin = "";
|
||||
renderDots(0);
|
||||
setMsg("");
|
||||
} else if (action === "submit") {
|
||||
if (pin.length === 0) { setMsg("Enter your PIN", ""); return; }
|
||||
submit();
|
||||
}
|
||||
});
|
||||
|
||||
// Also support physical keyboard for desktop testing.
|
||||
document.addEventListener("keydown", function (e) {
|
||||
if (locked) return;
|
||||
if (e.key >= "0" && e.key <= "9") {
|
||||
if (pin.length < MAX_LEN) {
|
||||
pin += e.key;
|
||||
renderDots(pin.length);
|
||||
setMsg("");
|
||||
}
|
||||
} else if (e.key === "Backspace") {
|
||||
pin = pin.slice(0, -1);
|
||||
renderDots(pin.length);
|
||||
setMsg("");
|
||||
} else if (e.key === "Enter") {
|
||||
submit();
|
||||
}
|
||||
});
|
||||
|
||||
function submit() {
|
||||
setDisabled(true);
|
||||
setMsg("Checking…", "");
|
||||
|
||||
fetch("/api/auth", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ pin: pin }),
|
||||
})
|
||||
.then(function (r) { return r.json().then(function (d) { return { status: r.status, data: d }; }); })
|
||||
.then(function (res) {
|
||||
if (res.status === 200) {
|
||||
setMsg("OK", "");
|
||||
// Small delay so the user sees "OK" before redirect.
|
||||
setTimeout(function () { window.location.replace("/"); }, 300);
|
||||
} else if (res.status === 429) {
|
||||
locked = true;
|
||||
setDisabled(true);
|
||||
const secs = res.data.retry_after || 600;
|
||||
setMsg("Too many attempts. Wait " + Math.ceil(secs / 60) + " min.", "locked");
|
||||
} else {
|
||||
pin = "";
|
||||
renderDots(0);
|
||||
setDisabled(false);
|
||||
setMsg("Wrong PIN", "error");
|
||||
}
|
||||
})
|
||||
.catch(function () {
|
||||
setDisabled(false);
|
||||
setMsg("Network error", "error");
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user