1use std::collections::VecDeque;
18use std::sync::Arc;
19use std::time::{Duration, SystemTime, UNIX_EPOCH};
20
21use crate::types::{
22 OsdSyncContent, OsdSyncEntry, OsdSyncImage, OsdSyncRequest, OsdSyncSlot, OsdSyncValue,
23 OSD_SYNC_FONT_MAX, OSD_SYNC_IMAGE_HEIGHT_MAX, OSD_SYNC_IMAGE_MAX, OSD_SYNC_IMAGE_WIDTH_MAX,
24 OSD_SYNC_PERCENT_MAX, OSD_SYNC_RECORDS_MAX, OSD_SYNC_SLOTS, OSD_SYNC_TEXT_MAX,
25};
26use crate::wire::{
27 build_osd_setting, build_osd_sync, crc32, op, Addressee, DeviceFeature, DeviceUid, OsdSyncWhat,
28};
29
30use super::control::ControlError;
31use super::logo_server::{Images, LogoServer};
32use super::{lock, Remote, Shared, SHUTDOWN_POLL};
33
34const SEQS_KEPT: usize = 64;
38
39const SETTINGS_PER_PASS: usize = 4;
42const PASS_GAP: Duration = Duration::from_millis(14);
43
44const PNG_SIGNATURE: &[u8] = b"\x89PNG\r\n\x1a\n";
46const PNG_IHDR_AT: usize = 12;
47
48pub(super) struct OsdSender {
50 held: Option<Held>,
51 next_seq: u16,
52 issued: VecDeque<u16>,
54 queue: VecDeque<Pending>,
55 server: Option<LogoServer>,
56}
57
58struct Held {
60 what: OsdSyncWhat,
62 entries: Arc<[OsdSyncEntry]>,
63 slots: [OsdSyncSlot; OSD_SYNC_SLOTS],
64 http_port: u16,
65 seqs: VecDeque<u16>,
67}
68
69#[derive(Clone, Copy, PartialEq, Eq)]
71struct Pending {
72 unit: DeviceUid,
73 seq: u16,
74 index: u16,
75}
76
77impl OsdSender {
78 pub(super) fn new() -> Self {
82 let ms = SystemTime::now()
83 .duration_since(UNIX_EPOCH)
84 .map_or(0, |d| d.as_millis());
85 Self {
86 held: None,
87 next_seq: (ms as u16).max(1),
88 issued: VecDeque::new(),
89 queue: VecDeque::new(),
90 server: None,
91 }
92 }
93
94 pub(super) fn holds(&self) -> bool {
96 self.held.is_some()
97 }
98
99 fn sync_frame(&mut self, target: DeviceUid, what: OsdSyncWhat) -> Option<(u16, Vec<u8>)> {
103 let seq = self.next_seq;
104 self.next_seq = seq.wrapping_add(1).max(1);
105 if self.issued.len() == SEQS_KEPT {
106 self.issued.pop_front();
107 }
108 self.issued.push_back(seq);
109 let held = self.held.as_mut()?;
110 if held.seqs.len() == SEQS_KEPT {
111 held.seqs.pop_front();
112 }
113 held.seqs.push_back(seq);
114 let records = held.entries.len() as u16;
115 let frame = build_osd_sync(seq, target, what, held.http_port, records, &held.slots);
116 Some((seq, frame))
117 }
118
119 fn queue_missing(&mut self, unit: DeviceUid, request: &OsdSyncRequest) {
122 let Some(held) = &self.held else {
123 return;
124 };
125 self.queue.retain(|p| p.unit != unit);
126 for index in 0..held.entries.len() as u16 {
127 if request.is_missing(index) {
128 self.queue.push_back(Pending {
129 unit,
130 seq: request.seq,
131 index,
132 });
133 }
134 }
135 }
136
137 fn next_pass(&mut self) -> Vec<(DeviceUid, Vec<u8>)> {
143 let mut pass = Vec::new();
144 while pass.len() < SETTINGS_PER_PASS {
145 let Some(p) = self.queue.pop_front() else {
146 break;
147 };
148 let (theirs, others): (VecDeque<_>, VecDeque<_>) =
149 self.queue.drain(..).partition(|q| q.unit == p.unit);
150 self.queue = others;
151 self.queue.extend(theirs);
152 let Some(held) = &self.held else {
153 break;
154 };
155 let Some(entry) = held.entries.get(usize::from(p.index)) else {
158 continue;
159 };
160 if !held.seqs.contains(&p.seq) {
161 continue;
162 }
163 let records = held.entries.len() as u16;
164 pass.push((
165 p.unit,
166 build_osd_setting(p.seq, p.unit, p.index, records, entry),
167 ));
168 }
169 pass
170 }
171}
172
173impl Shared {
174 fn addressee(&self, unit: DeviceUid) -> Option<Addressee> {
175 self.read(|state| state.device(unit).map(|d| Addressee::device(d)))
176 }
177
178 pub(super) fn answer_osd_requests(&self, requests: Vec<OsdSyncRequest>) {
186 for request in requests {
187 let unit = request.target;
188 let Some(to) = self.addressee(unit) else {
189 continue;
190 };
191 let sync = {
192 let mut osd = lock(&self.osd);
193 let Some(held) = &osd.held else {
194 return;
195 };
196 let (held_what, current) = (held.what, held.seqs.contains(&request.seq));
197 let ours = current || osd.issued.contains(&request.seq);
201 if request.seq != 0 && request.source.is_zero() && !ours {
202 continue;
203 }
204 if request.seq != 0 && current {
205 osd.queue_missing(unit, &request);
206 self.osd_queued.notify_one();
207 None
208 } else {
209 let what = request.what & held_what;
212 if what.is_empty() {
213 continue;
214 }
215 osd.sync_frame(unit, what)
216 }
217 };
218 if let Some((_, frame)) = sync {
219 let _ = self.send(&to, op::V2IP_OSD_SYNC, &frame);
220 }
221 }
222 }
223
224 pub(super) fn osd_pass(&self) -> usize {
226 let pass = lock(&self.osd).next_pass();
227 for (unit, frame) in &pass {
228 if let Some(to) = self.addressee(*unit) {
229 let _ = self.send(&to, op::V2IP_OSD_SYNC, frame);
230 }
231 }
232 pass.len()
233 }
234
235 pub(super) fn osd_loop(&self) {
237 while !self.closing.load(std::sync::atomic::Ordering::SeqCst) {
238 if self.osd_pass() > 0 {
239 std::thread::sleep(PASS_GAP);
240 continue;
241 }
242 let osd = lock(&self.osd);
243 if osd.queue.is_empty() {
244 let _ = self.osd_queued.wait_timeout(osd, SHUTDOWN_POLL);
245 }
246 }
247 }
248
249 pub(super) fn drop_osd_sync(&self) -> bool {
251 let (held, server) = {
252 let mut osd = lock(&self.osd);
253 (osd.held.take().is_some(), osd.server.take())
254 };
255 drop(server);
258 held
259 }
260}
261
262impl Remote {
263 pub fn send_osd_sync(&self, content: OsdSyncContent) -> Result<u16, ControlError> {
285 validate(&content)?;
286 self.hold_osd_sync(content)
287 .inspect_err(|_| self.clear_osd_sync())
288 }
289
290 fn hold_osd_sync(&self, content: OsdSyncContent) -> Result<u16, ControlError> {
291 let images = content.images.as_ref();
292 let mut what =
293 OsdSyncWhat::from_bits(content.what.bits() & !OsdSyncWhat::LOGO_SLOTS.bits());
294 if images.is_some() {
295 what |= OsdSyncWhat::LOGO_SLOTS;
296 }
297 let slots = images.map_or_else(Default::default, |images| {
298 core::array::from_fn(|i| OsdSyncSlot {
299 bytes: images[i].png.len() as u32,
300 crc32: crc32(&images[i].png),
301 percent: images[i].percent,
302 })
303 });
304 let (was_held, sync) = {
305 let mut osd = lock(&self.shared.osd);
306 let http_port = match images {
307 Some(images) => serve(&mut osd.server, self.shared.logo_port, images)?,
308 None => 0,
309 };
310 let was_held = osd.held.is_some();
311 osd.held = Some(Held {
312 what,
313 entries: content.entries.into(),
314 slots,
315 http_port,
316 seqs: VecDeque::new(),
317 });
318 (was_held, osd.sync_frame(DeviceUid::ZERO, what))
319 };
320 let (seq, frame) = sync.ok_or(ControlError::InvalidRequest("no overlay sync is held"))?;
321 if !was_held {
324 self.shared.announce();
325 }
326 self.shared
327 .send(&Addressee::Broadcast, op::V2IP_OSD_SYNC, &frame)?;
328 Ok(seq)
329 }
330
331 pub fn resend_osd_sync(&self, device: DeviceUid) -> Result<u16, ControlError> {
338 let to = self
339 .shared
340 .addressee(device)
341 .ok_or(ControlError::UnknownDevice(device))?;
342 let takes = self.shared.read(|state| {
343 state
344 .device(device)
345 .is_some_and(|d| d.hello.features.has(DeviceFeature::OSD_SYNC))
346 });
347 if !takes {
348 return Err(ControlError::Unsupported("the unit takes no overlay sync"));
349 }
350 let (seq, frame) = {
351 let mut osd = lock(&self.shared.osd);
352 let what = osd.held.as_ref().map(|h| h.what);
353 what.and_then(|what| osd.sync_frame(device, what))
354 .ok_or(ControlError::InvalidRequest("no overlay sync is held"))?
355 };
356 self.shared.send(&to, op::V2IP_OSD_SYNC, &frame)?;
357 Ok(seq)
358 }
359
360 pub fn clear_osd_sync(&self) {
366 if self.shared.drop_osd_sync() {
367 self.shared.announce();
368 }
369 }
370}
371
372fn serve(
375 server: &mut Option<LogoServer>,
376 port: Option<u16>,
377 images: &[OsdSyncImage; OSD_SYNC_SLOTS],
378) -> Result<u16, ControlError> {
379 let running = match server.take() {
380 Some(running) => running,
381 None => LogoServer::start(port.unwrap_or(0))
382 .map_err(|e| ControlError::Send(crate::wire::SendError::Io(e)))?,
383 };
384 let images: Images = core::array::from_fn(|i| Arc::from(images[i].png.as_slice()));
385 running.set_images(images);
386 let port = running.port();
387 *server = Some(running);
388 Ok(port)
389}
390
391fn validate(content: &OsdSyncContent) -> Result<(), ControlError> {
392 let invalid = |what| Err(ControlError::InvalidRequest(what));
393 let copies = OsdSyncWhat::from_bits(content.what.bits() & !OsdSyncWhat::LOGO_SLOTS.bits());
394 if copies.is_empty() && content.images.is_none() {
395 return invalid("an overlay sync copying something");
396 }
397 if content.entries.len() > OSD_SYNC_RECORDS_MAX {
398 return invalid("an overlay sync of at most 128 settings");
399 }
400 for entry in &content.entries {
401 match &entry.value {
402 OsdSyncValue::Caption(c) if c.font.len() > OSD_SYNC_FONT_MAX => {
403 return invalid("a caption font of at most 31 bytes");
404 }
405 OsdSyncValue::Caption(c) if c.text.len() > OSD_SYNC_TEXT_MAX => {
406 return invalid("a caption text of at most 360 bytes");
407 }
408 OsdSyncValue::Logo(l) if !(-1..=4).contains(&l.logo) => {
409 return invalid("a logo of -1 to 4");
410 }
411 _ => {}
412 }
413 }
414 for image in content.images.iter().flatten() {
415 if image.percent > OSD_SYNC_PERCENT_MAX {
416 return invalid("a logo size of 0 to 100 percent");
417 }
418 if !image.png.is_empty() && !fits(&image.png) {
419 return invalid("a PNG of at most 512 KB and 1920 by 1080 pixels");
420 }
421 }
422 Ok(())
423}
424
425fn fits(png: &[u8]) -> bool {
427 let be32 = |at: usize| {
428 png.get(at..at + 4)
429 .map(|b| u32::from_be_bytes([b[0], b[1], b[2], b[3]]))
430 };
431 let (Some(width), Some(height)) = (be32(PNG_IHDR_AT + 4), be32(PNG_IHDR_AT + 8)) else {
432 return false;
433 };
434 png.len() <= OSD_SYNC_IMAGE_MAX
435 && png.starts_with(PNG_SIGNATURE)
436 && png.get(PNG_IHDR_AT..PNG_IHDR_AT + 4) == Some(b"IHDR")
437 && (1..=OSD_SYNC_IMAGE_WIDTH_MAX).contains(&width)
438 && (1..=OSD_SYNC_IMAGE_HEIGHT_MAX).contains(&height)
439}
440
441#[cfg(test)]
442#[path = "osd_tests.rs"]
443mod tests;