HarmonyOS鸿蒙Next中使用摄像头录制时,前后切换摄像头时录制的视频出现上下颠倒问题

HarmonyOS鸿蒙Next中使用摄像头录制时,前后切换摄像头时录制的视频出现上下颠倒问题 使用摄像头录制时,前后切换摄像头时,录制的视频出现上下颠倒问题

  1. 开启前置摄像头预览
  2. 开始录制
  3. 录制过程中,切换到后置摄像头,后置摄像头在UI上显示是OK的
  4. 结束录制
  5. 查看录制文件,录制的文件播放时后置摄像头是上下颠倒的

Index.ets

@Entry
@Component
struct Index {
  @State message: string = 'Hello World';
  private cameraService: ICameraService = new CameraServiceImpl(getContext(this))
  private xComponentController: XComponentController = new XComponentController()

  build() {
    RelativeContainer() {
      XComponent({
        id: "x_id",
        type: XComponentType.SURFACE,
        controller: this.xComponentController
      })
        .onLoad(async () => {
          this.xComponentController.setXComponentSurfaceSize({
            surfaceWidth: 1280,
            surfaceHeight: 720
          })
        })
        .id("x_id")
        .width("auto")
        .animation({ duration: 300, curve: "ease" })
      Column() {
        Button("开始预览")
          .fontWeight(FontWeight.Bold)
          .onClick(() => {
            this.cameraService.startLocalPreview(this.xComponentController.getXComponentSurfaceId(), {
              previewFrameWidth: 1280,
              previewFrameHeight: 720
            })
          })
        Button("开始录制")
          .fontWeight(FontWeight.Bold)
          .onClick(() => {
            this.cameraService.startRecord({
              fileName: "camera_" + new Date().getTime(),
              audioBitrate: 100000, 
              audioChannels: 2, 
              audioCodec: media.CodecMimeType.AUDIO_AAC, 
              audioSampleRate: 8000, 
              fileFormat: media.ContainerFormatType.CFT_MPEG_4,
              videoBitrate: 2000000,
              videoCodec: media.CodecMimeType.VIDEO_AVC,
              videoFrameWidth: 1280,
              videoFrameHeight: 720,
              videoFrameRate: 20
            })
          })
        Button("切换镜头").onClick(() => {
          this.cameraService.switchCamera()
        })
        Button("结束录制")
          .fontWeight(FontWeight.Bold)
          .onClick(() => {
            this.cameraService.stopRecord().then(path => {
              console.log("path:" + path)
            })
          })
      }
      .alignRules({
        center: { anchor: '__container__', align: VerticalAlign.Center },
        middle: { anchor: '__container__', align: HorizontalAlign.Center }
      })
    }
    .height('100%')
    .width('100%')
  }
}

ICameraService.ts

import { media } from '@kit.MediaKit'

export interface PreviewConfig {
  previewFrameWidth: number, 
  previewFrameHeight: number, 
}

export interface RecordConfig {
  fileName: string, 
  audioBitrate: number, 
  audioChannels: number, 
  audioCodec: media.CodecMimeType, 
  audioSampleRate: number, 
  fileFormat: media.ContainerFormatType, 
  videoBitrate: number, 
  videoCodec: media.CodecMimeType, 
  videoFrameWidth: number, 
  videoFrameHeight: number, 
  videoFrameRate: number 
}

export enum CameraType {
  FRONT,
  BACK
}

export enum CameraException {
  DEVICE_EXCEPTION, 
  INPUT_EXCEPTION, 
  PREVIEW_EXCEPTION, 
  RECORD_EXCEPTION, 
  SWITCH_EXCEPTION, 
}

export interface ICameraException {
  onError(errorType: CameraException, errorMsg: string): void
}

export interface ICameraService {

  setExceptionCallback(callBack: ICameraException): void

  startLocalPreview(surfaceId: string, config: PreviewConfig): Promise<void>

  stopLocalPreview(): void

