implement
This commit is contained in:
@@ -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(§ion, &mut self.programs)
|
||||
if parse_eit_section(§ion, &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(§ion[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(
|
||||
§ion[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(§ion);
|
||||
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(§ion);
|
||||
packet
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user