HarmonyOS鸿蒙Next中XComponentSurface在NDK层使用后就无法进行摄像头预览

HarmonyOS鸿蒙Next中XComponentSurface在NDK层使用后就无法进行摄像头预览 正常情况:

创建一个XComponentSurface,打开摄像头预览,预览画面一切正常。

错误情况:

创建一个XComponentSurface,先在NDK层对XComponentSurface调用NativeWindowRequestBuffer函数后,再打开摄像头预览,预览画面没有出现,Hilog报错:

{OnError():47} CameraDeviceServiceCallback::OnError() is called!, errorType: 11, errorMsg: 0

请问是哪里有问题?

demo下载地址:https://gitee.com/chen_yi_ze/test-harmony-camera.git

设备:Nova 12 pro,系统:6.1.0,


更多关于HarmonyOS鸿蒙Next中XComponentSurface在NDK层使用后就无法进行摄像头预览的实战教程也可以访问 https://www.itying.com/category-93-b0.html

13 回复

尊敬的开发者,您好,

在预览场景下,XComponent的Surface是图像数据缓冲区的抽象概念,相机将预览流数据写入Surface,XComponent从Surface读取数据并显示。在问题所述场景中,XComponent消费Surface缓冲区数据之前,缓冲区内的数据已经被其它接口消费掉了,导致XComponent消费失败,无法渲染预览画面。原理可以参考:实现原理

其次,不同的Surface需要设置成不同的SurfaceID,若XComponent和ImageReceiver重用了一个重用了Surface,便会产生影响。

更多关于HarmonyOS鸿蒙Next中XComponentSurface在NDK层使用后就无法进行摄像头预览的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


我觉得你说的不对。

1、ImageReceiver是用的自己内部的Surface,和UI界面上的XComponent完全没有关系,我也没有把双路预览重用一个Surface。

2、同一个Surface,如果先给相机渲染预览画面正常,用完后,再给NDK层渲染画面还是正常的。

3、同一个Surface,如果先给NDK层渲染画面正常,用完后,再给相机渲染预览画面就报错了。

请问第3种情况,我要怎么做才能正常?是需要做什么清理工作吗?还是说根本无法实现?还是说是系统BUG?

如果你不懂,请更换更高级的技术支持来解答,谢谢。

官方文档通常不会直接写“Surface 使用权冲突”这几个字,可以从 API 角色来说明:Camera 预览是把 surfaceId 作为 createPreviewOutput 的输出目标;XComponent/NativeWindow 自绘则是应用侧通过 NativeWindowRequestBuffer 取 buffer 自己写入。同一个 Surface/BufferQueue 同时给 Camera 连续输出、又被应用侧当自绘缓冲消费,容易破坏生产者/消费者关系。

如果既要显示预览又要取帧,建议按双路输出设计:XComponent Surface 只负责预览显示,ImageReceiver/类似取帧 Surface 负责算法处理。这样更接近官方 Camera 预览 + 图像接收的组合用法,也比复用一个 XComponentSurface 稳。

我取帧是从ImageReceiver这里取的,但是我surface是复用一个,但是不是同时的,我预览的时候我就只预览,预览完了,我才做别的显示,别的显示完了,我又切过来做预览。

我是用ArkGraphics在摄像头上加载3d模型的,参考一下:

cke_5396.jpeg

