Skip to content

Matter camera: supportedResolutions can advertise frame rates the stream allocator will never request #3186

Description

@ldeora

Summary

videoStreamSettings.supportedResolutions is currently calculated from the camera's reported pixel-rate/FPS limits, but the stream allocator separately caps VideoStreamAllocate at the driver's max_frame_rate default of 60 fps.

This can make SmartThings advertise a frame rate that the same driver will never request.

The G350 exposed the mismatch directly: SmartThings advertised 120 fps resolutions while the allocation code was capped at 60 fps.

Environment / verification

Verified against SmartThingsCommunity/SmartThingsEdgeDrivers main at commit
60bbf2716412fdf04fd59f967bdd37be6995d664 (2026-08-12).

Real-device validation used:

  • Aqara Camera Hub G350
  • VID/PID: 0x115F / 0x3013
  • firmware: 4.5.70 / 4005070
  • reported Matter SpecificationVersion: 1.5.1
  • Camera device type: 0x0142
  • Camera endpoint in this device: endpoint 2

The proposed changes are generic Matter-camera changes. No Aqara VID/PID fingerprint or proprietary Aqara cluster handling is required.

Current behavior

Supported resolution FPS is calculated using the device-reported max_fps:

There is no driver-allocation ceiling applied in build_supported_resolutions().

The driver's stream defaults define:

min_frame_rate = 30
max_frame_rate = 60

When a stream is reallocated, the code caps max frame rate at that 60 fps default:

It also uses cmd.args.resolution.width and .height but does not use an explicitly selected cmd.args.resolution.fps to constrain the new allocation.

Reproduction

The G350 reported:

VideoSensorParams:
  width: 1920
  height: 1080
  maxFPS: 120

SmartThings consequently advertised:

"supportedResolutions": [
  {
    "width": 640,
    "height": 480,
    "fps": 120
  },
  {
    "width": 1920,
    "height": 1080,
    "fps": 120
  }
]

However, VideoStreamAllocate uses:

math.min(device_max_fps, camera_fields.video_stream_defaults.max_frame_rate)

where the default maximum is 60.

Therefore the UI/API advertises 120 fps while the allocator cannot request more than 60 fps.

Expected behavior

The advertised supported resolution/FPS tuples and the actual allocation path should agree.

Two reasonable implementations are possible.

Option A: conservative advertisement

Clamp advertised FPS to the same driver/platform allocation ceiling:

max_fps = math.min(max_fps, camera_fields.video_stream_defaults.max_frame_rate)

before building supportedResolutions.

Option B: honor selected FPS

When cmd.args.resolution.fps is provided, use it for the allocation, clamped against:

  • device maximum;
  • platform/driver maximum;
  • any other applicable stream constraints.

For consistency, the test build used both principles:

  • never advertise more than the allocator is willing to request;
  • honor the selected FPS within that valid range.

G350 evidence

Before the fix:

advertised: 120 fps
allocator ceiling: 60 fps

After applying the conservative advertisement logic in the test driver, the SmartThings capability no longer claimed an FPS above the driver's allocation ceiling.

The G350 continued to stream normally.

Affected files

  • src/sub_drivers/camera/camera_utils/utils.lua
  • src/sub_drivers/camera/camera_handlers/capability_handlers.lua
  • src/test/test_matter_camera.lua

Potentially:

  • src/sub_drivers/camera/camera_utils/fields.lua if the allocation ceiling is refactored.

Suggested regression coverage

Use a camera fixture with:

device max FPS = 120
driver max FPS = 60

Verify:

  1. supportedResolutions[*].fps <= 60;
  2. stream allocation never requests a value higher than the advertised value;
  3. when the user selects a supported FPS, the allocation request honors it;
  4. lower-FPS cameras are unaffected.

Minimal suggested diff

Applies to SmartThingsCommunity/SmartThingsEdgeDrivers commit
60bbf2716412fdf04fd59f967bdd37be6995d664.

diff --git a/src/sub_drivers/camera/camera_handlers/capability_handlers.lua b/src/sub_drivers/camera/camera_handlers/capability_handlers.lua
index a26afd0..dd2e5ad 100644
--- a/src/sub_drivers/camera/camera_handlers/capability_handlers.lua
+++ b/src/sub_drivers/camera/camera_handlers/capability_handlers.lua
@@ -469,14 +469,25 @@ function CameraCapabilityHandlers.handle_set_stream(driver, device, cmd)
     on_screen_display_enabled = camera_fields.video_stream_defaults.on_screen_display_enabled
   end
 
+  local max_supported_fps = math.min(
+    device:get_field(camera_fields.MAX_FRAMES_PER_SECOND) or camera_fields.video_stream_defaults.max_frame_rate,
+    camera_fields.video_stream_defaults.max_frame_rate
+  )
+  local min_frame_rate = camera_fields.video_stream_defaults.min_frame_rate
+  local max_frame_rate = max_supported_fps
+  if cmd.args.resolution ~= nil and cmd.args.resolution.fps ~= nil then
+    local requested_fps = utils.clamp_value(cmd.args.resolution.fps, 1, max_supported_fps)
+    min_frame_rate = requested_fps
+    max_frame_rate = requested_fps
+  end
+
   -- Use the same resolution (if available) for MinResolution and MaxResolution to force the server to allocate the
   -- stream with the desired resolution.
   device:send(clusters.CameraAvStreamManagement.server.commands.VideoStreamAllocate(device, endpoint_id,
     stream_usage,
     camera_fields.video_stream_defaults.codec,
-    camera_fields.video_stream_defaults.min_frame_rate,
-    math.min(device:get_field(camera_fields.MAX_FRAMES_PER_SECOND) or camera_fields.video_stream_defaults.max_frame_rate,
-      camera_fields.video_stream_defaults.max_frame_rate),
+    min_frame_rate,
+    max_frame_rate,
     min_resolution or device:get_field(camera_fields.MIN_RESOLUTION) or camera_fields.video_stream_defaults.min_resolution,
     max_resolution or device:get_field(camera_fields.MAX_RESOLUTION) or camera_fields.video_stream_defaults.max_resolution,
     camera_fields.video_stream_defaults.min_bitrate,
diff --git a/src/sub_drivers/camera/camera_utils/utils.lua b/src/sub_drivers/camera/camera_utils/utils.lua
index 78792b9..031bb92 100644
--- a/src/sub_drivers/camera/camera_utils/utils.lua
+++ b/src/sub_drivers/camera/camera_utils/utils.lua
@@ -103,6 +103,8 @@ function CameraUtils.compute_fps(max_encoded_pixel_rate, width, height, max_fps)
 end
 
 function CameraUtils.build_supported_resolutions(device, max_encoded_pixel_rate, max_fps)
+  -- Keep advertised FPS within the same ceiling used by VideoStreamAllocate.
+  max_fps = math.min(max_fps, camera_fields.video_stream_defaults.max_frame_rate)
   local resolutions = {}
   local added_resolutions = {}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions