HarmonyOS AR Engine常见问题排查实操——从空平面保护到会话重建

这次做了一个可以直接复现问题的 AR 稳定性诊断台。镜头正常扫描木地板时,页面显示 TRACKING、平面数量和边界顶点;遮住摄像头后,状态变成 PAUSED,原因是 INSUFFICIENT_FEATURES;快速移动手机时,又真实触发了 EXCESSIVE_MOTION。
镜头重新看到原来的环境后,页面会记录暂停了多久,以及恢复前后的位移和转角差。如果恢复结果不可信,还可以直接销毁旧会话并创建一个全新的 WORLD 会话。
正常扫描时,真机取得了 1 个跟踪平面和 11 个边界顶点。识别到平面之前,平面列表连续为空 4 次,页面只增加“空保护”计数,没有读取不存在的对象:

完整遮挡镜头后,真机暂停了 10162 ms。重新对准木地板时,会话恢复为 TRACKING,恢复前后的位移差为 0.692 m,转角差为 50.1°:

本次结果
| 验证项目 | 真机结果 |
|---|---|
| 设备 | HUAWEI Mate 60 Pro |
| 系统 | HarmonyOS 7.0 |
| SDK | API 26 |
| CAMERA 权限 | 已授予 |
| SLAM 能力 | 支持 |
| 正常平面 | 1 个 |
| 初次有效边界 | 11 个顶点 |
| 空平面保护 | 4 次,无异常 |
| 镜头遮挡原因 | INSUFFICIENT_FEATURES |
| 遮挡恢复 | 10162 ms / 0.692 m / 50.1° |
| 快速移动原因 | EXCESSIVE_MOTION |
| 快速移动恢复 | 1003 ms / 0.188 m / 34.3° |
| 一键重建 | 旧上下文销毁成功,新会话 22 ms 初始化 |
| 重建后结果 | 重新进入 TRACKING,取得 1 个平面 |
| 最终释放 | 暂停、恢复、销毁均成功 |
实验准备
本次需要:
- 一台支持
ARENGINE_FEATURE_TYPE_SLAM的 HarmonyOS 真机; - CAMERA 权限;
- 一块纹理明显、光线正常的地面;
- 一面纯色墙或可以完整遮挡摄像头的手掌;
- 手机周围留出安全活动空间,用于短距离快速移动测试。
磁铁、长时间高负载和故意让手机发烫都不在实验范围内。官方提到这些因素可能造成漂移或位姿跳变,但没有必要为了文章主动制造不安全条件。
诊断台要看哪些数据
这次没有把所有 AR 数据都铺在页面上,只保留能判断问题的五段链路:

