Skip to main content

rs_matter/dm/clusters/app/
cam_av_stream.rs

1/*
2 *
3 *    Copyright (c) 2026 Project CHIP Authors
4 *
5 *    Licensed under the Apache License, Version 2.0 (the "License");
6 *    you may not use this file except in compliance with the License.
7 *    You may obtain a copy of the License at
8 *
9 *        http://www.apache.org/licenses/LICENSE-2.0
10 *
11 *    Unless required by applicable law or agreed to in writing, software
12 *    distributed under the License is distributed on an "AS IS" BASIS,
13 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 *    See the License for the specific language governing permissions and
15 *    limitations under the License.
16 */
17
18//! Implementation of the Matter Camera AV Stream Management cluster (0x0551).
19//!
20//! Advertises the audio / video / snapshot streams a camera can produce and
21//! supports dynamic allocation/modification/deallocation of those streams by
22//! Matter controllers (notably SmartThings, Google Home, and the upstream
23//! `chip-tool` test suites).
24//!
25//! # Architecture (Pattern B1 — "Hooks")
26//!
27//! [`CameraAvStreamHandler`] owns the spec-defined state — the stream
28//! table, stable stream IDs, reference counts, and the mutable
29//! `StreamUsagePriorities` attribute — and performs all spec validation
30//! before delegating the side-effecting bits (open / close the encoder,
31//! flip watermark/OSD overlays) to a user-supplied
32//! [`CameraAvStreamHooks`] implementation.
33//!
34//! ```text
35//! ┌──────────────────────┐  ClusterAsyncHandler   ┌───────────────────┐
36//! │                      │◀── inbound commands ───│   rs-matter IM    │
37//! │ CameraAvStreamHandler│                        │     dispatcher    │
38//! └──────┬───────────────┘                        └───────────────────┘
39//!        │ delegates encoder open/modify/close
40//!        ▼
41//! ┌──────────────────────┐
42//! │ CameraAvStreamHooks  │  user-supplied (e.g. str0m / GStreamer)
43//! └──────────────────────┘
44//! ```
45//!
46//! # Cross-cluster reference counting
47//!
48//! `WebRTCTransportProvider` and `PushAvStreamTransport` both refer to
49//! video streams by ID. When such a transport binds a stream the consumer
50//! MUST call [`CameraAvStreamHandler::acquire_video`]; when it tears down,
51//! [`CameraAvStreamHandler::release_video`]. The handler refuses to
52//! deallocate any stream whose reference count is non-zero, mirroring the
53//! Matter 1.5 spec requirement.
54//!
55//! # Const generics
56//!
57//! * `NV` — maximum number of allocated video streams the handler can
58//!   hold at once. Spec MinLimit is 1 for any camera advertising the
59//!   `VIDEO` feature; commercial devices typically expose 2..=4.
60//!
61//! # Scope of v1
62//!
63//! * `VIDEO` feature only.
64//! * Full validation, allocation, modification, deallocation, priority
65//!   negotiation.
66//! * `WATERMARK` / `ON_SCREEN_DISPLAY` feature gating for the
67//!   per-stream toggles (not yet exposed at cluster level — once the
68//!   user sets those feature bits, modify accepts them).
69//! * NOT in scope (return `INVALID_ACTION` / `UNSUPPORTED_ATTRIBUTE`):
70//!   audio streams, snapshot streams, `CaptureSnapshot`, privacy modes,
71//!   night vision, image control, speaker / microphone, status light.
72//!   Each is a follow-up.
73
74use core::cell::{Cell, RefCell};
75use core::future::Future;
76
77use crate::dm::{ArrayAttributeRead, Cluster, Dataver, EndptId, InvokeContext, ReadContext};
78use crate::error::{Error, ErrorCode};
79use crate::tlv::{TLVArray, TLVBuilderParent, ToTLVArrayBuilder, ToTLVBuilder};
80use crate::utils::storage::Vec;
81use crate::utils::sync::blocking::Mutex;
82use crate::with;
83
84pub use crate::dm::clusters::decl::camera_av_stream_management::AudioCodecEnum;
85#[allow(unused_imports)]
86pub use crate::dm::clusters::decl::camera_av_stream_management::*;
87pub use crate::dm::clusters::decl::globals::StreamUsageEnum;
88
89use super::super::decl::camera_av_stream_management as decl;
90
91/// Static description of the camera image sensor.
92///
93/// Reported via the `VideoSensorParams` attribute (spec §"VideoSensorParams").
94#[derive(Debug, Clone, Copy)]
95#[cfg_attr(feature = "defmt", derive(defmt::Format))]
96pub struct VideoSensorParams {
97    pub sensor_width: u16,
98    pub sensor_height: u16,
99    pub max_fps: u16,
100    pub max_hdrfps: Option<u16>,
101}
102
103/// One operating point exposed via `RateDistortionTradeOffPoints`.
104///
105/// Each entry tells controllers "for codec X at minimum resolution
106/// (W,H), the encoder needs at least `min_bit_rate` bps." Controllers
107/// then build `VideoStreamAllocate` requests around those constraints.
108/// At least one entry is required by SmartThings to attempt a stream
109/// allocation; the validation logic in [`CameraAvStreamHandler`] insists
110/// every allocation references a codec present in this list.
111#[derive(Debug, Clone, Copy)]
112#[cfg_attr(feature = "defmt", derive(defmt::Format))]
113pub struct RateDistortionPoint {
114    pub codec: VideoCodecEnum,
115    pub min_resolution: (u16, u16),
116    pub min_bit_rate: u32,
117}
118
119/// One row in the `AllocatedAudioStreams` attribute.
120#[derive(Debug, Clone, Copy)]
121#[cfg_attr(feature = "defmt", derive(defmt::Format))]
122pub struct AudioStream {
123    pub audio_stream_id: u16,
124    pub stream_usage: StreamUsageEnum,
125    pub audio_codec: AudioCodecEnum,
126    pub channel_count: u8,
127    pub sample_rate: u32,
128    pub bit_rate: u32,
129    pub bit_depth: u8,
130    pub reference_count: u8,
131}
132
133/// Static description of the microphone, reported via `MicrophoneCapabilities`.
134#[derive(Debug, Clone, Copy)]
135pub struct AudioCapabilitiesConfig<'a> {
136    pub max_channels: u8,
137    pub supported_codecs: &'a [AudioCodecEnum],
138    pub supported_sample_rates: &'a [u32],
139    pub supported_bit_depths: &'a [u8],
140}
141
142/// One row in the `AllocatedVideoStreams` attribute.
143///
144/// The `reference_count` field is fully managed by the handler — callers
145/// of `allocate_video` / `modify_video` in [`CameraAvStreamHooks`] should
146/// ignore it; cross-cluster consumers (WebRTC, PushAV) adjust it via
147/// [`CameraAvStreamHandler::acquire_video`] /
148/// [`CameraAvStreamHandler::release_video`].
149#[derive(Debug, Clone, Copy)]
150#[cfg_attr(feature = "defmt", derive(defmt::Format))]
151pub struct VideoStream {
152    pub video_stream_id: u16,
153    pub stream_usage: StreamUsageEnum,
154    pub video_codec: VideoCodecEnum,
155    pub min_frame_rate: u16,
156    pub max_frame_rate: u16,
157    pub min_width: u16,
158    pub min_height: u16,
159    pub max_width: u16,
160    pub max_height: u16,
161    pub min_bit_rate: u32,
162    pub max_bit_rate: u32,
163    pub key_frame_interval: u16,
164    pub watermark_enabled: Option<bool>,
165    pub osd_enabled: Option<bool>,
166    pub reference_count: u8,
167}
168
169/// Errors a [`CameraAvStreamHooks`] implementation can surface back to
170/// the cluster. Each maps to a Matter cluster-status code.
171#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
172#[cfg_attr(feature = "defmt", derive(defmt::Format))]
173pub enum CamAvError {
174    /// `RESOURCE_EXHAUSTED` — encoder cannot accept another concurrent stream.
175    ResourceExhausted,
176    /// `DYNAMIC_CONSTRAINT_ERROR` — combination of params is unsupported
177    /// at runtime (e.g. requested bitrate exceeds what the codec can do
178    /// for the requested resolution).
179    DynamicConstraint,
180    /// `NOT_FOUND` — referenced stream ID does not exist.
181    NotFound,
182    /// `FAILURE` — any other hooks-level failure.
183    Failure,
184}
185
186impl From<CamAvError> for Error {
187    fn from(e: CamAvError) -> Self {
188        match e {
189            CamAvError::ResourceExhausted => ErrorCode::ResourceExhausted.into(),
190            CamAvError::DynamicConstraint => ErrorCode::ConstraintError.into(),
191            CamAvError::NotFound => ErrorCode::NotFound.into(),
192            CamAvError::Failure => ErrorCode::Failure.into(),
193        }
194    }
195}
196
197/// Application hooks for the side-effecting pieces of stream lifecycle.
198///
199/// All spec validation (priority membership, codec availability,
200/// resolution/framerate/bitrate min<=max, viewport floor, sensor ceiling,
201/// reference-count protection on deallocate) is done by
202/// [`CameraAvStreamHandler`] before any of these methods run. Implementors
203/// only need to interact with their actual encoder / camera.
204pub trait CameraAvStreamHooks {
205    /// Called when a new video stream has been validated and assigned a
206    /// stable ID. The implementation should provision encoder resources;
207    /// returning `Err` aborts the allocation (the stream is NOT added to
208    /// the cluster's `AllocatedVideoStreams` attribute and no ID is
209    /// returned to the controller).
210    ///
211    /// `stream` carries the just-assigned `video_stream_id` and the
212    /// validated parameters. `reference_count` is always 0 here.
213    fn allocate_video(&self, stream: &VideoStream) -> impl Future<Output = Result<(), CamAvError>>;
214
215    /// Called when a controller requests `VideoStreamModify`.
216    ///
217    /// The handler has already verified the stream exists and that the
218    /// requested toggles are legal under the advertised feature set.
219    /// `watermark_enabled` / `osd_enabled` carry the requested new
220    /// values (`None` = not changing). On success the cluster table is
221    /// updated to match; on `Err` the table is left unchanged.
222    fn modify_video(
223        &self,
224        video_stream_id: u16,
225        watermark_enabled: Option<bool>,
226        osd_enabled: Option<bool>,
227    ) -> impl Future<Output = Result<(), CamAvError>>;
228
229    /// Called when a controller requests `VideoStreamDeallocate` AND
230    /// the handler has confirmed the stream's reference count is zero.
231    fn deallocate_video(
232        &self,
233        video_stream_id: u16,
234    ) -> impl Future<Output = Result<(), CamAvError>>;
235}
236
237impl<T> CameraAvStreamHooks for &T
238where
239    T: CameraAvStreamHooks,
240{
241    fn allocate_video(&self, stream: &VideoStream) -> impl Future<Output = Result<(), CamAvError>> {
242        (*self).allocate_video(stream)
243    }
244
245    fn modify_video(
246        &self,
247        video_stream_id: u16,
248        watermark_enabled: Option<bool>,
249        osd_enabled: Option<bool>,
250    ) -> impl Future<Output = Result<(), CamAvError>> {
251        (*self).modify_video(video_stream_id, watermark_enabled, osd_enabled)
252    }
253
254    fn deallocate_video(
255        &self,
256        video_stream_id: u16,
257    ) -> impl Future<Output = Result<(), CamAvError>> {
258        (*self).deallocate_video(video_stream_id)
259    }
260}
261
262/// Maximum number of `StreamUsageEnum` entries the handler will hold for
263/// the (mutable) `StreamUsagePriorities` attribute. Spec defines six
264/// usages today; eight is comfortable headroom.
265const MAX_STREAM_USAGES: usize = 8;
266
267/// Static (non-allocatable) configuration for a [`CameraAvStreamHandler`].
268///
269/// Everything that does not change at runtime: sensor description,
270/// codec/resolution catalogue, supported usages, encoder limits.
271#[derive(Debug, Clone, Copy)]
272pub struct CameraAvStreamConfig<'a> {
273    pub max_concurrent_encoders: u8,
274    pub max_encoded_pixel_rate: u32,
275    pub sensor: VideoSensorParams,
276    /// Floor for both `MinViewportResolution` and any `min_resolution`
277    /// supplied by `VideoStreamAllocate`. Stored as `(width, height)`.
278    pub min_viewport: (u16, u16),
279    /// Reported via `MaxContentBufferSize`. Spec recommends ≥ 1 MiB.
280    pub max_content_buffer_size: u32,
281    /// Reported via `MaxNetworkBandwidth` (kbps).
282    pub max_network_bandwidth: u32,
283    /// Stable, server-defined list of every usage this camera *could*
284    /// expose. `StreamUsagePriorities` (mutable, see
285    /// `default_stream_usage_priorities`) is always a subset/permutation.
286    pub supported_stream_usages: &'a [StreamUsageEnum],
287    /// Initial priority order at boot. Must be a permutation of (a
288    /// subset of) `supported_stream_usages`.
289    pub default_stream_usage_priorities: &'a [StreamUsageEnum],
290    /// `RateDistortionTradeOffPoints` operating-points catalogue. At
291    /// least one entry strongly recommended for interop.
292    pub rate_distortion_points: &'a [RateDistortionPoint],
293    /// If `Some`, the `AUDIO` feature is active and this describes the
294    /// microphone via `MicrophoneCapabilities`. If `None`, only VIDEO is
295    /// supported (the `CLUSTER` constant; the `CLUSTER_VIDEO_AUDIO`
296    /// constant requires `Some`).
297    pub mic_capabilities: Option<AudioCapabilitiesConfig<'a>>,
298}
299
300struct State<const NV: usize, const NA: usize> {
301    videos: Vec<VideoStream, NV>,
302    audios: Vec<AudioStream, NA>,
303    stream_usage_priorities: Vec<StreamUsageEnum, MAX_STREAM_USAGES>,
304}
305
306impl<const NV: usize, const NA: usize> State<NV, NA> {
307    const fn new() -> Self {
308        Self {
309            videos: Vec::new(),
310            audios: Vec::new(),
311            stream_usage_priorities: Vec::new(),
312        }
313    }
314
315    fn find_video_mut(&mut self, id: u16) -> Result<&mut VideoStream, Error> {
316        self.videos
317            .iter_mut()
318            .find(|s| s.video_stream_id == id)
319            .ok_or_else(|| ErrorCode::NotFound.into())
320    }
321}
322
323/// Handler for the Camera AV Stream Management cluster (0x0551).
324///
325/// See the [module documentation](self) for architecture and usage.
326pub struct CameraAvStreamHandler<'a, H, const NV: usize, const NA: usize = 0>
327where
328    H: CameraAvStreamHooks,
329{
330    dataver: Dataver,
331    endpoint_id: EndptId,
332    config: CameraAvStreamConfig<'a>,
333    /// Bitfield of advertised feature bits — must match the value
334    /// returned by `Self::CLUSTER.feature_map`. Used only as a fast
335    /// in-handler test for `WATERMARK` / `ON_SCREEN_DISPLAY`.
336    features: u32,
337    hooks: H,
338    state: Mutex<RefCell<State<NV, NA>>>,
339    next_id: Mutex<Cell<u16>>,
340    next_audio_id: Mutex<Cell<u16>>,
341}
342
343impl<'a, H, const NV: usize, const NA: usize> CameraAvStreamHandler<'a, H, NV, NA>
344where
345    H: CameraAvStreamHooks,
346{
347    /// Cluster metadata advertising the `VIDEO` feature only and the
348    /// minimal mandatory attribute set under that feature.
349    ///
350    /// Use this when the camera does not (yet) need watermark / OSD /
351    /// audio / snapshot. For other combinations build a `Cluster` value
352    /// directly via the generated `decl::FULL_CLUSTER.with_features(...)`.
353    pub const CLUSTER: Cluster<'static> = decl::FULL_CLUSTER
354        .with_revision(1)
355        .with_features(decl::Feature::VIDEO.bits())
356        .with_attrs(with!(
357            required;
358            AttributeId::MaxConcurrentEncoders
359                | AttributeId::MaxEncodedPixelRate
360                | AttributeId::VideoSensorParams
361                | AttributeId::MinViewportResolution
362                | AttributeId::RateDistortionTradeOffPoints
363                | AttributeId::AllocatedVideoStreams
364        ))
365        .with_cmds(with!(
366            decl::CommandId::VideoStreamAllocate
367                | decl::CommandId::VideoStreamModify
368                | decl::CommandId::VideoStreamDeallocate
369                | decl::CommandId::SetStreamPriorities
370        ));
371
372    /// Cluster metadata advertising both `VIDEO` and `AUDIO` features.
373    ///
374    /// Use this (together with `CameraAvStreamConfig::mic_capabilities: Some(...)`)
375    /// when the camera also exposes a microphone and pre-allocated audio streams.
376    pub const CLUSTER_VIDEO_AUDIO: Cluster<'static> = decl::FULL_CLUSTER
377        .with_revision(1)
378        .with_features(decl::Feature::VIDEO.bits() | decl::Feature::AUDIO.bits())
379        .with_attrs(with!(
380            required;
381            AttributeId::MaxConcurrentEncoders
382                | AttributeId::MaxEncodedPixelRate
383                | AttributeId::VideoSensorParams
384                | AttributeId::MinViewportResolution
385                | AttributeId::RateDistortionTradeOffPoints
386                | AttributeId::AllocatedVideoStreams
387                | AttributeId::MicrophoneCapabilities
388                | AttributeId::AllocatedAudioStreams
389        ))
390        .with_cmds(with!(
391            decl::CommandId::VideoStreamAllocate
392                | decl::CommandId::VideoStreamModify
393                | decl::CommandId::VideoStreamDeallocate
394                | decl::CommandId::SetStreamPriorities
395        ));
396
397    /// Construct a new handler.
398    ///
399    /// `features` MUST equal the `feature_map` of the [`Cluster`] this
400    /// handler is registered with; the handler uses it to decide
401    /// whether `WATERMARK` / `ON_SCREEN_DISPLAY` toggles are legal in
402    /// `VideoStreamAllocate` / `VideoStreamModify`.
403    ///
404    /// Panics if `features` advertises `AUDIO` but
405    /// `config.mic_capabilities` is `None`. The `MicrophoneCapabilities`
406    /// attribute is mandatory whenever `AUDIO` is enabled (per Matter Spec)
407    /// and must be supplied at construction.
408    pub const fn new(
409        dataver: Dataver,
410        endpoint_id: EndptId,
411        config: CameraAvStreamConfig<'a>,
412        features: u32,
413        hooks: H,
414    ) -> Self {
415        // `core::assert!` is required here (not the crate-local `assert!`
416        // shim) because `::defmt::assert!` is not const-callable.
417        core::assert!(
418            (features & decl::Feature::AUDIO.bits()) == 0 || config.mic_capabilities.is_some(),
419            "CameraAvStreamHandler: AUDIO feature requires `config.mic_capabilities` to be Some",
420        );
421        Self {
422            dataver,
423            endpoint_id,
424            config,
425            features,
426            hooks,
427            state: Mutex::new(RefCell::new(State::new())),
428            next_id: Mutex::new(Cell::new(1)),
429            next_audio_id: Mutex::new(Cell::new(1)),
430        }
431    }
432
433    /// Wrap in the generic async adaptor for registration with a
434    /// `rs-matter` `Node`.
435    pub const fn adapt(self) -> decl::HandlerAsyncAdaptor<Self> {
436        decl::HandlerAsyncAdaptor(self)
437    }
438
439    /// Endpoint this handler is mounted on.
440    pub const fn endpoint_id(&self) -> EndptId {
441        self.endpoint_id
442    }
443
444    /// Increment the reference count of an allocated video stream so
445    /// that [`Self::deallocate_video`] cannot remove it underneath the
446    /// caller. Cross-cluster consumers (WebRTC, PushAV) MUST pair this
447    /// with [`Self::release_video`] when their session ends.
448    ///
449    /// Returns `NOT_FOUND` if the stream does not exist.
450    pub fn acquire_video(&self, video_stream_id: u16) -> Result<(), Error> {
451        let changed = self.state.lock(|cell| -> Result<bool, Error> {
452            let mut state = cell.borrow_mut();
453            let row = state.find_video_mut(video_stream_id)?;
454            row.reference_count = row.reference_count.saturating_add(1);
455            Ok(true)
456        })?;
457        if changed {
458            self.dataver.changed();
459        }
460        Ok(())
461    }
462
463    /// Decrement the reference count of an allocated video stream
464    /// (saturating at 0). Best-effort: silently ignores unknown IDs so
465    /// that callers can release in `Drop` without an extra existence
466    /// check after a parallel deallocation.
467    pub fn release_video(&self, video_stream_id: u16) {
468        let changed = self.state.lock(|cell| {
469            let mut state = cell.borrow_mut();
470            if let Some(row) = state
471                .videos
472                .iter_mut()
473                .find(|s| s.video_stream_id == video_stream_id)
474            {
475                row.reference_count = row.reference_count.saturating_sub(1);
476                true
477            } else {
478                false
479            }
480        });
481        if changed {
482            self.dataver.changed();
483        }
484    }
485
486    /// Snapshot the current set of allocated video streams. Useful for
487    /// applications that want to query state outside the data-model
488    /// dispatch path (e.g. logging, diagnostics).
489    pub fn video_streams(&self) -> Vec<VideoStream, NV> {
490        self.state.lock(|cell| cell.borrow().videos.clone())
491    }
492
493    /// Pre-seed an entry into `AllocatedVideoStreams` at boot without
494    /// invoking [`CameraAvStreamHooks::allocate_video`]. Suitable for
495    /// devices that have a single fixed encoder configuration baked
496    /// into the firmware and only need the cluster as a registry.
497    ///
498    /// The stream's `video_stream_id` field is overwritten with a
499    /// freshly allocated ID; `reference_count` is reset to 0. No spec
500    /// validation is performed — the caller is presumed to know what
501    /// the device can do.
502    ///
503    /// Returns the assigned ID, or `RESOURCE_EXHAUSTED` if the table
504    /// is full.
505    pub fn add_preallocated_video(&self, mut stream: VideoStream) -> Result<u16, Error> {
506        stream.reference_count = 0;
507        stream.video_stream_id = self.alloc_video_id();
508        let id = stream.video_stream_id;
509        let pushed = self.state.lock(|cell| {
510            let mut state = cell.borrow_mut();
511            state.videos.push(stream).is_ok()
512        });
513        if !pushed {
514            return Err(ErrorCode::ResourceExhausted.into());
515        }
516        self.dataver.changed();
517        Ok(id)
518    }
519
520    /// Snapshot the current set of allocated audio streams.
521    pub fn audio_streams(&self) -> Vec<AudioStream, NA> {
522        self.state.lock(|cell| cell.borrow().audios.clone())
523    }
524
525    /// Pre-seed an entry into `AllocatedAudioStreams` at boot without
526    /// calling any hooks. Mirrors [`Self::add_preallocated_video`].
527    ///
528    /// Returns the assigned ID, or `RESOURCE_EXHAUSTED` if the table
529    /// is full (or `NA = 0`).
530    pub fn add_preallocated_audio(&self, mut stream: AudioStream) -> Result<u16, Error> {
531        stream.reference_count = 0;
532        stream.audio_stream_id = self.alloc_audio_id();
533        let id = stream.audio_stream_id;
534        let pushed = self.state.lock(|cell| {
535            let mut state = cell.borrow_mut();
536            state.audios.push(stream).is_ok()
537        });
538        if !pushed {
539            return Err(ErrorCode::ResourceExhausted.into());
540        }
541        self.dataver.changed();
542        Ok(id)
543    }
544
545    // ----- internals -----
546
547    /// Allocate the next free `video_stream_id`. Wraps to 1 if `next_id`
548    /// rolls over `u16::MAX` (vanishingly unlikely on a real device but
549    /// cheap to defend against).
550    fn alloc_video_id(&self) -> u16 {
551        self.next_id.lock(|cell| {
552            let mut id = cell.get();
553            if id == 0 {
554                id = 1;
555            }
556            cell.set(id.wrapping_add(1).max(1));
557            id
558        })
559    }
560
561    fn alloc_audio_id(&self) -> u16 {
562        self.next_audio_id.lock(|cell| {
563            let mut id = cell.get();
564            if id == 0 {
565                id = 1;
566            }
567            cell.set(id.wrapping_add(1).max(1));
568            id
569        })
570    }
571
572    /// Initialise the mutable `StreamUsagePriorities` table from
573    /// `config.default_stream_usage_priorities` exactly once.
574    fn ensure_priorities_seeded(&self) {
575        self.state.lock(|cell| {
576            let mut state = cell.borrow_mut();
577            if state.stream_usage_priorities.is_empty() {
578                for u in self.config.default_stream_usage_priorities {
579                    let _ = state.stream_usage_priorities.push(*u);
580                }
581            }
582        });
583    }
584
585    fn has_feature(&self, bit: u32) -> bool {
586        self.features & bit != 0
587    }
588
589    /// Validate a `VideoStreamAllocate` request against the spec and
590    /// the static config. Returns a fresh [`VideoStream`] (without an
591    /// ID) on success.
592    fn validate_video_alloc(
593        &self,
594        request: &VideoStreamAllocateRequest<'_>,
595    ) -> Result<VideoStream, Error> {
596        let stream_usage = request.stream_usage()?;
597        let video_codec = request.video_codec()?;
598        let min_frame_rate = request.min_frame_rate()?;
599        let max_frame_rate = request.max_frame_rate()?;
600        let min_resolution = request.min_resolution()?;
601        let max_resolution = request.max_resolution()?;
602        let min_bit_rate = request.min_bit_rate()?;
603        let max_bit_rate = request.max_bit_rate()?;
604        let key_frame_interval = request.key_frame_interval()?;
605        let watermark = request.watermark_enabled()?;
606        let osd = request.osd_enabled()?;
607
608        let min_w = min_resolution.width()?;
609        let min_h = min_resolution.height()?;
610        let max_w = max_resolution.width()?;
611        let max_h = max_resolution.height()?;
612
613        // CONSTRAINT: stream usage is reserved.
614        if matches!(stream_usage, StreamUsageEnum::Internal) {
615            return Err(ErrorCode::ConstraintError.into());
616        }
617
618        // CONSTRAINT: stream usage must be a known one.
619        if !self.config.supported_stream_usages.contains(&stream_usage) {
620            return Err(ErrorCode::ConstraintError.into());
621        }
622
623        // INVALID_IN_STATE: stream usage absent from the (mutable)
624        // priority list — see TC_AVSM_2_7 step 18.
625        let in_priorities = self.state.lock(|cell| {
626            cell.borrow()
627                .stream_usage_priorities
628                .contains(&stream_usage)
629        });
630        if !in_priorities {
631            return Err(ErrorCode::InvalidAction.into());
632        }
633
634        // CONSTRAINT: codec must appear in RateDistortionTradeOffPoints
635        // (TC_AVSM_2_7 keys all subsequent steps off
636        // aRateDistortionTradeOffPoints[0].codec).
637        if !self
638            .config
639            .rate_distortion_points
640            .iter()
641            .any(|p| p.codec == video_codec)
642        {
643            return Err(ErrorCode::ConstraintError.into());
644        }
645
646        // CONSTRAINT: monotonic min ≤ max.
647        if min_frame_rate == 0
648            || min_frame_rate > max_frame_rate
649            || min_bit_rate > max_bit_rate
650            || min_w == 0
651            || min_h == 0
652            || min_w > max_w
653            || min_h > max_h
654        {
655            return Err(ErrorCode::ConstraintError.into());
656        }
657
658        // CONSTRAINT: framerate within sensor capability.
659        if max_frame_rate > self.config.sensor.max_fps {
660            return Err(ErrorCode::ConstraintError.into());
661        }
662
663        // CONSTRAINT: resolution within sensor and viewport bounds.
664        if max_w > self.config.sensor.sensor_width
665            || max_h > self.config.sensor.sensor_height
666            || min_w < self.config.min_viewport.0
667            || min_h < self.config.min_viewport.1
668        {
669            return Err(ErrorCode::ConstraintError.into());
670        }
671
672        // CONSTRAINT: watermark / OSD only legal if the matching
673        // feature is advertised.
674        if watermark.is_some() && !self.has_feature(decl::Feature::WATERMARK.bits()) {
675            return Err(ErrorCode::ConstraintError.into());
676        }
677        if osd.is_some() && !self.has_feature(decl::Feature::ON_SCREEN_DISPLAY.bits()) {
678            return Err(ErrorCode::ConstraintError.into());
679        }
680
681        Ok(VideoStream {
682            video_stream_id: 0, // assigned later by alloc_video_id
683            stream_usage,
684            video_codec,
685            min_frame_rate,
686            max_frame_rate,
687            min_width: min_w,
688            min_height: min_h,
689            max_width: max_w,
690            max_height: max_h,
691            min_bit_rate,
692            max_bit_rate,
693            key_frame_interval,
694            watermark_enabled: watermark,
695            osd_enabled: osd,
696            reference_count: 0,
697        })
698    }
699
700    /// Idempotency: if an existing stream has byte-for-byte matching
701    /// allocation params, return its ID instead of creating a new one
702    /// (spec §"Allocation idempotency"). Compares everything except
703    /// `video_stream_id` and `reference_count`.
704    fn find_matching_existing(&self, candidate: &VideoStream) -> Option<u16> {
705        self.state.lock(|cell| {
706            cell.borrow().videos.iter().find_map(|s| {
707                if s.stream_usage == candidate.stream_usage
708                    && s.video_codec == candidate.video_codec
709                    && s.min_frame_rate == candidate.min_frame_rate
710                    && s.max_frame_rate == candidate.max_frame_rate
711                    && s.min_width == candidate.min_width
712                    && s.min_height == candidate.min_height
713                    && s.max_width == candidate.max_width
714                    && s.max_height == candidate.max_height
715                    && s.min_bit_rate == candidate.min_bit_rate
716                    && s.max_bit_rate == candidate.max_bit_rate
717                    && s.key_frame_interval == candidate.key_frame_interval
718                    && s.watermark_enabled == candidate.watermark_enabled
719                    && s.osd_enabled == candidate.osd_enabled
720                {
721                    Some(s.video_stream_id)
722                } else {
723                    None
724                }
725            })
726        })
727    }
728}
729
730impl<'a, H, const NV: usize, const NA: usize> ClusterAsyncHandler
731    for CameraAvStreamHandler<'a, H, NV, NA>
732where
733    H: CameraAvStreamHooks,
734{
735    const CLUSTER: Cluster<'static> = Self::CLUSTER;
736
737    fn dataver(&self) -> u32 {
738        self.dataver.get()
739    }
740
741    fn dataver_changed(&self) {
742        self.dataver.changed();
743    }
744
745    async fn max_content_buffer_size(&self, _ctx: impl ReadContext) -> Result<u32, Error> {
746        Ok(self.config.max_content_buffer_size)
747    }
748
749    async fn max_network_bandwidth(&self, _ctx: impl ReadContext) -> Result<u32, Error> {
750        Ok(self.config.max_network_bandwidth)
751    }
752
753    async fn max_concurrent_encoders(&self, _ctx: impl ReadContext) -> Result<u8, Error> {
754        Ok(self.config.max_concurrent_encoders)
755    }
756
757    async fn max_encoded_pixel_rate(&self, _ctx: impl ReadContext) -> Result<u32, Error> {
758        Ok(self.config.max_encoded_pixel_rate)
759    }
760
761    async fn video_sensor_params<P: TLVBuilderParent>(
762        &self,
763        _ctx: impl ReadContext,
764        builder: VideoSensorParamsStructBuilder<P>,
765    ) -> Result<P, Error> {
766        builder
767            .sensor_width(self.config.sensor.sensor_width)?
768            .sensor_height(self.config.sensor.sensor_height)?
769            .max_fps(self.config.sensor.max_fps)?
770            .max_hdrfps(self.config.sensor.max_hdrfps)?
771            .end()
772    }
773
774    async fn min_viewport_resolution<P: TLVBuilderParent>(
775        &self,
776        _ctx: impl ReadContext,
777        builder: VideoResolutionStructBuilder<P>,
778    ) -> Result<P, Error> {
779        builder
780            .width(self.config.min_viewport.0)?
781            .height(self.config.min_viewport.1)?
782            .end()
783    }
784
785    async fn rate_distortion_trade_off_points<P: TLVBuilderParent>(
786        &self,
787        _ctx: impl ReadContext,
788        builder: ArrayAttributeRead<
789            RateDistortionTradeOffPointsStructArrayBuilder<P>,
790            RateDistortionTradeOffPointsStructBuilder<P>,
791        >,
792    ) -> Result<P, Error> {
793        match builder {
794            ArrayAttributeRead::ReadAll(mut b) => {
795                for p in self.config.rate_distortion_points {
796                    b = write_rate_distortion(b.push()?, p)?;
797                }
798                b.end()
799            }
800            ArrayAttributeRead::ReadOne(idx, b) => {
801                let Some(p) = self.config.rate_distortion_points.get(idx as usize) else {
802                    return Err(ErrorCode::ConstraintError.into());
803                };
804                write_rate_distortion(b, p)
805            }
806            ArrayAttributeRead::ReadNone(b) => b.end(),
807        }
808    }
809
810    async fn supported_stream_usages<P: TLVBuilderParent>(
811        &self,
812        _ctx: impl ReadContext,
813        builder: ArrayAttributeRead<
814            ToTLVArrayBuilder<P, StreamUsageEnum>,
815            ToTLVBuilder<P, StreamUsageEnum>,
816        >,
817    ) -> Result<P, Error> {
818        read_enum_array(builder, self.config.supported_stream_usages)
819    }
820
821    async fn stream_usage_priorities<P: TLVBuilderParent>(
822        &self,
823        _ctx: impl ReadContext,
824        builder: ArrayAttributeRead<
825            ToTLVArrayBuilder<P, StreamUsageEnum>,
826            ToTLVBuilder<P, StreamUsageEnum>,
827        >,
828    ) -> Result<P, Error> {
829        self.ensure_priorities_seeded();
830        let snapshot = self.state.lock(|cell| {
831            let s = cell.borrow();
832            // Avoid holding the lock across builder pushes.
833            let mut out: Vec<StreamUsageEnum, MAX_STREAM_USAGES> = Vec::new();
834            for u in s.stream_usage_priorities.iter() {
835                let _ = out.push(*u);
836            }
837            out
838        });
839        read_enum_array(builder, &snapshot)
840    }
841
842    async fn allocated_video_streams<P: TLVBuilderParent>(
843        &self,
844        _ctx: impl ReadContext,
845        builder: ArrayAttributeRead<VideoStreamStructArrayBuilder<P>, VideoStreamStructBuilder<P>>,
846    ) -> Result<P, Error> {
847        // Snapshot to avoid holding the lock across writer calls.
848        let snapshot = self.state.lock(|cell| cell.borrow().videos.clone());
849        match builder {
850            ArrayAttributeRead::ReadAll(mut b) => {
851                for s in snapshot.iter() {
852                    b = write_video_stream(b.push()?, s)?;
853                }
854                b.end()
855            }
856            ArrayAttributeRead::ReadOne(idx, b) => {
857                let Some(s) = snapshot.get(idx as usize) else {
858                    return Err(ErrorCode::ConstraintError.into());
859                };
860                write_video_stream(b, s)
861            }
862            ArrayAttributeRead::ReadNone(b) => b.end(),
863        }
864    }
865
866    // ----- Commands -----
867
868    async fn handle_video_stream_allocate<P: TLVBuilderParent>(
869        &self,
870        ctx: impl InvokeContext,
871        request: VideoStreamAllocateRequest<'_>,
872        response: VideoStreamAllocateResponseBuilder<P>,
873    ) -> Result<P, Error> {
874        self.ensure_priorities_seeded();
875        let mut candidate = self.validate_video_alloc(&request)?;
876
877        // Idempotency.
878        if let Some(existing) = self.find_matching_existing(&candidate) {
879            return response.video_stream_id(existing)?.end();
880        }
881
882        // Capacity. RESOURCE_EXHAUSTED per spec.
883        let full = self.state.lock(|cell| cell.borrow().videos.len() >= NV);
884        if full {
885            return Err(ErrorCode::ResourceExhausted.into());
886        }
887
888        // Assign ID and let the application open the encoder.
889        candidate.video_stream_id = self.alloc_video_id();
890        self.hooks.allocate_video(&candidate).await?;
891
892        // Commit. If push() fails (NV exhausted concurrently) the hooks
893        // already returned Ok and we leak a half-open stream — call
894        // deallocate to be tidy.
895        let pushed = self.state.lock(|cell| {
896            let mut state = cell.borrow_mut();
897            state.videos.push(candidate).is_ok()
898        });
899        if !pushed {
900            let _ = self.hooks.deallocate_video(candidate.video_stream_id).await;
901            return Err(ErrorCode::ResourceExhausted.into());
902        }
903        ctx.notify_own_attr_changed(AttributeId::AllocatedVideoStreams as _);
904
905        response.video_stream_id(candidate.video_stream_id)?.end()
906    }
907
908    async fn handle_video_stream_modify(
909        &self,
910        ctx: impl InvokeContext,
911        request: VideoStreamModifyRequest<'_>,
912    ) -> Result<(), Error> {
913        let id = request.video_stream_id()?;
914        let watermark = request.watermark_enabled()?;
915        let osd = request.osd_enabled()?;
916
917        if watermark.is_some() && !self.has_feature(decl::Feature::WATERMARK.bits()) {
918            return Err(ErrorCode::ConstraintError.into());
919        }
920        if osd.is_some() && !self.has_feature(decl::Feature::ON_SCREEN_DISPLAY.bits()) {
921            return Err(ErrorCode::ConstraintError.into());
922        }
923
924        // Existence check up-front so we can return NOT_FOUND without
925        // bothering the hooks.
926        let exists = self
927            .state
928            .lock(|cell| cell.borrow().videos.iter().any(|s| s.video_stream_id == id));
929        if !exists {
930            return Err(ErrorCode::NotFound.into());
931        }
932
933        self.hooks.modify_video(id, watermark, osd).await?;
934
935        self.state.lock(|cell| {
936            let mut state = cell.borrow_mut();
937            if let Some(row) = state.videos.iter_mut().find(|s| s.video_stream_id == id) {
938                if let Some(w) = watermark {
939                    row.watermark_enabled = Some(w);
940                }
941                if let Some(o) = osd {
942                    row.osd_enabled = Some(o);
943                }
944            }
945        });
946        ctx.notify_own_attr_changed(AttributeId::AllocatedVideoStreams as _);
947        Ok(())
948    }
949
950    async fn handle_video_stream_deallocate(
951        &self,
952        ctx: impl InvokeContext,
953        request: VideoStreamDeallocateRequest<'_>,
954    ) -> Result<(), Error> {
955        let id = request.video_stream_id()?;
956
957        // Spec: cannot deallocate a stream still bound to a transport.
958        // Map "in use" to INVALID_IN_STATE; "missing" to NOT_FOUND.
959        let status = self.state.lock(|cell| {
960            let state = cell.borrow();
961            match state.videos.iter().find(|s| s.video_stream_id == id) {
962                None => Err(ErrorCode::NotFound),
963                Some(s) if s.reference_count > 0 => Err(ErrorCode::InvalidAction),
964                Some(_) => Ok(()),
965            }
966        });
967        status.map_err(Error::from)?;
968
969        self.hooks.deallocate_video(id).await?;
970
971        self.state.lock(|cell| {
972            let mut state = cell.borrow_mut();
973            state.videos.retain(|s| s.video_stream_id != id);
974        });
975        ctx.notify_own_attr_changed(AttributeId::AllocatedVideoStreams as _);
976        Ok(())
977    }
978
979    async fn handle_set_stream_priorities(
980        &self,
981        ctx: impl InvokeContext,
982        request: SetStreamPrioritiesRequest<'_>,
983    ) -> Result<(), Error> {
984        let new_prio: TLVArray<'_, StreamUsageEnum> = request.stream_priorities()?;
985
986        // Build the new list while validating (a) every entry is in
987        // SupportedStreamUsages, (b) no duplicates, (c) it fits.
988        let mut buffer: Vec<StreamUsageEnum, MAX_STREAM_USAGES> = Vec::new();
989        for entry in new_prio.iter() {
990            let usage = entry?;
991            if !self.config.supported_stream_usages.contains(&usage) {
992                return Err(ErrorCode::ConstraintError.into());
993            }
994            if buffer.contains(&usage) {
995                return Err(ErrorCode::ConstraintError.into());
996            }
997            buffer
998                .push(usage)
999                .map_err(|_| Error::from(ErrorCode::ResourceExhausted))?;
1000        }
1001
1002        self.state.lock(|cell| {
1003            let mut state = cell.borrow_mut();
1004            state.stream_usage_priorities.clear();
1005            for u in buffer.iter() {
1006                let _ = state.stream_usage_priorities.push(*u);
1007            }
1008        });
1009        ctx.notify_own_attr_changed(AttributeId::StreamUsagePriorities as _);
1010        Ok(())
1011    }
1012
1013    async fn microphone_capabilities<P: TLVBuilderParent>(
1014        &self,
1015        _ctx: impl ReadContext,
1016        builder: AudioCapabilitiesStructBuilder<P>,
1017    ) -> Result<P, Error> {
1018        let Some(cfg) = self.config.mic_capabilities else {
1019            return Err(ErrorCode::InvalidAction.into());
1020        };
1021        let b = builder.max_number_of_channels(cfg.max_channels)?;
1022        let mut codecs = b.supported_codecs()?;
1023        for codec in cfg.supported_codecs {
1024            codecs = codecs.push(codec)?;
1025        }
1026        let b = codecs.end()?;
1027        let mut rates = b.supported_sample_rates()?;
1028        for r in cfg.supported_sample_rates {
1029            rates = rates.push(r)?;
1030        }
1031        let b = rates.end()?;
1032        let mut depths = b.supported_bit_depths()?;
1033        for d in cfg.supported_bit_depths {
1034            depths = depths.push(d)?;
1035        }
1036        depths.end()?.end()
1037    }
1038
1039    async fn allocated_audio_streams<P: TLVBuilderParent>(
1040        &self,
1041        _ctx: impl ReadContext,
1042        builder: ArrayAttributeRead<AudioStreamStructArrayBuilder<P>, AudioStreamStructBuilder<P>>,
1043    ) -> Result<P, Error> {
1044        let snapshot = self.state.lock(|cell| cell.borrow().audios.clone());
1045        match builder {
1046            ArrayAttributeRead::ReadAll(mut b) => {
1047                for s in snapshot.iter() {
1048                    b = write_audio_stream(b.push()?, s)?;
1049                }
1050                b.end()
1051            }
1052            ArrayAttributeRead::ReadOne(idx, b) => {
1053                let Some(s) = snapshot.get(idx as usize) else {
1054                    return Err(ErrorCode::ConstraintError.into());
1055                };
1056                write_audio_stream(b, s)
1057            }
1058            ArrayAttributeRead::ReadNone(b) => b.end(),
1059        }
1060    }
1061
1062    // ----- Commands deferred to follow-up sessions -----
1063
1064    async fn handle_audio_stream_allocate<P: TLVBuilderParent>(
1065        &self,
1066        _ctx: impl InvokeContext,
1067        _request: AudioStreamAllocateRequest<'_>,
1068        _response: AudioStreamAllocateResponseBuilder<P>,
1069    ) -> Result<P, Error> {
1070        Err(ErrorCode::CommandNotFound.into())
1071    }
1072
1073    async fn handle_audio_stream_deallocate(
1074        &self,
1075        _ctx: impl InvokeContext,
1076        _request: AudioStreamDeallocateRequest<'_>,
1077    ) -> Result<(), Error> {
1078        Err(ErrorCode::CommandNotFound.into())
1079    }
1080
1081    async fn handle_snapshot_stream_allocate<P: TLVBuilderParent>(
1082        &self,
1083        _ctx: impl InvokeContext,
1084        _request: SnapshotStreamAllocateRequest<'_>,
1085        _response: SnapshotStreamAllocateResponseBuilder<P>,
1086    ) -> Result<P, Error> {
1087        Err(ErrorCode::CommandNotFound.into())
1088    }
1089
1090    async fn handle_snapshot_stream_modify(
1091        &self,
1092        _ctx: impl InvokeContext,
1093        _request: SnapshotStreamModifyRequest<'_>,
1094    ) -> Result<(), Error> {
1095        Err(ErrorCode::CommandNotFound.into())
1096    }
1097
1098    async fn handle_snapshot_stream_deallocate(
1099        &self,
1100        _ctx: impl InvokeContext,
1101        _request: SnapshotStreamDeallocateRequest<'_>,
1102    ) -> Result<(), Error> {
1103        Err(ErrorCode::CommandNotFound.into())
1104    }
1105
1106    async fn handle_capture_snapshot<P: TLVBuilderParent>(
1107        &self,
1108        _ctx: impl InvokeContext,
1109        _request: CaptureSnapshotRequest<'_>,
1110        _response: CaptureSnapshotResponseBuilder<P>,
1111    ) -> Result<P, Error> {
1112        Err(ErrorCode::CommandNotFound.into())
1113    }
1114}
1115
1116// -----------------------------------------------------------------------
1117// Local helpers
1118// -----------------------------------------------------------------------
1119
1120fn read_enum_array<P: TLVBuilderParent>(
1121    builder: ArrayAttributeRead<
1122        ToTLVArrayBuilder<P, StreamUsageEnum>,
1123        ToTLVBuilder<P, StreamUsageEnum>,
1124    >,
1125    items: &[StreamUsageEnum],
1126) -> Result<P, Error> {
1127    match builder {
1128        ArrayAttributeRead::ReadAll(mut b) => {
1129            for item in items {
1130                b = b.push(item)?;
1131            }
1132            b.end()
1133        }
1134        ArrayAttributeRead::ReadOne(idx, b) => {
1135            let Some(item) = items.get(idx as usize) else {
1136                return Err(ErrorCode::ConstraintError.into());
1137            };
1138            b.set(item)
1139        }
1140        ArrayAttributeRead::ReadNone(b) => b.end(),
1141    }
1142}
1143
1144fn write_video_stream<P: TLVBuilderParent>(
1145    builder: VideoStreamStructBuilder<P>,
1146    s: &VideoStream,
1147) -> Result<P, Error> {
1148    let b = builder
1149        .video_stream_id(s.video_stream_id)?
1150        .stream_usage(s.stream_usage)?
1151        .video_codec(s.video_codec)?
1152        .min_frame_rate(s.min_frame_rate)?
1153        .max_frame_rate(s.max_frame_rate)?;
1154    let b = b
1155        .min_resolution()?
1156        .width(s.min_width)?
1157        .height(s.min_height)?
1158        .end()?;
1159    let b = b
1160        .max_resolution()?
1161        .width(s.max_width)?
1162        .height(s.max_height)?
1163        .end()?;
1164    b.min_bit_rate(s.min_bit_rate)?
1165        .max_bit_rate(s.max_bit_rate)?
1166        .key_frame_interval(s.key_frame_interval)?
1167        .watermark_enabled(s.watermark_enabled)?
1168        .osd_enabled(s.osd_enabled)?
1169        .reference_count(s.reference_count)?
1170        .end()
1171}
1172
1173fn write_audio_stream<P: TLVBuilderParent>(
1174    builder: AudioStreamStructBuilder<P>,
1175    s: &AudioStream,
1176) -> Result<P, Error> {
1177    builder
1178        .audio_stream_id(s.audio_stream_id)?
1179        .stream_usage(s.stream_usage)?
1180        .audio_codec(s.audio_codec)?
1181        .channel_count(s.channel_count)?
1182        .sample_rate(s.sample_rate)?
1183        .bit_rate(s.bit_rate)?
1184        .bit_depth(s.bit_depth)?
1185        .reference_count(s.reference_count)?
1186        .end()
1187}
1188
1189fn write_rate_distortion<P: TLVBuilderParent>(
1190    builder: RateDistortionTradeOffPointsStructBuilder<P>,
1191    p: &RateDistortionPoint,
1192) -> Result<P, Error> {
1193    let b = builder.codec(p.codec)?;
1194    let b = b
1195        .resolution()?
1196        .width(p.min_resolution.0)?
1197        .height(p.min_resolution.1)?
1198        .end()?;
1199    b.min_bit_rate(p.min_bit_rate)?.end()
1200}