// 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; } });