HarmonyOS鸿蒙Next中摄像头双路预览调用setFrameRate函数设置帧率会出现四个错误情况

HarmonyOS鸿蒙Next中摄像头双路预览调用setFrameRate函数设置帧率会出现四个错误情况 我在摄像头双路预览时,调用setFrameRate函数设置帧率会出现四个错误情况,具体如下:

1、正常情况:

代码:

this.previewOutput.setFrameRate( 1, 5 );
this.imageReceiverPreviewOutput.setFrameRate( 1, 5 );

结果: previewOutput和imageReceiverPreviewOutput的帧率都是5。

2、错误情况:

代码:

this.previewOutput.setFrameRate( 1, 5 );
this.imageReceiverPreviewOutput.setFrameRate( 1, 30 );

结果: hilog报错:{SetFrameRate():1087} PreviewOutputNapi::SetFrameRate! 7400110。

3、正常情况:

代码:

this.previewOutput.setFrameRate( 1, 5 );

结果: previewOutput的帧率是5,imageReceiverPreviewOutput的帧率是30。

4、正常情况:

代码:

this.imageReceiverPreviewOutput.setFrameRate( 1, 5 );

结果: previewOutput的帧率是30,imageReceiverPreviewOutput的帧率是5。

5、错误情况:

代码:

this.previewOutput.setFrameRate( 60, 60 );
this.imageReceiverPreviewOutput.setFrameRate( 60, 60 );

结果: previewOutput看不出帧率,imageReceiverPreviewOutput的帧率是26。

6、错误情况:

代码:

this.previewOutput.setFrameRate( 60, 60 );

结果: previewOutput看不出帧率,imageReceiverPreviewOutput的帧率是13。

7、错误情况:

代码:

this.imageReceiverPreviewOutput.setFrameRate( 60, 60 );

结果: previewOutput看不出帧率,imageReceiverPreviewOutput的帧率是30。

demo下载地址:https://gitee.com/chen_yi_ze/harmony-camera-frame-rate-error

具体代码在Index.ets文件的100行到112行。

设备:Nova 12 Pro,系统:HarmoryOS 6.1.0,DevEco:6.1.1 Beta1


更多关于HarmonyOS鸿蒙Next中摄像头双路预览调用setFrameRate函数设置帧率会出现四个错误情况的实战教程也可以访问 https://www.itying.com/category-93-b0.html

16 回复

开发者您好
1.关于错误2设置两个预览帧率时首先需要保持帧率在系统支持范围内,然后要保证两边设定的帧率参数一致。
2.关于错误5,6,7这边是在哪边看到帧数的,比如错误7,这边使用this.imageReceiverPreviewOutput.getActiveFrameRate();获取到的帧率是min为60,max为60。

更多关于HarmonyOS鸿蒙Next中摄像头双路预览调用setFrameRate函数设置帧率会出现四个错误情况的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


通过查看imageReceiverPreviewOutput的回调函数打印的日志就可以验证出实际的帧率。

开发者您好,这边是通过哪个回调验证的帧率?PreviewOutput的监听应该是没有返回帧率的,这边日志是怎么展现的?这边实际业务是因为这个帧率问题导致预览流掉帧吗?目前看提供的demo好像没有出现掉帧的情况。

每一次获取到一帧数据,就会回调一次imageReceiverPreviewOutput的on( ‘imageArrival’ )的回调函数,你统计一下每秒回调这个函数的次数,你就知道帧率是多少了。

这两个错误现象的根本原因都指向双路预览时两路流共享同一个底层帧率时钟,以及设备硬件能力上限
我建议看你需求来弄:如果你是双路都低帧率那就两路都显式设为相同帧率,如 setFrameRate(1, 5)
如果双路都高帧率那就先查设备上限,两路设相同值。若设备上限 30fps,设 60fps 也会降到 30fps
如果一路显示、一路处理(不同帧率需求)那就不要分别设不同帧率。两路统一设高帧率(如 30fps),在 imageArrival 回调里手动丢弃帧来模拟低帧率(如每 6 帧取 1 帧 ≈ 5fps)

