multimodalart HF Staff commited on
Commit
30435dc
·
verified ·
1 Parent(s): 94fce9e

Fix real-time frame render: decode+paint newest JPEG to canvas immediately (no img.src thrash/stale redraw/buffering)

Browse files
Files changed (1) hide show
  1. index.html +55 -10
index.html CHANGED
@@ -39,8 +39,8 @@
39
  /* Stage */
40
  .stage{position:relative;aspect-ratio:1280/704;background:#05070c;border-radius:12px;
41
  overflow:hidden;border:1px solid var(--edge);display:grid;place-items:center}
42
- .stage img{width:100%;height:100%;object-fit:cover;display:none;image-rendering:auto}
43
- .stage.playing img{display:block}
44
  .overlay{position:absolute;inset:0;display:grid;place-items:center;text-align:center;
45
  padding:24px;background:rgba(5,7,12,.55);backdrop-filter:blur(2px)}
46
  .overlay.hidden{display:none}
@@ -125,7 +125,7 @@
125
  <!-- Stage -->
126
  <div class="card">
127
  <div class="stage" id="stage" tabindex="0">
128
- <img id="view" alt="world view" />
129
  <div class="hud">
130
  <div class="chip"><span class="dot" id="status-dot"></span><span id="status-label">Idle</span></div>
131
  <div class="chip"><span id="fps-value">0</span> fps · <span id="frame-value">0</span> blk</div>
