// Taobao ar-camera contract: official component docType=20/docId=1484. // mode=faceDetection2D; onARFrame receives e.detail.{width,height,anchors}. // rect=[x1,y1,x2,y2], landmarks=[x0,y0,...x105,y105] are in frame pixels. // Native yaw/pitch/roll units are unspecified: use the same 68-point geometry // and capture gates as H5 instead. No network, bitmap decoding or photo storage. var HeadPose = require('../vendor/head-pose'); var CaptureQuality = require('../vendor/capture-quality'); // Official 106-point diagram: contour 0..32, brows/nose/eyes 33..63, // lips 84..103; 104/105 are additional eye centers, not mouth points. var POINTS_68 = []; for (var jaw = 0; jaw <= 32; jaw += 2) POINTS_68.push(jaw); for (var feature = 33; feature <= 63; feature++) POINTS_68.push(feature); for (var lip = 84; lip <= 103; lip++) POINTS_68.push(lip); function finite(value) { return typeof value === 'number' && isFinite(value); } function frameOf(event) { return event && event.detail ? event.detail : event; } function points68(flat) { if (!flat || typeof flat.length !== 'number' || flat.length < 212) return null; var points = POINTS_68.map(function (index) { return { x: flat[index * 2], y: flat[index * 2 + 1] }; }); return points.every(function (point) { return finite(point.x) && finite(point.y); }) ? points : null; } function normalize(event) { var frame = frameOf(event); if (!frame || !finite(frame.width) || !finite(frame.height) || frame.width <= 0 || frame.height <= 0 || !Array.isArray(frame.anchors)) return null; return { width: frame.width, height: frame.height, count: frame.anchors.length, detections: frame.anchors.map(function (anchor) { if (!anchor || !Array.isArray(anchor.rect) || anchor.rect.length !== 4 || !anchor.rect.every(finite)) return null; var positions = points68(anchor.landmarks), r = anchor.rect; if (!positions || r[2] <= r[0] || r[3] <= r[1]) return null; return { detection: { box: { x: r[0], y: r[1], width: r[2] - r[0], height: r[3] - r[1] }, // Native anchors expose no confidence. This is an explicit native // detection marker, not a measured face-api confidence value. score: 1 }, nativeAnchor: true, landmarks: { positions: positions } }; }) }; } function evaluate(event, target, mirrored, options) { options = options || {}; var frame = normalize(event); if (!frame) return { ok: false, reason: 'frame', hint: '等待相机真实画面…', frameReceived: false }; var detection = frame.detections[0], offsetX = 0; if (detection) { var box = detection.detection.box; // Match H5 object-fit:cover framing when viewport dimensions are known. var scale = finite(options.viewWidth) && options.viewWidth > 0 && finite(options.viewHeight) && options.viewHeight > 0 ? Math.max(options.viewWidth / frame.width, options.viewHeight / frame.height) / options.viewWidth : 1 / frame.width; offsetX = (box.x + box.width / 2 - frame.width / 2) * scale; } var verdict = CaptureQuality.evaluate(detection, { count: frame.count, target: target, mirrored: !!mirrored, width: frame.width, height: frame.height, offsetX: offsetX, luma: finite(options.luma) ? options.luma : null }); // Do not attach native buffers/landmarks to verdict: callers may setData it. verdict.frameReceived = true; verdict.faceCount = frame.count; verdict.width = frame.width; verdict.height = frame.height; return verdict; } function capability(platform) { var signal = 'unknown'; if (platform && typeof platform.canIUse === 'function') { try { signal = platform.canIUse('ar-camera') ? 'yes' : 'no'; } catch (_) {} } return { componentSignal: signal, contextFactory: !!(platform && typeof platform.createArCameraContext === 'function'), frameVerified: false }; } function context(platform, id) { if (!platform || typeof platform.createArCameraContext !== 'function') throw new Error('当前淘宝版本未提供 AR 相机上下文'); return platform.createArCameraContext(id); } function inspect(event) { var frame = frameOf(event), fields = [], seen = []; function visit(value, path, depth) { if (value && typeof value === 'object' && finite(value.byteLength)) { fields.push(path + ': ' + Object.prototype.toString.call(value).slice(8, -1) + '(' + value.byteLength + ' bytes)'); return; } if (Array.isArray(value)) { fields.push(path + ': Array(' + value.length + ')'); return; } if (value && typeof value === 'object' && depth < 2 && seen.indexOf(value) < 0) { seen.push(value); Object.keys(value).slice(0, 30).forEach(function (key) { visit(value[key], path ? path + '.' + key : key, depth + 1); }); return; } if (/(^|\.)(pixelFormat|format|width|height|stride|bytesPerRow)$/.test(path) && (typeof value === 'string' || finite(value))) { fields.push(path + ': ' + String(value).slice(0, 48)); return; } fields.push(path + ': ' + (typeof value === 'string' ? 'String(' + value.length + ')' : typeof value)); } if (frame && typeof frame === 'object') visit(frame, '', 0); return fields; } // Diagnostics use data descriptors only: inspecting a context/event must not // invoke native getters, stringify native objects, or reveal biometric values. function diagnosticValue(object, key) { var current = object, seen = []; for (var depth = 0; current && depth < 6 && seen.indexOf(current) < 0; depth++) { seen.push(current); try { var descriptor = Object.getOwnPropertyDescriptor(current, key); if (descriptor) return Object.prototype.hasOwnProperty.call(descriptor, 'value') ? descriptor.value : undefined; current = Object.getPrototypeOf(current); } catch (_) { return undefined; } } } function diagnosticMethods(object) { var current = object, names = [], seen = [], keys = [], hidden = ['constructor', 'toString', 'toLocaleString', 'valueOf', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable']; for (var depth = 0; current && depth < 6 && seen.indexOf(current) < 0; depth++) { seen.push(current); try { Object.getOwnPropertyNames(current).slice(0, 200).forEach(function (name) { if (!/^[A-Za-z][A-Za-z0-9_$]{0,79}$/.test(name) || hidden.indexOf(name) >= 0 || keys.indexOf(name) >= 0) return; keys.push(name); var descriptor = Object.getOwnPropertyDescriptor(current, name); if (descriptor && typeof descriptor.value === 'function') names.push(name); }); current = Object.getPrototypeOf(current); } catch (_) { break; } } return names.sort().slice(0, 100); } function diagnosticText(value) { if (typeof value !== 'string' || !value || value.length > 120 || /data:|base64|https?:\/\/|bearer\s/i.test(value)) return null; return value.replace(/[\r\n\t]/g, ' ').slice(0, 100); } function diagnosticSystem(platform) { var fields = [], info, getInfo = diagnosticValue(platform, 'getSystemInfoSync'); if (typeof getInfo === 'function') { try { info = getInfo.call(platform); } catch (_) { fields.push('getSystemInfoSync=failed'); } } else fields.push('getSystemInfoSync=unavailable'); ['brand', 'model', 'system', 'platform', 'app', 'appName', 'version', 'appVersion', 'clientVersion', 'SDKVersion', 'appxVersion', 'appXVersion', 'AppXVersion'].forEach(function (key) { var value = diagnosticValue(info, key), text = diagnosticText(value); if (text !== null) fields.push('system.' + key + '=' + text); }); ['SDKVersion', 'version', 'appVersion', 'appxVersion', 'appXVersion', 'AppXVersion'].forEach(function (key) { var text = diagnosticText(diagnosticValue(platform, key)); if (text !== null) fields.push('runtime.' + key + '=' + text); }); return fields; } function diagnosticToolbarTop(platform) { var info, getInfo = diagnosticValue(platform, 'getSystemInfoSync'); try { if (typeof getInfo === 'function') info = getInfo.call(platform); } catch (_) {} function inset(value) { return finite(value) && value >= 0 && value <= 200 ? value : 0; } var status = inset(diagnosticValue(info, 'statusBarHeight')); var safeTop = inset(diagnosticValue(diagnosticValue(info, 'safeArea'), 'top')); var title = inset(diagnosticValue(info, 'titleBarHeight')) || 44; // Some hosts extend fixed content under both native bars. Keep a generous // fallback even when system geometry is missing, with the page body following. return Math.max(112, Math.max(status, safeTop) + title + 12); } function diagnosticEvent(event) { var metadata = [], seen = [], type = diagnosticText(diagnosticValue(event, 'type')); if (!type || !/^[A-Za-z][A-Za-z0-9_.:-]{0,63}$/.test(type)) type = null; var numeric = ['width', 'height', 'imageWidth', 'imageHeight', 'cameraWidth', 'cameraHeight', 'stride', 'bytesPerRow', 'rotation', 'timestamp', 'timeStamp', 'error', 'errorCode', 'code']; var facing = ['devicePosition', 'cameraPosition', 'cameraFacing', 'facing', 'facingMode', 'position', 'isFrontCamera', 'frontCamera', 'mirrored', 'isMirrored']; var formats = ['pixelFormat', 'format', 'orientation', 'mode', 'trackMode']; var containers = ['detail', 'frame', 'data', 'camera', 'cameraInfo', 'image', 'capturedImage', 'result', 'metadata']; function visit(value, path, depth) { if (!value || typeof value !== 'object' || seen.indexOf(value) >= 0 || depth > 3) return; seen.push(value); numeric.concat(facing, formats).forEach(function (key) { var scalar = diagnosticValue(value, key), allowed = finite(scalar) || typeof scalar === 'boolean'; if (typeof scalar === 'string' && facing.indexOf(key) >= 0) allowed = /^(front|back|rear|user|environment|unknown|auto|0|1|2)$/i.test(scalar); if (typeof scalar === 'string' && formats.indexOf(key) >= 0) allowed = /^(RGBA|BGRA|ARGB|ABGR|RGB|RGB24|RGBA8888|BGRA8888|ARGB8888|ABGR8888|NV12|NV21|YUV|YUV420|YUV420P|JPEG|JPG|PNG|portrait|landscape|landscapeLeft|landscapeRight|portraitUpsideDown|faceDetection2D|faceTracking3D|imageTracking|worldTracking)$/i.test(scalar); if (allowed) metadata.push((path ? path + '.' : '') + key + '=' + String(scalar)); }); ['anchors', 'faces'].forEach(function (key) { var list = diagnosticValue(value, key); if (Array.isArray(list)) metadata.push((path ? path + '.' : '') + key + '.count=' + list.length); }); if (depth < 3) containers.forEach(function (key) { visit(diagnosticValue(value, key), path ? path + '.' + key : key, depth + 1); }); } visit(event, '', 0); return { type: type, metadata: metadata.slice(0, 80) }; } module.exports = { normalize: normalize, points68: points68, evaluate: evaluate, hold: CaptureQuality.hold, config: CaptureQuality.config, poseConfig: HeadPose.config, capability: capability, context: context, inspect: inspect, diagnostics: { methods: diagnosticMethods, system: diagnosticSystem, toolbarTop: diagnosticToolbarTop, event: diagnosticEvent } };