@ComponentV2
struct ARWorld {
  scene: scene3d.Scene | null = null;
  cam: Camera | null = null;
  node: Node | null | undefined = null;
  @Local arContext?: arViewController.ARViewContext = undefined;
  @Local bodyInfos: BodyInfo[] = [];
  @Local displayWidth: number = display.getDefaultDisplaySync().width;
  @Local displayHeight: number = display.getDefaultDisplaySync().height;
  @Local isFrontCamera: boolean = true;
  private params: arEngine.ARConfig = { type: arEngine.ARType.BODY };
  private uiContext: UIContext | null = null;
  private callbackImpl: ARViewCallbackImpl = new ARViewCallbackImpl();
  private currentModelIndex: number = 0;
  private allModels: Node[] = [];
  private onBodyInfoCb: OnBodyInfoCallback = (bodyInfos: arEngine.ARBody[]) => {
    if (display.getDefaultDisplaySync().width * 3 < display.getDefaultDisplaySync().height * 4) {
      this.displayWidth = display.getDefaultDisplaySync().width;
      this.displayHeight = this.displayWidth * DEFAULT_CONVERT_FACTOR;
    } else {
      this.displayHeight = display.getDefaultDisplaySync().height;
      this.displayWidth = this.displayHeight * DEFAULT_CONVERT_FACTOR;
    }

    this.bodyInfos = bodyInfos.map((value: arEngine.ARBody) => {
      let landmarks: arEngine.ARBodyLandmark2D[] = value.getLandmarks2D();
      landmarks.forEach((value: arEngine.ARBodyLandmark2D) => {
        value.x = value.x * this.displayWidth;
        value.y = value.y * this.displayHeight;
      })
      let info: BodyInfo = {
        trackId: value.trackId,
        landmarks: landmarks
      }
      return info;
    })
  }

  aboutToAppear() {
    this.uiContext = this.getUIContext();
  }

  aboutToDisappear() {
    this.stopARView();
  }

  private async initARView(): Promise<void> {
    if (this.scene !== null) {
      return;
    }

    const resource = $rawfile('ArkGraphics/assets/default.scene');

    scene3d.Scene.load(resource).then(async (result: Scene) => {
      this.scene = result;

      this.cam = result.root?.getNodeByPath("Perspective Camera") as Camera;
      if (this.cam) {
        this.cam.position.z = 5;
        this.cam.enabled = true;
      }

      let sceneLight = result.root?.getNodeByPath("Directional Light") as Light;
      if (sceneLight) {
        sceneLight.enabled = true;
        sceneLight.color = { r: 1.0, g: 1.0, b: 1.0, a: 1.0 };
        sceneLight.intensity = 1.5
        sceneLight.rotation = {
          x: 0.0,
          y: 1.0,
          z: 0.0,
          w: 0.0
        };
      }

      this.allModels = [];
      for (const name of MODEL_LIST) {
        const modelNode = result.root?.getNodeByPath(name);
        if (modelNode) {
          modelNode.visible = false;
          this.allModels.push(modelNode);
        }
      }

      this.node = this.allModels[this.currentModelIndex] ?? null;
      if (this.node) {
        this.node.visible = true;
      }

      this.scene.environment.backgroundType = EnvironmentBackgroundType.BACKGROUND_NONE;

      let viewContext: arViewController.ARViewContext = new arViewController.ARViewContext();
      viewContext.scene = result;

      this.callbackImpl = new ARViewCallbackImpl();
      this.callbackImpl.setCallback(this.onBodyInfoCb);
      this.callbackImpl.setIsFrontCamera(this.isFrontCamera);
      this.callbackImpl.setTargetNode(this.node);

      viewContext.callback = this.callbackImpl;
      viewContext.config = {
        type: arEngine.ARType.BODY,
        planeFindingMode: arEngine.ARPlaneFindingMode.DISABLED,
        powerMode: arEngine.ARPowerMode.NORMAL,
        semanticMode: arEngine.ARSemanticMode.NONE,
        poseMode: arEngine.ARPoseMode.GRAVITY,
        depthMode: arEngine.ARDepthMode.DISABLED,
        meshMode: arEngine.ARMeshMode.DISABLED,
        focusMode: arEngine.ARFocusMode.AUTO,
        maxDetectedBodyNum: this.params?.maxDetectedBodyNum ?? 1,
        cameraLensFacing: this.isFrontCamera ? 1 : 0
      };

      viewContext.init().then(() => {
        this.arContext = viewContext;
        console.info('Succeeded in initializing ARView.');
      }).catch((err: BusinessError) => {
        console.error(`Failed to init ARView. Code is ${err.code}, message is ${err.message}.`);
        this.scene?.destroy();
        this.scene = null;
        this.cam = null;
        this.node = null;
        this.allModels = [];
        this.arContext = undefined;
      });

      console.info(`AR scene loaded, active model: ${MODEL_LIST[this.currentModelIndex]}`);
    }).catch((reason: string) => {
      console.error(`init error: ${reason}`);
    });
  }

