- Client: split H.264 bitstream into NAL units before decode (was passing raw concatenated bytes to decoder.decode() — black screen) - YUV→RGB conversion moved inside decode loop (borrow checker: DecodedYUV borrows from Decoder) - Intro screen: adaptive based on registration status - Unregistered: PIN + register OR PIN + connect - Registered: host OR PIN + connect - No more separate skip button — both workflows are equal on intro
- commit
- 73866a5e1682aaf309200bb589fe84d592f7bec3
- parent
- 0ce78b08ac24e9822362b23f4ad1e2cd30d90e82
- author
- dami etoile <dami.etoile@gmail.com>
| M | src-tauri/ | 80 | ++++++++++++++++-------------- |
| M | src/ | 80 | ++++++++++++++++++++++++++----- |
@@ -27,6 +27,7 @@ use serde::{Deserialize, Serialize};
2727 use sha2::{Digest, Sha256};2828 use openh264::encoder::Encoder;2929 use openh264::formats::{RgbSliceU8, YUVBuffer, YUVSource};30+use openh264::nal_units;3031 use std::io::Cursor;3132 use std::sync::{Arc, Mutex};3233 use std::time::{Duration, Instant};@@ -893,46 +894,49 @@ pub async fn iroh_client_connect(
893894 }894895 895896 // Decode H.264 → YUV → RGB8 → JPEG896- match decoder.decode(&h264_buf) {897- Ok(Some(yuv)) => {898- let (yw, yh) = yuv.dimensions();899- let rgb_len = yuv.rgb8_len();900- let mut rgb_raw = vec![0u8; rgb_len];901- yuv.write_rgb8(&mut rgb_raw);902-903- // Re-encode as JPEG for canvas904- let img = image::RgbImage::from_raw(yw as u32, yh as u32, rgb_raw);905- if let Some(img) = img {906- let mut jpeg_buf = Vec::with_capacity(30_000);907- if image::DynamicImage::ImageRgb8(img)908- .write_to(&mut Cursor::new(&mut jpeg_buf), image::ImageFormat::Jpeg)909- .is_ok()910- {911- let b64 = base64::engine::general_purpose::STANDARD.encode(&jpeg_buf);912- let _ = app.emit(913- "frame",914- FramePayload {915- width: yw as u32,916- height: yh as u32,917- data: b64,918- },919- );897+ // Split into individual NAL units — decoder needs them separately898+ for nal in nal_units(&h264_buf) {899+ match decoder.decode(nal) {900+ Ok(Some(yuv)) => {901+ let (yw, yh) = yuv.dimensions();902+ let rgb_len = yuv.rgb8_len();903+ let mut rgb_raw = vec![0u8; rgb_len];904+ yuv.write_rgb8(&mut rgb_raw);905+906+ // Re-encode as JPEG for canvas907+ let img = image::RgbImage::from_raw(yw as u32, yh as u32, rgb_raw);908+ if let Some(img) = img {909+ let mut jpeg_buf = Vec::with_capacity(30_000);910+ if image::DynamicImage::ImageRgb8(img)911+ .write_to(&mut Cursor::new(&mut jpeg_buf), image::ImageFormat::Jpeg)912+ .is_ok()913+ {914+ let b64 = base64::engine::general_purpose::STANDARD.encode(&jpeg_buf);915+ let _ = app.emit(916+ "frame",917+ FramePayload {918+ width: yw as u32,919+ height: yh as u32,920+ data: b64,921+ },922+ );923+ }920924 }921- }922925 923- frame_count += 1;924- let elapsed = start.elapsed();925- let fps = frame_count as f64 / elapsed.as_secs_f64().max(0.001);926- let _ = app.emit(927- "frame-stats",928- serde_json::json!({ "fps": fps, "count": frame_count }),929- );930- }931- Ok(None) => {932- // Need more data — normal for first few NALs933- }934- Err(e) => {935- eprintln!("[client] decode error: {}", e);926+ frame_count += 1;927+ let elapsed = start.elapsed();928+ let fps = frame_count as f64 / elapsed.as_secs_f64().max(0.001);929+ let _ = app.emit(930+ "frame-stats",931+ serde_json::json!({ "fps": fps, "count": frame_count }),932+ );933+ }934+ Ok(None) => {935+ // Need more NALs — SPS/PPS or waiting for I-frame936+ }937+ Err(e) => {938+ eprintln!("[client] decode error: {}", e);939+ }936940 }937941 }938942 }@@ -372,18 +372,36 @@
372372 </style>373373 </head>374374 <body>375- <!-- Setup screen -->375+ <!-- Intro screen (adaptive — shows both workflows) -->376376 <div class="setup hidden" id="setup">377377 <div class="setup-content">378- <h1>register key</h1>379- <p>Plug in your Titan key. This one-time step derives your machine's identity and stores it in the OS keyring. After this, hosting starts without the key.</p>380- <div class="setup-form">381- <input class="setup-input" id="setup-pin" type="password" placeholder="PIN" autocomplete="off" />382- <button class="setup-btn" id="setup-btn" onclick="setupRegister()">register</button>378+ <h1 id="intro-title">keyhome</h1>379+ <p id="intro-desc">Plug in your Titan key and choose an action.</p>380+381+ <!-- Unregistered: register or connect -->382+ <div id="intro-unregistered">383+ <div class="setup-form">384+ <input class="setup-input" id="setup-pin" type="password" placeholder="PIN" autocomplete="off" />385+ </div>386+ <div style="display: flex; gap: 16px; justify-content: center; margin-top: 12px;">387+ <button class="setup-btn" id="setup-btn" onclick="setupRegister()">register</button>388+ <button class="setup-btn" onclick="introConnect()">connect</button>389+ </div>390+ <div class="setup-status" id="setup-status"></div>383391 </div>384- <div class="setup-status" id="setup-status"></div>385- <div style="margin-top: 20px;">386- <button class="setup-btn" onclick="skipSetup()">or connect</button>392+393+ <!-- Registered: host or connect -->394+ <div id="intro-registered" style="display: none;">395+ <div style="display: flex; gap: 16px; justify-content: center; margin-bottom: 16px;">396+ <button class="setup-btn" onclick="introHost()">host</button>397+ </div>398+ <div class="setup-form">399+ <input class="setup-input" id="intro-pin" type="password" placeholder="PIN" autocomplete="off" />400+ </div>401+ <div style="display: flex; gap: 16px; justify-content: center; margin-top: 12px;">402+ <button class="setup-btn" onclick="introConnectRegistered()">connect</button>403+ </div>404+ <div class="setup-status" id="setup-status-reg"></div>387405 </div>388406 </div>389407 </div>@@ -484,8 +502,12 @@
484502 try {485503 const reg = await invoke('host_registration_status');486504 registered = reg.registered;505+ const unregDiv = document.getElementById('intro-unregistered');506+ const regDiv = document.getElementById('intro-registered');487507 if (reg.registered) {488- document.getElementById('setup').classList.add('hidden');508+ document.getElementById('setup').classList.remove('hidden');509+ unregDiv.style.display = 'none';510+ regDiv.style.display = 'block';489511 document.getElementById('node-id-text').textContent =490512 reg.node_id.substring(0, 16) + '...';491513 document.getElementById('reg-status').textContent = 'registered';@@ -493,6 +515,8 @@
493515 reg.node_id.substring(0, 24) + '...';494516 } else {495517 document.getElementById('setup').classList.remove('hidden');518+ unregDiv.style.display = 'block';519+ regDiv.style.display = 'none';496520 document.getElementById('reg-status').textContent = 'not registered';497521 }498522 } catch (e) {@@ -500,11 +524,43 @@
500524 }501525 }502526 503- // ─── Setup ──────────────────────────────────────────────────────────504- function skipSetup() {527+ // ─── Intro actions ─────────────────────────────────────────────────528+ function hideIntro() {505529 document.getElementById('setup').classList.add('hidden');506530 }507531 532+ async function introConnect() {533+ const pin = document.getElementById('setup-pin').value.trim();534+ if (!pin) {535+ const s = document.getElementById('setup-status');536+ s.className = 'setup-status err';537+ s.textContent = 'enter pin';538+ return;539+ }540+ hideIntro();541+ document.getElementById('pin-input').value = pin;542+ await connectHost();543+ }544+545+ async function introConnectRegistered() {546+ const pin = document.getElementById('intro-pin').value.trim();547+ if (!pin) {548+ const s = document.getElementById('setup-status-reg');549+ s.className = 'setup-status err';550+ s.textContent = 'enter pin';551+ return;552+ }553+ hideIntro();554+ document.getElementById('pin-input').value = pin;555+ await connectHost();556+ }557+558+ async function introHost() {559+ hideIntro();560+ await startHost();561+ }562+563+ // ─── Setup (register from intro) ────────────────────────────────────508564 async function setupRegister() {509565 const pin = document.getElementById('setup-pin').value.trim();510566 const btn = document.getElementById('setup-btn');