- 先读相机的跟踪状态;
- 暂停时再看真实原因;
- 平面列表为空就安全跳过;
- 恢复后计算前后 Pose 差值;
- 需要时销毁旧会话并重新初始化。
页面顶部显示相机状态、平面数、Polygon 顶点、空列表保护次数和恢复次数。底部保存最近一次暂停时长、恢复位移和恢复转角。
进入前检查 CAMERA 和 SLAM
诊断页使用 WORLD 会话。入口先检查相机权限和 SLAM 能力,任一条件不满足都不创建 ARViewContext:
private startARStabilityDiagnostic(): void {
if (!this.cameraGranted) {
this.latestMessage =
'未获得相机权限,不启动诊断';
return;
}
const supported: boolean =
arViewController.isARTypeSupported(
arEngine.ARFeatureType
.ARENGINE_FEATURE_TYPE_SLAM);
if (!supported) {
this.latestMessage =
'当前设备不支持 SLAM';
return;
}
this.showARStabilityDiagnostic = true;
}
本次真机门禁通过:
STABILITY_GUARD camera=true slam=true
创建只包含必要能力的 WORLD 会话
平面诊断需要运动跟踪和水平/垂直平面。语义、深度和 Mesh 在本次实验中关闭,避免其他模型干扰结果:
const scene: Scene = await Scene.load();
const context =
new arViewController.ARViewContext();
context.scene = scene;
context.callback = this.callback;
context.config = {
type: arEngine.ARType.WORLD,
planeFindingMode:
arEngine.ARPlaneFindingMode
.HORIZONTAL_AND_VERTICAL,
powerMode: arEngine.ARPowerMode.NORMAL,
semanticMode: arEngine.ARSemanticMode.NONE,
poseMode: arEngine.ARPoseMode.GRAVITY,
depthMode: arEngine.ARDepthMode.DISABLED,
meshMode: arEngine.ARMeshMode.DISABLED,
focusMode: arEngine.ARFocusMode.AUTO
};
await context.init();
this.arContext = context;
首轮初始化用了 31 ms:
STABILITY_SESSION_INIT trigger=INIT
costMs=31 restart=0
先看 state,再解释 stateReason
每一帧从相机对象读取 state 和 stateReason:
frame = session.getFrame();
const camera: arEngine.ARCamera =
frame.getCamera();
const state = camera.state;
const reason = camera.stateReason;
if (state ===
arEngine.ARTrackingState.TRACKING) {
// 读取真实 Pose 和平面
} else if (state ===
arEngine.ARTrackingState.PAUSED) {
// 显示暂停原因,等待恢复
}
原因只负责解释当前状态:
private reasonText(
reason: arEngine.ARTrackingStateReason
): string {
if (reason ===
arEngine.ARTrackingStateReason
.EXCESSIVE_MOTION) {
return '设备移动过快';
}
if (reason ===
arEngine.ARTrackingStateReason
.INSUFFICIENT_FEATURES) {
return '纹理、光线或视觉特征不足';
}
return '系统正在建立或恢复跟踪';
}
真机中还出现了一个值得保留的细节:状态短暂保持 TRACKING 时,stateReason 已经先变成 1;约 0.5 秒后,状态才正式进入 PAUSED。
因此本案例只在 state === PAUSED 时记为跟踪中断。不能看到 reason=1 就直接把页面改成暂停,否则会把过渡帧当成失败。
平面为空时不要继续读取
会话刚进入 TRACKING 时,平面列表通常还是空的。本次首轮连续出现 4 次空列表:
STABILITY_PLANE_GUARD_EMPTY
sample=19 guardCount=1
action=skip_index_and_polygon
STABILITY_PLANE_GUARD_EMPTY
sample=24 guardCount=2
action=skip_index_and_polygon
处理方式是先检查数组长度。为空就更新页面并返回,不读取 trackables[0],也不调用 Polygon 接口:
trackables = session.getAllTrackables(
arEngine.ARTrackableType.PLANE);
if (trackables.length === 0) {
this.trackedPlanes = 0;
this.polygonVertexCount = 0;
this.emptyPlaneGuardCount += 1;
return;
}
官方 FAQ 中的 plane is nullptr / 401 是 C API 场景。ArkTS 侧不需要强行制造相同错误码,但根因相同:还没有取得有效平面,就继续读取具体对象。
把 Polygon 的 ArrayBuffer 转成坐标
只有类型为 PLANE、状态为 TRACKING 的对象才读取边界:
const plane: arEngine.ARPlane =
trackable as arEngine.ARPlane;
const polygonBuffer: ArrayBuffer =
plane.getPolygonXZ();
const values: Float32Array =
new Float32Array(polygonBuffer);
边界按 X, Z, X, Z... 排列,所以两个 Float32 数值组成一个顶点。正式计数前继续检查长度和有限值:
private isValidPolygon(
values: Float32Array
): boolean {
if (values.length < 6 ||
values.length % 2 !== 0) {
return false;
}
for (let index: number = 0;
index < values.length;
index += 1) {
if (!Number.isFinite(values[index])) {
return false;
}
}
return true;
}
const vertexCount: number =
Math.floor(values.length / 2);
真机首次取得的有效结果是:
STABILITY_PLANE_VALID sample=39
total=1 tracking=1 vertices=11 invalid=0
记录暂停前的最后一组 Pose
从 TRACKING 进入非跟踪状态时,保存当前时间和最后一组稳定 Pose:
if (previousState ===
arEngine.ARTrackingState.TRACKING &&
state !==
arEngine.ARTrackingState.TRACKING) {
this.pauseStartedAt = Date.now();
this.pauseReason = reason;
this.pauseReferencePose =
this.latestStablePose;
}
这里同时保存了“最初导致暂停的原因”。实测遮挡期间,原因先是 INSUFFICIENT_FEATURES,随后又变成 NONE。如果每帧都覆盖原因,恢复时就只剩下 NONE,真正的触发原因反而丢了。
恢复时计算位移和转角差
状态重新回到 TRACKING 后,取恢复后的第一组 Pose,与暂停前的 Pose 比较:
const dx = current.x - reference.x;
const dy = current.y - reference.y;
const dz = current.z - reference.z;
const distance = Math.sqrt(
dx * dx + dy * dy + dz * dz);
const dot = Math.abs(
currentRotation.x * referenceRotation.x +
currentRotation.y * referenceRotation.y +
currentRotation.z * referenceRotation.z +
currentRotation.w * referenceRotation.w);
const normalizedDot =
Math.min(1, Math.max(0, dot));
const rotation =
2 * Math.acos(normalizedDot) *
180 / Math.PI;
第一次遮挡实验的完整状态变化如下:
STABILITY_PAUSE_START sample=97
state=1 reason=2
reasonText=INSUFFICIENT_FEATURES
STABILITY_TRACKING sample=185
state=0 stateText=TRACKING reason=0
STABILITY_RECOVERY count=1
durationMs=10162 reason=2
hasReference=true
distance=0.692 rotation=50.1
这组数字不是手机真实移动距离的计量结果,而是 AR 会话在暂停前后返回的坐标差。它的用途是判断恢复后坐标是否发生了明显跳变。
快速移动测试
保持镜头对准木地板,短距离快速左右移动手机,随后停住等待恢复。本次真实触发了 EXCESSIVE_MOTION:
STABILITY_PAUSE_START sample=173
state=1 reason=1
reasonText=EXCESSIVE_MOTION
STABILITY_RECOVERY count=1
durationMs=1003 reason=1
distance=0.188 rotation=34.3
STABILITY_RECOVERY count=2
durationMs=1704 reason=1
distance=0.118 rotation=20.3
STABILITY_RECOVERY count=3
durationMs=8279 reason=1
distance=0.117 rotation=18.2
本轮没有把快速移动本身的普通 Pose 变化写成故障。只有状态真正进入 PAUSED,并且随后再次回到 TRACKING,才增加一次恢复计数。
一键销毁并重建会话
如果遮挡恢复后叠加物明显错位,或位姿差超出业务可以接受的范围,可以重新初始化会话。
页面的“重建会话”先销毁旧上下文,再清空旧 Pose、平面和暂停状态,最后创建新会话:
private async restartARView(): Promise<void> {
if (!this.arContext ||
this.isRestarting) {
return;
}
this.isRestarting = true;
this.restartCount += 1;
const oldContext = this.arContext;
this.arContext = undefined;
await oldContext.destroy();
this.resetRuntimeState();
await this.initARView('RESTART');
this.isRestarting = false;
}
真机执行结果:
STABILITY_SESSION_RESTART_BEGIN
restart=1 sample=166 recoveries=0
STABILITY_CONTEXT_DESTROY
action=RESTART success=true
STABILITY_SESSION_INIT
trigger=RESTART costMs=22 restart=1
STABILITY_SESSION_RESTART_END
restart=1 success=true
重建后新会话重新进入 TRACKING。识别平面之前,空列表保护再次生效;随后取得 1 个平面和 8 个边界顶点:

