Files
juzi/taobao/pages/camtest/camtest.js
T
xsl 774376fa16 feat: 完善淘宝小程序拍照、报告与登录流程
接入本地实时识别和自动三连拍,调整报告裁切、商品跳转、分享标题及居中页眉;完善游客会话、后台渠道隔离、部署脚本与回归测试。
2026-11-04 15:07:51 +08:00

318 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Standalone device probe. No account API, upload, server call or saved photo.
var AR = require('../../utils/ar-camera');
var image = require('../../utils/image');
var permissions = require('../../utils/permissions');
var config = require('../../config');
var PROBE_VERSION = 'AR-DIAG-20261102-05';
var CALIBRATION_FORMATS = ['RGBA', 'BGRA', 'ARGB', 'ABGR'];
function confirmedPixelFormat() {
try {
var format = image.confirmedFramePixelFormat();
return CALIBRATION_FORMATS.indexOf(format) >= 0 ? format : '';
} catch (_) { return ''; }
}
Page({
data: { running: false, busy: false, exporting: false, devicePosition: config.arFrontPosition === 'back' ? 'back' : 'front', cameraSession: 0, status: '请点击开启,验证淘宝真实 AR 帧与照片导出。',
probeVersion: PROBE_VERSION, toolbarTop: 112, environmentText: '', capabilityText: '', contextText: '等待 onInit 后读取公开方法名', eventText: '尚未收到原生事件', copyStatus: '', diagnosticText: '',
target: 'front', frameCount: 0, standardFrameCount: 0, frameText: '尚未收到帧事件',
calibrationItems: [], calibrationReady: false, calibrationText: '', calibrationConfirmed: '',
poseText: '', fieldText: '', errorText: '', snapshotUrl: '', snapshotText: '', compressWidth: 1, compressHeight: 1 },
onLoad: function () {
this.alive = true; this.visible = true; this.sequence = 0;
var capability = AR.capability(my);
this.diagnosticSystem = AR.diagnostics.system(my); this.contextMethodNames = []; this.nativeEventStats = {};
this.diagnosticCapability = 'ar-camera=' + capability.componentSignal + '; createArCameraContext=' + capability.contextFactory;
this.setData({ toolbarTop: AR.diagnostics.toolbarTop(my), calibrationConfirmed: confirmedPixelFormat(), environmentText: this.diagnosticSystem.join('\n') || '系统未返回可读取的设备/宿主版本;请手动补充淘宝版本。',
capabilityText: 'ar-camera 声明:' + capability.componentSignal + ';上下文工厂:' + (capability.contextFactory ? '存在' : '不存在') + '。声明结果不代表真机已支持。' });
},
onShow: function () { this.visible = true; },
flushFrameDiagnostic: function () {
var pending = this.pendingFrameDiagnostic;
if (!pending) return;
this.pendingFrameDiagnostic = null;
var record = this.nativeEventStats[pending.key], summary = AR.diagnostics.event(pending.event);
record.type = summary.type || '(未提供 type)'; record.metadata = summary.metadata;
this.lastDiagnosticMetadata = summary.metadata;
},
nativeEventView: function () {
var that = this;
return { eventText: Object.keys(this.nativeEventStats).map(function (item) {
var value = that.nativeEventStats[item];
return item + ' · type=' + value.type + ' · 次数 ' + value.count;
}).join('\n'), fieldText: (this.lastDiagnosticMetadata || []).join('\n') || '本事件未返回白名单元信息;不会输出未知字段值或人脸坐标。' };
},
recordNativeEvent: function (name, event, deferFrame) {
var key = name + ':request=' + this.data.devicePosition;
var record = this.nativeEventStats[key] || { count: 0, type: '(未提供 type)', metadata: [] };
record.count++;
this.nativeEventStats[key] = record;
// Count every frame in JS, but do not scan descriptors or cross the UI
// bridge until the 400ms refresh. Only the latest frame is retained.
if (deferFrame) { this.pendingFrameDiagnostic = { key: key, event: event }; return; }
this.flushFrameDiagnostic();
var summary = AR.diagnostics.event(event);
record.type = summary.type || '(未提供 type)'; record.metadata = summary.metadata;
this.lastDiagnosticMetadata = summary.metadata;
this.setData(this.nativeEventView());
return summary;
},
diagnosticReport: function () {
// A user-requested copy gets up-to-date counters and last-frame metadata
// without introducing another UI refresh on the high-rate native stream.
this.flushFrameDiagnostic();
var that = this, lines = [PROBE_VERSION].concat(this.diagnosticSystem || []);
lines.push(this.diagnosticCapability || 'capability=unavailable');
lines.push('requestedPosition=' + this.data.devicePosition + '; actualPosition=需核对实际画面');
lines.push('context.methods=' + ((this.contextMethodNames || []).join(', ') || '(none observed)'));
lines.push('pixelFormat.default=' + (CALIBRATION_FORMATS.indexOf(config.arPixelFormat) >= 0 ? config.arPixelFormat : '(none)') + '; defaultIsUserConfirmed=false');
lines.push('pixelFormat.confirmed=' + (confirmedPixelFormat() || '(unconfirmed)'));
lines.push('currentSession.frameEvents=' + (this.frames || 0) + '; parsableFrames=' + (this.standardFrames || 0));
Object.keys(this.nativeEventStats || {}).forEach(function (key) {
var record = that.nativeEventStats[key];
lines.push(key + '; type=' + record.type + '; count=' + record.count);
record.metadata.forEach(function (item) { lines.push(' ' + item); });
});
return lines.join('\n');
},
onCopyDiagnostics: function () {
if (!this.alive) return;
// Always reveal the same privacy-filtered text first, so clipboard failure
// or an absent API still leaves a native selectable-text fallback.
var report = this.diagnosticReport();
this.setData({ diagnosticText: report, copyStatus: '安全诊断已生成;下方文字可长按复制。' });
if (typeof my.setClipboard !== 'function') { this.setData({ copyStatus: '当前淘宝未提供剪贴板 API,请长按下方诊断文字复制。' }); return; }
var that = this;
try {
my.setClipboard({ text: report, success: function () { if (that.alive) that.setData({ copyStatus: '诊断信息已复制。也可长按下方文字复制。' }); },
fail: function () { if (that.alive) that.setData({ copyStatus: '自动复制失败,请长按下方诊断文字复制。' }); } });
} catch (_) { this.setData({ copyStatus: '自动复制失败,请长按下方诊断文字复制。' }); }
},
onOpenNativeProbe: function () {
if (this.data.busy || this.data.exporting) return;
my.redirectTo({ url: '/pages/camnative/camnative' });
},
currentCalibration: function (batch) {
return !!(batch && this.calibrationBatch === batch && batch.sequence === this.sequence && this.alive && this.visible && this.data.running);
},
invalidateCalibration: function () {
if (this.calibrationBatch) this.calibrationBatch.frame = null;
this.calibrationBatch = null;
if (this.alive) this.setData({ calibrationItems: [], calibrationReady: false, calibrationText: '' });
},
onCalibrate: function () {
if (!this.alive || !this.visible || !this.data.running) return Promise.resolve();
if (this.exportInFlight || this.data.busy) {
this.setData({ errorText: '上一批照片仍在导出,请稍候再校验颜色。' }); return Promise.resolve();
}
if (!this.lastFrame) { this.setData({ errorText: '尚未收到真实相机帧,无法校验照片颜色。' }); return Promise.resolve(); }
var frozen, formats;
try {
// Freeze once: every preview and the eventual explicit confirmation refer
// to the exact same image, not a later native/reused camera buffer.
frozen = image.freezeCalibrationFrame(this.lastFrame);
formats = image.frameCalibrationFormats(frozen).filter(function (format) { return CALIBRATION_FORMATS.indexOf(format) >= 0; });
} catch (error) { this.setData({ errorText: error.message || '当前帧无法进行本地颜色校验。' }); return Promise.resolve(); }
if (!formats.length) {
this.setData({ errorText: '当前帧不是可校验的无格式四通道图像,请等待新帧或直接导出照片。' }); return Promise.resolve();
}
this.invalidateCalibration();
var that = this, operation = {}, batch = { sequence: this.sequence, id: this.sequence + '-' + ((this.calibrationSerial || 0) + 1),
frame: frozen, formats: formats, items: [], settled: false };
this.calibrationSerial = (this.calibrationSerial || 0) + 1;
this.calibrationBatch = batch; this.exportInFlight = operation;
this.setData({ busy: true, exporting: true, errorText: '', snapshotUrl: '', snapshotText: '',
calibrationText: '正在按顺序生成本地颜色预览;全部完成后,请选择颜色正常的一张。' });
return formats.reduce(function (chain, format) {
return chain.then(function () {
if (!that.currentCalibration(batch)) return;
return image.captureFrameToBase64(batch.frame, that, { pixelFormat: format, maxSide: 480 }).then(function (url) {
if (!that.currentCalibration(batch)) return;
batch.items.push({ format: format, url: url, batchId: batch.id });
that.setData({ calibrationItems: batch.items.slice(), calibrationText: '已生成 ' + batch.items.length + '/' + batch.formats.length + ' 张,完成后可确认。' });
});
});
}, Promise.resolve()).then(function () {
batch.settled = true;
if (that.currentCalibration(batch)) that.setData({ calibrationReady: true, calibrationText: '请比较肤色和背景,点击颜色正常照片下方的按钮;尚未确认任何新格式。' });
}).catch(function (error) {
batch.settled = true;
if (that.currentCalibration(batch)) {
that.invalidateCalibration();
that.setData({ errorText: error.message || '颜色预览导出失败,请重试。', calibrationText: '本批颜色预览导出失败,未确认新格式,请重试。' });
}
}).then(function () {
if (!that.currentCalibration(batch)) batch.frame = null;
if (that.exportInFlight !== operation) return;
that.exportInFlight = null;
if (that.alive) {
var patch = { exporting: false };
if (batch.sequence === that.sequence) patch.busy = false;
that.setData(patch);
}
});
},
onConfirmCalibration: function (event) {
var batch = this.calibrationBatch, dataset = event && event.currentTarget && event.currentTarget.dataset || {}, format = dataset.format;
if (!this.currentCalibration(batch) || !batch.settled || String(dataset.batch) !== batch.id || batch.formats.indexOf(format) < 0
|| !batch.items.some(function (item) { return item.format === format; })) return;
try {
var confirmed = image.confirmFramePixelFormat(batch.frame, format);
if (confirmed !== format) throw new Error('未能确认相机格式,请重新校验。');
this.invalidateCalibration();
this.setData({ calibrationConfirmed: confirmed, calibrationText: '本次相机格式已确认:' + confirmed + ',已记录本次颜色选择。', errorText: '' });
} catch (error) { this.setData({ errorText: error.message || '格式确认失败,请重新校验。' }); }
},
acceptsCameraEvent: function (event) {
if (!this.alive || !this.visible || !this.data.running) return false;
var target = event && (event.currentTarget || event.target), dataset = target && target.dataset;
// Native callbacks from an unmounted camera must not enter a new session.
// Events without a marker remain compatible with older host callbacks.
if (dataset && dataset.session !== undefined && String(dataset.session) !== String(this.data.cameraSession)) return false;
var id = target && target.id;
return typeof id !== 'string' || id.indexOf('test-ar-camera-') !== 0 || id === 'test-ar-camera-' + this.data.cameraSession;
},
mountCamera: function (sequence) {
if (!this.alive || !this.visible || sequence !== this.sequence) return;
var that = this;
clearTimeout(this.frameTimer);
this.frames = 0; this.standardFrames = 0; this.lastFrame = null; this.context = null; this.lastUpdate = 0; this.frameUiShown = false; this.pendingFrameDiagnostic = null; this.contextMethodNames = [];
this.setData({ running: true, busy: false, cameraSession: sequence, frameCount: 0, standardFrameCount: 0, frameText: '尚未收到帧事件', contextText: '等待 onInit 后读取公开方法名',
status: '已请求' + (this.data.devicePosition === 'front' ? '前置' : '后置') + '镜头,等待真实帧;请核对实际画面…' });
this.frameTimer = setTimeout(function () {
if (that.alive && that.visible && sequence === that.sequence && !that.frames) that.setData({ status: '10秒内未收到帧事件(onARFrame)。请复制诊断,不能视为能力已通过。' });
}, 10000);
},
onStart: function () {
if (this.data.running || this.data.busy) return Promise.resolve();
var that = this, sequence = ++this.sequence;
this.setData({ busy: true, errorText: '', snapshotUrl: '', snapshotText: '', frameCount: 0, poseText: '', fieldText: '' });
// Camera permission only. Neither Taobao userInfo nor album is needed.
return permissions.ensure('scope.camera').then(function () {
if (!that.alive || !that.visible || sequence !== that.sequence) return;
that.mountCamera(sequence);
}).catch(function (error) {
if (that.alive && sequence === that.sequence) that.setData({ errorText: error.message || '相机授权失败' });
}).then(function () { if (that.alive && sequence === that.sequence) that.setData({ busy: false }); });
},
onARInit: function (event) {
if (!this.acceptsCameraEvent(event)) return;
this.recordNativeEvent('onInit', event);
try {
this.context = AR.context(my, 'test-ar-camera-' + this.data.cameraSession);
// Enumerate descriptors only. This diagnostic never probes unknown methods
// or executes property getters/automatic context actions.
this.contextMethodNames = AR.diagnostics.methods(this.context);
this.setData({ contextText: this.contextMethodNames.join(', ') || '未发现可读取的公开方法;不代表原生能力不存在。', status: 'AR 初始化完成,等待实时人脸数据…' });
} catch (error) { this.setData({ errorText: error.message || 'AR 上下文初始化失败' }); }
},
onARFrame: function (event) {
if (!this.acceptsCameraEvent(event)) return;
this.frames = (this.frames || 0) + 1; this.lastFrame = event && event.detail || event;
this.recordNativeEvent('onARFrame', event, true);
clearTimeout(this.frameTimer);
// Facing selects hardware, not the native landmark coordinate convention.
// Do not reverse yaw solely because a front camera was requested.
var verdict;
try { verdict = AR.evaluate(event, this.data.target, false); } catch (_) { verdict = { frameReceived: false }; }
if (verdict.frameReceived) this.standardFrames = (this.standardFrames || 0) + 1;
var now = Date.now();
if (this.frameUiShown && now - this.lastUpdate < 400) return;
this.lastUpdate = now; this.frameUiShown = true;
this.flushFrameDiagnostic();
var patch = this.nativeEventView();
patch.frameCount = this.frames; patch.standardFrameCount = this.standardFrames || 0;
if (!verdict.frameReceived) {
patch.status = '已收到 onARFrame 回调,但格式不符合 width/height/anchors;请复制诊断。';
patch.frameText = '已收到原生回调,尚无可解析的标准帧'; patch.poseText = '';
this.setData(patch);
return;
}
var observation = verdict.observation;
patch.status = verdict.ok ? '实时人脸判断通过;仍需点击导出照片验证原始图像。' : verdict.hint;
patch.frameText = verdict.width + '×' + verdict.height + ' · 人脸 ' + verdict.faceCount + ' · 已收到真实帧';
patch.poseText = observation ? '几何姿态 yaw ' + observation.yaw.toFixed(1) + '° / pitch ' + observation.pitch.toFixed(1) + '° / roll ' + observation.roll.toFixed(1) + '°' : '';
this.setData(patch);
},
onTarget: function (event) {
var target = event.currentTarget.dataset.target;
if (['front', 'left', 'right'].indexOf(target) >= 0) this.setData({ target: target });
},
onFlip: function () {
if (!this.alive || !this.visible || !this.data.running) return Promise.resolve();
var that = this, sequence = ++this.sequence;
clearTimeout(this.frameTimer);
this.flushFrameDiagnostic();
this.invalidateCalibration();
this.lastFrame = null; this.context = null; this.frames = 0; this.lastUpdate = 0;
// devicePosition is shared-component compatibility evidence. Its actual
// effect in the Taobao host must be checked on a phone; no context method
// or successful callback is treated as proof that hardware changed.
return new Promise(function (resolve) {
that.setData({ running: false, busy: true, devicePosition: that.data.devicePosition === 'front' ? 'back' : 'front',
cameraSession: sequence, frameCount: 0, frameText: '尚未收到真实帧', poseText: '', fieldText: '', errorText: '',
snapshotUrl: '', snapshotText: '', status: '正在重新挂载相机;切换效果以实际画面为准…' }, function () {
that.mountCamera(sequence);
resolve();
});
});
},
onSnapshot: function () {
// Image decoding/draw callbacks may still use the shared canvas after a
// camera session changes. Release this lock only when that export settles.
if (this.exportInFlight) {
this.setData({ errorText: '上一帧照片仍在导出,请稍候再试。' });
return Promise.resolve();
}
if (!this.lastFrame || this.data.busy) {
if (!this.lastFrame) this.setData({ errorText: '尚未收到真实相机帧,无法验证照片导出。' });
return Promise.resolve();
}
var that = this, sequence = this.sequence, operation = {};
this.exportInFlight = operation;
this.setData({ busy: true, exporting: true, errorText: '', snapshotUrl: '', snapshotText: '' });
var work;
try { work = image.captureFrameToBase64(this.lastFrame, this, { defaultPixelFormat: config.arPixelFormat }); } catch (error) { work = Promise.reject(error); }
return Promise.resolve(work).then(function (value) {
if (!that.alive || !that.visible || sequence !== that.sequence) return;
that.setData({ snapshotUrl: value, snapshotText: 'JPEG ' + that.data.compressWidth + '×' + that.data.compressHeight + ',仅本页预览,未上传、未保存到相册。', status: '已导出真实帧照片,请核对方向和颜色。' });
}).catch(function (error) {
if (that.alive && that.visible && sequence === that.sequence) that.setData({ errorText: error.message || '照片导出失败,请记录帧字段与实际错误。' });
}).then(function () {
if (that.exportInFlight !== operation) return;
that.exportInFlight = null;
if (that.alive) {
var patch = { exporting: false };
if (sequence === that.sequence) patch.busy = false;
that.setData(patch);
}
});
},
onARStop: function (event) {
if (!this.acceptsCameraEvent(event)) return;
this.recordNativeEvent('onStop', event);
this.invalidateCalibration();
this.sequence++;
this.setData({ status: 'AR相机已停止。请重新开启后验证。', running: false, busy: false });
this.lastFrame = null; this.context = null; clearTimeout(this.frameTimer);
},
onError: function (event) {
if (!this.acceptsCameraEvent(event)) return;
var summary = this.recordNativeEvent('onError', event);
this.invalidateCalibration();
this.sequence++;
this.lastFrame = null; this.context = null; clearTimeout(this.frameTimer);
this.setData({ running: false, busy: false, status: 'AR 相机未通过真机验证。', errorText: '收到 AR 错误事件。' + (summary.metadata.join(';') || '未返回可安全展示的错误码,请复制诊断。') });
},
onStop: function () {
this.flushFrameDiagnostic();
this.invalidateCalibration();
this.sequence++; this.lastFrame = null; this.context = null; clearTimeout(this.frameTimer);
if (this.alive) this.setData({ running: false, busy: false, status: '已停止,帧缓存已清空。' });
},
onHide: function () {
this.visible = false; this.onStop();
if (this.alive) this.setData({ snapshotUrl: '', snapshotText: '', poseText: '', fieldText: '' });
},
onUnload: function () { this.onHide(); this.alive = false; }
});