faradayfuture Claude Opus 5.5 commited on
Commit
8d83ceb
·
1 Parent(s): fae2f7e

teleopSample viewers: WeChat-safe autoplay (top-frame WeixinJSBridge, no readyState deadlock, tap retry)

Browse files

Same fix as the egoSample viewers: in-app WebViews never reach readyState>=2
before play(), and WeixinJSBridge only exists in the top frame. Covers the
task episode pages, epN viewers, lib/viewer.js (faberU viewers) and clip.html.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

teleopSample/clip.html CHANGED
@@ -26,6 +26,51 @@ document.getElementById('dl').href = v;
26
  document.getElementById('dl').setAttribute('download', (t || 'clip').replace(/[^\w.-]+/g, '_') + '.mp4');
27
  document.getElementById('name').textContent = t;
28
  document.title = t || 'clip';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  document.getElementById('back').onclick = e => {
30
  e.preventDefault();
31
  if (back) location.href = back;
 
26
  document.getElementById('dl').setAttribute('download', (t || 'clip').replace(/[^\w.-]+/g, '_') + '.mp4');
27
  document.getElementById('name').textContent = t;
28
  document.title = t || 'clip';
29
+ /* autoplay that also works in in-app WebViews (WeChat iOS/Android X5): they ignore
30
+ preload, so readyState>=2 / canplay never arrive before play() — don't wait forever.
31
+ WeChat injects WeixinJSBridge into the top frame only (this page usually runs in
32
+ index.html's iframe) and play() from its callback needs no tap; if play() is still
33
+ refused, roll back to paused and retry on the first tap. ?wxdebug on the top URL
34
+ shows a log. start() must return the play() promise, stop() resets the UI. */
35
+ function wxAuto(vids,isPlaying,start,stop){
36
+ var inWx=/MicroMessenger/i.test(navigator.userAgent), t0=Date.now(), started=false, armed=false, wxTried=false, wxWait=false, iv=null, dbgEl=null;
37
+ function frames(){var ws=[];try{ws.push(window.top);}catch(e){}try{ws.push(window.parent);}catch(e){}ws.push(window);return ws;}
38
+ function dbg(m){try{if(!/wxdebug/.test(window.top.location.search+window.location.search))return;}catch(e){return;}
39
+ if(!dbgEl){dbgEl=document.createElement('pre');dbgEl.style.cssText='position:fixed;left:0;bottom:0;z-index:99999;max-height:40vh;overflow:auto;margin:0;padding:4px 6px;font:10px/1.3 monospace;background:rgba(0,0,0,.8);color:#7f7;pointer-events:none;white-space:pre-wrap';document.body.appendChild(dbgEl);}
40
+ dbgEl.textContent+=(performance.now()/1000).toFixed(2)+' '+m+'\n';}
41
+ function bridge(){var ws=frames();for(var i=0;i<ws.length;i++){try{if(ws[i].WeixinJSBridge&&ws[i].WeixinJSBridge.invoke)return ws[i].WeixinJSBridge;}catch(e){}}return null;}
42
+ function wxCall(cb){var b=bridge();dbg('bridge '+(b?'yes':'no')+' top='+(window.top===window));
43
+ if(b){b.invoke('getNetworkType',{},function(){dbg('bridge cb');cb();});return;}
44
+ function on(){var b=bridge();dbg('bridge ready '+(b?'yes':'no'));if(b)b.invoke('getNetworkType',{},function(){dbg('bridge cb');cb();});}
45
+ frames().forEach(function(w){try{w.document.addEventListener('WeixinJSBridgeReady',on,{once:true});}catch(e){}});}
46
+ function ready(vs){return vs.every(function(v){return v.readyState>=2;})||
47
+ (Date.now()-t0>1500&&vs.every(function(v){return v.readyState>=1||v.src;}));}
48
+ function go(force){
49
+ if(started||isPlaying())return;
50
+ var vs=vids(); if(!vs.length||(!force&&!ready(vs)))return;
51
+ if(wxWait&&!force)return;
52
+ if(inWx&&!wxTried){wxTried=wxWait=true;wxCall(function(){wxWait=false;go(true);});setTimeout(function(){wxWait=false;go(true);},1500);return;}
53
+ started=true; if(iv){clearInterval(iv);iv=null;}
54
+ vs.forEach(function(v){v.muted=true;});
55
+ dbg('play() rs='+vs.map(function(v){return v.readyState;}).join(','));
56
+ Promise.resolve(start()).then(function(){dbg('play ok');}).catch(function(err){
57
+ dbg('play rejected '+(err&&err.name)); try{stop();}catch(e){} started=false; arm(); });
58
+ }
59
+ function arm(){
60
+ if(armed)return; armed=true;
61
+ function once(ev){if(ev&&ev.target&&ev.target.closest&&ev.target.closest('#play'))return;
62
+ document.removeEventListener('touchend',once,true);document.removeEventListener('click',once,true);
63
+ armed=false;go(true);}
64
+ document.addEventListener('touchend',once,true);document.addEventListener('click',once,true);
65
+ wxCall(function(){go(true);});
66
+ }
67
+ vids().forEach(function(v){v.addEventListener('canplay',function(){go();});v.addEventListener('loadeddata',function(){go();});});
68
+ window.addEventListener('load',function(){go();});
69
+ iv=setInterval(go,400);
70
+ setTimeout(function(){if(iv){clearInterval(iv);iv=null;}},90000);
71
+ }
72
+ { const vv = document.getElementById('v');
73
+ wxAuto(() => [vv], () => !vv.paused, () => vv.play(), () => vv.pause()); }
74
  document.getElementById('back').onclick = e => {
75
  e.preventDefault();
76
  if (back) location.href = back;
teleopSample/drink_ep0.html CHANGED
@@ -226,17 +226,51 @@ CAMS.forEach(c=>{ const a=document.createElement('a'); a.className='file-chip';
226
  const a=document.createElement('a'); a.className='file-chip'; a.href=href; a.innerHTML=lbl; fm.appendChild(a); });
227
 
228
  // ---- autoplay (muted) + loop
229
- (function(){
230
- let started=false, iv=setInterval(tryStart,400);
231
- function tryStart(){
232
- if(started||playing) return;
233
- if(!vids.length||!vids.every(v=>v.readyState>=2)) return;
234
- started=true; clearInterval(iv);
235
- vids.forEach(v=>v.muted=true); setPlaying(true);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
  }
237
- vids.forEach(v=>{ v.addEventListener('canplay',tryStart); v.addEventListener('loadeddata',tryStart); });
238
- window.addEventListener('load',tryStart);
239
- setTimeout(()=>clearInterval(iv),90000);
 
 
 
 
240
  setInterval(()=>{ if(playing&&master&&master.currentTime>=DUR-0.08){
241
  try{master.currentTime=0;}catch(e){} syncSlaves(0,true);
242
  if(playing) vids.forEach(v=>v.play().catch(()=>{})); }},200);
 
226
  const a=document.createElement('a'); a.className='file-chip'; a.href=href; a.innerHTML=lbl; fm.appendChild(a); });
227
 
228
  // ---- autoplay (muted) + loop
229
+ /* autoplay that also works in in-app WebViews (WeChat iOS/Android X5): they ignore
230
+ preload, so readyState>=2 / canplay never arrive before play() — don't wait forever.
231
+ WeChat injects WeixinJSBridge into the top frame only (this page usually runs in
232
+ index.html's iframe) and play() from its callback needs no tap; if play() is still
233
+ refused, roll back to paused and retry on the first tap. ?wxdebug on the top URL
234
+ shows a log. start() must return the play() promise, stop() resets the UI. */
235
+ function wxAuto(vids,isPlaying,start,stop){
236
+ var inWx=/MicroMessenger/i.test(navigator.userAgent), t0=Date.now(), started=false, armed=false, wxTried=false, wxWait=false, iv=null, dbgEl=null;
237
+ function frames(){var ws=[];try{ws.push(window.top);}catch(e){}try{ws.push(window.parent);}catch(e){}ws.push(window);return ws;}
238
+ function dbg(m){try{if(!/wxdebug/.test(window.top.location.search+window.location.search))return;}catch(e){return;}
239
+ if(!dbgEl){dbgEl=document.createElement('pre');dbgEl.style.cssText='position:fixed;left:0;bottom:0;z-index:99999;max-height:40vh;overflow:auto;margin:0;padding:4px 6px;font:10px/1.3 monospace;background:rgba(0,0,0,.8);color:#7f7;pointer-events:none;white-space:pre-wrap';document.body.appendChild(dbgEl);}
240
+ dbgEl.textContent+=(performance.now()/1000).toFixed(2)+' '+m+'\n';}
241
+ function bridge(){var ws=frames();for(var i=0;i<ws.length;i++){try{if(ws[i].WeixinJSBridge&&ws[i].WeixinJSBridge.invoke)return ws[i].WeixinJSBridge;}catch(e){}}return null;}
242
+ function wxCall(cb){var b=bridge();dbg('bridge '+(b?'yes':'no')+' top='+(window.top===window));
243
+ if(b){b.invoke('getNetworkType',{},function(){dbg('bridge cb');cb();});return;}
244
+ function on(){var b=bridge();dbg('bridge ready '+(b?'yes':'no'));if(b)b.invoke('getNetworkType',{},function(){dbg('bridge cb');cb();});}
245
+ frames().forEach(function(w){try{w.document.addEventListener('WeixinJSBridgeReady',on,{once:true});}catch(e){}});}
246
+ function ready(vs){return vs.every(function(v){return v.readyState>=2;})||
247
+ (Date.now()-t0>1500&&vs.every(function(v){return v.readyState>=1||v.src;}));}
248
+ function go(force){
249
+ if(started||isPlaying())return;
250
+ var vs=vids(); if(!vs.length||(!force&&!ready(vs)))return;
251
+ if(wxWait&&!force)return;
252
+ if(inWx&&!wxTried){wxTried=wxWait=true;wxCall(function(){wxWait=false;go(true);});setTimeout(function(){wxWait=false;go(true);},1500);return;}
253
+ started=true; if(iv){clearInterval(iv);iv=null;}
254
+ vs.forEach(function(v){v.muted=true;});
255
+ dbg('play() rs='+vs.map(function(v){return v.readyState;}).join(','));
256
+ Promise.resolve(start()).then(function(){dbg('play ok');}).catch(function(err){
257
+ dbg('play rejected '+(err&&err.name)); try{stop();}catch(e){} started=false; arm(); });
258
+ }
259
+ function arm(){
260
+ if(armed)return; armed=true;
261
+ function once(ev){if(ev&&ev.target&&ev.target.closest&&ev.target.closest('#play'))return;
262
+ document.removeEventListener('touchend',once,true);document.removeEventListener('click',once,true);
263
+ armed=false;go(true);}
264
+ document.addEventListener('touchend',once,true);document.addEventListener('click',once,true);
265
+ wxCall(function(){go(true);});
266
  }
267
+ vids().forEach(function(v){v.addEventListener('canplay',function(){go();});v.addEventListener('loadeddata',function(){go();});});
268
+ window.addEventListener('load',function(){go();});
269
+ iv=setInterval(go,400);
270
+ setTimeout(function(){if(iv){clearInterval(iv);iv=null;}},90000);
271
+ }
272
+ (function(){
273
+ wxAuto(()=>vids, ()=>playing, ()=>{ setPlaying(true); return master.play(); }, ()=>setPlaying(false));
274
  setInterval(()=>{ if(playing&&master&&master.currentTime>=DUR-0.08){
275
  try{master.currentTime=0;}catch(e){} syncSlaves(0,true);
276
  if(playing) vids.forEach(v=>v.play().catch(()=>{})); }},200);
teleopSample/drink_ep1.html CHANGED
@@ -226,17 +226,51 @@ CAMS.forEach(c=>{ const a=document.createElement('a'); a.className='file-chip';
226
  const a=document.createElement('a'); a.className='file-chip'; a.href=href; a.innerHTML=lbl; fm.appendChild(a); });
227
 
228
  // ---- autoplay (muted) + loop
229
- (function(){
230
- let started=false, iv=setInterval(tryStart,400);
231
- function tryStart(){
232
- if(started||playing) return;
233
- if(!vids.length||!vids.every(v=>v.readyState>=2)) return;
234
- started=true; clearInterval(iv);
235
- vids.forEach(v=>v.muted=true); setPlaying(true);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
  }
237
- vids.forEach(v=>{ v.addEventListener('canplay',tryStart); v.addEventListener('loadeddata',tryStart); });
238
- window.addEventListener('load',tryStart);
239
- setTimeout(()=>clearInterval(iv),90000);
 
 
 
 
240
  setInterval(()=>{ if(playing&&master&&master.currentTime>=DUR-0.08){
241
  try{master.currentTime=0;}catch(e){} syncSlaves(0,true);
242
  if(playing) vids.forEach(v=>v.play().catch(()=>{})); }},200);
 
226
  const a=document.createElement('a'); a.className='file-chip'; a.href=href; a.innerHTML=lbl; fm.appendChild(a); });
227
 
228
  // ---- autoplay (muted) + loop
229
+ /* autoplay that also works in in-app WebViews (WeChat iOS/Android X5): they ignore
230
+ preload, so readyState>=2 / canplay never arrive before play() — don't wait forever.
231
+ WeChat injects WeixinJSBridge into the top frame only (this page usually runs in
232
+ index.html's iframe) and play() from its callback needs no tap; if play() is still
233
+ refused, roll back to paused and retry on the first tap. ?wxdebug on the top URL
234
+ shows a log. start() must return the play() promise, stop() resets the UI. */
235
+ function wxAuto(vids,isPlaying,start,stop){
236
+ var inWx=/MicroMessenger/i.test(navigator.userAgent), t0=Date.now(), started=false, armed=false, wxTried=false, wxWait=false, iv=null, dbgEl=null;
237
+ function frames(){var ws=[];try{ws.push(window.top);}catch(e){}try{ws.push(window.parent);}catch(e){}ws.push(window);return ws;}
238
+ function dbg(m){try{if(!/wxdebug/.test(window.top.location.search+window.location.search))return;}catch(e){return;}
239
+ if(!dbgEl){dbgEl=document.createElement('pre');dbgEl.style.cssText='position:fixed;left:0;bottom:0;z-index:99999;max-height:40vh;overflow:auto;margin:0;padding:4px 6px;font:10px/1.3 monospace;background:rgba(0,0,0,.8);color:#7f7;pointer-events:none;white-space:pre-wrap';document.body.appendChild(dbgEl);}
240
+ dbgEl.textContent+=(performance.now()/1000).toFixed(2)+' '+m+'\n';}
241
+ function bridge(){var ws=frames();for(var i=0;i<ws.length;i++){try{if(ws[i].WeixinJSBridge&&ws[i].WeixinJSBridge.invoke)return ws[i].WeixinJSBridge;}catch(e){}}return null;}
242
+ function wxCall(cb){var b=bridge();dbg('bridge '+(b?'yes':'no')+' top='+(window.top===window));
243
+ if(b){b.invoke('getNetworkType',{},function(){dbg('bridge cb');cb();});return;}
244
+ function on(){var b=bridge();dbg('bridge ready '+(b?'yes':'no'));if(b)b.invoke('getNetworkType',{},function(){dbg('bridge cb');cb();});}
245
+ frames().forEach(function(w){try{w.document.addEventListener('WeixinJSBridgeReady',on,{once:true});}catch(e){}});}
246
+ function ready(vs){return vs.every(function(v){return v.readyState>=2;})||
247
+ (Date.now()-t0>1500&&vs.every(function(v){return v.readyState>=1||v.src;}));}
248
+ function go(force){
249
+ if(started||isPlaying())return;
250
+ var vs=vids(); if(!vs.length||(!force&&!ready(vs)))return;
251
+ if(wxWait&&!force)return;
252
+ if(inWx&&!wxTried){wxTried=wxWait=true;wxCall(function(){wxWait=false;go(true);});setTimeout(function(){wxWait=false;go(true);},1500);return;}
253
+ started=true; if(iv){clearInterval(iv);iv=null;}
254
+ vs.forEach(function(v){v.muted=true;});
255
+ dbg('play() rs='+vs.map(function(v){return v.readyState;}).join(','));
256
+ Promise.resolve(start()).then(function(){dbg('play ok');}).catch(function(err){
257
+ dbg('play rejected '+(err&&err.name)); try{stop();}catch(e){} started=false; arm(); });
258
+ }
259
+ function arm(){
260
+ if(armed)return; armed=true;
261
+ function once(ev){if(ev&&ev.target&&ev.target.closest&&ev.target.closest('#play'))return;
262
+ document.removeEventListener('touchend',once,true);document.removeEventListener('click',once,true);
263
+ armed=false;go(true);}
264
+ document.addEventListener('touchend',once,true);document.addEventListener('click',once,true);
265
+ wxCall(function(){go(true);});
266
  }
267
+ vids().forEach(function(v){v.addEventListener('canplay',function(){go();});v.addEventListener('loadeddata',function(){go();});});
268
+ window.addEventListener('load',function(){go();});
269
+ iv=setInterval(go,400);
270
+ setTimeout(function(){if(iv){clearInterval(iv);iv=null;}},90000);
271
+ }
272
+ (function(){
273
+ wxAuto(()=>vids, ()=>playing, ()=>{ setPlaying(true); return master.play(); }, ()=>setPlaying(false));
274
  setInterval(()=>{ if(playing&&master&&master.currentTime>=DUR-0.08){
275
  try{master.currentTime=0;}catch(e){} syncSlaves(0,true);
276
  if(playing) vids.forEach(v=>v.play().catch(()=>{})); }},200);
teleopSample/ep0/viewer.html CHANGED
@@ -380,28 +380,53 @@ window.addEventListener('load',update);
380
  button. Videos are muted, which is what browser autoplay policies require;
381
  if a browser still refuses, we roll back to the paused state so the manual
382
  button keeps working exactly as before. */
383
- (function(){
384
- var started=false, iv=null;
385
- function vlist(){try{return videos;}catch(e){return [];}}
386
- function autoStart(){
387
- if(started||playing)return;
388
- var vs=vlist();
389
- if(!vs.length||!vs.every(function(v){return v.readyState>=2;}))return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
  started=true; if(iv){clearInterval(iv);iv=null;}
391
  vs.forEach(function(v){v.muted=true;});
392
- Promise.all(vs.map(function(v){return Promise.resolve(v.play());})).then(function(){
393
- playing=true; playBtn.textContent='\u275a\u275a'; requestAnimationFrame(loop);
394
- }).catch(function(){
395
- vs.forEach(function(v){try{v.pause();}catch(e){}}); playing=false; playBtn.textContent='\u25b6\ufe0e';
396
- });
397
  }
398
- vlist().forEach(function(v){
399
- v.addEventListener('canplay',autoStart); v.addEventListener('loadeddata',autoStart);
400
- });
401
- window.addEventListener('load',autoStart);
402
- iv=setInterval(autoStart,400);
 
 
 
 
 
 
403
  setTimeout(function(){if(iv){clearInterval(iv);iv=null;}},90000);
404
- })();
 
 
 
 
405
 
406
  /* demo loop: when the clip ends, wrap every video back to the start and keep playing.
407
  ended is the primary trigger; the interval is a fallback for decoders that never fire it. */
 
380
  button. Videos are muted, which is what browser autoplay policies require;
381
  if a browser still refuses, we roll back to the paused state so the manual
382
  button keeps working exactly as before. */
383
+ /* autoplay that also works in in-app WebViews (WeChat iOS/Android X5): they ignore
384
+ preload, so readyState>=2 / canplay never arrive before play() — don't wait forever.
385
+ WeChat injects WeixinJSBridge into the top frame only (this page usually runs in
386
+ index.html's iframe) and play() from its callback needs no tap; if play() is still
387
+ refused, roll back to paused and retry on the first tap. ?wxdebug on the top URL
388
+ shows a log. start() must return the play() promise, stop() resets the UI. */
389
+ function wxAuto(vids,isPlaying,start,stop){
390
+ var inWx=/MicroMessenger/i.test(navigator.userAgent), t0=Date.now(), started=false, armed=false, wxTried=false, wxWait=false, iv=null, dbgEl=null;
391
+ function frames(){var ws=[];try{ws.push(window.top);}catch(e){}try{ws.push(window.parent);}catch(e){}ws.push(window);return ws;}
392
+ function dbg(m){try{if(!/wxdebug/.test(window.top.location.search+window.location.search))return;}catch(e){return;}
393
+ if(!dbgEl){dbgEl=document.createElement('pre');dbgEl.style.cssText='position:fixed;left:0;bottom:0;z-index:99999;max-height:40vh;overflow:auto;margin:0;padding:4px 6px;font:10px/1.3 monospace;background:rgba(0,0,0,.8);color:#7f7;pointer-events:none;white-space:pre-wrap';document.body.appendChild(dbgEl);}
394
+ dbgEl.textContent+=(performance.now()/1000).toFixed(2)+' '+m+'\n';}
395
+ function bridge(){var ws=frames();for(var i=0;i<ws.length;i++){try{if(ws[i].WeixinJSBridge&&ws[i].WeixinJSBridge.invoke)return ws[i].WeixinJSBridge;}catch(e){}}return null;}
396
+ function wxCall(cb){var b=bridge();dbg('bridge '+(b?'yes':'no')+' top='+(window.top===window));
397
+ if(b){b.invoke('getNetworkType',{},function(){dbg('bridge cb');cb();});return;}
398
+ function on(){var b=bridge();dbg('bridge ready '+(b?'yes':'no'));if(b)b.invoke('getNetworkType',{},function(){dbg('bridge cb');cb();});}
399
+ frames().forEach(function(w){try{w.document.addEventListener('WeixinJSBridgeReady',on,{once:true});}catch(e){}});}
400
+ function ready(vs){return vs.every(function(v){return v.readyState>=2;})||
401
+ (Date.now()-t0>1500&&vs.every(function(v){return v.readyState>=1||v.src;}));}
402
+ function go(force){
403
+ if(started||isPlaying())return;
404
+ var vs=vids(); if(!vs.length||(!force&&!ready(vs)))return;
405
+ if(wxWait&&!force)return;
406
+ if(inWx&&!wxTried){wxTried=wxWait=true;wxCall(function(){wxWait=false;go(true);});setTimeout(function(){wxWait=false;go(true);},1500);return;}
407
  started=true; if(iv){clearInterval(iv);iv=null;}
408
  vs.forEach(function(v){v.muted=true;});
409
+ dbg('play() rs='+vs.map(function(v){return v.readyState;}).join(','));
410
+ Promise.resolve(start()).then(function(){dbg('play ok');}).catch(function(err){
411
+ dbg('play rejected '+(err&&err.name)); try{stop();}catch(e){} started=false; arm(); });
 
 
412
  }
413
+ function arm(){
414
+ if(armed)return; armed=true;
415
+ function once(ev){if(ev&&ev.target&&ev.target.closest&&ev.target.closest('#play'))return;
416
+ document.removeEventListener('touchend',once,true);document.removeEventListener('click',once,true);
417
+ armed=false;go(true);}
418
+ document.addEventListener('touchend',once,true);document.addEventListener('click',once,true);
419
+ wxCall(function(){go(true);});
420
+ }
421
+ vids().forEach(function(v){v.addEventListener('canplay',function(){go();});v.addEventListener('loadeddata',function(){go();});});
422
+ window.addEventListener('load',function(){go();});
423
+ iv=setInterval(go,400);
424
  setTimeout(function(){if(iv){clearInterval(iv);iv=null;}},90000);
425
+ }
426
+ wxAuto(function(){try{return videos;}catch(e){return [];}}, function(){return playing;},
427
+ function(){ return Promise.all(videos.map(function(v){return Promise.resolve(v.play());})).then(function(){
428
+ playing=true; playBtn.textContent='\u275a\u275a'; requestAnimationFrame(loop); }); },
429
+ function(){ videos.forEach(function(v){try{v.pause();}catch(e){}}); playing=false; playBtn.textContent='\u25b6\ufe0e'; });
430
 
431
  /* demo loop: when the clip ends, wrap every video back to the start and keep playing.
432
  ended is the primary trigger; the interval is a fallback for decoders that never fire it. */
teleopSample/ep1/viewer.html CHANGED
@@ -380,28 +380,53 @@ window.addEventListener('load',update);
380
  button. Videos are muted, which is what browser autoplay policies require;
381
  if a browser still refuses, we roll back to the paused state so the manual
382
  button keeps working exactly as before. */
383
- (function(){
384
- var started=false, iv=null;
385
- function vlist(){try{return videos;}catch(e){return [];}}
386
- function autoStart(){
387
- if(started||playing)return;
388
- var vs=vlist();
389
- if(!vs.length||!vs.every(function(v){return v.readyState>=2;}))return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
  started=true; if(iv){clearInterval(iv);iv=null;}
391
  vs.forEach(function(v){v.muted=true;});
392
- Promise.all(vs.map(function(v){return Promise.resolve(v.play());})).then(function(){
393
- playing=true; playBtn.textContent='\u275a\u275a'; requestAnimationFrame(loop);
394
- }).catch(function(){
395
- vs.forEach(function(v){try{v.pause();}catch(e){}}); playing=false; playBtn.textContent='\u25b6\ufe0e';
396
- });
397
  }
398
- vlist().forEach(function(v){
399
- v.addEventListener('canplay',autoStart); v.addEventListener('loadeddata',autoStart);
400
- });
401
- window.addEventListener('load',autoStart);
402
- iv=setInterval(autoStart,400);
 
 
 
 
 
 
403
  setTimeout(function(){if(iv){clearInterval(iv);iv=null;}},90000);
404
- })();
 
 
 
 
405
 
406
  /* demo loop: when the clip ends, wrap every video back to the start and keep playing.
407
  ended is the primary trigger; the interval is a fallback for decoders that never fire it. */
 
380
  button. Videos are muted, which is what browser autoplay policies require;
381
  if a browser still refuses, we roll back to the paused state so the manual
382
  button keeps working exactly as before. */
383
+ /* autoplay that also works in in-app WebViews (WeChat iOS/Android X5): they ignore
384
+ preload, so readyState>=2 / canplay never arrive before play() — don't wait forever.
385
+ WeChat injects WeixinJSBridge into the top frame only (this page usually runs in
386
+ index.html's iframe) and play() from its callback needs no tap; if play() is still
387
+ refused, roll back to paused and retry on the first tap. ?wxdebug on the top URL
388
+ shows a log. start() must return the play() promise, stop() resets the UI. */
389
+ function wxAuto(vids,isPlaying,start,stop){
390
+ var inWx=/MicroMessenger/i.test(navigator.userAgent), t0=Date.now(), started=false, armed=false, wxTried=false, wxWait=false, iv=null, dbgEl=null;
391
+ function frames(){var ws=[];try{ws.push(window.top);}catch(e){}try{ws.push(window.parent);}catch(e){}ws.push(window);return ws;}
392
+ function dbg(m){try{if(!/wxdebug/.test(window.top.location.search+window.location.search))return;}catch(e){return;}
393
+ if(!dbgEl){dbgEl=document.createElement('pre');dbgEl.style.cssText='position:fixed;left:0;bottom:0;z-index:99999;max-height:40vh;overflow:auto;margin:0;padding:4px 6px;font:10px/1.3 monospace;background:rgba(0,0,0,.8);color:#7f7;pointer-events:none;white-space:pre-wrap';document.body.appendChild(dbgEl);}
394
+ dbgEl.textContent+=(performance.now()/1000).toFixed(2)+' '+m+'\n';}
395
+ function bridge(){var ws=frames();for(var i=0;i<ws.length;i++){try{if(ws[i].WeixinJSBridge&&ws[i].WeixinJSBridge.invoke)return ws[i].WeixinJSBridge;}catch(e){}}return null;}
396
+ function wxCall(cb){var b=bridge();dbg('bridge '+(b?'yes':'no')+' top='+(window.top===window));
397
+ if(b){b.invoke('getNetworkType',{},function(){dbg('bridge cb');cb();});return;}
398
+ function on(){var b=bridge();dbg('bridge ready '+(b?'yes':'no'));if(b)b.invoke('getNetworkType',{},function(){dbg('bridge cb');cb();});}
399
+ frames().forEach(function(w){try{w.document.addEventListener('WeixinJSBridgeReady',on,{once:true});}catch(e){}});}
400
+ function ready(vs){return vs.every(function(v){return v.readyState>=2;})||
401
+ (Date.now()-t0>1500&&vs.every(function(v){return v.readyState>=1||v.src;}));}
402
+ function go(force){
403
+ if(started||isPlaying())return;
404
+ var vs=vids(); if(!vs.length||(!force&&!ready(vs)))return;
405
+ if(wxWait&&!force)return;
406
+ if(inWx&&!wxTried){wxTried=wxWait=true;wxCall(function(){wxWait=false;go(true);});setTimeout(function(){wxWait=false;go(true);},1500);return;}
407
  started=true; if(iv){clearInterval(iv);iv=null;}
408
  vs.forEach(function(v){v.muted=true;});
409
+ dbg('play() rs='+vs.map(function(v){return v.readyState;}).join(','));
410
+ Promise.resolve(start()).then(function(){dbg('play ok');}).catch(function(err){
411
+ dbg('play rejected '+(err&&err.name)); try{stop();}catch(e){} started=false; arm(); });
 
 
412
  }
413
+ function arm(){
414
+ if(armed)return; armed=true;
415
+ function once(ev){if(ev&&ev.target&&ev.target.closest&&ev.target.closest('#play'))return;
416
+ document.removeEventListener('touchend',once,true);document.removeEventListener('click',once,true);
417
+ armed=false;go(true);}
418
+ document.addEventListener('touchend',once,true);document.addEventListener('click',once,true);
419
+ wxCall(function(){go(true);});
420
+ }
421
+ vids().forEach(function(v){v.addEventListener('canplay',function(){go();});v.addEventListener('loadeddata',function(){go();});});
422
+ window.addEventListener('load',function(){go();});
423
+ iv=setInterval(go,400);
424
  setTimeout(function(){if(iv){clearInterval(iv);iv=null;}},90000);
425
+ }
426
+ wxAuto(function(){try{return videos;}catch(e){return [];}}, function(){return playing;},
427
+ function(){ return Promise.all(videos.map(function(v){return Promise.resolve(v.play());})).then(function(){
428
+ playing=true; playBtn.textContent='\u275a\u275a'; requestAnimationFrame(loop); }); },
429
+ function(){ videos.forEach(function(v){try{v.pause();}catch(e){}}); playing=false; playBtn.textContent='\u25b6\ufe0e'; });
430
 
431
  /* demo loop: when the clip ends, wrap every video back to the start and keep playing.
432
  ended is the primary trigger; the interval is a fallback for decoders that never fire it. */
teleopSample/ep2/viewer.html CHANGED
@@ -380,28 +380,53 @@ window.addEventListener('load',update);
380
  button. Videos are muted, which is what browser autoplay policies require;
381
  if a browser still refuses, we roll back to the paused state so the manual
382
  button keeps working exactly as before. */
383
- (function(){
384
- var started=false, iv=null;
385
- function vlist(){try{return videos;}catch(e){return [];}}
386
- function autoStart(){
387
- if(started||playing)return;
388
- var vs=vlist();
389
- if(!vs.length||!vs.every(function(v){return v.readyState>=2;}))return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
  started=true; if(iv){clearInterval(iv);iv=null;}
391
  vs.forEach(function(v){v.muted=true;});
392
- Promise.all(vs.map(function(v){return Promise.resolve(v.play());})).then(function(){
393
- playing=true; playBtn.textContent='\u275a\u275a'; requestAnimationFrame(loop);
394
- }).catch(function(){
395
- vs.forEach(function(v){try{v.pause();}catch(e){}}); playing=false; playBtn.textContent='\u25b6\ufe0e';
396
- });
397
  }
398
- vlist().forEach(function(v){
399
- v.addEventListener('canplay',autoStart); v.addEventListener('loadeddata',autoStart);
400
- });
401
- window.addEventListener('load',autoStart);
402
- iv=setInterval(autoStart,400);
 
 
 
 
 
 
403
  setTimeout(function(){if(iv){clearInterval(iv);iv=null;}},90000);
404
- })();
 
 
 
 
405
 
406
  /* demo loop: when the clip ends, wrap every video back to the start and keep playing.
407
  ended is the primary trigger; the interval is a fallback for decoders that never fire it. */
 
380
  button. Videos are muted, which is what browser autoplay policies require;
381
  if a browser still refuses, we roll back to the paused state so the manual
382
  button keeps working exactly as before. */
383
+ /* autoplay that also works in in-app WebViews (WeChat iOS/Android X5): they ignore
384
+ preload, so readyState>=2 / canplay never arrive before play() — don't wait forever.
385
+ WeChat injects WeixinJSBridge into the top frame only (this page usually runs in
386
+ index.html's iframe) and play() from its callback needs no tap; if play() is still
387
+ refused, roll back to paused and retry on the first tap. ?wxdebug on the top URL
388
+ shows a log. start() must return the play() promise, stop() resets the UI. */
389
+ function wxAuto(vids,isPlaying,start,stop){
390
+ var inWx=/MicroMessenger/i.test(navigator.userAgent), t0=Date.now(), started=false, armed=false, wxTried=false, wxWait=false, iv=null, dbgEl=null;
391
+ function frames(){var ws=[];try{ws.push(window.top);}catch(e){}try{ws.push(window.parent);}catch(e){}ws.push(window);return ws;}
392
+ function dbg(m){try{if(!/wxdebug/.test(window.top.location.search+window.location.search))return;}catch(e){return;}
393
+ if(!dbgEl){dbgEl=document.createElement('pre');dbgEl.style.cssText='position:fixed;left:0;bottom:0;z-index:99999;max-height:40vh;overflow:auto;margin:0;padding:4px 6px;font:10px/1.3 monospace;background:rgba(0,0,0,.8);color:#7f7;pointer-events:none;white-space:pre-wrap';document.body.appendChild(dbgEl);}
394
+ dbgEl.textContent+=(performance.now()/1000).toFixed(2)+' '+m+'\n';}
395
+ function bridge(){var ws=frames();for(var i=0;i<ws.length;i++){try{if(ws[i].WeixinJSBridge&&ws[i].WeixinJSBridge.invoke)return ws[i].WeixinJSBridge;}catch(e){}}return null;}
396
+ function wxCall(cb){var b=bridge();dbg('bridge '+(b?'yes':'no')+' top='+(window.top===window));
397
+ if(b){b.invoke('getNetworkType',{},function(){dbg('bridge cb');cb();});return;}
398
+ function on(){var b=bridge();dbg('bridge ready '+(b?'yes':'no'));if(b)b.invoke('getNetworkType',{},function(){dbg('bridge cb');cb();});}
399
+ frames().forEach(function(w){try{w.document.addEventListener('WeixinJSBridgeReady',on,{once:true});}catch(e){}});}
400
+ function ready(vs){return vs.every(function(v){return v.readyState>=2;})||
401
+ (Date.now()-t0>1500&&vs.every(function(v){return v.readyState>=1||v.src;}));}
402
+ function go(force){
403
+ if(started||isPlaying())return;
404
+ var vs=vids(); if(!vs.length||(!force&&!ready(vs)))return;
405
+ if(wxWait&&!force)return;
406
+ if(inWx&&!wxTried){wxTried=wxWait=true;wxCall(function(){wxWait=false;go(true);});setTimeout(function(){wxWait=false;go(true);},1500);return;}
407
  started=true; if(iv){clearInterval(iv);iv=null;}
408
  vs.forEach(function(v){v.muted=true;});
409
+ dbg('play() rs='+vs.map(function(v){return v.readyState;}).join(','));
410
+ Promise.resolve(start()).then(function(){dbg('play ok');}).catch(function(err){
411
+ dbg('play rejected '+(err&&err.name)); try{stop();}catch(e){} started=false; arm(); });
 
 
412
  }
413
+ function arm(){
414
+ if(armed)return; armed=true;
415
+ function once(ev){if(ev&&ev.target&&ev.target.closest&&ev.target.closest('#play'))return;
416
+ document.removeEventListener('touchend',once,true);document.removeEventListener('click',once,true);
417
+ armed=false;go(true);}
418
+ document.addEventListener('touchend',once,true);document.addEventListener('click',once,true);
419
+ wxCall(function(){go(true);});
420
+ }
421
+ vids().forEach(function(v){v.addEventListener('canplay',function(){go();});v.addEventListener('loadeddata',function(){go();});});
422
+ window.addEventListener('load',function(){go();});
423
+ iv=setInterval(go,400);
424
  setTimeout(function(){if(iv){clearInterval(iv);iv=null;}},90000);
425
+ }
426
+ wxAuto(function(){try{return videos;}catch(e){return [];}}, function(){return playing;},
427
+ function(){ return Promise.all(videos.map(function(v){return Promise.resolve(v.play());})).then(function(){
428
+ playing=true; playBtn.textContent='\u275a\u275a'; requestAnimationFrame(loop); }); },
429
+ function(){ videos.forEach(function(v){try{v.pause();}catch(e){}}); playing=false; playBtn.textContent='\u25b6\ufe0e'; });
430
 
431
  /* demo loop: when the clip ends, wrap every video back to the start and keep playing.
432
  ended is the primary trigger; the interval is a fallback for decoders that never fire it. */
teleopSample/ep3/viewer.html CHANGED
@@ -380,28 +380,53 @@ window.addEventListener('load',update);
380
  button. Videos are muted, which is what browser autoplay policies require;
381
  if a browser still refuses, we roll back to the paused state so the manual
382
  button keeps working exactly as before. */
383
- (function(){
384
- var started=false, iv=null;
385
- function vlist(){try{return videos;}catch(e){return [];}}
386
- function autoStart(){
387
- if(started||playing)return;
388
- var vs=vlist();
389
- if(!vs.length||!vs.every(function(v){return v.readyState>=2;}))return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
  started=true; if(iv){clearInterval(iv);iv=null;}
391
  vs.forEach(function(v){v.muted=true;});
392
- Promise.all(vs.map(function(v){return Promise.resolve(v.play());})).then(function(){
393
- playing=true; playBtn.textContent='\u275a\u275a'; requestAnimationFrame(loop);
394
- }).catch(function(){
395
- vs.forEach(function(v){try{v.pause();}catch(e){}}); playing=false; playBtn.textContent='\u25b6\ufe0e';
396
- });
397
  }
398
- vlist().forEach(function(v){
399
- v.addEventListener('canplay',autoStart); v.addEventListener('loadeddata',autoStart);
400
- });
401
- window.addEventListener('load',autoStart);
402
- iv=setInterval(autoStart,400);
 
 
 
 
 
 
403
  setTimeout(function(){if(iv){clearInterval(iv);iv=null;}},90000);
404
- })();
 
 
 
 
405
 
406
  /* demo loop: when the clip ends, wrap every video back to the start and keep playing.
407
  ended is the primary trigger; the interval is a fallback for decoders that never fire it. */
 
380
  button. Videos are muted, which is what browser autoplay policies require;
381
  if a browser still refuses, we roll back to the paused state so the manual
382
  button keeps working exactly as before. */
383
+ /* autoplay that also works in in-app WebViews (WeChat iOS/Android X5): they ignore
384
+ preload, so readyState>=2 / canplay never arrive before play() — don't wait forever.
385
+ WeChat injects WeixinJSBridge into the top frame only (this page usually runs in
386
+ index.html's iframe) and play() from its callback needs no tap; if play() is still
387
+ refused, roll back to paused and retry on the first tap. ?wxdebug on the top URL
388
+ shows a log. start() must return the play() promise, stop() resets the UI. */
389
+ function wxAuto(vids,isPlaying,start,stop){
390
+ var inWx=/MicroMessenger/i.test(navigator.userAgent), t0=Date.now(), started=false, armed=false, wxTried=false, wxWait=false, iv=null, dbgEl=null;
391
+ function frames(){var ws=[];try{ws.push(window.top);}catch(e){}try{ws.push(window.parent);}catch(e){}ws.push(window);return ws;}
392
+ function dbg(m){try{if(!/wxdebug/.test(window.top.location.search+window.location.search))return;}catch(e){return;}
393
+ if(!dbgEl){dbgEl=document.createElement('pre');dbgEl.style.cssText='position:fixed;left:0;bottom:0;z-index:99999;max-height:40vh;overflow:auto;margin:0;padding:4px 6px;font:10px/1.3 monospace;background:rgba(0,0,0,.8);color:#7f7;pointer-events:none;white-space:pre-wrap';document.body.appendChild(dbgEl);}
394
+ dbgEl.textContent+=(performance.now()/1000).toFixed(2)+' '+m+'\n';}
395
+ function bridge(){var ws=frames();for(var i=0;i<ws.length;i++){try{if(ws[i].WeixinJSBridge&&ws[i].WeixinJSBridge.invoke)return ws[i].WeixinJSBridge;}catch(e){}}return null;}
396
+ function wxCall(cb){var b=bridge();dbg('bridge '+(b?'yes':'no')+' top='+(window.top===window));
397
+ if(b){b.invoke('getNetworkType',{},function(){dbg('bridge cb');cb();});return;}
398
+ function on(){var b=bridge();dbg('bridge ready '+(b?'yes':'no'));if(b)b.invoke('getNetworkType',{},function(){dbg('bridge cb');cb();});}
399
+ frames().forEach(function(w){try{w.document.addEventListener('WeixinJSBridgeReady',on,{once:true});}catch(e){}});}
400
+ function ready(vs){return vs.every(function(v){return v.readyState>=2;})||
401
+ (Date.now()-t0>1500&&vs.every(function(v){return v.readyState>=1||v.src;}));}
402
+ function go(force){
403
+ if(started||isPlaying())return;
404
+ var vs=vids(); if(!vs.length||(!force&&!ready(vs)))return;
405
+ if(wxWait&&!force)return;
406
+ if(inWx&&!wxTried){wxTried=wxWait=true;wxCall(function(){wxWait=false;go(true);});setTimeout(function(){wxWait=false;go(true);},1500);return;}
407
  started=true; if(iv){clearInterval(iv);iv=null;}
408
  vs.forEach(function(v){v.muted=true;});
409
+ dbg('play() rs='+vs.map(function(v){return v.readyState;}).join(','));
410
+ Promise.resolve(start()).then(function(){dbg('play ok');}).catch(function(err){
411
+ dbg('play rejected '+(err&&err.name)); try{stop();}catch(e){} started=false; arm(); });
 
 
412
  }
413
+ function arm(){
414
+ if(armed)return; armed=true;
415
+ function once(ev){if(ev&&ev.target&&ev.target.closest&&ev.target.closest('#play'))return;
416
+ document.removeEventListener('touchend',once,true);document.removeEventListener('click',once,true);
417
+ armed=false;go(true);}
418
+ document.addEventListener('touchend',once,true);document.addEventListener('click',once,true);
419
+ wxCall(function(){go(true);});
420
+ }
421
+ vids().forEach(function(v){v.addEventListener('canplay',function(){go();});v.addEventListener('loadeddata',function(){go();});});
422
+ window.addEventListener('load',function(){go();});
423
+ iv=setInterval(go,400);
424
  setTimeout(function(){if(iv){clearInterval(iv);iv=null;}},90000);
425
+ }
426
+ wxAuto(function(){try{return videos;}catch(e){return [];}}, function(){return playing;},
427
+ function(){ return Promise.all(videos.map(function(v){return Promise.resolve(v.play());})).then(function(){
428
+ playing=true; playBtn.textContent='\u275a\u275a'; requestAnimationFrame(loop); }); },
429
+ function(){ videos.forEach(function(v){try{v.pause();}catch(e){}}); playing=false; playBtn.textContent='\u25b6\ufe0e'; });
430
 
431
  /* demo loop: when the clip ends, wrap every video back to the start and keep playing.
432
  ended is the primary trigger; the interval is a fallback for decoders that never fire it. */
teleopSample/ep4/viewer.html CHANGED
@@ -380,28 +380,53 @@ window.addEventListener('load',update);
380
  button. Videos are muted, which is what browser autoplay policies require;
381
  if a browser still refuses, we roll back to the paused state so the manual
382
  button keeps working exactly as before. */
383
- (function(){
384
- var started=false, iv=null;
385
- function vlist(){try{return videos;}catch(e){return [];}}
386
- function autoStart(){
387
- if(started||playing)return;
388
- var vs=vlist();
389
- if(!vs.length||!vs.every(function(v){return v.readyState>=2;}))return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
  started=true; if(iv){clearInterval(iv);iv=null;}
391
  vs.forEach(function(v){v.muted=true;});
392
- Promise.all(vs.map(function(v){return Promise.resolve(v.play());})).then(function(){
393
- playing=true; playBtn.textContent='\u275a\u275a'; requestAnimationFrame(loop);
394
- }).catch(function(){
395
- vs.forEach(function(v){try{v.pause();}catch(e){}}); playing=false; playBtn.textContent='\u25b6\ufe0e';
396
- });
397
  }
398
- vlist().forEach(function(v){
399
- v.addEventListener('canplay',autoStart); v.addEventListener('loadeddata',autoStart);
400
- });
401
- window.addEventListener('load',autoStart);
402
- iv=setInterval(autoStart,400);
 
 
 
 
 
 
403
  setTimeout(function(){if(iv){clearInterval(iv);iv=null;}},90000);
404
- })();
 
 
 
 
405
 
406
  /* demo loop: when the clip ends, wrap every video back to the start and keep playing.
407
  ended is the primary trigger; the interval is a fallback for decoders that never fire it. */
 
380
  button. Videos are muted, which is what browser autoplay policies require;
381
  if a browser still refuses, we roll back to the paused state so the manual
382
  button keeps working exactly as before. */
383
+ /* autoplay that also works in in-app WebViews (WeChat iOS/Android X5): they ignore
384
+ preload, so readyState>=2 / canplay never arrive before play() — don't wait forever.
385
+ WeChat injects WeixinJSBridge into the top frame only (this page usually runs in
386
+ index.html's iframe) and play() from its callback needs no tap; if play() is still
387
+ refused, roll back to paused and retry on the first tap. ?wxdebug on the top URL
388
+ shows a log. start() must return the play() promise, stop() resets the UI. */
389
+ function wxAuto(vids,isPlaying,start,stop){
390
+ var inWx=/MicroMessenger/i.test(navigator.userAgent), t0=Date.now(), started=false, armed=false, wxTried=false, wxWait=false, iv=null, dbgEl=null;
391
+ function frames(){var ws=[];try{ws.push(window.top);}catch(e){}try{ws.push(window.parent);}catch(e){}ws.push(window);return ws;}
392
+ function dbg(m){try{if(!/wxdebug/.test(window.top.location.search+window.location.search))return;}catch(e){return;}
393
+ if(!dbgEl){dbgEl=document.createElement('pre');dbgEl.style.cssText='position:fixed;left:0;bottom:0;z-index:99999;max-height:40vh;overflow:auto;margin:0;padding:4px 6px;font:10px/1.3 monospace;background:rgba(0,0,0,.8);color:#7f7;pointer-events:none;white-space:pre-wrap';document.body.appendChild(dbgEl);}
394
+ dbgEl.textContent+=(performance.now()/1000).toFixed(2)+' '+m+'\n';}
395
+ function bridge(){var ws=frames();for(var i=0;i<ws.length;i++){try{if(ws[i].WeixinJSBridge&&ws[i].WeixinJSBridge.invoke)return ws[i].WeixinJSBridge;}catch(e){}}return null;}
396
+ function wxCall(cb){var b=bridge();dbg('bridge '+(b?'yes':'no')+' top='+(window.top===window));
397
+ if(b){b.invoke('getNetworkType',{},function(){dbg('bridge cb');cb();});return;}
398
+ function on(){var b=bridge();dbg('bridge ready '+(b?'yes':'no'));if(b)b.invoke('getNetworkType',{},function(){dbg('bridge cb');cb();});}
399
+ frames().forEach(function(w){try{w.document.addEventListener('WeixinJSBridgeReady',on,{once:true});}catch(e){}});}
400
+ function ready(vs){return vs.every(function(v){return v.readyState>=2;})||
401
+ (Date.now()-t0>1500&&vs.every(function(v){return v.readyState>=1||v.src;}));}
402
+ function go(force){
403
+ if(started||isPlaying())return;
404
+ var vs=vids(); if(!vs.length||(!force&&!ready(vs)))return;
405
+ if(wxWait&&!force)return;
406
+ if(inWx&&!wxTried){wxTried=wxWait=true;wxCall(function(){wxWait=false;go(true);});setTimeout(function(){wxWait=false;go(true);},1500);return;}
407
  started=true; if(iv){clearInterval(iv);iv=null;}
408
  vs.forEach(function(v){v.muted=true;});
409
+ dbg('play() rs='+vs.map(function(v){return v.readyState;}).join(','));
410
+ Promise.resolve(start()).then(function(){dbg('play ok');}).catch(function(err){
411
+ dbg('play rejected '+(err&&err.name)); try{stop();}catch(e){} started=false; arm(); });
 
 
412
  }
413
+ function arm(){
414
+ if(armed)return; armed=true;
415
+ function once(ev){if(ev&&ev.target&&ev.target.closest&&ev.target.closest('#play'))return;
416
+ document.removeEventListener('touchend',once,true);document.removeEventListener('click',once,true);
417
+ armed=false;go(true);}
418
+ document.addEventListener('touchend',once,true);document.addEventListener('click',once,true);
419
+ wxCall(function(){go(true);});
420
+ }
421
+ vids().forEach(function(v){v.addEventListener('canplay',function(){go();});v.addEventListener('loadeddata',function(){go();});});
422
+ window.addEventListener('load',function(){go();});
423
+ iv=setInterval(go,400);
424
  setTimeout(function(){if(iv){clearInterval(iv);iv=null;}},90000);
425
+ }
426
+ wxAuto(function(){try{return videos;}catch(e){return [];}}, function(){return playing;},
427
+ function(){ return Promise.all(videos.map(function(v){return Promise.resolve(v.play());})).then(function(){
428
+ playing=true; playBtn.textContent='\u275a\u275a'; requestAnimationFrame(loop); }); },
429
+ function(){ videos.forEach(function(v){try{v.pause();}catch(e){}}); playing=false; playBtn.textContent='\u25b6\ufe0e'; });
430
 
431
  /* demo loop: when the clip ends, wrap every video back to the start and keep playing.
432
  ended is the primary trigger; the interval is a fallback for decoders that never fire it. */
teleopSample/ep5/viewer.html CHANGED
@@ -380,28 +380,53 @@ window.addEventListener('load',update);
380
  button. Videos are muted, which is what browser autoplay policies require;
381
  if a browser still refuses, we roll back to the paused state so the manual
382
  button keeps working exactly as before. */
383
- (function(){
384
- var started=false, iv=null;
385
- function vlist(){try{return videos;}catch(e){return [];}}
386
- function autoStart(){
387
- if(started||playing)return;
388
- var vs=vlist();
389
- if(!vs.length||!vs.every(function(v){return v.readyState>=2;}))return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
  started=true; if(iv){clearInterval(iv);iv=null;}
391
  vs.forEach(function(v){v.muted=true;});
392
- Promise.all(vs.map(function(v){return Promise.resolve(v.play());})).then(function(){
393
- playing=true; playBtn.textContent='\u275a\u275a'; requestAnimationFrame(loop);
394
- }).catch(function(){
395
- vs.forEach(function(v){try{v.pause();}catch(e){}}); playing=false; playBtn.textContent='\u25b6\ufe0e';
396
- });
397
  }
398
- vlist().forEach(function(v){
399
- v.addEventListener('canplay',autoStart); v.addEventListener('loadeddata',autoStart);
400
- });
401
- window.addEventListener('load',autoStart);
402
- iv=setInterval(autoStart,400);
 
 
 
 
 
 
403
  setTimeout(function(){if(iv){clearInterval(iv);iv=null;}},90000);
404
- })();
 
 
 
 
405
 
406
  /* demo loop: when the clip ends, wrap every video back to the start and keep playing.
407
  ended is the primary trigger; the interval is a fallback for decoders that never fire it. */
 
380
  button. Videos are muted, which is what browser autoplay policies require;
381
  if a browser still refuses, we roll back to the paused state so the manual
382
  button keeps working exactly as before. */
383
+ /* autoplay that also works in in-app WebViews (WeChat iOS/Android X5): they ignore
384
+ preload, so readyState>=2 / canplay never arrive before play() — don't wait forever.
385
+ WeChat injects WeixinJSBridge into the top frame only (this page usually runs in
386
+ index.html's iframe) and play() from its callback needs no tap; if play() is still
387
+ refused, roll back to paused and retry on the first tap. ?wxdebug on the top URL
388
+ shows a log. start() must return the play() promise, stop() resets the UI. */
389
+ function wxAuto(vids,isPlaying,start,stop){
390
+ var inWx=/MicroMessenger/i.test(navigator.userAgent), t0=Date.now(), started=false, armed=false, wxTried=false, wxWait=false, iv=null, dbgEl=null;
391
+ function frames(){var ws=[];try{ws.push(window.top);}catch(e){}try{ws.push(window.parent);}catch(e){}ws.push(window);return ws;}
392
+ function dbg(m){try{if(!/wxdebug/.test(window.top.location.search+window.location.search))return;}catch(e){return;}
393
+ if(!dbgEl){dbgEl=document.createElement('pre');dbgEl.style.cssText='position:fixed;left:0;bottom:0;z-index:99999;max-height:40vh;overflow:auto;margin:0;padding:4px 6px;font:10px/1.3 monospace;background:rgba(0,0,0,.8);color:#7f7;pointer-events:none;white-space:pre-wrap';document.body.appendChild(dbgEl);}
394
+ dbgEl.textContent+=(performance.now()/1000).toFixed(2)+' '+m+'\n';}
395
+ function bridge(){var ws=frames();for(var i=0;i<ws.length;i++){try{if(ws[i].WeixinJSBridge&&ws[i].WeixinJSBridge.invoke)return ws[i].WeixinJSBridge;}catch(e){}}return null;}
396
+ function wxCall(cb){var b=bridge();dbg('bridge '+(b?'yes':'no')+' top='+(window.top===window));
397
+ if(b){b.invoke('getNetworkType',{},function(){dbg('bridge cb');cb();});return;}
398
+ function on(){var b=bridge();dbg('bridge ready '+(b?'yes':'no'));if(b)b.invoke('getNetworkType',{},function(){dbg('bridge cb');cb();});}
399
+ frames().forEach(function(w){try{w.document.addEventListener('WeixinJSBridgeReady',on,{once:true});}catch(e){}});}
400
+ function ready(vs){return vs.every(function(v){return v.readyState>=2;})||
401
+ (Date.now()-t0>1500&&vs.every(function(v){return v.readyState>=1||v.src;}));}
402
+ function go(force){
403
+ if(started||isPlaying())return;
404
+ var vs=vids(); if(!vs.length||(!force&&!ready(vs)))return;
405
+ if(wxWait&&!force)return;
406
+ if(inWx&&!wxTried){wxTried=wxWait=true;wxCall(function(){wxWait=false;go(true);});setTimeout(function(){wxWait=false;go(true);},1500);return;}
407
  started=true; if(iv){clearInterval(iv);iv=null;}
408
  vs.forEach(function(v){v.muted=true;});
409
+ dbg('play() rs='+vs.map(function(v){return v.readyState;}).join(','));
410
+ Promise.resolve(start()).then(function(){dbg('play ok');}).catch(function(err){
411
+ dbg('play rejected '+(err&&err.name)); try{stop();}catch(e){} started=false; arm(); });
 
 
412
  }
413
+ function arm(){
414
+ if(armed)return; armed=true;
415
+ function once(ev){if(ev&&ev.target&&ev.target.closest&&ev.target.closest('#play'))return;
416
+ document.removeEventListener('touchend',once,true);document.removeEventListener('click',once,true);
417
+ armed=false;go(true);}
418
+ document.addEventListener('touchend',once,true);document.addEventListener('click',once,true);
419
+ wxCall(function(){go(true);});
420
+ }
421
+ vids().forEach(function(v){v.addEventListener('canplay',function(){go();});v.addEventListener('loadeddata',function(){go();});});
422
+ window.addEventListener('load',function(){go();});
423
+ iv=setInterval(go,400);
424
  setTimeout(function(){if(iv){clearInterval(iv);iv=null;}},90000);
425
+ }
426
+ wxAuto(function(){try{return videos;}catch(e){return [];}}, function(){return playing;},
427
+ function(){ return Promise.all(videos.map(function(v){return Promise.resolve(v.play());})).then(function(){
428
+ playing=true; playBtn.textContent='\u275a\u275a'; requestAnimationFrame(loop); }); },
429
+ function(){ videos.forEach(function(v){try{v.pause();}catch(e){}}); playing=false; playBtn.textContent='\u25b6\ufe0e'; });
430
 
431
  /* demo loop: when the clip ends, wrap every video back to the start and keep playing.
432
  ended is the primary trigger; the interval is a fallback for decoders that never fire it. */
teleopSample/faberU/t2_water_bottle_into_steel_tray/20260707052527/viewer.html CHANGED
@@ -72,5 +72,5 @@
72
  <script id="cams" type="application/json">[{"key":"hand_left_color","label":"wrist \u00b7 left","src":"video/hand_left_color.mp4","size":"341.1 KB"},{"key":"head_color","label":"head \u00b7 color","src":"video/head_color.mp4","size":"559.3 KB"},{"key":"hand_right_color","label":"wrist \u00b7 right","src":"video/hand_right_color.mp4","size":"1.3 MB"},{"key":"head_stereo_left","label":"head \u00b7 stereo L","src":"video/head_stereo_left.mp4","size":"475.8 KB"},{"key":"head_depth","label":"head \u00b7 depth","src":"video/head_depth.mp4","size":"2.3 MB"},{"key":"head_stereo_right","label":"head \u00b7 stereo R","src":"video/head_stereo_right.mp4","size":"479.9 KB"}]</script>
73
  <script id="task" type="application/json">{"instruction":"Pick up the clear plastic water bottle from the retail checkout counter and set it upright in the white plastic tray.","object":"clear plastic water bottle (red label with QR code)","target":"white plastic tray","scene":"retail store checkout counter, shelves of household goods behind"}</script>
74
  <script src="../../../lib/steps-ui.js"></script>
75
- <script src="../../../lib/viewer.js"></script>
76
  </body></html>
 
72
  <script id="cams" type="application/json">[{"key":"hand_left_color","label":"wrist \u00b7 left","src":"video/hand_left_color.mp4","size":"341.1 KB"},{"key":"head_color","label":"head \u00b7 color","src":"video/head_color.mp4","size":"559.3 KB"},{"key":"hand_right_color","label":"wrist \u00b7 right","src":"video/hand_right_color.mp4","size":"1.3 MB"},{"key":"head_stereo_left","label":"head \u00b7 stereo L","src":"video/head_stereo_left.mp4","size":"475.8 KB"},{"key":"head_depth","label":"head \u00b7 depth","src":"video/head_depth.mp4","size":"2.3 MB"},{"key":"head_stereo_right","label":"head \u00b7 stereo R","src":"video/head_stereo_right.mp4","size":"479.9 KB"}]</script>
73
  <script id="task" type="application/json">{"instruction":"Pick up the clear plastic water bottle from the retail checkout counter and set it upright in the white plastic tray.","object":"clear plastic water bottle (red label with QR code)","target":"white plastic tray","scene":"retail store checkout counter, shelves of household goods behind"}</script>
74
  <script src="../../../lib/steps-ui.js"></script>
75
+ <script src="../../../lib/viewer.js?v=wx2"></script>
76
  </body></html>
teleopSample/faberU/t2_water_bottle_into_steel_tray/20260707052609/viewer.html CHANGED
@@ -72,5 +72,5 @@
72
  <script id="cams" type="application/json">[{"key":"hand_left_color","label":"wrist \u00b7 left","src":"video/hand_left_color.mp4","size":"359.3 KB"},{"key":"head_color","label":"head \u00b7 color","src":"video/head_color.mp4","size":"557.4 KB"},{"key":"hand_right_color","label":"wrist \u00b7 right","src":"video/hand_right_color.mp4","size":"1.3 MB"},{"key":"head_stereo_left","label":"head \u00b7 stereo L","src":"video/head_stereo_left.mp4","size":"443.5 KB"},{"key":"head_depth","label":"head \u00b7 depth","src":"video/head_depth.mp4","size":"2.6 MB"},{"key":"head_stereo_right","label":"head \u00b7 stereo R","src":"video/head_stereo_right.mp4","size":"459.0 KB"}]</script>
73
  <script id="task" type="application/json">{"instruction":"Pick up the clear plastic water bottle from the retail checkout counter and set it upright in the white plastic tray.","object":"clear plastic water bottle (red label with QR code)","target":"white plastic tray","scene":"retail store checkout counter, shelves of household goods behind"}</script>
74
  <script src="../../../lib/steps-ui.js"></script>
75
- <script src="../../../lib/viewer.js"></script>
76
  </body></html>
 
72
  <script id="cams" type="application/json">[{"key":"hand_left_color","label":"wrist \u00b7 left","src":"video/hand_left_color.mp4","size":"359.3 KB"},{"key":"head_color","label":"head \u00b7 color","src":"video/head_color.mp4","size":"557.4 KB"},{"key":"hand_right_color","label":"wrist \u00b7 right","src":"video/hand_right_color.mp4","size":"1.3 MB"},{"key":"head_stereo_left","label":"head \u00b7 stereo L","src":"video/head_stereo_left.mp4","size":"443.5 KB"},{"key":"head_depth","label":"head \u00b7 depth","src":"video/head_depth.mp4","size":"2.6 MB"},{"key":"head_stereo_right","label":"head \u00b7 stereo R","src":"video/head_stereo_right.mp4","size":"459.0 KB"}]</script>
73
  <script id="task" type="application/json">{"instruction":"Pick up the clear plastic water bottle from the retail checkout counter and set it upright in the white plastic tray.","object":"clear plastic water bottle (red label with QR code)","target":"white plastic tray","scene":"retail store checkout counter, shelves of household goods behind"}</script>
74
  <script src="../../../lib/steps-ui.js"></script>
75
+ <script src="../../../lib/viewer.js?v=wx2"></script>
76
  </body></html>
teleopSample/faberU/t3_juice_bottle_onto_plate/20260724050737/viewer.html CHANGED
@@ -72,5 +72,5 @@
72
  <script id="cams" type="application/json">[{"key":"hand_left_color","label":"wrist \u00b7 left","src":"video/hand_left_color.mp4","size":"1.5 MB"},{"key":"head_color","label":"head \u00b7 color","src":"video/head_color.mp4","size":"509.3 KB"},{"key":"hand_right_color","label":"wrist \u00b7 right","src":"video/hand_right_color.mp4","size":"274.3 KB"},{"key":"head_stereo_left","label":"head \u00b7 stereo L","src":"video/head_stereo_left.mp4","size":"483.9 KB"},{"key":"head_depth","label":"head \u00b7 depth","src":"video/head_depth.mp4","size":"2.0 MB"},{"key":"head_stereo_right","label":"head \u00b7 stereo R","src":"video/head_stereo_right.mp4","size":"449.9 KB"}]</script>
73
  <script id="task" type="application/json">{"instruction":"Pick up the CapriSun juice bottle from the yellow table and stand it upright on the blue-and-white patterned plate.","object":"CapriSun juice bottle (green/blue label, orange cap)","target":"blue-and-white patterned plate","scene":"lab bay behind acoustic screens, yellow table"}</script>
74
  <script src="../../../lib/steps-ui.js"></script>
75
- <script src="../../../lib/viewer.js"></script>
76
  </body></html>
 
72
  <script id="cams" type="application/json">[{"key":"hand_left_color","label":"wrist \u00b7 left","src":"video/hand_left_color.mp4","size":"1.5 MB"},{"key":"head_color","label":"head \u00b7 color","src":"video/head_color.mp4","size":"509.3 KB"},{"key":"hand_right_color","label":"wrist \u00b7 right","src":"video/hand_right_color.mp4","size":"274.3 KB"},{"key":"head_stereo_left","label":"head \u00b7 stereo L","src":"video/head_stereo_left.mp4","size":"483.9 KB"},{"key":"head_depth","label":"head \u00b7 depth","src":"video/head_depth.mp4","size":"2.0 MB"},{"key":"head_stereo_right","label":"head \u00b7 stereo R","src":"video/head_stereo_right.mp4","size":"449.9 KB"}]</script>
73
  <script id="task" type="application/json">{"instruction":"Pick up the CapriSun juice bottle from the yellow table and stand it upright on the blue-and-white patterned plate.","object":"CapriSun juice bottle (green/blue label, orange cap)","target":"blue-and-white patterned plate","scene":"lab bay behind acoustic screens, yellow table"}</script>
74
  <script src="../../../lib/steps-ui.js"></script>
75
+ <script src="../../../lib/viewer.js?v=wx2"></script>
76
  </body></html>
teleopSample/faberU/t3_juice_bottle_onto_plate/20260724050827/viewer.html CHANGED
@@ -72,5 +72,5 @@
72
  <script id="cams" type="application/json">[{"key":"hand_left_color","label":"wrist \u00b7 left","src":"video/hand_left_color.mp4","size":"1.5 MB"},{"key":"head_color","label":"head \u00b7 color","src":"video/head_color.mp4","size":"501.9 KB"},{"key":"hand_right_color","label":"wrist \u00b7 right","src":"video/hand_right_color.mp4","size":"315.9 KB"},{"key":"head_stereo_left","label":"head \u00b7 stereo L","src":"video/head_stereo_left.mp4","size":"489.9 KB"},{"key":"head_depth","label":"head \u00b7 depth","src":"video/head_depth.mp4","size":"2.1 MB"},{"key":"head_stereo_right","label":"head \u00b7 stereo R","src":"video/head_stereo_right.mp4","size":"481.5 KB"}]</script>
73
  <script id="task" type="application/json">{"instruction":"Pick up the CapriSun juice bottle from the yellow table and stand it upright on the blue-and-white patterned plate.","object":"CapriSun juice bottle (green/blue label, orange cap)","target":"blue-and-white patterned plate","scene":"lab bay behind acoustic screens, yellow table"}</script>
74
  <script src="../../../lib/steps-ui.js"></script>
75
- <script src="../../../lib/viewer.js"></script>
76
  </body></html>
 
72
  <script id="cams" type="application/json">[{"key":"hand_left_color","label":"wrist \u00b7 left","src":"video/hand_left_color.mp4","size":"1.5 MB"},{"key":"head_color","label":"head \u00b7 color","src":"video/head_color.mp4","size":"501.9 KB"},{"key":"hand_right_color","label":"wrist \u00b7 right","src":"video/hand_right_color.mp4","size":"315.9 KB"},{"key":"head_stereo_left","label":"head \u00b7 stereo L","src":"video/head_stereo_left.mp4","size":"489.9 KB"},{"key":"head_depth","label":"head \u00b7 depth","src":"video/head_depth.mp4","size":"2.1 MB"},{"key":"head_stereo_right","label":"head \u00b7 stereo R","src":"video/head_stereo_right.mp4","size":"481.5 KB"}]</script>
73
  <script id="task" type="application/json">{"instruction":"Pick up the CapriSun juice bottle from the yellow table and stand it upright on the blue-and-white patterned plate.","object":"CapriSun juice bottle (green/blue label, orange cap)","target":"blue-and-white patterned plate","scene":"lab bay behind acoustic screens, yellow table"}</script>
74
  <script src="../../../lib/steps-ui.js"></script>
75
+ <script src="../../../lib/viewer.js?v=wx2"></script>
76
  </body></html>
teleopSample/faberU/t4_juice_bottle_into_black_tray/20260728075709/viewer.html CHANGED
@@ -72,5 +72,5 @@
72
  <script id="cams" type="application/json">[{"key":"hand_left_color","label":"wrist \u00b7 left","src":"video/hand_left_color.mp4","size":"829.7 KB"},{"key":"head_color","label":"head \u00b7 color","src":"video/head_color.mp4","size":"274.1 KB"},{"key":"hand_right_color","label":"wrist \u00b7 right","src":"video/hand_right_color.mp4","size":"201.2 KB"},{"key":"head_stereo_left","label":"head \u00b7 stereo L","src":"video/head_stereo_left.mp4","size":"266.2 KB"},{"key":"head_depth","label":"head \u00b7 depth","src":"video/head_depth.mp4","size":"1.3 MB"},{"key":"head_stereo_right","label":"head \u00b7 stereo R","src":"video/head_stereo_right.mp4","size":"255.3 KB"}]</script>
73
  <script id="task" type="application/json">{"instruction":"Pick up the green juice bottle from the yellow table and place it into the black tray.","object":"green juice bottle (yellow cap)","target":"black tray","scene":"lab bay behind acoustic screens, yellow table"}</script>
74
  <script src="../../../lib/steps-ui.js"></script>
75
- <script src="../../../lib/viewer.js"></script>
76
  </body></html>
 
72
  <script id="cams" type="application/json">[{"key":"hand_left_color","label":"wrist \u00b7 left","src":"video/hand_left_color.mp4","size":"829.7 KB"},{"key":"head_color","label":"head \u00b7 color","src":"video/head_color.mp4","size":"274.1 KB"},{"key":"hand_right_color","label":"wrist \u00b7 right","src":"video/hand_right_color.mp4","size":"201.2 KB"},{"key":"head_stereo_left","label":"head \u00b7 stereo L","src":"video/head_stereo_left.mp4","size":"266.2 KB"},{"key":"head_depth","label":"head \u00b7 depth","src":"video/head_depth.mp4","size":"1.3 MB"},{"key":"head_stereo_right","label":"head \u00b7 stereo R","src":"video/head_stereo_right.mp4","size":"255.3 KB"}]</script>
73
  <script id="task" type="application/json">{"instruction":"Pick up the green juice bottle from the yellow table and place it into the black tray.","object":"green juice bottle (yellow cap)","target":"black tray","scene":"lab bay behind acoustic screens, yellow table"}</script>
74
  <script src="../../../lib/steps-ui.js"></script>
75
+ <script src="../../../lib/viewer.js?v=wx2"></script>
76
  </body></html>
teleopSample/faberU/t4_juice_bottle_into_black_tray/20260728080339/viewer.html CHANGED
@@ -72,5 +72,5 @@
72
  <script id="cams" type="application/json">[{"key":"hand_left_color","label":"wrist \u00b7 left","src":"video/hand_left_color.mp4","size":"1.0 MB"},{"key":"head_color","label":"head \u00b7 color","src":"video/head_color.mp4","size":"288.0 KB"},{"key":"hand_right_color","label":"wrist \u00b7 right","src":"video/hand_right_color.mp4","size":"227.8 KB"},{"key":"head_stereo_left","label":"head \u00b7 stereo L","src":"video/head_stereo_left.mp4","size":"296.9 KB"},{"key":"head_depth","label":"head \u00b7 depth","src":"video/head_depth.mp4","size":"1.7 MB"},{"key":"head_stereo_right","label":"head \u00b7 stereo R","src":"video/head_stereo_right.mp4","size":"280.4 KB"}]</script>
73
  <script id="task" type="application/json">{"instruction":"Pick up the green juice bottle from the yellow table and place it into the black tray.","object":"green juice bottle (yellow cap)","target":"black tray","scene":"lab bay behind acoustic screens, yellow table"}</script>
74
  <script src="../../../lib/steps-ui.js"></script>
75
- <script src="../../../lib/viewer.js"></script>
76
  </body></html>
 
72
  <script id="cams" type="application/json">[{"key":"hand_left_color","label":"wrist \u00b7 left","src":"video/hand_left_color.mp4","size":"1.0 MB"},{"key":"head_color","label":"head \u00b7 color","src":"video/head_color.mp4","size":"288.0 KB"},{"key":"hand_right_color","label":"wrist \u00b7 right","src":"video/hand_right_color.mp4","size":"227.8 KB"},{"key":"head_stereo_left","label":"head \u00b7 stereo L","src":"video/head_stereo_left.mp4","size":"296.9 KB"},{"key":"head_depth","label":"head \u00b7 depth","src":"video/head_depth.mp4","size":"1.7 MB"},{"key":"head_stereo_right","label":"head \u00b7 stereo R","src":"video/head_stereo_right.mp4","size":"280.4 KB"}]</script>
73
  <script id="task" type="application/json">{"instruction":"Pick up the green juice bottle from the yellow table and place it into the black tray.","object":"green juice bottle (yellow cap)","target":"black tray","scene":"lab bay behind acoustic screens, yellow table"}</script>
74
  <script src="../../../lib/steps-ui.js"></script>
75
+ <script src="../../../lib/viewer.js?v=wx2"></script>
76
  </body></html>
teleopSample/faberU/t6_pick_up_green_tea_bottle/20260730045050/viewer.html CHANGED
@@ -72,5 +72,5 @@
72
  <script id="cams" type="application/json">[{"key":"hand_left_color","label":"wrist \u00b7 left","src":"video/hand_left_color.mp4","size":"1.0 MB"},{"key":"head_color","label":"head \u00b7 color","src":"video/head_color.mp4","size":"348.4 KB"},{"key":"hand_right_color","label":"wrist \u00b7 right","src":"video/hand_right_color.mp4","size":"170.4 KB"},{"key":"head_stereo_left","label":"head \u00b7 stereo L","src":"video/head_stereo_left.mp4","size":"290.2 KB"},{"key":"head_depth","label":"head \u00b7 depth","src":"video/head_depth.mp4","size":"1.9 MB"},{"key":"head_stereo_right","label":"head \u00b7 stereo R","src":"video/head_stereo_right.mp4","size":"275.5 KB"}]</script>
73
  <script id="task" type="application/json">{"instruction":"Pick up the green-tea bottle from the yellow table and lift it clear of the surface; the episode ends with the bottle still held.","object":"green-tea bottle (transparent, green label)","target":"- (pick-and-hold task)","scene":"lab bay behind acoustic screens, yellow table"}</script>
74
  <script src="../../../lib/steps-ui.js"></script>
75
- <script src="../../../lib/viewer.js"></script>
76
  </body></html>
 
72
  <script id="cams" type="application/json">[{"key":"hand_left_color","label":"wrist \u00b7 left","src":"video/hand_left_color.mp4","size":"1.0 MB"},{"key":"head_color","label":"head \u00b7 color","src":"video/head_color.mp4","size":"348.4 KB"},{"key":"hand_right_color","label":"wrist \u00b7 right","src":"video/hand_right_color.mp4","size":"170.4 KB"},{"key":"head_stereo_left","label":"head \u00b7 stereo L","src":"video/head_stereo_left.mp4","size":"290.2 KB"},{"key":"head_depth","label":"head \u00b7 depth","src":"video/head_depth.mp4","size":"1.9 MB"},{"key":"head_stereo_right","label":"head \u00b7 stereo R","src":"video/head_stereo_right.mp4","size":"275.5 KB"}]</script>
73
  <script id="task" type="application/json">{"instruction":"Pick up the green-tea bottle from the yellow table and lift it clear of the surface; the episode ends with the bottle still held.","object":"green-tea bottle (transparent, green label)","target":"- (pick-and-hold task)","scene":"lab bay behind acoustic screens, yellow table"}</script>
74
  <script src="../../../lib/steps-ui.js"></script>
75
+ <script src="../../../lib/viewer.js?v=wx2"></script>
76
  </body></html>
teleopSample/faberU/t6_pick_up_green_tea_bottle/20260730045152/viewer.html CHANGED
@@ -72,5 +72,5 @@
72
  <script id="cams" type="application/json">[{"key":"hand_left_color","label":"wrist \u00b7 left","src":"video/hand_left_color.mp4","size":"1.4 MB"},{"key":"head_color","label":"head \u00b7 color","src":"video/head_color.mp4","size":"569.8 KB"},{"key":"hand_right_color","label":"wrist \u00b7 right","src":"video/hand_right_color.mp4","size":"245.0 KB"},{"key":"head_stereo_left","label":"head \u00b7 stereo L","src":"video/head_stereo_left.mp4","size":"518.8 KB"},{"key":"head_depth","label":"head \u00b7 depth","src":"video/head_depth.mp4","size":"2.4 MB"},{"key":"head_stereo_right","label":"head \u00b7 stereo R","src":"video/head_stereo_right.mp4","size":"473.6 KB"}]</script>
73
  <script id="task" type="application/json">{"instruction":"Pick up the green-tea bottle from the yellow table and lift it clear of the surface; the episode ends with the bottle still held.","object":"green-tea bottle (transparent, green label)","target":"- (pick-and-hold task)","scene":"lab bay behind acoustic screens, yellow table"}</script>
74
  <script src="../../../lib/steps-ui.js"></script>
75
- <script src="../../../lib/viewer.js"></script>
76
  </body></html>
 
72
  <script id="cams" type="application/json">[{"key":"hand_left_color","label":"wrist \u00b7 left","src":"video/hand_left_color.mp4","size":"1.4 MB"},{"key":"head_color","label":"head \u00b7 color","src":"video/head_color.mp4","size":"569.8 KB"},{"key":"hand_right_color","label":"wrist \u00b7 right","src":"video/hand_right_color.mp4","size":"245.0 KB"},{"key":"head_stereo_left","label":"head \u00b7 stereo L","src":"video/head_stereo_left.mp4","size":"518.8 KB"},{"key":"head_depth","label":"head \u00b7 depth","src":"video/head_depth.mp4","size":"2.4 MB"},{"key":"head_stereo_right","label":"head \u00b7 stereo R","src":"video/head_stereo_right.mp4","size":"473.6 KB"}]</script>
73
  <script id="task" type="application/json">{"instruction":"Pick up the green-tea bottle from the yellow table and lift it clear of the surface; the episode ends with the bottle still held.","object":"green-tea bottle (transparent, green label)","target":"- (pick-and-hold task)","scene":"lab bay behind acoustic screens, yellow table"}</script>
74
  <script src="../../../lib/steps-ui.js"></script>
75
+ <script src="../../../lib/viewer.js?v=wx2"></script>
76
  </body></html>
teleopSample/lib/viewer.js CHANGED
@@ -54,16 +54,50 @@
54
  else videos.forEach(v => v.pause());
55
  }
56
  playBtn.onclick = () => setPlaying(!playing);
57
- let started = false, iv = setInterval(tryStart, 400);
58
- function tryStart() {
59
- if (started || playing || !videos.length || !videos.every(v => v.readyState >= 2)) return;
60
- started = true; clearInterval(iv);
61
- videos.forEach(v => v.muted = true);
62
- setPlaying(true);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  }
64
- videos.forEach(v => { v.addEventListener('canplay', tryStart); v.addEventListener('loadeddata', tryStart); });
65
- window.addEventListener('load', tryStart);
66
- setTimeout(() => clearInterval(iv), 90000);
67
  setInterval(() => {
68
  if (playing && master.currentTime >= DUR - 0.08) {
69
  seekTo(0); videos.forEach(v => v.play().catch(() => {}));
 
54
  else videos.forEach(v => v.pause());
55
  }
56
  playBtn.onclick = () => setPlaying(!playing);
57
+ /* autoplay that also works in in-app WebViews (WeChat iOS/Android X5): they ignore
58
+ preload, so readyState>=2 / canplay never arrive before play() — don't wait forever.
59
+ WeChat injects WeixinJSBridge into the top frame only (this page usually runs in
60
+ index.html's iframe) and play() from its callback needs no tap; if play() is still
61
+ refused, roll back to paused and retry on the first tap. ?wxdebug on the top URL
62
+ shows a log. start() must return the play() promise, stop() resets the UI. */
63
+ function wxAuto(vids,isPlaying,start,stop){
64
+ var inWx=/MicroMessenger/i.test(navigator.userAgent), t0=Date.now(), started=false, armed=false, wxTried=false, wxWait=false, iv=null, dbgEl=null;
65
+ function frames(){var ws=[];try{ws.push(window.top);}catch(e){}try{ws.push(window.parent);}catch(e){}ws.push(window);return ws;}
66
+ function dbg(m){try{if(!/wxdebug/.test(window.top.location.search+window.location.search))return;}catch(e){return;}
67
+ if(!dbgEl){dbgEl=document.createElement('pre');dbgEl.style.cssText='position:fixed;left:0;bottom:0;z-index:99999;max-height:40vh;overflow:auto;margin:0;padding:4px 6px;font:10px/1.3 monospace;background:rgba(0,0,0,.8);color:#7f7;pointer-events:none;white-space:pre-wrap';document.body.appendChild(dbgEl);}
68
+ dbgEl.textContent+=(performance.now()/1000).toFixed(2)+' '+m+'\n';}
69
+ function bridge(){var ws=frames();for(var i=0;i<ws.length;i++){try{if(ws[i].WeixinJSBridge&&ws[i].WeixinJSBridge.invoke)return ws[i].WeixinJSBridge;}catch(e){}}return null;}
70
+ function wxCall(cb){var b=bridge();dbg('bridge '+(b?'yes':'no')+' top='+(window.top===window));
71
+ if(b){b.invoke('getNetworkType',{},function(){dbg('bridge cb');cb();});return;}
72
+ function on(){var b=bridge();dbg('bridge ready '+(b?'yes':'no'));if(b)b.invoke('getNetworkType',{},function(){dbg('bridge cb');cb();});}
73
+ frames().forEach(function(w){try{w.document.addEventListener('WeixinJSBridgeReady',on,{once:true});}catch(e){}});}
74
+ function ready(vs){return vs.every(function(v){return v.readyState>=2;})||
75
+ (Date.now()-t0>1500&&vs.every(function(v){return v.readyState>=1||v.src;}));}
76
+ function go(force){
77
+ if(started||isPlaying())return;
78
+ var vs=vids(); if(!vs.length||(!force&&!ready(vs)))return;
79
+ if(wxWait&&!force)return;
80
+ if(inWx&&!wxTried){wxTried=wxWait=true;wxCall(function(){wxWait=false;go(true);});setTimeout(function(){wxWait=false;go(true);},1500);return;}
81
+ started=true; if(iv){clearInterval(iv);iv=null;}
82
+ vs.forEach(function(v){v.muted=true;});
83
+ dbg('play() rs='+vs.map(function(v){return v.readyState;}).join(','));
84
+ Promise.resolve(start()).then(function(){dbg('play ok');}).catch(function(err){
85
+ dbg('play rejected '+(err&&err.name)); try{stop();}catch(e){} started=false; arm(); });
86
+ }
87
+ function arm(){
88
+ if(armed)return; armed=true;
89
+ function once(ev){if(ev&&ev.target&&ev.target.closest&&ev.target.closest('#play'))return;
90
+ document.removeEventListener('touchend',once,true);document.removeEventListener('click',once,true);
91
+ armed=false;go(true);}
92
+ document.addEventListener('touchend',once,true);document.addEventListener('click',once,true);
93
+ wxCall(function(){go(true);});
94
+ }
95
+ vids().forEach(function(v){v.addEventListener('canplay',function(){go();});v.addEventListener('loadeddata',function(){go();});});
96
+ window.addEventListener('load',function(){go();});
97
+ iv=setInterval(go,400);
98
+ setTimeout(function(){if(iv){clearInterval(iv);iv=null;}},90000);
99
  }
100
+ wxAuto(() => videos, () => playing, () => { setPlaying(true); return master.play(); }, () => setPlaying(false));
 
 
101
  setInterval(() => {
102
  if (playing && master.currentTime >= DUR - 0.08) {
103
  seekTo(0); videos.forEach(v => v.play().catch(() => {}));
teleopSample/mem_ep0.html CHANGED
@@ -226,17 +226,51 @@ CAMS.forEach(c=>{ const a=document.createElement('a'); a.className='file-chip';
226
  const a=document.createElement('a'); a.className='file-chip'; a.href=href; a.innerHTML=lbl; fm.appendChild(a); });
227
 
228
  // ---- autoplay (muted) + loop
229
- (function(){
230
- let started=false, iv=setInterval(tryStart,400);
231
- function tryStart(){
232
- if(started||playing) return;
233
- if(!vids.length||!vids.every(v=>v.readyState>=2)) return;
234
- started=true; clearInterval(iv);
235
- vids.forEach(v=>v.muted=true); setPlaying(true);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
  }
237
- vids.forEach(v=>{ v.addEventListener('canplay',tryStart); v.addEventListener('loadeddata',tryStart); });
238
- window.addEventListener('load',tryStart);
239
- setTimeout(()=>clearInterval(iv),90000);
 
 
 
 
240
  setInterval(()=>{ if(playing&&master&&master.currentTime>=DUR-0.08){
241
  try{master.currentTime=0;}catch(e){} syncSlaves(0,true);
242
  if(playing) vids.forEach(v=>v.play().catch(()=>{})); }},200);
 
226
  const a=document.createElement('a'); a.className='file-chip'; a.href=href; a.innerHTML=lbl; fm.appendChild(a); });
227
 
228
  // ---- autoplay (muted) + loop
229
+ /* autoplay that also works in in-app WebViews (WeChat iOS/Android X5): they ignore
230
+ preload, so readyState>=2 / canplay never arrive before play() — don't wait forever.
231
+ WeChat injects WeixinJSBridge into the top frame only (this page usually runs in
232
+ index.html's iframe) and play() from its callback needs no tap; if play() is still
233
+ refused, roll back to paused and retry on the first tap. ?wxdebug on the top URL
234
+ shows a log. start() must return the play() promise, stop() resets the UI. */
235
+ function wxAuto(vids,isPlaying,start,stop){
236
+ var inWx=/MicroMessenger/i.test(navigator.userAgent), t0=Date.now(), started=false, armed=false, wxTried=false, wxWait=false, iv=null, dbgEl=null;
237
+ function frames(){var ws=[];try{ws.push(window.top);}catch(e){}try{ws.push(window.parent);}catch(e){}ws.push(window);return ws;}
238
+ function dbg(m){try{if(!/wxdebug/.test(window.top.location.search+window.location.search))return;}catch(e){return;}
239
+ if(!dbgEl){dbgEl=document.createElement('pre');dbgEl.style.cssText='position:fixed;left:0;bottom:0;z-index:99999;max-height:40vh;overflow:auto;margin:0;padding:4px 6px;font:10px/1.3 monospace;background:rgba(0,0,0,.8);color:#7f7;pointer-events:none;white-space:pre-wrap';document.body.appendChild(dbgEl);}
240
+ dbgEl.textContent+=(performance.now()/1000).toFixed(2)+' '+m+'\n';}
241
+ function bridge(){var ws=frames();for(var i=0;i<ws.length;i++){try{if(ws[i].WeixinJSBridge&&ws[i].WeixinJSBridge.invoke)return ws[i].WeixinJSBridge;}catch(e){}}return null;}
242
+ function wxCall(cb){var b=bridge();dbg('bridge '+(b?'yes':'no')+' top='+(window.top===window));
243
+ if(b){b.invoke('getNetworkType',{},function(){dbg('bridge cb');cb();});return;}
244
+ function on(){var b=bridge();dbg('bridge ready '+(b?'yes':'no'));if(b)b.invoke('getNetworkType',{},function(){dbg('bridge cb');cb();});}
245
+ frames().forEach(function(w){try{w.document.addEventListener('WeixinJSBridgeReady',on,{once:true});}catch(e){}});}
246
+ function ready(vs){return vs.every(function(v){return v.readyState>=2;})||
247
+ (Date.now()-t0>1500&&vs.every(function(v){return v.readyState>=1||v.src;}));}
248
+ function go(force){
249
+ if(started||isPlaying())return;
250
+ var vs=vids(); if(!vs.length||(!force&&!ready(vs)))return;
251
+ if(wxWait&&!force)return;
252
+ if(inWx&&!wxTried){wxTried=wxWait=true;wxCall(function(){wxWait=false;go(true);});setTimeout(function(){wxWait=false;go(true);},1500);return;}
253
+ started=true; if(iv){clearInterval(iv);iv=null;}
254
+ vs.forEach(function(v){v.muted=true;});
255
+ dbg('play() rs='+vs.map(function(v){return v.readyState;}).join(','));
256
+ Promise.resolve(start()).then(function(){dbg('play ok');}).catch(function(err){
257
+ dbg('play rejected '+(err&&err.name)); try{stop();}catch(e){} started=false; arm(); });
258
+ }
259
+ function arm(){
260
+ if(armed)return; armed=true;
261
+ function once(ev){if(ev&&ev.target&&ev.target.closest&&ev.target.closest('#play'))return;
262
+ document.removeEventListener('touchend',once,true);document.removeEventListener('click',once,true);
263
+ armed=false;go(true);}
264
+ document.addEventListener('touchend',once,true);document.addEventListener('click',once,true);
265
+ wxCall(function(){go(true);});
266
  }
267
+ vids().forEach(function(v){v.addEventListener('canplay',function(){go();});v.addEventListener('loadeddata',function(){go();});});
268
+ window.addEventListener('load',function(){go();});
269
+ iv=setInterval(go,400);
270
+ setTimeout(function(){if(iv){clearInterval(iv);iv=null;}},90000);
271
+ }
272
+ (function(){
273
+ wxAuto(()=>vids, ()=>playing, ()=>{ setPlaying(true); return master.play(); }, ()=>setPlaying(false));
274
  setInterval(()=>{ if(playing&&master&&master.currentTime>=DUR-0.08){
275
  try{master.currentTime=0;}catch(e){} syncSlaves(0,true);
276
  if(playing) vids.forEach(v=>v.play().catch(()=>{})); }},200);
teleopSample/mem_ep1.html CHANGED
@@ -226,17 +226,51 @@ CAMS.forEach(c=>{ const a=document.createElement('a'); a.className='file-chip';
226
  const a=document.createElement('a'); a.className='file-chip'; a.href=href; a.innerHTML=lbl; fm.appendChild(a); });
227
 
228
  // ---- autoplay (muted) + loop
229
- (function(){
230
- let started=false, iv=setInterval(tryStart,400);
231
- function tryStart(){
232
- if(started||playing) return;
233
- if(!vids.length||!vids.every(v=>v.readyState>=2)) return;
234
- started=true; clearInterval(iv);
235
- vids.forEach(v=>v.muted=true); setPlaying(true);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
  }
237
- vids.forEach(v=>{ v.addEventListener('canplay',tryStart); v.addEventListener('loadeddata',tryStart); });
238
- window.addEventListener('load',tryStart);
239
- setTimeout(()=>clearInterval(iv),90000);
 
 
 
 
240
  setInterval(()=>{ if(playing&&master&&master.currentTime>=DUR-0.08){
241
  try{master.currentTime=0;}catch(e){} syncSlaves(0,true);
242
  if(playing) vids.forEach(v=>v.play().catch(()=>{})); }},200);
 
226
  const a=document.createElement('a'); a.className='file-chip'; a.href=href; a.innerHTML=lbl; fm.appendChild(a); });
227
 
228
  // ---- autoplay (muted) + loop
229
+ /* autoplay that also works in in-app WebViews (WeChat iOS/Android X5): they ignore
230
+ preload, so readyState>=2 / canplay never arrive before play() — don't wait forever.
231
+ WeChat injects WeixinJSBridge into the top frame only (this page usually runs in
232
+ index.html's iframe) and play() from its callback needs no tap; if play() is still
233
+ refused, roll back to paused and retry on the first tap. ?wxdebug on the top URL
234
+ shows a log. start() must return the play() promise, stop() resets the UI. */
235
+ function wxAuto(vids,isPlaying,start,stop){
236
+ var inWx=/MicroMessenger/i.test(navigator.userAgent), t0=Date.now(), started=false, armed=false, wxTried=false, wxWait=false, iv=null, dbgEl=null;
237
+ function frames(){var ws=[];try{ws.push(window.top);}catch(e){}try{ws.push(window.parent);}catch(e){}ws.push(window);return ws;}
238
+ function dbg(m){try{if(!/wxdebug/.test(window.top.location.search+window.location.search))return;}catch(e){return;}
239
+ if(!dbgEl){dbgEl=document.createElement('pre');dbgEl.style.cssText='position:fixed;left:0;bottom:0;z-index:99999;max-height:40vh;overflow:auto;margin:0;padding:4px 6px;font:10px/1.3 monospace;background:rgba(0,0,0,.8);color:#7f7;pointer-events:none;white-space:pre-wrap';document.body.appendChild(dbgEl);}
240
+ dbgEl.textContent+=(performance.now()/1000).toFixed(2)+' '+m+'\n';}
241
+ function bridge(){var ws=frames();for(var i=0;i<ws.length;i++){try{if(ws[i].WeixinJSBridge&&ws[i].WeixinJSBridge.invoke)return ws[i].WeixinJSBridge;}catch(e){}}return null;}
242
+ function wxCall(cb){var b=bridge();dbg('bridge '+(b?'yes':'no')+' top='+(window.top===window));
243
+ if(b){b.invoke('getNetworkType',{},function(){dbg('bridge cb');cb();});return;}
244
+ function on(){var b=bridge();dbg('bridge ready '+(b?'yes':'no'));if(b)b.invoke('getNetworkType',{},function(){dbg('bridge cb');cb();});}
245
+ frames().forEach(function(w){try{w.document.addEventListener('WeixinJSBridgeReady',on,{once:true});}catch(e){}});}
246
+ function ready(vs){return vs.every(function(v){return v.readyState>=2;})||
247
+ (Date.now()-t0>1500&&vs.every(function(v){return v.readyState>=1||v.src;}));}
248
+ function go(force){
249
+ if(started||isPlaying())return;
250
+ var vs=vids(); if(!vs.length||(!force&&!ready(vs)))return;
251
+ if(wxWait&&!force)return;
252
+ if(inWx&&!wxTried){wxTried=wxWait=true;wxCall(function(){wxWait=false;go(true);});setTimeout(function(){wxWait=false;go(true);},1500);return;}
253
+ started=true; if(iv){clearInterval(iv);iv=null;}
254
+ vs.forEach(function(v){v.muted=true;});
255
+ dbg('play() rs='+vs.map(function(v){return v.readyState;}).join(','));
256
+ Promise.resolve(start()).then(function(){dbg('play ok');}).catch(function(err){
257
+ dbg('play rejected '+(err&&err.name)); try{stop();}catch(e){} started=false; arm(); });
258
+ }
259
+ function arm(){
260
+ if(armed)return; armed=true;
261
+ function once(ev){if(ev&&ev.target&&ev.target.closest&&ev.target.closest('#play'))return;
262
+ document.removeEventListener('touchend',once,true);document.removeEventListener('click',once,true);
263
+ armed=false;go(true);}
264
+ document.addEventListener('touchend',once,true);document.addEventListener('click',once,true);
265
+ wxCall(function(){go(true);});
266
  }
267
+ vids().forEach(function(v){v.addEventListener('canplay',function(){go();});v.addEventListener('loadeddata',function(){go();});});
268
+ window.addEventListener('load',function(){go();});
269
+ iv=setInterval(go,400);
270
+ setTimeout(function(){if(iv){clearInterval(iv);iv=null;}},90000);
271
+ }
272
+ (function(){
273
+ wxAuto(()=>vids, ()=>playing, ()=>{ setPlaying(true); return master.play(); }, ()=>setPlaying(false));
274
  setInterval(()=>{ if(playing&&master&&master.currentTime>=DUR-0.08){
275
  try{master.currentTime=0;}catch(e){} syncSlaves(0,true);
276
  if(playing) vids.forEach(v=>v.play().catch(()=>{})); }},200);
teleopSample/sort_ep0.html CHANGED
@@ -226,17 +226,51 @@ CAMS.forEach(c=>{ const a=document.createElement('a'); a.className='file-chip';
226
  const a=document.createElement('a'); a.className='file-chip'; a.href=href; a.innerHTML=lbl; fm.appendChild(a); });
227
 
228
  // ---- autoplay (muted) + loop
229
- (function(){
230
- let started=false, iv=setInterval(tryStart,400);
231
- function tryStart(){
232
- if(started||playing) return;
233
- if(!vids.length||!vids.every(v=>v.readyState>=2)) return;
234
- started=true; clearInterval(iv);
235
- vids.forEach(v=>v.muted=true); setPlaying(true);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
  }
237
- vids.forEach(v=>{ v.addEventListener('canplay',tryStart); v.addEventListener('loadeddata',tryStart); });
238
- window.addEventListener('load',tryStart);
239
- setTimeout(()=>clearInterval(iv),90000);
 
 
 
 
240
  setInterval(()=>{ if(playing&&master&&master.currentTime>=DUR-0.08){
241
  try{master.currentTime=0;}catch(e){} syncSlaves(0,true);
242
  if(playing) vids.forEach(v=>v.play().catch(()=>{})); }},200);
 
226
  const a=document.createElement('a'); a.className='file-chip'; a.href=href; a.innerHTML=lbl; fm.appendChild(a); });
227
 
228
  // ---- autoplay (muted) + loop
229
+ /* autoplay that also works in in-app WebViews (WeChat iOS/Android X5): they ignore
230
+ preload, so readyState>=2 / canplay never arrive before play() — don't wait forever.
231
+ WeChat injects WeixinJSBridge into the top frame only (this page usually runs in
232
+ index.html's iframe) and play() from its callback needs no tap; if play() is still
233
+ refused, roll back to paused and retry on the first tap. ?wxdebug on the top URL
234
+ shows a log. start() must return the play() promise, stop() resets the UI. */
235
+ function wxAuto(vids,isPlaying,start,stop){
236
+ var inWx=/MicroMessenger/i.test(navigator.userAgent), t0=Date.now(), started=false, armed=false, wxTried=false, wxWait=false, iv=null, dbgEl=null;
237
+ function frames(){var ws=[];try{ws.push(window.top);}catch(e){}try{ws.push(window.parent);}catch(e){}ws.push(window);return ws;}
238
+ function dbg(m){try{if(!/wxdebug/.test(window.top.location.search+window.location.search))return;}catch(e){return;}
239
+ if(!dbgEl){dbgEl=document.createElement('pre');dbgEl.style.cssText='position:fixed;left:0;bottom:0;z-index:99999;max-height:40vh;overflow:auto;margin:0;padding:4px 6px;font:10px/1.3 monospace;background:rgba(0,0,0,.8);color:#7f7;pointer-events:none;white-space:pre-wrap';document.body.appendChild(dbgEl);}
240
+ dbgEl.textContent+=(performance.now()/1000).toFixed(2)+' '+m+'\n';}
241
+ function bridge(){var ws=frames();for(var i=0;i<ws.length;i++){try{if(ws[i].WeixinJSBridge&&ws[i].WeixinJSBridge.invoke)return ws[i].WeixinJSBridge;}catch(e){}}return null;}
242
+ function wxCall(cb){var b=bridge();dbg('bridge '+(b?'yes':'no')+' top='+(window.top===window));
243
+ if(b){b.invoke('getNetworkType',{},function(){dbg('bridge cb');cb();});return;}
244
+ function on(){var b=bridge();dbg('bridge ready '+(b?'yes':'no'));if(b)b.invoke('getNetworkType',{},function(){dbg('bridge cb');cb();});}
245
+ frames().forEach(function(w){try{w.document.addEventListener('WeixinJSBridgeReady',on,{once:true});}catch(e){}});}
246
+ function ready(vs){return vs.every(function(v){return v.readyState>=2;})||
247
+ (Date.now()-t0>1500&&vs.every(function(v){return v.readyState>=1||v.src;}));}
248
+ function go(force){
249
+ if(started||isPlaying())return;
250
+ var vs=vids(); if(!vs.length||(!force&&!ready(vs)))return;
251
+ if(wxWait&&!force)return;
252
+ if(inWx&&!wxTried){wxTried=wxWait=true;wxCall(function(){wxWait=false;go(true);});setTimeout(function(){wxWait=false;go(true);},1500);return;}
253
+ started=true; if(iv){clearInterval(iv);iv=null;}
254
+ vs.forEach(function(v){v.muted=true;});
255
+ dbg('play() rs='+vs.map(function(v){return v.readyState;}).join(','));
256
+ Promise.resolve(start()).then(function(){dbg('play ok');}).catch(function(err){
257
+ dbg('play rejected '+(err&&err.name)); try{stop();}catch(e){} started=false; arm(); });
258
+ }
259
+ function arm(){
260
+ if(armed)return; armed=true;
261
+ function once(ev){if(ev&&ev.target&&ev.target.closest&&ev.target.closest('#play'))return;
262
+ document.removeEventListener('touchend',once,true);document.removeEventListener('click',once,true);
263
+ armed=false;go(true);}
264
+ document.addEventListener('touchend',once,true);document.addEventListener('click',once,true);
265
+ wxCall(function(){go(true);});
266
  }
267
+ vids().forEach(function(v){v.addEventListener('canplay',function(){go();});v.addEventListener('loadeddata',function(){go();});});
268
+ window.addEventListener('load',function(){go();});
269
+ iv=setInterval(go,400);
270
+ setTimeout(function(){if(iv){clearInterval(iv);iv=null;}},90000);
271
+ }
272
+ (function(){
273
+ wxAuto(()=>vids, ()=>playing, ()=>{ setPlaying(true); return master.play(); }, ()=>setPlaying(false));
274
  setInterval(()=>{ if(playing&&master&&master.currentTime>=DUR-0.08){
275
  try{master.currentTime=0;}catch(e){} syncSlaves(0,true);
276
  if(playing) vids.forEach(v=>v.play().catch(()=>{})); }},200);
teleopSample/sort_ep1.html CHANGED
@@ -226,17 +226,51 @@ CAMS.forEach(c=>{ const a=document.createElement('a'); a.className='file-chip';
226
  const a=document.createElement('a'); a.className='file-chip'; a.href=href; a.innerHTML=lbl; fm.appendChild(a); });
227
 
228
  // ---- autoplay (muted) + loop
229
- (function(){
230
- let started=false, iv=setInterval(tryStart,400);
231
- function tryStart(){
232
- if(started||playing) return;
233
- if(!vids.length||!vids.every(v=>v.readyState>=2)) return;
234
- started=true; clearInterval(iv);
235
- vids.forEach(v=>v.muted=true); setPlaying(true);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
  }
237
- vids.forEach(v=>{ v.addEventListener('canplay',tryStart); v.addEventListener('loadeddata',tryStart); });
238
- window.addEventListener('load',tryStart);
239
- setTimeout(()=>clearInterval(iv),90000);
 
 
 
 
240
  setInterval(()=>{ if(playing&&master&&master.currentTime>=DUR-0.08){
241
  try{master.currentTime=0;}catch(e){} syncSlaves(0,true);
242
  if(playing) vids.forEach(v=>v.play().catch(()=>{})); }},200);
 
226
  const a=document.createElement('a'); a.className='file-chip'; a.href=href; a.innerHTML=lbl; fm.appendChild(a); });
227
 
228
  // ---- autoplay (muted) + loop
229
+ /* autoplay that also works in in-app WebViews (WeChat iOS/Android X5): they ignore
230
+ preload, so readyState>=2 / canplay never arrive before play() — don't wait forever.
231
+ WeChat injects WeixinJSBridge into the top frame only (this page usually runs in
232
+ index.html's iframe) and play() from its callback needs no tap; if play() is still
233
+ refused, roll back to paused and retry on the first tap. ?wxdebug on the top URL
234
+ shows a log. start() must return the play() promise, stop() resets the UI. */
235
+ function wxAuto(vids,isPlaying,start,stop){
236
+ var inWx=/MicroMessenger/i.test(navigator.userAgent), t0=Date.now(), started=false, armed=false, wxTried=false, wxWait=false, iv=null, dbgEl=null;
237
+ function frames(){var ws=[];try{ws.push(window.top);}catch(e){}try{ws.push(window.parent);}catch(e){}ws.push(window);return ws;}
238
+ function dbg(m){try{if(!/wxdebug/.test(window.top.location.search+window.location.search))return;}catch(e){return;}
239
+ if(!dbgEl){dbgEl=document.createElement('pre');dbgEl.style.cssText='position:fixed;left:0;bottom:0;z-index:99999;max-height:40vh;overflow:auto;margin:0;padding:4px 6px;font:10px/1.3 monospace;background:rgba(0,0,0,.8);color:#7f7;pointer-events:none;white-space:pre-wrap';document.body.appendChild(dbgEl);}
240
+ dbgEl.textContent+=(performance.now()/1000).toFixed(2)+' '+m+'\n';}
241
+ function bridge(){var ws=frames();for(var i=0;i<ws.length;i++){try{if(ws[i].WeixinJSBridge&&ws[i].WeixinJSBridge.invoke)return ws[i].WeixinJSBridge;}catch(e){}}return null;}
242
+ function wxCall(cb){var b=bridge();dbg('bridge '+(b?'yes':'no')+' top='+(window.top===window));
243
+ if(b){b.invoke('getNetworkType',{},function(){dbg('bridge cb');cb();});return;}
244
+ function on(){var b=bridge();dbg('bridge ready '+(b?'yes':'no'));if(b)b.invoke('getNetworkType',{},function(){dbg('bridge cb');cb();});}
245
+ frames().forEach(function(w){try{w.document.addEventListener('WeixinJSBridgeReady',on,{once:true});}catch(e){}});}
246
+ function ready(vs){return vs.every(function(v){return v.readyState>=2;})||
247
+ (Date.now()-t0>1500&&vs.every(function(v){return v.readyState>=1||v.src;}));}
248
+ function go(force){
249
+ if(started||isPlaying())return;
250
+ var vs=vids(); if(!vs.length||(!force&&!ready(vs)))return;
251
+ if(wxWait&&!force)return;
252
+ if(inWx&&!wxTried){wxTried=wxWait=true;wxCall(function(){wxWait=false;go(true);});setTimeout(function(){wxWait=false;go(true);},1500);return;}
253
+ started=true; if(iv){clearInterval(iv);iv=null;}
254
+ vs.forEach(function(v){v.muted=true;});
255
+ dbg('play() rs='+vs.map(function(v){return v.readyState;}).join(','));
256
+ Promise.resolve(start()).then(function(){dbg('play ok');}).catch(function(err){
257
+ dbg('play rejected '+(err&&err.name)); try{stop();}catch(e){} started=false; arm(); });
258
+ }
259
+ function arm(){
260
+ if(armed)return; armed=true;
261
+ function once(ev){if(ev&&ev.target&&ev.target.closest&&ev.target.closest('#play'))return;
262
+ document.removeEventListener('touchend',once,true);document.removeEventListener('click',once,true);
263
+ armed=false;go(true);}
264
+ document.addEventListener('touchend',once,true);document.addEventListener('click',once,true);
265
+ wxCall(function(){go(true);});
266
  }
267
+ vids().forEach(function(v){v.addEventListener('canplay',function(){go();});v.addEventListener('loadeddata',function(){go();});});
268
+ window.addEventListener('load',function(){go();});
269
+ iv=setInterval(go,400);
270
+ setTimeout(function(){if(iv){clearInterval(iv);iv=null;}},90000);
271
+ }
272
+ (function(){
273
+ wxAuto(()=>vids, ()=>playing, ()=>{ setPlaying(true); return master.play(); }, ()=>setPlaying(false));
274
  setInterval(()=>{ if(playing&&master&&master.currentTime>=DUR-0.08){
275
  try{master.currentTime=0;}catch(e){} syncSlaves(0,true);
276
  if(playing) vids.forEach(v=>v.play().catch(()=>{})); }},200);
teleopSample/wipe_ep0.html CHANGED
@@ -226,17 +226,51 @@ CAMS.forEach(c=>{ const a=document.createElement('a'); a.className='file-chip';
226
  const a=document.createElement('a'); a.className='file-chip'; a.href=href; a.innerHTML=lbl; fm.appendChild(a); });
227
 
228
  // ---- autoplay (muted) + loop
229
- (function(){
230
- let started=false, iv=setInterval(tryStart,400);
231
- function tryStart(){
232
- if(started||playing) return;
233
- if(!vids.length||!vids.every(v=>v.readyState>=2)) return;
234
- started=true; clearInterval(iv);
235
- vids.forEach(v=>v.muted=true); setPlaying(true);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
  }
237
- vids.forEach(v=>{ v.addEventListener('canplay',tryStart); v.addEventListener('loadeddata',tryStart); });
238
- window.addEventListener('load',tryStart);
239
- setTimeout(()=>clearInterval(iv),90000);
 
 
 
 
240
  setInterval(()=>{ if(playing&&master&&master.currentTime>=DUR-0.08){
241
  try{master.currentTime=0;}catch(e){} syncSlaves(0,true);
242
  if(playing) vids.forEach(v=>v.play().catch(()=>{})); }},200);
 
226
  const a=document.createElement('a'); a.className='file-chip'; a.href=href; a.innerHTML=lbl; fm.appendChild(a); });
227
 
228
  // ---- autoplay (muted) + loop
229
+ /* autoplay that also works in in-app WebViews (WeChat iOS/Android X5): they ignore
230
+ preload, so readyState>=2 / canplay never arrive before play() — don't wait forever.
231
+ WeChat injects WeixinJSBridge into the top frame only (this page usually runs in
232
+ index.html's iframe) and play() from its callback needs no tap; if play() is still
233
+ refused, roll back to paused and retry on the first tap. ?wxdebug on the top URL
234
+ shows a log. start() must return the play() promise, stop() resets the UI. */
235
+ function wxAuto(vids,isPlaying,start,stop){
236
+ var inWx=/MicroMessenger/i.test(navigator.userAgent), t0=Date.now(), started=false, armed=false, wxTried=false, wxWait=false, iv=null, dbgEl=null;
237
+ function frames(){var ws=[];try{ws.push(window.top);}catch(e){}try{ws.push(window.parent);}catch(e){}ws.push(window);return ws;}
238
+ function dbg(m){try{if(!/wxdebug/.test(window.top.location.search+window.location.search))return;}catch(e){return;}
239
+ if(!dbgEl){dbgEl=document.createElement('pre');dbgEl.style.cssText='position:fixed;left:0;bottom:0;z-index:99999;max-height:40vh;overflow:auto;margin:0;padding:4px 6px;font:10px/1.3 monospace;background:rgba(0,0,0,.8);color:#7f7;pointer-events:none;white-space:pre-wrap';document.body.appendChild(dbgEl);}
240
+ dbgEl.textContent+=(performance.now()/1000).toFixed(2)+' '+m+'\n';}
241
+ function bridge(){var ws=frames();for(var i=0;i<ws.length;i++){try{if(ws[i].WeixinJSBridge&&ws[i].WeixinJSBridge.invoke)return ws[i].WeixinJSBridge;}catch(e){}}return null;}
242
+ function wxCall(cb){var b=bridge();dbg('bridge '+(b?'yes':'no')+' top='+(window.top===window));
243
+ if(b){b.invoke('getNetworkType',{},function(){dbg('bridge cb');cb();});return;}
244
+ function on(){var b=bridge();dbg('bridge ready '+(b?'yes':'no'));if(b)b.invoke('getNetworkType',{},function(){dbg('bridge cb');cb();});}
245
+ frames().forEach(function(w){try{w.document.addEventListener('WeixinJSBridgeReady',on,{once:true});}catch(e){}});}
246
+ function ready(vs){return vs.every(function(v){return v.readyState>=2;})||
247
+ (Date.now()-t0>1500&&vs.every(function(v){return v.readyState>=1||v.src;}));}
248
+ function go(force){
249
+ if(started||isPlaying())return;
250
+ var vs=vids(); if(!vs.length||(!force&&!ready(vs)))return;
251
+ if(wxWait&&!force)return;
252
+ if(inWx&&!wxTried){wxTried=wxWait=true;wxCall(function(){wxWait=false;go(true);});setTimeout(function(){wxWait=false;go(true);},1500);return;}
253
+ started=true; if(iv){clearInterval(iv);iv=null;}
254
+ vs.forEach(function(v){v.muted=true;});
255
+ dbg('play() rs='+vs.map(function(v){return v.readyState;}).join(','));
256
+ Promise.resolve(start()).then(function(){dbg('play ok');}).catch(function(err){
257
+ dbg('play rejected '+(err&&err.name)); try{stop();}catch(e){} started=false; arm(); });
258
+ }
259
+ function arm(){
260
+ if(armed)return; armed=true;
261
+ function once(ev){if(ev&&ev.target&&ev.target.closest&&ev.target.closest('#play'))return;
262
+ document.removeEventListener('touchend',once,true);document.removeEventListener('click',once,true);
263
+ armed=false;go(true);}
264
+ document.addEventListener('touchend',once,true);document.addEventListener('click',once,true);
265
+ wxCall(function(){go(true);});
266
  }
267
+ vids().forEach(function(v){v.addEventListener('canplay',function(){go();});v.addEventListener('loadeddata',function(){go();});});
268
+ window.addEventListener('load',function(){go();});
269
+ iv=setInterval(go,400);
270
+ setTimeout(function(){if(iv){clearInterval(iv);iv=null;}},90000);
271
+ }
272
+ (function(){
273
+ wxAuto(()=>vids, ()=>playing, ()=>{ setPlaying(true); return master.play(); }, ()=>setPlaying(false));
274
  setInterval(()=>{ if(playing&&master&&master.currentTime>=DUR-0.08){
275
  try{master.currentTime=0;}catch(e){} syncSlaves(0,true);
276
  if(playing) vids.forEach(v=>v.play().catch(()=>{})); }},200);
teleopSample/wipe_ep1.html CHANGED
@@ -226,17 +226,51 @@ CAMS.forEach(c=>{ const a=document.createElement('a'); a.className='file-chip';
226
  const a=document.createElement('a'); a.className='file-chip'; a.href=href; a.innerHTML=lbl; fm.appendChild(a); });
227
 
228
  // ---- autoplay (muted) + loop
229
- (function(){
230
- let started=false, iv=setInterval(tryStart,400);
231
- function tryStart(){
232
- if(started||playing) return;
233
- if(!vids.length||!vids.every(v=>v.readyState>=2)) return;
234
- started=true; clearInterval(iv);
235
- vids.forEach(v=>v.muted=true); setPlaying(true);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
  }
237
- vids.forEach(v=>{ v.addEventListener('canplay',tryStart); v.addEventListener('loadeddata',tryStart); });
238
- window.addEventListener('load',tryStart);
239
- setTimeout(()=>clearInterval(iv),90000);
 
 
 
 
240
  setInterval(()=>{ if(playing&&master&&master.currentTime>=DUR-0.08){
241
  try{master.currentTime=0;}catch(e){} syncSlaves(0,true);
242
  if(playing) vids.forEach(v=>v.play().catch(()=>{})); }},200);
 
226
  const a=document.createElement('a'); a.className='file-chip'; a.href=href; a.innerHTML=lbl; fm.appendChild(a); });
227
 
228
  // ---- autoplay (muted) + loop
229
+ /* autoplay that also works in in-app WebViews (WeChat iOS/Android X5): they ignore
230
+ preload, so readyState>=2 / canplay never arrive before play() — don't wait forever.
231
+ WeChat injects WeixinJSBridge into the top frame only (this page usually runs in
232
+ index.html's iframe) and play() from its callback needs no tap; if play() is still
233
+ refused, roll back to paused and retry on the first tap. ?wxdebug on the top URL
234
+ shows a log. start() must return the play() promise, stop() resets the UI. */
235
+ function wxAuto(vids,isPlaying,start,stop){
236
+ var inWx=/MicroMessenger/i.test(navigator.userAgent), t0=Date.now(), started=false, armed=false, wxTried=false, wxWait=false, iv=null, dbgEl=null;
237
+ function frames(){var ws=[];try{ws.push(window.top);}catch(e){}try{ws.push(window.parent);}catch(e){}ws.push(window);return ws;}
238
+ function dbg(m){try{if(!/wxdebug/.test(window.top.location.search+window.location.search))return;}catch(e){return;}
239
+ if(!dbgEl){dbgEl=document.createElement('pre');dbgEl.style.cssText='position:fixed;left:0;bottom:0;z-index:99999;max-height:40vh;overflow:auto;margin:0;padding:4px 6px;font:10px/1.3 monospace;background:rgba(0,0,0,.8);color:#7f7;pointer-events:none;white-space:pre-wrap';document.body.appendChild(dbgEl);}
240
+ dbgEl.textContent+=(performance.now()/1000).toFixed(2)+' '+m+'\n';}
241
+ function bridge(){var ws=frames();for(var i=0;i<ws.length;i++){try{if(ws[i].WeixinJSBridge&&ws[i].WeixinJSBridge.invoke)return ws[i].WeixinJSBridge;}catch(e){}}return null;}
242
+ function wxCall(cb){var b=bridge();dbg('bridge '+(b?'yes':'no')+' top='+(window.top===window));
243
+ if(b){b.invoke('getNetworkType',{},function(){dbg('bridge cb');cb();});return;}
244
+ function on(){var b=bridge();dbg('bridge ready '+(b?'yes':'no'));if(b)b.invoke('getNetworkType',{},function(){dbg('bridge cb');cb();});}
245
+ frames().forEach(function(w){try{w.document.addEventListener('WeixinJSBridgeReady',on,{once:true});}catch(e){}});}
246
+ function ready(vs){return vs.every(function(v){return v.readyState>=2;})||
247
+ (Date.now()-t0>1500&&vs.every(function(v){return v.readyState>=1||v.src;}));}
248
+ function go(force){
249
+ if(started||isPlaying())return;
250
+ var vs=vids(); if(!vs.length||(!force&&!ready(vs)))return;
251
+ if(wxWait&&!force)return;
252
+ if(inWx&&!wxTried){wxTried=wxWait=true;wxCall(function(){wxWait=false;go(true);});setTimeout(function(){wxWait=false;go(true);},1500);return;}
253
+ started=true; if(iv){clearInterval(iv);iv=null;}
254
+ vs.forEach(function(v){v.muted=true;});
255
+ dbg('play() rs='+vs.map(function(v){return v.readyState;}).join(','));
256
+ Promise.resolve(start()).then(function(){dbg('play ok');}).catch(function(err){
257
+ dbg('play rejected '+(err&&err.name)); try{stop();}catch(e){} started=false; arm(); });
258
+ }
259
+ function arm(){
260
+ if(armed)return; armed=true;
261
+ function once(ev){if(ev&&ev.target&&ev.target.closest&&ev.target.closest('#play'))return;
262
+ document.removeEventListener('touchend',once,true);document.removeEventListener('click',once,true);
263
+ armed=false;go(true);}
264
+ document.addEventListener('touchend',once,true);document.addEventListener('click',once,true);
265
+ wxCall(function(){go(true);});
266
  }
267
+ vids().forEach(function(v){v.addEventListener('canplay',function(){go();});v.addEventListener('loadeddata',function(){go();});});
268
+ window.addEventListener('load',function(){go();});
269
+ iv=setInterval(go,400);
270
+ setTimeout(function(){if(iv){clearInterval(iv);iv=null;}},90000);
271
+ }
272
+ (function(){
273
+ wxAuto(()=>vids, ()=>playing, ()=>{ setPlaying(true); return master.play(); }, ()=>setPlaying(false));
274
  setInterval(()=>{ if(playing&&master&&master.currentTime>=DUR-0.08){
275
  try{master.currentTime=0;}catch(e){} syncSlaves(0,true);
276
  if(playing) vids.forEach(v=>v.play().catch(()=>{})); }},200);