implement

This commit is contained in:
CyberRex
2026-07-31 15:48:30 +09:00
parent 38670efd46
commit f4c2369057
10 changed files with 2711 additions and 327 deletions

View File

@@ -5,12 +5,13 @@
use std::collections::{BTreeMap, HashMap, HashSet};
use encoding_rs::EUC_JP;
use mirakurun_types::{
Program, ProgramGenre, ProgramVideo, ProgramVideoResolution, ProgramVideoType, program_id,
Program, ProgramAudio, ProgramGenre, ProgramRelatedItem, ProgramRelatedItemType, ProgramSeries,
ProgramVideo, ProgramVideoResolution, ProgramVideoType, program_id,
};
use crate::{
arib::decode as decode_arib,
filter::SectionAssembler,
ts::{Packet, PacketFramer, crc32_mpeg2},
};
@@ -22,6 +23,7 @@ pub struct EitCollector {
framer: PacketFramer,
assembler: SectionAssembler,
programs: HashMap<u64, Program>,
states: HashMap<u64, ProgramState>,
sections_seen: HashSet<(u8, u16, u8, u8)>,
packet_count: u64,
section_count: u64,
@@ -40,7 +42,7 @@ impl EitCollector {
continue;
}
for section in self.assembler.push(packet) {
if parse_eit_section(&section, &mut self.programs)
if parse_eit_section(&section, &mut self.programs, &mut self.states)
&& self.sections_seen.insert((
section[0],
u16::from_be_bytes([section[3], section[4]]),
@@ -72,7 +74,61 @@ impl EitCollector {
}
}
fn parse_eit_section(section: &[u8], programs: &mut HashMap<u64, Program>) -> bool {
#[derive(Debug, Default)]
struct VersionRecord(HashMap<u8, u8>);
impl VersionRecord {
fn should_update(&self, table_id: u8, version: u8) -> bool {
if self.0.contains_key(&0x4e) && table_id != 0x4e {
return false;
}
if self.0.contains_key(&0x4f) && !matches!(table_id, 0x4e | 0x4f) {
return false;
}
self.0.get(&table_id).copied() != Some(version)
}
fn mark(&mut self, table_id: u8, version: u8) {
self.0.insert(table_id, version);
}
}
#[derive(Debug, Default)]
struct ProgramState {
initialized: bool,
present: bool,
following: bool,
event: VersionRecord,
short: VersionRecord,
extended: ExtendedState,
component: VersionRecord,
content: VersionRecord,
audio: HashMap<u8, VersionRecord>,
audios: BTreeMap<u8, ProgramAudio>,
series: VersionRecord,
group: HashMap<u8, VersionRecord>,
groups: BTreeMap<u8, Vec<ProgramRelatedItem>>,
}
#[derive(Debug, Default)]
struct ExtendedState {
versions: VersionRecord,
parts: BTreeMap<u8, Vec<ExtendedItem>>,
last_descriptor: Option<u8>,
done: bool,
}
#[derive(Debug)]
struct ExtendedItem {
description: Vec<u8>,
value: Vec<u8>,
}
fn parse_eit_section(
section: &[u8],
programs: &mut HashMap<u64, Program>,
states: &mut HashMap<u64, ProgramState>,
) -> bool {
if section.len() < 18
|| !matches!(section[0], 0x4e..=0x6f)
|| section[5] & 0x01 == 0
@@ -80,10 +136,14 @@ fn parse_eit_section(section: &[u8], programs: &mut HashMap<u64, Program>) -> bo
{
return false;
}
let table_id = section[0];
let version = (section[5] >> 1) & 0x1f;
let is_present_following = matches!(section[0], 0x4e | 0x4f);
if is_present_following && section[6] > 1 {
return false;
}
let is_present = is_present_following && section[6] == 0;
let is_following = is_present_following && section[6] == 1;
let service_id = u16::from_be_bytes([section[3], section[4]]);
let network_id = u16::from_be_bytes([section[10], section[11]]);
let Some(end) = section.len().checked_sub(4) else {
@@ -105,36 +165,64 @@ fn parse_eit_section(section: &[u8], programs: &mut HashMap<u64, Program>) -> bo
if descriptor_end > end {
return false;
}
if let Some(start_at) = decode_mjd_time(start) {
let id = program_id(network_id, service_id, event_id);
let program = programs.entry(id).or_insert_with(|| Program {
id,
event_id,
service_id,
network_id,
start_at,
duration: decode_bcd_duration(duration).unwrap_or(1),
is_free: section[offset + 10] & 0x10 == 0,
name: None,
description: None,
genres: None,
video: None,
audios: None,
series: None,
extended: None,
related_items: None,
});
program.start_at = start_at;
program.duration = decode_bcd_duration(duration).unwrap_or(1);
program.is_free = section[offset + 10] & 0x10 == 0;
parse_descriptors(&section[offset + 12..descriptor_end], program);
let id = program_id(network_id, service_id, event_id);
let start_at = decode_mjd_time(start);
if !programs.contains_key(&id) && start_at.is_none() {
offset = descriptor_end;
continue;
}
let program = programs.entry(id).or_insert_with(|| Program {
id,
event_id,
service_id,
network_id,
start_at: start_at.unwrap_or_default(),
duration: decode_bcd_duration(duration).unwrap_or(1),
is_free: section[offset + 10] & 0x10 == 0,
name: None,
description: None,
genres: None,
video: None,
audios: None,
series: None,
extended: None,
related_items: None,
});
let state = states.entry(id).or_default();
let update_event = !state.initialized
|| (!state.present && is_present)
|| (!state.following && is_following)
|| state.event.should_update(table_id, version);
if update_event {
if let Some(start_at) = start_at {
program.start_at = start_at;
program.duration = decode_bcd_duration(duration).unwrap_or(1);
program.is_free = section[offset + 10] & 0x10 == 0;
}
state.initialized = true;
state.present = is_present;
state.following = is_following;
state.event.mark(table_id, version);
}
parse_descriptors(
&section[offset + 12..descriptor_end],
program,
state,
table_id,
version,
);
offset = descriptor_end;
}
true
}
fn parse_descriptors(mut descriptors: &[u8], program: &mut Program) {
fn parse_descriptors(
mut descriptors: &[u8],
program: &mut Program,
state: &mut ProgramState,
table_id: u8,
version: u8,
) {
while descriptors.len() >= 2 {
let length = usize::from(descriptors[1]);
let Some(end) = 2usize.checked_add(length) else {
@@ -145,43 +233,81 @@ fn parse_descriptors(mut descriptors: &[u8], program: &mut Program) {
}
let body = &descriptors[2..end];
match descriptors[0] {
0x4d => parse_short_event(body, program),
0x4e => parse_extended_event(body, program),
0x50 => parse_video_component(body, program),
0x54 => parse_content(body, program),
0x4d if state.short.should_update(table_id, version) => {
if let Some((name, description)) = parse_short_event(body) {
state.short.mark(table_id, version);
program.name = Some(name);
program.description = Some(description);
}
}
0x4e => parse_extended_event(body, program, &mut state.extended, table_id, version),
0x50 if state.component.should_update(table_id, version) => {
if let Some(video) = parse_video_component(body) {
state.component.mark(table_id, version);
program.video = Some(video);
}
}
0x54 if state.content.should_update(table_id, version) => {
if let Some(genres) = parse_content(body) {
state.content.mark(table_id, version);
program.genres = Some(genres);
}
}
0xc4 => parse_audio_component(body, program, state, table_id, version),
0xd5 if state.series.should_update(table_id, version) => {
if let Some(series) = parse_series(body) {
state.series.mark(table_id, version);
program.series = Some(series);
}
}
0xd6 => parse_event_group(body, program, state, table_id, version),
_ => {}
}
descriptors = &descriptors[end..];
}
}
fn parse_short_event(body: &[u8], program: &mut Program) {
fn parse_short_event(body: &[u8]) -> Option<(String, String)> {
if body.len() < 5 {
return;
return None;
}
let name_length = usize::from(body[3]);
let Some(text_length_offset) = 4usize.checked_add(name_length) else {
return;
};
let text_length_offset = 4usize.checked_add(name_length)?;
if text_length_offset >= body.len() {
return;
return None;
}
let text_length = usize::from(body[text_length_offset]);
let text_start = text_length_offset + 1;
let Some(text_end) = text_start.checked_add(text_length) else {
return;
};
let text_end = text_start.checked_add(text_length)?;
if text_end > body.len() {
return;
return None;
}
program.name = Some(decode_arib(&body[4..text_length_offset]));
program.description = Some(decode_arib(&body[text_start..text_end]));
Some((
decode_arib(&body[4..text_length_offset]),
decode_arib(&body[text_start..text_end]),
))
}
fn parse_extended_event(body: &[u8], program: &mut Program) {
fn parse_extended_event(
body: &[u8],
program: &mut Program,
state: &mut ExtendedState,
table_id: u8,
version: u8,
) {
if body.len() < 6 {
return;
}
let descriptor_number = body[0] >> 4;
let last_descriptor = body[0] & 0x0f;
if state.versions.should_update(table_id, version) {
state.versions.mark(table_id, version);
state.parts.clear();
state.last_descriptor = Some(last_descriptor);
state.done = false;
} else if state.done {
return;
}
let items_length = usize::from(body[4]);
let Some(items_end) = 5usize.checked_add(items_length) else {
return;
@@ -189,44 +315,78 @@ fn parse_extended_event(body: &[u8], program: &mut Program) {
if items_end > body.len() {
return;
}
let extended = program.extended.get_or_insert_with(BTreeMap::new);
let mut items = &body[5..items_end];
let mut previous_key = String::new();
let Some(items) = parse_extended_items(&body[5..items_end]) else {
return;
};
state.parts.entry(descriptor_number).or_insert(items);
let Some(last) = state.last_descriptor else {
return;
};
if !(0..=last).all(|number| state.parts.contains_key(&number)) {
return;
}
program.extended = Some(build_extended(&state.parts));
state.done = true;
}
fn parse_extended_items(mut items: &[u8]) -> Option<Vec<ExtendedItem>> {
let mut parsed = Vec::new();
while !items.is_empty() {
let description_length = usize::from(items[0]);
let Some(description_end) = 1usize.checked_add(description_length) else {
return;
};
let description_end = 1usize.checked_add(description_length)?;
if description_end >= items.len() {
return;
return None;
}
let item_length = usize::from(items[description_end]);
let item_start = description_end + 1;
let Some(item_end) = item_start.checked_add(item_length) else {
return;
};
let item_end = item_start.checked_add(item_length)?;
if item_end > items.len() {
return;
return None;
}
let decoded_key = decode_arib(&items[1..description_end]);
let key = if decoded_key.is_empty() {
previous_key.clone()
} else {
previous_key.clone_from(&decoded_key);
decoded_key
};
let value = decode_arib(&items[item_start..item_end]);
extended
.entry(key)
.and_modify(|current| current.push_str(&value))
.or_insert(value);
parsed.push(ExtendedItem {
description: items[1..description_end].to_vec(),
value: items[item_start..item_end].to_vec(),
});
items = &items[item_end..];
}
Some(parsed)
}
fn parse_video_component(body: &[u8], program: &mut Program) {
fn build_extended(parts: &BTreeMap<u8, Vec<ExtendedItem>>) -> BTreeMap<String, String> {
let mut buffers: BTreeMap<String, Vec<Vec<u8>>> = BTreeMap::new();
let mut current_key = String::new();
for items in parts.values() {
for item in items {
let decoded_key = decode_arib(&item.description);
if !decoded_key.is_empty() {
current_key = decoded_key;
}
let values = buffers.entry(current_key.clone()).or_default();
if item.description.is_empty() && !values.is_empty() {
if let Some(previous) = values.last_mut() {
previous.extend_from_slice(&item.value);
}
} else {
values.push(item.value.clone());
}
}
}
buffers
.into_iter()
.map(|(key, values)| {
let value = values
.iter()
.map(|value| decode_arib(value))
.collect::<Vec<_>>()
.join("\n\n");
(key, value)
})
.collect()
}
fn parse_video_component(body: &[u8]) -> Option<ProgramVideo> {
if body.len() < 2 {
return;
return None;
}
let stream_content = body[0] & 0x0f;
let component_type = body[1];
@@ -234,7 +394,7 @@ fn parse_video_component(body: &[u8], program: &mut Program) {
1 => ProgramVideoType::Mpeg2,
5 => ProgramVideoType::H264,
9 => ProgramVideoType::H265,
_ => return,
_ => return None,
};
let resolution = match component_type {
0x01..=0x04 => ProgramVideoResolution::I480,
@@ -245,17 +405,17 @@ fn parse_video_component(body: &[u8], program: &mut Program) {
0xc1..=0xc4 => ProgramVideoResolution::P720,
0xd1..=0xd4 => ProgramVideoResolution::P240,
0xe1..=0xe4 => ProgramVideoResolution::P1080,
_ => return,
_ => return None,
};
program.video = Some(ProgramVideo {
Some(ProgramVideo {
video_type,
resolution,
stream_content,
component_type,
});
})
}
fn parse_content(body: &[u8], program: &mut Program) {
fn parse_content(body: &[u8]) -> Option<Vec<ProgramGenre>> {
let genres = body
.chunks_exact(2)
.map(|content| ProgramGenre {
@@ -265,9 +425,133 @@ fn parse_content(body: &[u8], program: &mut Program) {
un2: content[1] & 0x0f,
})
.collect::<Vec<_>>();
if !genres.is_empty() {
program.genres = Some(genres);
(!genres.is_empty()).then_some(genres)
}
fn parse_audio_component(
body: &[u8],
program: &mut Program,
state: &mut ProgramState,
table_id: u8,
version: u8,
) {
if body.len() < 9 {
return;
}
let component_tag = body[2];
let versions = state.audio.entry(component_tag).or_default();
if !versions.should_update(table_id, version) {
return;
}
let multilingual = body[5] & 0x80 != 0;
if multilingual && body.len() < 12 {
return;
}
let mut langs = vec![language_code(&body[6..9])];
if multilingual {
langs.push(language_code(&body[9..12]));
}
let sampling_rate = match (body[5] >> 1) & 0x07 {
1 => 16_000,
2 => 22_050,
3 => 24_000,
5 => 32_000,
6 => 44_100,
7 => 48_000,
_ => return,
};
versions.mark(table_id, version);
state.audios.insert(
component_tag,
ProgramAudio {
component_type: body[1],
component_tag,
is_main: body[5] & 0x40 != 0,
sampling_rate,
langs,
},
);
program.audios = Some(state.audios.values().cloned().collect());
}
fn language_code(value: &[u8]) -> String {
match value {
b"jpn" | b"eng" | b"deu" | b"fra" | b"ita" | b"rus" | b"zho" | b"kor" | b"spa" => {
String::from_utf8_lossy(value).into_owned()
}
_ => "etc".into(),
}
}
fn parse_series(body: &[u8]) -> Option<ProgramSeries> {
if body.len() < 8 {
return None;
}
let expires_at = if body[2] & 0x01 != 0 {
decode_mjd_date(&body[3..5]).unwrap_or(-1)
} else {
-1
};
let episodes = (u32::from(body[5]) << 16) | (u32::from(body[6]) << 8) | u32::from(body[7]);
Some(ProgramSeries {
id: u16::from_be_bytes([body[0], body[1]]),
repeat: body[2] >> 4,
pattern: (body[2] >> 1) & 0x07,
expires_at,
episode: u16::try_from(episodes >> 12).ok()?,
last_episode: u16::try_from(episodes & 0x0fff).ok()?,
name: decode_arib(&body[8..]),
})
}
fn parse_event_group(
body: &[u8],
program: &mut Program,
state: &mut ProgramState,
table_id: u8,
version: u8,
) {
let Some(&header) = body.first() else {
return;
};
let group_type = header >> 4;
let event_count = usize::from(header & 0x0f);
let versions = state.group.entry(group_type).or_default();
if !versions.should_update(table_id, version) {
return;
}
let local_end = 1usize.saturating_add(event_count.saturating_mul(4));
if local_end > body.len() {
return;
}
let item_type = match group_type {
1 => ProgramRelatedItemType::Shared,
2 | 4 => ProgramRelatedItemType::Relay,
_ => ProgramRelatedItemType::Movement,
};
let mut items = Vec::new();
if matches!(group_type, 4 | 5) {
for event in body[local_end..].chunks_exact(8) {
items.push(ProgramRelatedItem {
item_type,
network_id: Some(u16::from_be_bytes([event[0], event[1]])),
service_id: u16::from_be_bytes([event[4], event[5]]),
event_id: u16::from_be_bytes([event[6], event[7]]),
});
}
} else {
for event in body[1..local_end].chunks_exact(4) {
items.push(ProgramRelatedItem {
item_type,
network_id: None,
service_id: u16::from_be_bytes([event[0], event[1]]),
event_id: u16::from_be_bytes([event[2], event[3]]),
});
}
}
versions.mark(table_id, version);
state.groups.insert(group_type, items);
program.related_items = Some(state.groups.values().flatten().cloned().collect());
}
fn decode_mjd_time(value: &[u8]) -> Option<u64> {
@@ -289,6 +573,17 @@ fn decode_mjd_time(value: &[u8]) -> Option<u64> {
u64::try_from(unix_seconds.checked_mul(1000)?).ok()
}
fn decode_mjd_date(value: &[u8]) -> Option<i64> {
if value.len() != 2 || value.iter().all(|byte| *byte == 0xff) {
return None;
}
let mjd = i64::from(u16::from_be_bytes([value[0], value[1]]));
(mjd - 40_587)
.checked_mul(86_400)?
.checked_sub(9 * 3_600)?
.checked_mul(1000)
}
fn decode_bcd_duration(value: &[u8]) -> Option<u64> {
if value.len() != 3 || value.iter().all(|byte| *byte == 0xff) {
return None;
@@ -312,152 +607,12 @@ fn decode_bcd(value: u8) -> Option<u8> {
(high <= 9 && low <= 9).then_some(high * 10 + low)
}
#[derive(Debug, Clone, Copy)]
enum GraphicSet {
Kanji,
Alphanumeric,
Hiragana,
Katakana,
}
pub(crate) fn decode_arib(input: &[u8]) -> String {
let mut output = String::new();
let mut sets = [
GraphicSet::Kanji,
GraphicSet::Alphanumeric,
GraphicSet::Hiragana,
GraphicSet::Katakana,
];
let mut left = 0usize;
let mut right = 2usize;
let mut single_shift = None;
let mut offset = 0;
while offset < input.len() {
let byte = input[offset];
match byte {
0x0e => left = 1,
0x0f => left = 0,
0x19 => single_shift = Some(2),
0x1d => single_shift = Some(3),
0x1b => {
offset += consume_escape(&input[offset..], &mut sets, &mut left, &mut right)
.saturating_sub(1);
}
0x0d => output.push('\n'),
0x20 | 0xa0 => output.push(' '),
0x21..=0x7e => {
let set = single_shift.take().unwrap_or(left);
let consumed = decode_graphic(sets[set], &input[offset..], false, &mut output);
offset += consumed.saturating_sub(1);
}
0xa1..=0xfe => {
let consumed = decode_graphic(sets[right], &input[offset..], true, &mut output);
offset += consumed.saturating_sub(1);
}
_ => {}
}
offset += 1;
}
output
}
fn decode_graphic(set: GraphicSet, input: &[u8], high_bit: bool, output: &mut String) -> usize {
let first = if high_bit { input[0] & 0x7f } else { input[0] };
match set {
GraphicSet::Kanji => {
let Some(second) = input.get(1).copied() else {
return 1;
};
let second = if high_bit { second & 0x7f } else { second };
append_euc_jp(&[first | 0x80, second | 0x80], output);
2
}
GraphicSet::Alphanumeric => {
output.push(char::from(first));
1
}
GraphicSet::Hiragana => {
append_euc_jp(&[0xa4, first | 0x80], output);
1
}
GraphicSet::Katakana => {
append_euc_jp(&[0xa5, first | 0x80], output);
1
}
}
}
fn append_euc_jp(bytes: &[u8], output: &mut String) {
let (decoded, had_errors) = EUC_JP.decode_without_bom_handling(bytes);
if had_errors {
output.push('\u{fffd}');
} else {
output.push_str(&decoded);
}
}
fn consume_escape(
input: &[u8],
sets: &mut [GraphicSet; 4],
left: &mut usize,
right: &mut usize,
) -> usize {
let Some(second) = input.get(1).copied() else {
return 1;
};
match second {
0x6e => *left = 2,
0x6f => *left = 3,
0x7c => *right = 3,
0x7d => *right = 2,
0x7e => *right = 1,
0x28..=0x2b => {
let Some(code) = input.get(2).copied() else {
return 2;
};
if let Some(set) = graphic_set(code) {
sets[usize::from(second - 0x28)] = set;
}
return 3;
}
0x24 => {
let Some(third) = input.get(2).copied() else {
return 2;
};
if matches!(third, 0x28..=0x2b) {
let Some(code) = input.get(3).copied() else {
return 3;
};
if let Some(set) = graphic_set(code) {
sets[usize::from(third - 0x28)] = set;
}
return 4;
}
if let Some(set) = graphic_set(third) {
sets[0] = set;
}
return 3;
}
_ => {}
}
2
}
const fn graphic_set(code: u8) -> Option<GraphicSet> {
match code {
0x42 | 0x39 => Some(GraphicSet::Kanji),
0x4a | 0x36 => Some(GraphicSet::Alphanumeric),
0x30 | 0x37 => Some(GraphicSet::Hiragana),
0x31 | 0x38 | 0x49 => Some(GraphicSet::Katakana),
_ => None,
}
}
#[cfg(test)]
mod tests {
use crate::ts::{PACKET_SIZE, crc32_mpeg2};
use mirakurun_types::ProgramRelatedItemType;
use super::{EitCollector, decode_arib, decode_bcd_duration, decode_mjd_time};
use super::{EitCollector, VersionRecord, decode_arib, decode_bcd_duration, decode_mjd_time};
#[test]
fn decodes_common_arib_character_sets() {
@@ -506,4 +661,108 @@ mod tests {
assert_eq!(programs[0].duration, 3_600_000);
assert!(programs[0].is_free);
}
#[test]
fn parses_audio_series_and_related_program_descriptors() {
let mut descriptors = vec![
0xc4, 0x09, 0x02, 0x03, 0x10, 0x0f, 0xff, 0x4e, b'j', b'p', b'n',
];
let series_name = [0x0e, b'S', b'e', b'r', b'i', b'e', b's'];
descriptors.extend_from_slice(&[
0xd5,
u8::try_from(8 + series_name.len()).unwrap_or(0),
0x12,
0x34,
0x14,
0xff,
0xff,
0x00,
0x50,
0x0c,
]);
descriptors.extend_from_slice(&series_name);
descriptors.extend_from_slice(&[0xd6, 0x05, 0x11, 0x00, 0x65, 0x00, 0x02]);
let packet = eit_packet(0x4e, 0, &descriptors);
let mut collector = EitCollector::default();
collector.push(&packet);
let programs = collector.into_programs();
let program = &programs[0];
let audio = &program.audios.as_ref().expect("audio descriptor")[0];
assert_eq!(audio.component_type, 0x03);
assert_eq!(audio.component_tag, 0x10);
assert!(audio.is_main);
assert_eq!(audio.sampling_rate, 48_000);
assert_eq!(audio.langs, ["jpn"]);
let series = program.series.as_ref().expect("series descriptor");
assert_eq!(series.id, 0x1234);
assert_eq!(series.repeat, 1);
assert_eq!(series.pattern, 2);
assert_eq!(series.episode, 5);
assert_eq!(series.last_episode, 12);
assert_eq!(series.name, "Series");
let related = &program.related_items.as_ref().expect("event group")[0];
assert_eq!(related.item_type, ProgramRelatedItemType::Shared);
assert_eq!(related.network_id, None);
assert_eq!(related.service_id, 101);
assert_eq!(related.event_id, 2);
}
#[test]
fn present_following_versions_take_priority_over_schedule_tables() {
let mut versions = VersionRecord::default();
assert!(versions.should_update(0x50, 1));
versions.mark(0x50, 1);
assert!(versions.should_update(0x4e, 1));
versions.mark(0x4e, 1);
assert!(!versions.should_update(0x50, 2));
assert!(versions.should_update(0x4e, 2));
}
fn eit_packet(table_id: u8, version: u8, descriptors: &[u8]) -> [u8; PACKET_SIZE] {
let descriptor_length = descriptors.len();
let mut section = vec![
table_id,
0xb0,
0x00,
0x00,
0x65,
0xc1 | ((version & 0x1f) << 1),
0x00,
0x00,
0x00,
0x01,
0x7f,
0xf0,
0x00,
table_id,
0x00,
0x01,
0x9e,
0x8b,
0x09,
0x00,
0x00,
0x01,
0x00,
0x00,
0x80 | u8::try_from(descriptor_length >> 8).unwrap_or(0),
u8::try_from(descriptor_length & 0xff).unwrap_or(0),
];
section.extend_from_slice(descriptors);
let section_length = section.len() - 3 + 4;
section[1] = 0xb0 | u8::try_from(section_length >> 8).unwrap_or(0);
section[2] = u8::try_from(section_length & 0xff).unwrap_or(0);
let crc = crc32_mpeg2(&section);
section.extend_from_slice(&crc.to_be_bytes());
let mut packet = [0xff; PACKET_SIZE];
packet[0] = 0x47;
packet[1] = 0x40;
packet[2] = 0x12;
packet[3] = 0x10;
packet[4] = 0;
packet[5..5 + section.len()].copy_from_slice(&section);
packet
}
}