947 lines
65 KiB
JavaScript
947 lines
65 KiB
JavaScript
var api = require('../../utils/api'), image = require('../../utils/image'), voice = require('../../utils/voice');
|
|
var permissions = require('../../utils/permissions');
|
|
var development = require('../../utils/development');
|
|
var config = require('../../config');
|
|
var arCamera = require('../../utils/ar-camera');
|
|
var nativeQuality = require('../../utils/native-face-quality');
|
|
var CAPTURE_PROBE_VERSION = 'LOCAL-FACE-20261102-09';
|
|
var GPU_STAGES = ['uninitialized', 'create_canvas', 'get_context', 'native_preflight', 'register_backend', 'matmul', 'conv2d', 'readback', 'warmup', 'ready', 'context_lost', 'disposed'];
|
|
var GPU_CODES = ['NONE', 'API_MISSING', 'CANVAS_FAILED', 'CONTEXT_FAILED', 'INSTRUMENTATION_FAILED', 'PREFLIGHT_FAILED', 'DRAW_UNSUPPORTED', 'BACKEND_FAILED', 'MATMUL_FAILED', 'CONV_FAILED', 'READBACK_FAILED', 'CONTEXT_LOST', 'WARMUP_FAILED', 'DISPOSED'];
|
|
var GPU_SYNTHETIC_FIELDS = ['actualLength', 'nonFiniteCount', 'mismatchCount', 'maxAbsError', 'firstExpected', 'firstActual', 'expectedChecksum', 'actualChecksum'];
|
|
var NATIVE_GPU_STAGES = ['not_run', 'clear', 'constant_arrays', 'constant_indexed', 'constant_quad', 'shader', 'byte_texture', 'float_texture', 'passed'];
|
|
var NATIVE_GPU_CODES = ['NONE', 'READ_UNCHANGED', 'VALUE_MISMATCH', 'GL_ERROR', 'FRAMEBUFFER_INCOMPLETE', 'SHADER_COMPILE_FAILED', 'PROGRAM_LINK_FAILED', 'API_MISSING', 'API_FAILED'];
|
|
function nativeFrameSize(value) { return ['small', 'medium', 'large'].indexOf(value) >= 0 ? value : 'medium'; }
|
|
function nativeEngineOptions() {
|
|
var input = config.nativeFaceInputSize, side = config.nativeFaceMaxSide;
|
|
return { backend: config.nativeFaceBackend === 'webgl' ? 'webgl' : 'cpu', platform: my, defaultPixelFormat: config.nativePixelFormat || 'RGBA',
|
|
inputSize: typeof input === 'number' && input >= 128 && input <= 320 && input % 32 === 0 ? input : 256,
|
|
maxSide: typeof side === 'number' && side >= 128 && side <= 320 && Math.floor(side) === side ? side : 320,
|
|
landmarkModel: config.nativeFaceLandmarkModel === 'tiny' ? 'tiny' : 'full', scoreThreshold: nativeQuality.scoreThreshold('front') };
|
|
}
|
|
function probeValue(object, key) {
|
|
try { var descriptor = object && Object.getOwnPropertyDescriptor(object, key); return descriptor && Object.prototype.hasOwnProperty.call(descriptor, 'value') ? descriptor.value : undefined; } catch (_) {}
|
|
}
|
|
function probeNumber(value) { return typeof value === 'number' && isFinite(value) && value >= 0 ? value : 0; }
|
|
function probeEngineMetadata(info, previous) {
|
|
previous = previous || {};
|
|
var backend = probeValue(info, 'backend'), model = probeValue(info, 'landmarkModel'), optimization = probeValue(info, 'cpuOptimization');
|
|
function size(key) { var value = probeValue(info, key); return typeof value === 'number' && isFinite(value) && value > 0 && Math.floor(value) === value ? value : previous[key] || 'not_reported'; }
|
|
return { backend: ['cpu', 'webgl', 'wasm'].indexOf(backend) >= 0 ? backend : previous.backend || 'not_reported',
|
|
inputSize: size('inputSize'), maxSide: size('maxSide'), landmarkModel: ['tiny', 'full'].indexOf(model) >= 0 ? model : previous.landmarkModel || 'not_reported',
|
|
cpuOptimization: optimization === undefined ? previous.cpuOptimization || 'not_reported' : ['pad-copy', 'none'].indexOf(optimization) >= 0 ? optimization : 'not_reported' };
|
|
}
|
|
function probeTimingMetadata(info) {
|
|
var timings = probeValue(info, 'timings'), safe = {};
|
|
['snapshotMs', 'modelMs', 'tensorMs', 'detectorMs', 'landmarkMs', 'totalMs'].forEach(function (key) {
|
|
var value = probeValue(timings, key); safe[key] = typeof value === 'number' && isFinite(value) && value >= 0 ? value : 'not_reported';
|
|
});
|
|
return safe;
|
|
}
|
|
function probeSyntheticMetadata(info, previous) {
|
|
var safe = {};
|
|
GPU_SYNTHETIC_FIELDS.forEach(function (key) {
|
|
var value = probeValue(info, key);
|
|
if (value === undefined && previous) value = previous[key];
|
|
var valid = typeof value === 'number' && isFinite(value);
|
|
if (['actualLength', 'nonFiniteCount', 'mismatchCount'].indexOf(key) >= 0) valid = valid && value >= 0 && Math.floor(value) === value;
|
|
if (key === 'maxAbsError') valid = valid && value >= 0;
|
|
safe[key] = value === null ? null : valid ? value : 'not_reported';
|
|
});
|
|
return safe;
|
|
}
|
|
function probeNativeGpuMetadata(info, previous) {
|
|
previous = previous || {};
|
|
var safe = {};
|
|
[['stage', NATIVE_GPU_STAGES], ['code', NATIVE_GPU_CODES],
|
|
['readReturn', ['not_run', 'undefined', 'arraybuffer', 'typedarray', 'promise', 'other']],
|
|
['clear', ['not_run', 'passed']], ['constantArrays', ['not_run', 'passed']], ['constantIndexed', ['not_run', 'passed']], ['constantQuad', ['not_run', 'passed']],
|
|
['shader', ['not_run', 'passed']], ['byteTexture', ['not_run', 'passed']], ['floatTexture', ['not_run', 'passed']]
|
|
].forEach(function (entry) {
|
|
var value = probeValue(info, entry[0]);
|
|
if (value === undefined) value = previous[entry[0]];
|
|
safe[entry[0]] = entry[1].indexOf(value) >= 0 ? value : 'not_reported';
|
|
});
|
|
['changedBytes', 'mismatchCount', 'expectedChecksum', 'actualChecksum', 'glError', 'framebufferStatus', 'vertexBufferBytes', 'indexBufferBytes', 'pixel0', 'pixel1', 'pixel2', 'pixel3'].forEach(function (key) {
|
|
var value = probeValue(info, key);
|
|
if (value === undefined) value = previous[key];
|
|
var valid = typeof value === 'number' && isFinite(value) && value >= 0 && Math.floor(value) === value;
|
|
if (/^pixel[0-3]$/.test(key)) valid = valid && value <= 4294967295;
|
|
safe[key] = valid ? value : 'not_reported';
|
|
});
|
|
return safe;
|
|
}
|
|
function probeGpuMetadata(info, previous, includeSynthetic) {
|
|
previous = previous || {};
|
|
var safe = {}, requested = probeValue(info, 'requested'), lost = probeValue(info, 'contextLost');
|
|
safe.requested = typeof requested === 'boolean' ? requested : previous.requested === true;
|
|
safe.contextLost = typeof lost === 'boolean' ? lost : previous.contextLost === true;
|
|
[['selftest', ['not_run', 'running', 'passed', 'failed']], ['stage', GPU_STAGES], ['code', GPU_CODES]].forEach(function (entry) {
|
|
var value = probeValue(info, entry[0]);
|
|
safe[entry[0]] = entry[1].indexOf(value) >= 0 ? value : value === undefined && previous[entry[0]] ? previous[entry[0]] : 'not_reported';
|
|
});
|
|
// These values belong exclusively to fixed, synthetic startup selftests.
|
|
// Never refresh them from a model detection, camera frame or later stats.
|
|
['matmulDraws', 'convDraws', 'pixelReads', 'floatReads', 'byteReads', 'selftestMs', 'warmupMs', 'glError', 'framebufferStatus', 'quadStripDraws'].forEach(function (key) {
|
|
var value = includeSynthetic ? probeValue(info, key) : undefined;
|
|
if (value === undefined) value = previous[key];
|
|
var valid = typeof value === 'number' && isFinite(value) && value >= 0;
|
|
if (key !== 'selftestMs' && key !== 'warmupMs') valid = valid && Math.floor(value) === value;
|
|
safe[key] = valid ? value : 'not_reported';
|
|
});
|
|
var profile = includeSynthetic ? probeValue(info, 'profile') : undefined;
|
|
var attempts = includeSynthetic ? probeValue(info, 'attemptCount') : undefined;
|
|
if (profile === undefined) profile = previous.profile;
|
|
if (attempts === undefined) attempts = previous.attemptCount;
|
|
safe.profile = ['default', 'packed_byte', 'unpacked_byte'].indexOf(profile) >= 0 ? profile : 'not_reported';
|
|
safe.attemptCount = typeof attempts === 'number' && Math.floor(attempts) === attempts && attempts >= 1 && attempts <= 3 ? attempts : 'not_reported';
|
|
var nativeStrategy = includeSynthetic ? probeValue(info, 'nativeStrategy') : undefined;
|
|
var nativeAttempts = includeSynthetic ? probeValue(info, 'nativeAttempts') : undefined;
|
|
if (nativeStrategy === undefined) nativeStrategy = previous.nativeStrategy;
|
|
if (nativeAttempts === undefined) nativeAttempts = previous.nativeAttempts;
|
|
safe.nativeStrategy = ['standard', 'buffer_copy', 'framebuffer_y', 'buffer_copy_framebuffer_y'].indexOf(nativeStrategy) >= 0 ? nativeStrategy : 'not_reported';
|
|
safe.nativeAttempts = typeof nativeAttempts === 'number' && Math.floor(nativeAttempts) === nativeAttempts && nativeAttempts >= 1 && nativeAttempts <= 8 ? nativeAttempts : 'not_reported';
|
|
var drawStrategy = includeSynthetic ? probeValue(info, 'drawStrategy') : undefined;
|
|
if (drawStrategy === undefined) drawStrategy = previous.drawStrategy;
|
|
safe.drawStrategy = ['indexed', 'quad_strip'].indexOf(drawStrategy) >= 0 ? drawStrategy : 'not_reported';
|
|
[['nativeInitialStage', NATIVE_GPU_STAGES], ['nativeInitialCode', NATIVE_GPU_CODES]].forEach(function (entry) {
|
|
var value = includeSynthetic ? probeValue(info, entry[0]) : undefined;
|
|
if (value === undefined) value = previous[entry[0]];
|
|
safe[entry[0]] = entry[1].indexOf(value) >= 0 ? value : 'not_reported';
|
|
});
|
|
safe.matmul = probeSyntheticMetadata(includeSynthetic ? probeValue(info, 'matmul') : undefined, previous.matmul);
|
|
safe.conv = probeSyntheticMetadata(includeSynthetic ? probeValue(info, 'conv') : undefined, previous.conv);
|
|
safe.nativeProbe = probeNativeGpuMetadata(includeSynthetic ? probeValue(info, 'nativeProbe') : undefined, previous.nativeProbe);
|
|
return safe;
|
|
}
|
|
function isGPUFailure(error) {
|
|
var code = probeValue(error, 'code');
|
|
return typeof code === 'string' && (code.indexOf('NATIVE_FACE_WEBGL_') === 0 || code === 'NATIVE_FACE_BACKEND_CONFLICT');
|
|
}
|
|
function probeRawMetadata(frame) {
|
|
var data = probeValue(frame, 'data'), length = 0;
|
|
try {
|
|
if (ArrayBuffer.isView(data)) length = data.byteLength;
|
|
else if (data) length = Object.getOwnPropertyDescriptor(ArrayBuffer.prototype, 'byteLength').get.call(data);
|
|
} catch (_) {}
|
|
var format = probeValue(frame, 'pixelFormat') || probeValue(frame, 'format'), rotation = probeValue(frame, 'rotation');
|
|
return { width: probeNumber(probeValue(frame, 'width')), height: probeNumber(probeValue(frame, 'height')), byteLength: probeNumber(length),
|
|
format: typeof format === 'string' && /^(RGBA|BGRA|ARGB|ABGR)(8888)?$/i.test(format) ? format.toUpperCase() : format === undefined ? 'not_reported' : 'unrecognized',
|
|
rotation: typeof rotation === 'number' && isFinite(rotation) && Math.abs(rotation) <= 360 ? rotation : 'not_reported' };
|
|
}
|
|
function probeCode(error) {
|
|
var code = probeValue(error, 'code');
|
|
if (code === 'none') return 'none';
|
|
if (typeof code === 'string' && code.indexOf('NATIVE_FACE_WEBGL_') === 0 && GPU_CODES.indexOf(code.slice(18)) >= 0) return code;
|
|
return ['NATIVE_FACE_BACKEND', 'NATIVE_FACE_BACKEND_CONFLICT', 'NATIVE_FACE_INIT_TIMEOUT', 'NATIVE_FACE_FRAME', 'NATIVE_FACE_FORMAT', 'NATIVE_FACE_STRIDE', 'NATIVE_FACE_ROTATION', 'NATIVE_FACE_DISPOSED', 'NATIVE_FACE_BUSY'].indexOf(code) >= 0 ? code : 'unavailable';
|
|
}
|
|
function probeCapability(name) {
|
|
try { return typeof my.canIUse !== 'function' ? 'unavailable' : my.canIUse(name) ? 'yes' : 'no'; } catch (_) { return 'error'; }
|
|
}
|
|
// Keep the logical lens used by the guide separate from the AR compatibility
|
|
// request. This mapping is a host setting, not a claim about actual hardware.
|
|
function arDevicePosition(logicalPosition) {
|
|
var front = config.arFrontPosition === 'back' ? 'back' : 'front';
|
|
return logicalPosition === 'back' ? (front === 'front' ? 'back' : 'front') : front;
|
|
}
|
|
var STEPS = [
|
|
{ target: 'front', tip: '请正脸出镜', turn: '正脸居中', mask: 0, done: '正脸拍摄完成' },
|
|
{ target: 'right', tip: '请右脸出镜', turn: '右脸居中', mask: 2, done: '右脸拍摄完成', next: '下一张:右脸出镜\n请向左转头' },
|
|
{ target: 'left', tip: '请左脸出镜', turn: '左脸居中', mask: 1, done: '左脸拍摄完成', next: '下一张:左脸出镜\n请向右转头' }
|
|
];
|
|
Page(require('../../utils/page-header').page({
|
|
onShareAppMessage: function () { return require('../../utils/share').message(); },
|
|
data: { stepIndex: 0, stepText: '1/3', stepTip: STEPS[0].tip, guides: ['正脸居中', '取下眼镜', '光线充足'],
|
|
maskStep: 0, maskName: 'front', maskInsetX: 0, maskInsetY: 0,
|
|
maskStageStyle: 'width:375px;height:726px;', scanStageStyle: 'width:375px;height:726px;',
|
|
devicePosition: 'front', arDevicePosition: 'front', cameraSession: 0, camError: false, previewUrl: '', doneTip: '', nextTip: '', analyzing: false,
|
|
countdown: 0, fillLight: false, analysisStage: '', analysisError: '', scanError: false,
|
|
useAR: config.captureMode === 'ar', useNativeFace: config.captureMode === 'native-face',
|
|
autoCapture: config.captureMode === 'ar' || config.captureMode === 'native-face', nativeCameraId: 'skin-camera', nativeFrameSize: nativeFrameSize(config.nativeFrameSize), liveHint: '请将脸部移入取景框', liveGood: false,
|
|
identityPending: false, identityError: '',
|
|
compressWidth: 1, compressHeight: 1, cameraEnabled: false, permissionMessage: '',
|
|
debugMode: false, debugComplete: false, debugPhotos: [], uploadOnly: false,
|
|
localFaceProbe: false, captureProbeVersion: CAPTURE_PROBE_VERSION, probeToolbarTop: 112,
|
|
showDiagnose: config.environment !== 'production' && config.cameraDebug === true, diagText: '', cameraSupported: false },
|
|
onLoad: function (query) {
|
|
this.alive = true; this.visible = true; this.photos = []; this.busy = false; this.seq = 0;
|
|
this.layout();
|
|
var supported = config.nativeCamera === true && (this.data.useAR
|
|
? typeof my.createArCameraContext === 'function'
|
|
: (typeof permissions.cameraSupported === 'function' ? permissions.cameraSupported() : true));
|
|
this.cameraSupported = supported;
|
|
this.debugKind = query && query.debug === 'local-face' && typeof development.nativeFaceProbeEnabled === 'function' && development.nativeFaceProbeEnabled() ? 'local-face'
|
|
: query && query.debug === 'camera' && development.cameraDebugEnabled() ? 'camera' : '';
|
|
this.debugMode = !!this.debugKind; this.localFaceProbe = this.debugKind === 'local-face';
|
|
var uploadOnly = config.photoInputMode === 'upload' && !this.debugMode;
|
|
if (uploadOnly) { supported = false; this.cameraSupported = false;
|
|
this.setData({ uploadOnly: true, autoCapture: false, useAR: false, useNativeFace: false }); }
|
|
this.probeFramesProcessed = 0; this.probeAutoCaptured = 0; this.probeStage = 'initializing'; this.probeFailureStage = 'none'; this.probeFailureCode = 'none';
|
|
this.nativeFrameCallbacks = 0; this.nativeFramesSkippedInFlight = 0; this.nativeAcceptedCalls = 0;
|
|
if (this.localFaceProbe) this.probeRuntime = arCamera.diagnostics.system(my);
|
|
this.setData({ debugMode: this.debugMode, localFaceProbe: this.localFaceProbe, probeToolbarTop: arCamera.diagnostics.toolbarTop(my),
|
|
cameraSupported: supported, devicePosition: 'front', arDevicePosition: arDevicePosition('front') });
|
|
if (this.debugMode) {
|
|
if (!supported) {
|
|
this.probeStage = 'failed'; this.probeFailureStage = 'listener';
|
|
this.setData({ camError: true, liveHint: '实时相机能力检查未通过', permissionMessage: '当前运行时未通过相机能力检查,请复制识别诊断;尚未完成本地拍摄验收。' });
|
|
}
|
|
return;
|
|
}
|
|
this.ownerId = api.user() && api.user().id;
|
|
var app = typeof getApp === 'function' ? getApp() : null;
|
|
if (!api.user() || !app || !app.globalData.profile) { this.loadBlocked = true; my.redirectTo({ url: '/pages/splash/splash' }); return; }
|
|
if (!supported && !uploadOnly) {
|
|
// 淘宝 <camera> 组件不可用:直接用系统相机/相册,页面不再挂相机组件(否则整页崩溃)
|
|
this.setData({ cameraEnabled: false, camError: true,
|
|
permissionMessage: this.data.autoCapture ? '当前设备未提供实时识别相机,请打开相机测试检查;尚未完成自动拍摄。' : '请用下方按钮拍摄,三张依次为正脸、右脸、左脸。' });
|
|
}
|
|
},
|
|
onReady: function () {
|
|
this.pageReady = true;
|
|
if (this.loadBlocked) return;
|
|
this.layout();
|
|
if (this.cameraAuthorized) this.mountCamera();
|
|
},
|
|
onShow: function () {
|
|
this.visible = true;
|
|
if (this.loadBlocked) return;
|
|
var that = this;
|
|
if (!this.debugMode && typeof api.restore === 'function') {
|
|
this.setData({ identityPending: true, identityError: '' });
|
|
api.restore().then(function () {
|
|
if (!that.alive || !that.visible) return;
|
|
that.setData({ identityPending: false }); that.resumeVisible();
|
|
}).catch(function (err) {
|
|
if (!that.alive || !that.visible) return;
|
|
if (!api.user()) { that.onCancel(); return; }
|
|
that.setData({ identityError: err.message || '暂时无法确认登录状态,请重试' });
|
|
});
|
|
return;
|
|
}
|
|
this.resumeVisible();
|
|
},
|
|
onRetryIdentity: function () { this.onShow(); },
|
|
debugGateEnabled: function () {
|
|
return this.debugKind === 'local-face' ? typeof development.nativeFaceProbeEnabled === 'function' && development.nativeFaceProbeEnabled()
|
|
: this.debugKind === 'camera' && development.cameraDebugEnabled();
|
|
},
|
|
onCopyCaptureDiagnostics: function () {
|
|
if (!this.alive || !this.localFaceProbe || !this.debugGateEnabled()) return;
|
|
this.readNativeEngineDiagnostics(this.nativeEngineRecord);
|
|
var last = this.probeDetection || {}, meta = this.probeFrame || {}, verdict = this.latestNativeVerdict;
|
|
var engine = this.probeEngine || probeEngineMetadata(), timings = this.probeTimings || probeTimingMetadata(), requested = nativeEngineOptions();
|
|
var gpu = this.probeGPU || probeGpuMetadata({ requested: requested.backend === 'webgl' });
|
|
var stage = ['initializing', 'listening', 'detecting', 'capturing', 'complete', 'failed'].indexOf(this.probeStage) >= 0 ? this.probeStage : 'initializing';
|
|
var failureStage = ['init', 'listener', 'frame', 'photo', 'performance', 'none'].indexOf(this.probeFailureStage) >= 0 ? this.probeFailureStage : 'none';
|
|
var reason = ['ok', 'frame', 'angle', 'quality', 'unavailable'].indexOf(last.reason) >= 0 ? last.reason : 'unavailable';
|
|
var format = typeof meta.format === 'string' && /^(RGBA|BGRA|ARGB|ABGR)(8888)?$/.test(meta.format) ? meta.format : meta.format === 'unrecognized' ? 'unrecognized' : 'not_reported';
|
|
var rotation = typeof meta.rotation === 'number' && isFinite(meta.rotation) && Math.abs(meta.rotation) <= 360 ? meta.rotation : 'not_reported';
|
|
var lines = [CAPTURE_PROBE_VERSION].concat(this.probeRuntime || []).concat(['mode=local-face; captureMode=native-face',
|
|
'frameSize.requested=' + nativeFrameSize(this.data.nativeFrameSize),
|
|
'engine.requested.backend=' + requested.backend,
|
|
'engine.requested.inputSize=' + requested.inputSize + '; maxSide=' + requested.maxSide + '; landmarkModel=' + requested.landmarkModel,
|
|
'engine.actual.backend=' + engine.backend + '; inputSize=' + engine.inputSize + '; maxSide=' + engine.maxSide + '; landmarkModel=' + engine.landmarkModel + '; cpuOptimization=' + engine.cpuOptimization,
|
|
'stage=' + stage + '; failureStage=' + failureStage,
|
|
'typeof.createCameraContext=' + typeof my.createCameraContext,
|
|
'typeof.createOffscreenCanvas=' + typeof my.createOffscreenCanvas + '; typeof.createCanvas=' + typeof my.createCanvas + '; typeof.createWorker=' + typeof my.createWorker,
|
|
'canIUse.camera=' + probeCapability('camera') + '; canIUse.createCameraContext=' + probeCapability('createCameraContext'),
|
|
'logicalPosition=' + (this.data.devicePosition === 'back' ? 'back' : 'front') + '; step=' + (this.data.stepIndex + 1),
|
|
'framesProcessed=' + probeNumber(this.probeFramesProcessed) + '; inflight=' + !!this.nativeInference,
|
|
'frameEvents.total=' + probeNumber(this.nativeFrameCallbacks) + '; skippedInFlight=' + probeNumber(this.nativeFramesSkippedInFlight) + '; acceptedCalls=' + probeNumber(this.nativeAcceptedCalls),
|
|
'currentDetection.elapsedMs=' + probeNumber(last.elapsedMs) + '; faceCount=' + probeNumber(last.faceCount),
|
|
'currentDetection.sourceAgeMs=' + probeNumber(last.sourceAgeMs),
|
|
'quality.reason=' + reason + '; quality.code=' + probeCode({ code: last.code || this.probeFailureCode || 'none' }) + '; validFresh=' + !!(verdict && verdict.ok && Date.now() - this.latestNativeAt <= 1500),
|
|
'slowResults=' + probeNumber(this.nativeSlowResults) + '; waiting=' + !!this.nativeCaptureWait,
|
|
'autoCapturedPhotoCount=' + probeNumber(this.probeAutoCaptured) + '; done=' + !!this.data.debugComplete,
|
|
'frame.width=' + probeNumber(meta.width) + '; frame.height=' + probeNumber(meta.height) + '; frame.byteLength=' + probeNumber(meta.byteLength) + '; frame.format=' + format + '; frame.rotation=' + rotation]);
|
|
lines.push('gpu.requested=' + gpu.requested + '; selftest=' + gpu.selftest + '; stage=' + gpu.stage + '; code=' + gpu.code + '; contextLost=' + gpu.contextLost);
|
|
lines.push('gpu.matmulDraws=' + gpu.matmulDraws + '; convDraws=' + gpu.convDraws + '; pixelReads=' + gpu.pixelReads + '; selftestMs=' + gpu.selftestMs + '; warmupMs=' + gpu.warmupMs);
|
|
lines.push('gpu.profile=' + gpu.profile + '; attemptCount=' + gpu.attemptCount);
|
|
lines.push('gpu.nativeStrategy=' + gpu.nativeStrategy + '; nativeAttempts=' + gpu.nativeAttempts);
|
|
lines.push('gpu.drawStrategy=' + gpu.drawStrategy + '; quadStripDraws=' + gpu.quadStripDraws);
|
|
lines.push('gpu.nativeInitialStage=' + gpu.nativeInitialStage + '; nativeInitialCode=' + gpu.nativeInitialCode);
|
|
lines.push('gpu.synthetic.floatReads=' + gpu.floatReads + '; byteReads=' + gpu.byteReads);
|
|
lines.push('gpu.synthetic.glError=' + gpu.glError + '; framebufferStatus=' + gpu.framebufferStatus);
|
|
var nativeProbe = gpu.nativeProbe;
|
|
lines.push('gpu.native.stage=' + nativeProbe.stage + '; code=' + nativeProbe.code + '; clear=' + nativeProbe.clear + '; shader=' + nativeProbe.shader + '; byteTexture=' + nativeProbe.byteTexture + '; floatTexture=' + nativeProbe.floatTexture);
|
|
lines.push('gpu.native.constantArrays=' + nativeProbe.constantArrays + '; constantIndexed=' + nativeProbe.constantIndexed + '; constantQuad=' + nativeProbe.constantQuad);
|
|
lines.push('gpu.native.vertexBufferBytes=' + nativeProbe.vertexBufferBytes + '; indexBufferBytes=' + nativeProbe.indexBufferBytes);
|
|
lines.push('gpu.native.pixel0=' + nativeProbe.pixel0 + '; pixel1=' + nativeProbe.pixel1 + '; pixel2=' + nativeProbe.pixel2 + '; pixel3=' + nativeProbe.pixel3);
|
|
lines.push('gpu.native.changedBytes=' + nativeProbe.changedBytes + '; mismatchCount=' + nativeProbe.mismatchCount + '; expectedChecksum=' + nativeProbe.expectedChecksum + '; actualChecksum=' + nativeProbe.actualChecksum);
|
|
lines.push('gpu.native.glError=' + nativeProbe.glError + '; framebufferStatus=' + nativeProbe.framebufferStatus + '; readReturn=' + nativeProbe.readReturn);
|
|
['matmul', 'conv'].forEach(function (operation) {
|
|
GPU_SYNTHETIC_FIELDS.forEach(function (key) { lines.push('gpu.synthetic.' + operation + '.' + key + '=' + gpu[operation][key]); });
|
|
});
|
|
['snapshotMs', 'modelMs', 'tensorMs', 'detectorMs', 'landmarkMs', 'totalMs'].forEach(function (key) { lines.push('timings.' + key + '=' + timings[key]); });
|
|
var report = lines.join('\n'); this.setData({ diagText: report });
|
|
try { if (typeof my.setClipboard === 'function') my.setClipboard({ text: report, success: function () {}, fail: function () {} }); } catch (_) {}
|
|
},
|
|
resumeVisible: function () {
|
|
if (!this.debugMode && this.ownerId !== (api.user() && api.user().id)) { this.onCancel(); return; }
|
|
if (this.pendingChoice) { this.acceptChosenPhoto(); return; }
|
|
if (this.choosing) return;
|
|
if (this.data.analyzing) { this.submitPhotos(); return; }
|
|
if (this.data.doneTip) { this.busy = false; clearTimeout(this.nextTimer); }
|
|
if (!this.data.previewUrl && !this.data.analyzing && !this.data.debugComplete) this.renderStep();
|
|
if (!this.cameraSupported) return; // 平台没有内置相机:直接用系统相机/相册
|
|
var that = this;
|
|
permissions.settings().then(function (current) {
|
|
if (!that.alive || !that.visible) return;
|
|
if (permissions.state(current, 'scope.camera') === true) { that.cameraAuthorized = true; that.mountCamera(); }
|
|
else if (permissions.state(current, 'scope.camera') === false) that.setData({ cameraEnabled: false, permissionMessage: '相机权限未开启,可前往授权设置或使用相册。' });
|
|
else that.onEnableCamera(); // First visit follows the main flow into the platform permission prompt.
|
|
}).catch(function () {});
|
|
},
|
|
layout: function () {
|
|
var info = {};
|
|
try { if (typeof my.getSystemInfoSync === 'function') info = my.getSystemInfoSync(); } catch (_) {}
|
|
var w = info.windowWidth || 375, h = Math.max(1, (info.windowHeight || 726) - (this.data.brandHeader ? this.data.brandHeader.height : 0));
|
|
this.applyCameraLayout(w, h);
|
|
// The native title bar can change the drawable viewport after onLoad.
|
|
// Use the camera's mounted bounds for both its mask and framing checks.
|
|
if (!this.pageReady || typeof my.createSelectorQuery !== 'function') return;
|
|
var that = this;
|
|
try {
|
|
my.createSelectorQuery().select('.cam').boundingClientRect().exec(function (rows) {
|
|
var rect = rows && rows[0];
|
|
if (!that.alive || !that.visible || !rect || !(rect.width > 0) || !(rect.height > 0)) return;
|
|
that.applyCameraLayout(rect.width, rect.height);
|
|
});
|
|
} catch (_) {}
|
|
},
|
|
applyCameraLayout: function (w, h) {
|
|
this.viewWidth = w; this.viewHeight = h;
|
|
var scale = Math.min(w / 375, h / 726), width = 375 * scale, height = 726 * scale;
|
|
var scan = Math.min(w / 1080, h / 2091);
|
|
this.setData({ maskInsetX: (w - width) / 2, maskInsetY: (h - height) / 2,
|
|
maskStageStyle: 'width:' + width + 'px;height:' + height + 'px;',
|
|
scanStageStyle: 'width:' + (1080 * scan) + 'px;height:' + (2091 * scan) + 'px;' });
|
|
},
|
|
mountCamera: function () {
|
|
if (!this.alive || !this.visible || !this.pageReady || this.data.cameraEnabled || this.data.debugComplete || this.data.analyzing || this.data.analysisError) return;
|
|
if (!this.cameraSupported) return;
|
|
var that = this;
|
|
var cameraSession = this.data.cameraSession + 1;
|
|
var nativeId = this.data.useNativeFace ? 'skin-camera-' + cameraSession : 'skin-camera';
|
|
this.setData({ cameraEnabled: true, cameraSession: cameraSession, nativeCameraId: nativeId, camError: false }, function () {
|
|
if (!that.alive || !that.visible || !that.data.cameraEnabled || cameraSession !== that.data.cameraSession) return;
|
|
try {
|
|
if (that.data.useAR) { that.arContext = my.createArCameraContext('skin-ar-camera-' + cameraSession); return; }
|
|
that.cameraCtx = my.createCameraContext(nativeId);
|
|
if (!that.cameraCtx || !that.cameraCtx.takePhoto) that.onCameraError();
|
|
else if (that.data.useNativeFace) that.startNativeFrames(cameraSession);
|
|
} catch (_) { that.onCameraError(); }
|
|
});
|
|
},
|
|
isCurrentCameraEvent: function (event) {
|
|
var target = event && event.currentTarget, session = target && target.dataset && target.dataset.cameraSession;
|
|
var id = target && target.id, match = typeof id === 'string' && /^skin-(?:ar-)?camera-(\d+)$/.exec(id);
|
|
if (session === undefined && match) session = match[1];
|
|
return session === undefined || Number(session) === this.data.cameraSession;
|
|
},
|
|
onARInit: function (event) {
|
|
if (!this.alive || !this.visible || !this.data.cameraEnabled || !this.isCurrentCameraEvent(event)) return;
|
|
this.setData({ liveHint: '请将脸部移入取景框', permissionMessage: '' });
|
|
// Skin analysis must use unretouched input. This is a documented AR API.
|
|
try { if (this.arContext && typeof this.arContext.faceBeauty === 'function') this.arContext.faceBeauty({ level: 0 }, function () {}); } catch (_) {}
|
|
},
|
|
onARFrame: function (event) {
|
|
if (!this.alive || !this.visible || !this.data.cameraEnabled || !this.isCurrentCameraEvent(event)) return;
|
|
var frame = event && (event.detail || event);
|
|
this.latestFrame = frame; this.latestFrameAt = Date.now();
|
|
if (this.canvasOperation) {
|
|
if (!this.data.analyzing && !this.data.analysisError && (this.data.liveHint !== '正在准备相机…' || this.data.liveGood)) this.setData({ liveHint: '正在准备相机…', liveGood: false });
|
|
this.holdState = null; return;
|
|
}
|
|
if (this.busy || this.data.analyzing || this.data.analysisError || this.data.doneTip) return;
|
|
if (this.lastDetectAt && Date.now() - this.lastDetectAt < 120) return;
|
|
this.lastDetectAt = Date.now();
|
|
var verdict = arCamera.evaluate(frame, STEPS[this.data.stepIndex].target, false, { viewWidth: this.viewWidth, viewHeight: this.viewHeight });
|
|
this.updateLiveHint(verdict.ok ? (verdict.warning ? verdict.warning + ',保持不动即可拍摄' : '保持不动') : verdict.hint, verdict.ok);
|
|
if (!verdict.ok) { this.holdState = null; return; }
|
|
var held = arCamera.hold(this.holdState, verdict.observation, Date.now());
|
|
this.holdState = held.state;
|
|
if (held.ready) this.startAutoCapture();
|
|
},
|
|
stopNativeFrames: function () {
|
|
this.nativeListenerGeneration = (this.nativeListenerGeneration || 0) + 1;
|
|
this.nativeListening = false; clearTimeout(this.nativeFrameTimer); clearTimeout(this.nativeInitTimer);
|
|
this.clearNativeCaptureWait();
|
|
var listener = this.nativeFrameListener; this.nativeFrameListener = null;
|
|
this.latestNativeVerdict = null; this.latestNativeAt = 0; this.holdState = null;
|
|
try { if (listener && typeof listener.stop === 'function') listener.stop({ success: function () {}, fail: function () {} }); } catch (_) {}
|
|
},
|
|
nativeCurrent: function (seq, session, generation) {
|
|
return this.alive && this.visible && this.data.cameraEnabled && this.seq === seq && this.data.cameraSession === session
|
|
&& this.nativeListenerGeneration === generation && this.nativeListening;
|
|
},
|
|
readNativeEngineDiagnostics: function (record, info) {
|
|
if (!this.localFaceProbe) return;
|
|
var stats;
|
|
try { if (record && record.detector && typeof record.detector.stats === 'function') stats = record.detector.stats(); } catch (_) {}
|
|
if (stats) {
|
|
this.probeEngine = probeEngineMetadata(stats, this.probeEngine);
|
|
this.probeGPU = probeGpuMetadata(probeValue(stats, 'gpu'), this.probeGPU, !!record && !record.ready);
|
|
}
|
|
if (info) {
|
|
this.probeEngine = probeEngineMetadata(info, this.probeEngine);
|
|
this.probeGPU = probeGpuMetadata(probeValue(info, 'gpu'), this.probeGPU, !!record && !record.ready);
|
|
}
|
|
},
|
|
retireNativeDetector: function (record) {
|
|
record = record || this.nativeEngineRecord;
|
|
if (!record || record.retired) return;
|
|
record.retired = true;
|
|
// dispose is documented by our engine as idempotent and pending-safe. Its
|
|
// resources stay alive until its own init/detect operation has settled.
|
|
try { if (record.detector && typeof record.detector.dispose === 'function') record.detector.dispose(); } catch (_) {}
|
|
if (this.nativeEngineRecord === record) {
|
|
this.nativeEngineRecord = null; this.nativeDetector = null; this.nativeInitPromise = null;
|
|
}
|
|
var that = this, waits = [this.nativeRetirement, record.init, record.inference].filter(function (value) { return !!value; });
|
|
var retirement = Promise.all(waits.map(function (value) { return Promise.resolve(value).catch(function () {}); }));
|
|
this.nativeRetirement = retirement;
|
|
retirement.then(function () { if (that.nativeRetirement === retirement) that.nativeRetirement = null; });
|
|
},
|
|
ensureNativeDetector: function () {
|
|
if (this.nativeInitPromise) {
|
|
if (nativeEngineOptions().backend === 'webgl' && this.nativeInferenceCompletion) {
|
|
var current = this;
|
|
return this.nativeInferenceCompletion.catch(function () {}).then(function () { return current.ensureNativeDetector(); });
|
|
}
|
|
return this.nativeInitPromise;
|
|
}
|
|
var that = this, record = { retired: false, detector: null }, barrier = this.nativeRetirement;
|
|
this.nativeEngineRecord = record;
|
|
var initialization = (barrier ? Promise.resolve(barrier).catch(function () {}) : Promise.resolve()).then(function () {
|
|
if (!that.alive || record.retired || that.nativeEngineRecord !== record) throw { canceled: true };
|
|
var implementation = require('../../utils/native-face');
|
|
if (!implementation || typeof implementation.create !== 'function') throw new Error('本地人脸识别引擎不可用,请重新开启或更新预览');
|
|
var options = nativeEngineOptions();
|
|
if (that.localFaceProbe) that.probeGPU = probeGpuMetadata({ requested: options.backend === 'webgl', selftest: 'not_run', stage: 'uninitialized', code: 'NONE', contextLost: false });
|
|
record.detector = implementation.create(options); that.nativeDetector = record.detector;
|
|
if (!record.detector || typeof record.detector.init !== 'function' || typeof record.detector.detect !== 'function') throw new Error('本地人脸识别引擎不完整');
|
|
return record.detector.init();
|
|
}).then(function (info) {
|
|
if (!that.alive || record.retired || that.nativeEngineRecord !== record) throw { canceled: true };
|
|
that.readNativeEngineDiagnostics(record, info);
|
|
if (nativeEngineOptions().backend === 'webgl') {
|
|
var gpu = probeGpuMetadata(probeValue(info, 'gpu'), null, true);
|
|
if (probeValue(info, 'backend') !== 'webgl' || gpu.selftest !== 'passed' || gpu.stage !== 'ready' || gpu.contextLost
|
|
|| !(gpu.matmulDraws > 0 && gpu.convDraws > 0 && gpu.pixelReads > 0)) {
|
|
var error = new Error('WebGL 数值自测或模型热身未通过,已停止实时识别');
|
|
error.code = 'NATIVE_FACE_WEBGL_BACKEND_FAILED'; error.gpu = probeValue(info, 'gpu'); throw error;
|
|
}
|
|
}
|
|
record.ready = true;
|
|
return record.detector;
|
|
}).catch(function (error) {
|
|
if (that.nativeEngineRecord === record || !that.nativeEngineRecord) that.readNativeEngineDiagnostics(record, error);
|
|
that.retireNativeDetector(record); throw error;
|
|
});
|
|
record.init = initialization; this.nativeInitPromise = initialization;
|
|
return initialization;
|
|
},
|
|
failNativeCapture: function (message, failureStage) {
|
|
if (this.localFaceProbe) { this.probeStage = 'failed'; this.probeFailureStage = ['init', 'listener', 'frame', 'photo', 'performance'].indexOf(failureStage) >= 0 ? failureStage : 'frame'; }
|
|
this.stopNativeFrames(); this.seq++; this.busy = false; clearTimeout(this.captureTimer);
|
|
this.cameraCtx = null;
|
|
this.setData({ cameraEnabled: false, camError: true, countdown: 0, fillLight: false, liveGood: false,
|
|
liveHint: message, permissionMessage: message + ';请重新开启实时相机,尚未完成自动拍摄。' });
|
|
},
|
|
startNativeFrames: function (session) {
|
|
this.stopNativeFrames(); this.nativeListening = true; this.lastNativeDetectAt = 0; this.nativeSlowResults = 0;
|
|
var that = this, seq = this.seq, generation = this.nativeListenerGeneration, context = this.cameraCtx;
|
|
var listenerSetup = false;
|
|
if (this.localFaceProbe) {
|
|
this.probeStage = 'initializing'; this.probeFailureStage = 'none'; this.probeFailureCode = 'none'; this.probeDetection = null; this.probeTimings = null;
|
|
}
|
|
this.setData({ liveGood: false, liveHint: nativeEngineOptions().backend === 'webgl' ? '正在校验图形加速并预热模型…' : '正在加载本地人脸识别…', permissionMessage: '' });
|
|
if (!context || typeof context.onCameraFrame !== 'function') { this.failNativeCapture('当前相机缺少实时帧监听能力', 'listener'); return; }
|
|
this.nativeInitTimer = setTimeout(function () {
|
|
if (!that.nativeCurrent(seq, session, generation) || listenerSetup) return;
|
|
that.readNativeEngineDiagnostics(that.nativeEngineRecord);
|
|
if (that.localFaceProbe) that.probeFailureCode = 'NATIVE_FACE_INIT_TIMEOUT';
|
|
that.retireNativeDetector();
|
|
that.failNativeCapture('本地识别初始化超过30秒,请复制识别诊断后重试', 'init');
|
|
}, 30000);
|
|
this.ensureNativeDetector().then(function () {
|
|
if (!that.nativeCurrent(seq, session, generation)) return;
|
|
clearTimeout(that.nativeInitTimer);
|
|
listenerSetup = true; if (that.localFaceProbe) that.probeStage = 'listening';
|
|
var received = false;
|
|
that.nativeFrameListener = context.onCameraFrame(function (frame) {
|
|
if (!that.nativeCurrent(seq, session, generation)) return;
|
|
if (!received) { received = true; clearTimeout(that.nativeFrameTimer); }
|
|
that.detectNativeFrame(frame, seq, session, generation);
|
|
});
|
|
var listener = that.nativeFrameListener;
|
|
if (!listener || typeof listener.start !== 'function' || typeof listener.stop !== 'function') throw new Error('当前相机未返回可控制的实时帧监听器');
|
|
that.nativeFrameTimer = setTimeout(function () {
|
|
if (that.nativeCurrent(seq, session, generation) && !received) that.failNativeCapture('相机10秒未返回真实帧', 'listener');
|
|
}, 10000);
|
|
listener.start({ success: function () {
|
|
if (that.nativeCurrent(seq, session, generation) && !received) that.setData({ liveHint: '等待真实相机帧…' });
|
|
}, fail: function () { if (that.nativeCurrent(seq, session, generation)) that.failNativeCapture('实时帧监听启动失败', 'listener'); } });
|
|
}).catch(function (error) {
|
|
if (that.nativeCurrent(seq, session, generation)) {
|
|
clearTimeout(that.nativeInitTimer);
|
|
if (that.localFaceProbe) that.probeFailureCode = probeCode(error);
|
|
that.failNativeCapture(error.message || '本地人脸识别启动失败', listenerSetup ? 'listener' : 'init');
|
|
}
|
|
});
|
|
},
|
|
detectNativeFrame: function (frame, seq, session, generation) {
|
|
if (!this.nativeCurrent(seq, session, generation)) return;
|
|
this.nativeFrameCallbacks++;
|
|
if (this.nativeInference) { this.nativeFramesSkippedInFlight++; return; }
|
|
if (this.data.analyzing || this.data.analysisError || this.data.doneTip || this.data.identityPending || this.data.previewUrl) return;
|
|
if (this.nativePhotoOperation || this.canvasOperation) {
|
|
this.holdState = null;
|
|
if (this.data.liveGood || this.data.liveHint !== '正在等待上次拍照处理结束…') this.setData({ liveGood: false, liveHint: '正在等待上次拍照处理结束…' });
|
|
return;
|
|
}
|
|
// After the countdown starts, freeze recognition gates until the shutter.
|
|
// Normal hand/head motion must never restart an already armed photo.
|
|
if (this.busy && !this.countdownVoiceTicket) return;
|
|
var frameAt = Date.now();
|
|
if (this.lastNativeDetectAt && frameAt - this.lastNativeDetectAt < 120) return;
|
|
this.lastNativeDetectAt = frameAt;
|
|
this.nativeAcceptedCalls++;
|
|
if (this.localFaceProbe) { this.probeFrame = probeRawMetadata(frame); this.probeStage = 'detecting'; }
|
|
var that = this, step = this.data.stepIndex, engineRecord = this.nativeEngineRecord;
|
|
var operation;
|
|
// The in-flight/throttle checks above precede the engine's raw-buffer copy.
|
|
try { operation = Promise.resolve(this.nativeDetector.detect(frame)); } catch (error) { operation = Promise.reject(error); }
|
|
if (engineRecord) engineRecord.inference = operation;
|
|
this.nativeInference = operation;
|
|
var completion = operation.then(function (result) {
|
|
if (!that.nativeCurrent(seq, session, generation) || that.data.stepIndex !== step || that.data.analyzing || that.data.doneTip || that.nativePhotoOperation) return;
|
|
if (that.data.countdown || that.data.fillLight) return;
|
|
// Phone verification: front-camera yaw is opposite to the prior raw-frame
|
|
// assumption. Reverse its sign for both side steps, preserving guide copy,
|
|
// photo order and the existing rear-camera mapping.
|
|
var verdict = nativeQuality.evaluate(result, STEPS[step].target, { viewWidth: that.viewWidth, viewHeight: that.viewHeight,
|
|
mirrored: that.data.devicePosition === 'front' });
|
|
if (that.localFaceProbe) {
|
|
that.probeStage = 'listening';
|
|
that.probeFramesProcessed++;
|
|
that.readNativeEngineDiagnostics(engineRecord, result); that.probeTimings = probeTimingMetadata(result);
|
|
that.probeDetection = { elapsedMs: probeNumber(result.elapsedMs), faceCount: probeNumber(verdict.faceCount),
|
|
sourceAgeMs: probeNumber(Date.now() - frameAt),
|
|
reason: ['frame', 'angle', 'quality'].indexOf(verdict.reason) >= 0 ? verdict.reason : verdict.ok ? 'ok' : 'unavailable', code: 'none' };
|
|
}
|
|
that.latestNativeVerdict = verdict; that.latestNativeAt = frameAt;
|
|
var fresh = Date.now() - frameAt <= 1500;
|
|
that.nativeSlowResults = fresh ? 0 : (that.nativeSlowResults || 0) + 1;
|
|
if (that.nativeSlowResults >= 3) { that.failNativeCapture('当前识别链路连续耗时超过1.5秒,已暂停抓拍', 'performance'); return; }
|
|
that.updateLiveHint(!fresh ? '本地识别较慢,请保持不动…' : verdict.ok ? (verdict.warning ? verdict.warning + ',保持不动即可拍摄' : '保持不动') : verdict.hint, verdict.ok && fresh);
|
|
if (!verdict.ok || !fresh) {
|
|
that.holdState = null;
|
|
if (that.data.countdown || that.data.fillLight || that.nativeCaptureWait || that.countdownVoiceTicket) {
|
|
that.countdownVoiceTicket = null; that.spokenHint = '';
|
|
clearTimeout(that.captureTimer); that.clearNativeCaptureWait(); that.busy = false; that.setData({ countdown: 0, fillLight: false }); if (!that.debugMode) voice.stop(true);
|
|
}
|
|
return;
|
|
}
|
|
if (that.busy) return;
|
|
var held = nativeQuality.hold(that.holdState, verdict.observation, Date.now());
|
|
that.holdState = held.state;
|
|
if (held.ready) that.startAutoCapture();
|
|
}).catch(function (error) {
|
|
if (isGPUFailure(error)) {
|
|
if (that.nativeEngineRecord === engineRecord || !that.nativeEngineRecord) that.readNativeEngineDiagnostics(engineRecord, error);
|
|
that.retireNativeDetector(engineRecord);
|
|
}
|
|
if (that.localFaceProbe && that.nativeCurrent(seq, session, generation)) that.probeDetection = { elapsedMs: 0, faceCount: 0, reason: 'frame', code: probeCode(error) };
|
|
if (that.nativeCurrent(seq, session, generation) && !error.canceled && error.code !== 'NATIVE_FACE_BUSY') that.failNativeCapture(error.message || '本地人脸识别失败');
|
|
}).then(function () {
|
|
if (engineRecord && engineRecord.inference === completion) engineRecord.inference = null;
|
|
if (that.nativeInference === operation) that.nativeInference = null;
|
|
if (that.nativeInferenceCompletion === completion) that.nativeInferenceCompletion = null;
|
|
});
|
|
this.nativeInferenceCompletion = completion;
|
|
if (engineRecord) engineRecord.inference = completion;
|
|
},
|
|
clearNativeCaptureWait: function () {
|
|
if (this.nativeCaptureWait) clearTimeout(this.nativeCaptureWait.timer);
|
|
this.nativeCaptureWait = null;
|
|
},
|
|
finishNativeCountdown: function (seq) {
|
|
if (!this.alive || !this.visible || seq !== this.seq || !this.data.cameraEnabled || this.data.identityPending) return;
|
|
// Pose/freshness was checked before arming. Do not revalidate at zero.
|
|
this.captureNativePhoto(seq);
|
|
},
|
|
captureNativePhoto: function (seq) {
|
|
var that = this, context = this.cameraCtx;
|
|
if (this.nativePhotoOperation || !context || typeof context.takePhoto !== 'function') { this.failNativeCapture('普通相机无法开始拍照', 'photo'); return; }
|
|
if (this.localFaceProbe) this.probeStage = 'capturing';
|
|
var operation = { seq: seq, session: this.data.cameraSession, done: false }; this.nativePhotoOperation = operation;
|
|
operation.timer = setTimeout(function () {
|
|
if (!operation.done && that.alive && that.visible && seq === that.seq) that.setData({ countdown: 0, fillLight: false, liveGood: false,
|
|
liveHint: '原生拍照未返回,请退出拍照页后重新进入', permissionMessage: '正在等待原生拍照结束,未开始新的拍照。' });
|
|
}, 10000);
|
|
function finish(kind, result) {
|
|
if (operation.done || that.nativePhotoOperation !== operation) return;
|
|
operation.done = true; clearTimeout(operation.timer); that.nativePhotoOperation = null;
|
|
if (!that.alive || !that.visible || seq !== that.seq || operation.session !== that.data.cameraSession) return;
|
|
var file = result && (result.tempImagePath || result.tempFilePath || result.apFilePath);
|
|
if (kind !== 'success' || typeof file !== 'string' || !file) { that.failNativeCapture('原生相机未成功返回照片', 'photo'); return; }
|
|
if (that.localFaceProbe) that.probeAutoCaptured++;
|
|
if (that.localFaceProbe) that.probeStage = 'listening';
|
|
that.busy = false; that.confirmPhoto(file);
|
|
}
|
|
try { context.takePhoto({ quality: 'high', success: function (result) { finish('success', result); }, fail: function (error) { finish('fail', error); }, complete: function () { finish('complete'); } }); }
|
|
catch (_) { finish('fail'); }
|
|
},
|
|
startAutoCapture: function () {
|
|
if (this.busy || !this.data.autoCapture || !this.data.cameraEnabled) return;
|
|
if (this.data.useNativeFace && (this.nativePhotoOperation || !this.latestNativeVerdict || !this.latestNativeVerdict.ok || Date.now() - this.latestNativeAt > 1500)) return;
|
|
if (this.canvasOperation) { this.setData({ liveHint: '正在准备相机…', liveGood: false }); return; }
|
|
var that = this, seq = this.seq, left = 3;
|
|
this.busy = true; this.holdState = null;
|
|
if (!this.debugMode) {
|
|
if (typeof voice.dropHint === 'function') voice.dropHint();
|
|
}
|
|
function tick() {
|
|
if (!that.alive || !that.visible || seq !== that.seq) return;
|
|
that.setData({ countdown: left });
|
|
if (left-- > 0) { that.captureTimer = setTimeout(tick, 1000); return; }
|
|
that.setData({ fillLight: true });
|
|
that.captureTimer = setTimeout(function () {
|
|
if (!that.alive || !that.visible || seq !== that.seq) return;
|
|
if (that.data.useNativeFace) {
|
|
that.finishNativeCountdown(seq); return;
|
|
}
|
|
if (!that.latestFrame || Date.now() - that.latestFrameAt > 1500) {
|
|
that.busy = false; that.setData({ fillLight: false, liveHint: '实时相机已暂停,请重新开启' }); return;
|
|
}
|
|
var latest = arCamera.evaluate(that.latestFrame, STEPS[that.data.stepIndex].target, false,
|
|
{ viewWidth: that.viewWidth, viewHeight: that.viewHeight });
|
|
if (!latest.ok) {
|
|
that.busy = false; that.holdState = null;
|
|
that.setData({ countdown: 0, fillLight: false, liveGood: false, liveHint: latest.hint }); return;
|
|
}
|
|
var frame = that.latestFrame;
|
|
that.runCanvas(function () {
|
|
if (!that.alive || !that.visible || seq !== that.seq) throw { canceled: true };
|
|
return image.captureFrameToBase64(frame, that, { defaultPixelFormat: config.arPixelFormat });
|
|
}).then(function (photo) {
|
|
if (!that.alive || !that.visible || seq !== that.seq) return;
|
|
that.preparedPhotos = that.preparedPhotos || []; that.preparedPhotos[that.data.stepIndex] = photo;
|
|
that.busy = false; that.confirmPhoto(photo);
|
|
}).catch(function (error) {
|
|
if (!that.alive || seq !== that.seq) return;
|
|
that.busy = false; that.setData({ countdown: 0, fillLight: false, liveHint: error.message, liveGood: false });
|
|
// Stop automatic retries after an unsupported pixel layout; a new AR
|
|
// session or explicit retry is required, avoiding endless countdowns.
|
|
that.setData({ cameraEnabled: false, permissionMessage: error.message });
|
|
});
|
|
}, 300);
|
|
}
|
|
if (this.debugMode || typeof voice.afterCurrent !== 'function') {
|
|
if (!this.debugMode) voice.play(voice.VOICE.countdown);
|
|
tick(); return;
|
|
}
|
|
// Let the step introduction finish before the spoken/visual countdown.
|
|
// A stalled audio host gets only a bounded delay and cannot block photos.
|
|
var ticket = {}; this.countdownVoiceTicket = ticket;
|
|
function begin() {
|
|
if (that.countdownVoiceTicket !== ticket || !that.alive || !that.visible || seq !== that.seq || !that.busy) return;
|
|
that.countdownVoiceTicket = null; clearTimeout(that.captureTimer);
|
|
if (that.data.useNativeFace && (!that.latestNativeVerdict || !that.latestNativeVerdict.ok || Date.now() - that.latestNativeAt > 1500)) {
|
|
that.busy = false; that.holdState = null; return;
|
|
}
|
|
voice.play(voice.VOICE.countdown, { cut: true }); tick();
|
|
}
|
|
this.captureTimer = setTimeout(begin, 3000);
|
|
voice.afterCurrent(begin);
|
|
},
|
|
// Native canvas operations cannot be cancelled by hiding/remounting a camera.
|
|
// Keep their promise across pause/restart, and serialize both raw AR exports
|
|
// and ordinary image compression on this page's single canvas.
|
|
runCanvas: function (work) {
|
|
var that = this, previous = this.canvasOperation;
|
|
var operation = (previous ? previous.catch(function () {}) : Promise.resolve()).then(work);
|
|
this.canvasOperation = operation;
|
|
function release() {
|
|
if (that.canvasOperation !== operation) return;
|
|
that.canvasOperation = null;
|
|
if (that.alive && that.visible && that.data.cameraEnabled && !that.busy && !that.data.analyzing) {
|
|
that.setData({ liveHint: '请将脸部移入取景框', liveGood: false });
|
|
}
|
|
}
|
|
return operation.then(function (value) { release(); return value; }, function (error) { release(); throw error; });
|
|
},
|
|
onEnableCamera: function () {
|
|
if (this.authorizing) return;
|
|
var that = this; this.authorizing = true;
|
|
this.setData({ camError: false, permissionMessage: '' });
|
|
permissions.ensure('scope.camera').then(function () { that.cameraAuthorized = true; that.mountCamera(); }).catch(function (err) {
|
|
if (that.alive && that.visible) that.setData({ camError: true, permissionMessage: err.message });
|
|
}).then(function () { that.authorizing = false; });
|
|
},
|
|
onOpenSettings: function () {
|
|
var that = this;
|
|
permissions.open().then(function (current) {
|
|
if (permissions.state(current, 'scope.camera') === true) { that.cameraAuthorized = true; that.mountCamera(); }
|
|
}).catch(function (err) { if (that.alive) my.showToast({ content: err.message, type: 'none' }); });
|
|
},
|
|
onCloseDiagnose: function () { this.setData({ diagText: '' }); },
|
|
// 开发版:把真机上的相机能力/授权返回值直接打在页面上,供截图定位
|
|
onDiagnose: function () {
|
|
if (this.localFaceProbe) { this.onCopyCaptureDiagnostics(); return; }
|
|
var that = this;
|
|
this.setData({ diagText: '检测中…' });
|
|
permissions.diagnose().then(function (info) {
|
|
if (that.alive) that.setData({ diagText: info.concat(['logicalPosition=' + that.data.devicePosition,
|
|
'requestedPosition=' + (that.data.useAR ? that.data.arDevicePosition : that.data.devicePosition), 'actualPosition=未核验']).join('\n') });
|
|
}).catch(function (err) {
|
|
if (that.alive) that.setData({ diagText: 'diagnose error: ' + (err && err.message ? err.message : String(err)) });
|
|
});
|
|
},
|
|
onHide: function () { this.pause(); },
|
|
onUnload: function () {
|
|
this.pause(); this.alive = false; this.photos = []; this.preparedPhotos = []; this.reportPhotos = []; this.debugResults = [];
|
|
if (this.nativePhotoOperation) clearTimeout(this.nativePhotoOperation.timer);
|
|
this.retireNativeDetector();
|
|
this.pendingChoice = null; this.choiceSeq = (this.choiceSeq || 0) + 1;
|
|
if (!this.debugMode && typeof api.discardAnalysis === 'function') api.discardAnalysis(this.attempt);
|
|
this.attempt = null;
|
|
},
|
|
pause: function () {
|
|
this.visible = false; this.seq++; this.busy = false;
|
|
this.countdownVoiceTicket = null;
|
|
this.stopNativeFrames();
|
|
clearTimeout(this.nextTimer); clearTimeout(this.captureTimer);
|
|
if (this.signal) this.signal.canceled = true;
|
|
if (!this.debugMode) voice.stop(true);
|
|
this.cameraCtx = null;
|
|
this.arContext = null; this.latestFrame = null; this.holdState = null;
|
|
if (this.alive) this.setData({ cameraEnabled: false, countdown: 0, fillLight: false });
|
|
},
|
|
renderStep: function () {
|
|
var cfg = STEPS[this.data.stepIndex];
|
|
this.setData({ stepText: (this.data.stepIndex + 1) + '/3', stepTip: cfg.tip,
|
|
guides: [cfg.turn, '取下眼镜', '光线充足'], maskStep: cfg.mask, maskName: cfg.target, doneTip: '', nextTip: '' });
|
|
this.spokenHint = '';
|
|
if (!this.debugMode) {
|
|
if (typeof voice.dropHint === 'function') voice.dropHint();
|
|
voice.play(voice.VOICE.step[this.data.stepIndex]);
|
|
}
|
|
this.holdState = null; this.latestNativeVerdict = null; this.latestNativeAt = 0;
|
|
},
|
|
updateLiveHint: function (text, good) {
|
|
this.setData({ liveHint: text, liveGood: good });
|
|
if (this.debugMode || text === this.spokenHint) return;
|
|
this.spokenHint = text;
|
|
var id = voice.VOICE.hint && voice.VOICE.hint[text];
|
|
if (id) voice.play(id, { hint: true });
|
|
else if (typeof voice.dropHint === 'function') voice.dropHint();
|
|
},
|
|
onCameraError: function (event) {
|
|
if (!this.alive || !this.visible) return;
|
|
if (event && (!this.data.cameraEnabled || !this.isCurrentCameraEvent(event))) return;
|
|
if (this.localFaceProbe) { this.probeStage = 'failed'; this.probeFailureStage = 'listener'; }
|
|
// Stopping a native camera invalidates a pending shutter/countdown/export.
|
|
// An already-submitted analysis has a separate lifetime and must continue.
|
|
if (!this.data.analyzing && !this.submitting) { this.seq++; this.busy = false; }
|
|
this.stopNativeFrames();
|
|
clearTimeout(this.captureTimer);
|
|
this.latestFrame = null; this.latestFrameAt = 0; this.lastDetectAt = 0; this.holdState = null;
|
|
this.arContext = null; this.cameraCtx = null;
|
|
this.setData({ camError: true, cameraEnabled: false, countdown: 0, fillLight: false, liveGood: false,
|
|
permissionMessage: this.data.useNativeFace ? '实时相机暂不可用,请检查相机权限后重新开启;尚未完成自动拍摄。' : '相机暂不可用,请检查淘宝的系统相机权限,或使用系统相机/相册。' });
|
|
my.showToast({ content: this.data.useNativeFace ? '实时相机不可用,请重新开启' : '相机不可用,请使用系统相机或相册', type: 'none' });
|
|
},
|
|
onCameraStop: function (event) { if (this.alive && this.visible && this.data.cameraEnabled && this.isCurrentCameraEvent(event)) this.onCameraError(event); },
|
|
onShutter: function () {
|
|
if (this.busy || this.data.previewUrl || this.data.analyzing || this.data.doneTip || this.data.analysisError) return;
|
|
if (this.data.autoCapture) { my.showToast({ content: '实时相机将在姿态合适时自动拍摄,请开启相机并保持面部在框内', type: 'none' }); return; }
|
|
if (this.data.camError || !this.cameraCtx) return this.choose(['camera']);
|
|
var that = this, seq = this.seq;
|
|
this.busy = true;
|
|
this.cameraCtx.takePhoto({ quality: 'high', success: function (res) {
|
|
if (!that.alive || !that.visible || seq !== that.seq) return;
|
|
var file = res.tempImagePath || res.tempFilePath || res.apFilePath;
|
|
if (file) {
|
|
that.busy = false;
|
|
if (that.debugMode) that.setData({ previewUrl: file });
|
|
else that.confirmPhoto(file);
|
|
}
|
|
else my.showToast({ content: '拍摄失败,请重试', type: 'none' });
|
|
}, fail: function () {
|
|
if (!that.alive || !that.visible || seq !== that.seq) return;
|
|
// 真机上内置相机 takePhoto 失败(机型/组件差异、权限被拒等):
|
|
// 不打断流程,直接退回系统相机走 my.chooseImage 那条已验证过的路。
|
|
that.busy = false;
|
|
my.showToast({ content: '内置相机不可用,改用系统相机', type: 'none' });
|
|
that.choose(['camera']);
|
|
}, complete: function () {
|
|
if (seq === that.seq && !that.data.doneTip && !that.data.analyzing && !that.data.previewUrl) that.busy = false;
|
|
} });
|
|
},
|
|
choose: function (sourceType) {
|
|
if (this.localFaceProbe) return;
|
|
if (this.busy || this.choosing || this.data.previewUrl || this.data.analyzing || this.data.doneTip || this.data.analysisError || this.data.identityPending) return;
|
|
var that = this, ticket = this.choiceSeq = (this.choiceSeq || 0) + 1;
|
|
this.busy = true; this.choosing = true;
|
|
// Camera capture must not depend on unrelated album authorization.
|
|
permissions.ensure(sourceType[0] === 'camera' ? 'scope.camera' : 'scope.album').then(function () {
|
|
if (!that.alive || !that.visible || ticket !== that.choiceSeq) { that.busy = false; that.choosing = false; return; }
|
|
my.chooseImage({ count: 1, sourceType: sourceType,
|
|
success: function (res) {
|
|
var paths = res.apFilePaths || res.tempFilePaths || [];
|
|
if (that.alive && ticket === that.choiceSeq && paths[0] && (that.debugMode || that.ownerId === (api.user() && api.user().id))) {
|
|
that.pendingChoice = paths[0];
|
|
if (that.visible && !that.data.identityPending) that.acceptChosenPhoto();
|
|
}
|
|
}, fail: function () { if (that.alive && that.visible) my.showToast({ content: '未选取照片,可重试或检查系统权限', type: 'none' }); },
|
|
complete: function () {
|
|
if (!that.alive || ticket !== that.choiceSeq) return;
|
|
that.choosing = false;
|
|
if (!that.data.doneTip && !that.data.analyzing && !that.data.previewUrl) that.busy = false;
|
|
if (that.pendingChoice && that.visible && !that.data.identityPending) that.acceptChosenPhoto();
|
|
} });
|
|
}).catch(function (err) {
|
|
that.busy = false; that.choosing = false;
|
|
if (that.alive && that.visible) { that.setData({ permissionMessage: err.message }); my.showToast({ content: err.message, type: 'none' }); }
|
|
});
|
|
},
|
|
acceptChosenPhoto: function () {
|
|
if (!this.pendingChoice || !this.alive || !this.visible || this.data.identityPending || this.data.analyzing) return;
|
|
if (!this.debugMode && this.ownerId !== (api.user() && api.user().id)) { this.pendingChoice = null; return; }
|
|
var photo = this.pendingChoice; this.pendingChoice = null; this.busy = false;
|
|
if (this.debugMode) this.setData({ previewUrl: photo });
|
|
else this.confirmPhoto(photo);
|
|
},
|
|
onSystemCamera: function () { if (this.localFaceProbe) return; this.choose(['camera']); },
|
|
onAlbum: function () { if (this.localFaceProbe) return; this.choose(['album']); },
|
|
onFlip: function () {
|
|
if (!this.alive || !this.visible || (this.busy && !this.data.countdown && !this.nativeCaptureWait) || this.choosing || this.data.identityPending ||
|
|
this.data.previewUrl || this.data.analyzing || this.data.doneTip || this.data.analysisError) return;
|
|
if (!this.data.cameraEnabled) { my.showToast({ content: '请在系统相机内切换前后镜头', type: 'none' }); return; }
|
|
var that = this, nextPosition = this.data.devicePosition === 'front' ? 'back' : 'front';
|
|
// Changing the requested lens starts a fresh native component. Invalidate
|
|
// the countdown and old frame/export callbacks before releasing the camera.
|
|
this.seq++; this.busy = false;
|
|
this.stopNativeFrames();
|
|
clearTimeout(this.captureTimer);
|
|
this.latestFrame = null; this.latestFrameAt = 0; this.lastDetectAt = 0; this.holdState = null;
|
|
this.arContext = null; this.cameraCtx = null; if (!this.debugMode) voice.stop(true);
|
|
this.setData({ cameraEnabled: false, devicePosition: nextPosition, arDevicePosition: arDevicePosition(nextPosition), countdown: 0, fillLight: false,
|
|
liveGood: false, liveHint: '请将脸部移入取景框', permissionMessage: '' }, function () {
|
|
that.mountCamera();
|
|
});
|
|
},
|
|
onRetake: function () { if (!this.busy) this.setData({ previewUrl: '' }); },
|
|
onConfirm: function () {
|
|
this.confirmPhoto(this.data.previewUrl);
|
|
},
|
|
confirmPhoto: function (photo) {
|
|
if (this.busy || typeof photo !== 'string' || !photo || this.data.analyzing) return;
|
|
var that = this, step = this.data.stepIndex;
|
|
this.busy = true; this.photos[step] = photo;
|
|
if (!this.debugMode) voice.play(voice.VOICE.shot, { cut: true });
|
|
if (step === 2) { this.submitPhotos(); return; }
|
|
// Automatic acceptance never mounts the manual review overlay. Clear the
|
|
// flash/countdown in the same render that shows the next-step transition.
|
|
this.setData({ stepIndex: step + 1, previewUrl: '', countdown: 0, fillLight: false,
|
|
doneTip: STEPS[step].done, nextTip: STEPS[step + 1].next });
|
|
this.nextTimer = setTimeout(function () {
|
|
if (!that.alive || !that.visible) return;
|
|
that.busy = false; that.renderStep();
|
|
}, 2000);
|
|
},
|
|
submitPhotos: function () {
|
|
if (this.submitting || !this.photos[0] || !this.photos[1] || !this.photos[2]) return;
|
|
var that = this, seq = ++this.seq, dataUrls = [];
|
|
var debugResults = [];
|
|
this.signal = { canceled: false }; var signal = this.signal;
|
|
this.submitting = true;
|
|
this.stopNativeFrames();
|
|
// All three photos are already captured. Release the live camera instead
|
|
// of decoding/recognizing frames behind the report waiting screen.
|
|
this.cameraCtx = null; this.arContext = null; this.latestFrame = null; this.holdState = null;
|
|
clearTimeout(this.captureTimer);
|
|
this.busy = true; this.setData({ analyzing: true, cameraEnabled: false, previewUrl: '', countdown: 0, fillLight: false,
|
|
doneTip: '', analysisError: '', analysisStage: '正在处理照片…' });
|
|
if (!this.debugMode) voice.play(voice.VOICE.analyzing);
|
|
var existing = this.attempt && this.attempt.profile;
|
|
// One shared native canvas: draw the photos sequentially.
|
|
(existing ? Promise.resolve() : this.photos.reduce(function (chain, photo, index) {
|
|
return chain.then(function () {
|
|
if (signal.canceled) throw { canceled: true };
|
|
if (that.alive) that.setData({ analysisStage: '正在处理第 ' + (index + 1) + '/3 张照片…' });
|
|
return that.preparedPhotos && that.preparedPhotos[index] ? Promise.resolve(that.preparedPhotos[index]) : that.runCanvas(function () {
|
|
if (signal.canceled || !that.alive || !that.visible || seq !== that.seq) throw { canceled: true };
|
|
return image.compressToBase64(photo, that);
|
|
});
|
|
}).then(function (value) {
|
|
dataUrls.push(value);
|
|
if (that.debugMode) debugResults.push({ key: STEPS[debugResults.length].target,
|
|
title: STEPS[debugResults.length].tip.replace(/^请/, ''), photo: photo,
|
|
width: that.data.compressWidth, height: that.data.compressHeight,
|
|
kilobytes: Math.round(value.replace(/^data:[^,]+,/, '').length * 3 / 4 / 1024) });
|
|
});
|
|
}, Promise.resolve())).then(function () {
|
|
if (signal.canceled) throw { canceled: true };
|
|
if (that.debugMode) {
|
|
if (!that.alive || !that.visible || seq !== that.seq) return;
|
|
that.busy = false; that.cameraCtx = null;
|
|
dataUrls = []; // Do not keep base64, upload, create an account or touch report state.
|
|
if (that.localFaceProbe) that.probeStage = 'complete';
|
|
that.setData({ analyzing: false, cameraEnabled: false, debugComplete: true, debugPhotos: debugResults });
|
|
return;
|
|
}
|
|
var ordered = [dataUrls[0], dataUrls[2], dataUrls[1]];
|
|
if (!existing) that.reportPhotos = ordered;
|
|
that.setData({ analysisStage: existing ? '正在恢复本次检测…' : '正在上传照片并生成报告…' });
|
|
if (!that.attempt && typeof api.createAnalysisAttempt === 'function') that.attempt = api.createAnalysisAttempt();
|
|
var task = existing ? api.resumeAnalysis(that.attempt, signal) : api.analyze(ordered, getApp().globalData.profile, signal, that.attempt);
|
|
return task.then(function (payload) {
|
|
if (!that.alive || !that.visible || seq !== that.seq) return;
|
|
getApp().globalData.photos = that.reportPhotos; getApp().globalData.payload = payload;
|
|
that.setData({ analyzing: false });
|
|
voice.stop(true); my.redirectTo({ url: '/pages/report/report' });
|
|
});
|
|
}).catch(function (err) {
|
|
if (err.canceled || !that.alive || !that.visible || seq !== that.seq) return;
|
|
that.busy = false; that.setData({ analyzing: false, analysisError: err.message || err.errorMessage || '照片处理失败,请重试' });
|
|
if (!that.debugMode) voice.play(voice.VOICE.fail, { cut: true });
|
|
my.showToast({ content: err.message || '照片处理失败,请重新拍摄', type: 'none', duration: 3000 });
|
|
}).then(function () {
|
|
that.submitting = false;
|
|
if (signal.canceled && that.alive && that.visible && that.data.analyzing) that.submitPhotos();
|
|
});
|
|
},
|
|
onRetryAnalysis: function () { if (!this.busy) this.submitPhotos(); },
|
|
onScanError: function () { this.setData({ scanError: true }); },
|
|
onRestart: function () {
|
|
if (this.debugMode && !this.debugGateEnabled()) return;
|
|
if (this.signal) this.signal.canceled = true;
|
|
if (!this.debugMode && typeof api.discardAnalysis === 'function') api.discardAnalysis(this.attempt);
|
|
this.stopNativeFrames();
|
|
this.seq++; this.photos = []; this.preparedPhotos = []; this.reportPhotos = []; this.attempt = null; this.busy = false;
|
|
this.setData({ stepIndex: 0, previewUrl: '', analyzing: false, analysisError: '', doneTip: '', cameraEnabled: false });
|
|
this.renderStep(); this.mountCamera();
|
|
},
|
|
onDebugRestart: function () {
|
|
if (!this.debugMode || !this.debugGateEnabled()) return;
|
|
if (this.signal) this.signal.canceled = true;
|
|
this.stopNativeFrames(); clearTimeout(this.captureTimer); clearTimeout(this.nextTimer);
|
|
this.photos = []; this.preparedPhotos = []; this.reportPhotos = []; this.debugResults = []; this.pendingChoice = null;
|
|
this.choiceSeq = (this.choiceSeq || 0) + 1; this.busy = false; this.seq++;
|
|
this.probeFramesProcessed = 0; this.probeAutoCaptured = 0; this.probeFrame = null; this.probeDetection = null;
|
|
this.nativeFrameCallbacks = 0; this.nativeFramesSkippedInFlight = 0; this.nativeAcceptedCalls = 0; this.probeTimings = null;
|
|
this.probeStage = 'initializing'; this.probeFailureStage = 'none'; this.probeFailureCode = 'none';
|
|
this.setData({ debugComplete: false, debugPhotos: [], stepIndex: 0, previewUrl: '', analyzing: false, analysisError: '', doneTip: '', cameraEnabled: false, diagText: '' });
|
|
this.renderStep(); this.onEnableCamera();
|
|
},
|
|
onCancel: function () {
|
|
if (this.debugMode) { this.photos = []; this.preparedPhotos = []; this.reportPhotos = []; this.debugResults = []; this.setData({ debugPhotos: [], previewUrl: '', diagText: '' }); }
|
|
this.pause(); my.navigateBack();
|
|
},
|
|
noop: function () {}
|
|
}, typeof my === 'undefined' ? {} : my));
|