1#![allow(clippy::too_many_arguments)]
7
8use crate::tlv;
9use anyhow;
10use serde_json;
11
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
16#[repr(u8)]
17pub enum MetricType {
18 Pixels = 0,
20 Percentage = 1,
22}
23
24impl MetricType {
25 pub fn from_u8(value: u8) -> Option<Self> {
27 match value {
28 0 => Some(MetricType::Pixels),
29 1 => Some(MetricType::Percentage),
30 _ => None,
31 }
32 }
33
34 pub fn to_u8(self) -> u8 {
36 self as u8
37 }
38}
39
40impl From<MetricType> for u8 {
41 fn from(val: MetricType) -> Self {
42 val as u8
43 }
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
47#[repr(u8)]
48pub enum ParameterEnum {
49 Actor = 0,
51 Channel = 1,
53 Character = 2,
55 Director = 3,
57 Event = 4,
59 Franchise = 5,
61 Genre = 6,
63 League = 7,
65 Popularity = 8,
67 Provider = 9,
69 Sport = 10,
71 Sportsteam = 11,
73 Type = 12,
75 Video = 13,
77 Season = 14,
79 Episode = 15,
81 Any = 16,
83}
84
85impl ParameterEnum {
86 pub fn from_u8(value: u8) -> Option<Self> {
88 match value {
89 0 => Some(ParameterEnum::Actor),
90 1 => Some(ParameterEnum::Channel),
91 2 => Some(ParameterEnum::Character),
92 3 => Some(ParameterEnum::Director),
93 4 => Some(ParameterEnum::Event),
94 5 => Some(ParameterEnum::Franchise),
95 6 => Some(ParameterEnum::Genre),
96 7 => Some(ParameterEnum::League),
97 8 => Some(ParameterEnum::Popularity),
98 9 => Some(ParameterEnum::Provider),
99 10 => Some(ParameterEnum::Sport),
100 11 => Some(ParameterEnum::Sportsteam),
101 12 => Some(ParameterEnum::Type),
102 13 => Some(ParameterEnum::Video),
103 14 => Some(ParameterEnum::Season),
104 15 => Some(ParameterEnum::Episode),
105 16 => Some(ParameterEnum::Any),
106 _ => None,
107 }
108 }
109
110 pub fn to_u8(self) -> u8 {
112 self as u8
113 }
114}
115
116impl From<ParameterEnum> for u8 {
117 fn from(val: ParameterEnum) -> Self {
118 val as u8
119 }
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
123#[repr(u8)]
124pub enum Status {
125 Success = 0,
127 Urlnotavailable = 1,
129 Authfailed = 2,
131 Texttracknotavailable = 3,
133 Audiotracknotavailable = 4,
135}
136
137impl Status {
138 pub fn from_u8(value: u8) -> Option<Self> {
140 match value {
141 0 => Some(Status::Success),
142 1 => Some(Status::Urlnotavailable),
143 2 => Some(Status::Authfailed),
144 3 => Some(Status::Texttracknotavailable),
145 4 => Some(Status::Audiotracknotavailable),
146 _ => None,
147 }
148 }
149
150 pub fn to_u8(self) -> u8 {
152 self as u8
153 }
154}
155
156impl From<Status> for u8 {
157 fn from(val: Status) -> Self {
158 val as u8
159 }
160}
161
162pub type SupportedProtocols = u8;
166
167pub mod supportedprotocols {
169 pub const DASH: u8 = 0x01;
171 pub const HLS: u8 = 0x02;
173}
174
175#[derive(Debug, serde::Serialize)]
178pub struct AdditionalInfo {
179 pub name: Option<String>,
180 pub value: Option<String>,
181}
182
183#[derive(Debug, serde::Serialize)]
184pub struct BrandingInformation {
185 pub provider_name: Option<String>,
186 pub background: Option<StyleInformation>,
187 pub logo: Option<StyleInformation>,
188 pub progress_bar: Option<StyleInformation>,
189 pub splash: Option<StyleInformation>,
190 pub water_mark: Option<StyleInformation>,
191}
192
193#[derive(Debug, serde::Serialize)]
194pub struct ContentSearch {
195 pub parameter_list: Option<Vec<Parameter>>,
196}
197
198#[derive(Debug, serde::Serialize)]
199pub struct Dimension {
200 pub width: Option<u8>,
201 pub height: Option<u8>,
202 pub metric: Option<MetricType>,
203}
204
205#[derive(Debug, serde::Serialize)]
206pub struct Parameter {
207 pub type_: Option<ParameterEnum>,
208 pub value: Option<String>,
209 pub external_id_list: Option<Vec<AdditionalInfo>>,
210}
211
212#[derive(Debug, serde::Serialize)]
213pub struct PlaybackPreferences {
214 pub playback_position: Option<u64>,
215 pub text_track: Option<TrackPreference>,
216 pub audio_tracks: Option<Vec<TrackPreference>>,
217}
218
219#[derive(Debug, serde::Serialize)]
220pub struct StyleInformation {
221 pub image_url: Option<String>,
222 pub color: Option<String>,
223 pub size: Option<Dimension>,
224}
225
226#[derive(Debug, serde::Serialize)]
227pub struct TrackPreference {
228 pub language_code: Option<String>,
229 pub characteristics: Option<Vec<u8>>,
230 pub audio_output_index: Option<u8>,
231}
232
233pub fn encode_launch_content(search: ContentSearch, auto_play: bool, data: String, playback_preferences: PlaybackPreferences, use_current_context: bool) -> anyhow::Result<Vec<u8>> {
237 let mut search_fields = Vec::new();
239 if let Some(listv) = search.parameter_list {
240 let inner_vec: Vec<_> = listv.into_iter().map(|inner| {
241 let mut nested_fields = Vec::new();
242 if let Some(x) = inner.type_ { nested_fields.push((0, tlv::TlvItemValueEnc::UInt8(x.to_u8())).into()); }
243 if let Some(x) = inner.value { nested_fields.push((1, tlv::TlvItemValueEnc::String(x.clone())).into()); }
244 if let Some(listv) = inner.external_id_list {
245 let inner_vec: Vec<_> = listv.into_iter().map(|inner| {
246 let mut nested_fields = Vec::new();
247 if let Some(x) = inner.name { nested_fields.push((0, tlv::TlvItemValueEnc::String(x.clone())).into()); }
248 if let Some(x) = inner.value { nested_fields.push((1, tlv::TlvItemValueEnc::String(x.clone())).into()); }
249 (0, tlv::TlvItemValueEnc::StructAnon(nested_fields)).into()
250 }).collect();
251 nested_fields.push((2, tlv::TlvItemValueEnc::Array(inner_vec)).into());
252 }
253 (0, tlv::TlvItemValueEnc::StructAnon(nested_fields)).into()
254 }).collect();
255 search_fields.push((0, tlv::TlvItemValueEnc::Array(inner_vec)).into());
256 }
257 let mut playback_preferences_fields = Vec::new();
259 if let Some(x) = playback_preferences.playback_position { playback_preferences_fields.push((0, tlv::TlvItemValueEnc::UInt64(x)).into()); }
260 if let Some(inner) = playback_preferences.text_track {
261 let mut text_track_nested_fields = Vec::new();
262 if let Some(x) = inner.language_code { text_track_nested_fields.push((0, tlv::TlvItemValueEnc::String(x.clone())).into()); }
263 if let Some(listv) = inner.characteristics { text_track_nested_fields.push((1, tlv::TlvItemValueEnc::StructAnon(listv.into_iter().map(|x| (0, tlv::TlvItemValueEnc::UInt8(x)).into()).collect())).into()); }
264 if let Some(x) = inner.audio_output_index { text_track_nested_fields.push((2, tlv::TlvItemValueEnc::UInt8(x)).into()); }
265 playback_preferences_fields.push((1, tlv::TlvItemValueEnc::StructInvisible(text_track_nested_fields)).into());
266 }
267 if let Some(listv) = playback_preferences.audio_tracks {
268 let inner_vec: Vec<_> = listv.into_iter().map(|inner| {
269 let mut nested_fields = Vec::new();
270 if let Some(x) = inner.language_code { nested_fields.push((0, tlv::TlvItemValueEnc::String(x.clone())).into()); }
271 if let Some(listv) = inner.characteristics { nested_fields.push((1, tlv::TlvItemValueEnc::StructAnon(listv.into_iter().map(|x| (0, tlv::TlvItemValueEnc::UInt8(x)).into()).collect())).into()); }
272 if let Some(x) = inner.audio_output_index { nested_fields.push((2, tlv::TlvItemValueEnc::UInt8(x)).into()); }
273 (0, tlv::TlvItemValueEnc::StructAnon(nested_fields)).into()
274 }).collect();
275 playback_preferences_fields.push((2, tlv::TlvItemValueEnc::Array(inner_vec)).into());
276 }
277 let tlv = tlv::TlvItemEnc {
278 tag: 0,
279 value: tlv::TlvItemValueEnc::StructInvisible(vec![
280 (0, tlv::TlvItemValueEnc::StructInvisible(search_fields)).into(),
281 (1, tlv::TlvItemValueEnc::Bool(auto_play)).into(),
282 (2, tlv::TlvItemValueEnc::String(data)).into(),
283 (3, tlv::TlvItemValueEnc::StructInvisible(playback_preferences_fields)).into(),
284 (4, tlv::TlvItemValueEnc::Bool(use_current_context)).into(),
285 ]),
286 };
287 Ok(tlv.encode()?)
288}
289
290pub fn encode_launch_url(content_url: String, display_string: String, branding_information: BrandingInformation, playback_preferences: PlaybackPreferences) -> anyhow::Result<Vec<u8>> {
292 let mut branding_information_fields = Vec::new();
294 if let Some(x) = branding_information.provider_name { branding_information_fields.push((0, tlv::TlvItemValueEnc::String(x.clone())).into()); }
295 if let Some(inner) = branding_information.background {
296 let mut background_nested_fields = Vec::new();
297 if let Some(x) = inner.image_url { background_nested_fields.push((0, tlv::TlvItemValueEnc::String(x.clone())).into()); }
298 if let Some(x) = inner.color { background_nested_fields.push((1, tlv::TlvItemValueEnc::String(x.clone())).into()); }
299 if let Some(inner) = inner.size {
300 let mut size_nested_fields = Vec::new();
301 if let Some(x) = inner.metric { size_nested_fields.push((2, tlv::TlvItemValueEnc::UInt8(x.to_u8())).into()); }
304 background_nested_fields.push((2, tlv::TlvItemValueEnc::StructInvisible(size_nested_fields)).into());
305 }
306 branding_information_fields.push((1, tlv::TlvItemValueEnc::StructInvisible(background_nested_fields)).into());
307 }
308 if let Some(inner) = branding_information.logo {
309 let mut logo_nested_fields = Vec::new();
310 if let Some(x) = inner.image_url { logo_nested_fields.push((0, tlv::TlvItemValueEnc::String(x.clone())).into()); }
311 if let Some(x) = inner.color { logo_nested_fields.push((1, tlv::TlvItemValueEnc::String(x.clone())).into()); }
312 if let Some(inner) = inner.size {
313 let mut size_nested_fields = Vec::new();
314 if let Some(x) = inner.metric { size_nested_fields.push((2, tlv::TlvItemValueEnc::UInt8(x.to_u8())).into()); }
317 logo_nested_fields.push((2, tlv::TlvItemValueEnc::StructInvisible(size_nested_fields)).into());
318 }
319 branding_information_fields.push((2, tlv::TlvItemValueEnc::StructInvisible(logo_nested_fields)).into());
320 }
321 if let Some(inner) = branding_information.progress_bar {
322 let mut progress_bar_nested_fields = Vec::new();
323 if let Some(x) = inner.image_url { progress_bar_nested_fields.push((0, tlv::TlvItemValueEnc::String(x.clone())).into()); }
324 if let Some(x) = inner.color { progress_bar_nested_fields.push((1, tlv::TlvItemValueEnc::String(x.clone())).into()); }
325 if let Some(inner) = inner.size {
326 let mut size_nested_fields = Vec::new();
327 if let Some(x) = inner.metric { size_nested_fields.push((2, tlv::TlvItemValueEnc::UInt8(x.to_u8())).into()); }
330 progress_bar_nested_fields.push((2, tlv::TlvItemValueEnc::StructInvisible(size_nested_fields)).into());
331 }
332 branding_information_fields.push((3, tlv::TlvItemValueEnc::StructInvisible(progress_bar_nested_fields)).into());
333 }
334 if let Some(inner) = branding_information.splash {
335 let mut splash_nested_fields = Vec::new();
336 if let Some(x) = inner.image_url { splash_nested_fields.push((0, tlv::TlvItemValueEnc::String(x.clone())).into()); }
337 if let Some(x) = inner.color { splash_nested_fields.push((1, tlv::TlvItemValueEnc::String(x.clone())).into()); }
338 if let Some(inner) = inner.size {
339 let mut size_nested_fields = Vec::new();
340 if let Some(x) = inner.metric { size_nested_fields.push((2, tlv::TlvItemValueEnc::UInt8(x.to_u8())).into()); }
343 splash_nested_fields.push((2, tlv::TlvItemValueEnc::StructInvisible(size_nested_fields)).into());
344 }
345 branding_information_fields.push((4, tlv::TlvItemValueEnc::StructInvisible(splash_nested_fields)).into());
346 }
347 if let Some(inner) = branding_information.water_mark {
348 let mut water_mark_nested_fields = Vec::new();
349 if let Some(x) = inner.image_url { water_mark_nested_fields.push((0, tlv::TlvItemValueEnc::String(x.clone())).into()); }
350 if let Some(x) = inner.color { water_mark_nested_fields.push((1, tlv::TlvItemValueEnc::String(x.clone())).into()); }
351 if let Some(inner) = inner.size {
352 let mut size_nested_fields = Vec::new();
353 if let Some(x) = inner.metric { size_nested_fields.push((2, tlv::TlvItemValueEnc::UInt8(x.to_u8())).into()); }
356 water_mark_nested_fields.push((2, tlv::TlvItemValueEnc::StructInvisible(size_nested_fields)).into());
357 }
358 branding_information_fields.push((5, tlv::TlvItemValueEnc::StructInvisible(water_mark_nested_fields)).into());
359 }
360 let mut playback_preferences_fields = Vec::new();
362 if let Some(x) = playback_preferences.playback_position { playback_preferences_fields.push((0, tlv::TlvItemValueEnc::UInt64(x)).into()); }
363 if let Some(inner) = playback_preferences.text_track {
364 let mut text_track_nested_fields = Vec::new();
365 if let Some(x) = inner.language_code { text_track_nested_fields.push((0, tlv::TlvItemValueEnc::String(x.clone())).into()); }
366 if let Some(listv) = inner.characteristics { text_track_nested_fields.push((1, tlv::TlvItemValueEnc::StructAnon(listv.into_iter().map(|x| (0, tlv::TlvItemValueEnc::UInt8(x)).into()).collect())).into()); }
367 if let Some(x) = inner.audio_output_index { text_track_nested_fields.push((2, tlv::TlvItemValueEnc::UInt8(x)).into()); }
368 playback_preferences_fields.push((1, tlv::TlvItemValueEnc::StructInvisible(text_track_nested_fields)).into());
369 }
370 if let Some(listv) = playback_preferences.audio_tracks {
371 let inner_vec: Vec<_> = listv.into_iter().map(|inner| {
372 let mut nested_fields = Vec::new();
373 if let Some(x) = inner.language_code { nested_fields.push((0, tlv::TlvItemValueEnc::String(x.clone())).into()); }
374 if let Some(listv) = inner.characteristics { nested_fields.push((1, tlv::TlvItemValueEnc::StructAnon(listv.into_iter().map(|x| (0, tlv::TlvItemValueEnc::UInt8(x)).into()).collect())).into()); }
375 if let Some(x) = inner.audio_output_index { nested_fields.push((2, tlv::TlvItemValueEnc::UInt8(x)).into()); }
376 (0, tlv::TlvItemValueEnc::StructAnon(nested_fields)).into()
377 }).collect();
378 playback_preferences_fields.push((2, tlv::TlvItemValueEnc::Array(inner_vec)).into());
379 }
380 let tlv = tlv::TlvItemEnc {
381 tag: 0,
382 value: tlv::TlvItemValueEnc::StructInvisible(vec![
383 (0, tlv::TlvItemValueEnc::String(content_url)).into(),
384 (1, tlv::TlvItemValueEnc::String(display_string)).into(),
385 (2, tlv::TlvItemValueEnc::StructInvisible(branding_information_fields)).into(),
386 (3, tlv::TlvItemValueEnc::StructInvisible(playback_preferences_fields)).into(),
387 ]),
388 };
389 Ok(tlv.encode()?)
390}
391
392pub fn decode_accept_header(inp: &tlv::TlvItemValue) -> anyhow::Result<Vec<String>> {
396 let mut res = Vec::new();
397 if let tlv::TlvItemValue::List(v) = inp {
398 for item in v {
399 if let tlv::TlvItemValue::String(s) = &item.value {
400 res.push(s.clone());
401 }
402 }
403 }
404 Ok(res)
405}
406
407pub fn decode_supported_streaming_protocols(inp: &tlv::TlvItemValue) -> anyhow::Result<SupportedProtocols> {
409 if let tlv::TlvItemValue::Int(v) = inp {
410 Ok(*v as u8)
411 } else {
412 Err(anyhow::anyhow!("Expected Integer"))
413 }
414}
415
416
417pub fn decode_attribute_json(cluster_id: u32, attribute_id: u32, tlv_value: &crate::tlv::TlvItemValue) -> String {
429 if cluster_id != 0x050A {
431 return format!("{{\"error\": \"Invalid cluster ID. Expected 0x050A, got {}\"}}", cluster_id);
432 }
433
434 match attribute_id {
435 0x0000 => {
436 match decode_accept_header(tlv_value) {
437 Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
438 Err(e) => format!("{{\"error\": \"{}\"}}", e),
439 }
440 }
441 0x0001 => {
442 match decode_supported_streaming_protocols(tlv_value) {
443 Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
444 Err(e) => format!("{{\"error\": \"{}\"}}", e),
445 }
446 }
447 _ => format!("{{\"error\": \"Unknown attribute ID: {}\"}}", attribute_id),
448 }
449}
450
451pub fn get_attribute_list() -> Vec<(u32, &'static str)> {
456 vec![
457 (0x0000, "AcceptHeader"),
458 (0x0001, "SupportedStreamingProtocols"),
459 ]
460}
461
462pub fn get_command_list() -> Vec<(u32, &'static str)> {
465 vec![
466 (0x00, "LaunchContent"),
467 (0x01, "LaunchURL"),
468 ]
469}
470
471pub fn get_command_name(cmd_id: u32) -> Option<&'static str> {
472 match cmd_id {
473 0x00 => Some("LaunchContent"),
474 0x01 => Some("LaunchURL"),
475 _ => None,
476 }
477}
478
479pub fn get_command_schema(cmd_id: u32) -> Option<Vec<crate::clusters::codec::CommandField>> {
480 match cmd_id {
481 0x00 => Some(vec![
482 crate::clusters::codec::CommandField { tag: 0, name: "search", kind: crate::clusters::codec::FieldKind::Struct { name: "ContentSearchStruct" }, optional: false, nullable: false },
483 crate::clusters::codec::CommandField { tag: 1, name: "auto_play", kind: crate::clusters::codec::FieldKind::Bool, optional: false, nullable: false },
484 crate::clusters::codec::CommandField { tag: 2, name: "data", kind: crate::clusters::codec::FieldKind::String, optional: true, nullable: false },
485 crate::clusters::codec::CommandField { tag: 3, name: "playback_preferences", kind: crate::clusters::codec::FieldKind::Struct { name: "PlaybackPreferencesStruct" }, optional: true, nullable: false },
486 crate::clusters::codec::CommandField { tag: 4, name: "use_current_context", kind: crate::clusters::codec::FieldKind::Bool, optional: true, nullable: false },
487 ]),
488 0x01 => Some(vec![
489 crate::clusters::codec::CommandField { tag: 0, name: "content_url", kind: crate::clusters::codec::FieldKind::String, optional: false, nullable: false },
490 crate::clusters::codec::CommandField { tag: 1, name: "display_string", kind: crate::clusters::codec::FieldKind::String, optional: true, nullable: false },
491 crate::clusters::codec::CommandField { tag: 2, name: "branding_information", kind: crate::clusters::codec::FieldKind::Struct { name: "BrandingInformationStruct" }, optional: true, nullable: false },
492 crate::clusters::codec::CommandField { tag: 3, name: "playback_preferences", kind: crate::clusters::codec::FieldKind::Struct { name: "PlaybackPreferencesStruct" }, optional: true, nullable: false },
493 ]),
494 _ => None,
495 }
496}
497
498pub fn encode_command_json(cmd_id: u32, _args: &serde_json::Value) -> anyhow::Result<Vec<u8>> {
499 match cmd_id {
500 0x00 => Err(anyhow::anyhow!("command \"LaunchContent\" has complex args: use raw mode")),
501 0x01 => Err(anyhow::anyhow!("command \"LaunchURL\" has complex args: use raw mode")),
502 _ => Err(anyhow::anyhow!("unknown command ID: 0x{:02X}", cmd_id)),
503 }
504}
505
506#[derive(Debug, serde::Serialize)]
507pub struct LauncherResponse {
508 pub status: Option<Status>,
509 pub data: Option<String>,
510}
511
512pub fn decode_launcher_response(inp: &tlv::TlvItemValue) -> anyhow::Result<LauncherResponse> {
516 if let tlv::TlvItemValue::List(_fields) = inp {
517 let item = tlv::TlvItem { tag: 0, value: inp.clone() };
518 Ok(LauncherResponse {
519 status: item.get_int(&[0]).and_then(|v| Status::from_u8(v as u8)),
520 data: item.get_string_owned(&[1]),
521 })
522 } else {
523 Err(anyhow::anyhow!("Expected struct fields"))
524 }
525}
526
527pub async fn launch_content(conn: &crate::controller::Connection, endpoint: u16, search: ContentSearch, auto_play: bool, data: String, playback_preferences: PlaybackPreferences, use_current_context: bool) -> anyhow::Result<LauncherResponse> {
531 let tlv = conn.invoke_request2(endpoint, crate::clusters::defs::CLUSTER_ID_CONTENT_LAUNCHER, crate::clusters::defs::CLUSTER_CONTENT_LAUNCHER_CMD_ID_LAUNCHCONTENT, &encode_launch_content(search, auto_play, data, playback_preferences, use_current_context)?).await?;
532 decode_launcher_response(&tlv)
533}
534
535pub async fn launch_url(conn: &crate::controller::Connection, endpoint: u16, content_url: String, display_string: String, branding_information: BrandingInformation, playback_preferences: PlaybackPreferences) -> anyhow::Result<LauncherResponse> {
537 let tlv = conn.invoke_request2(endpoint, crate::clusters::defs::CLUSTER_ID_CONTENT_LAUNCHER, crate::clusters::defs::CLUSTER_CONTENT_LAUNCHER_CMD_ID_LAUNCHURL, &encode_launch_url(content_url, display_string, branding_information, playback_preferences)?).await?;
538 decode_launcher_response(&tlv)
539}
540
541pub async fn read_accept_header(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<Vec<String>> {
543 let tlv = conn.read_request2(endpoint, crate::clusters::defs::CLUSTER_ID_CONTENT_LAUNCHER, crate::clusters::defs::CLUSTER_CONTENT_LAUNCHER_ATTR_ID_ACCEPTHEADER).await?;
544 decode_accept_header(&tlv)
545}
546
547pub async fn read_supported_streaming_protocols(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<SupportedProtocols> {
549 let tlv = conn.read_request2(endpoint, crate::clusters::defs::CLUSTER_ID_CONTENT_LAUNCHER, crate::clusters::defs::CLUSTER_CONTENT_LAUNCHER_ATTR_ID_SUPPORTEDSTREAMINGPROTOCOLS).await?;
550 decode_supported_streaming_protocols(&tlv)
551}
552