@@ -223,6 +223,7 @@ const state = {
223
  // --- DOM ---
224
  const $ = (id) => document.getElementById(id);
225
  const stage = $("stage"), view = $("view"), overlay = $("overlay");
 
226
  const spinner = $("spinner"), overlayTitle = $("overlay-title"), overlaySub = $("overlay-sub");
227
  const statusDot = $("status-dot"), statusLabel = $("status-label");
228
  const fpsValue = $("fps-value"), frameValue = $("frame-value");
@@ -292,6 +293,52 @@ async function initClient(){
292
  if(!state.client) state.client = await Client.connect(window.location.origin);
293
  }
294
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
295
  // --- WebSocket frame stream ---
296
  function connectWS(){
297
  const proto = location.protocol === "https:" ? "wss:" : "ws:";
@@ -302,13 +349,10 @@ function connectWS(){
302
  const dv = new DataView(e.data);
303
  const blk = dv.getUint32(0);
304
  const fps = dv.getUint32(4)/10;
305
- const blob = new Blob([e.data.slice(8)], {type:"image/jpeg"});
306
- const url = URL.createObjectURL(blob);
307
- view.onload = ()=>URL.revokeObjectURL(url);
308
- view.src = url;
309
- if(!state.playing){ hideOverlay(); setPlaying(true); setStatus("playing","Playing"); }
310
- frameValue.textContent = blk;
311
- fpsValue.textContent = fps.toFixed(1);
312
  }else{
313
  try{
314
  const msg = JSON.parse(e.data);
@@ -395,6 +439,7 @@ async function startWorld(){
395
  async function stopWorld(){
396
  try{ if(state.client) await state.client.predict("/stop_game", { session_id: state.sessionId }); }catch{}
397
  if(state.ws){ try{ state.ws.send(JSON.stringify({type:"stop"})); }catch{} state.ws.close(); state.ws=null; }
 
398
  stopCtrlLoop();
399
  setPlaying(false);
400
  state.pressed.clear(); renderKeys();
 
39
  /* Stage */
40
  .stage{position:relative;aspect-ratio:1280/704;background:#05070c;border-radius:12px;
41
  overflow:hidden;border:1px solid var(--edge);display:grid;place-items:center}
42
+ .stage canvas{width:100%;height:100%;object-fit:cover;display:none;image-rendering:auto}
43
+ .stage.playing canvas{display:block}
44
  .overlay{position:absolute;inset:0;display:grid;place-items:center;text-align:center;
45
  padding:24px;background:rgba(5,7,12,.55);backdrop-filter:blur(2px)}
46
  .overlay.hidden{display:none}
 
125
  <!-- Stage -->
126
  <div class="card">
127
  <div class="stage" id="stage" tabindex="0">
128
+ <canvas id="view" width="1280" height="704"></canvas>
129
  <div class="hud">
130
  <div class="chip"><span class="dot" id="status-dot"></span><span id="status-label">Idle</span></div>
131
  <div class="chip"><span id="fps-value">0</span> fps · <span id="frame-value">0</span> blk</div>
 
223
  // --- DOM ---
224
  const $ = (id) => document.getElementById(id);
225
  const stage = $("stage"), view = $("view"), overlay = $("overlay");
226
+ const viewCtx = view.getContext("2d", { alpha: false, desynchronized: true });
227
  const spinner = $("spinner"), overlayTitle = $("overlay-title"), overlaySub = $("overlay-sub");
228
  const statusDot = $("status-dot"), statusLabel = $("status-label");
229
  const fpsValue = $("fps-value"), frameValue = $("frame-value");
 
293
  if(!state.client) state.client = await Client.connect(window.location.origin);
294
  }
295
 
296
+ // --- Frame decode + paint ---
297
+ // Decode each incoming JPEG and paint it to the canvas as soon as it is ready.
298
+ // We always keep only the FRESHEST pending frame (coalesce) so a slow decode
299
+ // can never build up a backlog / play stale buffered frames — the visible
300
+ // output tracks the real generation rate reported by the fps counter.
301
+ let pendingFrame = null; // {bytes, blk, fps} newest arrived, not yet decoded
302
+ let decoding = false;
303
+
304
+ async function drainFrames(){
305
+ if(decoding) return;
306
+ decoding = true;
307
+ try{
308
+ while(pendingFrame){
309
+ const { bytes, blk, fps } = pendingFrame;
310
+ pendingFrame = null; // grab the freshest, drop anything older
311
+ let painted = false;
312
+ try{
313
+ const bitmap = await createImageBitmap(new Blob([bytes], {type:"image/jpeg"}));
314
+ viewCtx.drawImage(bitmap, 0, 0, view.width, view.height);
315
+ if(bitmap.close) bitmap.close();
316
+ painted = true;
317
+ }catch(err){
318
+ // Fallback for browsers without createImageBitmap JPEG support.
319
+ painted = await paintViaImage(bytes);
320
+ }
321
+ if(painted){
322
+ if(!state.playing){ hideOverlay(); setPlaying(true); setStatus("playing","Playing"); }
323
+ frameValue.textContent = blk;
324
+ fpsValue.textContent = fps.toFixed(1);
325
+ }
326
+ }
327
+ }finally{
328
+ decoding = false;
329
+ }
330
+ }
331
+
332
+ function paintViaImage(bytes){
333
+ return new Promise((resolve)=>{
334
+ const url = URL.createObjectURL(new Blob([bytes], {type:"image/jpeg"}));
335
+ const im = new Image();
336
+ im.onload = ()=>{ viewCtx.drawImage(im, 0, 0, view.width, view.height); URL.revokeObjectURL(url); resolve(true); };
337
+ im.onerror = ()=>{ URL.revokeObjectURL(url); resolve(false); };
338
+ im.src = url;
339
+ });
340
+ }
341
+
342
  // --- WebSocket frame stream ---
343
  function connectWS(){
344
  const proto = location.protocol === "https:" ? "wss:" : "ws:";
 
349
  const dv = new DataView(e.data);
350
  const blk = dv.getUint32(0);
351
  const fps = dv.getUint32(4)/10;
352
+ // Store the newest frame's bytes and decode/paint immediately (coalescing
353
+ // any frame that hasn't been painted yet), so display keeps pace with fps.
354
+ pendingFrame = { bytes: e.data.slice(8), blk, fps };
355
+ drainFrames();
 
 
 
356
  }else{
357
  try{
358
  const msg = JSON.parse(e.data);
 
439
  async function stopWorld(){
440
  try{ if(state.client) await state.client.predict("/stop_game", { session_id: state.sessionId }); }catch{}
441
  if(state.ws){ try{ state.ws.send(JSON.stringify({type:"stop"})); }catch{} state.ws.close(); state.ws=null; }
442
+ pendingFrame = null;
443
  stopCtrlLoop();
444
  setPlaying(false);
445
  state.pressed.clear(); renderKeys();