references/device-diag.html
references/device-diag.htmlBrowse 22 files
2,775 tokens
9,149 bytes
Token encoding: o200k_base
Snapshot 24fd22b
← Back to SKILL.md
1<!doctype html>2<!--3 device-diag: real-device scrub diagnostic for scrollcraft builds.4 5 Headless verification cannot reproduce a phone's video decoder, autoplay6 policy, Low Power Mode, or touch scrolling. When a clip is reported frozen7 on a real device, deploy this page beside the site (same origin as the8 clips), open it on the device, scroll up and down a few times, and read the9 verdicts. One screenshot isolates the failing layer:10 11 suspect-blob FROZEN, suspect-direct MOVING -> the blob: URL path is the problem12 suspect FROZEN, known-good MOVING -> something specific to that clip's lifecycle13 all MOVING -> the video is fine; the fault is in the14 engine's reveal/tick path on the real page15 all FROZEN -> the device refuses scrub decode; use the16 poster fallback for that act17 18 To use: edit the TESTS array below. Point the first two entries at the19 suspect clip (same file, loaded two ways) and the third at a clip known to20 work on the device. Paths resolve against this page's URL.21 22 Scrubbing is driven by a rAF poll of scrollY, not scroll events, because a23 driven browser can move the page without firing them.24-->25<html lang="en">26<head>27<meta charset="utf-8">28<meta name="viewport" content="width=device-width, initial-scale=1">29<meta name="robots" content="noindex, nofollow">30<title>scrub diagnostic</title>31<style>32 * { margin: 0; box-sizing: border-box; }33 html { background: #0b0f14; color: #e8edf2; font: 13px/1.45 ui-monospace, Menlo, Consolas, monospace; }34 body { height: 520vh; }35 .panel { position: fixed; inset: 0; padding: 10px; display: flex; flex-direction: column; gap: 8px; }36 h1 { font-size: 14px; font-weight: 600; }37 .hint { color: #8fa0b3; }38 .row { display: flex; gap: 8px; flex: 0 0 auto; }39 .cell { flex: 1 1 0; min-width: 0; }40 .cell video { width: 100%; aspect-ratio: 9 / 16; object-fit: cover; background: #000; display: block; border: 1px solid #26303c; border-radius: 6px; }41 .name { font-weight: 600; margin: 6px 0 2px; }42 .verdict { font-size: 15px; font-weight: 700; padding: 2px 6px; border-radius: 4px; display: inline-block; }43 .wait { background: #26303c; }44 .ok { background: #0f5132; color: #b6f0cd; }45 .bad { background: #6b1220; color: #ffc2cd; }46 .stats { color: #aab7c4; font-size: 11.5px; white-space: pre-line; }47 #meta { color: #8fa0b3; font-size: 11.5px; }48 #log { flex: 1 1 auto; overflow: auto; background: #10161d; border: 1px solid #26303c; border-radius: 6px; padding: 8px; font-size: 11px; color: #9fb4c8; white-space: pre-wrap; }49</style>50</head>51<body>52<div class="panel">53 <h1>scrub diagnostic — SCROLL UP AND DOWN A FEW TIMES, then read the verdicts</h1>54 <div class="hint">A: suspect clip, blob URL (how the engine loads it) B: suspect clip, direct file C: a clip that works on this device, blob URL</div>55 <div class="row" id="cells"></div>56 <div id="meta"></div>57 <div id="log"></div>58</div>59<script>60(function () {61 'use strict';62 63 // EDIT THESE for the build under test.64 var SUSPECT = 'assets/01-suspect-m.mp4';65 var KNOWN_GOOD = 'assets/02-known-good-m.mp4';66 var TESTS = [67 { key: 'A', label: 'A suspect blob', src: SUSPECT, mode: 'blob' },68 { key: 'B', label: 'B suspect direct', src: SUSPECT, mode: 'direct' },69 { key: 'C', label: 'C known-good blob', src: KNOWN_GOOD, mode: 'blob' }70 ];71 72 var logEl = document.getElementById('log');73 function log(m) {74 logEl.textContent += m + '\n';75 logEl.scrollTop = logEl.scrollHeight;76 }77 window.addEventListener('error', function (e) { log('JS ERROR: ' + e.message); });78 79 var maxScrolled = 0;80 var cellsEl = document.getElementById('cells');81 var canvas = document.createElement('canvas');82 canvas.width = 16; canvas.height = 16;83 var ctx = canvas.getContext('2d', { willReadFrequently: true });84 85 function make(t) {86 var cell = document.createElement('div');87 cell.className = 'cell';88 cell.innerHTML = '<div class="name">' + t.label + '</div>' +89 '<span class="verdict wait" id="v' + t.key + '">…</span>' +90 '<video muted playsinline preload="auto" id="el' + t.key + '"></video>' +91 '<div class="stats" id="s' + t.key + '"></div>';92 cellsEl.appendChild(cell);93 94 var T = {95 key: t.key, el: cell.querySelector('video'),96 verdict: cell.querySelector('.verdict'),97 stats: cell.querySelector('.stats'),98 seeked: 0, hashes: {}, hashCount: 0, prime: 'not tried',99 primed: false, priming: false, err: ''100 };101 T.el.muted = true; T.el.playsInline = true;102 103 T.el.addEventListener('seeked', function () { T.seeked++; });104 T.el.addEventListener('error', function () {105 var e = T.el.error;106 T.err = 'MediaError code ' + (e ? e.code : '?');107 log(t.key + ': ' + T.err);108 });109 T.el.addEventListener('loadedmetadata', function () {110 log(t.key + ': metadata, duration ' + T.el.duration.toFixed(2) + 's');111 try { T.el.currentTime = 0.001; } catch (e) {}112 prime(T, 'load'); // same as the engine: try with no gesture first113 });114 115 if (t.mode === 'blob') {116 fetch(t.src)117 .then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.blob(); })118 .then(function (b) {119 log(t.key + ': fetched ' + (b.size / 1048576).toFixed(2) + ' MB, blob URL set');120 T.el.src = URL.createObjectURL(b);121 })122 .catch(function (e) { T.err = 'fetch failed: ' + e.message; log(t.key + ': ' + T.err); });123 } else {124 T.el.src = t.src;125 log(t.key + ': direct src set');126 }127 return T;128 }129 130 function prime(T, why) {131 if (T.primed || T.priming || !T.el.src) return;132 T.priming = true;133 var pr;134 try { pr = T.el.play(); } catch (e) { T.priming = false; T.prime = 'threw (' + why + ')'; return; }135 setTimeout(function () {136 if (T.priming) { T.priming = false; if (T.prime === 'not tried') T.prime = 'play() never settled (' + why + ')'; }137 }, 2000);138 if (pr && pr.then) {139 pr.then(function () {140 T.priming = false; T.primed = true; T.prime = 'OK (' + why + ')';141 try { T.el.pause(); } catch (e) {}142 log(T.key + ': primed via ' + why);143 }, function (e) {144 T.priming = false; T.prime = (e && e.name ? e.name : 'rejected') + ' (' + why + ')';145 log(T.key + ': play() rejected via ' + why + ': ' + (e && e.name));146 });147 } else { T.priming = false; T.primed = true; T.prime = 'OK sync (' + why + ')'; try { T.el.pause(); } catch (e) {} }148 }149 150 var tests = TESTS.map(make);151 152 function primeAll(ev) { tests.forEach(function (T) { prime(T, ev); }); }153 ['touchstart', 'touchend', 'pointerdown', 'click', 'scroll'].forEach(function (ev) {154 addEventListener(ev, function () { primeAll(ev); }, { passive: true });155 });156 157 // scroll position drives every clip, exactly like the engine. Polled from a158 // rAF loop rather than a scroll listener, so the diagnostic cannot be blinded159 // by an environment that moves the page without firing scroll events.160 var lastY = -1, lastPrimeTry = 0;161 function loop() {162 var maxY = document.body.scrollHeight - innerHeight;163 var p = maxY > 0 ? Math.max(0, Math.min(1, scrollY / maxY)) : 0;164 if (scrollY !== lastY) {165 if (lastY !== -1 && performance.now() - lastPrimeTry > 600) {166 lastPrimeTry = performance.now();167 primeAll('scrollmove');168 }169 lastY = scrollY;170 maxScrolled = Math.max(maxScrolled, p);171 }172 tests.forEach(function (T) {173 if (!T.el.duration || T.el.seeking) return;174 var t = p * T.el.duration * 0.98;175 if (Math.abs(T.el.currentTime - t) > 0.02) {176 try { T.el.currentTime = t; } catch (e) {}177 }178 });179 requestAnimationFrame(loop);180 }181 requestAnimationFrame(loop);182 183 function frameHash(T) {184 if (T.el.readyState < 2) return null;185 try {186 ctx.drawImage(T.el, 0, 0, 16, 16);187 var d = ctx.getImageData(0, 0, 16, 16).data, h = 0;188 for (var i = 0; i < d.length; i += 7) h = (h * 31 + d[i]) >>> 0;189 return h;190 } catch (e) { return null; }191 }192 193 setInterval(function () {194 tests.forEach(function (T) {195 var h = frameHash(T);196 if (h !== null && !T.hashes[h]) { T.hashes[h] = 1; T.hashCount++; }197 T.stats.textContent =198 'prime: ' + T.prime +199 '\nreadyState ' + T.el.readyState + ' · seeks done ' + T.seeked +200 '\ntime ' + T.el.currentTime.toFixed(2) + 's · frames seen ' + T.hashCount +201 (T.err ? '\n' + T.err : '');202 if (T.hashCount >= 3) { T.verdict.textContent = 'MOVING'; T.verdict.className = 'verdict ok'; }203 else if (T.err) { T.verdict.textContent = 'ERROR'; T.verdict.className = 'verdict bad'; }204 else if (maxScrolled > 0.35) { T.verdict.textContent = 'FROZEN'; T.verdict.className = 'verdict bad'; }205 });206 }, 250);207 208 document.getElementById('meta').textContent =209 navigator.userAgent + ' · ' + innerWidth + 'x' + innerHeight +210 ' · dpr ' + devicePixelRatio;211})();212</script>213</body>214</html>215