  private toggleCamera(): void {
    this.stopARView();
    this.arContext = undefined;
    this.isFrontCamera = !this.isFrontCamera;
    this.scene = null;
    this.initARView();
  }

  private stopARView(): void {
    if (!this.arContext) {
      return;
    }
    try {
      this.arContext.destroy();
      this.arContext = undefined;
      if (this.scene) {
        this.scene.destroy();
        this.scene = null;
      }
      this.cam = null;
      this.node = null;
      this.allModels = [];
      this.bodyInfos = [];
    } catch (error) {
      const err: BusinessError = error as BusinessError;
      console.error(`Failed to stop context. Code is ${err.code}, message is ${err.message}`);
    }
  }

  private pauseARView(): void {
    if (!this.arContext) {
      return;
    }
    try {
      this.arContext.pause();
    } catch (error) {
      const err: BusinessError = error as BusinessError;
      console.error(`Failed to pause context. Code is ${err.code}, message is ${err.message}.`);
    }
  }

  private resumeARView(): void {
    if (!this.arContext) {
      return;
    }
    try {
      this.arContext.resume();
    } catch (error) {
      const err: BusinessError = error as BusinessError;
      console.error(`Failed to resume context. Code is ${err.code}, message is ${err.message}.`);
    }
  }

  build() {
    NavDestination() {
      Stack() {
        Column() {
          RelativeContainer() {
            if (this.arContext) {
              ARView({ context: this.arContext })
                .height(FULL_SCREEN_SIZE)
                .width(FULL_SCREEN_SIZE)
                .alignRules({
                  center: { anchor: '__container__', align: VerticalAlign.Center },
                  middle: { anchor: '__container__', align: HorizontalAlign.Center }
                })
            }
          }
        }
        .width(this.uiContext ? this.uiContext.px2vp(this.displayWidth) : FULL_SCREEN_SIZE)
        .height(this.uiContext ? this.uiContext.px2vp(this.displayHeight) : FULL_SCREEN_SIZE)
        .justifyContent(FlexAlign.Center)
        .alignItems(HorizontalAlign.Center)

        if (!this.bodyInfos.length) {
          Column() {
            SymbolGlyph($r('sys.symbol.person_fill'))
              .fontSize(52)
              .renderingStrategy(SymbolRenderingStrategy.SINGLE)
              .fontColor([Color.White])
              .opacity(0.5)
            Text('请将全身置于摄像头前')
              .fontSize(16)
              .fontColor('#FFFFFF')
              .opacity(0.5)
              .margin({ top: 16 })
          }
          .position({ x: '50%', y: '40%' })
          .translate({ x: '-50%', y: '-50%' })
        }

        Button() {
          Column() {
            SymbolGlyph($r('sys.symbol.camera_fill'))
              .renderingStrategy(SymbolRenderingStrategy.SINGLE)
              .fontSize(24)
              .fontColor([Color.White])
            Text(this.isFrontCamera ? '前置' : '后置')
              .fontSize(11)
              .fontColor('#FFFFFF')
              .margin({ top: 6 })
          }
        }
        .width(68)
        .height(68)
        .borderRadius(34)
        .backgroundColor('#66000000')
        .shadow({ radius: 16, color: '#40000000', offsetX: 0, offsetY: 6 })
        .position({ x: '50%', y: '92%' })
        .translate({ x: '-50%', y: '-50%' })
        .onClick(() => {
          this.toggleCamera();
        })
      }
      .width(FULL_SCREEN_SIZE)
      .height(FULL_SCREEN_SIZE)
    }
    .onAppear(() => {
    })
    .onWillDisappear(() => {
      this.stopARView();
    })
    .onShown(() => {
      this.resumeARView();
    })
    .onHidden(() => {
      this.pauseARView();
    })
    .onReady(ctx => {
      if (ctx.pathInfo && ctx.pathInfo.param) {
        const routeParams = ctx.pathInfo.param as Record<string, Object>;
        const modelIdx = routeParams['modelIndex'] as number;
        if (modelIdx !== undefined && modelIdx >= 0 && modelIdx < MODEL_LIST.length) {
          this.currentModelIndex = modelIdx;
        }
      }
      this.params.type = arEngine.ARType.BODY;
      this.initARView();
    })
    .hideTitleBar(true)
    .hideBackButton(false)
    .hideToolBar(true)
  }