实操步骤
1. 建立正常基线
进入诊断页后,把镜头缓慢扫向木地板。页面先显示空保护计数,随后出现“平面链路正常”。本次基线为 1 个平面、11 个顶点。
2. 遮挡镜头
用手完整遮住后置摄像头约 3 秒,然后移开手并重新对准木地板。真机先返回 INSUFFICIENT_FEATURES,恢复时记录 10162 ms、0.692 m 和 50.1°。
3. 快速移动
在安全范围内短距离快速左右移动手机 3~5 次,随后停住。真机返回 EXCESSIVE_MOTION,并完成多次暂停—恢复。
4. 重建会话
点击“重建会话”。旧相机上下文销毁后,页面回到建立环境坐标状态;继续扫描地面,新会话再次进入 TRACKING 并得到平面。
5. 验证生命周期
先点击“暂停”,等待数秒,再点击“恢复”,最后退出页面。本轮暂停和恢复时采样数都为 100,证明暂停期间没有继续采样:
STABILITY_SESSION_PAUSE
sample=100 state=0 planes=1
STABILITY_SESSION_RESUME
sample=100 state=0 planes=1
STABILITY_CONTEXT_DESTROY
action=EXIT success=true
STABILITY_SESSION_DESTROY
samples=100 recoveries=1
emptyGuards=9 maxPlanes=1 restarts=1
实验中遇到的情况
stateReason 会先于状态变化
快速移动时,个别帧仍是 TRACKING,但原因已经变成 EXCESSIVE_MOTION。约半秒后状态才进入 PAUSED。最终逻辑以 state 判断是否暂停,以 stateReason 解释原因。
暂停原因会从具体原因变回 NONE
遮挡后先返回 INSUFFICIENT_FEATURES,恢复过程中又返回 NONE。页面在第一次离开 TRACKING 时保存触发原因,避免后续帧覆盖。
入场阶段没有平面是正常状态
新会话进入 TRACKING 不代表平面已经生成。空数组需要跳过,而不是直接读取第一个元素。首轮和重建后的真机日志都验证了这条保护。
重建必须先销毁旧上下文
只清空页面数值并不能重置 AR 坐标系。真正的重建需要等待旧 ARViewContext.destroy() 完成,再创建新的 Scene、Context 和会话配置。
资源释放
每轮帧读取结束后释放 Pose、全部 Trackable 和 Frame:
if (pose) {
await pose.release();
}
for (const trackable of trackables) {
await trackable.release();
}
if (frame) {
await frame.release();
}
页面退出前销毁 ARViewContext,防止相机继续占用。暂停、恢复、重建和退出都带重复调用保护。
最终结果
这次实操把 AR Engine FAQ 中几类容易混在一起的问题拆成了可以观察的数据:
- 没有平面时,空数组保护可以避免继续读取无效对象;
- 纯色、遮挡或视觉信息不足时,真机返回
INSUFFICIENT_FEATURES; - 快速移动时,真机返回
EXCESSIVE_MOTION; - 遮挡恢复后,前后 Pose 差可以直接显示坐标跳变量;
- 对恢复结果不放心时,可以销毁旧上下文并创建新会话;
- 暂停、恢复和退出后,采样与相机资源都按预期收口。
磁场干扰、设备发热和重复纹理造成的漂移没有在本次实验中主动制造,因此不写成已经复现。诊断台验证的是状态读取、防空、恢复差值和会话重建这条完整链路。
参考资料
更多推荐

所有评论(0)