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    fn construct_media_item(
443        &mut self,
444        item: MediaItem,
445    ) -> flatbuffers::WIPOffset<flat::MediaItem<'a>> {
446        let headers = if let Some(headers) = item.headers {
447            let elems = headers
448                .iter()
449                .map(|(k, v)| {
450                    let header = flat::RequestHeaderArgs {
451                        key: create_str!(self, k),
452                        value: create_str!(self, v),
453                    };
454                    flat::RequestHeader::create(&mut self.builder, &header)
455                })
456                .collect::<Vec<_>>();
457            Some(self.builder.create_vector(&elems))
458        } else {
459            None
460        };
461
462        let (metadata_type, metadata) = match item.metadata {
463            Some(Metadata::Video { .. }) => {
464                let meta = flat::VideoMetadataArgs { chapters: None };
465                let meta = flat::VideoMetadata::create(&mut self.builder, &meta).as_union_value();
466                (flat::Metadata::Video, Some(meta))
467            }
468            Some(Metadata::Audio { artist, album }) => {
469                let meta = flat::AudioMetadataArgs {
470                    artist: maybe_create_str!(self, artist),
471                    album: maybe_create_str!(self, album),
472                    chapters: None,
473                };
474                let meta = flat::AudioMetadata::create(&mut self.builder, &meta).as_union_value();
475                (flat::Metadata::Audio, Some(meta))
476            }
477            None => (flat::Metadata::NONE, None),
478        };
479
480        let extra_metadata = item
481            .extra_metadata
482            .as_ref()
483            .filter(|m| !m.is_empty())
484            .map(|m| self.build_extra_metadata(m));
485
486        let start_time = item
487            .start_time
488            .map(|s| flat::Time::new(Duration::from_secs_f64(s).as_micros() as u64));
489        let item = flat::MediaItemArgs {
490            container: create_str!(self, item.container),
491            source_url: create_str!(self, item.source_url),
492            start_time: start_time.as_ref(),
493            volume: item.volume,
494            speed: item.speed,
495            headers,
496            title: maybe_create_str!(self, item.title),
497            thumbnail_url: maybe_create_str!(self, item.thumbnail_url),
498            metadata_type,
499            metadata,
500            extra_metadata,
501        };
502
503        flat::MediaItem::create(&mut self.builder, &item)
504    }
505
506    pub fn load_single(mut self, item: MediaItem) -> ConstructedMessage<'a> {
507        let item = self.construct_media_item(item).as_union_value();
508        create_msg!(self, Load, source_type: flat::MediaSource::Single, source: Some(item))
509    }
510
511    pub fn load_queue<'m>(
512        mut self,
513        items: impl Iterator<Item = MediaItem<'m>>,
514        start_index: Option<u8>,
515    ) -> ConstructedMessage<'a> {
516        let items = items
517            .map(|item| {
518                let item = self.construct_media_item(item);
519                flat::QueueItem::create(
520                    &mut self.builder,
521                    &flat::QueueItemArgs {
522                        media_item: Some(item),
523                        playback_duration: None,
524                    },
525                )
526            })
527            .collect::<Vec<_>>();
528
529        let items = self.builder.create_vector(&items);
530        let queue = flat::Queue::create(
531            &mut self.builder,
532            &flat::QueueArgs {
533                items: Some(items),
534                start_index,
535                autoplay: false,
536            },
537        )
538        .as_union_value();
539
540        create_msg!(self, Load, source_type: flat::MediaSource::Queue, source: Some(queue))
541    }
542
543    fn queue_position(
544        &mut self,
545        position: QueuePosition,
546    ) -> (
547        flat::QueuePosition,
548        flatbuffers::WIPOffset<flatbuffers::UnionWIPOffset>,
549    ) {
550        match position {
551            QueuePosition::Index(index) => (
552                flat::QueuePosition::Index,
553                flat::QueueIndex::create(&mut self.builder, &flat::QueueIndexArgs { index })
554                    .as_union_value(),
555            ),
556            QueuePosition::Front => (
557                flat::QueuePosition::Front,
558                flat::QueueMarkerFront::create(&mut self.builder, &flat::QueueMarkerFrontArgs {})
559                    .as_union_value(),
560            ),
561            QueuePosition::Back => (
562                flat::QueuePosition::Back,
563                flat::QueueMarkerBack::create(&mut self.builder, &flat::QueueMarkerBackArgs {})
564                    .as_union_value(),
565            ),
566        }
567    }
568
569    pub fn queue_remove(mut self, position: QueuePosition) -> ConstructedMessage<'a> {
570        let (typ, position) = self.queue_position(position);
571        create_msg!(self, QueueRemove, position_type: typ, position: Some(position))
572    }
573
574    pub fn queue_insert(
575        mut self,
576        item: MediaItem,
577        position: QueuePosition,
578    ) -> ConstructedMessage<'a> {
579        let (pos_type, position) = self.queue_position(position);
580        let item = self.construct_media_item(item);
581        let q_item = flat::QueueItem::create(
582            &mut self.builder,
583            &flat::QueueItemArgs {
584                media_item: Some(item),
585                playback_duration: None,
586            },
587        );
588        create_msg!(self, QueueInsert, item: Some(q_item), position_type: pos_type, position: Some(position))
589    }
590
591    pub fn queue_select(mut self, position: QueuePosition) -> ConstructedMessage<'a> {
592        let (position_type, position) = self.queue_position(position);
593        create_msg!(self, QueueItemSelected, position_type, position: Some(position))
594    }
595
596    pub fn tracks_available(
597        mut self,
598        tracks: impl Iterator<Item = MediaTrack>,
599    ) -> ConstructedMessage<'a> {
600        let tracks_vec = tracks
601            .map(|track| {
602                let title = if let Some(title) = track.title {
603                    Some(self.builder.create_string(&title))
604                } else {
605                    None
606                };
607                let iso_639 = self.builder.create_string(&track.iso_639);
608                let (metadata_type, metadata) = match track.metadata {
609                    Some(MediaTrackMetadata::Video) => (
610                        flat::MediaTrackMetadata::Video,
611                        Some(
612                            flat::VideoTrackMeta::create(
613                                &mut self.builder,
614                                &flat::VideoTrackMetaArgs { resolution: None },
615                            )
616                            .as_union_value(),
617                        ),
618                    ),
619                    Some(MediaTrackMetadata::Audio) => (
620                        flat::MediaTrackMetadata::Audio,
621                        Some(
622                            flat::AudioTrackMeta::create(
623                                &mut self.builder,
624                                &flat::AudioTrackMetaArgs {},
625                            )
626                            .as_union_value(),
627                        ),
628                    ),
629                    Some(MediaTrackMetadata::Subtitle) => (
630                        flat::MediaTrackMetadata::Subtitle,
631                        Some(
632                            flat::SubtitleTrackMeta::create(
633                                &mut self.builder,
634                                &flat::SubtitleTrackMetaArgs {},
635                            )
636                            .as_union_value(),
637                        ),
638                    ),
639                    None => (flat::MediaTrackMetadata::NONE, None),
640                };
641                flat::MediaTrack::create(
642                    &mut self.builder,
643                    &flat::MediaTrackArgs {
644                        id: track.id,
645                        title,
646                        iso_639: Some(iso_639),
647                        metadata_type,
648                        metadata,
649                    },
650                )
651            })
652            .collect::<Vec<_>>();
653        let tracks = self.builder.create_vector(&tracks_vec);
654
655        create_msg!(self, TracksAvailable, tracks: Some(tracks))
656    }
657
658    fn create_str_vector(
659        &mut self,
660        strs: impl Iterator<Item = &'static str>,
661    ) -> flatbuffers::WIPOffset<flatbuffers::Vector<'a, flatbuffers::ForwardsUOffset<&'a str>>>
662    {
663        let strs = strs
664            .map(|p| self.builder.create_string(p))
665            .collect::<Vec<_>>();
666        self.builder.create_vector(&strs)
667    }
668
669    pub fn receiver_introduction(
670        mut self,
671        device_info: &DeviceInfo,
672        supported_streaming_protocols: impl Iterator<Item = &'static str>,
673        supported_containers: impl Iterator<Item = &'static str>,
674        supported_video_formats: impl Iterator<Item = &'static str>,
675        supported_audio_formats: impl Iterator<Item = &'static str>,
676        supported_subtitle_formats: impl Iterator<Item = &'static str>,
677        supported_hdr_formats: impl Iterator<Item = &'static str>,
678        supported_image_formats: impl Iterator<Item = &'static str>,
679        supports_external_subtitles: bool,
680        supports_mirroring: bool,
681        volume_step_interval: f32,
682    ) -> ConstructedMessage<'a> {
683        let device_info = Some(create_device_info!(self, device_info));
684        let protocols = self.create_str_vector(supported_streaming_protocols);
685        let containers = self.create_str_vector(supported_containers);
686        let videos = self.create_str_vector(supported_video_formats);
687        let audios = self.create_str_vector(supported_audio_formats);
688        let subtitles = self.create_str_vector(supported_subtitle_formats);
689        let hdrs = self.create_str_vector(supported_hdr_formats);
690        let images = self.create_str_vector(supported_image_formats);
691
692        let media_capabilities = flat::MediaCapabilities::create(
693            &mut self.builder,
694            &flat::MediaCapabilitiesArgs {
695                protocols: Some(protocols),
696                containers: Some(containers),
697                video_formats: Some(videos),
698                audio_formats: Some(audios),
699                subtitle_formats: Some(subtitles),
700                hdr_formats: Some(hdrs),
701                image_formats: Some(images),
702                external_subtitles: supports_external_subtitles,
703                mirroring: supports_mirroring,
704            },
705        );
706        let display_capabilities = flat::DisplayCapabilities::create(
707            &mut self.builder,
708            &flat::DisplayCapabilitiesArgs { resolution: None },
709        );
710        let audio_capabilities = flat::AudioCapabilities::create(
711            &mut self.builder,
712            &flat::AudioCapabilitiesArgs {
713                volume_step_interval,
714            },
715        );
716
717        let capabilities = Some(flat::ReceiverCapabilities::create(
718            &mut self.builder,
719            &flat::ReceiverCapabilitiesArgs {
720                media: Some(media_capabilities),
721                display: Some(display_capabilities),
722                audio: Some(audio_capabilities),
723            },
724        ));
725
726        create_msg!(self, ReceiverIntroduction, device_info, capabilities)
727    }
728
729    pub fn error(
730        mut self,
731        packet_num: Option<u32>,
732        kind: flat::ErrorKind,
733    ) -> ConstructedMessage<'a> {
734        create_msg!(self, Error, kind, packet_num)
735    }
736
737    pub fn companion_resource_request(
738        mut self,
739        request_id: u32,
740        resource_id: u32,
741        read_head: Option<flat::ResourceReadHead>,
742    ) -> ConstructedMessage<'a> {
743        create_msg!(self, CompanionResourceRequest, request_id, resource_id, read_head: read_head.as_ref())
744    }
745}
746
747/// A custom metadata value, mirroring the flatbuffer `GenericMetaValue` union
748/// one-to-one. Used for [`MediaItem::extra_metadata`] so sender-supplied fields
749/// map directly onto the wire representation with no lossy conversion.
750#[derive(Debug, Clone, PartialEq)]
751pub enum MetaValue {
752    String(String),
753    Float(f64),
754    Int(i64),
755    List(Vec<MetaValue>),
756    /// A single nested key/value pair (the union's `KVPair`/`MetadataKV` arm).
757    KvPair { key: String, value: Box<MetaValue> },
758}
759
760/// Read one `GenericMetaValue` off a holder (a `MetadataKV` or a
761/// `WrappedGenericMetaValue`, which share the same `value_as_*` accessors) into
762/// a [`MetaValue`]. Inverse of `MessageBuilder::build_meta_value`. An unset or
763/// unknown union tag (never produced by the builder) reads as an empty list.
764macro_rules! read_meta_union {
765    ($holder:expr) => {{
766        let holder = $holder;
767        match holder.value_type() {
768            flat::GenericMetaValue::String => MetaValue::String(
769                holder
770                    .value_as_string()
771                    .and_then(|s| s.value())
772                    .unwrap_or_default()
773                    .to_owned(),
774            ),
775            flat::GenericMetaValue::Float => {
776                MetaValue::Float(holder.value_as_float().map(|f| f.value()).unwrap_or_default())
777            }
778            flat::GenericMetaValue::Int => {
779                MetaValue::Int(holder.value_as_int().map(|i| i.value()).unwrap_or_default())
780            }
781            flat::GenericMetaValue::List => holder
782                .value_as_list()
783                .map(read_meta_list)
784                .unwrap_or_else(|| MetaValue::List(Vec::new())),
785            flat::GenericMetaValue::KVPair => holder
786                .value_as_kvpair()
787                .map(|inner| MetaValue::KvPair {
788                    key: inner.key().to_owned(),
789                    value: Box::new(read_meta_kv_value(&inner)),
790                })
791                .unwrap_or_else(|| MetaValue::List(Vec::new())),
792            _ => MetaValue::List(Vec::new()),
793        }
794    }};
795}
796
797fn read_meta_kv_value(kv: &flat::MetadataKV) -> MetaValue {
798    read_meta_union!(kv)
799}
800
801fn read_wrapped_meta_value(wrapped: &flat::WrappedGenericMetaValue) -> MetaValue {
802    read_meta_union!(wrapped)
803}
804
805fn read_meta_list(list: flat::GenericMetaList) -> MetaValue {
806    let Some(items) = list.value() else {
807        return MetaValue::List(Vec::new());
808    };
809    MetaValue::List(items.iter().map(|w| read_wrapped_meta_value(&w)).collect())
810}
811
812/// Read a `MediaItem`'s `extra_metadata` into a map, or `None` when the item
813/// carries none. Inverse of `MessageBuilder::build_extra_metadata`.
814pub fn read_extra_metadata(item: &flat::MediaItem) -> Option<HashMap<String, MetaValue>> {
815    let kvs = item.extra_metadata()?;
816    let mut map = HashMap::with_capacity(kvs.len());
817    for kv in kvs {
818        map.insert(kv.key().to_owned(), read_meta_kv_value(&kv));
819    }
820    Some(map)
821}
822
823pub enum Metadata {
824    Video {
825        subtitle_url: Option<String>,
826    },
827    Audio {
828        artist: Option<String>,
829        album: Option<String>,
830    },
831}
832
833pub struct MediaItem<'a> {
834    /// The MIME type
835    pub container: String,
836    pub source_url: String,
837    /// The time to start playing in seconds
838    pub start_time: Option<f64>,
839    /// The desired volume (0-1)
840    pub volume: Option<f32>,
841    /// Initial playback speed
842    pub speed: Option<f32>,
843    /// HTTP request headers to add to the play request
844    pub headers: Option<HashMap<String, String>>,
845    pub title: Option<&'a str>,
846    pub thumbnail_url: Option<&'a str>,
847    pub metadata: Option<Metadata>,
848    pub extra_metadata: Option<HashMap<String, MetaValue>>,
849}
850
851#[derive(Debug)]
852pub struct DeviceInfo {
853    pub display_name: Option<String>,
854    pub app_name: Option<String>,
855    pub app_version: Option<String>,
856}
857
858pub enum MediaTrackMetadata {
859    Video,
860    Audio,
861    Subtitle,
862}
863
864pub struct MediaTrack {
865    pub id: u32,
866    pub title: Option<SmolStr>,
867    pub iso_639: SmolStr,
868    pub metadata: Option<MediaTrackMetadata>,
869}
870
871#[derive(Debug, Clone, Copy, PartialEq)]
872pub enum QueuePosition {
873    Index(u8),
874    Front,
875    Back,
876}
877
878#[cfg(test)]
879mod tests {
880    use super::*;
881
882    fn media_item_with_extra(extra: HashMap<String, MetaValue>) -> MediaItem<'static> {
883        MediaItem {
884            container: "video/mp4".to_owned(),
885            source_url: "http://example.test/v.mp4".to_owned(),
886            start_time: None,
887            volume: None,
888            speed: None,
889            headers: None,
890            title: Some("Title"),
891            thumbnail_url: None,
892            metadata: None,
893            extra_metadata: Some(extra),
894        }
895    }
896
897    /// A Load's custom `extra_metadata` must round-trip through the peer-relay
898    /// path (`from_play_stripped`), covering scalar, float, and nested list
899    /// values. This is the receiver-side guarantee the multi-sender FAST cases
900    /// assert end-to-end.
901    #[test]
902    fn extra_metadata_survives_relay_strip() {
903        let mut extra = HashMap::new();
904        extra.insert("director".to_owned(), MetaValue::String("Sacha".to_owned()));
905        extra.insert("year".to_owned(), MetaValue::Int(2008));
906        extra.insert("rating".to_owned(), MetaValue::Float(4.5));
907        extra.insert(
908            "tags".to_owned(),
909            MetaValue::List(vec![
910                MetaValue::String("cgi".to_owned()),
911                MetaValue::String("short".to_owned()),
912            ]),
913        );
914        extra.insert(
915            "credits".to_owned(),
916            MetaValue::KvPair {
917                key: "writer".to_owned(),
918                value: Box::new(MetaValue::String("Proog".to_owned())),
919            },
920        );
921
922        // Serialize a single-item Load carrying the custom metadata.
923        let msg = MessageBuilder::new().load_single(media_item_with_extra(extra));
924        let load = flat::root_as_packet(&msg).unwrap().payload_as_load().unwrap();
925
926        // Run it through the peer-broadcast strip, then read the fields back.
927        let relayed = MessageBuilder::new().from_play_stripped(&load).unwrap();
928        let single = flat::root_as_packet(&relayed)
929            .unwrap()
930            .payload_as_load()
931            .unwrap()
932            .source_as_single()
933            .unwrap();
934
935        let got = read_extra_metadata(&single).expect("relayed item keeps extra_metadata");
936        assert_eq!(got.get("director"), Some(&MetaValue::String("Sacha".to_owned())));
937        assert_eq!(got.get("year"), Some(&MetaValue::Int(2008)));
938        assert_eq!(got.get("rating"), Some(&MetaValue::Float(4.5)));
939        assert_eq!(
940            got.get("tags"),
941            Some(&MetaValue::List(vec![
942                MetaValue::String("cgi".to_owned()),
943                MetaValue::String("short".to_owned()),
944            ]))
945        );
946        assert_eq!(
947            got.get("credits"),
948            Some(&MetaValue::KvPair {
949                key: "writer".to_owned(),
950                value: Box::new(MetaValue::String("Proog".to_owned())),
951            })
952        );
953        // Headers are still deliberately dropped on relay.
954        assert!(single.headers().is_none());
955    }
956}