修正 iOS 27 Beta 掃描無反應,掃描頁加入畫面內錯誤與除錯顯示(v8)

根本原因:
iOS 27 Beta 的 WebKit 回歸使 tfjs 打包著色器連結失敗
(Failed to link vertex and fragment shaders),MindAR 辨識引擎
啟動時無聲掛掉;相機畫面只是一般 video 元素照常顯示,
使用者看到的症狀就是「掃描完全沒反應」。錯誤只進 console,
行動裝置上看不到,導致先前無從排查。

影響:
iOS 27 Beta 裝置無論掃哪張照片都不會觸發辨識;iOS 26 與桌面正常。

修法:
1. 在 MindAR 載入前偵測 iOS 裝置,自動把 tfjsflags=WEBGL_PACK:false
   補進網址(tfjs 由 query string 讀取設定),改用未打包著色器,
   已在 iOS 27 Beta 實機驗證可正常辨識;網址手動帶 tfjsflags 時不覆蓋。
2. 未攔截的 JS 錯誤改以紅色橫幅直接顯示在畫面頂端;網址加 ?debug
   可在畫面底部看到掃描過程(目標數、相機解析度、targetFound/Lost、
   console 訊息),行動裝置也能自行排查。版本字樣升至 v8。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-28 11:41:20 +08:00
co-authored by Claude Fable 5
parent 1c806d45e1
commit 77fff4326c
+82 -1
View File
@@ -4,6 +4,21 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>動態相片</title>
<script>
// iOS 27 Beta 的 WebKit 會讓 tfjs 打包著色器連結失敗
// Failed to link vertex and fragment shaders,辨識引擎無聲掛掉),
// 改用未打包著色器可正常運作。必須在 MindAR 載入前把 tfjsflags
// 補進網址,tfjs 是從 query string 讀取這個設定的;
// 網址已手動帶 tfjsflags 時不覆蓋,方便之後實驗其他參數
(() => {
const isIOS = /iPhone|iPad|iPod/.test(navigator.userAgent) ||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
if (isIOS && !location.search.includes('tfjsflags')) {
const q = location.search ? location.search + '&' : '?';
history.replaceState(null, '', q + 'tfjsflags=WEBGL_PACK:false' + location.hash);
}
})();
</script>
<script src="/vendor/aframe.min.js"></script>
<script src="/vendor/mindar-image-aframe.prod.js"></script>
<style>
@@ -41,6 +56,20 @@
}
#dlBtn:active { transform: scale(0.94); }
a { color: #8fb4ff; }
#errBox {
position: fixed; top: 0; left: 0; right: 0; z-index: 2000;
display: none; padding: 10px 14px;
background: rgba(170, 32, 32, .92); color: #fff;
font: 12px/1.6 monospace; white-space: pre-wrap; word-break: break-all;
max-height: 45%; overflow: auto;
}
#dbgBox {
position: fixed; bottom: 0; left: 0; z-index: 2000;
display: none; padding: 8px 12px; max-width: 100%;
background: rgba(0, 0, 0, .6); color: #9f9;
font: 11px/1.5 monospace; white-space: pre-wrap; word-break: break-all;
max-height: 40%; overflow: auto;
}
</style>
</head>
<body>
@@ -49,10 +78,12 @@
<p id="status">載入中…</p>
<button id="startBtn" hidden>開始掃描</button>
<p class="hint">按下開始後,將相機對準已登錄的照片,<br>對應的影片就會覆蓋在照片上播放。</p>
<p class="hint" style="font-size:11px">v7</p>
<p class="hint" style="font-size:11px">v8</p>
</div>
<button id="stopBtn" title="停止"></button>
<a id="dlBtn" title="下載影片" download></a>
<div id="errBox"></div>
<div id="dbgBox"></div>
<script>
const overlay = document.getElementById('overlay');
@@ -60,6 +91,47 @@ const statusEl = document.getElementById('status');
const startBtn = document.getElementById('startBtn');
const stopBtn = document.getElementById('stopBtn');
const dlBtn = document.getElementById('dlBtn');
const errBox = document.getElementById('errBox');
const dbgBox = document.getElementById('dbgBox');
// 掃描在某些裝置上「沒反應」時,錯誤常常只進 console 看不到。
// 這裡把所有未攔截的錯誤直接顯示在畫面頂端,行動裝置也能看到
function showErr(msg) {
errBox.style.display = 'block';
errBox.textContent += (errBox.textContent ? '\n' : '') + msg;
}
window.addEventListener('error', (e) => {
showErr('錯誤:' + (e.message || String(e.error || '未知')));
});
window.addEventListener('unhandledrejection', (e) => {
const r = e.reason;
showErr('錯誤:' + ((r && r.message) || String(r)));
});
// 網址加 ?debug 會在畫面底部顯示掃描過程(相機解析度、targetFound 等)
const DEBUG = new URLSearchParams(location.search).has('debug');
function dbg(msg) {
if (!DEBUG) return;
dbgBox.style.display = 'block';
const t = (performance.now() / 1000).toFixed(1);
dbgBox.textContent += `[${t}s] ${msg}\n`;
dbgBox.scrollTop = dbgBox.scrollHeight;
}
// debug 模式下把 console 訊息也收進畫面:
// tfjs 著色器編譯失敗的詳細原因只會 console.log,行動裝置上否則看不到
if (DEBUG) {
for (const level of ['log', 'warn', 'error']) {
const orig = console[level].bind(console);
console[level] = (...args) => {
orig(...args);
try {
const text = args.map((a) => typeof a === 'string' ? a : JSON.stringify(a)).join(' ');
if (text && !text.startsWith('%c')) dbg(`console.${level}: ${text.slice(0, 400)}`);
} catch {}
};
}
}
let sceneEl = null;
const videos = [];
@@ -92,6 +164,7 @@ async function init() {
return;
}
dbg(`state 載入完成:${entries.length} 個目標,編譯於 ${new Date(state.targets.compiledAt).toLocaleString()}`);
buildScene(state.targets.compiledAt, entries);
statusEl.textContent = '';
startBtn.hidden = false;
@@ -153,6 +226,7 @@ function buildScene(version, entries) {
}
});
anchor.addEventListener('targetFound', () => {
dbg(`targetFound#${t.index}${t.pair.name}`);
isFound = true;
video.muted = false; // 掃到照片這一刻才解除靜音
video.defaultMuted = false;
@@ -167,6 +241,7 @@ function buildScene(version, entries) {
dlBtn.style.display = 'flex';
});
anchor.addEventListener('targetLost', () => {
dbg(`targetLost#${t.index}`);
isFound = false;
video.pause();
});
@@ -204,6 +279,12 @@ startBtn.addEventListener('click', async () => {
if (!v.paused) v.pause();
v.currentTime = 0;
}
// MindAR 的相機 video 是掛在 body 下、沒有 id 的那個元素
const cam = document.querySelector('body > video');
dbg('arReady:辨識引擎啟動' + (cam ? `,相機 ${cam.videoWidth}x${cam.videoHeight}` : ''));
}, { once: true });
sceneEl.addEventListener('arError', () => {
showErr('AR 啟動失敗(arError):通常是相機無法開啟或 WebGL 資源不足');
}, { once: true });
const start = () => {