  startRecord(config: RecordConfig): void

  stopRecord(): Promise<string>

  switchCameraType(type: CameraType): void

  switchCamera(): void

  getCameraType(): CameraType

  isCameraOpen(): boolean

  release(): void

  pause(): void

  resume(): void
}

CameraServiceImpl.ets

import { camera } from '@kit.CameraKit';

import fs from '@ohos.file.fs';
import { media } from '@kit.MediaKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { CameraType, ICameraException, ICameraService, PreviewConfig, RecordConfig } from '../interface/ICameraService';
import { JSON } from '@kit.ArkTS';

export class CameraServiceImpl implements ICameraService {
  private context: Context
  private cameraManager: camera.CameraManager
  private videoSession: camera.VideoSession | undefined = undefined;
  private videoOutput: camera.VideoOutput | undefined = undefined;
  private avRecorder: media.AVRecorder | undefined = undefined;
  private recordSurfaceId: string = ""
  private recordConfig: RecordConfig | undefined
  private cameraInput: camera.CameraInput | undefined = undefined;
  private previewOutput: camera.PreviewOutput | undefined = undefined;
  private file: fs.File | undefined = undefined
  private cameraArray: Array<camera.CameraDevice> = []
  private cameraType: CameraType = CameraType.FRONT
  private surfaceId: string = ""
  private previewWidth: number = 1280
  private previewHeight: number = 720
  private isInPreviewing = false
  private isInRecording = false
  private exceptionCallback: ICameraException | undefined

  constructor(context: Context) {
    this.context = context
    this.cameraManager = camera.getCameraManager(context);
    if (!this.cameraManager) {
      console.error("camera.getCameraManager error")
      return;
    }

    this.cameraArray = this.cameraManager.getSupportedCameras();

    if (this.cameraArray.length <= 0) {
      console.error("cameraManager.getSupportedCameras error")
      return;
    }
  }

  setExceptionCallback(callBack: ICameraException): void {
    this.exceptionCallback = callBack
  }

  async startLocalPreview(surfaceId: string, previewConfig: PreviewConfig): Promise<void> {
    if (this.isInPreviewing) {
      return
    }
    this.videoSession = this.cameraManager.createSession(camera.SceneMode.NORMAL_VIDEO) as camera.VideoSession;
    this.surfaceId = surfaceId
    this.isInPreviewing = true
    this.previewWidth = previewConfig.previewFrameWidth
    this.previewHeight = previewConfig.previewFrameHeight
    let cameraDevice = this.getDevice()
    if (!cameraDevice) {
      console.error("相机设备不存在")
      this.exceptionCallback?.onError(1, "相机设备不存在")
      return
    }
    let cameraOutputCap: camera.CameraOutputCapability = this.cameraManager.getSupportedOutputCapability(cameraDevice, camera.SceneMode.NORMAL_VIDEO);
    try {
      this.videoSession?.beginConfig();
      let cameraInput = await this.getCameraInput(cameraDevice)
      if (cameraInput == undefined) {
        this.exceptionCallback?.onError(1, "相机设备不存在")
        return
      }
      this.videoSession.addInput(cameraInput);
      this.cameraInput = cameraInput
      let previewProfilesArray: Array<camera.Profile> = cameraOutputCap.previewProfiles;
      let previewOutput = await this.getPreviewOutput(previewProfilesArray)
      if (previewOutput === undefined) {
        this.exceptionCallback?.onError(2, "预览配置不存在")
        return;
      }
      this.videoSession.addOutput(previewOutput);
      this.previewOutput = previewOutput
      await this.videoSession.commitConfig();
      await this.videoSession.start();
    } catch (error) {
      let err = error as BusinessError;
      console.error(`Failed to startPreview. error: ${JSON.stringify(err)}`);
      this.exceptionCallback?.onError(3, "预览配置异常:" + err.message)
      return
    }
  }

