From bf77d6333c99ddfc1f63d774ea4cf9f49c4212ad Mon Sep 17 00:00:00 2001 From: Nick DeBoom Date: Wed, 14 Jan 2026 15:01:42 -0600 Subject: [PATCH] Matter Camera: Include all supported resolutions This change adds logic for including the resolutions exposed by the VideoSensorParams and MinViewport attributes in supportedResolutions, rather than only looking at the resolution embedded within RateDistortionTradeOffPoints. --- .../camera_handlers/attribute_handlers.lua | 91 ++++++++----------- .../camera/camera_utils/fields.lua | 2 + .../sub_drivers/camera/camera_utils/utils.lua | 32 +++++++ .../src/test/test_matter_camera.lua | 38 +++++++- 4 files changed, 106 insertions(+), 57 deletions(-) diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/attribute_handlers.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/attribute_handlers.lua index 484e4c7a15..df99d50f94 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/attribute_handlers.lua @@ -113,9 +113,6 @@ end function CameraAttributeHandlers.rate_distortion_trade_off_points_handler(driver, device, ib, response) if not ib.data.elements then return end local resolutions = {} - local max_encoded_pixel_rate = device:get_field(camera_fields.MAX_ENCODED_PIXEL_RATE) - local max_fps = device:get_field(camera_fields.MAX_FRAMES_PER_SECOND) - local emit_capability = max_encoded_pixel_rate ~= nil and max_fps ~= nil for _, v in ipairs(ib.data.elements) do local rate_distortion_trade_off_points = v.elements local width = rate_distortion_trade_off_points.resolution.elements.width.value @@ -124,77 +121,63 @@ function CameraAttributeHandlers.rate_distortion_trade_off_points_handler(driver width = width, height = height }) - if emit_capability then - local fps = camera_utils.compute_fps(max_encoded_pixel_rate, width, height, max_fps) - if fps > 0 then - resolutions[#resolutions].fps = fps - end - end - end - if emit_capability then - device:emit_event_for_endpoint(ib, capabilities.videoStreamSettings.supportedResolutions(resolutions)) end device:set_field(camera_fields.SUPPORTED_RESOLUTIONS, resolutions) + local max_encoded_pixel_rate = device:get_field(camera_fields.MAX_ENCODED_PIXEL_RATE) + local max_fps = device:get_field(camera_fields.MAX_FRAMES_PER_SECOND) + if max_encoded_pixel_rate and max_fps and device:get_field(camera_fields.MAX_RESOLUTION) and device:get_field(camera_fields.MIN_RESOLUTION) then + local supported_resolutions = camera_utils.build_supported_resolutions(device, max_encoded_pixel_rate, max_fps) + device:emit_event_for_endpoint(ib, capabilities.videoStreamSettings.supportedResolutions(supported_resolutions)) + end end function CameraAttributeHandlers.max_encoded_pixel_rate_handler(driver, device, ib, response) - local resolutions = device:get_field(camera_fields.SUPPORTED_RESOLUTIONS) + device:set_field(camera_fields.MAX_ENCODED_PIXEL_RATE, ib.data.value) local max_fps = device:get_field(camera_fields.MAX_FRAMES_PER_SECOND) - local emit_capability = resolutions ~= nil and max_fps ~= nil - if emit_capability then - for _, v in pairs(resolutions or {}) do - local fps = camera_utils.compute_fps(ib.data.value, v.width, v.height, max_fps) - if fps > 0 then - v.fps = fps - end - end - device:emit_event_for_endpoint(ib, capabilities.videoStreamSettings.supportedResolutions(resolutions)) + if max_fps and device:get_field(camera_fields.SUPPORTED_RESOLUTIONS) and device:get_field(camera_fields.MAX_RESOLUTION) and device:get_field(camera_fields.MIN_RESOLUTION) then + local supported_resolutions = camera_utils.build_supported_resolutions(device, ib.data.value, max_fps) + device:emit_event_for_endpoint(ib, capabilities.videoStreamSettings.supportedResolutions(supported_resolutions)) end - device:set_field(camera_fields.MAX_ENCODED_PIXEL_RATE, ib.data.value) end function CameraAttributeHandlers.video_sensor_parameters_handler(driver, device, ib, response) if not ib.data.elements then return end - local resolutions = device:get_field(camera_fields.SUPPORTED_RESOLUTIONS) + local sensor_width = ib.data.elements.sensor_width.value + local sensor_height = ib.data.elements.sensor_height.value + local max_fps = ib.data.elements.max_fps.value + device:set_field(camera_fields.MAX_RESOLUTION, { + width = sensor_width, + height = sensor_height + }) + device:set_field(camera_fields.MAX_FRAMES_PER_SECOND, max_fps) + device:emit_event_for_endpoint(ib, capabilities.cameraViewportSettings.videoSensorParameters({ + width = sensor_width, + height = sensor_height, + maxFPS = max_fps + })) local max_encoded_pixel_rate = device:get_field(camera_fields.MAX_ENCODED_PIXEL_RATE) - local emit_capability = resolutions ~= nil and max_encoded_pixel_rate ~= nil - local sensor_width, sensor_height, max_fps - for _, v in pairs(ib.data.elements) do - if v.field_id == 0 then - sensor_width = v.value - elseif v.field_id == 1 then - sensor_height = v.value - elseif v.field_id == 2 then - max_fps = v.value - end - end - - if max_fps then - if sensor_width and sensor_height then - device:emit_event_for_endpoint(ib, capabilities.cameraViewportSettings.videoSensorParameters({ - width = sensor_width, - height = sensor_height, - maxFPS = max_fps - })) - end - if emit_capability then - for _, v in pairs(resolutions or {}) do - local fps = camera_utils.compute_fps(max_encoded_pixel_rate, v.width, v.height, max_fps) - if fps > 0 then - v.fps = fps - end - end - device:emit_event_for_endpoint(ib, capabilities.videoStreamSettings.supportedResolutions(resolutions)) - end - device:set_field(camera_fields.MAX_FRAMES_PER_SECOND, max_fps) + if max_encoded_pixel_rate and max_fps and device:get_field(camera_fields.SUPPORTED_RESOLUTIONS) and device:get_field(camera_fields.MIN_RESOLUTION) then + local supported_resolutions = camera_utils.build_supported_resolutions(device, max_encoded_pixel_rate, max_fps) + device:emit_event_for_endpoint(ib, capabilities.videoStreamSettings.supportedResolutions(supported_resolutions)) end end function CameraAttributeHandlers.min_viewport_handler(driver, device, ib, response) + if not ib.data.elements then return end device:emit_event_for_endpoint(ib, capabilities.cameraViewportSettings.minViewportResolution({ width = ib.data.elements.width.value, height = ib.data.elements.height.value })) + device:set_field(camera_fields.MIN_RESOLUTION, { + width = ib.data.elements.width.value, + height = ib.data.elements.height.value + }) + local max_encoded_pixel_rate = device:get_field(camera_fields.MAX_ENCODED_PIXEL_RATE) + local max_fps = device:get_field(camera_fields.MAX_FRAMES_PER_SECOND) + if max_encoded_pixel_rate and max_fps and device:get_field(camera_fields.SUPPORTED_RESOLUTIONS) and device:get_field(camera_fields.MAX_RESOLUTION) then + local supported_resolutions = camera_utils.build_supported_resolutions(device, max_encoded_pixel_rate, max_fps) + device:emit_event_for_endpoint(ib, capabilities.videoStreamSettings.supportedResolutions(supported_resolutions)) + end end function CameraAttributeHandlers.allocated_video_streams_handler(driver, device, ib, response) diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/fields.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/fields.lua index 677e2d5dd6..7598b89893 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/fields.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/fields.lua @@ -10,6 +10,8 @@ CameraFields.MAX_FRAMES_PER_SECOND = "__max_frames_per_second" CameraFields.MAX_VOLUME_LEVEL = "__max_volume_level" CameraFields.MIN_VOLUME_LEVEL = "__min_volume_level" CameraFields.SUPPORTED_RESOLUTIONS = "__supported_resolutions" +CameraFields.MAX_RESOLUTION = "__max_resolution" +CameraFields.MIN_RESOLUTION = "__min_resolution" CameraFields.TRIGGERED_ZONES = "__triggered_zones" CameraFields.VIEWPORT = "__viewport" diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua index a93e757c16..1caa9737bb 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua @@ -102,6 +102,38 @@ function CameraUtils.compute_fps(max_encoded_pixel_rate, width, height, max_fps) return math.tointeger(math.floor(fps / fps_step) * fps_step) end +function CameraUtils.build_supported_resolutions(device, max_encoded_pixel_rate, max_fps) + local resolutions = {} + local added_resolutions = {} + + local function add_resolution(width, height) + local key = width .. "x" .. height + if not added_resolutions[key] then + local resolution = { width = width, height = height } + resolution.fps = CameraUtils.compute_fps(max_encoded_pixel_rate, width, height, max_fps) + table.insert(resolutions, resolution) + added_resolutions[key] = true + end + end + + local min_resolution = device:get_field(camera_fields.MIN_RESOLUTION) + if min_resolution then + add_resolution(min_resolution.width, min_resolution.height) + end + + local trade_off_resolutions = device:get_field(camera_fields.SUPPORTED_RESOLUTIONS) + for _, v in pairs(trade_off_resolutions or {}) do + add_resolution(v.width, v.height) + end + + local max_resolution = device:get_field(camera_fields.MAX_RESOLUTION) + if max_resolution then + add_resolution(max_resolution.width, max_resolution.height) + end + + return resolutions +end + function CameraUtils.profile_changed(synced_components, prev_components) if #synced_components ~= #prev_components then return true diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua index 1028197590..b15034779d 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua @@ -683,6 +683,18 @@ local function receive_max_encoded_pixel_rate() }) end +local function receive_min_viewport() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.attributes.MinViewportResolution:build_test_report_data( + mock_device, CAMERA_EP, clusters.CameraAvStreamManagement.types.VideoResolutionStruct({ + width = 1920, + height = 1080 + }) + ) + }) +end + local function receive_video_sensor_params() test.socket.matter:__queue_receive({ mock_device.id, @@ -697,6 +709,15 @@ local function receive_video_sensor_params() }) end +local function emit_min_viewport() + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.cameraViewportSettings.minViewportResolution({ + width = 1920, + height = 1080, + })) + ) +end + local function emit_video_sensor_parameters() test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.cameraViewportSettings.videoSensorParameters({ @@ -719,6 +740,11 @@ local function emit_supported_resolutions() width = 3840, height = 2160, fps = 15 + }, + { + width = 7360, + height = 4912, + fps = 0 } })) ) @@ -730,12 +756,14 @@ end -- videoStreamSettings.supportedResolutions is emitted after all three attributes are received. test.register_coroutine_test( - "Rate Distortion Trade Off Points, MaxEncodedPixelRate, VideoSensorParams reports should generate appropriate events", + "Rate Distortion Trade Off Points, MaxEncodedPixelRate, MinViewport, VideoSensorParams reports should generate appropriate events", function() update_device_profile() test.wait_for_events() receive_rate_distortion_trade_off_points() receive_max_encoded_pixel_rate() + receive_min_viewport() + emit_min_viewport() receive_video_sensor_params() emit_video_sensor_parameters() emit_supported_resolutions() @@ -743,11 +771,13 @@ test.register_coroutine_test( ) test.register_coroutine_test( - "Rate Distortion Trade Off Points, VideoSensorParams, MaxEncodedPixelRate reports should generate appropriate events", + "Rate Distortion Trade Off Points, MinViewport, VideoSensorParams, MaxEncodedPixelRate reports should generate appropriate events", function() update_device_profile() test.wait_for_events() receive_rate_distortion_trade_off_points() + receive_min_viewport() + emit_min_viewport() receive_video_sensor_params() emit_video_sensor_parameters() receive_max_encoded_pixel_rate() @@ -756,11 +786,13 @@ test.register_coroutine_test( ) test.register_coroutine_test( - "MaxEncodedPixelRate, VideoSensorParams, Rate Distortion Trade Off Points reports should generate appropriate events", + "MaxEncodedPixelRate, MinViewport, VideoSensorParams, Rate Distortion Trade Off Points reports should generate appropriate events", function() update_device_profile() test.wait_for_events() receive_max_encoded_pixel_rate() + receive_min_viewport() + emit_min_viewport() receive_video_sensor_params() emit_video_sensor_parameters() receive_rate_distortion_trade_off_points()