代码修正示例:

// 双路预览时,两路必须同帧率
const targetFps = 5; // 或 30,根据设备能力和业务需求

// 先查询支持范围(可选)
const rates1 = this.previewOutput.getSupportedFrameRates();
const rates2 = this.imageReceiverPreviewOutput.getSupportedFrameRates();

// 两路设成完全一致
this.previewOutput.setFrameRate(targetFps, targetFps);
this.imageReceiverPreviewOutput.setFrameRate(targetFps, targetFps);

如果业务确实需要"显示 30fps + 处理 5fps":

// 两路都设 30fps
this.previewOutput.setFrameRate(30, 30);
this.imageReceiverPreviewOutput.setFrameRate(30, 30);

// 在 imageReceiver 回调里手动降频
let frameCount = 0;
this.receiver.on('imageArrival', () => {
  frameCount++;
  if (frameCount % 6 !== 0) return; // 每 6 帧取 1 帧,模拟 5fps
  // 处理逻辑...
});

说白了应该就是双路预览的两个 PreviewOutput 必须设置相同帧率。5fps 和 30fps 混设会触发 7400110 配置冲突;60fps 被压到 30fps 是因为该设备双路预览硬件上限就是 30fps。需要不同帧率时,统一设高帧率再在应用层手动抽帧。

那我第3和4种情况用的就是不同的帧率。

我获取了支持的帧率列表,是支持60帧的,如果不支持60帧的话,会报异常的,程序都跑不起来。

1、如果你说的是对的,那么系统其实是可以做到一个5帧一个30帧的,对吧?

2、既然系统可以做到,那为什么还要加这样的限制呢?挺搞笑的。

3、你这个说法我觉得不太靠谱,我目前根本就调不出60帧率的方法,我把第一路预览关掉也不行。

直接看下这个博客哈,看看能不能解决问题,需要注意的是为了方便我没有设置比例,其余的都差不多,效果图

我看了下你的代码,你这个软件没有给ImageReceiver设置帧率,所以遇不到我的问题。

在一个相机session中,所有输出流必须使用相同的帧率,目前无法同时以不同帧率输出到多个目标;

建议先判断当前设备是否支持,然后在配置帧率

import { abilityAccessCtrl } from '@kit.AbilityKit';
import { camera } from '@kit.CameraKit';
import { image } from '@kit.ImageKit';

type NullUndef = null | undefined;

interface FrameRateRange {
    min: number;
    max: number;
}

@Entry
@Component
struct Index
{
    m_XComponentControllerPt: XComponentController = new XComponentController();

    async aboutToAppear(): Promise<void>
    {
        console.log( "FirstPage aboutToAppear" );

        await abilityAccessCtrl.createAtManager().requestPermissionsFromUser( this.getUIContext().getHostContext()!, [ 'ohos.permission.CAMERA' ] );
    }

    // --- 相机核心对象 ---
    private cameraManager: camera.CameraManager | NullUndef;
    private cameraInput: camera.CameraInput | NullUndef;
    private previewOutput: camera.PreviewOutput | NullUndef;
    private imageReceiver: image.ImageReceiver | NullUndef;
    private imageReceiverPreviewOutput: camera.PreviewOutput | NullUndef;
    private captureSession: camera.Session | NullUndef;

