HarmonyOS鸿蒙Next中使用摄像头录制时,前后切换摄像头时录制的视频出现上下颠倒问题
HarmonyOS鸿蒙Next中使用摄像头录制时,前后切换摄像头时录制的视频出现上下颠倒问题 使用摄像头录制时,前后切换摄像头时,录制的视频出现上下颠倒问题
- 开启前置摄像头预览
- 开始录制
- 录制过程中,切换到后置摄像头,后置摄像头在UI上显示是OK的
- 结束录制
- 查看录制文件,录制的文件播放时后置摄像头是上下颠倒的
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
摄像头获取预览的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在切换摄像头时,会根据摄像头的传感器方向自动调整视频的显示方向,但如果摄像头传感器的方向信息未正确传递或处理,可能会导致视频方向错误。
具体原因可能涉及以下几个方面:
-
摄像头传感器方向信息不正确:摄像头传感器的方向信息未正确传递到系统,导致系统无法正确调整视频方向。
-
摄像头切换时的方向处理逻辑问题:在切换摄像头时,系统未能正确处理摄像头传感器的方向信息,导致视频方向错误。
-
摄像头驱动或固件问题:摄像头驱动或固件可能存在Bug,导致方向信息无法正确传递给系统。
解决此类问题通常需要检查摄像头传感器的方向信息是否正确传递,确保系统在切换摄像头时正确处理方向信息。此外,更新摄像头驱动或固件也可能有助于解决问题。
在HarmonyOS鸿蒙Next中,前后摄像头切换时视频上下颠倒的问题通常与摄像头传感器方向配置有关。建议检查CameraConfig
中的orientation
参数,确保其与设备物理摄像头的方向一致。可以通过Camera.getCameraInfo()
获取摄像头信息,动态调整orientation
。若问题依旧,可尝试在录制前调用Camera.setDisplayOrientation()
调整预览方向,确保与录制方向一致。