270 lines
19 KiB
JavaScript
270 lines
19 KiB
JavaScript
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';
|
||
}
|