  async stopLocalPreview(): Promise<void> {
    if (!this.isInPreviewing || this.isInRecording) {
      return
    }
    this.isInPreviewing = false
    try {
      this.videoSession?.stop()
      this.videoSession?.release()
    } catch (error) {
      let err = error as BusinessError;
      console.error(`stopLocalPreview error: ${JSON.stringify(err)}`);
    }
    this.cameraInput = undefined
    this.previewOutput = undefined
    this.videoSession = undefined
  }

  async startRecord(config: RecordConfig): Promise<void> {
    if (!this.isInPreviewing || this.isInRecording) {
      return
    }
    this.isInRecording = true
    try {
      this.recordConfig = config
      this.videoOutput = await this.getVideoOutput(true)
      this.videoSession?.beginConfig()
      this.videoSession?.addOutput(this.videoOutput)
      await this.videoSession?.commitConfig()
      await this.videoSession?.start()
      await this.videoOutput?.start();
      await this.avRecorder?.start()
      console.info("start record then")
    } catch (e) {
      console.error("startRecord exception:" + JSON.stringify(e))
      this.exceptionCallback?.onError(4, "录制错误:" + e.message)
    }
  }

  async stopRecord(): Promise<string> {
    if (!this.isInRecording) {
      return ""
    }
    this.isInRecording = false
    let filePath = ""
    try {
      this.avRecorder?.stop();
      this.avRecorder?.release();
      this.videoSession?.beginConfig()
      if (this.videoOutput) {
        this.videoOutput?.stop()
        this.videoSession?.removeOutput(this.videoOutput)
      }
      await this.videoSession?.commitConfig()
      await this.videoSession?.start()
    } catch (error) {
      let err = error as BusinessError;
      console.error(`stop error: ${JSON.stringify(err)}`);
      this.exceptionCallback?.onError(5, "录制错误:" + err.message)
    } finally {
      if (this.file) {
        filePath = this.file?.path
        console.info(filePath)
        fs.closeSync(this.file);
      }
      this.videoOutput = undefined
      this.avRecorder = undefined
    }
    return filePath
  }

  async switchCameraType(type: CameraType): Promise<void> {
    if (this.cameraType == type || !this.isInPreviewing) {
      return
    }
    this.cameraType = type
    this.switchCameraInternal()
  }

  switchCamera(): void {
    if (!this.isInPreviewing) {
      return
    }
    if (this.cameraType == CameraType.FRONT) {
      this.cameraType = CameraType.BACK
    } else {
      this.cameraType = CameraType.FRONT
    }
    this.switchCameraInternal()
  }

  getCameraType(): CameraType {
    return this.cameraType
  }

  isCameraOpen(): boolean {
    return this.isInPreviewing
  }

  async resume() {
    try {
      if (this.isInPreviewing) {
        this.videoSession = this.cameraManager.createSession(camera.SceneMode.NORMAL_VIDEO) as camera.VideoSession;
        let cameraDevice = this.getDevice()
        if (!cameraDevice) {
          console.error("相机设备不存在")
          this.exceptionCallback?.onError(1, "相机设备不存在")
          return
        }
        let cameraOutputCap: camera.CameraOutputCapability = this.cameraManager.getSupportedOutputCapability(cameraDevice, camera.SceneMode.NORMAL_VIDEO);
        this.videoSession?.beginConfig();
        let cameraInput = await this.getCameraInput(cameraDevice)
        if (cameraInput == undefined) {
          this.exceptionCallback?.onError(1, "相机设备不存在")
          return
        }
        this.videoSession.addInput(cameraInput);
        this.cameraInput = cameraInput
        let previewProfilesArray: Array<camera.Profile> = cameraOutputCap.previewProfiles;
        let previewOutput = await this.getPreviewOutput(previewProfilesArray)
        if (previewOutput === undefined) {
          this.exceptionCallback?.onError(2, "预览配置不存在")
          return;
        }
        this.videoSession.addOutput(previewOutput);
        this.previewOutput = previewOutput

        if (this.isInRecording && this.avRecorder) {
          this.videoSession.addOutput(this.videoOutput);
        }
        await this.videoSession.commitConfig();
        await this.videoSession.start();
      }
      if (this.isInRecording) {
        await this.videoOutput?.start()
        this.avRecorder?.resume()
      }
    } catch (error) {
      let err = error as BusinessError;
      console.error(`Failed to startPreview. error: ${JSON.stringify(err)}`);
      this.exceptionCallback?.onError(6, "恢复摄像机异常:" + error)
    }
  }

