1use std::{collections::HashMap, time::Duration};
2
3use flatbuffers::FlatBufferBuilder;
4
5pub mod fcast_flatbuffers {
6 #![allow(dead_code)]
7 #![allow(unused_imports)]
8 #![allow(clippy::extra_unused_lifetimes)]
9 #![allow(clippy::missing_safety_doc)]
10 #![allow(clippy::derivable_impls)]
11
12 include!("v4_generated.rs");
13}
14
15pub use flatbuffers;
16
17pub use fcast_flatbuffers::fcast::{v4 as flat, v4::PlaybackState};
18
19pub const MAX_PACKET_SIZE: usize = 512 * 1024;
20
21pub struct ConstructedMessage<'a> {
22 builder: flatbuffers::FlatBufferBuilder<'a>,
23}
24
25impl std::fmt::Debug for ConstructedMessage<'_> {
26 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27 f.write_str("ConstructedMessage { ... }")
28 }
29}
30
31impl PartialEq for ConstructedMessage<'_> {
32 fn eq(&self, _other: &Self) -> bool {
33 false
34 }
35}
36
37impl std::ops::Deref for ConstructedMessage<'_> {
38 type Target = [u8];
39
40 fn deref(&self) -> &Self::Target {
41 self.builder.finished_data()
42 }
43}
44
45pub struct MessageBuilder<'a> {
46 builder: flatbuffers::FlatBufferBuilder<'a>,
47}
48
49use paste::paste;
50use smol_str::SmolStr;
51
52macro_rules! create_msg {
53 ($self:expr, $name:expr, $($field:ident $(: $value:expr)? ),* $(,)?) => {{
54 paste! {
55 let value = flat::[<$name>]::create(
56 &mut $self.builder,
57 &flat:: [<$name Args>] {
58 $($field: create_msg!(@value $field $(: $value)?)),*
59 }
60
61 ).as_union_value();
62 $self.create_and_finish_envelope(flat::Message::[<$name>], value)
63 }
64 }};
65 (@value $field:ident : $value:expr) => { $value };
66 (@value $field:ident) => { $field };
67}
68
69macro_rules! create_str {
70 ($self:expr, $str:expr) => {
71 Some($self.builder.create_string(&$str))
72 };
73}
74
75macro_rules! maybe_create_str {
76 ($self:expr, $str:expr) => {
77 if let Some(s) = $str {
78 create_str!($self, s)
79 } else {
80 None
81 }
82 };
83}
84
85macro_rules! create_device_info {
86 ($self:expr, $device_info:expr) => {{
87 let args = flat::DeviceInfoArgs {
88 display_name: maybe_create_str!($self, &$device_info.display_name),
89 app_name: maybe_create_str!($self, &$device_info.app_name),
90 app_version: maybe_create_str!($self, &$device_info.app_version),
91 };
92
93 flat::DeviceInfo::create(&mut $self.builder, &args)
94 }};
95}
96
97impl<'a> MessageBuilder<'a> {
98 #[allow(clippy::new_without_default)]
99 pub fn new() -> Self {
100 Self {
101 builder: FlatBufferBuilder::new(),
102 }
103 }
104
105 fn create_and_finish_envelope(
106 mut self,
107 payload_type: flat::Message,
108 payload: flatbuffers::WIPOffset<flatbuffers::UnionWIPOffset>,
109 ) -> ConstructedMessage<'a> {
110 let envelope = flat::Packet::create(
111 &mut self.builder,
112 &flat::PacketArgs {
113 payload_type,
114 payload: Some(payload),
115 },
116 );
117
118 self.builder.finish(envelope, None);
119
120 ConstructedMessage {
121 builder: self.builder,
122 }
123 }
124
125 pub fn progress_changed_raw(
126 mut self,
127 position: Option<&flat::Time>,
128 duration: Option<&flat::Time>,
129 ) -> ConstructedMessage<'a> {
130 create_msg!(self, ProgressChanged, position, duration)
131 }
132
133 pub fn progress_changed(
134 self,
135 position: flat::Time,
136 duration: flat::Time,
137 ) -> ConstructedMessage<'a> {
138 self.progress_changed_raw(Some(&position), Some(&duration))
139 }
140
141 pub fn set_progress_update_interval_raw(
142 mut self,
143 interval: Option<&flat::Time>,
144 ) -> ConstructedMessage<'a> {
145 create_msg!(self, SetProgressUpdateInterval, interval)
146 }
147
148 pub fn set_progress_update_interval(self, interval: flat::Time) -> ConstructedMessage<'a> {
149 self.set_progress_update_interval_raw(Some(&interval))
150 }
151
152 pub fn volume_changed(mut self, volume: f32) -> ConstructedMessage<'a> {
153 create_msg!(self, VolumeChanged, volume)
154 }
155
156 pub fn speed_changed(mut self, speed: f32) -> ConstructedMessage<'a> {
157 create_msg!(self, SpeedChanged, speed)
158 }
159
160 pub fn playback_state_changed(mut self, state: flat::PlaybackState) -> ConstructedMessage<'a> {
161 create_msg!(self, PlaybackStateChanged, state)
162 }
163
164 pub fn change_track(
165 mut self,
166 id: Option<u32>,
167 typ: flat::MediaTrackType,
168 ) -> ConstructedMessage<'a> {
169 create_msg!(self, ChangeTrack, id, track_type: typ)
170 }
171
172 pub fn add_subtitle_source(
173 mut self,
174 url: &str,
175 select: bool,
176 name: Option<&str>,
177 ) -> ConstructedMessage<'a> {
178 let url = self.builder.create_string(url);
179 let name = name.map(|n| self.builder.create_string(n));
180 create_msg!(
181 self,
182 AddSubtitleSource,
183 url: Some(url),
184 select,
185 name,
186 metadata: None
187 )
188 }
189
190 pub fn companion_hello_request(mut self) -> ConstructedMessage<'a> {
191 create_msg!(self, CompanionHelloRequest,)
192 }
193
194 pub fn companion_hello_response(mut self, provider_id: u16) -> ConstructedMessage<'a> {
195 create_msg!(self, CompanionHelloResponse, provider_id)
196 }
197
198 pub fn companion_resource_info_request(
199 mut self,
200 request_id: u32,
201 resource_id: u32,
202 ) -> ConstructedMessage<'a> {
203 create_msg!(self, CompanionResourceInfoRequest, request_id, resource_id)
204 }
205
206 pub fn companion_resource_info_response(
207 mut self,
208 request_id: u32,
209 content_type: &str,
210 size: Option<u64>,
211 ) -> ConstructedMessage<'a> {
212 let content_type = self.builder.create_string(content_type);
213 let (resource_size_type, resource_size) = match size {
214 Some(size) => (
215 flat::CompanionResourceSize::Known,
216 flat::KnownResourceSize::create(
217 &mut self.builder,
218 &flat::KnownResourceSizeArgs { size },
219 )
220 .as_union_value(),
221 ),
222 None => (
223 flat::CompanionResourceSize::Unknown,
224 flat::UnknownResourceSize::create(
225 &mut self.builder,
226 &flat::UnknownResourceSizeArgs {},
227 )
228 .as_union_value(),
229 ),
230 };
231 create_msg!(
232 self,
233 CompanionResourceInfoResponse,
234 request_id,
235 content_type: Some(content_type),
236 resource_size_type: resource_size_type,
237 resource_size: Some(resource_size)
238 )
239 }
240
241 pub fn sender_introduction(mut self, device_info: &DeviceInfo) -> ConstructedMessage<'a> {
242 let device_info = create_device_info!(self, device_info);
243 create_msg!(self, SenderIntroduction, device_info: Some(device_info))
244 }
245
246 pub fn start_mirroring_session(mut self, session_id: u16) -> ConstructedMessage<'a> {
247 create_msg!(self, StartMirroringSession, session_id)
248 }
249
250 pub fn mirroring_session_description(
251 mut self,
252 session_id: u16,
253 sdp: &str,
254 ) -> ConstructedMessage<'a> {
255 let sdp = self.builder.create_string(sdp);
256 create_msg!(self, MirroringSessionDescription, session_id, sdp: Some(sdp))
257 }
258
259 pub fn stop_playback(mut self) -> ConstructedMessage<'a> {
260 create_msg!(self, StopPlayback,)
261 }
262
263 fn strip_flat_media_item(
264 &mut self,
265 item: flat::MediaItem<'_>,
266 ) -> flatbuffers::WIPOffset<flat::MediaItem<'a>> {
267 let extra_metadata = read_extra_metadata(&item)
268 .filter(|m| !m.is_empty())
269 .map(|m| self.build_extra_metadata(&m));
270 let args = flat::MediaItemArgs {
271 container: create_str!(self, item.container()),
272 source_url: create_str!(self, item.source_url()),
273 start_time: item.start_time(),
274 volume: item.volume(),
275 speed: item.speed(),
276 headers: None, title: item.title().map(|s| self.builder.create_string(s)),
278 thumbnail_url: item.thumbnail_url().map(|s| self.builder.create_string(s)),
279 metadata_type: flat::Metadata::NONE,
281 metadata: None,
282 extra_metadata,
283 };
284 flat::MediaItem::create(&mut self.builder, &args)
285 }
286
287 pub fn from_play_stripped(mut self, play: &flat::Load) -> Option<ConstructedMessage<'a>> {
288 let (source_type, source) = match play.source_type() {
289 flat::MediaSource::Single => {
290 let item = self
291 .strip_flat_media_item(play.source_as_single()?)
292 .as_union_value();
293 (flat::MediaSource::Single, item)
294 }
295 flat::MediaSource::Queue => {
296 let msg = play.source_as_queue()?;
297 let queue_items = msg
298 .items()
299 .iter()
300 .map(|queue_item| {
301 let item = self.strip_flat_media_item(queue_item.media_item());
302 flat::QueueItem::create(
303 &mut self.builder,
304 &flat::QueueItemArgs {
305 media_item: Some(item),
306 playback_duration: queue_item.playback_duration(),
307 },
308 )
309 })
310 .collect::<Vec<_>>();
311
312 let items = self.builder.create_vector(&queue_items);
313 let queue = flat::Queue::create(
314 &mut self.builder,
315 &flat::QueueArgs {
316 items: Some(items),
317 start_index: msg.start_index(),
318 autoplay: msg.autoplay(),
319 },
320 )
321 .as_union_value();
322
323 (flat::MediaSource::Queue, queue)
324 }
325 _ => return None,
326 };
327
328 Some(create_msg!(self, Load, source_type, source: Some(source)))
329 }
330
331 pub fn from_queue_insert_stripped(
332 mut self,
333 insert: &flat::QueueInsert,
334 ) -> Option<ConstructedMessage<'a>> {
335 let position = match insert.position_type() {
336 flat::QueuePosition::Index => QueuePosition::Index(insert.position_as_index()?.index()),
337 flat::QueuePosition::Front => QueuePosition::Front,
338 flat::QueuePosition::Back => QueuePosition::Back,
339 _ => return None,
340 };
341 let (pos_type, pos) = self.queue_position(position);
342 let item = self.strip_flat_media_item(insert.item().media_item());
343 let q_item = flat::QueueItem::create(
344 &mut self.builder,
345 &flat::QueueItemArgs {
346 media_item: Some(item),
347 playback_duration: insert.item().playback_duration(),
348 },
349 );
350 Some(
351 create_msg!(self, QueueInsert, item: Some(q_item), position_type: pos_type, position: Some(pos)),
352 )
353 }
354
355 fn build_meta_value(
359 &mut self,
360 value: &MetaValue,
361 ) -> (
362 flat::GenericMetaValue,
363 flatbuffers::WIPOffset<flatbuffers::UnionWIPOffset>,
364 ) {
365 match value {
366 MetaValue::String(s) => {
367 let v = self.builder.create_string(s);
368 let off = flat::GenericMetaString::create(
369 &mut self.builder,
370 &flat::GenericMetaStringArgs { value: Some(v) },
371 );
372 (flat::GenericMetaValue::String, off.as_union_value())
373 }
374 MetaValue::Float(f) => {
375 let off = flat::GenericMetaFloat::create(
376 &mut self.builder,
377 &flat::GenericMetaFloatArgs { value: *f },
378 );
379 (flat::GenericMetaValue::Float, off.as_union_value())
380 }
381 MetaValue::Int(i) => {
382 let off = flat::GenericMetaInt::create(
383 &mut self.builder,
384 &flat::GenericMetaIntArgs { value: *i },
385 );
386 (flat::GenericMetaValue::Int, off.as_union_value())
387 }
388 MetaValue::List(items) => {
389 let wrapped = items
390 .iter()
391 .map(|v| {
392 let (value_type, value) = self.build_meta_value(v);
393 flat::WrappedGenericMetaValue::create(
394 &mut self.builder,
395 &flat::WrappedGenericMetaValueArgs { value_type, value: Some(value) },
396 )
397 })
398 .collect::<Vec<_>>();
399 let vec = self.builder.create_vector(&wrapped);
400 let off = flat::GenericMetaList::create(
401 &mut self.builder,
402 &flat::GenericMetaListArgs { value: Some(vec) },
403 );
404 (flat::GenericMetaValue::List, off.as_union_value())
405 }
406 MetaValue::KvPair { key, value } => {
407 let kv = self.build_meta_kv(key, value);
408 (flat::GenericMetaValue::KVPair, kv.as_union_value())
409 }
410 }
411 }
412
413 fn build_meta_kv(
414 &mut self,
415 key: &str,
416 value: &MetaValue,
417 ) -> flatbuffers::WIPOffset<flat::MetadataKV<'a>> {
418 let (value_type, value_off) = self.build_meta_value(value);
419 let key = self.builder.create_string(key);
420 flat::MetadataKV::create(
421 &mut self.builder,
422 &flat::MetadataKVArgs { key: Some(key), value_type, value: Some(value_off) },
423 )
424 }
425
426 fn build_extra_metadata(
429 &mut self,
430 extra: &HashMap<String, MetaValue>,
431 ) -> flatbuffers::WIPOffset<flatbuffers::Vector<'a, flatbuffers::ForwardsUOffset<flat::MetadataKV<'a>>>>
432 {
433 let mut entries: Vec<(&String, &MetaValue)> = extra.iter().collect();
434 entries.sort_by(|a, b| a.0.cmp(b.0));
435 let kvs = entries
436 .into_iter()
437 .map(|(k, v)| self.build_meta_kv(k, v))
438 .collect::<Vec<_>>();
439 self.builder.create_vector(&kvs)
440 }
441
442 fn time_from_secs_f64(secs: f64) -> Option<flat::Time> {
445 let duration = Duration::try_from_secs_f64(secs).ok()?;
446 Some(flat::Time::new(
447 u64::try_from(duration.as_micros()).unwrap_or(u64::MAX),
448 ))
449 }
450
451 fn construct_media_item(
452 &mut self,
453 item: MediaItem,
454 ) -> flatbuffers::WIPOffset<flat::MediaItem<'a>> {
455 let headers = if let Some(headers) = item.headers {
456 let elems = headers
457 .iter()
458 .map(|(k, v)| {
459 let header = flat::RequestHeaderArgs {
460 key: create_str!(self, k),
461 value: create_str!(self, v),
462 };
463 flat::RequestHeader::create(&mut self.builder, &header)
464 })
465 .collect::<Vec<_>>();
466 Some(self.builder.create_vector(&elems))
467 } else {
468 None
469 };
470
471 let (metadata_type, metadata) = match item.metadata {
472 Some(Metadata::Video { .. }) => {
473 let meta = flat::VideoMetadataArgs { chapters: None };
474 let meta = flat::VideoMetadata::create(&mut self.builder, &meta).as_union_value();
475 (flat::Metadata::Video, Some(meta))
476 }
477 Some(Metadata::Audio { artist, album }) => {
478 let meta = flat::AudioMetadataArgs {
479 artist: maybe_create_str!(self, artist),
480 album: maybe_create_str!(self, album),
481 chapters: None,
482 };
483 let meta = flat::AudioMetadata::create(&mut self.builder, &meta).as_union_value();
484 (flat::Metadata::Audio, Some(meta))
485 }
486 None => (flat::Metadata::NONE, None),
487 };
488
489 let extra_metadata = item
490 .extra_metadata
491 .as_ref()
492 .filter(|m| !m.is_empty())
493 .map(|m| self.build_extra_metadata(m));
494
495 let start_time = item.start_time.and_then(Self::time_from_secs_f64);
496 let item = flat::MediaItemArgs {
497 container: create_str!(self, item.container),
498 source_url: create_str!(self, item.source_url),
499 start_time: start_time.as_ref(),
500 volume: item.volume,
501 speed: item.speed,
502 headers,
503 title: maybe_create_str!(self, item.title),
504 thumbnail_url: maybe_create_str!(self, item.thumbnail_url),
505 metadata_type,
506 metadata,
507 extra_metadata,
508 };
509
510 flat::MediaItem::create(&mut self.builder, &item)
511 }
512
513 pub fn load_single(mut self, item: MediaItem) -> ConstructedMessage<'a> {
514 let item = self.construct_media_item(item).as_union_value();
515 create_msg!(self, Load, source_type: flat::MediaSource::Single, source: Some(item))
516 }
517
518 pub fn load_queue(
521 mut self,
522 items: impl Iterator<Item = (MediaItem, Option<f64>)>,
523 start_index: Option<u8>,
524 autoplay: bool,
525 ) -> ConstructedMessage<'a> {
526 let items = items
527 .map(|(item, playback_duration)| {
528 let item = self.construct_media_item(item);
529 let playback_duration = playback_duration.and_then(Self::time_from_secs_f64);
530 flat::QueueItem::create(
531 &mut self.builder,
532 &flat::QueueItemArgs {
533 media_item: Some(item),
534 playback_duration: playback_duration.as_ref(),
535 },
536 )
537 })
538 .collect::<Vec<_>>();
539
540 let items = self.builder.create_vector(&items);
541 let queue = flat::Queue::create(
542 &mut self.builder,
543 &flat::QueueArgs {
544 items: Some(items),
545 start_index,
546 autoplay,
547 },
548 )
549 .as_union_value();
550
551 create_msg!(self, Load, source_type: flat::MediaSource::Queue, source: Some(queue))
552 }
553
554 fn queue_position(
555 &mut self,
556 position: QueuePosition,
557 ) -> (
558 flat::QueuePosition,
559 flatbuffers::WIPOffset<flatbuffers::UnionWIPOffset>,
560 ) {
561 match position {
562 QueuePosition::Index(index) => (
563 flat::QueuePosition::Index,
564 flat::QueueIndex::create(&mut self.builder, &flat::QueueIndexArgs { index })
565 .as_union_value(),
566 ),
567 QueuePosition::Front => (
568 flat::QueuePosition::Front,
569 flat::QueueMarkerFront::create(&mut self.builder, &flat::QueueMarkerFrontArgs {})
570 .as_union_value(),
571 ),
572 QueuePosition::Back => (
573 flat::QueuePosition::Back,
574 flat::QueueMarkerBack::create(&mut self.builder, &flat::QueueMarkerBackArgs {})
575 .as_union_value(),
576 ),
577 }
578 }
579
580 pub fn queue_remove(mut self, position: QueuePosition) -> ConstructedMessage<'a> {
581 let (typ, position) = self.queue_position(position);
582 create_msg!(self, QueueRemove, position_type: typ, position: Some(position))
583 }
584
585 pub fn queue_insert(
586 mut self,
587 item: MediaItem,
588 playback_duration: Option<f64>,
589 position: QueuePosition,
590 ) -> ConstructedMessage<'a> {
591 let (pos_type, position) = self.queue_position(position);
592 let item = self.construct_media_item(item);
593 let playback_duration = playback_duration.and_then(Self::time_from_secs_f64);
594 let q_item = flat::QueueItem::create(
595 &mut self.builder,
596 &flat::QueueItemArgs {
597 media_item: Some(item),
598 playback_duration: playback_duration.as_ref(),
599 },
600 );
601 create_msg!(self, QueueInsert, item: Some(q_item), position_type: pos_type, position: Some(position))
602 }
603
604 pub fn queue_select(mut self, position: QueuePosition) -> ConstructedMessage<'a> {
605 let (position_type, position) = self.queue_position(position);
606 create_msg!(self, QueueItemSelected, position_type, position: Some(position))
607 }
608
609 pub fn tracks_available(
610 mut self,
611 tracks: impl Iterator<Item = MediaTrack>,
612 ) -> ConstructedMessage<'a> {
613 let tracks_vec = tracks
614 .map(|track| {
615 let title = if let Some(title) = track.title {
616 Some(self.builder.create_string(&title))
617 } else {
618 None
619 };
620 let iso_639 = self.builder.create_string(&track.iso_639);
621 let (metadata_type, metadata) = match track.metadata {
622 Some(MediaTrackMetadata::Video) => (
623 flat::MediaTrackMetadata::Video,
624 Some(
625 flat::VideoTrackMeta::create(
626 &mut self.builder,
627 &flat::VideoTrackMetaArgs { resolution: None },
628 )
629 .as_union_value(),
630 ),
631 ),
632 Some(MediaTrackMetadata::Audio) => (
633 flat::MediaTrackMetadata::Audio,
634 Some(
635 flat::AudioTrackMeta::create(
636 &mut self.builder,
637 &flat::AudioTrackMetaArgs {},
638 )
639 .as_union_value(),
640 ),
641 ),
642 Some(MediaTrackMetadata::Subtitle) => (
643 flat::MediaTrackMetadata::Subtitle,
644 Some(
645 flat::SubtitleTrackMeta::create(
646 &mut self.builder,
647 &flat::SubtitleTrackMetaArgs {},
648 )
649 .as_union_value(),
650 ),
651 ),
652 None => (flat::MediaTrackMetadata::NONE, None),
653 };
654 flat::MediaTrack::create(
655 &mut self.builder,
656 &flat::MediaTrackArgs {
657 id: track.id,
658 title,
659 iso_639: Some(iso_639),
660 metadata_type,
661 metadata,
662 },
663 )
664 })
665 .collect::<Vec<_>>();
666 let tracks = self.builder.create_vector(&tracks_vec);
667
668 create_msg!(self, TracksAvailable, tracks: Some(tracks))
669 }
670
671 fn create_str_vector(
672 &mut self,
673 strs: impl Iterator<Item = &'static str>,
674 ) -> flatbuffers::WIPOffset<flatbuffers::Vector<'a, flatbuffers::ForwardsUOffset<&'a str>>>
675 {
676 let strs = strs
677 .map(|p| self.builder.create_string(p))
678 .collect::<Vec<_>>();
679 self.builder.create_vector(&strs)
680 }
681
682 pub fn receiver_introduction(
683 mut self,
684 device_info: &DeviceInfo,
685 supported_streaming_protocols: impl Iterator<Item = &'static str>,
686 supported_containers: impl Iterator<Item = &'static str>,
687 supported_video_formats: impl Iterator<Item = &'static str>,
688 supported_audio_formats: impl Iterator<Item = &'static str>,
689 supported_subtitle_formats: impl Iterator<Item = &'static str>,
690 supported_hdr_formats: impl Iterator<Item = &'static str>,
691 supported_image_formats: impl Iterator<Item = &'static str>,
692 supports_external_subtitles: bool,
693 supports_mirroring: bool,
694 volume_step_interval: f32,
695 ) -> ConstructedMessage<'a> {
696 let device_info = Some(create_device_info!(self, device_info));
697 let protocols = self.create_str_vector(supported_streaming_protocols);
698 let containers = self.create_str_vector(supported_containers);
699 let videos = self.create_str_vector(supported_video_formats);
700 let audios = self.create_str_vector(supported_audio_formats);
701 let subtitles = self.create_str_vector(supported_subtitle_formats);
702 let hdrs = self.create_str_vector(supported_hdr_formats);
703 let images = self.create_str_vector(supported_image_formats);
704
705 let media_capabilities = flat::MediaCapabilities::create(
706 &mut self.builder,
707 &flat::MediaCapabilitiesArgs {
708 protocols: Some(protocols),
709 containers: Some(containers),
710 video_formats: Some(videos),
711 audio_formats: Some(audios),
712 subtitle_formats: Some(subtitles),
713 hdr_formats: Some(hdrs),
714 image_formats: Some(images),
715 external_subtitles: supports_external_subtitles,
716 mirroring: supports_mirroring,
717 },
718 );
719 let display_capabilities = flat::DisplayCapabilities::create(
720 &mut self.builder,
721 &flat::DisplayCapabilitiesArgs { resolution: None },
722 );
723 let audio_capabilities = flat::AudioCapabilities::create(
724 &mut self.builder,
725 &flat::AudioCapabilitiesArgs {
726 volume_step_interval,
727 },
728 );
729
730 let capabilities = Some(flat::ReceiverCapabilities::create(
731 &mut self.builder,
732 &flat::ReceiverCapabilitiesArgs {
733 media: Some(media_capabilities),
734 display: Some(display_capabilities),
735 audio: Some(audio_capabilities),
736 },
737 ));
738
739 create_msg!(self, ReceiverIntroduction, device_info, capabilities)
740 }
741
742 pub fn error(
743 mut self,
744 packet_num: Option<u32>,
745 kind: flat::ErrorKind,
746 ) -> ConstructedMessage<'a> {
747 create_msg!(self, Error, kind, packet_num)
748 }
749
750 pub fn companion_resource_request(
751 mut self,
752 request_id: u32,
753 resource_id: u32,
754 read_head: Option<flat::ResourceReadHead>,
755 ) -> ConstructedMessage<'a> {
756 create_msg!(self, CompanionResourceRequest, request_id, resource_id, read_head: read_head.as_ref())
757 }
758}
759
760#[derive(Debug, Clone, PartialEq)]
764pub enum MetaValue {
765 String(String),
766 Float(f64),
767 Int(i64),
768 List(Vec<MetaValue>),
769 KvPair { key: String, value: Box<MetaValue> },
771}
772
773macro_rules! read_meta_union {
778 ($holder:expr) => {{
779 let holder = $holder;
780 match holder.value_type() {
781 flat::GenericMetaValue::String => MetaValue::String(
782 holder
783 .value_as_string()
784 .and_then(|s| s.value())
785 .unwrap_or_default()
786 .to_owned(),
787 ),
788 flat::GenericMetaValue::Float => {
789 MetaValue::Float(holder.value_as_float().map(|f| f.value()).unwrap_or_default())
790 }
791 flat::GenericMetaValue::Int => {
792 MetaValue::Int(holder.value_as_int().map(|i| i.value()).unwrap_or_default())
793 }
794 flat::GenericMetaValue::List => holder
795 .value_as_list()
796 .map(read_meta_list)
797 .unwrap_or_else(|| MetaValue::List(Vec::new())),
798 flat::GenericMetaValue::KVPair => holder
799 .value_as_kvpair()
800 .map(|inner| MetaValue::KvPair {
801 key: inner.key().to_owned(),
802 value: Box::new(read_meta_kv_value(&inner)),
803 })
804 .unwrap_or_else(|| MetaValue::List(Vec::new())),
805 _ => MetaValue::List(Vec::new()),
806 }
807 }};
808}
809
810fn read_meta_kv_value(kv: &flat::MetadataKV) -> MetaValue {
811 read_meta_union!(kv)
812}
813
814fn read_wrapped_meta_value(wrapped: &flat::WrappedGenericMetaValue) -> MetaValue {
815 read_meta_union!(wrapped)
816}
817
818fn read_meta_list(list: flat::GenericMetaList) -> MetaValue {
819 let Some(items) = list.value() else {
820 return MetaValue::List(Vec::new());
821 };
822 MetaValue::List(items.iter().map(|w| read_wrapped_meta_value(&w)).collect())
823}
824
825pub fn read_extra_metadata(item: &flat::MediaItem) -> Option<HashMap<String, MetaValue>> {
828 let kvs = item.extra_metadata()?;
829 let mut map = HashMap::with_capacity(kvs.len());
830 for kv in kvs {
831 map.insert(kv.key().to_owned(), read_meta_kv_value(&kv));
832 }
833 Some(map)
834}
835
836pub enum Metadata {
837 Video {
838 subtitle_url: Option<String>,
839 },
840 Audio {
841 artist: Option<String>,
842 album: Option<String>,
843 },
844}
845
846pub struct MediaItem {
847 pub container: String,
849 pub source_url: String,
850 pub start_time: Option<f64>,
852 pub volume: Option<f32>,
854 pub speed: Option<f32>,
856 pub headers: Option<HashMap<String, String>>,
858 pub title: Option<String>,
859 pub thumbnail_url: Option<String>,
860 pub metadata: Option<Metadata>,
861 pub extra_metadata: Option<HashMap<String, MetaValue>>,
862}
863
864#[derive(Debug)]
865pub struct DeviceInfo {
866 pub display_name: Option<String>,
867 pub app_name: Option<String>,
868 pub app_version: Option<String>,
869}
870
871pub enum MediaTrackMetadata {
872 Video,
873 Audio,
874 Subtitle,
875}
876
877pub struct MediaTrack {
878 pub id: u32,
879 pub title: Option<SmolStr>,
880 pub iso_639: SmolStr,
881 pub metadata: Option<MediaTrackMetadata>,
882}
883
884#[derive(Debug, Clone, Copy, PartialEq)]
885pub enum QueuePosition {
886 Index(u8),
887 Front,
888 Back,
889}
890
891#[cfg(test)]
892mod tests {
893 use super::*;
894
895 fn media_item_with_extra(extra: HashMap<String, MetaValue>) -> MediaItem {
896 MediaItem {
897 container: "video/mp4".to_owned(),
898 source_url: "http://example.test/v.mp4".to_owned(),
899 start_time: None,
900 volume: None,
901 speed: None,
902 headers: None,
903 title: Some("Title".to_owned()),
904 thumbnail_url: None,
905 metadata: None,
906 extra_metadata: Some(extra),
907 }
908 }
909
910 #[test]
911 fn hostile_time_values_do_not_panic() {
912 for bad in [-1.0, f64::NAN, f64::INFINITY, f64::MAX] {
913 let mut item = media_item_with_extra(HashMap::new());
914 item.start_time = Some(bad);
915 let _ = MessageBuilder::new().load_queue(
916 std::iter::once((item, Some(bad))),
917 Some(0),
918 false,
919 );
920
921 let mut item = media_item_with_extra(HashMap::new());
922 item.start_time = Some(bad);
923 let _ = MessageBuilder::new().queue_insert(item, Some(bad), QueuePosition::Front);
924 }
925 }
926
927 #[test]
932 fn extra_metadata_survives_relay_strip() {
933 let mut extra = HashMap::new();
934 extra.insert("director".to_owned(), MetaValue::String("Sacha".to_owned()));
935 extra.insert("year".to_owned(), MetaValue::Int(2008));
936 extra.insert("rating".to_owned(), MetaValue::Float(4.5));
937 extra.insert(
938 "tags".to_owned(),
939 MetaValue::List(vec![
940 MetaValue::String("cgi".to_owned()),
941 MetaValue::String("short".to_owned()),
942 ]),
943 );
944 extra.insert(
945 "credits".to_owned(),
946 MetaValue::KvPair {
947 key: "writer".to_owned(),
948 value: Box::new(MetaValue::String("Proog".to_owned())),
949 },
950 );
951
952 let msg = MessageBuilder::new().load_single(media_item_with_extra(extra));
954 let load = flat::root_as_packet(&msg).unwrap().payload_as_load().unwrap();
955
956 let relayed = MessageBuilder::new().from_play_stripped(&load).unwrap();
958 let single = flat::root_as_packet(&relayed)
959 .unwrap()
960 .payload_as_load()
961 .unwrap()
962 .source_as_single()
963 .unwrap();
964
965 let got = read_extra_metadata(&single).expect("relayed item keeps extra_metadata");
966 assert_eq!(got.get("director"), Some(&MetaValue::String("Sacha".to_owned())));
967 assert_eq!(got.get("year"), Some(&MetaValue::Int(2008)));
968 assert_eq!(got.get("rating"), Some(&MetaValue::Float(4.5)));
969 assert_eq!(
970 got.get("tags"),
971 Some(&MetaValue::List(vec![
972 MetaValue::String("cgi".to_owned()),
973 MetaValue::String("short".to_owned()),
974 ]))
975 );
976 assert_eq!(
977 got.get("credits"),
978 Some(&MetaValue::KvPair {
979 key: "writer".to_owned(),
980 value: Box::new(MetaValue::String("Proog".to_owned())),
981 })
982 );
983 assert!(single.headers().is_none());
985 }
986}