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

270 lines
19 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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';
}