  async pause() {
    try {
      if (this.isInPreviewing) {
        this.videoSession?.beginConfig()
        if (this.isInRecording) {
          this.videoOutput?.stop()
          this.avRecorder?.pause()
          this.videoSession?.removeOutput(this.videoOutput)
        }
        this.videoSession?.commitConfig()
        this.videoSession?.stop()
        this.videoSession?.release()
      }
    } catch (error) {
      let err = error as BusinessError;
      console.error(`stopLocalPreview error: ${JSON.stringify(err)}`);
      this.exceptionCallback?.onError(7, "暂停摄像机异常:" + error)
    }
    this.cameraInput = undefined
    this.previewOutput = undefined
    this.videoSession = undefined
  }

  release(): void {
    this.videoSession?.stop();

    this.cameraInput?.close();
    this.cameraInput = undefined
    this.previewOutput?.release();
    this.previewOutput = undefined
    this.videoOutput?.release();
    this.videoOutput = undefined
    this.avRecorder?.release();
    this.avRecorder = undefined
    this.videoSession?.release();

    this.videoSession = undefined;
  }

  private getDevice(): camera.CameraDevice | undefined {
    let device = this.cameraArray.find(value => {
      if (this.cameraType == CameraType.FRONT) {
        return value.cameraPosition == camera.CameraPosition.CAMERA_POSITION_FRONT
      } else {
        return value.cameraPosition == camera.CameraPosition.CAMERA_POSITION_BACK
      }
    })
    return device
  }

  private async switchCameraInternal() {
    let cameraDevice = this.getDevice()
    if (!cameraDevice) {
      console.error("相机设备不存在")
      this.exceptionCallback?.onError(8, "相机设备不存在")
      return
    }
    try {
      this.videoSession?.beginConfig()
      if (this.cameraInput) {
        this?.cameraInput?.close()
        this.videoSession?.removeInput(this?.cameraInput);
      }
      if (this.isInRecording) {
        this.videoOutput?.stop()
        this.avRecorder?.pause()
      }
      let cameraInput = await this.getCameraInput(cameraDevice)
      if (cameraInput == undefined) {
        this.exceptionCallback?.onError(1, "相机输入异常")
        return
      }
      this.videoSession?.addInput(cameraInput);
      this.cameraInput = cameraInput
      await this.videoSession.commitConfig()
      await this.videoSession.start()
      if (this.isInRecording) {
        this.videoOutput?.start()
        this.avRecorder?.resume()
      }
    } catch (error) {
      let err = error as BusinessError;
      console.error(`Failed to add cameraInput. error: ${JSON.stringify(err)}`);
      this.exceptionCallback?.onError(1, "切换相机异常:" + err.message)
    }
  }

  private async getCameraInput(cameraDevice: camera.CameraDevice): Promise<camera.CameraInput | undefined> {
    let cameraInput: camera.CameraInput | undefined
    try {
      cameraInput = this.cameraManager.createCameraInput(cameraDevice);
    } catch (error) {
      let err = error as BusinessError;
      console.error(`Failed to createCameraInput. error: ${JSON.stringify(err)}`);
    }
    if (cameraInput === undefined) {
      return undefined;
    }
    try {
      await cameraInput.open();
    } catch (error) {
      let err = error as BusinessError;
      console.error(`Failed to open cameraInput. error: ${JSON.stringify(err)}`);
    }

    return cameraInput
  }

