@@ -1,10 +1,317 @@
// Standalone device probe. No account API, upload, server call or saved photo.
var AR = require ( '../../utils/ar-camera' ) ;
var image = require ( '../../utils/image' ) ;
var permissions = require ( '../../utils/permissions' ) ;
var config = require ( '../../config' ) ;
var PROBE _VERSION = 'AR-DIAG-20261102-05' ;
var CALIBRATION _FORMATS = [ 'RGBA' , 'BGRA' , 'ARGB' , 'ABGR' ] ;
function confirmedPixelFormat ( ) {
try {
var format = image . confirmedFramePixelFormat ( ) ;
return CALIBRATION _FORMATS . indexOf ( format ) >= 0 ? format : '' ;
} catch ( _ ) { return '' ; }
}
Page ( {
data : { } ,
data : { running : false , busy : false , exporting : false , devicePosition : config . arFrontPosition === 'back' ? 'back' : 'front' , cameraSession : 0 , status : '请点击开启,验证淘宝真实 AR 帧与照片导出。' ,
probeVersion : PROBE _VERSION , toolbarTop : 112 , environmentText : '' , capabilityText : '' , contextText : '等待 onInit 后读取公开方法名' , eventText : '尚未收到原生事件' , copyStatus : '' , diagnosticText : '' ,
target : 'front' , frameCount : 0 , standardFrameCount : 0 , frameText : '尚未收到帧事件' ,
calibrationItems : [ ] , calibrationReady : false , calibrationText : '' , calibrationConfirmed : '' ,
poseText : '' , fieldText : '' , errorText : '' , snapshotUrl : '' , snapshotText : '' , compressWidth : 1 , compressHeight : 1 } ,
onLoad : function ( ) {
this . alive = true ;
this . alive = true ; this . visible = true ; this . sequence = 0 ;
var capability = AR . capability ( my ) ;
this . diagnosticSystem = AR . diagnostics . system ( my ) ; this . contextMethodNames = [ ] ; this . nativeEventStats = { } ;
this . diagnosticCapability = 'ar-camera=' + capability . componentSignal + '; createArCameraContext=' + capability . contextFactory ;
this . setData ( { toolbarTop : AR . diagnostics . toolbarTop ( my ) , calibrationConfirmed : confirmedPixelFormat ( ) , environmentText : this . diagnosticSystem . join ( '\n' ) || '系统未返回可读取的设备/宿主版本;请手动补充淘宝版本。' ,
capabilityText : 'ar-camera 声明:' + capability . componentSignal + ';上下文工厂:' + ( capability . contextFactory ? '存在' : '不存在' ) + '。声明结果不代表真机已支持。' } ) ;
} ,
onUnload : function ( ) { this . alive = false ; } ,
onError : function ( err ) {
my . showToast ( { content : '相机组件报错: ' + JSON . stringify ( err || { } ) , type : 'none' , duration : 6000 } ) ;
}
onShow : function ( ) { this . visible = true ; } ,
flushFrameDiagnostic : function ( ) {
var pending = this . pendingFrameDiagnostic ;
if ( ! pending ) return ;
this . pendingFrameDiagnostic = null ;
var record = this . nativeEventStats [ pending . key ] , summary = AR . diagnostics . event ( pending . event ) ;
record . type = summary . type || '(未提供 type)' ; record . metadata = summary . metadata ;
this . lastDiagnosticMetadata = summary . metadata ;
} ,
nativeEventView : function ( ) {
var that = this ;
return { eventText : Object . keys ( this . nativeEventStats ) . map ( function ( item ) {
var value = that . nativeEventStats [ item ] ;
return item + ' · type=' + value . type + ' · 次数 ' + value . count ;
} ) . join ( '\n' ) , fieldText : ( this . lastDiagnosticMetadata || [ ] ) . join ( '\n' ) || '本事件未返回白名单元信息;不会输出未知字段值或人脸坐标。' } ;
} ,
recordNativeEvent : function ( name , event , deferFrame ) {
var key = name + ':request=' + this . data . devicePosition ;
var record = this . nativeEventStats [ key ] || { count : 0 , type : '(未提供 type)' , metadata : [ ] } ;
record . count ++ ;
this . nativeEventStats [ key ] = record ;
// Count every frame in JS, but do not scan descriptors or cross the UI
// bridge until the 400ms refresh. Only the latest frame is retained.
if ( deferFrame ) { this . pendingFrameDiagnostic = { key : key , event : event } ; return ; }
this . flushFrameDiagnostic ( ) ;
var summary = AR . diagnostics . event ( event ) ;
record . type = summary . type || '(未提供 type)' ; record . metadata = summary . metadata ;
this . lastDiagnosticMetadata = summary . metadata ;
this . setData ( this . nativeEventView ( ) ) ;
return summary ;
} ,
diagnosticReport : function ( ) {
// A user-requested copy gets up-to-date counters and last-frame metadata
// without introducing another UI refresh on the high-rate native stream.
this . flushFrameDiagnostic ( ) ;
var that = this , lines = [ PROBE _VERSION ] . concat ( this . diagnosticSystem || [ ] ) ;
lines . push ( this . diagnosticCapability || 'capability=unavailable' ) ;
lines . push ( 'requestedPosition=' + this . data . devicePosition + '; actualPosition=需核对实际画面' ) ;
lines . push ( 'context.methods=' + ( ( this . contextMethodNames || [ ] ) . join ( ', ' ) || '(none observed)' ) ) ;
lines . push ( 'pixelFormat.default=' + ( CALIBRATION _FORMATS . indexOf ( config . arPixelFormat ) >= 0 ? config . arPixelFormat : '(none)' ) + '; defaultIsUserConfirmed=false' ) ;
lines . push ( 'pixelFormat.confirmed=' + ( confirmedPixelFormat ( ) || '(unconfirmed)' ) ) ;
lines . push ( 'currentSession.frameEvents=' + ( this . frames || 0 ) + '; parsableFrames=' + ( this . standardFrames || 0 ) ) ;
Object . keys ( this . nativeEventStats || { } ) . forEach ( function ( key ) {
var record = that . nativeEventStats [ key ] ;
lines . push ( key + '; type=' + record . type + '; count=' + record . count ) ;
record . metadata . forEach ( function ( item ) { lines . push ( ' ' + item ) ; } ) ;
} ) ;
return lines . join ( '\n' ) ;
} ,
onCopyDiagnostics : function ( ) {
if ( ! this . alive ) return ;
// Always reveal the same privacy-filtered text first, so clipboard failure
// or an absent API still leaves a native selectable-text fallback.
var report = this . diagnosticReport ( ) ;
this . setData ( { diagnosticText : report , copyStatus : '安全诊断已生成;下方文字可长按复制。' } ) ;
if ( typeof my . setClipboard !== 'function' ) { this . setData ( { copyStatus : '当前淘宝未提供剪贴板 API,请长按下方诊断文字复制。' } ) ; return ; }
var that = this ;
try {
my . setClipboard ( { text : report , success : function ( ) { if ( that . alive ) that . setData ( { copyStatus : '诊断信息已复制。也可长按下方文字复制。' } ) ; } ,
fail : function ( ) { if ( that . alive ) that . setData ( { copyStatus : '自动复制失败,请长按下方诊断文字复制。' } ) ; } } ) ;
} catch ( _ ) { this . setData ( { copyStatus : '自动复制失败,请长按下方诊断文字复制。' } ) ; }
} ,
onOpenNativeProbe : function ( ) {
if ( this . data . busy || this . data . exporting ) return ;
my . redirectTo ( { url : '/pages/camnative/camnative' } ) ;
} ,
currentCalibration : function ( batch ) {
return ! ! ( batch && this . calibrationBatch === batch && batch . sequence === this . sequence && this . alive && this . visible && this . data . running ) ;
} ,
invalidateCalibration : function ( ) {
if ( this . calibrationBatch ) this . calibrationBatch . frame = null ;
this . calibrationBatch = null ;
if ( this . alive ) this . setData ( { calibrationItems : [ ] , calibrationReady : false , calibrationText : '' } ) ;
} ,
onCalibrate : function ( ) {
if ( ! this . alive || ! this . visible || ! this . data . running ) return Promise . resolve ( ) ;
if ( this . exportInFlight || this . data . busy ) {
this . setData ( { errorText : '上一批照片仍在导出,请稍候再校验颜色。' } ) ; return Promise . resolve ( ) ;
}
if ( ! this . lastFrame ) { this . setData ( { errorText : '尚未收到真实相机帧,无法校验照片颜色。' } ) ; return Promise . resolve ( ) ; }
var frozen , formats ;
try {
// Freeze once: every preview and the eventual explicit confirmation refer
// to the exact same image, not a later native/reused camera buffer.
frozen = image . freezeCalibrationFrame ( this . lastFrame ) ;
formats = image . frameCalibrationFormats ( frozen ) . filter ( function ( format ) { return CALIBRATION _FORMATS . indexOf ( format ) >= 0 ; } ) ;
} catch ( error ) { this . setData ( { errorText : error . message || '当前帧无法进行本地颜色校验。' } ) ; return Promise . resolve ( ) ; }
if ( ! formats . length ) {
this . setData ( { errorText : '当前帧不是可校验的无格式四通道图像,请等待新帧或直接导出照片。' } ) ; return Promise . resolve ( ) ;
}
this . invalidateCalibration ( ) ;
var that = this , operation = { } , batch = { sequence : this . sequence , id : this . sequence + '-' + ( ( this . calibrationSerial || 0 ) + 1 ) ,
frame : frozen , formats : formats , items : [ ] , settled : false } ;
this . calibrationSerial = ( this . calibrationSerial || 0 ) + 1 ;
this . calibrationBatch = batch ; this . exportInFlight = operation ;
this . setData ( { busy : true , exporting : true , errorText : '' , snapshotUrl : '' , snapshotText : '' ,
calibrationText : '正在按顺序生成本地颜色预览;全部完成后,请选择颜色正常的一张。' } ) ;
return formats . reduce ( function ( chain , format ) {
return chain . then ( function ( ) {
if ( ! that . currentCalibration ( batch ) ) return ;
return image . captureFrameToBase64 ( batch . frame , that , { pixelFormat : format , maxSide : 480 } ) . then ( function ( url ) {
if ( ! that . currentCalibration ( batch ) ) return ;
batch . items . push ( { format : format , url : url , batchId : batch . id } ) ;
that . setData ( { calibrationItems : batch . items . slice ( ) , calibrationText : '已生成 ' + batch . items . length + '/' + batch . formats . length + ' 张,完成后可确认。' } ) ;
} ) ;
} ) ;
} , Promise . resolve ( ) ) . then ( function ( ) {
batch . settled = true ;
if ( that . currentCalibration ( batch ) ) that . setData ( { calibrationReady : true , calibrationText : '请比较肤色和背景,点击颜色正常照片下方的按钮;尚未确认任何新格式。' } ) ;
} ) . catch ( function ( error ) {
batch . settled = true ;
if ( that . currentCalibration ( batch ) ) {
that . invalidateCalibration ( ) ;
that . setData ( { errorText : error . message || '颜色预览导出失败,请重试。' , calibrationText : '本批颜色预览导出失败,未确认新格式,请重试。' } ) ;
}
} ) . then ( function ( ) {
if ( ! that . currentCalibration ( batch ) ) batch . frame = null ;
if ( that . exportInFlight !== operation ) return ;
that . exportInFlight = null ;
if ( that . alive ) {
var patch = { exporting : false } ;
if ( batch . sequence === that . sequence ) patch . busy = false ;
that . setData ( patch ) ;
}
} ) ;
} ,
onConfirmCalibration : function ( event ) {
var batch = this . calibrationBatch , dataset = event && event . currentTarget && event . currentTarget . dataset || { } , format = dataset . format ;
if ( ! this . currentCalibration ( batch ) || ! batch . settled || String ( dataset . batch ) !== batch . id || batch . formats . indexOf ( format ) < 0
|| ! batch . items . some ( function ( item ) { return item . format === format ; } ) ) return ;
try {
var confirmed = image . confirmFramePixelFormat ( batch . frame , format ) ;
if ( confirmed !== format ) throw new Error ( '未能确认相机格式,请重新校验。' ) ;
this . invalidateCalibration ( ) ;
this . setData ( { calibrationConfirmed : confirmed , calibrationText : '本次相机格式已确认:' + confirmed + ',已记录本次颜色选择。' , errorText : '' } ) ;
} catch ( error ) { this . setData ( { errorText : error . message || '格式确认失败,请重新校验。' } ) ; }
} ,
acceptsCameraEvent : function ( event ) {
if ( ! this . alive || ! this . visible || ! this . data . running ) return false ;
var target = event && ( event . currentTarget || event . target ) , dataset = target && target . dataset ;
// Native callbacks from an unmounted camera must not enter a new session.
// Events without a marker remain compatible with older host callbacks.
if ( dataset && dataset . session !== undefined && String ( dataset . session ) !== String ( this . data . cameraSession ) ) return false ;
var id = target && target . id ;
return typeof id !== 'string' || id . indexOf ( 'test-ar-camera-' ) !== 0 || id === 'test-ar-camera-' + this . data . cameraSession ;
} ,
mountCamera : function ( sequence ) {
if ( ! this . alive || ! this . visible || sequence !== this . sequence ) return ;
var that = this ;
clearTimeout ( this . frameTimer ) ;
this . frames = 0 ; this . standardFrames = 0 ; this . lastFrame = null ; this . context = null ; this . lastUpdate = 0 ; this . frameUiShown = false ; this . pendingFrameDiagnostic = null ; this . contextMethodNames = [ ] ;
this . setData ( { running : true , busy : false , cameraSession : sequence , frameCount : 0 , standardFrameCount : 0 , frameText : '尚未收到帧事件' , contextText : '等待 onInit 后读取公开方法名' ,
status : '已请求' + ( this . data . devicePosition === 'front' ? '前置' : '后置' ) + '镜头,等待真实帧;请核对实际画面…' } ) ;
this . frameTimer = setTimeout ( function ( ) {
if ( that . alive && that . visible && sequence === that . sequence && ! that . frames ) that . setData ( { status : '10秒内未收到帧事件(onARFrame)。请复制诊断,不能视为能力已通过。' } ) ;
} , 10000 ) ;
} ,
onStart : function ( ) {
if ( this . data . running || this . data . busy ) return Promise . resolve ( ) ;
var that = this , sequence = ++ this . sequence ;
this . setData ( { busy : true , errorText : '' , snapshotUrl : '' , snapshotText : '' , frameCount : 0 , poseText : '' , fieldText : '' } ) ;
// Camera permission only. Neither Taobao userInfo nor album is needed.
return permissions . ensure ( 'scope.camera' ) . then ( function ( ) {
if ( ! that . alive || ! that . visible || sequence !== that . sequence ) return ;
that . mountCamera ( sequence ) ;
} ) . catch ( function ( error ) {
if ( that . alive && sequence === that . sequence ) that . setData ( { errorText : error . message || '相机授权失败' } ) ;
} ) . then ( function ( ) { if ( that . alive && sequence === that . sequence ) that . setData ( { busy : false } ) ; } ) ;
} ,
onARInit : function ( event ) {
if ( ! this . acceptsCameraEvent ( event ) ) return ;
this . recordNativeEvent ( 'onInit' , event ) ;
try {
this . context = AR . context ( my , 'test-ar-camera-' + this . data . cameraSession ) ;
// Enumerate descriptors only. This diagnostic never probes unknown methods
// or executes property getters/automatic context actions.
this . contextMethodNames = AR . diagnostics . methods ( this . context ) ;
this . setData ( { contextText : this . contextMethodNames . join ( ', ' ) || '未发现可读取的公开方法;不代表原生能力不存在。' , status : 'AR 初始化完成,等待实时人脸数据…' } ) ;
} catch ( error ) { this . setData ( { errorText : error . message || 'AR 上下文初始化失败' } ) ; }
} ,
onARFrame : function ( event ) {
if ( ! this . acceptsCameraEvent ( event ) ) return ;
this . frames = ( this . frames || 0 ) + 1 ; this . lastFrame = event && event . detail || event ;
this . recordNativeEvent ( 'onARFrame' , event , true ) ;
clearTimeout ( this . frameTimer ) ;
// Facing selects hardware, not the native landmark coordinate convention.
// Do not reverse yaw solely because a front camera was requested.
var verdict ;
try { verdict = AR . evaluate ( event , this . data . target , false ) ; } catch ( _ ) { verdict = { frameReceived : false } ; }
if ( verdict . frameReceived ) this . standardFrames = ( this . standardFrames || 0 ) + 1 ;
var now = Date . now ( ) ;
if ( this . frameUiShown && now - this . lastUpdate < 400 ) return ;
this . lastUpdate = now ; this . frameUiShown = true ;
this . flushFrameDiagnostic ( ) ;
var patch = this . nativeEventView ( ) ;
patch . frameCount = this . frames ; patch . standardFrameCount = this . standardFrames || 0 ;
if ( ! verdict . frameReceived ) {
patch . status = '已收到 onARFrame 回调,但格式不符合 width/height/anchors;请复制诊断。' ;
patch . frameText = '已收到原生回调,尚无可解析的标准帧' ; patch . poseText = '' ;
this . setData ( patch ) ;
return ;
}
var observation = verdict . observation ;
patch . status = verdict . ok ? '实时人脸判断通过;仍需点击导出照片验证原始图像。' : verdict . hint ;
patch . frameText = verdict . width + '× ' + verdict . height + ' · 人脸 ' + verdict . faceCount + ' · 已收到真实帧' ;
patch . poseText = observation ? '几何姿态 yaw ' + observation . yaw . toFixed ( 1 ) + '° / pitch ' + observation . pitch . toFixed ( 1 ) + '° / roll ' + observation . roll . toFixed ( 1 ) + '°' : '' ;
this . setData ( patch ) ;
} ,
onTarget : function ( event ) {
var target = event . currentTarget . dataset . target ;
if ( [ 'front' , 'left' , 'right' ] . indexOf ( target ) >= 0 ) this . setData ( { target : target } ) ;
} ,
onFlip : function ( ) {
if ( ! this . alive || ! this . visible || ! this . data . running ) return Promise . resolve ( ) ;
var that = this , sequence = ++ this . sequence ;
clearTimeout ( this . frameTimer ) ;
this . flushFrameDiagnostic ( ) ;
this . invalidateCalibration ( ) ;
this . lastFrame = null ; this . context = null ; this . frames = 0 ; this . lastUpdate = 0 ;
// devicePosition is shared-component compatibility evidence. Its actual
// effect in the Taobao host must be checked on a phone; no context method
// or successful callback is treated as proof that hardware changed.
return new Promise ( function ( resolve ) {
that . setData ( { running : false , busy : true , devicePosition : that . data . devicePosition === 'front' ? 'back' : 'front' ,
cameraSession : sequence , frameCount : 0 , frameText : '尚未收到真实帧' , poseText : '' , fieldText : '' , errorText : '' ,
snapshotUrl : '' , snapshotText : '' , status : '正在重新挂载相机;切换效果以实际画面为准…' } , function ( ) {
that . mountCamera ( sequence ) ;
resolve ( ) ;
} ) ;
} ) ;
} ,
onSnapshot : function ( ) {
// Image decoding/draw callbacks may still use the shared canvas after a
// camera session changes. Release this lock only when that export settles.
if ( this . exportInFlight ) {
this . setData ( { errorText : '上一帧照片仍在导出,请稍候再试。' } ) ;
return Promise . resolve ( ) ;
}
if ( ! this . lastFrame || this . data . busy ) {
if ( ! this . lastFrame ) this . setData ( { errorText : '尚未收到真实相机帧,无法验证照片导出。' } ) ;
return Promise . resolve ( ) ;
}
var that = this , sequence = this . sequence , operation = { } ;
this . exportInFlight = operation ;
this . setData ( { busy : true , exporting : true , errorText : '' , snapshotUrl : '' , snapshotText : '' } ) ;
var work ;
try { work = image . captureFrameToBase64 ( this . lastFrame , this , { defaultPixelFormat : config . arPixelFormat } ) ; } catch ( error ) { work = Promise . reject ( error ) ; }
return Promise . resolve ( work ) . then ( function ( value ) {
if ( ! that . alive || ! that . visible || sequence !== that . sequence ) return ;
that . setData ( { snapshotUrl : value , snapshotText : 'JPEG ' + that . data . compressWidth + '× ' + that . data . compressHeight + ',仅本页预览,未上传、未保存到相册。' , status : '已导出真实帧照片,请核对方向和颜色。' } ) ;
} ) . catch ( function ( error ) {
if ( that . alive && that . visible && sequence === that . sequence ) that . setData ( { errorText : error . message || '照片导出失败,请记录帧字段与实际错误。' } ) ;
} ) . then ( function ( ) {
if ( that . exportInFlight !== operation ) return ;
that . exportInFlight = null ;
if ( that . alive ) {
var patch = { exporting : false } ;
if ( sequence === that . sequence ) patch . busy = false ;
that . setData ( patch ) ;
}
} ) ;
} ,
onARStop : function ( event ) {
if ( ! this . acceptsCameraEvent ( event ) ) return ;
this . recordNativeEvent ( 'onStop' , event ) ;
this . invalidateCalibration ( ) ;
this . sequence ++ ;
this . setData ( { status : 'AR相机已停止。请重新开启后验证。' , running : false , busy : false } ) ;
this . lastFrame = null ; this . context = null ; clearTimeout ( this . frameTimer ) ;
} ,
onError : function ( event ) {
if ( ! this . acceptsCameraEvent ( event ) ) return ;
var summary = this . recordNativeEvent ( 'onError' , event ) ;
this . invalidateCalibration ( ) ;
this . sequence ++ ;
this . lastFrame = null ; this . context = null ; clearTimeout ( this . frameTimer ) ;
this . setData ( { running : false , busy : false , status : 'AR 相机未通过真机验证。' , errorText : '收到 AR 错误事件。' + ( summary . metadata . join ( '; ' ) || '未返回可安全展示的错误码,请复制诊断。' ) } ) ;
} ,
onStop : function ( ) {
this . flushFrameDiagnostic ( ) ;
this . invalidateCalibration ( ) ;
this . sequence ++ ; this . lastFrame = null ; this . context = null ; clearTimeout ( this . frameTimer ) ;
if ( this . alive ) this . setData ( { running : false , busy : false , status : '已停止,帧缓存已清空。' } ) ;
} ,
onHide : function ( ) {
this . visible = false ; this . onStop ( ) ;
if ( this . alive ) this . setData ( { snapshotUrl : '' , snapshotText : '' , poseText : '' , fieldText : '' } ) ;
} ,
onUnload : function ( ) { this . onHide ( ) ; this . alive = false ; }
} ) ;