    async initCamera()
    {
        // 1. 获取CameraManager实例
        this.cameraManager = camera.getCameraManager( getContext( this ) );
        if( !this.cameraManager ) return;

        // 2. 获取相机设备(优先使用后置摄像头)
        const cameras = this.cameraManager.getSupportedCameras( );
        const cameraDevice = cameras.find( device => device.cameraPosition === camera.CameraPosition.CAMERA_POSITION_FRONT );
        if( !cameraDevice ) return;

        // 3. 创建相机输入流
        this.cameraInput = this.cameraManager.createCameraInput( cameraDevice );
        await this.cameraInput.open( );

        // 4. 获取相机输出能力,选取合适的预览配置
        const SupportedSceneModes = this.cameraManager.getSupportedSceneModes( cameraDevice );
        const cameraOutputCap = this.cameraManager.getSupportedOutputCapability( cameraDevice, camera.SceneMode.NORMAL_VIDEO );
        //const previewProfile = cameraOutputCap.previewProfiles[ cameraOutputCap.previewProfiles.length - 1 ];
        const previewProfile = cameraOutputCap.previewProfiles[ 0 ];

        // 5. 获取预览显示用的Surface ID(来自XComponent)
        let xComponentSurfaceId: string = this.m_XComponentControllerPt.getXComponentSurfaceId( );

        // 6. 创建用于显示的预览输出流
        this.previewOutput = this.cameraManager.createPreviewOutput( previewProfile, xComponentSurfaceId );

        // ========== 关键步骤:创建ImageReceiver用于获取帧数据 ==========
        // 创建ImageReceiver实例,分辨率建议与预览流保持一致
        this.imageReceiver = image.createImageReceiver(
            { width: previewProfile.size.width, height: previewProfile.size.height },
            image.ImageFormat.JPEG, // 相机原始数据通常为YUV格式
            10 // 缓冲区容量,推荐8
        );

        // 注册图像到达回调,每帧数据都会触发
        this.imageReceiver.on('imageArrival', async () =>
        {
            console.info( 'image arrival' );

            let img: image.Image | NullUndef = null;
            try {
                img = await this.imageReceiver?.readNextImage();
                if (img) {
                    // 获取图像组件(YUV数据)
                    const components = await img.getComponent( image.ComponentType.JPEG );
                    let p_width = img.size.width;
                    let p_height = img.size.height;

                    //其他业务代码。

                    img.release(); // 务必释放图像资源,防止内存泄漏
                }
            } catch (err) {
                console.error(`处理帧数据失败: ${err.code} - ${err.message}`);
            } finally {
            }
        });

        // 7. 创建第二条预览输出流,关联到ImageReceiver的Surface
        this.imageReceiverPreviewOutput = this.cameraManager.createPreviewOutput( previewProfile, await this.imageReceiver.getReceivingSurfaceId() );

        // 8. 创建相机会话并配置
        this.captureSession = this.cameraManager.createSession( camera.SceneMode.NORMAL_VIDEO );
        this.captureSession.beginConfig( );
        this.captureSession.addInput( this.cameraInput );
        this.captureSession.addOutput( this.previewOutput );
        this.captureSession.addOutput( this.imageReceiverPreviewOutput ); // 添加第二条预览流

        const previewOutputSupportedFrameRates = this.previewOutput.getSupportedFrameRates( );
        const imageReceiverPreviewOutputSupportedFrameRates = this.imageReceiverPreviewOutput.getSupportedFrameRates( );

        console.info(`previewOutput支持的帧率范围: ${JSON.stringify(previewOutputSupportedFrameRates)}`);
        console.info(`imageReceiverPreviewOutput支持的帧率范围: ${JSON.stringify(imageReceiverPreviewOutputSupportedFrameRates)}`);

        const targetFps = 30;

        const isPreviewSupportTarget = this.isFrameRateSupported(previewOutputSupportedFrameRates, targetFps);
        const isImageReceiverSupportTarget = this.isFrameRateSupported(imageReceiverPreviewOutputSupportedFrameRates, targetFps);

        console.info(`previewOutput是否支持${targetFps}fps: ${isPreviewSupportTarget}`);
        console.info(`imageReceiverPreviewOutput是否支持${targetFps}fps: ${isImageReceiverSupportTarget}`);

        if (isPreviewSupportTarget && isImageReceiverSupportTarget) {
            this.previewOutput.setFrameRate(targetFps, targetFps);
            this.imageReceiverPreviewOutput.setFrameRate(targetFps, targetFps);
            console.info(`成功设置帧率为${targetFps}fps`);
        } else {
            const maxCommonFps = this.getMaxCommonFrameRate(previewOutputSupportedFrameRates, imageReceiverPreviewOutputSupportedFrameRates);
            if (maxCommonFps > 0) {
                this.previewOutput.setFrameRate(maxCommonFps, maxCommonFps);
                this.imageReceiverPreviewOutput.setFrameRate(maxCommonFps, maxCommonFps);
                console.info(`使用最大共同支持帧率${maxCommonFps}fps`);
            } else {
                console.warn('未找到共同支持的帧率,使用默认设置');
            }
        }

        await this.captureSession.commitConfig();
        await this.captureSession.start();
    }