  @Builder
  drawBodyPerception() {
    Shape() {
      ForEach(this.bodyInfos, (bodyInfo: BodyInfo, idx: number) => {
        this.drawBodyBones(arLandmarksToMap(bodyInfo.landmarks));
        this.drawBodyLandmarks(bodyInfo.landmarks);
      })
    }
    .width(this.uiContext ? this.uiContext.px2vp(this.displayWidth) : FULL_SCREEN_SIZE)
    .height(this.uiContext ? this.uiContext.px2vp(this.displayHeight) : FULL_SCREEN_SIZE)
  }

  @Builder
  drawBodyLandmarks(bodyLandmarks: arEngine.ARBodyLandmark2D[]) {
    ForEach(bodyLandmarks, (landmark: arEngine.ARBodyLandmark2D, index: number) => {
      Circle({ width: 4, height: 4 })
        .position({
          x: this.uiContext ? this.uiContext.px2vp(landmark.x) : 0,
          y: this.uiContext ? this.uiContext.px2vp(landmark.y) : 0
        })
        .fillOpacity(1)
        .fill(Color.White)
    })
  }

  @Builder
  drawBodyBones(bodyLandmarks: Map<arEngine.ARBodyLandmarkType, arEngine.ARBodyLandmark2D>) {
    this.drawBodyBoneLine(bodyLandmarks, arEngine.ARBodyLandmarkType.NOSE, arEngine.ARBodyLandmarkType.LEFT_SHOULDER,
      Color.Orange);
    this.drawBodyBoneLine(bodyLandmarks, arEngine.ARBodyLandmarkType.NOSE, arEngine.ARBodyLandmarkType.RIGHT_SHOULDER,
      Color.Orange);
    this.drawBodyBoneLine(bodyLandmarks, arEngine.ARBodyLandmarkType.LEFT_SHOULDER,
      arEngine.ARBodyLandmarkType.RIGHT_SHOULDER, Color.Orange);
    this.drawBodyBoneLine(bodyLandmarks, arEngine.ARBodyLandmarkType.RIGHT_SHOULDER,
      arEngine.ARBodyLandmarkType.RIGHT_HIP, Color.Orange);
    this.drawBodyBoneLine(bodyLandmarks, arEngine.ARBodyLandmarkType.RIGHT_HIP, arEngine.ARBodyLandmarkType.LEFT_HIP,
      Color.Orange);
    this.drawBodyBoneLine(bodyLandmarks, arEngine.ARBodyLandmarkType.LEFT_HIP,
      arEngine.ARBodyLandmarkType.LEFT_SHOULDER, Color.Orange);
    this.drawBodyBoneLine(bodyLandmarks, arEngine.ARBodyLandmarkType.LEFT_SHOULDER,
      arEngine.ARBodyLandmarkType.LEFT_ELBOW, Color.Green);
    this.drawBodyBoneLine(bodyLandmarks, arEngine.ARBodyLandmarkType.LEFT_ELBOW, arEngine.ARBodyLandmarkType.LEFT_WRIST,
      Color.Green);
    this.drawBodyBoneLine(bodyLandmarks, arEngine.ARBodyLandmarkType.LEFT_HIP, arEngine.ARBodyLandmarkType.LEFT_KNEE,
      Color.Green);
    this.drawBodyBoneLine(bodyLandmarks, arEngine.ARBodyLandmarkType.LEFT_KNEE, arEngine.ARBodyLandmarkType.LEFT_ANKLE,
      Color.Green);
    this.drawBodyBoneLine(bodyLandmarks, arEngine.ARBodyLandmarkType.RIGHT_SHOULDER,
      arEngine.ARBodyLandmarkType.RIGHT_ELBOW, Color.Blue);
    this.drawBodyBoneLine(bodyLandmarks, arEngine.ARBodyLandmarkType.RIGHT_ELBOW,
      arEngine.ARBodyLandmarkType.RIGHT_WRIST, Color.Blue);
    this.drawBodyBoneLine(bodyLandmarks, arEngine.ARBodyLandmarkType.RIGHT_HIP, arEngine.ARBodyLandmarkType.RIGHT_KNEE,
      Color.Blue);
    this.drawBodyBoneLine(bodyLandmarks, arEngine.ARBodyLandmarkType.RIGHT_KNEE,
      arEngine.ARBodyLandmarkType.RIGHT_ANKLE, Color.Blue);
  }

