Skip to main content

openipc_core/
radiotap.rs

1/// 802.11 data frame type used for ordinary injected payloads.
2pub const FRAME_TYPE_DATA: u8 = 0x08;
3/// 802.11 RTS frame type used by OpenIPC adaptive-link uplink packets.
4pub const FRAME_TYPE_RTS: u8 = 0xb4;
5
6const IEEE80211_RADIOTAP_MCS_HAVE_BW: u8 = 0x01;
7const IEEE80211_RADIOTAP_MCS_HAVE_MCS: u8 = 0x02;
8const IEEE80211_RADIOTAP_MCS_HAVE_GI: u8 = 0x04;
9const IEEE80211_RADIOTAP_MCS_HAVE_FEC: u8 = 0x10;
10const IEEE80211_RADIOTAP_MCS_HAVE_STBC: u8 = 0x20;
11const IEEE80211_RADIOTAP_MCS_SGI: u8 = 0x04;
12const IEEE80211_RADIOTAP_MCS_FEC_LDPC: u8 = 0x10;
13const IEEE80211_RADIOTAP_MCS_STBC_SHIFT: u8 = 5;
14
15const IEEE80211_RADIOTAP_VHT_FLAG_STBC: u8 = 0x01;
16const IEEE80211_RADIOTAP_VHT_FLAG_SGI: u8 = 0x04;
17const IEEE80211_RADIOTAP_VHT_CODING_LDPC_USER0: u8 = 0x01;
18
19const RADIOTAP_PRESENT_RATE: u32 = 1 << 2;
20const RADIOTAP_PRESENT_TX_FLAGS: u32 = 1 << 15;
21const RADIOTAP_PRESENT_MCS: u32 = 1 << 19;
22const RADIOTAP_PRESENT_VHT: u32 = 1 << 21;
23const RADIOTAP_TX_FLAGS_NO_ACK: u16 = 0x0008;
24
25/// Length of the legacy-rate radiotap header built by this crate.
26pub const RADIOTAP_LEGACY_LEN: usize = 13;
27/// Length of the HT/MCS radiotap header built by this crate.
28pub const RADIOTAP_HT_LEN: usize = 13;
29/// Length of the VHT radiotap header built by this crate.
30pub const RADIOTAP_VHT_LEN: usize = 22;
31
32/// Channel bandwidth requested for an injected frame.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum ChannelBandwidth {
35    /// 20 MHz channel.
36    Mhz20,
37    /// 40 MHz channel.
38    Mhz40,
39    /// 80 MHz channel.
40    Mhz80,
41    /// 160 MHz channel.
42    Mhz160,
43}
44
45impl ChannelBandwidth {
46    /// Convert this bandwidth to the compact bit pattern used in Realtek TX descriptors.
47    pub const fn realtek_desc_bits(self) -> u8 {
48        match self {
49            Self::Mhz20 => 0,
50            Self::Mhz40 => 1,
51            Self::Mhz80 | Self::Mhz160 => 2,
52        }
53    }
54
55    const fn ht_mcs_bits(self) -> u8 {
56        match self {
57            Self::Mhz20 => 0,
58            Self::Mhz40 | Self::Mhz80 | Self::Mhz160 => 1,
59        }
60    }
61
62    const fn vht_bits(self) -> u8 {
63        match self {
64            Self::Mhz20 => 0x00,
65            Self::Mhz40 => 0x01,
66            Self::Mhz80 => 0x04,
67            Self::Mhz160 => 0x0b,
68        }
69    }
70}
71
72/// Physical-layer rate family for a transmit packet.
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum TxModeKind {
75    /// Legacy 802.11a/b/g rate.
76    Legacy,
77    /// 802.11n HT MCS rate.
78    Ht,
79    /// 802.11ac VHT MCS rate.
80    Vht,
81}
82
83/// Parsed or requested TX mode for radiotap and Realtek descriptor generation.
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub struct TxMode {
86    /// Rate family.
87    pub kind: TxModeKind,
88    /// Legacy rate in 500 kbps units, matching the radiotap rate field.
89    pub legacy_rate_500kbps: u8,
90    /// HT MCS index for HT frames.
91    pub ht_mcs: u8,
92    /// VHT MCS index for VHT frames.
93    pub vht_mcs: u8,
94    /// VHT spatial stream count.
95    pub vht_nss: u8,
96    /// Requested channel bandwidth.
97    pub bandwidth: ChannelBandwidth,
98    /// Use short guard interval when supported.
99    pub short_gi: bool,
100    /// Use LDPC coding when supported.
101    pub ldpc: bool,
102    /// Use STBC when supported.
103    pub stbc: bool,
104}
105
106impl TxMode {
107    /// Build a legacy TX mode from a radiotap 500 kbps rate value.
108    pub const fn legacy(rate_500kbps: u8) -> Self {
109        Self {
110            kind: TxModeKind::Legacy,
111            legacy_rate_500kbps: rate_500kbps,
112            ht_mcs: 0,
113            vht_mcs: 0,
114            vht_nss: 1,
115            bandwidth: ChannelBandwidth::Mhz20,
116            short_gi: false,
117            ldpc: false,
118            stbc: false,
119        }
120    }
121
122    /// Build the default OpenIPC legacy 6 Mbps TX mode.
123    pub const fn legacy_6m() -> Self {
124        Self::legacy(12)
125    }
126
127    /// Build a legacy 1 Mbps TX mode.
128    pub const fn legacy_1m() -> Self {
129        Self::legacy(2)
130    }
131
132    /// Build an HT TX mode with the supplied MCS index.
133    pub const fn ht(mcs: u8) -> Self {
134        Self {
135            kind: TxModeKind::Ht,
136            legacy_rate_500kbps: 12,
137            ht_mcs: mcs,
138            vht_mcs: 0,
139            vht_nss: 1,
140            bandwidth: ChannelBandwidth::Mhz20,
141            short_gi: false,
142            ldpc: false,
143            stbc: false,
144        }
145    }
146
147    /// Build a VHT TX mode with the supplied stream count and MCS index.
148    pub const fn vht(nss: u8, mcs: u8) -> Self {
149        Self {
150            kind: TxModeKind::Vht,
151            legacy_rate_500kbps: 12,
152            ht_mcs: 0,
153            vht_mcs: mcs,
154            vht_nss: nss,
155            bandwidth: ChannelBandwidth::Mhz20,
156            short_gi: false,
157            ldpc: false,
158            stbc: false,
159        }
160    }
161}
162
163impl Default for TxMode {
164    fn default() -> Self {
165        Self::legacy_6m()
166    }
167}
168
169/// Compact radio parameters used when building OpenIPC uplink radiotap headers.
170#[derive(Debug, Clone, Copy, PartialEq, Eq)]
171pub struct TxRadioParams {
172    /// MCS index for HT/VHT rates.
173    pub mcs_index: u8,
174    /// Number of spatial streams for VHT rates.
175    pub nss: u8,
176    /// Requested channel bandwidth.
177    pub bandwidth: ChannelBandwidth,
178    /// Use short guard interval when supported.
179    pub short_gi: bool,
180    /// STBC stream count encoded into radiotap.
181    pub stbc: u8,
182    /// Use LDPC coding when supported.
183    pub ldpc: bool,
184    /// Emit a VHT header instead of an HT header.
185    pub vht: bool,
186    /// 802.11 frame type to use when wrapping WFB packets.
187    pub frame_type: u8,
188}
189
190impl TxRadioParams {
191    /// Return the default OpenIPC adaptive-link uplink radio parameters.
192    pub const fn openipc_uplink_default() -> Self {
193        Self {
194            mcs_index: 0,
195            nss: 1,
196            bandwidth: ChannelBandwidth::Mhz20,
197            short_gi: false,
198            stbc: 1,
199            ldpc: true,
200            vht: false,
201            frame_type: FRAME_TYPE_RTS,
202        }
203    }
204}
205
206impl Default for TxRadioParams {
207    fn default() -> Self {
208        Self::openipc_uplink_default()
209    }
210}
211
212/// Decoded radiotap TX information in the older OpenIPC adaptive-link shape.
213#[derive(Debug, Clone, Copy, PartialEq, Eq)]
214pub struct RadiotapTxInfo {
215    /// Whether this packet used VHT rather than HT/legacy radiotap fields.
216    pub vht: bool,
217    /// MCS index when present.
218    pub mcs_index: u8,
219    /// Number of spatial streams.
220    pub nss: u8,
221    /// Channel bandwidth.
222    pub bandwidth: ChannelBandwidth,
223    /// Whether short guard interval was requested.
224    pub short_gi: bool,
225    /// STBC stream count.
226    pub stbc: u8,
227    /// Whether LDPC coding was requested.
228    pub ldpc: bool,
229}
230
231/// Error returned while parsing a radiotap TX header.
232#[derive(Debug, Clone, PartialEq, Eq)]
233pub enum RadiotapError {
234    /// Packet is too short to contain the radiotap fixed header.
235    TooShort,
236    /// Radiotap length field is zero, out of range, or inconsistent.
237    InvalidLength,
238    /// The header uses fields this minimal parser does not support.
239    UnsupportedHeader,
240}
241
242impl std::fmt::Display for RadiotapError {
243    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
244        match self {
245            Self::TooShort => write!(f, "radiotap packet is too short"),
246            Self::InvalidLength => write!(f, "radiotap length is invalid"),
247            Self::UnsupportedHeader => write!(f, "unsupported radiotap TX header"),
248        }
249    }
250}
251
252impl std::error::Error for RadiotapError {}
253
254/// Build an HT or VHT radiotap header from compact OpenIPC TX parameters.
255pub fn build_radiotap_header(params: TxRadioParams) -> Vec<u8> {
256    if params.vht {
257        build_vht_radiotap_header(params)
258    } else {
259        build_ht_radiotap_header(params)
260    }
261}
262
263/// Build a radiotap header for a parsed or requested stream TX mode.
264pub fn build_stream_radiotap(mode: TxMode) -> Vec<u8> {
265    match mode.kind {
266        TxModeKind::Legacy => build_legacy_radiotap_header(mode),
267        TxModeKind::Ht => build_ht_radiotap_header(TxRadioParams {
268            mcs_index: mode.ht_mcs,
269            bandwidth: mode.bandwidth,
270            short_gi: mode.short_gi,
271            stbc: u8::from(mode.stbc),
272            ldpc: mode.ldpc,
273            vht: false,
274            ..TxRadioParams::default()
275        }),
276        TxModeKind::Vht => build_vht_radiotap_header(TxRadioParams {
277            mcs_index: mode.vht_mcs,
278            nss: mode.vht_nss,
279            bandwidth: mode.bandwidth,
280            short_gi: mode.short_gi,
281            stbc: u8::from(mode.stbc),
282            ldpc: mode.ldpc,
283            vht: true,
284            ..TxRadioParams::default()
285        }),
286    }
287}
288
289/// Build a legacy-rate radiotap header.
290pub fn build_legacy_radiotap_header(mode: TxMode) -> Vec<u8> {
291    vec![
292        0x00,
293        0x00,
294        RADIOTAP_LEGACY_LEN as u8,
295        0x00,
296        (RADIOTAP_PRESENT_RATE | RADIOTAP_PRESENT_TX_FLAGS) as u8,
297        ((RADIOTAP_PRESENT_RATE | RADIOTAP_PRESENT_TX_FLAGS) >> 8) as u8,
298        0x00,
299        0x00,
300        mode.legacy_rate_500kbps,
301        0x00,
302        (RADIOTAP_TX_FLAGS_NO_ACK & 0xff) as u8,
303        (RADIOTAP_TX_FLAGS_NO_ACK >> 8) as u8,
304        0x00,
305    ]
306}
307
308/// Build an HT/MCS radiotap header.
309pub fn build_ht_radiotap_header(params: TxRadioParams) -> Vec<u8> {
310    let known = IEEE80211_RADIOTAP_MCS_HAVE_MCS
311        | IEEE80211_RADIOTAP_MCS_HAVE_BW
312        | IEEE80211_RADIOTAP_MCS_HAVE_GI
313        | IEEE80211_RADIOTAP_MCS_HAVE_STBC
314        | IEEE80211_RADIOTAP_MCS_HAVE_FEC;
315    let mut flags = params.bandwidth.ht_mcs_bits();
316    if params.short_gi {
317        flags |= IEEE80211_RADIOTAP_MCS_SGI;
318    }
319    if params.ldpc {
320        flags |= IEEE80211_RADIOTAP_MCS_FEC_LDPC;
321    }
322    flags |= (params.stbc & 0x03) << IEEE80211_RADIOTAP_MCS_STBC_SHIFT;
323
324    vec![
325        0x00,
326        0x00,
327        RADIOTAP_HT_LEN as u8,
328        0x00,
329        0x00,
330        0x80,
331        0x08,
332        0x00,
333        0x08,
334        0x00,
335        known,
336        flags,
337        params.mcs_index.min(31),
338    ]
339}
340
341/// Build a VHT radiotap header.
342pub fn build_vht_radiotap_header(params: TxRadioParams) -> Vec<u8> {
343    let mut flags = 0u8;
344    if params.stbc != 0 {
345        flags |= IEEE80211_RADIOTAP_VHT_FLAG_STBC;
346    }
347    if params.short_gi {
348        flags |= IEEE80211_RADIOTAP_VHT_FLAG_SGI;
349    }
350    let nss = params.nss.clamp(1, 4);
351    let mcs = params.mcs_index.min(9);
352    let mcs_nss0 = (mcs << 4) | nss;
353    let coding = if params.ldpc {
354        IEEE80211_RADIOTAP_VHT_CODING_LDPC_USER0
355    } else {
356        0
357    };
358
359    vec![
360        0x00,
361        0x00,
362        RADIOTAP_VHT_LEN as u8,
363        0x00,
364        0x00,
365        0x80,
366        0x20,
367        0x00,
368        0x08,
369        0x00,
370        0x45,
371        0x00,
372        flags,
373        params.bandwidth.vht_bits(),
374        mcs_nss0,
375        0x00,
376        0x00,
377        0x00,
378        coding,
379        0x00,
380        0x00,
381        0x00,
382    ]
383}
384
385/// Return the radiotap header length from a radiotap packet.
386pub fn radiotap_len(packet: &[u8]) -> Result<usize, RadiotapError> {
387    if packet.len() < 4 {
388        return Err(RadiotapError::TooShort);
389    }
390    let len = u16::from_le_bytes([packet[2], packet[3]]) as usize;
391    if len == 0 || len >= packet.len() {
392        return Err(RadiotapError::InvalidLength);
393    }
394    Ok(len)
395}
396
397/// Parse a human-readable TX mode such as `6M`, `MCS0/SGI`, or `VHT1SS_MCS3/80`.
398pub fn parse_tx_mode_str(spec: &str) -> TxMode {
399    let trimmed = spec
400        .chars()
401        .filter(|ch| !ch.is_whitespace())
402        .flat_map(char::to_uppercase)
403        .collect::<String>();
404    if trimmed.is_empty() {
405        return TxMode::default();
406    }
407
408    let mut tokens = trimmed.split('/');
409    let Some(rate_token) = tokens.next() else {
410        return TxMode::default();
411    };
412    let Some(mut mode) = parse_tx_rate_token(rate_token) else {
413        return TxMode::default();
414    };
415
416    for token in tokens {
417        match token {
418            "SGI" => mode.short_gi = true,
419            "LDPC" => mode.ldpc = true,
420            "STBC" => mode.stbc = true,
421            "20" => mode.bandwidth = ChannelBandwidth::Mhz20,
422            "40" => mode.bandwidth = ChannelBandwidth::Mhz40,
423            "80" => mode.bandwidth = ChannelBandwidth::Mhz80,
424            "160" => mode.bandwidth = ChannelBandwidth::Mhz160,
425            _ => {}
426        }
427    }
428    mode
429}
430
431/// Parse supported radiotap TX fields into a `TxMode`.
432pub fn parse_radiotap_tx_mode(packet: &[u8]) -> Result<Option<TxMode>, RadiotapError> {
433    let len = radiotap_len(packet)?;
434    if len < 8 || packet.len() < len {
435        return Err(RadiotapError::InvalidLength);
436    }
437    let present = u32::from_le_bytes(packet[4..8].try_into().expect("present word is in range"));
438    if present & (1 << 31) != 0 {
439        return Err(RadiotapError::UnsupportedHeader);
440    }
441
442    let mut offset = 8usize;
443    let mut mode = None;
444
445    if present & RADIOTAP_PRESENT_RATE != 0 {
446        require_field(packet, len, offset, 1)?;
447        mode = Some(TxMode::legacy(packet[offset]));
448        offset += 1;
449    }
450
451    if present & RADIOTAP_PRESENT_TX_FLAGS != 0 {
452        offset = align(offset, 2);
453        require_field(packet, len, offset, 2)?;
454        offset += 2;
455    }
456
457    if present & RADIOTAP_PRESENT_MCS != 0 {
458        require_field(packet, len, offset, 3)?;
459        let known = packet[offset];
460        let flags = packet[offset + 1];
461        let mcs = packet[offset + 2];
462        let mut ht = TxMode::ht(if known & IEEE80211_RADIOTAP_MCS_HAVE_MCS != 0 {
463            mcs.min(31)
464        } else {
465            0
466        });
467        ht.bandwidth = if known & IEEE80211_RADIOTAP_MCS_HAVE_BW != 0 && flags & 0x03 == 1 {
468            ChannelBandwidth::Mhz40
469        } else {
470            ChannelBandwidth::Mhz20
471        };
472        ht.short_gi =
473            known & IEEE80211_RADIOTAP_MCS_HAVE_GI != 0 && flags & IEEE80211_RADIOTAP_MCS_SGI != 0;
474        ht.ldpc = known & IEEE80211_RADIOTAP_MCS_HAVE_FEC != 0
475            && flags & IEEE80211_RADIOTAP_MCS_FEC_LDPC != 0;
476        ht.stbc = known & IEEE80211_RADIOTAP_MCS_HAVE_STBC != 0
477            && ((flags >> IEEE80211_RADIOTAP_MCS_STBC_SHIFT) & 0x03) != 0;
478        mode = Some(ht);
479        offset += 3;
480    }
481
482    if present & RADIOTAP_PRESENT_VHT != 0 {
483        offset = align(offset, 2);
484        require_field(packet, len, offset, 12)?;
485        let known = u16::from_le_bytes([packet[offset], packet[offset + 1]]);
486        let flags = packet[offset + 2];
487        let bandwidth = match packet[offset + 3] & 0x1f {
488            1..=3 => ChannelBandwidth::Mhz40,
489            4..=10 => ChannelBandwidth::Mhz80,
490            11..=31 => ChannelBandwidth::Mhz160,
491            _ => ChannelBandwidth::Mhz20,
492        };
493        let mcs_nss = packet[offset + 4];
494        let mut vht = TxMode::vht((mcs_nss & 0x0f).clamp(1, 4), ((mcs_nss >> 4) & 0x0f).min(9));
495        if known & (1 << 6) != 0 {
496            vht.bandwidth = bandwidth;
497        }
498        vht.short_gi = known & (1 << 2) != 0 && flags & IEEE80211_RADIOTAP_VHT_FLAG_SGI != 0;
499        vht.stbc = known & 1 != 0 && flags & IEEE80211_RADIOTAP_VHT_FLAG_STBC != 0;
500        vht.ldpc = packet[offset + 8] & IEEE80211_RADIOTAP_VHT_CODING_LDPC_USER0 != 0;
501        mode = Some(vht);
502    }
503
504    Ok(mode)
505}
506
507/// Parse supported radiotap TX fields into compact adaptive-link TX info.
508pub fn parse_radiotap_tx_info(packet: &[u8]) -> Result<RadiotapTxInfo, RadiotapError> {
509    match parse_radiotap_tx_mode(packet)? {
510        Some(mode) => Ok(RadiotapTxInfo {
511            vht: mode.kind == TxModeKind::Vht,
512            mcs_index: match mode.kind {
513                TxModeKind::Legacy | TxModeKind::Ht => mode.ht_mcs,
514                TxModeKind::Vht => mode.vht_mcs,
515            },
516            nss: mode.vht_nss,
517            bandwidth: mode.bandwidth,
518            short_gi: mode.short_gi,
519            stbc: u8::from(mode.stbc),
520            ldpc: mode.ldpc,
521        }),
522        None => Err(RadiotapError::UnsupportedHeader),
523    }
524}
525
526fn parse_tx_rate_token(token: &str) -> Option<TxMode> {
527    match token {
528        "1M" => Some(TxMode::legacy(2)),
529        "2M" => Some(TxMode::legacy(4)),
530        "5.5M" | "5_5M" | "5M" => Some(TxMode::legacy(11)),
531        "6M" => Some(TxMode::legacy(12)),
532        "9M" => Some(TxMode::legacy(18)),
533        "11M" => Some(TxMode::legacy(22)),
534        "12M" => Some(TxMode::legacy(24)),
535        "18M" => Some(TxMode::legacy(36)),
536        "24M" => Some(TxMode::legacy(48)),
537        "36M" => Some(TxMode::legacy(72)),
538        "48M" => Some(TxMode::legacy(96)),
539        "54M" => Some(TxMode::legacy(108)),
540        _ => {
541            if let Some(raw) = token.strip_prefix("MCS") {
542                return raw
543                    .parse::<u8>()
544                    .ok()
545                    .filter(|mcs| *mcs <= 31)
546                    .map(TxMode::ht);
547            }
548            if let Some(raw) = token.strip_prefix("VHT") {
549                let (nss_raw, mcs_raw) = raw.split_once("SS_MCS")?;
550                let nss = nss_raw.parse::<u8>().ok()?;
551                let mcs = mcs_raw.parse::<u8>().ok()?;
552                if (1..=4).contains(&nss) && mcs <= 9 {
553                    return Some(TxMode::vht(nss, mcs));
554                }
555            }
556            None
557        }
558    }
559}
560
561const fn align(offset: usize, alignment: usize) -> usize {
562    (offset + alignment - 1) & !(alignment - 1)
563}
564
565fn require_field(
566    packet: &[u8],
567    radiotap_len: usize,
568    offset: usize,
569    len: usize,
570) -> Result<(), RadiotapError> {
571    if offset + len <= radiotap_len && offset + len <= packet.len() {
572        Ok(())
573    } else {
574        Err(RadiotapError::InvalidLength)
575    }
576}
577
578#[cfg(test)]
579mod tests {
580    use super::*;
581
582    #[test]
583    fn ht_header_roundtrips_tx_info() {
584        let params = TxRadioParams {
585            mcs_index: 3,
586            bandwidth: ChannelBandwidth::Mhz40,
587            short_gi: true,
588            stbc: 1,
589            ldpc: true,
590            ..TxRadioParams::default()
591        };
592        let mut packet = build_radiotap_header(params);
593        packet.extend_from_slice(&[0u8; 24]);
594        let parsed = parse_radiotap_tx_info(&packet).unwrap();
595        assert_eq!(parsed.mcs_index, 3);
596        assert_eq!(parsed.bandwidth, ChannelBandwidth::Mhz40);
597        assert!(parsed.short_gi);
598        assert!(parsed.ldpc);
599        assert_eq!(parsed.stbc, 1);
600    }
601
602    #[test]
603    fn parses_devourer_tx_mode_strings() {
604        let mode = parse_tx_mode_str("vht2ss_mcs5/80/sgi/ldpc/stbc");
605        assert_eq!(mode.kind, TxModeKind::Vht);
606        assert_eq!(mode.vht_nss, 2);
607        assert_eq!(mode.vht_mcs, 5);
608        assert_eq!(mode.bandwidth, ChannelBandwidth::Mhz80);
609        assert!(mode.short_gi);
610        assert!(mode.ldpc);
611        assert!(mode.stbc);
612    }
613
614    #[test]
615    fn parses_devourer_cck_rate_names() {
616        for (spec, rate) in [
617            ("1M", 2),
618            ("2M", 4),
619            ("5.5M", 11),
620            ("5_5M", 11),
621            ("11M", 22),
622        ] {
623            let mode = parse_tx_mode_str(spec);
624            assert_eq!(mode.kind, TxModeKind::Legacy);
625            assert_eq!(mode.legacy_rate_500kbps, rate);
626        }
627    }
628
629    #[test]
630    fn legacy_stream_radiotap_carries_rate_and_tx_flags() {
631        let mode = TxMode::legacy(12);
632        let mut packet = build_stream_radiotap(mode);
633        packet.extend_from_slice(&[0u8; 24]);
634        let parsed = parse_radiotap_tx_mode(&packet).unwrap().unwrap();
635        assert_eq!(parsed.kind, TxModeKind::Legacy);
636        assert_eq!(parsed.legacy_rate_500kbps, 12);
637    }
638}