修正 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:
+82
-1
@@ -4,6 +4,21 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||||
<title>動態相片</title>
|
<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/aframe.min.js"></script>
|
||||||
<script src="/vendor/mindar-image-aframe.prod.js"></script>
|
<script src="/vendor/mindar-image-aframe.prod.js"></script>
|
||||||
<style>
|
<style>
|
||||||
@@ -41,6 +56,20 @@
|
|||||||
}
|
}
|
||||||
#dlBtn:active { transform: scale(0.94); }
|
#dlBtn:active { transform: scale(0.94); }
|
||||||
a { color: #8fb4ff; }
|
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -49,10 +78,12 @@
|
|||||||
<p id="status">載入中…</p>
|
<p id="status">載入中…</p>
|
||||||
<button id="startBtn" hidden>開始掃描</button>
|
<button id="startBtn" hidden>開始掃描</button>
|
||||||
<p class="hint">按下開始後,將相機對準已登錄的照片,<br>對應的影片就會覆蓋在照片上播放。</p>
|
<p class="hint">按下開始後,將相機對準已登錄的照片,<br>對應的影片就會覆蓋在照片上播放。</p>
|
||||||
<p class="hint" style="font-size:11px">v7</p>
|
<p class="hint" style="font-size:11px">v8</p>
|
||||||
</div>
|
</div>
|
||||||
<button id="stopBtn" title="停止">✕</button>
|
<button id="stopBtn" title="停止">✕</button>
|
||||||
<a id="dlBtn" title="下載影片" download>⬇</a>
|
<a id="dlBtn" title="下載影片" download>⬇</a>
|
||||||
|
<div id="errBox"></div>
|
||||||
|
<div id="dbgBox"></div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const overlay = document.getElementById('overlay');
|
const overlay = document.getElementById('overlay');
|
||||||
@@ -60,6 +91,47 @@ const statusEl = document.getElementById('status');
|
|||||||
const startBtn = document.getElementById('startBtn');
|
const startBtn = document.getElementById('startBtn');
|
||||||
const stopBtn = document.getElementById('stopBtn');
|
const stopBtn = document.getElementById('stopBtn');
|
||||||
const dlBtn = document.getElementById('dlBtn');
|
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;
|
let sceneEl = null;
|
||||||
const videos = [];
|
const videos = [];
|
||||||
@@ -92,6 +164,7 @@ async function init() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dbg(`state 載入完成:${entries.length} 個目標,編譯於 ${new Date(state.targets.compiledAt).toLocaleString()}`);
|
||||||
buildScene(state.targets.compiledAt, entries);
|
buildScene(state.targets.compiledAt, entries);
|
||||||
statusEl.textContent = '';
|
statusEl.textContent = '';
|
||||||
startBtn.hidden = false;
|
startBtn.hidden = false;
|
||||||
@@ -153,6 +226,7 @@ function buildScene(version, entries) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
anchor.addEventListener('targetFound', () => {
|
anchor.addEventListener('targetFound', () => {
|
||||||
|
dbg(`targetFound:#${t.index}(${t.pair.name})`);
|
||||||
isFound = true;
|
isFound = true;
|
||||||
video.muted = false; // 掃到照片這一刻才解除靜音
|
video.muted = false; // 掃到照片這一刻才解除靜音
|
||||||
video.defaultMuted = false;
|
video.defaultMuted = false;
|
||||||
@@ -167,6 +241,7 @@ function buildScene(version, entries) {
|
|||||||
dlBtn.style.display = 'flex';
|
dlBtn.style.display = 'flex';
|
||||||
});
|
});
|
||||||
anchor.addEventListener('targetLost', () => {
|
anchor.addEventListener('targetLost', () => {
|
||||||
|
dbg(`targetLost:#${t.index}`);
|
||||||
isFound = false;
|
isFound = false;
|
||||||
video.pause();
|
video.pause();
|
||||||
});
|
});
|
||||||
@@ -204,6 +279,12 @@ startBtn.addEventListener('click', async () => {
|
|||||||
if (!v.paused) v.pause();
|
if (!v.paused) v.pause();
|
||||||
v.currentTime = 0;
|
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 });
|
}, { once: true });
|
||||||
|
|
||||||
const start = () => {
|
const start = () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user