  @Builder
  drawBodyBoneLine(bodyLandmarks: Map<arEngine.ARBodyLandmarkType, arEngine.ARBodyLandmark2D>,
    start: arEngine.ARBodyLandmarkType, end: arEngine.ARBodyLandmarkType, color: Color) {
    if (bodyLandmarks.has(start) && bodyLandmarks.has(end) && this.uiContext) {
      Line()
        .startPoint([this.uiContext.px2vp(bodyLandmarks.get(start)?.x),
          this.uiContext.px2vp(bodyLandmarks.get(start)?.y)])
        .endPoint([this.uiContext.px2vp(bodyLandmarks.get(end)?.x), this.uiContext.px2vp(bodyLandmarks.get(end)?.y)])
        .stroke(color)
        .strokeWidth(3)
    }
  }
}

看你代码里有goto,就不分析你代码了。
参考《自定义渲染 (XComponent)》《基于XComponent组件实现图像绘制功能》
对比下步骤,看看是不是落了什么。

你就知道NDK层就是调用了一次NativeWindowRequestBuffer函数,其他都没做。

即使只在预览前调用一次 NativeWindowRequestBuffer,也可能已经把这一路 Surface 先放进应用侧 dequeue/queue 的使用链路了;相机预览输出同样要管理这个 Surface 对应的 BufferQueue,两边混用就容易出现 session 建好了但没有画面的情况。

建议不要让同一个 XComponentSurface 同时承担 NDK 自绘取 buffer 和 Camera 预览输出。预览用 XComponentSurface;如果还要拿预览帧,按双路预览思路再单独创建 ImageReceiver Surface 作为第二路输出。若业务必须自绘同一个 Surface,也要等相机会话 stop/release 之后再做 NativeWindow 操作。可以先做一个最小验证:完全移除 NativeWindowRequestBuffer 后预览恢复,就基本能确认是 Surface 使用权冲突。

你这个说的Surface 使用权冲突有没有官方文档说明?

问题大概率在 Surface 使用权上。XComponentSurface 交给 Camera 做预览输出后,就不要再先用 NativeWindowRequestBuffer 把同一个 Surface 当成 NDK 自绘窗口消费了;相机预览也需要向这个 Surface 的 BufferQueue 送帧,两边同时操作很容易导致预览输出创建成功但实际无画面。建议拆成两路:XComponent 的 surfaceId 只给 createPreviewOutput 做预览显示;如果你还要在 NDK 层拿帧/处理图像,用 ImageReceiver 或第二路预览 Surface,而不是提前 request 同一个 XComponent buffer。

大哥,我就在预览前调了一次NativeWindowRequestBuffer函数,后续没有任何调用了。

HarmonyOS Next 中,XComponentSurface 在 NDK 层被获取并用于渲染后,其内部 Surface 对象状态会从空闲变为占用状态。摄像头预览需要通过 CameraKit 绑定同一 Surface 才能输出帧数据,但此时 Surface 已被 NDK 层持有并可能修改了缓冲区配置或显示模式,导致 CameraKit 无法再正确识别或绑定该 Surface,从而预览失败。这是资源独占与生命周期管理的约束所致。

在 NDK 层直接对 XComponent 的 Surface 调用 NativeWindowRequestBuffer 后,该 Surface 的 buffer 被锁定占用,导致摄像头服务无法再从该 Surface 获取可用的图形缓冲区,因此预览初始化失败,并触发 errorType: 11(通常为 Buffer 相关错误)。
XComponent 的 Surface 由框架管理,除非在自渲染场景下严格按照 NAPI 规范进行 buffer 的申请、绘制与释放,否则不应主动调用 buffer 请求接口,否则会破坏 Surface 的可用状态,使摄像头输出目标失效。

回到顶部