import Foundation import CoreVideo /// Microsoft Teams adapter (native app: com.microsoft.teams2 / .teams). /// /// Teams' active-speaker cue is a **coloured ring/border** (Teams violet) around /// the speaking participant's tile; the **name sits in the tile's bottom-left**. /// Not a launch priority — included so the four detected platforms all have a /// visual adapter and degrade no worse than colored-border detection. /// /// Detection *logic* is validated on synthetic frames; geometry constants are a /// first pass pending real Teams screenshots. struct TeamsAdapter: AppAdapter { static let bundleIDs = ["com.microsoft.teams2", "com.microsoft.teams"] let adapterVersion = "teams-0.1.0" let preferredFPS = 3 private let analyzer: GridCallAnalyzer init() { var config = GridCallAnalyzer.Config() config.nameAnchor = .bottomLeft config.detectColoredBorder = true // Teams-violet speaking ring (faint) config.detectWhiteBorder = false // Teams' ring is muted: brand violet #6264A7 ≈ 0.41 sat, light variants // ~0.27 — well under the 0.5 default, so the default misses it entirely. // Drop the threshold and (since low sat invites warm-video false positives) // gate to the violet/indigo hue band. Both pending real-fixture calibration. config.colorSaturation = 0.22 config.colorHueRange = 215...275 config.tileExpandX = 3.0 config.tileExpandY = 5.0 self.analyzer = GridCallAnalyzer(config: config) } func analyze(frame: CVPixelBuffer, at t: TimeInterval) -> [SpeakerObservation] { analyzer.analyze(pixelBuffer: frame, at: t) } // Exposed for fixture/synthetic tests. func analyze(cgImage: CGImage, at t: TimeInterval) -> [SpeakerObservation] { analyzer.analyze(cgImage: cgImage, at: t) } }