var Diagnostics = require('../../utils/ar-camera').diagnostics; var development = require('../../utils/development'); var PROBE_VERSION = 'CAM-NATIVE-20261102-04'; var KNOWN_FORMATS = ['RGBA', 'RGBA8888', 'RGB', 'RGB888', 'RGBX', 'BGRA', 'BGRA8888', 'BGR', 'BGR888', 'ARGB', 'ARGB8888', 'ABGR', 'ABGR8888', 'NV21', 'NV12', 'YUV420', 'YUV420P', 'YUV420SP', 'I420', 'YV12', 'JPEG', 'JPG', 'PNG']; function message(error, fallback) { var value = error && (error.errorMessage || error.errMsg || error.message); return typeof value === 'string' ? value.slice(0, 160) : fallback; } function positive(value) { return typeof value === 'number' && isFinite(value) && value > 0; } function frameMeta(frame) { frame = frame || {}; var data = frame.data, bytes = data && data.byteLength; var meta = { width: positive(frame.width) ? frame.width : 0, height: positive(frame.height) ? frame.height : 0, dataType: Object.prototype.toString.call(data), byteLength: positive(bytes) ? bytes : 0 }; if (typeof frame.timestamp === 'number' && isFinite(frame.timestamp)) meta.timestamp = frame.timestamp; if (typeof frame.pixelFormat === 'string') meta.pixelFormat = frame.pixelFormat.slice(0, 40); if (typeof frame.format === 'string') meta.format = frame.format.slice(0, 40); return meta; } Page({ data: { probeVersion: PROBE_VERSION, toolbarTop: 112, showAutoTest: false, diagnosticText: '', copyStatus: '', cameraEnabled: false, cameraSession: 0, devicePosition: 'front', nativeReady: false, cameraStatus: '正在打开普通 camera', contextStatus: '尚未创建上下文', frameStatus: '尚未监听', startStatus: '尚未调用', stopStatus: '尚未调用', frameCallbacks: 0, frameCount: 0, invalidFrameCount: 0, firstFrameMs: '', frameInfo: '', photoBusy: false, photoStatus: '仅在点击拍照时生成临时照片', photoPath: '', photoPosition: '' }, onLoad: function () { this.alive = true; this.visible = false; this.pageReady = false; this.generation = 0; this.photoOperation = null; this.diagnosticSystem = Diagnostics.system(my); this.copySequence = 0; this.setData({ toolbarTop: Diagnostics.toolbarTop(my), showAutoTest: development.nativeFaceProbeEnabled() }); this.contextCapabilities = { takePhoto: false, onCameraFrame: false, listenerStart: false, listenerStop: false }; this.listenerResults = { start: { state: 'not_called', code: 'unavailable' }, stop: { state: 'not_called', code: 'unavailable' } }; this.photoAttempted = false; this.photoResultSuccess = false; this.photoReturnedPath = false; }, onReady: function () { this.pageReady = true; this.mountCamera(); }, onShow: function () { this.visible = true; if (this.photoOperation && this.photoOperation.timedOut) this.setData({ photoBusy: true, photoStatus: '拍照10秒未返回;等待原操作结束,或退出此诊断页后重试' }); this.mountCamera(); }, onHide: function () { this.visible = false; this.disposeCamera(); this.setData({ cameraEnabled: false, photoBusy: !!this.photoOperation, cameraStatus: '页面已隐藏,相机已卸载' }); }, onUnload: function () { this.visible = false; this.alive = false; this.disposeCamera(); if (this.photoOperation) clearTimeout(this.photoOperation.timer); this.photoOperation = null; this.data.photoPath = ''; }, current: function (generation) { return this.alive && this.visible && this.data.cameraEnabled && this.generation === generation; }, diagnosticReport: function () { var caps = this.contextCapabilities || {}, results = this.listenerResults || {}, meta = this.lastDiagnosticFrame; var lines = [PROBE_VERSION].concat(this.diagnosticSystem || []); lines.push('requestedPosition=' + (this.data.devicePosition === 'front' || this.data.devicePosition === 'back' ? this.data.devicePosition : 'unknown')); lines.push('actualPosition=not_verified; requestedPosition does not identify the actual lens'); lines.push('context.takePhoto=' + !!caps.takePhoto + '; context.onCameraFrame=' + !!caps.onCameraFrame); lines.push('listener.start=' + !!caps.listenerStart + '; listener.stop=' + !!caps.listenerStop); lines.push('nativeReady=' + !!this.data.nativeReady + '; cameraMounted=' + !!this.data.cameraEnabled); lines.push('frameCallbacks=' + (this.frameCallbacks || 0) + '; rawFrames=' + (this.frameCount || 0) + '; invalidCallbacks=' + (this.invalidFrameCount || 0)); if (meta) lines.push('frame.width=' + meta.width + '; frame.height=' + meta.height + '; frame.byteLength=' + meta.byteLength + '; frame.pixelFormat=' + meta.pixelFormat + '; frame.format=' + meta.format); ['start', 'stop'].forEach(function (key) { var result = results[key] || { state: 'not_called', code: 'unavailable' }; lines.push('listener.' + key + '.state=' + result.state + '; listener.' + key + '.code=' + result.code); }); lines.push('photo.attempted=' + !!this.photoAttempted + '; photo.pending=' + !!this.photoOperation + '; photo.success=' + !!this.photoResultSuccess + '; photo.returnedPath=' + !!this.photoReturnedPath + '; photo.previewPresent=' + !!this.data.photoPath); return lines.join('\n'); }, onGenerateDiagnostics: function () { if (!this.alive) return; this.setData({ diagnosticText: this.diagnosticReport(), copyStatus: '脱敏诊断已生成,可长按下方文字选择复制。' }); }, onCopyDiagnostics: function () { if (!this.alive) return; var that = this, sequence = ++this.copySequence, report = this.diagnosticReport(); this.setData({ diagnosticText: report, copyStatus: '正在复制;也可长按下方脱敏文字选择复制。' }); function status(value) { if (that.alive && that.visible && sequence === that.copySequence) that.setData({ copyStatus: value }); } try { if (typeof my.setClipboard !== 'function') { status('当前宿主缺少剪贴板 API,请长按下方脱敏文字选择复制。'); return; } my.setClipboard({ text: report, success: function () { status('普通相机诊断已复制,仅含版本和相机状态。'); }, fail: function () { status('剪贴板复制失败,请长按下方脱敏文字选择复制。'); } }); } catch (_) { status('剪贴板复制失败,请长按下方脱敏文字选择复制。'); } }, currentEvent: function (event) { if (!this.alive || !this.visible || !this.data.cameraEnabled) return false; var target = event && event.currentTarget, session = target && target.dataset && target.dataset.cameraSession; var match = target && typeof target.id === 'string' && /^native-camera-(\d+)$/.exec(target.id); if (session === undefined && match) session = match[1]; return session === undefined || Number(session) === this.data.cameraSession; }, mountCamera: function () { if (!this.alive || !this.visible || !this.pageReady || this.data.cameraEnabled) return; var that = this, generation = ++this.generation, session = this.data.cameraSession + 1; this.frameCallbacks = 0; this.frameCount = 0; this.invalidFrameCount = 0; this.lastFramePaint = 0; this.lastDiagnosticFrame = null; this.contextCapabilities = { takePhoto: false, onCameraFrame: false, listenerStart: false, listenerStop: false }; this.listenerResults = { start: { state: 'not_called', code: 'unavailable' }, stop: { state: 'not_called', code: 'unavailable' } }; this.setData({ cameraEnabled: true, cameraSession: session, nativeReady: false, cameraStatus: '已请求' + (this.data.devicePosition === 'front' ? '前置' : '后置') + ',请观察真实取景', contextStatus: '正在创建上下文', frameStatus: '尚未监听', startStatus: '尚未调用', stopStatus: '尚未调用', frameCallbacks: 0, frameCount: 0, invalidFrameCount: 0, firstFrameMs: '', frameInfo: '', photoBusy: !!this.photoOperation }, function () { if (!that.current(generation) || session !== that.data.cameraSession) return; if (typeof my.createCameraContext !== 'function') { that.setData({ contextStatus: '缺少 my.createCameraContext', frameStatus: '无法监听' }); return; } try { that.cameraContext = my.createCameraContext('native-camera-' + session); if (!that.cameraContext) throw new Error('未返回 CameraContext'); var methods = Diagnostics.methods(that.cameraContext); that.contextCapabilities.takePhoto = methods.indexOf('takePhoto') >= 0; that.contextCapabilities.onCameraFrame = methods.indexOf('onCameraFrame') >= 0; that.setData({ contextStatus: 'takePhoto: ' + (typeof that.cameraContext.takePhoto === 'function') + ';onCameraFrame: ' + (typeof that.cameraContext.onCameraFrame === 'function') }); that.startFrames(generation); } catch (error) { if (that.current(generation)) that.setData({ contextStatus: message(error, '创建相机上下文失败'), frameStatus: '无法监听' }); } }); }, onNativeReady: function (event) { if (this.currentEvent(event)) this.setData({ nativeReady: true }); }, startFrames: function (generation) { if (!this.current(generation)) return; var that = this, context = this.cameraContext; if (!context || typeof context.onCameraFrame !== 'function') { this.listenerResults.start = { state: 'missing_method', code: 'unavailable' }; this.setData({ frameStatus: '当前上下文缺少 onCameraFrame,不能取得实时帧' }); return; } this.listenerResults.start = { state: 'pending', code: 'unavailable' }; this.framesActive = true; this.framesStartedAt = Date.now(); this.setData({ frameStatus: '等待真实帧;start 成功也不代表收到帧', startStatus: '正在调用' }); this.frameTimer3 = setTimeout(function () { if (that.current(generation) && that.framesActive && !that.frameCount) that.setData({ frameStatus: '3秒未收到可用原始帧,继续等到10秒(回调 ' + that.frameCallbacks + ' 次)' }); }, 3000); this.frameTimer10 = setTimeout(function () { if (!that.current(generation) || !that.framesActive || that.frameCount) return; that.setData({ frameStatus: '10秒仍未收到可用原始帧,本轮已停止;不能判定宿主支持', frameCallbacks: that.frameCallbacks, invalidFrameCount: that.invalidFrameCount }); that.stopFrames(false); }, 10000); try { this.frameListener = context.onCameraFrame(function (frame) { that.receiveFrame(generation, frame); }); var methods = Diagnostics.methods(this.frameListener); this.contextCapabilities.listenerStart = methods.indexOf('start') >= 0; this.contextCapabilities.listenerStop = methods.indexOf('stop') >= 0; if (!this.frameListener || typeof this.frameListener.start !== 'function' || typeof this.frameListener.stop !== 'function') throw new Error('未返回具备 start/stop 的 CameraFrameListener'); this.frameListener.start({ success: function () { if (that.current(generation) && that.framesActive) { that.listenerResults.start = { state: 'success', code: 0 }; that.setData({ startStatus: '调用成功(需另看真实帧计数)' }); } }, fail: function (error) { if (!that.current(generation) || !that.framesActive) return; that.listenerResults.start = { state: 'failure', code: diagnosticCode(error) }; that.setData({ startStatus: message(error, '监听启动失败'), frameStatus: '监听启动失败' }); that.stopFrames(false); } }); } catch (error) { if (!this.current(generation)) return; this.listenerResults.start = { state: 'throw', code: diagnosticCode(error) }; this.setData({ startStatus: message(error, '监听接口调用失败'), frameStatus: '监听接口调用失败' }); this.stopFrames(false); } }, receiveFrame: function (generation, frame) { if (!this.current(generation) || !this.framesActive) return; this.frameCallbacks++; var meta = frameMeta(frame), valid = meta.width > 0 && meta.height > 0 && meta.byteLength > 0; this.lastDiagnosticFrame = { width: meta.width, height: meta.height, byteLength: meta.byteLength, pixelFormat: diagnosticFormat(meta.pixelFormat), format: diagnosticFormat(meta.format) }; var firstValid = valid && !this.frameCount; if (valid) { this.frameCount++; if (this.frameCount === 1) { this.clearFrameTimers(); this.setData({ firstFrameMs: Math.max(0, Date.now() - this.framesStartedAt) + 'ms' }); } } else this.invalidFrameCount++; // Keep no frame/pixel buffer; publish scalar metadata at most ~3 times/s. if (this.frameCallbacks === 1 || firstValid || Date.now() - this.lastFramePaint >= 350) { this.lastFramePaint = Date.now(); this.setData({ frameCallbacks: this.frameCallbacks, frameCount: this.frameCount, invalidFrameCount: this.invalidFrameCount, frameInfo: JSON.stringify(meta), frameStatus: this.frameCount ? '已收到原始帧;像素格式和人脸识别能力仍需另验' : '收到回调,但尺寸或原始字节缺失,未计为可用帧' }); } }, clearFrameTimers: function () { clearTimeout(this.frameTimer3); clearTimeout(this.frameTimer10); this.frameTimer3 = null; this.frameTimer10 = null; }, stopFrames: function (showStatus) { this.framesActive = false; this.clearFrameTimers(); var listener = this.frameListener; this.frameListener = null; var that = this, generation = this.generation; if (showStatus && this.current(generation)) this.setData({ frameStatus: '已主动停止监听', frameCallbacks: this.frameCallbacks, frameCount: this.frameCount, invalidFrameCount: this.invalidFrameCount }); if (!listener || typeof listener.stop !== 'function') return; this.listenerResults.stop = { state: 'pending', code: 'unavailable' }; if (this.current(generation)) this.setData({ stopStatus: '正在调用' }); try { listener.stop({ success: function () { if (that.current(generation)) { that.listenerResults.stop = { state: 'success', code: 0 }; that.setData({ stopStatus: '调用成功' }); } }, fail: function (error) { if (that.current(generation)) { that.listenerResults.stop = { state: 'failure', code: diagnosticCode(error) }; that.setData({ stopStatus: message(error, '停止监听失败') }); } } }); } catch (error) { if (this.current(generation)) { this.listenerResults.stop = { state: 'throw', code: diagnosticCode(error) }; this.setData({ stopStatus: message(error, '停止监听异常') }); } } }, onStopFrames: function () { this.stopFrames(true); }, disposeCamera: function () { this.generation++; this.stopFrames(false); this.cameraContext = null; }, remount: function (position) { if (!this.alive || !this.visible) return; if (this.photoOperation) { this.setData({ photoStatus: '上次拍照尚未结束,请等待;若持续无响应,请退出此诊断页再进入' }); return; } this.disposeCamera(); var that = this; this.setData({ cameraEnabled: false, devicePosition: position }, function () { that.mountCamera(); }); }, onFlip: function () { this.remount(this.data.devicePosition === 'front' ? 'back' : 'front'); }, onRetryCamera: function () { this.remount(this.data.devicePosition); }, onNativeError: function (event) { if (!this.currentEvent(event)) return; this.disposeCamera(); this.setData({ cameraEnabled: false, cameraStatus: message(event && event.detail, '相机组件报错,请检查系统相机权限'), frameStatus: '相机异常,监听已停止' }); }, onNativeStop: function (event) { if (!this.currentEvent(event)) return; this.disposeCamera(); this.setData({ cameraEnabled: false, cameraStatus: '相机组件已异常停止,可点重新打开', frameStatus: '监听已停止' }); }, onTakePhoto: function () { if (!this.alive || !this.visible || !this.data.cameraEnabled || this.photoOperation) return; var context = this.cameraContext, that = this; if (!context || typeof context.takePhoto !== 'function') { this.setData({ photoStatus: '当前上下文缺少 takePhoto' }); return; } var operation = { generation: this.generation, position: this.data.devicePosition, done: false }; this.photoAttempted = true; this.photoResultSuccess = false; this.photoReturnedPath = false; this.photoOperation = operation; this.setData({ photoBusy: true, photoStatus: '正在拍照;仅在本页预览' }); operation.timer = setTimeout(function () { // A timeout cannot cancel native takePhoto. Keep the lock until its callback // or page destruction, so a late native export cannot overlap a retry. if (operation.done || that.photoOperation !== operation) return; operation.timedOut = true; if (that.alive && that.visible) that.setData({ photoStatus: '拍照10秒未返回;等待原操作结束,或退出此诊断页后重试' }); }, 10000); try { context.takePhoto({ quality: 'high', success: function (result) { that.finishPhoto(operation, 'success', result); }, fail: function (error) { that.finishPhoto(operation, 'fail', error); }, complete: function () { that.finishPhoto(operation, 'complete'); } }); } catch (error) { this.finishPhoto(operation, 'fail', error); } }, finishPhoto: function (operation, kind, result) { if (operation.done || this.photoOperation !== operation) return; operation.done = true; clearTimeout(operation.timer); this.photoOperation = null; if (!this.alive || !this.visible) return; this.setData({ photoBusy: false }); if (!this.current(operation.generation)) return; var file = result && (result.tempImagePath || result.tempFilePath || result.apFilePath); this.photoReturnedPath = typeof file === 'string' && !!file; this.photoResultSuccess = kind === 'success' && this.photoReturnedPath; if (kind === 'success' && typeof file === 'string' && file) this.setData({ photoPath: file, photoPosition: operation.position, photoStatus: '已取得临时照片,请观察照片是否来自所请求镜头' }); else this.setData({ photoStatus: message(result, kind === 'success' ? '拍照成功回调未返回可预览的临时路径' : '拍照未成功返回照片') }); }, onOpenAutoCapture: function () { if (!development.nativeFaceProbeEnabled() || this.photoOperation || this.data.photoBusy) return; my.redirectTo({ url: '/pages/camera/camera?debug=local-face' }); }, onOpenARProbe: function () { if (this.photoOperation || this.data.photoBusy) return; my.redirectTo({ url: '/pages/camtest/camtest' }); }, onClearPhoto: function () { this.setData({ photoPath: '', photoPosition: '', photoStatus: '本页照片预览已清除' }); }, onPreviewError: function () { if (this.alive && this.visible) this.setData({ photoStatus: '已收到拍照路径,但本页图片预览加载失败' }); } }); function diagnosticFormat(value) { return typeof value !== 'string' ? 'not_reported' : (KNOWN_FORMATS.indexOf(value.toUpperCase()) >= 0 ? value.toUpperCase() : 'unrecognized'); } function diagnosticCode(error) { if (!error || typeof error !== 'object') return 'unavailable'; var keys = ['error', 'errorCode', 'code']; for (var index = 0; index < keys.length; index++) { var descriptor; try { descriptor = Object.getOwnPropertyDescriptor(error, keys[index]); } catch (_) { continue; } if (!descriptor || !Object.prototype.hasOwnProperty.call(descriptor, 'value')) continue; var value = descriptor.value; if (typeof value === 'number' && isFinite(value) && Math.floor(value) === value && Math.abs(value) <= 999999999) return value; if (typeof value === 'string' && /^-?\d{1,9}$/.test(value)) return Number(value); } return 'unavailable'; }