    /**
     * 判断帧率是否支持
     * @param supportedRates
     * @param targetFps
     * @returns
     */
    private isFrameRateSupported(supportedRates: Array<FrameRateRange>, targetFps: number): boolean {
        for (const range of supportedRates) {
            if (targetFps >= range.min && targetFps <= range.max) {
                return true;
            }
        }
        return false;
    }

    /**
     * 获取最大共同支持帧率
     * @param previewRates
     * @param imageReceiverRates
     * @returns
     */
    private getMaxCommonFrameRate(
        previewRates: Array<FrameRateRange>,
        imageReceiverRates: Array<FrameRateRange>
    ): number {
        let maxCommonFps = 0;
        
        for (const previewRange of previewRates) {
            for (const receiverRange of imageReceiverRates) {
                const overlapMin = Math.max(previewRange.min, receiverRange.min);
                const overlapMax = Math.min(previewRange.max, receiverRange.max);
                
                if (overlapMin <= overlapMax && overlapMax > maxCommonFps) {
                    maxCommonFps = overlapMax;
                }
            }
        }
        
        return maxCommonFps;
    }

    build()
    {
        RelativeContainer()
        {
            XComponent( {
                type: XComponentType.SURFACE,
                controller: this.m_XComponentControllerPt
            } )
                .width('100%')
                .height('30%')
                .onLoad( () =>
                {
                } )
                .onClick( () =>
                {

                } )

            Button()
            {
                Text( "打开摄像头" )
                    .fontSize( $r( 'app.float.page_text_font_size' ) )
            }
            .id( 'HelloWorld' )
            .fontSize( $r( 'app.float.page_text_font_size' ) )
            .fontWeight( FontWeight.Bold )
            .alignRules( {
                center: { anchor: '__container__', align: VerticalAlign.Center },
                middle: { anchor: '__container__', align: HorizontalAlign.Center }
            } )
            .onClick( () =>
            {
                this.initCamera();
            } )
        }
        .height( '100%' )
        .width( '100%' )
    }
}

那我第3和4种情况用的就是不同的帧率。

你配置了

this.imageReceiverPreviewOutput.setFrameRate(1, 5); 

后怎么知道previewOutput的帧率是30的

5帧和30帧的应该还是很容易分辨的。

在HarmonyOS鸿蒙Next中,双路预览调用setFrameRate设置帧率时,常见四种错误:

  1. 传入的帧率值超出摄像头硬件支持的合法范围(如<1或>最大帧率)。
  2. 双路流中某一路正在捕获或流状态未处于PREPARED/STARTED,导致设置无效。
  3. 帧率与分辨率组合不被当前摄像头管道支持(如高分辨率下只能达到较低帧率)。
  4. 前后两次setFrameRate调用间隔过短,底层驱动拒绝处理。

双路预览时,两个输出(PreviewOutput 和 ImageReceiver)共享同一摄像头采集流,因此帧率必须统一。同时调用 setFrameRate 设置不同的帧率范围(如 5 和 30)会导致冲突,报错 7400110 表示帧率设置不兼容。应保证两处设置的 minFpsmaxFps 完全一致,或只设置其中一个,另一个会自动跟随。

当设置 60 帧但实际只能达到 26/30 帧,是因为当前设备(Nova 12 Pro)摄像头硬件最高支持 30 帧,强制 60 会触发内部降频策略,导致异常。建议先通过 CameraManager.getSupportedFrameRates() 查询设备能力,仅设置支持的帧率范围。

单独设置一个输出时另一路帧率看似为 30,是由于未同步触发帧率变更,采集流仍维持初始默认帧率,而预览与图像回调统计方式不同造成的错觉;实际后续会逐渐对齐。最佳实践是统一帧率、避免分路设置。

回到顶部