Skip to main content

fcast_protocol/
v4.rs

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, // Don't include potentially sensitive values
277            title: item.title().map(|s| self.builder.create_string(s)),
278            thumbnail_url: item.thumbnail_url().map(|s| self.builder.create_string(s)),
279            // The typed Video/Audio metadata union is not relayed yet.
280            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    /// Serialize a [`MetaValue`] into the recursive `GenericMetaValue` union,
356    /// returning the union tag and its offset. Each variant maps 1:1 onto a
357    /// union member.
358    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    /// Serialize an `extra_metadata` map into a `[MetadataKV]` vector, sorted by
427    /// key so the output is deterministic.
428    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    /// Encode a seconds value as a wire `Time` (microseconds). Values a `Duration` cannot represent
443    /// (negative, NaN, infinite, or overflowing) are treated as unset.
444    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    /// Build a `Load` carrying a queue. Each item is paired with an optional `playback_duration`
519    /// (seconds). `None` plays the item to its natural end.
520    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/// A custom metadata value, mirroring the flatbuffer `GenericMetaValue` union
761/// one-to-one. Used for [`MediaItem::extra_metadata`] so sender-supplied fields
762/// map directly onto the wire representation with no lossy conversion.
763#[derive(Debug, Clone, PartialEq)]
764pub enum MetaValue {
765    String(String),
766    Float(f64),
767    Int(i64),
768    List(Vec<MetaValue>),
769    /// A single nested key/value pair (the union's `KVPair`/`MetadataKV` arm).
770    KvPair { key: String, value: Box<MetaValue> },
771}
772
773/// Read one `GenericMetaValue` off a holder (a `MetadataKV` or a
774/// `WrappedGenericMetaValue`, which share the same `value_as_*` accessors) into
775/// a [`MetaValue`]. Inverse of `MessageBuilder::build_meta_value`. An unset or
776/// unknown union tag (never produced by the builder) reads as an empty list.
777macro_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
825/// Read a `MediaItem`'s `extra_metadata` into a map, or `None` when the item
826/// carries none. Inverse of `MessageBuilder::build_extra_metadata`.
827pub 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    /// The MIME type
848    pub container: String,
849    pub source_url: String,
850    /// The time to start playing in seconds
851    pub start_time: Option<f64>,
852    /// The desired volume (0-1)
853    pub volume: Option<f32>,
854    /// Initial playback speed
855    pub speed: Option<f32>,
856    /// HTTP request headers to add to the play request
857    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    /// A Load's custom `extra_metadata` must round-trip through the peer-relay
928    /// path (`from_play_stripped`), covering scalar, float, and nested list
929    /// values. This is the receiver-side guarantee the multi-sender FAST cases
930    /// assert end-to-end.
931    #[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        // Serialize a single-item Load carrying the custom metadata.
953        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        // Run it through the peer-broadcast strip, then read the fields back.
957        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        // Headers are still deliberately dropped on relay.
984        assert!(single.headers().is_none());
985    }
986}