  private async getPreviewOutput(previewProfilesArray: Array<camera.Profile>): Promise<camera.PreviewOutput | undefined> {
    try {
      let arr = previewProfilesArray.sort((a: camera.Profile, b: camera.Profile) => {
        return Math.abs((this.previewHeight / this.previewWidth) - (a.size.width / a.size.height)) -
        Math.abs((this.previewHeight / this.previewWidth) - (b.size.width / b.size.height))
      })
      let previewProfile = arr[0]
      console.info(`screen ${this.previewWidth}*${this.previewHeight}, preview ${previewProfile.size.width}*${previewProfile.size.height}`)
      return this.cameraManager.createPreviewOutput(previewProfile, this.surfaceId);
    } catch (error) {
      let err = error as BusinessError;
      console.error(`Failed to create the PreviewOutput instance. error: ${JSON.stringify(err)}`);
      return undefined
    }
  }

  private async getVideoOutput(isNeedRecorder: boolean): Promise<camera.VideoOutput | undefined> {
    try {
      if (!this.recordConfig) {
        return
      }
      if (isNeedRecorder) {
        this.recordSurfaceId = await this.createRecorder(this.recordConfig);
      }
      let cameraDevice = this.getDevice()
      if (!cameraDevice) {
        console.error("相机设备不存在")
        return undefined
      }
      let cameraOutputCap: camera.CameraOutputCapability = this.cameraManager.getSupportedOutputCapability(cameraDevice, camera.SceneMode.NORMAL_VIDEO);
      let videoProfilesArray: Array<camera.VideoProfile> = cameraOutputCap.videoProfiles;
      let videoProfile: undefined | camera.VideoProfile

      for (let i = 0; i < videoProfilesArray.length; i++) {
        let profile = videoProfilesArray[i]
        console.log("item---" + JSON.stringify(profile))
        if (profile.size.width == this.recordConfig?.videoFrameWidth &&
          profile.size.height == this.recordConfig?.videoFrameHeight
        ) {
          videoProfile = profile
          break
        }
      }
      console.info("getVideoOutput = " + JSON.stringify(videoProfile))
      return this.cameraManager.createVideoOutput(videoProfile, this.recordSurfaceId);
    } catch (error) {
      let err = error as BusinessError;
      console.error(`createAVRecorder call failed. error code: ${err.code}`);
      return undefined
    }
  }

