Files
juzi/taobao/utils/voice.js
T
xsl fed52930cc feat(miniapp): align UI with H5 and harden device debugging
五个页面按 h5/css 的 style.css → ui-20261009 → ui-20261012 覆盖顺序逐条重排,
换算规则固定为 calc(N * var(--u)) → 2N rpx,固定 px 的字号保持 px。

- app.acss:换回 H5 设计 token(方角小倒角按钮 10rpx、卡片 14rpx、.chev 折线箭头)
- splash/profile/history:底图与按钮文字图改用 H5 资源,隐私/政策弹窗按 H5 重排
- report:环形分用 conic-gradient、人脸裁切窗口叠加 face_maps、标记点百分比盒、
  侧脸皱纹 pending/unavailable 文案;用 Playwright 抓 H5 94 个选择器几何比对,
  整页高 2438.7 vs 2438.67
- camera:淘宝 <camera> 组件在真机整页崩溃(嵌了微信专有的 <cover-view>),
  改为默认系统相机 + `config.nativeCamera` 开关 + pages/camtest 冒烟页验证组件
- permissions:淘宝用带前缀的 scope.camera(裸名返回 5022 Scope:camera信息不存在),
  先试带前缀再退回裸名,拒绝后不再重复弹窗
- voice:非 HTTPS 资源导致的异常不再冒泡成整页崩溃;app.js 加开发版错误 toast
- 开发版入口:权限诊断(启动页/相机页)、相机组件测试
- 测试 20/20(新增权限与相机能力用例)
2026-11-01 23:30:38 +08:00

126 lines
4.6 KiB
JavaScript

/* ═════ 语音播报:my.createInnerAudioContext 播放远程 MP3 ═════
* 文案/文件名契约:docs/语音播报内容设计文档_V1.0.md(A 步骤引导 / B 姿态提示 / C 流程音)
* 播放策略与 H5 一致:单通道;流程音排队顺序播不互截;姿态提示同条 4s 去重、不打断流程音;
* 资源远程化:{ASSET_BASE}voice/<id>.mp3,清单 manifest.json 缺失时默认全量尝试(404 静默) */
var api = require('./api.js');
// A/B/C 三类清单(与 H5 app.js VOICE 常量一致)
var VOICE = {
step: ['step1-front', 'step2-rightface', 'step3-leftface'], // A. 步骤引导
hint: { // B. 姿态提示(手动快门模式下基本不用,保留契约)
'未检测到人脸': 'hint-noface', '请靠近一点': 'hint-near', '请远一点': 'hint-far',
'请正对屏幕': 'hint-front', '请向左转头': 'hint-turn-left', '请向右转头': 'hint-turn-right',
'保持不动': 'hint-hold'
},
countdown: 'countdown-321', shot: 'shot-done', // C. 拍摄与流程
analyzing: 'analyzing', fail: 'analyze-fail'
};
var DEDUPE_MS = 4000; // 同条提示 4s 内不重复播
var audio = null; // InnerAudioContext 单实例
var files = null; // null=清单未加载;{}=清单不可用(默认全量尝试)
var lastAt = {}; // id → 上次开播时间(去重)
var cur = null; // { id, kind:'flow'|'hint', token }
var queue = []; // 排队中的流程音
var pendingHint = null; // 流程音期间挂起的姿态提示(只留最新)
var waiters = []; // afterCurrent 回调:全部播报收尾后执行
// 拉取远程清单;失败则放行全量(语音缺失时 InnerAudioContext onError 静默跳过)
function init() {
if (audio) return;
if (typeof my.createInnerAudioContext !== 'function') return;
try { audio = my.createInnerAudioContext(); } catch (_) { return; }
audio.onEnded(function () { drain(); });
audio.onError(function () { drain(); }); // 单条加载失败:跳过,继续队列
files = {}; // Known voice IDs; unavailable files are skipped by onError.
}
function ensure() { if (!audio) init(); }
function playable(id) { return !!id && (files === null || !Object.keys(files).length || files[id]); }
function start(id, kind) {
ensure();
if (!audio) { drain(); return; }
lastAt[id] = Date.now();
cur = { id: id, kind: kind, token: {} };
var token = cur.token;
try {
audio.stop();
audio.src = api.assetUrl('voice/' + id + '.mp3');
audio.play();
} catch (_) {
// 资源域名不可用、平台拒绝非 HTTPS src 等情况:静默跳过。
// 语音只是辅助,绝不能让异常冒泡成页面崩溃(真机曾整页白屏)。
cur = null;
drain();
return;
}
// 播放失败/结束时 cur 清理放在 drain 前,避免排队卡死
cur.token = token;
}
function stopCur() {
if (!cur) return;
cur = null;
if (audio) { try { audio.stop(); } catch (e) {} }
}
function drain() {
cur = null;
if (queue.length) { start(queue.shift(), 'flow'); return; }
if (pendingHint) { var h = pendingHint; pendingHint = null; start(h, 'hint'); return; }
var w = waiters;
waiters = [];
w.forEach(function (fn) { fn(); });
}
function busyWithFlow() { return (cur && cur.kind === 'flow') || queue.length > 0; }
/**
* 播报一条语音
* @param {string} id 文件名(去 .mp3)
* @param {object} [opts] hint:姿态提示(4s 去重);cut:打断在播与排队(失败提示用)
*/
function play(id, opts) {
opts = opts || {};
if (!playable(id)) return;
ensure();
if (opts.hint) {
if (lastAt[id] && Date.now() - lastAt[id] < DEDUPE_MS) return;
if (busyWithFlow()) { pendingHint = id; return; }
start(id, 'hint');
return;
}
if (opts.cut) { queue = []; pendingHint = null; stopCur(); start(id, 'flow'); return; }
if (busyWithFlow()) { if (queue.indexOf(id) < 0) queue.push(id); return; }
start(id, 'flow');
}
function dropHint() { pendingHint = null; } // 换步/提示清空:挂起的旧提示作废
// 流程切走:作废排队/挂起/等待;hard 连在播一并掐断(退出拍照页),否则让在播自然收尾(出报告)
function stop(hard) {
queue = [];
pendingHint = null;
waiters = [];
if (hard) stopCur();
}
// 全部播报(含排队与挂起)自然收尾后执行 cb;空闲则立即执行
function afterCurrent(cb) {
if (!cur && !queue.length && !pendingHint) { cb(); return; }
waiters.push(cb);
}
module.exports = {
VOICE: VOICE,
init: init,
play: play,
dropHint: dropHint,
stop: stop,
afterCurrent: afterCurrent
};