First commit

This commit is contained in:
CyberRex
2026-07-31 14:39:33 +09:00
commit 38670efd46
103 changed files with 22514 additions and 0 deletions

View File

@@ -0,0 +1,509 @@
// Copyright 2026 Mirakurun contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
use std::collections::{BTreeMap, HashMap, HashSet};
use encoding_rs::EUC_JP;
use mirakurun_types::{
Program, ProgramGenre, ProgramVideo, ProgramVideoResolution, ProgramVideoType, program_id,
};
use crate::{
filter::SectionAssembler,
ts::{Packet, PacketFramer, crc32_mpeg2},
};
const PID_EIT: u16 = 0x0012;
#[derive(Debug, Default)]
pub struct EitCollector {
framer: PacketFramer,
assembler: SectionAssembler,
programs: HashMap<u64, Program>,
sections_seen: HashSet<(u8, u16, u8, u8)>,
packet_count: u64,
section_count: u64,
}
impl EitCollector {
pub fn push(&mut self, chunk: &[u8]) {
let mut packets = Vec::new();
self.framer.push(chunk, |packet| packets.push(*packet));
for packet_bytes in packets {
self.packet_count = self.packet_count.saturating_add(1);
let Ok(packet) = Packet::new(&packet_bytes) else {
continue;
};
if packet.pid() != PID_EIT {
continue;
}
for section in self.assembler.push(packet) {
if parse_eit_section(&section, &mut self.programs)
&& self.sections_seen.insert((
section[0],
u16::from_be_bytes([section[3], section[4]]),
(section[5] >> 1) & 0x1f,
section[6],
))
{
self.section_count = self.section_count.saturating_add(1);
}
}
}
}
#[must_use]
pub const fn packet_count(&self) -> u64 {
self.packet_count
}
#[must_use]
pub const fn section_count(&self) -> u64 {
self.section_count
}
#[must_use]
pub fn into_programs(self) -> Vec<Program> {
let mut programs = self.programs.into_values().collect::<Vec<_>>();
programs.sort_by_key(|program| (program.start_at, program.id));
programs
}
}
fn parse_eit_section(section: &[u8], programs: &mut HashMap<u64, Program>) -> bool {
if section.len() < 18
|| !matches!(section[0], 0x4e..=0x6f)
|| section[5] & 0x01 == 0
|| crc32_mpeg2(section) != 0
{
return false;
}
let is_present_following = matches!(section[0], 0x4e | 0x4f);
if is_present_following && section[6] > 1 {
return false;
}
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 {
return false;
};
let mut offset = 14;
while offset + 12 <= end {
let event_id = u16::from_be_bytes([section[offset], section[offset + 1]]);
let start = &section[offset + 2..offset + 7];
let duration = &section[offset + 7..offset + 10];
let descriptor_length =
(usize::from(section[offset + 10] & 0x0f) << 8) | usize::from(section[offset + 11]);
let Some(descriptor_end) = offset
.checked_add(12)
.and_then(|value| value.checked_add(descriptor_length))
else {
return false;
};
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);
}
offset = descriptor_end;
}
true
}
fn parse_descriptors(mut descriptors: &[u8], program: &mut Program) {
while descriptors.len() >= 2 {
let length = usize::from(descriptors[1]);
let Some(end) = 2usize.checked_add(length) else {
return;
};
if end > descriptors.len() {
return;
}
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),
_ => {}
}
descriptors = &descriptors[end..];
}
}
fn parse_short_event(body: &[u8], program: &mut Program) {
if body.len() < 5 {
return;
}
let name_length = usize::from(body[3]);
let Some(text_length_offset) = 4usize.checked_add(name_length) else {
return;
};
if text_length_offset >= body.len() {
return;
}
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;
};
if text_end > body.len() {
return;
}
program.name = Some(decode_arib(&body[4..text_length_offset]));
program.description = Some(decode_arib(&body[text_start..text_end]));
}
fn parse_extended_event(body: &[u8], program: &mut Program) {
if body.len() < 6 {
return;
}
let items_length = usize::from(body[4]);
let Some(items_end) = 5usize.checked_add(items_length) else {
return;
};
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();
while !items.is_empty() {
let description_length = usize::from(items[0]);
let Some(description_end) = 1usize.checked_add(description_length) else {
return;
};
if description_end >= items.len() {
return;
}
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;
};
if item_end > items.len() {
return;
}
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);
items = &items[item_end..];
}
}
fn parse_video_component(body: &[u8], program: &mut Program) {
if body.len() < 2 {
return;
}
let stream_content = body[0] & 0x0f;
let component_type = body[1];
let video_type = match stream_content {
1 => ProgramVideoType::Mpeg2,
5 => ProgramVideoType::H264,
9 => ProgramVideoType::H265,
_ => return,
};
let resolution = match component_type {
0x01..=0x04 => ProgramVideoResolution::I480,
0x83 => ProgramVideoResolution::P4320,
0x91..=0x94 => ProgramVideoResolution::P2160,
0xa1..=0xa4 => ProgramVideoResolution::P480,
0xb1..=0xb4 => ProgramVideoResolution::I1080,
0xc1..=0xc4 => ProgramVideoResolution::P720,
0xd1..=0xd4 => ProgramVideoResolution::P240,
0xe1..=0xe4 => ProgramVideoResolution::P1080,
_ => return,
};
program.video = Some(ProgramVideo {
video_type,
resolution,
stream_content,
component_type,
});
}
fn parse_content(body: &[u8], program: &mut Program) {
let genres = body
.chunks_exact(2)
.map(|content| ProgramGenre {
lv1: content[0] >> 4,
lv2: content[0] & 0x0f,
un1: content[1] >> 4,
un2: content[1] & 0x0f,
})
.collect::<Vec<_>>();
if !genres.is_empty() {
program.genres = Some(genres);
}
}
fn decode_mjd_time(value: &[u8]) -> Option<u64> {
if value.len() != 5 || value.iter().all(|byte| *byte == 0xff) {
return None;
}
let mjd = i64::from(u16::from_be_bytes([value[0], value[1]]));
let hour = i64::from(decode_bcd(value[2])?);
let minute = i64::from(decode_bcd(value[3])?);
let second = i64::from(decode_bcd(value[4])?);
if hour > 23 || minute > 59 || second > 59 {
return None;
}
let unix_seconds = (mjd - 40_587)
.checked_mul(86_400)?
.checked_add((hour - 9).checked_mul(3600)?)?
.checked_add(minute.checked_mul(60)?)?
.checked_add(second)?;
u64::try_from(unix_seconds.checked_mul(1000)?).ok()
}
fn decode_bcd_duration(value: &[u8]) -> Option<u64> {
if value.len() != 3 || value.iter().all(|byte| *byte == 0xff) {
return None;
}
let hours = u64::from(decode_bcd(value[0])?);
let minutes = u64::from(decode_bcd(value[1])?);
let seconds = u64::from(decode_bcd(value[2])?);
if minutes > 59 || seconds > 59 {
return None;
}
hours
.checked_mul(3600)?
.checked_add(minutes.checked_mul(60)?)?
.checked_add(seconds)?
.checked_mul(1000)
}
fn decode_bcd(value: u8) -> Option<u8> {
let high = value >> 4;
let low = value & 0x0f;
(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 super::{EitCollector, decode_arib, decode_bcd_duration, decode_mjd_time};
#[test]
fn decodes_common_arib_character_sets() {
assert_eq!(decode_arib(&[0x0e, b'T', b'e', b's', b't']), "Test");
assert_eq!(decode_arib(&[0x19, 0x22]), "");
assert_eq!(decode_arib(&[0x1d, 0x22]), "");
}
#[test]
fn converts_arib_time_fields() {
assert_eq!(decode_mjd_time(&[0x9e, 0x8b, 0x09, 0x00, 0x00]), Some(0));
assert_eq!(decode_bcd_duration(&[0x01, 0x30, 0x00]), Some(5_400_000));
}
#[test]
fn collects_program_from_eit_packet() {
let short_event = [
0x4d, 0x0d, b'j', b'p', b'n', 0x05, 0x0e, b'T', b'e', b's', b't', 0x03, 0x0e, b'O',
b'K',
];
let mut section = vec![
0x4e, 0xb0, 0x00, 0x00, 0x65, 0xc1, 0x00, 0x00, 0x00, 0x01, 0x7f, 0xf0, 0x00, 0x4e,
0x00, 0x01, 0x9e, 0x8b, 0x09, 0x00, 0x00, 0x01, 0x00, 0x00, 0x80, 0x0f,
];
section.extend_from_slice(&short_event);
let section_length = section.len() - 3 + 4;
section[1] = 0xb0 | u8::try_from(section_length >> 8).expect("section length");
section[2] = u8::try_from(section_length & 0xff).expect("section length");
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);
let mut collector = EitCollector::default();
collector.push(&packet);
let programs = collector.into_programs();
assert_eq!(programs.len(), 1);
assert_eq!(programs[0].name.as_deref(), Some("Test"));
assert_eq!(programs[0].description.as_deref(), Some("OK"));
assert_eq!(programs[0].duration, 3_600_000);
assert!(programs[0].is_free);
}
}