Skip to main content

oximedia_core/
codec_negotiation.rs

1//! Codec negotiation utilities for `OxiMedia`.
2//!
3//! This module provides types and functions for negotiating codec parameters
4//! between local and remote endpoints, preferring hardware-accelerated codecs
5//! when available.
6//!
7//! # Example
8//!
9//! ```
10//! use oximedia_core::codec_negotiation::{CodecCapability, CodecNegotiator, negotiate};
11//!
12//! let local = vec![
13//!     CodecCapability {
14//!         name: "av1".to_string(),
15//!         profiles: vec!["main".to_string()],
16//!         max_level: 40,
17//!         hardware_accelerated: false,
18//!     },
19//! ];
20//! let remote = vec![
21//!     CodecCapability {
22//!         name: "av1".to_string(),
23//!         profiles: vec!["main".to_string()],
24//!         max_level: 30,
25//!         hardware_accelerated: false,
26//!     },
27//! ];
28//! let result = negotiate(&local, &remote);
29//! assert!(result.is_some());
30//! assert_eq!(result.expect("negotiation succeeded").selected_codec, "av1");
31//! ```
32
33#![allow(dead_code)]
34
35/// Describes the codec capabilities of one endpoint.
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct CodecCapability {
38    /// Codec name (e.g. `"av1"`, `"vp9"`).
39    pub name: String,
40    /// List of supported codec profiles.
41    pub profiles: Vec<String>,
42    /// Maximum codec level supported (e.g. 40 for level 4.0).
43    pub max_level: u32,
44    /// Whether the codec benefits from hardware acceleration on this device.
45    pub hardware_accelerated: bool,
46}
47
48impl CodecCapability {
49    /// Creates a new `CodecCapability`.
50    #[must_use]
51    pub fn new(
52        name: impl Into<String>,
53        profiles: Vec<String>,
54        max_level: u32,
55        hardware_accelerated: bool,
56    ) -> Self {
57        Self {
58            name: name.into(),
59            profiles,
60            max_level,
61            hardware_accelerated,
62        }
63    }
64
65    /// Returns `true` if `profile` is listed in this capability.
66    #[must_use]
67    pub fn supports_profile(&self, profile: &str) -> bool {
68        self.profiles.iter().any(|p| p == profile)
69    }
70
71    /// Returns `true` if this codec uses hardware acceleration.
72    #[must_use]
73    pub fn is_hw_accelerated(&self) -> bool {
74        self.hardware_accelerated
75    }
76}
77
78/// Handles codec negotiation between a local and a remote set of capabilities.
79#[derive(Debug, Default)]
80pub struct CodecNegotiator {
81    /// Codecs supported locally.
82    pub local_caps: Vec<CodecCapability>,
83    /// Codecs supported by the remote endpoint.
84    pub remote_caps: Vec<CodecCapability>,
85}
86
87impl CodecNegotiator {
88    /// Creates an empty `CodecNegotiator`.
89    #[must_use]
90    pub fn new() -> Self {
91        Self::default()
92    }
93
94    /// Adds a local codec capability.
95    pub fn add_local(&mut self, cap: CodecCapability) {
96        self.local_caps.push(cap);
97    }
98
99    /// Adds a remote codec capability.
100    pub fn add_remote(&mut self, cap: CodecCapability) {
101        self.remote_caps.push(cap);
102    }
103
104    /// Returns the names of codecs supported by both endpoints.
105    #[must_use]
106    pub fn common_codecs(&self) -> Vec<&str> {
107        self.local_caps
108            .iter()
109            .filter(|l| self.remote_caps.iter().any(|r| r.name == l.name))
110            .map(|l| l.name.as_str())
111            .collect()
112    }
113
114    /// Returns the preferred common codec, favouring hardware-accelerated ones.
115    ///
116    /// Returns `None` when there are no codecs in common.
117    #[must_use]
118    pub fn preferred_codec(&self) -> Option<&str> {
119        // Collect common names first.
120        let common = self.common_codecs();
121        if common.is_empty() {
122            return None;
123        }
124        // Prefer hardware-accelerated; fall back to first common.
125        for name in &common {
126            if let Some(cap) = self.local_caps.iter().find(|c| &c.name.as_str() == name) {
127                if cap.hardware_accelerated {
128                    return Some(name);
129                }
130            }
131        }
132        common.into_iter().next()
133    }
134}
135
136/// The result of a successful codec negotiation.
137#[derive(Debug, Clone, PartialEq, Eq)]
138pub struct NegotiationResult {
139    /// The codec selected by both endpoints.
140    pub selected_codec: String,
141    /// The agreed-upon profile.
142    pub profile: String,
143    /// The agreed-upon level (minimum of local and remote max levels).
144    pub level: u32,
145    /// Whether the selected codec uses hardware acceleration on the local side.
146    pub hardware_accelerated: bool,
147}
148
149impl NegotiationResult {
150    /// Returns `true` if the selected codec uses hardware acceleration.
151    #[must_use]
152    pub fn is_hardware(&self) -> bool {
153        self.hardware_accelerated
154    }
155}
156
157/// Attempts to negotiate a codec between `local` and `remote` capability sets.
158///
159/// Hardware-accelerated codecs are preferred. The first common codec whose
160/// profile list has at least one entry in common is selected.
161///
162/// Returns `None` when no mutually supported codec/profile pair exists.
163#[must_use]
164pub fn negotiate(
165    local: &[CodecCapability],
166    remote: &[CodecCapability],
167) -> Option<NegotiationResult> {
168    // Build a prioritised list: hw-accelerated local caps first.
169    let mut ordered: Vec<&CodecCapability> = local.iter().collect();
170    ordered.sort_by_key(|c| u8::from(!c.hardware_accelerated));
171
172    for local_cap in ordered {
173        if let Some(remote_cap) = remote.iter().find(|r| r.name == local_cap.name) {
174            // Find a common profile.
175            let common_profile = local_cap
176                .profiles
177                .iter()
178                .find(|p| remote_cap.profiles.contains(p));
179            if let Some(profile) = common_profile {
180                let level = local_cap.max_level.min(remote_cap.max_level);
181                return Some(NegotiationResult {
182                    selected_codec: local_cap.name.clone(),
183                    profile: profile.clone(),
184                    level,
185                    hardware_accelerated: local_cap.hardware_accelerated,
186                });
187            }
188        }
189    }
190    None
191}
192
193// ─────────────────────────────────────────────────────────────────────────────
194// Automatic format negotiation
195// ─────────────────────────────────────────────────────────────────────────────
196
197use crate::types::{PixelFormat, SampleFormat};
198
199/// Pixel format capabilities of a codec.
200#[derive(Debug, Clone, PartialEq, Eq)]
201pub struct PixelFormatCaps {
202    /// Supported pixel formats, in order of preference.
203    pub formats: Vec<PixelFormat>,
204}
205
206impl PixelFormatCaps {
207    /// Creates new pixel-format capabilities.
208    #[must_use]
209    pub fn new(formats: Vec<PixelFormat>) -> Self {
210        Self { formats }
211    }
212
213    /// Returns the first format supported by both sides, or `None`.
214    #[must_use]
215    pub fn negotiate(&self, other: &Self) -> Option<PixelFormat> {
216        self.formats
217            .iter()
218            .find(|f| other.formats.contains(f))
219            .copied()
220    }
221
222    /// Returns all formats supported by both sides.
223    #[must_use]
224    pub fn common_formats(&self, other: &Self) -> Vec<PixelFormat> {
225        self.formats
226            .iter()
227            .filter(|f| other.formats.contains(f))
228            .copied()
229            .collect()
230    }
231}
232
233/// Audio sample format capabilities of a codec.
234#[derive(Debug, Clone, PartialEq, Eq)]
235pub struct SampleFormatCaps {
236    /// Supported sample formats, in order of preference.
237    pub formats: Vec<SampleFormat>,
238}
239
240impl SampleFormatCaps {
241    /// Creates new sample-format capabilities.
242    #[must_use]
243    pub fn new(formats: Vec<SampleFormat>) -> Self {
244        Self { formats }
245    }
246
247    /// Returns the first format supported by both sides, or `None`.
248    #[must_use]
249    pub fn negotiate(&self, other: &Self) -> Option<SampleFormat> {
250        self.formats
251            .iter()
252            .find(|f| other.formats.contains(f))
253            .copied()
254    }
255
256    /// Returns all formats supported by both sides.
257    #[must_use]
258    pub fn common_formats(&self, other: &Self) -> Vec<SampleFormat> {
259        self.formats
260            .iter()
261            .filter(|f| other.formats.contains(f))
262            .copied()
263            .collect()
264    }
265}
266
267/// Full format capabilities for automatic negotiation between encoder/decoder.
268#[derive(Debug, Clone)]
269pub struct FormatCapabilities {
270    /// Codec name (e.g. "av1", "opus").
271    pub codec_name: String,
272    /// Supported pixel formats (empty for audio-only codecs).
273    pub pixel_formats: PixelFormatCaps,
274    /// Supported sample formats (empty for video-only codecs).
275    pub sample_formats: SampleFormatCaps,
276    /// Supported sample rates (empty for video-only codecs).
277    pub sample_rates: Vec<u32>,
278    /// Supported channel counts (empty for video-only codecs).
279    pub channel_counts: Vec<u32>,
280}
281
282impl FormatCapabilities {
283    /// Creates a new `FormatCapabilities` for a video codec.
284    #[must_use]
285    pub fn video(codec_name: impl Into<String>, pixel_formats: Vec<PixelFormat>) -> Self {
286        Self {
287            codec_name: codec_name.into(),
288            pixel_formats: PixelFormatCaps::new(pixel_formats),
289            sample_formats: SampleFormatCaps::new(vec![]),
290            sample_rates: vec![],
291            channel_counts: vec![],
292        }
293    }
294
295    /// Creates a new `FormatCapabilities` for an audio codec.
296    #[must_use]
297    pub fn audio(
298        codec_name: impl Into<String>,
299        sample_formats: Vec<SampleFormat>,
300        sample_rates: Vec<u32>,
301        channel_counts: Vec<u32>,
302    ) -> Self {
303        Self {
304            codec_name: codec_name.into(),
305            pixel_formats: PixelFormatCaps::new(vec![]),
306            sample_formats: SampleFormatCaps::new(sample_formats),
307            sample_rates,
308            channel_counts,
309        }
310    }
311}
312
313/// Result of automatic format negotiation between encoder and decoder.
314#[derive(Debug, Clone, PartialEq, Eq)]
315pub struct FormatNegotiationResult {
316    /// Negotiated pixel format (if video).
317    pub pixel_format: Option<PixelFormat>,
318    /// Negotiated sample format (if audio).
319    pub sample_format: Option<SampleFormat>,
320    /// Negotiated sample rate (if audio).
321    pub sample_rate: Option<u32>,
322    /// Negotiated channel count (if audio).
323    pub channel_count: Option<u32>,
324}
325
326/// Negotiates the best format parameters between a decoder's output capabilities
327/// and an encoder's input capabilities.
328///
329/// For each parameter, the first mutually-supported value (in the decoder's
330/// preference order) is chosen. Returns `None` if the two capability sets have
331/// no format in common for the relevant media type.
332#[must_use]
333pub fn negotiate_formats(
334    decoder: &FormatCapabilities,
335    encoder: &FormatCapabilities,
336) -> Option<FormatNegotiationResult> {
337    let is_video =
338        !decoder.pixel_formats.formats.is_empty() && !encoder.pixel_formats.formats.is_empty();
339    let is_audio =
340        !decoder.sample_formats.formats.is_empty() && !encoder.sample_formats.formats.is_empty();
341
342    if !is_video && !is_audio {
343        return None;
344    }
345
346    let pixel_format = if is_video {
347        let pf = decoder.pixel_formats.negotiate(&encoder.pixel_formats);
348        pf?;
349        pf
350    } else {
351        None
352    };
353
354    let sample_format = if is_audio {
355        let sf = decoder.sample_formats.negotiate(&encoder.sample_formats);
356        sf?;
357        sf
358    } else {
359        None
360    };
361
362    let sample_rate = if is_audio {
363        decoder
364            .sample_rates
365            .iter()
366            .find(|r| encoder.sample_rates.contains(r))
367            .copied()
368    } else {
369        None
370    };
371
372    let channel_count = if is_audio {
373        decoder
374            .channel_counts
375            .iter()
376            .find(|c| encoder.channel_counts.contains(c))
377            .copied()
378    } else {
379        None
380    };
381
382    Some(FormatNegotiationResult {
383        pixel_format,
384        sample_format,
385        sample_rate,
386        channel_count,
387    })
388}
389
390// ─────────────────────────────────────────────────────────────────────────────
391// Resolution / bitrate constraints for full auto-negotiation
392// ─────────────────────────────────────────────────────────────────────────────
393
394/// Resolution constraint for video negotiation.
395#[derive(Debug, Clone, PartialEq, Eq)]
396pub struct ResolutionRange {
397    /// Minimum supported width.
398    pub min_width: u32,
399    /// Maximum supported width.
400    pub max_width: u32,
401    /// Minimum supported height.
402    pub min_height: u32,
403    /// Maximum supported height.
404    pub max_height: u32,
405}
406
407impl ResolutionRange {
408    /// Creates a new resolution range.
409    #[must_use]
410    pub fn new(min_width: u32, max_width: u32, min_height: u32, max_height: u32) -> Self {
411        Self {
412            min_width,
413            max_width,
414            min_height,
415            max_height,
416        }
417    }
418
419    /// Returns whether the given dimensions fit within this range.
420    #[must_use]
421    pub fn contains(&self, width: u32, height: u32) -> bool {
422        width >= self.min_width
423            && width <= self.max_width
424            && height >= self.min_height
425            && height <= self.max_height
426    }
427
428    /// Returns the intersection of two resolution ranges, or `None` if they don't overlap.
429    #[must_use]
430    pub fn intersect(&self, other: &Self) -> Option<Self> {
431        let min_w = self.min_width.max(other.min_width);
432        let max_w = self.max_width.min(other.max_width);
433        let min_h = self.min_height.max(other.min_height);
434        let max_h = self.max_height.min(other.max_height);
435        if min_w <= max_w && min_h <= max_h {
436            Some(Self::new(min_w, max_w, min_h, max_h))
437        } else {
438            None
439        }
440    }
441}
442
443impl Default for ResolutionRange {
444    fn default() -> Self {
445        Self {
446            min_width: 1,
447            max_width: 8192,
448            min_height: 1,
449            max_height: 4320,
450        }
451    }
452}
453
454/// Bitrate constraint for negotiation.
455#[derive(Debug, Clone, PartialEq, Eq)]
456pub struct BitrateRange {
457    /// Minimum bitrate in bits per second.
458    pub min_bps: u64,
459    /// Maximum bitrate in bits per second.
460    pub max_bps: u64,
461}
462
463impl BitrateRange {
464    /// Creates a new bitrate range.
465    #[must_use]
466    pub fn new(min_bps: u64, max_bps: u64) -> Self {
467        Self { min_bps, max_bps }
468    }
469
470    /// Returns whether the given bitrate fits within this range.
471    #[must_use]
472    pub fn contains(&self, bps: u64) -> bool {
473        bps >= self.min_bps && bps <= self.max_bps
474    }
475
476    /// Returns the intersection of two bitrate ranges, or `None` if they don't overlap.
477    #[must_use]
478    pub fn intersect(&self, other: &Self) -> Option<Self> {
479        let min = self.min_bps.max(other.min_bps);
480        let max = self.max_bps.min(other.max_bps);
481        if min <= max {
482            Some(Self::new(min, max))
483        } else {
484            None
485        }
486    }
487}
488
489impl Default for BitrateRange {
490    fn default() -> Self {
491        Self {
492            min_bps: 0,
493            max_bps: u64::MAX,
494        }
495    }
496}
497
498/// Full endpoint capabilities for automatic encoder/decoder negotiation.
499///
500/// Combines codec capabilities, format capabilities, and hardware constraints
501/// into a single description that `AutoNegotiator` can reason about.
502#[derive(Debug, Clone)]
503pub struct EndpointCapabilities {
504    /// Codec-level capabilities (profiles, levels, hw).
505    pub codec: CodecCapability,
506    /// Format capabilities (pixel formats, sample formats, sample rates, channels).
507    pub formats: FormatCapabilities,
508    /// Resolution constraint (video only; ignored for audio).
509    pub resolution: ResolutionRange,
510    /// Bitrate constraint.
511    pub bitrate: BitrateRange,
512}
513
514impl EndpointCapabilities {
515    /// Creates video endpoint capabilities.
516    #[must_use]
517    pub fn video(
518        codec: CodecCapability,
519        pixel_formats: Vec<PixelFormat>,
520        resolution: ResolutionRange,
521        bitrate: BitrateRange,
522    ) -> Self {
523        Self {
524            formats: FormatCapabilities::video(&codec.name, pixel_formats),
525            codec,
526            resolution,
527            bitrate,
528        }
529    }
530
531    /// Creates audio endpoint capabilities.
532    #[must_use]
533    pub fn audio(
534        codec: CodecCapability,
535        sample_formats: Vec<SampleFormat>,
536        sample_rates: Vec<u32>,
537        channel_counts: Vec<u32>,
538        bitrate: BitrateRange,
539    ) -> Self {
540        Self {
541            formats: FormatCapabilities::audio(
542                &codec.name,
543                sample_formats,
544                sample_rates,
545                channel_counts,
546            ),
547            codec,
548            resolution: ResolutionRange::default(),
549            bitrate,
550        }
551    }
552}
553
554/// Result of full automatic negotiation.
555#[derive(Debug, Clone, PartialEq)]
556pub struct AutoNegotiationResult {
557    /// Selected codec name.
558    pub codec: String,
559    /// Agreed profile.
560    pub profile: String,
561    /// Agreed level (min of both sides).
562    pub level: u32,
563    /// Whether hardware acceleration is available.
564    pub hardware_accelerated: bool,
565    /// Negotiated format parameters.
566    pub format: FormatNegotiationResult,
567    /// Negotiated resolution range (video only).
568    pub resolution: Option<ResolutionRange>,
569    /// Negotiated bitrate range.
570    pub bitrate: Option<BitrateRange>,
571    /// Quality score (0.0 - 1.0, higher is better).
572    pub score: f64,
573}
574
575/// Automatic negotiator that finds the best encoder/decoder pairing.
576///
577/// Combines codec negotiation, format negotiation, resolution/bitrate constraint
578/// intersection, and quality scoring into a single pass.
579///
580/// # Example
581///
582/// ```
583/// use oximedia_core::codec_negotiation::*;
584/// use oximedia_core::types::{PixelFormat, SampleFormat};
585///
586/// let decoder = EndpointCapabilities::video(
587///     CodecCapability::new("av1", vec!["main".into()], 50, true),
588///     vec![PixelFormat::Yuv420p, PixelFormat::Yuv420p10le],
589///     ResolutionRange::new(1, 3840, 1, 2160),
590///     BitrateRange::new(500_000, 20_000_000),
591/// );
592/// let encoder = EndpointCapabilities::video(
593///     CodecCapability::new("av1", vec!["main".into()], 40, false),
594///     vec![PixelFormat::Yuv420p10le, PixelFormat::Yuv420p],
595///     ResolutionRange::new(1, 1920, 1, 1080),
596///     BitrateRange::new(1_000_000, 15_000_000),
597/// );
598/// let result = auto_negotiate(&decoder, &encoder).expect("should negotiate");
599/// assert_eq!(result.codec, "av1");
600/// assert_eq!(result.format.pixel_format, Some(PixelFormat::Yuv420p));
601/// assert!(result.resolution.is_some());
602/// ```
603#[must_use]
604pub fn auto_negotiate(
605    decoder: &EndpointCapabilities,
606    encoder: &EndpointCapabilities,
607) -> Option<AutoNegotiationResult> {
608    // 1. Codec-level negotiation
609    let codec_result = negotiate(
610        std::slice::from_ref(&decoder.codec),
611        std::slice::from_ref(&encoder.codec),
612    )?;
613
614    // 2. Format negotiation
615    let format_result = negotiate_formats(&decoder.formats, &encoder.formats)?;
616
617    // 3. Resolution intersection
618    let resolution = decoder.resolution.intersect(&encoder.resolution);
619
620    // 4. Bitrate intersection
621    let bitrate = decoder.bitrate.intersect(&encoder.bitrate);
622
623    // 5. Compute quality score
624    let score = compute_score(&codec_result, &format_result, &resolution, &bitrate);
625
626    Some(AutoNegotiationResult {
627        codec: codec_result.selected_codec,
628        profile: codec_result.profile,
629        level: codec_result.level,
630        hardware_accelerated: codec_result.hardware_accelerated,
631        format: format_result,
632        resolution,
633        bitrate,
634        score,
635    })
636}
637
638/// Computes a quality score in [0.0, 1.0] for a negotiation result.
639///
640/// Factors:
641/// - Hardware acceleration: +0.2
642/// - Higher codec level: up to +0.3
643/// - Higher bit-depth pixel format: up to +0.2
644/// - Resolution available: +0.15
645/// - Bitrate available: +0.15
646fn compute_score(
647    codec: &NegotiationResult,
648    format: &FormatNegotiationResult,
649    resolution: &Option<ResolutionRange>,
650    bitrate: &Option<BitrateRange>,
651) -> f64 {
652    let mut score = 0.0;
653
654    // Hardware acceleration bonus
655    if codec.hardware_accelerated {
656        score += 0.2;
657    }
658
659    // Codec level score (normalised to 0..0.3 assuming level range 10..63)
660    let level_norm = f64::from(codec.level.min(63).saturating_sub(10)) / 53.0;
661    score += level_norm * 0.3;
662
663    // Pixel format bit depth score
664    if let Some(pf) = format.pixel_format {
665        let depth_score = f64::from(pf.bits_per_component().min(16)) / 16.0;
666        score += depth_score * 0.2;
667    }
668
669    // Resolution available
670    if resolution.is_some() {
671        score += 0.15;
672    }
673
674    // Bitrate available
675    if bitrate.is_some() {
676        score += 0.15;
677    }
678
679    // Clamp to [0.0, 1.0]
680    score.min(1.0)
681}
682
683// ─────────────────────────────────────────────────────────────────────────────
684// FormatCost trait + FormatNegotiator
685// ─────────────────────────────────────────────────────────────────────────────
686
687/// Describes the cost of converting between two instances of the same format
688/// type (e.g., two [`PixelFormat`] values or two [`SampleFormat`] values).
689///
690/// Implementors return `Some(cost)` where the ordinal indicates relative
691/// expense:
692/// - `0` — identical formats (no conversion)
693/// - `1` — same family, trivial re-interpretation (e.g., NV12 ↔ NV21)
694/// - `2` — same colour space, different subsampling or bit-depth
695/// - `3` — different colour space (e.g., YUV ↔ RGB)
696/// - `4` — any other cross-family conversion
697///
698/// Return `None` to indicate that no conversion path exists at all.
699pub trait FormatCost: Clone + PartialEq {
700    /// Returns the conversion cost from `self` to `target`, or `None` if no
701    /// conversion path is available.
702    fn conversion_cost(&self, target: &Self) -> Option<u32>;
703}
704
705// ── PixelFormat ──────────────────────────────────────────────────────────────
706
707/// Helper — returns `true` if `f` belongs to the YUV 4:2:0 family.
708fn pf_is_yuv420(f: &PixelFormat) -> bool {
709    matches!(
710        f,
711        PixelFormat::Yuv420p | PixelFormat::Nv12 | PixelFormat::Nv21
712    )
713}
714
715/// Helper — returns `true` if `f` belongs to the YUV 4:2:2 family.
716fn pf_is_yuv422(f: &PixelFormat) -> bool {
717    matches!(f, PixelFormat::Yuv422p)
718}
719
720/// Helper — returns `true` if `f` belongs to the YUV 4:4:4 family.
721fn pf_is_yuv444(f: &PixelFormat) -> bool {
722    matches!(f, PixelFormat::Yuv444p)
723}
724
725/// Helper — returns `true` if `f` is a packed / planar RGB/RGBA format.
726fn pf_is_rgb(f: &PixelFormat) -> bool {
727    matches!(f, PixelFormat::Rgb24 | PixelFormat::Rgba32)
728}
729
730/// Helper — returns `true` if `f` is a greyscale format.
731fn pf_is_gray(f: &PixelFormat) -> bool {
732    matches!(f, PixelFormat::Gray8 | PixelFormat::Gray16)
733}
734
735/// Helper — returns `true` if `f` is a high-bit-depth YUV semi-planar format.
736fn pf_is_hbd_yuv(f: &PixelFormat) -> bool {
737    matches!(
738        f,
739        PixelFormat::Yuv420p10le | PixelFormat::Yuv420p12le | PixelFormat::P010 | PixelFormat::P016
740    )
741}
742
743impl FormatCost for PixelFormat {
744    fn conversion_cost(&self, target: &Self) -> Option<u32> {
745        if self == target {
746            return Some(0);
747        }
748
749        // Same family — cost 1
750        if (pf_is_yuv420(self) && pf_is_yuv420(target))
751            || (pf_is_yuv422(self) && pf_is_yuv422(target))
752            || (pf_is_yuv444(self) && pf_is_yuv444(target))
753            || (pf_is_rgb(self) && pf_is_rgb(target))
754            || (pf_is_gray(self) && pf_is_gray(target))
755            || (pf_is_hbd_yuv(self) && pf_is_hbd_yuv(target))
756        {
757            return Some(1);
758        }
759
760        let self_yuv =
761            pf_is_yuv420(self) || pf_is_yuv422(self) || pf_is_yuv444(self) || pf_is_hbd_yuv(self);
762        let target_yuv = pf_is_yuv420(target)
763            || pf_is_yuv422(target)
764            || pf_is_yuv444(target)
765            || pf_is_hbd_yuv(target);
766
767        // Same colour space (YUV), different subsampling — cost 2
768        if self_yuv && target_yuv {
769            return Some(2);
770        }
771
772        // RGB ↔ YUV — cost 3
773        if (pf_is_rgb(self) && target_yuv) || (self_yuv && pf_is_rgb(target)) {
774            return Some(3);
775        }
776
777        // Any other cross-family conversion — cost 4
778        Some(4)
779    }
780}
781
782// ── SampleFormat ─────────────────────────────────────────────────────────────
783
784impl FormatCost for SampleFormat {
785    fn conversion_cost(&self, target: &Self) -> Option<u32> {
786        if self == target {
787            return Some(0);
788        }
789
790        // Interleaved ↔ planar of same width & encoding — cost 1
791        // (self.to_packed() == target.to_packed() covers e.g. S16 ↔ S16p)
792        if self.to_packed() == target.to_packed() {
793            return Some(1);
794        }
795
796        // Same numeric family (both int or both float), different bit-depth — cost 2
797        let self_float = self.is_float();
798        let target_float = target.is_float();
799        if self_float == target_float {
800            return Some(2);
801        }
802
803        // Integer ↔ float — cost 3
804        Some(3)
805    }
806}
807
808// ── FormatConversionResult ────────────────────────────────────────────────────
809
810/// Result of a [`FormatNegotiator`] negotiation run.
811///
812/// Discriminates between a direct match (no conversion needed), a conversion
813/// with an associated cost ordinal, and a fully incompatible pair.
814///
815/// # Examples
816///
817/// ```
818/// use oximedia_core::codec_negotiation::{FormatNegotiator, FormatConversionResult};
819/// use oximedia_core::types::PixelFormat;
820///
821/// let neg = FormatNegotiator::<PixelFormat> {
822///     decoder_produces: &[PixelFormat::Yuv420p],
823///     encoder_accepts: &[PixelFormat::Yuv420p],
824/// };
825/// assert_eq!(neg.negotiate(), FormatConversionResult::Direct(PixelFormat::Yuv420p));
826/// ```
827#[derive(Debug, Clone, PartialEq)]
828pub enum FormatConversionResult<F> {
829    /// Encoder directly accepts what the decoder produces — no conversion needed.
830    Direct(F),
831    /// A conversion is required.  `cost` is an ordinal (0 = identity, higher =
832    /// more expensive).
833    Convert {
834        /// The format the decoder produces.
835        from: F,
836        /// The format the encoder accepts.
837        to: F,
838        /// Conversion cost ordinal.
839        cost: u32,
840    },
841    /// No compatible conversion path exists.
842    Incompatible,
843}
844
845// ── FormatNegotiator ──────────────────────────────────────────────────────────
846
847/// Automatically selects the best [`PixelFormat`] or [`SampleFormat`] that
848/// bridges what a decoder produces and what an encoder accepts.
849///
850/// The negotiator first looks for a **direct match** (zero-cost); if none
851/// exists it picks the conversion with the **lowest cost** as determined by
852/// [`FormatCost::conversion_cost`].  If no conversion path exists it returns
853/// [`FormatConversionResult::Incompatible`].
854///
855/// # Examples
856///
857/// ```
858/// use oximedia_core::codec_negotiation::{FormatNegotiator, FormatConversionResult};
859/// use oximedia_core::types::PixelFormat;
860///
861/// let neg = FormatNegotiator::<PixelFormat> {
862///     decoder_produces: &[PixelFormat::Yuv422p],
863///     encoder_accepts: &[PixelFormat::Yuv420p],
864/// };
865/// match neg.negotiate() {
866///     FormatConversionResult::Convert { from, to, cost } => {
867///         assert_eq!(from, PixelFormat::Yuv422p);
868///         assert_eq!(to, PixelFormat::Yuv420p);
869///         assert!(cost >= 1);
870///     }
871///     other => panic!("unexpected: {other:?}"),
872/// }
873/// ```
874pub struct FormatNegotiator<'a, F> {
875    /// Pixel / sample formats the decoder is able to output.
876    pub decoder_produces: &'a [F],
877    /// Pixel / sample formats the encoder is able to accept as input.
878    pub encoder_accepts: &'a [F],
879}
880
881impl<F> FormatNegotiator<'_, F>
882where
883    F: FormatCost + std::fmt::Debug,
884{
885    /// Runs the negotiation and returns the best [`FormatConversionResult`].
886    #[must_use]
887    pub fn negotiate(&self) -> FormatConversionResult<F> {
888        // Pass 1: direct match (identity — cost 0)
889        for prod in self.decoder_produces {
890            for acc in self.encoder_accepts {
891                if prod == acc {
892                    return FormatConversionResult::Direct(prod.clone());
893                }
894            }
895        }
896
897        // Pass 2: cheapest conversion
898        let mut best: Option<(F, F, u32)> = None;
899        for prod in self.decoder_produces {
900            for acc in self.encoder_accepts {
901                if let Some(cost) = prod.conversion_cost(acc) {
902                    let is_better = best.as_ref().map_or(true, |(_, _, bc)| cost < *bc);
903                    if is_better {
904                        best = Some((prod.clone(), acc.clone(), cost));
905                    }
906                }
907            }
908        }
909
910        if let Some((from, to, cost)) = best {
911            FormatConversionResult::Convert { from, to, cost }
912        } else {
913            FormatConversionResult::Incompatible
914        }
915    }
916}
917
918// ─────────────────────────────────────────────────────────────────────────────
919// Tests
920// ─────────────────────────────────────────────────────────────────────────────
921
922#[cfg(test)]
923mod tests {
924    use super::*;
925
926    fn av1_cap(hw: bool) -> CodecCapability {
927        CodecCapability::new("av1", vec!["main".to_string(), "high".to_string()], 40, hw)
928    }
929
930    fn vp9_cap() -> CodecCapability {
931        CodecCapability::new("vp9", vec!["profile0".to_string()], 50, false)
932    }
933
934    // 1. supports_profile – positive
935    #[test]
936    fn test_supports_profile_positive() {
937        let cap = av1_cap(false);
938        assert!(cap.supports_profile("main"));
939        assert!(cap.supports_profile("high"));
940    }
941
942    // 2. supports_profile – negative
943    #[test]
944    fn test_supports_profile_negative() {
945        let cap = av1_cap(false);
946        assert!(!cap.supports_profile("baseline"));
947    }
948
949    // 3. is_hw_accelerated
950    #[test]
951    fn test_is_hw_accelerated() {
952        assert!(av1_cap(true).is_hw_accelerated());
953        assert!(!av1_cap(false).is_hw_accelerated());
954    }
955
956    // 4. CodecNegotiator::common_codecs – overlap
957    #[test]
958    fn test_common_codecs_overlap() {
959        let mut neg = CodecNegotiator::new();
960        neg.add_local(av1_cap(false));
961        neg.add_local(vp9_cap());
962        neg.add_remote(av1_cap(false));
963        let common = neg.common_codecs();
964        assert_eq!(common, vec!["av1"]);
965    }
966
967    // 5. CodecNegotiator::common_codecs – no overlap
968    #[test]
969    fn test_common_codecs_no_overlap() {
970        let mut neg = CodecNegotiator::new();
971        neg.add_local(vp9_cap());
972        neg.add_remote(av1_cap(false));
973        assert!(neg.common_codecs().is_empty());
974    }
975
976    // 6. preferred_codec – hw preferred
977    #[test]
978    fn test_preferred_codec_hw_first() {
979        let mut neg = CodecNegotiator::new();
980        neg.add_local(vp9_cap()); // software
981        neg.add_local(av1_cap(true)); // hardware
982        neg.add_remote(vp9_cap());
983        neg.add_remote(av1_cap(true));
984        // av1 is hw, should be preferred
985        assert_eq!(neg.preferred_codec(), Some("av1"));
986    }
987
988    // 7. preferred_codec – no common
989    #[test]
990    fn test_preferred_codec_none() {
991        let mut neg = CodecNegotiator::new();
992        neg.add_local(vp9_cap());
993        neg.add_remote(av1_cap(false));
994        assert!(neg.preferred_codec().is_none());
995    }
996
997    // 8. preferred_codec – falls back to first when no hw
998    #[test]
999    fn test_preferred_codec_fallback() {
1000        let mut neg = CodecNegotiator::new();
1001        neg.add_local(av1_cap(false));
1002        neg.add_local(vp9_cap());
1003        neg.add_remote(av1_cap(false));
1004        neg.add_remote(vp9_cap());
1005        // No hw acceleration; first common codec returned
1006        let pref = neg.preferred_codec();
1007        assert!(pref.is_some());
1008    }
1009
1010    // 9. negotiate – success
1011    #[test]
1012    fn test_negotiate_success() {
1013        let local = vec![av1_cap(false)];
1014        let remote = vec![av1_cap(false)];
1015        let result = negotiate(&local, &remote).expect("negotiation should succeed");
1016        assert_eq!(result.selected_codec, "av1");
1017        assert!(result.profile == "main" || result.profile == "high");
1018        assert_eq!(result.level, 40);
1019        assert!(!result.is_hardware());
1020    }
1021
1022    // 10. negotiate – hw preferred
1023    #[test]
1024    fn test_negotiate_prefers_hw() {
1025        let local = vec![vp9_cap(), av1_cap(true)];
1026        let remote = vec![vp9_cap(), av1_cap(true)];
1027        let result = negotiate(&local, &remote).expect("negotiation should succeed");
1028        assert_eq!(result.selected_codec, "av1");
1029        assert!(result.is_hardware());
1030    }
1031
1032    // 11. negotiate – level is min of both
1033    #[test]
1034    fn test_negotiate_level_min() {
1035        let local = vec![CodecCapability::new(
1036            "av1",
1037            vec!["main".to_string()],
1038            50,
1039            false,
1040        )];
1041        let remote = vec![CodecCapability::new(
1042            "av1",
1043            vec!["main".to_string()],
1044            30,
1045            false,
1046        )];
1047        let result = negotiate(&local, &remote).expect("negotiation should succeed");
1048        assert_eq!(result.level, 30);
1049    }
1050
1051    // 12. negotiate – no common codec returns None
1052    #[test]
1053    fn test_negotiate_no_common() {
1054        let local = vec![av1_cap(false)];
1055        let remote = vec![vp9_cap()];
1056        assert!(negotiate(&local, &remote).is_none());
1057    }
1058
1059    // 13. negotiate – profile mismatch returns None
1060    #[test]
1061    fn test_negotiate_profile_mismatch() {
1062        let local = vec![CodecCapability::new(
1063            "av1",
1064            vec!["high".to_string()],
1065            40,
1066            false,
1067        )];
1068        let remote = vec![CodecCapability::new(
1069            "av1",
1070            vec!["baseline".to_string()],
1071            40,
1072            false,
1073        )];
1074        assert!(negotiate(&local, &remote).is_none());
1075    }
1076
1077    // 14. NegotiationResult::is_hardware
1078    #[test]
1079    fn test_negotiation_result_is_hardware() {
1080        let r = NegotiationResult {
1081            selected_codec: "av1".to_string(),
1082            profile: "main".to_string(),
1083            level: 40,
1084            hardware_accelerated: true,
1085        };
1086        assert!(r.is_hardware());
1087        let r2 = NegotiationResult {
1088            hardware_accelerated: false,
1089            ..r
1090        };
1091        assert!(!r2.is_hardware());
1092    }
1093
1094    // ── Format negotiation tests ──────────────────────────────────────
1095
1096    #[test]
1097    fn test_pixel_format_negotiate_common() {
1098        let dec = PixelFormatCaps::new(vec![PixelFormat::Yuv420p, PixelFormat::Nv12]);
1099        let enc = PixelFormatCaps::new(vec![PixelFormat::Nv12, PixelFormat::Yuv420p]);
1100        // Decoder prefers Yuv420p, encoder also supports it
1101        assert_eq!(dec.negotiate(&enc), Some(PixelFormat::Yuv420p));
1102    }
1103
1104    #[test]
1105    fn test_pixel_format_negotiate_no_common() {
1106        let dec = PixelFormatCaps::new(vec![PixelFormat::Yuv420p]);
1107        let enc = PixelFormatCaps::new(vec![PixelFormat::Rgb24]);
1108        assert_eq!(dec.negotiate(&enc), None);
1109    }
1110
1111    #[test]
1112    fn test_pixel_format_common_formats() {
1113        let dec = PixelFormatCaps::new(vec![
1114            PixelFormat::Yuv420p,
1115            PixelFormat::Nv12,
1116            PixelFormat::Rgb24,
1117        ]);
1118        let enc = PixelFormatCaps::new(vec![PixelFormat::Nv12, PixelFormat::Rgb24]);
1119        let common = dec.common_formats(&enc);
1120        assert_eq!(common, vec![PixelFormat::Nv12, PixelFormat::Rgb24]);
1121    }
1122
1123    #[test]
1124    fn test_sample_format_negotiate_common() {
1125        let dec = SampleFormatCaps::new(vec![SampleFormat::F32, SampleFormat::S16]);
1126        let enc = SampleFormatCaps::new(vec![SampleFormat::S16, SampleFormat::S24]);
1127        assert_eq!(dec.negotiate(&enc), Some(SampleFormat::S16));
1128    }
1129
1130    #[test]
1131    fn test_sample_format_negotiate_no_common() {
1132        let dec = SampleFormatCaps::new(vec![SampleFormat::F32]);
1133        let enc = SampleFormatCaps::new(vec![SampleFormat::S24]);
1134        assert_eq!(dec.negotiate(&enc), None);
1135    }
1136
1137    #[test]
1138    fn test_sample_format_common_formats() {
1139        let dec = SampleFormatCaps::new(vec![
1140            SampleFormat::F32,
1141            SampleFormat::S16,
1142            SampleFormat::S24,
1143        ]);
1144        let enc = SampleFormatCaps::new(vec![SampleFormat::S24, SampleFormat::F32]);
1145        let common = dec.common_formats(&enc);
1146        assert_eq!(common, vec![SampleFormat::F32, SampleFormat::S24]);
1147    }
1148
1149    #[test]
1150    fn test_negotiate_formats_video() {
1151        let decoder =
1152            FormatCapabilities::video("av1", vec![PixelFormat::Yuv420p, PixelFormat::Yuv420p10le]);
1153        let encoder =
1154            FormatCapabilities::video("av1", vec![PixelFormat::Yuv420p10le, PixelFormat::Yuv420p]);
1155        let result = negotiate_formats(&decoder, &encoder).expect("should negotiate");
1156        assert_eq!(result.pixel_format, Some(PixelFormat::Yuv420p));
1157        assert_eq!(result.sample_format, None);
1158    }
1159
1160    #[test]
1161    fn test_negotiate_formats_audio() {
1162        let decoder = FormatCapabilities::audio(
1163            "opus",
1164            vec![SampleFormat::F32, SampleFormat::S16],
1165            vec![48000, 44100],
1166            vec![2, 1],
1167        );
1168        let encoder = FormatCapabilities::audio(
1169            "opus",
1170            vec![SampleFormat::S16, SampleFormat::F32],
1171            vec![48000],
1172            vec![2],
1173        );
1174        let result = negotiate_formats(&decoder, &encoder).expect("should negotiate");
1175        assert_eq!(result.sample_format, Some(SampleFormat::F32));
1176        assert_eq!(result.sample_rate, Some(48000));
1177        assert_eq!(result.channel_count, Some(2));
1178        assert_eq!(result.pixel_format, None);
1179    }
1180
1181    #[test]
1182    fn test_negotiate_formats_no_common_pixel_format() {
1183        let decoder = FormatCapabilities::video("av1", vec![PixelFormat::Yuv420p]);
1184        let encoder = FormatCapabilities::video("av1", vec![PixelFormat::Rgb24]);
1185        assert!(negotiate_formats(&decoder, &encoder).is_none());
1186    }
1187
1188    #[test]
1189    fn test_negotiate_formats_no_common_sample_format() {
1190        let decoder =
1191            FormatCapabilities::audio("opus", vec![SampleFormat::F32], vec![48000], vec![2]);
1192        let encoder =
1193            FormatCapabilities::audio("opus", vec![SampleFormat::S24], vec![48000], vec![2]);
1194        assert!(negotiate_formats(&decoder, &encoder).is_none());
1195    }
1196
1197    #[test]
1198    fn test_negotiate_formats_empty_caps() {
1199        let decoder = FormatCapabilities::video("av1", vec![]);
1200        let encoder = FormatCapabilities::video("av1", vec![]);
1201        assert!(negotiate_formats(&decoder, &encoder).is_none());
1202    }
1203
1204    #[test]
1205    fn test_negotiate_formats_video_with_semi_planar() {
1206        let decoder = FormatCapabilities::video("av1", vec![PixelFormat::Nv12, PixelFormat::P010]);
1207        let encoder =
1208            FormatCapabilities::video("av1", vec![PixelFormat::P010, PixelFormat::Yuv420p]);
1209        let result = negotiate_formats(&decoder, &encoder).expect("should negotiate");
1210        assert_eq!(result.pixel_format, Some(PixelFormat::P010));
1211    }
1212
1213    #[test]
1214    fn test_format_capabilities_video_constructor() {
1215        let caps = FormatCapabilities::video("vp9", vec![PixelFormat::Yuv420p]);
1216        assert_eq!(caps.codec_name, "vp9");
1217        assert_eq!(caps.pixel_formats.formats.len(), 1);
1218        assert!(caps.sample_formats.formats.is_empty());
1219        assert!(caps.sample_rates.is_empty());
1220        assert!(caps.channel_counts.is_empty());
1221    }
1222
1223    #[test]
1224    fn test_format_capabilities_audio_constructor() {
1225        let caps = FormatCapabilities::audio(
1226            "flac",
1227            vec![SampleFormat::S16, SampleFormat::S24],
1228            vec![44100, 48000, 96000],
1229            vec![1, 2],
1230        );
1231        assert_eq!(caps.codec_name, "flac");
1232        assert!(caps.pixel_formats.formats.is_empty());
1233        assert_eq!(caps.sample_formats.formats.len(), 2);
1234        assert_eq!(caps.sample_rates.len(), 3);
1235        assert_eq!(caps.channel_counts.len(), 2);
1236    }
1237
1238    // ── ResolutionRange tests ───────────────────────────────────────
1239
1240    #[test]
1241    fn test_resolution_range_contains() {
1242        let r = ResolutionRange::new(1, 1920, 1, 1080);
1243        assert!(r.contains(1920, 1080));
1244        assert!(r.contains(1, 1));
1245        assert!(r.contains(1280, 720));
1246        assert!(!r.contains(3840, 2160));
1247        assert!(!r.contains(0, 0));
1248    }
1249
1250    #[test]
1251    fn test_resolution_range_intersect() {
1252        let a = ResolutionRange::new(1, 3840, 1, 2160);
1253        let b = ResolutionRange::new(640, 1920, 480, 1080);
1254        let intersect = a.intersect(&b).expect("should intersect");
1255        assert_eq!(intersect.min_width, 640);
1256        assert_eq!(intersect.max_width, 1920);
1257        assert_eq!(intersect.min_height, 480);
1258        assert_eq!(intersect.max_height, 1080);
1259    }
1260
1261    #[test]
1262    fn test_resolution_range_no_intersect() {
1263        let a = ResolutionRange::new(1, 640, 1, 480);
1264        let b = ResolutionRange::new(1920, 3840, 1080, 2160);
1265        assert!(a.intersect(&b).is_none());
1266    }
1267
1268    #[test]
1269    fn test_resolution_range_default() {
1270        let r = ResolutionRange::default();
1271        assert!(r.contains(1920, 1080));
1272        assert!(r.contains(7680, 4320));
1273    }
1274
1275    // ── BitrateRange tests ──────────────────────────────────────────
1276
1277    #[test]
1278    fn test_bitrate_range_contains() {
1279        let b = BitrateRange::new(1_000_000, 10_000_000);
1280        assert!(b.contains(5_000_000));
1281        assert!(b.contains(1_000_000));
1282        assert!(b.contains(10_000_000));
1283        assert!(!b.contains(500_000));
1284        assert!(!b.contains(20_000_000));
1285    }
1286
1287    #[test]
1288    fn test_bitrate_range_intersect() {
1289        let a = BitrateRange::new(500_000, 20_000_000);
1290        let b = BitrateRange::new(1_000_000, 15_000_000);
1291        let intersect = a.intersect(&b).expect("should intersect");
1292        assert_eq!(intersect.min_bps, 1_000_000);
1293        assert_eq!(intersect.max_bps, 15_000_000);
1294    }
1295
1296    #[test]
1297    fn test_bitrate_range_no_intersect() {
1298        let a = BitrateRange::new(1_000_000, 2_000_000);
1299        let b = BitrateRange::new(5_000_000, 10_000_000);
1300        assert!(a.intersect(&b).is_none());
1301    }
1302
1303    #[test]
1304    fn test_bitrate_range_default() {
1305        let b = BitrateRange::default();
1306        assert!(b.contains(0));
1307        assert!(b.contains(1_000_000_000));
1308    }
1309
1310    // ── auto_negotiate tests ────────────────────────────────────────
1311
1312    #[test]
1313    fn test_auto_negotiate_video_success() {
1314        let decoder = EndpointCapabilities::video(
1315            CodecCapability::new("av1", vec!["main".into()], 50, true),
1316            vec![PixelFormat::Yuv420p, PixelFormat::Yuv420p10le],
1317            ResolutionRange::new(1, 3840, 1, 2160),
1318            BitrateRange::new(500_000, 20_000_000),
1319        );
1320        let encoder = EndpointCapabilities::video(
1321            CodecCapability::new("av1", vec!["main".into()], 40, false),
1322            vec![PixelFormat::Yuv420p10le, PixelFormat::Yuv420p],
1323            ResolutionRange::new(1, 1920, 1, 1080),
1324            BitrateRange::new(1_000_000, 15_000_000),
1325        );
1326        let result = auto_negotiate(&decoder, &encoder).expect("should negotiate");
1327        assert_eq!(result.codec, "av1");
1328        assert_eq!(result.profile, "main");
1329        assert_eq!(result.level, 40); // min of 50 and 40
1330        assert!(result.hardware_accelerated); // decoder has hw
1331        assert_eq!(result.format.pixel_format, Some(PixelFormat::Yuv420p));
1332
1333        let res = result.resolution.expect("should have resolution");
1334        assert_eq!(res.max_width, 1920);
1335        assert_eq!(res.max_height, 1080);
1336
1337        let br = result.bitrate.expect("should have bitrate");
1338        assert_eq!(br.min_bps, 1_000_000);
1339        assert_eq!(br.max_bps, 15_000_000);
1340
1341        assert!(result.score > 0.0);
1342        assert!(result.score <= 1.0);
1343    }
1344
1345    #[test]
1346    fn test_auto_negotiate_audio_success() {
1347        let decoder = EndpointCapabilities::audio(
1348            CodecCapability::new("opus", vec!["default".into()], 0, false),
1349            vec![SampleFormat::F32, SampleFormat::S16],
1350            vec![48000, 44100],
1351            vec![2, 1],
1352            BitrateRange::new(64_000, 510_000),
1353        );
1354        let encoder = EndpointCapabilities::audio(
1355            CodecCapability::new("opus", vec!["default".into()], 0, false),
1356            vec![SampleFormat::S16, SampleFormat::F32],
1357            vec![48000],
1358            vec![2],
1359            BitrateRange::new(96_000, 256_000),
1360        );
1361        let result = auto_negotiate(&decoder, &encoder).expect("should negotiate");
1362        assert_eq!(result.codec, "opus");
1363        assert_eq!(result.format.sample_format, Some(SampleFormat::F32));
1364        assert_eq!(result.format.sample_rate, Some(48000));
1365        assert_eq!(result.format.channel_count, Some(2));
1366
1367        let br = result.bitrate.expect("should have bitrate");
1368        assert_eq!(br.min_bps, 96_000);
1369        assert_eq!(br.max_bps, 256_000);
1370    }
1371
1372    #[test]
1373    fn test_auto_negotiate_codec_mismatch() {
1374        let decoder = EndpointCapabilities::video(
1375            CodecCapability::new("av1", vec!["main".into()], 40, false),
1376            vec![PixelFormat::Yuv420p],
1377            ResolutionRange::default(),
1378            BitrateRange::default(),
1379        );
1380        let encoder = EndpointCapabilities::video(
1381            CodecCapability::new("vp9", vec!["profile0".into()], 40, false),
1382            vec![PixelFormat::Yuv420p],
1383            ResolutionRange::default(),
1384            BitrateRange::default(),
1385        );
1386        assert!(auto_negotiate(&decoder, &encoder).is_none());
1387    }
1388
1389    #[test]
1390    fn test_auto_negotiate_format_mismatch() {
1391        let decoder = EndpointCapabilities::video(
1392            CodecCapability::new("av1", vec!["main".into()], 40, false),
1393            vec![PixelFormat::Yuv420p],
1394            ResolutionRange::default(),
1395            BitrateRange::default(),
1396        );
1397        let encoder = EndpointCapabilities::video(
1398            CodecCapability::new("av1", vec!["main".into()], 40, false),
1399            vec![PixelFormat::Rgb24],
1400            ResolutionRange::default(),
1401            BitrateRange::default(),
1402        );
1403        assert!(auto_negotiate(&decoder, &encoder).is_none());
1404    }
1405
1406    #[test]
1407    fn test_auto_negotiate_hw_boosts_score() {
1408        let make_endpoint = |hw: bool| {
1409            EndpointCapabilities::video(
1410                CodecCapability::new("av1", vec!["main".into()], 40, hw),
1411                vec![PixelFormat::Yuv420p],
1412                ResolutionRange::default(),
1413                BitrateRange::default(),
1414            )
1415        };
1416        let hw_result =
1417            auto_negotiate(&make_endpoint(true), &make_endpoint(true)).expect("should negotiate");
1418        let sw_result =
1419            auto_negotiate(&make_endpoint(false), &make_endpoint(false)).expect("should negotiate");
1420        assert!(hw_result.score > sw_result.score);
1421    }
1422
1423    #[test]
1424    fn test_auto_negotiate_resolution_no_overlap_still_returns() {
1425        // Resolution ranges don't overlap, but codec+format do => result returned with None resolution
1426        let decoder = EndpointCapabilities::video(
1427            CodecCapability::new("av1", vec!["main".into()], 40, false),
1428            vec![PixelFormat::Yuv420p],
1429            ResolutionRange::new(1, 640, 1, 480),
1430            BitrateRange::default(),
1431        );
1432        let encoder = EndpointCapabilities::video(
1433            CodecCapability::new("av1", vec!["main".into()], 40, false),
1434            vec![PixelFormat::Yuv420p],
1435            ResolutionRange::new(1920, 3840, 1080, 2160),
1436            BitrateRange::default(),
1437        );
1438        let result = auto_negotiate(&decoder, &encoder).expect("should still negotiate");
1439        assert!(result.resolution.is_none());
1440    }
1441
1442    #[test]
1443    fn test_score_range() {
1444        // Score should always be in [0, 1]
1445        let endpoint = EndpointCapabilities::video(
1446            CodecCapability::new("av1", vec!["main".into()], 63, true),
1447            vec![PixelFormat::Yuv420p10le],
1448            ResolutionRange::default(),
1449            BitrateRange::default(),
1450        );
1451        let result = auto_negotiate(&endpoint, &endpoint).expect("should negotiate");
1452        assert!(result.score >= 0.0);
1453        assert!(result.score <= 1.0);
1454    }
1455
1456    #[test]
1457    fn test_endpoint_capabilities_video_constructor() {
1458        let ep = EndpointCapabilities::video(
1459            CodecCapability::new("vp9", vec!["profile0".into()], 50, false),
1460            vec![PixelFormat::Yuv420p, PixelFormat::Nv12],
1461            ResolutionRange::new(1, 1920, 1, 1080),
1462            BitrateRange::new(1_000_000, 10_000_000),
1463        );
1464        assert_eq!(ep.codec.name, "vp9");
1465        assert_eq!(ep.formats.pixel_formats.formats.len(), 2);
1466        assert_eq!(ep.resolution.max_width, 1920);
1467        assert_eq!(ep.bitrate.max_bps, 10_000_000);
1468    }
1469
1470    #[test]
1471    fn test_endpoint_capabilities_audio_constructor() {
1472        let ep = EndpointCapabilities::audio(
1473            CodecCapability::new("flac", vec!["default".into()], 0, false),
1474            vec![SampleFormat::S16, SampleFormat::S24],
1475            vec![44100, 48000, 96000],
1476            vec![1, 2],
1477            BitrateRange::new(0, 5_000_000),
1478        );
1479        assert_eq!(ep.codec.name, "flac");
1480        assert_eq!(ep.formats.sample_formats.formats.len(), 2);
1481        assert_eq!(ep.formats.sample_rates.len(), 3);
1482    }
1483}