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: Option<String>, playback_preferences: Option<PlaybackPreferences>, use_current_context: Option<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 tlv_fields: Vec<tlv::TlvItemEnc> = Vec::new();
258 tlv_fields.push((0, tlv::TlvItemValueEnc::StructInvisible(search_fields)).into());
259 tlv_fields.push((1, tlv::TlvItemValueEnc::Bool(auto_play)).into());
260 if let Some(x) = data { tlv_fields.push((2, tlv::TlvItemValueEnc::String(x)).into()); }
261 if let Some(playback_preferences) = playback_preferences {
262 let mut playback_preferences_fields = Vec::new();
264 if let Some(x) = playback_preferences.playback_position { playback_preferences_fields.push((0, tlv::TlvItemValueEnc::UInt64(x)).into()); }
265 if let Some(inner) = playback_preferences.text_track {
266 let mut text_track_nested_fields = Vec::new();
267 if let Some(x) = inner.language_code { text_track_nested_fields.push((0, tlv::TlvItemValueEnc::String(x.clone())).into()); }
268 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()); }
269 if let Some(x) = inner.audio_output_index { text_track_nested_fields.push((2, tlv::TlvItemValueEnc::UInt8(x)).into()); }
270 playback_preferences_fields.push((1, tlv::TlvItemValueEnc::StructInvisible(text_track_nested_fields)).into());
271 }
272 if let Some(listv) = playback_preferences.audio_tracks {
273 let inner_vec: Vec<_> = listv.into_iter().map(|inner| {
274 let mut nested_fields = Vec::new();
275 if let Some(x) = inner.language_code { nested_fields.push((0, tlv::TlvItemValueEnc::String(x.clone())).into()); }
276 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()); }
277 if let Some(x) = inner.audio_output_index { nested_fields.push((2, tlv::TlvItemValueEnc::UInt8(x)).into()); }
278 (0, tlv::TlvItemValueEnc::StructAnon(nested_fields)).into()
279 }).collect();
280 playback_preferences_fields.push((2, tlv::TlvItemValueEnc::Array(inner_vec)).into());
281 }
282 tlv_fields.push((3, tlv::TlvItemValueEnc::StructInvisible(playback_preferences_fields)).into());
283 }
284 if let Some(x) = use_current_context { tlv_fields.push((4, tlv::TlvItemValueEnc::Bool(x)).into()); }
285 let tlv = tlv::TlvItemEnc {
286 tag: 0,
287 value: tlv::TlvItemValueEnc::StructInvisible(tlv_fields),
288 };
289 Ok(tlv.encode()?)
290}
291
292pub fn encode_launch_url(content_url: String, display_string: Option<String>, branding_information: Option<BrandingInformation>, playback_preferences: Option<PlaybackPreferences>) -> anyhow::Result<Vec<u8>> {
294 let mut tlv_fields: Vec<tlv::TlvItemEnc> = Vec::new();
295 tlv_fields.push((0, tlv::TlvItemValueEnc::String(content_url)).into());
296 if let Some(x) = display_string { tlv_fields.push((1, tlv::TlvItemValueEnc::String(x)).into()); }
297 if let Some(branding_information) = branding_information {
298 let mut branding_information_fields = Vec::new();
300 if let Some(x) = branding_information.provider_name { branding_information_fields.push((0, tlv::TlvItemValueEnc::String(x.clone())).into()); }
301 if let Some(inner) = branding_information.background {
302 let mut background_nested_fields = Vec::new();
303 if let Some(x) = inner.image_url { background_nested_fields.push((0, tlv::TlvItemValueEnc::String(x.clone())).into()); }
304 if let Some(x) = inner.color { background_nested_fields.push((1, tlv::TlvItemValueEnc::String(x.clone())).into()); }
305 if let Some(inner) = inner.size {
306 let mut size_nested_fields = Vec::new();
307 if let Some(x) = inner.metric { size_nested_fields.push((2, tlv::TlvItemValueEnc::UInt8(x.to_u8())).into()); }
310 background_nested_fields.push((2, tlv::TlvItemValueEnc::StructInvisible(size_nested_fields)).into());
311 }
312 branding_information_fields.push((1, tlv::TlvItemValueEnc::StructInvisible(background_nested_fields)).into());
313 }
314 if let Some(inner) = branding_information.logo {
315 let mut logo_nested_fields = Vec::new();
316 if let Some(x) = inner.image_url { logo_nested_fields.push((0, tlv::TlvItemValueEnc::String(x.clone())).into()); }
317 if let Some(x) = inner.color { logo_nested_fields.push((1, tlv::TlvItemValueEnc::String(x.clone())).into()); }
318 if let Some(inner) = inner.size {
319 let mut size_nested_fields = Vec::new();
320 if let Some(x) = inner.metric { size_nested_fields.push((2, tlv::TlvItemValueEnc::UInt8(x.to_u8())).into()); }
323 logo_nested_fields.push((2, tlv::TlvItemValueEnc::StructInvisible(size_nested_fields)).into());
324 }
325 branding_information_fields.push((2, tlv::TlvItemValueEnc::StructInvisible(logo_nested_fields)).into());
326 }
327 if let Some(inner) = branding_information.progress_bar {
328 let mut progress_bar_nested_fields = Vec::new();
329 if let Some(x) = inner.image_url { progress_bar_nested_fields.push((0, tlv::TlvItemValueEnc::String(x.clone())).into()); }
330 if let Some(x) = inner.color { progress_bar_nested_fields.push((1, tlv::TlvItemValueEnc::String(x.clone())).into()); }
331 if let Some(inner) = inner.size {
332 let mut size_nested_fields = Vec::new();
333 if let Some(x) = inner.metric { size_nested_fields.push((2, tlv::TlvItemValueEnc::UInt8(x.to_u8())).into()); }
336 progress_bar_nested_fields.push((2, tlv::TlvItemValueEnc::StructInvisible(size_nested_fields)).into());
337 }
338 branding_information_fields.push((3, tlv::TlvItemValueEnc::StructInvisible(progress_bar_nested_fields)).into());
339 }
340 if let Some(inner) = branding_information.splash {
341 let mut splash_nested_fields = Vec::new();
342 if let Some(x) = inner.image_url { splash_nested_fields.push((0, tlv::TlvItemValueEnc::String(x.clone())).into()); }
343 if let Some(x) = inner.color { splash_nested_fields.push((1, tlv::TlvItemValueEnc::String(x.clone())).into()); }
344 if let Some(inner) = inner.size {
345 let mut size_nested_fields = Vec::new();
346 if let Some(x) = inner.metric { size_nested_fields.push((2, tlv::TlvItemValueEnc::UInt8(x.to_u8())).into()); }
349 splash_nested_fields.push((2, tlv::TlvItemValueEnc::StructInvisible(size_nested_fields)).into());
350 }
351 branding_information_fields.push((4, tlv::TlvItemValueEnc::StructInvisible(splash_nested_fields)).into());
352 }
353 if let Some(inner) = branding_information.water_mark {
354 let mut water_mark_nested_fields = Vec::new();
355 if let Some(x) = inner.image_url { water_mark_nested_fields.push((0, tlv::TlvItemValueEnc::String(x.clone())).into()); }
356 if let Some(x) = inner.color { water_mark_nested_fields.push((1, tlv::TlvItemValueEnc::String(x.clone())).into()); }
357 if let Some(inner) = inner.size {
358 let mut size_nested_fields = Vec::new();
359 if let Some(x) = inner.metric { size_nested_fields.push((2, tlv::TlvItemValueEnc::UInt8(x.to_u8())).into()); }
362 water_mark_nested_fields.push((2, tlv::TlvItemValueEnc::StructInvisible(size_nested_fields)).into());
363 }
364 branding_information_fields.push((5, tlv::TlvItemValueEnc::StructInvisible(water_mark_nested_fields)).into());
365 }
366 tlv_fields.push((2, tlv::TlvItemValueEnc::StructInvisible(branding_information_fields)).into());
367 }
368 if let Some(playback_preferences) = playback_preferences {
369 let mut playback_preferences_fields = Vec::new();
371 if let Some(x) = playback_preferences.playback_position { playback_preferences_fields.push((0, tlv::TlvItemValueEnc::UInt64(x)).into()); }
372 if let Some(inner) = playback_preferences.text_track {
373 let mut text_track_nested_fields = Vec::new();
374 if let Some(x) = inner.language_code { text_track_nested_fields.push((0, tlv::TlvItemValueEnc::String(x.clone())).into()); }
375 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()); }
376 if let Some(x) = inner.audio_output_index { text_track_nested_fields.push((2, tlv::TlvItemValueEnc::UInt8(x)).into()); }
377 playback_preferences_fields.push((1, tlv::TlvItemValueEnc::StructInvisible(text_track_nested_fields)).into());
378 }
379 if let Some(listv) = playback_preferences.audio_tracks {
380 let inner_vec: Vec<_> = listv.into_iter().map(|inner| {
381 let mut nested_fields = Vec::new();
382 if let Some(x) = inner.language_code { nested_fields.push((0, tlv::TlvItemValueEnc::String(x.clone())).into()); }
383 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()); }
384 if let Some(x) = inner.audio_output_index { nested_fields.push((2, tlv::TlvItemValueEnc::UInt8(x)).into()); }
385 (0, tlv::TlvItemValueEnc::StructAnon(nested_fields)).into()
386 }).collect();
387 playback_preferences_fields.push((2, tlv::TlvItemValueEnc::Array(inner_vec)).into());
388 }
389 tlv_fields.push((3, tlv::TlvItemValueEnc::StructInvisible(playback_preferences_fields)).into());
390 }
391 let tlv = tlv::TlvItemEnc {
392 tag: 0,
393 value: tlv::TlvItemValueEnc::StructInvisible(tlv_fields),
394 };
395 Ok(tlv.encode()?)
396}
397
398pub fn decode_accept_header(inp: &tlv::TlvItemValue) -> anyhow::Result<Vec<String>> {
402 let mut res = Vec::new();
403 if let tlv::TlvItemValue::List(v) = inp {
404 for item in v {
405 if let tlv::TlvItemValue::String(s) = &item.value {
406 res.push(s.clone());
407 }
408 }
409 }
410 Ok(res)
411}
412
413pub fn decode_supported_streaming_protocols(inp: &tlv::TlvItemValue) -> anyhow::Result<SupportedProtocols> {
415 if let tlv::TlvItemValue::Int(v) = inp {
416 Ok(*v as u8)
417 } else {
418 Err(anyhow::anyhow!("Expected Integer"))
419 }
420}
421
422
423pub fn decode_attribute_json(cluster_id: u32, attribute_id: u32, tlv_value: &crate::tlv::TlvItemValue) -> String {
435 if cluster_id != 0x050A {
437 return format!("{{\"error\": \"Invalid cluster ID. Expected 0x050A, got {}\"}}", cluster_id);
438 }
439
440 match attribute_id {
441 0x0000 => {
442 match decode_accept_header(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 0x0001 => {
448 match decode_supported_streaming_protocols(tlv_value) {
449 Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
450 Err(e) => format!("{{\"error\": \"{}\"}}", e),
451 }
452 }
453 _ => format!("{{\"error\": \"Unknown attribute ID: {}\"}}", attribute_id),
454 }
455}
456
457pub fn get_attribute_list() -> Vec<(u32, &'static str)> {
462 vec![
463 (0x0000, "AcceptHeader"),
464 (0x0001, "SupportedStreamingProtocols"),
465 ]
466}
467
468pub fn get_command_list() -> Vec<(u32, &'static str)> {
471 vec![
472 (0x00, "LaunchContent"),
473 (0x01, "LaunchURL"),
474 ]
475}
476
477pub fn get_command_name(cmd_id: u32) -> Option<&'static str> {
478 match cmd_id {
479 0x00 => Some("LaunchContent"),
480 0x01 => Some("LaunchURL"),
481 _ => None,
482 }
483}
484
485pub fn get_command_schema(cmd_id: u32) -> Option<Vec<crate::clusters::codec::CommandField>> {
486 match cmd_id {
487 0x00 => Some(vec![
488 crate::clusters::codec::CommandField { tag: 0, name: "search", kind: crate::clusters::codec::FieldKind::Struct { name: "ContentSearchStruct" }, optional: false, nullable: false },
489 crate::clusters::codec::CommandField { tag: 1, name: "auto_play", kind: crate::clusters::codec::FieldKind::Bool, optional: false, nullable: false },
490 crate::clusters::codec::CommandField { tag: 2, name: "data", kind: crate::clusters::codec::FieldKind::String, optional: true, nullable: false },
491 crate::clusters::codec::CommandField { tag: 3, name: "playback_preferences", kind: crate::clusters::codec::FieldKind::Struct { name: "PlaybackPreferencesStruct" }, optional: true, nullable: false },
492 crate::clusters::codec::CommandField { tag: 4, name: "use_current_context", kind: crate::clusters::codec::FieldKind::Bool, optional: true, nullable: false },
493 ]),
494 0x01 => Some(vec![
495 crate::clusters::codec::CommandField { tag: 0, name: "content_url", kind: crate::clusters::codec::FieldKind::String, optional: false, nullable: false },
496 crate::clusters::codec::CommandField { tag: 1, name: "display_string", kind: crate::clusters::codec::FieldKind::String, optional: true, nullable: false },
497 crate::clusters::codec::CommandField { tag: 2, name: "branding_information", kind: crate::clusters::codec::FieldKind::Struct { name: "BrandingInformationStruct" }, optional: true, nullable: false },
498 crate::clusters::codec::CommandField { tag: 3, name: "playback_preferences", kind: crate::clusters::codec::FieldKind::Struct { name: "PlaybackPreferencesStruct" }, optional: true, nullable: false },
499 ]),
500 _ => None,
501 }
502}
503
504pub fn encode_command_json(cmd_id: u32, _args: &serde_json::Value) -> anyhow::Result<Vec<u8>> {
505 match cmd_id {
506 0x00 => Err(anyhow::anyhow!("command \"LaunchContent\" has complex args: use raw mode")),
507 0x01 => Err(anyhow::anyhow!("command \"LaunchURL\" has complex args: use raw mode")),
508 _ => Err(anyhow::anyhow!("unknown command ID: 0x{:02X}", cmd_id)),
509 }
510}
511
512#[derive(Debug, serde::Serialize)]
513pub struct LauncherResponse {
514 pub status: Option<Status>,
515 pub data: Option<String>,
516}
517
518pub fn decode_launcher_response(inp: &tlv::TlvItemValue) -> anyhow::Result<LauncherResponse> {
522 if let tlv::TlvItemValue::List(_fields) = inp {
523 let item = tlv::TlvItem { tag: 0, value: inp.clone() };
524 Ok(LauncherResponse {
525 status: item.get_int(&[0]).and_then(|v| Status::from_u8(v as u8)),
526 data: item.get_string_owned(&[1]),
527 })
528 } else {
529 Err(anyhow::anyhow!("Expected struct fields"))
530 }
531}
532
533pub async fn launch_content(conn: &crate::controller::Connection, endpoint: u16, search: ContentSearch, auto_play: bool, data: Option<String>, playback_preferences: Option<PlaybackPreferences>, use_current_context: Option<bool>) -> 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_LAUNCHCONTENT, &encode_launch_content(search, auto_play, data, playback_preferences, use_current_context)?).await?;
538 decode_launcher_response(&tlv)
539}
540
541pub async fn launch_url(conn: &crate::controller::Connection, endpoint: u16, content_url: String, display_string: Option<String>, branding_information: Option<BrandingInformation>, playback_preferences: Option<PlaybackPreferences>) -> anyhow::Result<LauncherResponse> {
543 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?;
544 decode_launcher_response(&tlv)
545}
546
547pub async fn read_accept_header(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<Vec<String>> {
549 let tlv = conn.read_request2(endpoint, crate::clusters::defs::CLUSTER_ID_CONTENT_LAUNCHER, crate::clusters::defs::CLUSTER_CONTENT_LAUNCHER_ATTR_ID_ACCEPTHEADER).await?;
550 decode_accept_header(&tlv)
551}
552
553pub async fn read_supported_streaming_protocols(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<SupportedProtocols> {
555 let tlv = conn.read_request2(endpoint, crate::clusters::defs::CLUSTER_ID_CONTENT_LAUNCHER, crate::clusters::defs::CLUSTER_CONTENT_LAUNCHER_ATTR_ID_SUPPORTEDSTREAMINGPROTOCOLS).await?;
556 decode_supported_streaming_protocols(&tlv)
557}
558