  private async createRecorder(config: RecordConfig): Promise<string> {
    try {
      let dir = this.context.cacheDir + "/" + config.fileName + ".mp4"
      this.file = fs.openSync(dir, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
      let rotation: number
      if (this.cameraType == CameraType.FRONT) {
        rotation = 270
      } else {
        rotation = 90
      }
      let aVRecorderConfig: media.AVRecorderConfig = {
        audioSourceType: media.AudioSourceType.AUDIO_SOURCE_TYPE_DEFAULT,
        videoSourceType: media.VideoSourceType.VIDEO_SOURCE_TYPE_SURFACE_YUV,
        profile: config,
        url: `fd://${this.file.fd.toString()}`, 
        rotation: rotation, 
      };
      this.avRecorder = await media.createAVRecorder();
      await this.avRecorder.prepare(aVRecorderConfig);
      return await this.avRecorder.getInputSurface();
    } catch (e) {
      console.info("createRecorder error:" + JSON.stringify(e))
      return ""
    }
  }
}

更多关于HarmonyOS鸿蒙Next中使用摄像头录制时,前后切换摄像头时录制的视频出现上下颠倒问题的实战教程也可以访问 https://www.itying.com/category-93-b0.html

3 回复

摄像头获取预览的YUV数据并写到文件中保存,可以使用Camera_PhotoCaptureSetting中的rotation设置拍照的旋转角度,默认应该是0度,请参考: https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/_camera___photo_capture_setting-V5#ZH-CN_TOPIC_0000001893209937__rotation

当前规格,预览流旋转角度固定值 90 度,目前双路预览流角度固定前置摄像头得到的YUV数据顺时针旋转了90度,后置摄像头得到的YUV数据顺时针旋转了270度。换算前后摄像头的数据角度可以通过YUV数据进行旋转操作,对于前置摄像头的数据还需进行镜像翻转操作。

旋转操作可以参考:

private byte[] rotateYUVDegree270(byte[] data, int imageWidth, int imageHeight) {
byte[] yuv = new byte[imageWidth * imageHeight * 3 / 2];
// Rotate the Y luma
int i = 0;
for (int x = imageWidth - 1; x >= 0; x--) {
    for (int y = 0; y < imageHeight; y++) {
        yuv[i] = data[y * imageWidth + x];
        i++;
    }
}
// Rotate the U and V color components
i = imageWidth * imageHeight;
for (int x = imageWidth - 1; x > 0; x = x - 2) {
    for (int y = 0; y < imageHeight / 2; y++) {
        yuv[i] = data[(imageWidth * imageHeight) + (y * imageWidth) + (x - 1)];
        i++;
        yuv[i] = data[(imageWidth * imageHeight) + (y * imageWidth) + x];
        i++;
    }
}
return yuv;
}

对于前置摄像头的数据还需要进行镜像翻转的操作:

private byte[] rotateYUVDegree270(byte[] data, int imageWidth, int imageHeight) {
byte[] yuv = new byte[imageWidth * imageHeight * 3 / 2];
// Rotate the Y luma
int i = 0;
for (int x = imageWidth - 1; x >= 0; x--) {
    for (int y = 0; y < imageHeight; y++) {
        yuv[i] = data[y * imageWidth + x];
        i++;
    }
}
// Rotate the U and V color components
i = imageWidth * imageHeight;
for (int x = imageWidth - 1; x > 0; x = x - 2) {
    for (int y = 0; y < imageHeight / 2; y++) {
        yuv[i] = data[(imageWidth * imageHeight) + (y * imageWidth) + (x - 1)];
        i++;
        yuv[i] = data[(imageWidth * imageHeight) + (y * imageWidth) + x];
        i++;
    }
}
return yuv;
}

更多关于HarmonyOS鸿蒙Next中使用摄像头录制时,前后切换摄像头时录制的视频出现上下颠倒问题的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


在HarmonyOS鸿蒙Next中,使用摄像头录制视频时,前后切换摄像头导致录制的视频出现上下颠倒问题,可能是由于摄像头传感器方向与系统预期方向不一致导致的。HarmonyOS在切换摄像头时,会根据摄像头的传感器方向自动调整视频的显示方向,但如果摄像头传感器的方向信息未正确传递或处理,可能会导致视频方向错误。

具体原因可能涉及以下几个方面:

  1. 摄像头传感器方向信息不正确:摄像头传感器的方向信息未正确传递到系统,导致系统无法正确调整视频方向。

  2. 摄像头切换时的方向处理逻辑问题:在切换摄像头时,系统未能正确处理摄像头传感器的方向信息,导致视频方向错误。

  3. 摄像头驱动或固件问题:摄像头驱动或固件可能存在Bug,导致方向信息无法正确传递给系统。

解决此类问题通常需要检查摄像头传感器的方向信息是否正确传递,确保系统在切换摄像头时正确处理方向信息。此外,更新摄像头驱动或固件也可能有助于解决问题。

在HarmonyOS鸿蒙Next中,前后摄像头切换时视频上下颠倒的问题通常与摄像头传感器方向配置有关。建议检查CameraConfig中的orientation参数,确保其与设备物理摄像头的方向一致。可以通过Camera.getCameraInfo()获取摄像头信息,动态调整orientation。若问题依旧,可尝试在录制前调用Camera.setDisplayOrientation()调整预览方向,确保与录制方向一致。

回到顶部