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

@@ -0,0 +1,486 @@
// 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::HashMap;
use encoding_rs::EUC_JP;
#[path = "arib_symbols.rs"]
mod symbols;
const ESC: u8 = 0x1b;
const MACRO: u8 = 0x95;
const MACRO_END: u8 = 0x4f;
const MAX_MACRO_DEPTH: usize = 8;
const DRCS_PRIVATE_USE_BASE: u32 = 0x0f_0000;
const DRCS_0_COUNT: u32 = 94 * 94;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum GraphicSet {
Kanji,
Alphanumeric,
Hiragana,
Katakana,
JisX0201Katakana,
Mosaic(u8),
JisPlane1,
JisPlane2,
AdditionalSymbols,
Drcs(u8),
Macro,
Unsupported(u8),
}
impl GraphicSet {
const fn width(self) -> usize {
match self {
Self::Kanji
| Self::JisPlane1
| Self::JisPlane2
| Self::AdditionalSymbols
| Self::Drcs(0) => 2,
Self::Unsupported(width) => width as usize,
_ => 1,
}
}
}
struct Decoder {
sets: [GraphicSet; 4],
left: usize,
right: usize,
single_shift: Option<usize>,
macros: HashMap<u8, Vec<u8>>,
output: String,
}
impl Default for Decoder {
fn default() -> Self {
Self {
sets: [
GraphicSet::Kanji,
GraphicSet::Alphanumeric,
GraphicSet::Hiragana,
GraphicSet::Katakana,
],
left: 0,
right: 2,
single_shift: None,
macros: HashMap::new(),
output: String::new(),
}
}
}
impl Decoder {
fn decode(mut self, input: &[u8]) -> String {
self.decode_sequence(input, 0);
self.output
}
fn decode_sequence(&mut self, input: &[u8], depth: usize) {
if depth > MAX_MACRO_DEPTH {
return;
}
let mut offset = 0;
while offset < input.len() {
let byte = input[offset];
match byte {
0x0e => self.left = 1,
0x0f => self.left = 0,
0x19 => self.single_shift = Some(2),
0x1d => self.single_shift = Some(3),
ESC => {
offset += self.consume_escape(&input[offset..]).saturating_sub(1);
}
0x0d => self.output.push('\n'),
0x16 => offset = offset.saturating_add(1),
0x1c => offset = offset.saturating_add(2),
0x20 | 0xa0 => self.output.push(' '),
MACRO => {
offset += self
.consume_macro_definition(&input[offset..], depth)
.saturating_sub(1);
}
0x9b => {
offset += consume_csi(&input[offset..]).saturating_sub(1);
}
0x90 => {
offset += consume_col(&input[offset..]).saturating_sub(1);
}
0x91..=0x94 | 0x97..=0x98 => offset = offset.saturating_add(1),
0x9d => {
offset += consume_time(&input[offset..]).saturating_sub(1);
}
0x21..=0x7e => {
let set = self.single_shift.take().unwrap_or(self.left);
let consumed =
self.decode_graphic(self.sets[set], &input[offset..], false, depth);
offset += consumed.saturating_sub(1);
}
0xa1..=0xfe => {
let consumed =
self.decode_graphic(self.sets[self.right], &input[offset..], true, depth);
offset += consumed.saturating_sub(1);
}
_ => {}
}
offset += 1;
}
}
fn decode_graphic(
&mut self,
set: GraphicSet,
input: &[u8],
high_bit: bool,
depth: usize,
) -> usize {
let width = set.width();
if input.len() < width {
return input.len().max(1);
}
let first = normalize_graphic(input[0], high_bit);
let second = (width == 2).then(|| normalize_graphic(input[1], high_bit));
if !(0x21..=0x7e).contains(&first)
|| second.is_some_and(|value| !(0x21..=0x7e).contains(&value))
{
return width;
}
match set {
GraphicSet::Kanji | GraphicSet::JisPlane1 => {
append_euc_jp(
&[first | 0x80, second.unwrap_or(0) | 0x80],
&mut self.output,
);
}
GraphicSet::JisPlane2 => {
append_euc_jp(
&[0x8f, first | 0x80, second.unwrap_or(0) | 0x80],
&mut self.output,
);
}
GraphicSet::Alphanumeric => self.output.push(char::from(first)),
GraphicSet::Hiragana => append_euc_jp(&[0xa4, first | 0x80], &mut self.output),
GraphicSet::Katakana => append_euc_jp(&[0xa5, first | 0x80], &mut self.output),
GraphicSet::JisX0201Katakana => {
let codepoint = 0xff61 + u32::from(first - 0x21);
self.output
.push(char::from_u32(codepoint).unwrap_or('\u{fffd}'));
}
GraphicSet::Mosaic(kind) => self.output.push(mosaic_character(kind, first)),
GraphicSet::AdditionalSymbols => {
self.output.push(
symbols::additional_symbol(first, second.unwrap_or(0)).unwrap_or('\u{fffd}'),
);
}
GraphicSet::Drcs(set_number) => {
self.output
.push(drcs_character(set_number, first, second).unwrap_or('\u{fffd}'));
}
GraphicSet::Macro => self.invoke_macro(first, depth),
GraphicSet::Unsupported(_) => self.output.push('\u{fffd}'),
}
width
}
fn invoke_macro(&mut self, code: u8, depth: usize) {
if let Some(body) = self.macros.get(&code).cloned() {
self.decode_sequence(&body, depth + 1);
} else {
self.apply_default_macro(code);
}
}
fn apply_default_macro(&mut self, code: u8) {
let Some(sets) = default_macro_sets(code) else {
return;
};
self.sets = sets;
self.left = 0;
self.right = 2;
self.single_shift = None;
}
fn consume_macro_definition(&mut self, input: &[u8], depth: usize) -> usize {
let Some(&mode) = input.get(1) else {
return 1;
};
if !matches!(mode, 0x40 | 0x41) {
return 2;
}
let Some(&code) = input.get(2) else {
return 2;
};
let mut end = 3;
while end + 1 < input.len() {
if input[end] == MACRO && input[end + 1] == MACRO_END {
let body = input[3..end].to_vec();
self.macros.insert(code, body.clone());
if mode == 0x41 {
self.decode_sequence(&body, depth + 1);
}
return end + 2;
}
end += 1;
}
input.len()
}
fn consume_escape(&mut self, input: &[u8]) -> usize {
let Some(&second) = input.get(1) else {
return 1;
};
match second {
0x6e => self.left = 2,
0x6f => self.left = 3,
0x7c => self.right = 3,
0x7d => self.right = 2,
0x7e => self.right = 1,
0x28..=0x2b => {
let Some(&third) = input.get(2) else {
return 2;
};
let index = usize::from(second - 0x28);
if third == 0x20 {
let Some(&final_byte) = input.get(3) else {
return 3;
};
self.sets[index] = drcs_set(final_byte, 1);
return 4;
}
self.sets[index] =
one_byte_graphic_set(third).unwrap_or(GraphicSet::Unsupported(1));
return 3;
}
0x24 => {
let Some(&third) = input.get(2) else {
return 2;
};
if matches!(third, 0x28..=0x2b) {
let index = usize::from(third - 0x28);
let Some(&fourth) = input.get(3) else {
return 3;
};
if fourth == 0x20 {
let Some(&final_byte) = input.get(4) else {
return 4;
};
self.sets[index] = drcs_set(final_byte, 2);
return 5;
}
if fourth == 0x28 {
self.sets[index] = GraphicSet::Unsupported(1);
return input.get(4).map_or(4, |_| 5);
}
self.sets[index] =
two_byte_graphic_set(fourth).unwrap_or(GraphicSet::Unsupported(2));
return 4;
}
self.sets[0] = two_byte_graphic_set(third).unwrap_or(GraphicSet::Unsupported(2));
return 3;
}
_ => {}
}
2
}
}
#[must_use]
pub(crate) fn decode(input: &[u8]) -> String {
Decoder::default().decode(input)
}
const fn normalize_graphic(byte: u8, high_bit: bool) -> u8 {
if high_bit { byte & 0x7f } else { byte }
}
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);
}
}
const fn one_byte_graphic_set(code: u8) -> Option<GraphicSet> {
match code {
0x30 | 0x37 => Some(GraphicSet::Hiragana),
0x31 | 0x38 => Some(GraphicSet::Katakana),
0x32..=0x35 => Some(GraphicSet::Mosaic(code - 0x32)),
0x36 | 0x4a => Some(GraphicSet::Alphanumeric),
0x49 => Some(GraphicSet::JisX0201Katakana),
0x70 => Some(GraphicSet::Macro),
_ => None,
}
}
const fn two_byte_graphic_set(code: u8) -> Option<GraphicSet> {
match code {
0x39 => Some(GraphicSet::JisPlane1),
0x3a => Some(GraphicSet::JisPlane2),
0x3b => Some(GraphicSet::AdditionalSymbols),
0x42 => Some(GraphicSet::Kanji),
_ => None,
}
}
const fn drcs_set(final_byte: u8, width: u8) -> GraphicSet {
if final_byte >= 0x40 && final_byte <= 0x4f {
GraphicSet::Drcs(final_byte - 0x40)
} else {
GraphicSet::Unsupported(width)
}
}
fn drcs_character(set: u8, first: u8, second: Option<u8>) -> Option<char> {
let index = if set == 0 {
u32::from(first.checked_sub(0x21)?) * 94 + u32::from(second?.checked_sub(0x21)?)
} else {
DRCS_0_COUNT + u32::from(set.checked_sub(1)?) * 94 + u32::from(first.checked_sub(0x21)?)
};
char::from_u32(DRCS_PRIVATE_USE_BASE + index)
}
const fn mosaic_character(kind: u8, code: u8) -> char {
const MOSAIC: [char; 4] = ['▦', '▥', '▧', '▨'];
let index = ((kind as usize) + (code as usize & 0x03)) % MOSAIC.len();
MOSAIC[index]
}
const fn default_macro_sets(code: u8) -> Option<[GraphicSet; 4]> {
let macro_set = GraphicSet::Macro;
match code {
0x60 => Some([
GraphicSet::Kanji,
GraphicSet::Alphanumeric,
GraphicSet::Hiragana,
macro_set,
]),
0x61 => Some([
GraphicSet::Kanji,
GraphicSet::Katakana,
GraphicSet::Hiragana,
macro_set,
]),
0x62 => Some([
GraphicSet::Kanji,
GraphicSet::Drcs(1),
GraphicSet::Hiragana,
macro_set,
]),
0x63 => Some([
GraphicSet::Mosaic(0),
GraphicSet::Mosaic(2),
GraphicSet::Mosaic(3),
macro_set,
]),
0x64 => Some([
GraphicSet::Mosaic(0),
GraphicSet::Mosaic(1),
GraphicSet::Mosaic(3),
macro_set,
]),
0x65 => Some([
GraphicSet::Mosaic(0),
GraphicSet::Drcs(1),
GraphicSet::Mosaic(3),
macro_set,
]),
0x66..=0x6a => {
let first = 1 + (code - 0x66) * 3;
Some([
GraphicSet::Drcs(first),
GraphicSet::Drcs(first + 1),
GraphicSet::Drcs(first + 2),
macro_set,
])
}
0x6b..=0x6d => Some([
GraphicSet::Kanji,
GraphicSet::Drcs(2 + code - 0x6b),
GraphicSet::Hiragana,
macro_set,
]),
0x6e => Some([
GraphicSet::Katakana,
GraphicSet::Hiragana,
GraphicSet::Alphanumeric,
macro_set,
]),
0x6f => Some([
GraphicSet::Alphanumeric,
GraphicSet::Mosaic(0),
GraphicSet::Drcs(1),
macro_set,
]),
_ => None,
}
}
fn consume_csi(input: &[u8]) -> usize {
input
.iter()
.enumerate()
.skip(1)
.find_map(|(index, byte)| (0x40..=0x7e).contains(byte).then_some(index + 1))
.unwrap_or(input.len())
}
fn consume_col(input: &[u8]) -> usize {
match input.get(1) {
Some(0x20) => input.len().min(3),
Some(_) => input.len().min(2),
None => 1,
}
}
fn consume_time(input: &[u8]) -> usize {
match input.get(1) {
Some(0x20 | 0x28) => input.len().min(3),
Some(_) => consume_csi(input),
None => 1,
}
}
#[cfg(test)]
mod tests {
use super::{DRCS_PRIVATE_USE_BASE, decode};
#[test]
fn decodes_common_character_sets() {
assert_eq!(decode(&[0x0e, b'T', b'e', b's', b't']), "Test");
assert_eq!(decode(&[0x19, 0x22]), "");
assert_eq!(decode(&[0x1d, 0x22]), "");
}
#[test]
fn decodes_additional_symbols_and_kanji() {
assert_eq!(decode(&[0x1b, 0x24, 0x3b, 0x7a, 0x30]), "🅿");
assert_eq!(decode(&[0x1b, 0x24, 0x3b, 0x75, 0x21]), "");
}
#[test]
fn preserves_drcs_identity_in_private_use_plane() {
let expected = char::from_u32(DRCS_PRIVATE_USE_BASE).map(String::from);
assert_eq!(
decode(&[0x1b, 0x24, 0x28, 0x20, 0x40, 0x21, 0x21]),
expected.unwrap_or_default()
);
}
#[test]
fn applies_default_and_dynamically_defined_macros() {
assert_eq!(decode(&[0x1b, 0x2b, 0x70, 0x1d, 0x61, 0x0e, 0x22]), "");
assert_eq!(
decode(&[
0x1b, 0x2b, 0x70, 0x95, 0x40, 0x21, 0x0e, 0x95, 0x4f, 0x1d, 0x21, b'A',
]),
"A"
);
}
}

View File

@@ -0,0 +1,548 @@
// 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.
// ARIB STD-B24 additional Kanji and symbol mappings.
#[allow(
clippy::match_same_arms,
clippy::too_many_lines,
clippy::unreadable_literal
)]
pub(super) fn additional_symbol(first: u8, second: u8) -> Option<char> {
let code = (u16::from(first) << 8) | u16::from(second);
match code {
0x7A21 => char::from_u32(0x000026CC),
0x7A22 => char::from_u32(0x000026CD),
0x7A23 => char::from_u32(0x00002757),
0x7A24 => char::from_u32(0x000026CF),
0x7A25 => char::from_u32(0x000026D0),
0x7A26 => char::from_u32(0x000026D1),
0x7A28 => char::from_u32(0x000026D2),
0x7A29 => char::from_u32(0x000026D5),
0x7A2A => char::from_u32(0x000026D3),
0x7A2B => char::from_u32(0x000026D4),
0x7A30 => char::from_u32(0x0001F17F),
0x7A31 => char::from_u32(0x0001F18A),
0x7A34 => char::from_u32(0x000026D6),
0x7A35 => char::from_u32(0x000026D7),
0x7A36 => char::from_u32(0x000026D8),
0x7A37 => char::from_u32(0x000026D9),
0x7A38 => char::from_u32(0x000026DA),
0x7A39 => char::from_u32(0x000026DB),
0x7A3A => char::from_u32(0x000026DC),
0x7A3B => char::from_u32(0x000026DD),
0x7A3C => char::from_u32(0x000026DE),
0x7A3D => char::from_u32(0x000026DF),
0x7A3E => char::from_u32(0x000026E0),
0x7A3F => char::from_u32(0x000026E1),
0x7A40 => char::from_u32(0x00002B55),
0x7A41 => char::from_u32(0x00003248),
0x7A42 => char::from_u32(0x00003249),
0x7A43 => char::from_u32(0x0000324A),
0x7A44 => char::from_u32(0x0000324B),
0x7A45 => char::from_u32(0x0000324C),
0x7A46 => char::from_u32(0x0000324D),
0x7A47 => char::from_u32(0x0000324E),
0x7A48 => char::from_u32(0x0000324F),
0x7A4D => char::from_u32(0x00002491),
0x7A4E => char::from_u32(0x00002492),
0x7A4F => char::from_u32(0x00002493),
0x7A50 => char::from_u32(0x0001F14A),
0x7A51 => char::from_u32(0x0001F14C),
0x7A52 => char::from_u32(0x0001F13F),
0x7A53 => char::from_u32(0x0001F146),
0x7A54 => char::from_u32(0x0001F14B),
0x7A55 => char::from_u32(0x0001F210),
0x7A56 => char::from_u32(0x0001F211),
0x7A57 => char::from_u32(0x0001F212),
0x7A58 => char::from_u32(0x0001F213),
0x7A59 => char::from_u32(0x0001F142),
0x7A5A => char::from_u32(0x0001F214),
0x7A5B => char::from_u32(0x0001F215),
0x7A5C => char::from_u32(0x0001F216),
0x7A5D => char::from_u32(0x0001F14D),
0x7A5E => char::from_u32(0x0001F131),
0x7A5F => char::from_u32(0x0001F13D),
0x7A60 => char::from_u32(0x00002B1B),
0x7A61 => char::from_u32(0x00002B24),
0x7A62 => char::from_u32(0x0001F217),
0x7A63 => char::from_u32(0x0001F218),
0x7A64 => char::from_u32(0x0001F219),
0x7A65 => char::from_u32(0x0001F21A),
0x7A66 => char::from_u32(0x0001F21B),
0x7A67 => char::from_u32(0x000026BF),
0x7A68 => char::from_u32(0x0001F21C),
0x7A69 => char::from_u32(0x0001F21D),
0x7A6A => char::from_u32(0x0001F21E),
0x7A6B => char::from_u32(0x0001F21F),
0x7A6C => char::from_u32(0x0001F220),
0x7A6D => char::from_u32(0x0001F221),
0x7A6E => char::from_u32(0x0001F222),
0x7A6F => char::from_u32(0x0001F223),
0x7A70 => char::from_u32(0x0001F224),
0x7A71 => char::from_u32(0x0001F225),
0x7A72 => char::from_u32(0x0001F14E),
0x7A73 => char::from_u32(0x00003299),
0x7A74 => char::from_u32(0x0001F200),
0x7B21 => char::from_u32(0x000026E3),
0x7B22 => char::from_u32(0x00002B56),
0x7B23 => char::from_u32(0x00002B57),
0x7B24 => char::from_u32(0x00002B58),
0x7B25 => char::from_u32(0x00002B59),
0x7B26 => char::from_u32(0x00002613),
0x7B27 => char::from_u32(0x0000328B),
0x7B28 => char::from_u32(0x00003012),
0x7B29 => char::from_u32(0x000026E8),
0x7B2A => char::from_u32(0x00003246),
0x7B2B => char::from_u32(0x00003245),
0x7B2C => char::from_u32(0x000026E9),
0x7B2D => char::from_u32(0x00000FD6),
0x7B2E => char::from_u32(0x000026EA),
0x7B2F => char::from_u32(0x000026EB),
0x7B30 => char::from_u32(0x000026EC),
0x7B31 => char::from_u32(0x00002668),
0x7B32 => char::from_u32(0x000026ED),
0x7B33 => char::from_u32(0x000026EE),
0x7B34 => char::from_u32(0x000026EF),
0x7B35 => char::from_u32(0x00002693),
0x7B36 => char::from_u32(0x00002708),
0x7B37 => char::from_u32(0x000026F0),
0x7B38 => char::from_u32(0x000026F1),
0x7B39 => char::from_u32(0x000026F2),
0x7B3A => char::from_u32(0x000026F3),
0x7B3B => char::from_u32(0x000026F4),
0x7B3C => char::from_u32(0x000026F5),
0x7B3D => char::from_u32(0x0001F157),
0x7B3E => char::from_u32(0x000024B9),
0x7B3F => char::from_u32(0x000024C8),
0x7B40 => char::from_u32(0x000026F6),
0x7B41 => char::from_u32(0x0001F15F),
0x7B42 => char::from_u32(0x0001F18B),
0x7B43 => char::from_u32(0x0001F18D),
0x7B44 => char::from_u32(0x0001F18C),
0x7B45 => char::from_u32(0x0001F179),
0x7B46 => char::from_u32(0x000026F7),
0x7B47 => char::from_u32(0x000026F8),
0x7B48 => char::from_u32(0x000026F9),
0x7B49 => char::from_u32(0x000026FA),
0x7B4A => char::from_u32(0x0001F17B),
0x7B4B => char::from_u32(0x0000260E),
0x7B4C => char::from_u32(0x000026FB),
0x7B4D => char::from_u32(0x000026FC),
0x7B4E => char::from_u32(0x000026FD),
0x7B4F => char::from_u32(0x000026FE),
0x7B50 => char::from_u32(0x0001F17C),
0x7B51 => char::from_u32(0x000026FF),
0x7C21 => char::from_u32(0x000027A1),
0x7C22 => char::from_u32(0x00002B05),
0x7C23 => char::from_u32(0x00002B06),
0x7C24 => char::from_u32(0x00002B07),
0x7C25 => char::from_u32(0x00002B2F),
0x7C26 => char::from_u32(0x00002B2E),
0x7C27 => char::from_u32(0x00005E74),
0x7C28 => char::from_u32(0x00006708),
0x7C29 => char::from_u32(0x000065E5),
0x7C2A => char::from_u32(0x00005186),
0x7C2B => char::from_u32(0x000033A1),
0x7C2C => char::from_u32(0x000033A5),
0x7C2D => char::from_u32(0x0000339D),
0x7C2E => char::from_u32(0x000033A0),
0x7C2F => char::from_u32(0x000033A4),
0x7C30 => char::from_u32(0x0001F100),
0x7C31 => char::from_u32(0x00002488),
0x7C32 => char::from_u32(0x00002489),
0x7C33 => char::from_u32(0x0000248A),
0x7C34 => char::from_u32(0x0000248B),
0x7C35 => char::from_u32(0x0000248C),
0x7C36 => char::from_u32(0x0000248D),
0x7C37 => char::from_u32(0x0000248E),
0x7C38 => char::from_u32(0x0000248F),
0x7C39 => char::from_u32(0x00002490),
0x7C3A => char::from_u32(0x0000E290),
0x7C3B => char::from_u32(0x0000E291),
0x7C3C => char::from_u32(0x0000E292),
0x7C3D => char::from_u32(0x0000E293),
0x7C3E => char::from_u32(0x0000E294),
0x7C3F => char::from_u32(0x0000E295),
0x7C40 => char::from_u32(0x0001F101),
0x7C41 => char::from_u32(0x0001F102),
0x7C42 => char::from_u32(0x0001F103),
0x7C43 => char::from_u32(0x0001F104),
0x7C44 => char::from_u32(0x0001F105),
0x7C45 => char::from_u32(0x0001F106),
0x7C46 => char::from_u32(0x0001F107),
0x7C47 => char::from_u32(0x0001F108),
0x7C48 => char::from_u32(0x0001F109),
0x7C49 => char::from_u32(0x0001F10A),
0x7C4A => char::from_u32(0x00003233),
0x7C4B => char::from_u32(0x00003236),
0x7C4C => char::from_u32(0x00003232),
0x7C4D => char::from_u32(0x00003231),
0x7C4E => char::from_u32(0x00003239),
0x7C4F => char::from_u32(0x00003244),
0x7C50 => char::from_u32(0x000025B6),
0x7C51 => char::from_u32(0x000025C0),
0x7C52 => char::from_u32(0x00003016),
0x7C53 => char::from_u32(0x00003017),
0x7C54 => char::from_u32(0x000027D0),
0x7C55 => char::from_u32(0x000000B2),
0x7C56 => char::from_u32(0x000000B3),
0x7C57 => char::from_u32(0x0001F12D),
0x7C58 => char::from_u32(0x0000E2A5),
0x7C59 => char::from_u32(0x0000E2A6),
0x7C5A => char::from_u32(0x0000E2A7),
0x7C5B => char::from_u32(0x0000E2A8),
0x7C5C => char::from_u32(0x0000E2A9),
0x7C5D => char::from_u32(0x0000E2AA),
0x7C5E => char::from_u32(0x0000E2AB),
0x7C5F => char::from_u32(0x0000E2AC),
0x7C60 => char::from_u32(0x0000E2AD),
0x7C61 => char::from_u32(0x0000E2AE),
0x7C62 => char::from_u32(0x0000E2AF),
0x7C63 => char::from_u32(0x0000E2B0),
0x7C64 => char::from_u32(0x0000E2B1),
0x7C65 => char::from_u32(0x0000E2B2),
0x7C66 => char::from_u32(0x0000E2B3),
0x7C67 => char::from_u32(0x0000E2B4),
0x7C68 => char::from_u32(0x0000E2B5),
0x7C69 => char::from_u32(0x0000E2B6),
0x7C6A => char::from_u32(0x0000E2B7),
0x7C6B => char::from_u32(0x0000E2B8),
0x7C6C => char::from_u32(0x0000E2B9),
0x7C6D => char::from_u32(0x0000E2BA),
0x7C6E => char::from_u32(0x0000E2BB),
0x7C6F => char::from_u32(0x0000E2BC),
0x7C70 => char::from_u32(0x0000E2BD),
0x7C71 => char::from_u32(0x0000E2BE),
0x7C72 => char::from_u32(0x0000E2BF),
0x7C73 => char::from_u32(0x0000E2C0),
0x7C74 => char::from_u32(0x0000E2C1),
0x7C75 => char::from_u32(0x0000E2C2),
0x7C76 => char::from_u32(0x0001F12C),
0x7C77 => char::from_u32(0x0001F12B),
0x7C78 => char::from_u32(0x00003247),
0x7C79 => char::from_u32(0x0001F190),
0x7C7A => char::from_u32(0x0001F226),
0x7C7B => char::from_u32(0x0000213B),
0x7D21 => char::from_u32(0x0000322A),
0x7D22 => char::from_u32(0x0000322B),
0x7D23 => char::from_u32(0x0000322C),
0x7D24 => char::from_u32(0x0000322D),
0x7D25 => char::from_u32(0x0000322E),
0x7D26 => char::from_u32(0x0000322F),
0x7D27 => char::from_u32(0x00003230),
0x7D28 => char::from_u32(0x00003237),
0x7D29 => char::from_u32(0x0000337E),
0x7D2A => char::from_u32(0x0000337D),
0x7D2B => char::from_u32(0x0000337C),
0x7D2C => char::from_u32(0x0000337B),
0x7D2D => char::from_u32(0x00002116),
0x7D2E => char::from_u32(0x00002121),
0x7D2F => char::from_u32(0x00003036),
0x7D30 => char::from_u32(0x000026BE),
0x7D31 => char::from_u32(0x0001F240),
0x7D32 => char::from_u32(0x0001F241),
0x7D33 => char::from_u32(0x0001F242),
0x7D34 => char::from_u32(0x0001F243),
0x7D35 => char::from_u32(0x0001F244),
0x7D36 => char::from_u32(0x0001F245),
0x7D37 => char::from_u32(0x0001F246),
0x7D38 => char::from_u32(0x0001F247),
0x7D39 => char::from_u32(0x0001F248),
0x7D3A => char::from_u32(0x0001F12A),
0x7D3B => char::from_u32(0x0001F227),
0x7D3C => char::from_u32(0x0001F228),
0x7D3D => char::from_u32(0x0001F229),
0x7D3E => char::from_u32(0x0001F214),
0x7D3F => char::from_u32(0x0001F22A),
0x7D40 => char::from_u32(0x0001F22B),
0x7D41 => char::from_u32(0x0001F22C),
0x7D42 => char::from_u32(0x0001F22D),
0x7D43 => char::from_u32(0x0001F22E),
0x7D44 => char::from_u32(0x0001F22F),
0x7D45 => char::from_u32(0x0001F230),
0x7D46 => char::from_u32(0x0001F231),
0x7D47 => char::from_u32(0x00002113),
0x7D48 => char::from_u32(0x0000338F),
0x7D49 => char::from_u32(0x00003390),
0x7D4A => char::from_u32(0x000033CA),
0x7D4B => char::from_u32(0x0000339E),
0x7D4C => char::from_u32(0x000033A2),
0x7D4D => char::from_u32(0x00003371),
0x7D50 => char::from_u32(0x000000BD),
0x7D51 => char::from_u32(0x00002189),
0x7D52 => char::from_u32(0x00002153),
0x7D53 => char::from_u32(0x00002154),
0x7D54 => char::from_u32(0x000000BC),
0x7D55 => char::from_u32(0x000000BE),
0x7D56 => char::from_u32(0x00002155),
0x7D57 => char::from_u32(0x00002156),
0x7D58 => char::from_u32(0x00002157),
0x7D59 => char::from_u32(0x00002158),
0x7D5A => char::from_u32(0x00002159),
0x7D5B => char::from_u32(0x0000215A),
0x7D5C => char::from_u32(0x00002150),
0x7D5D => char::from_u32(0x0000215B),
0x7D5E => char::from_u32(0x00002151),
0x7D5F => char::from_u32(0x00002152),
0x7D60 => char::from_u32(0x00002600),
0x7D61 => char::from_u32(0x00002601),
0x7D62 => char::from_u32(0x00002602),
0x7D63 => char::from_u32(0x000026C4),
0x7D64 => char::from_u32(0x00002616),
0x7D65 => char::from_u32(0x00002617),
0x7D66 => char::from_u32(0x000026C9),
0x7D67 => char::from_u32(0x000026CA),
0x7D68 => char::from_u32(0x00002666),
0x7D69 => char::from_u32(0x00002665),
0x7D6A => char::from_u32(0x00002663),
0x7D6B => char::from_u32(0x00002660),
0x7D6C => char::from_u32(0x000026CB),
0x7D6D => char::from_u32(0x00002A00),
0x7D6E => char::from_u32(0x0000203C),
0x7D6F => char::from_u32(0x00002049),
0x7D70 => char::from_u32(0x000026C5),
0x7D71 => char::from_u32(0x00002614),
0x7D72 => char::from_u32(0x000026C6),
0x7D73 => char::from_u32(0x00002603),
0x7D74 => char::from_u32(0x000026C7),
0x7D75 => char::from_u32(0x000026A1),
0x7D76 => char::from_u32(0x000026C8),
0x7D78 => char::from_u32(0x0000269E),
0x7D79 => char::from_u32(0x0000269F),
0x7D7A => char::from_u32(0x0000266C),
0x7D7B => char::from_u32(0x0000260E),
0x7E21 => char::from_u32(0x00002160),
0x7E22 => char::from_u32(0x00002161),
0x7E23 => char::from_u32(0x00002162),
0x7E24 => char::from_u32(0x00002163),
0x7E25 => char::from_u32(0x00002164),
0x7E26 => char::from_u32(0x00002165),
0x7E27 => char::from_u32(0x00002166),
0x7E28 => char::from_u32(0x00002167),
0x7E29 => char::from_u32(0x00002168),
0x7E2A => char::from_u32(0x00002169),
0x7E2B => char::from_u32(0x0000216A),
0x7E2C => char::from_u32(0x0000216B),
0x7E2D => char::from_u32(0x00002470),
0x7E2E => char::from_u32(0x00002471),
0x7E2F => char::from_u32(0x00002472),
0x7E30 => char::from_u32(0x00002473),
0x7E31 => char::from_u32(0x00002474),
0x7E32 => char::from_u32(0x00002475),
0x7E33 => char::from_u32(0x00002476),
0x7E34 => char::from_u32(0x00002477),
0x7E35 => char::from_u32(0x00002478),
0x7E36 => char::from_u32(0x00002479),
0x7E37 => char::from_u32(0x0000247A),
0x7E38 => char::from_u32(0x0000247B),
0x7E39 => char::from_u32(0x0000247C),
0x7E3A => char::from_u32(0x0000247D),
0x7E3B => char::from_u32(0x0000247E),
0x7E3C => char::from_u32(0x0000247F),
0x7E3D => char::from_u32(0x00003251),
0x7E3E => char::from_u32(0x00003252),
0x7E3F => char::from_u32(0x00003253),
0x7E40 => char::from_u32(0x00003254),
0x7E41 => char::from_u32(0x0001F110),
0x7E42 => char::from_u32(0x0001F111),
0x7E43 => char::from_u32(0x0001F112),
0x7E44 => char::from_u32(0x0001F113),
0x7E45 => char::from_u32(0x0001F114),
0x7E46 => char::from_u32(0x0001F115),
0x7E47 => char::from_u32(0x0001F116),
0x7E48 => char::from_u32(0x0001F117),
0x7E49 => char::from_u32(0x0001F118),
0x7E4A => char::from_u32(0x0001F119),
0x7E4B => char::from_u32(0x0001F11A),
0x7E4C => char::from_u32(0x0001F11B),
0x7E4D => char::from_u32(0x0001F11C),
0x7E4E => char::from_u32(0x0001F11D),
0x7E4F => char::from_u32(0x0001F11E),
0x7E50 => char::from_u32(0x0001F11F),
0x7E51 => char::from_u32(0x0001F120),
0x7E52 => char::from_u32(0x0001F121),
0x7E53 => char::from_u32(0x0001F122),
0x7E54 => char::from_u32(0x0001F123),
0x7E55 => char::from_u32(0x0001F124),
0x7E56 => char::from_u32(0x0001F125),
0x7E57 => char::from_u32(0x0001F126),
0x7E58 => char::from_u32(0x0001F127),
0x7E59 => char::from_u32(0x0001F128),
0x7E5A => char::from_u32(0x0001F129),
0x7E5B => char::from_u32(0x00003255),
0x7E5C => char::from_u32(0x00003256),
0x7E5D => char::from_u32(0x00003257),
0x7E5E => char::from_u32(0x00003258),
0x7E5F => char::from_u32(0x00003259),
0x7E60 => char::from_u32(0x0000325A),
0x7E61 => char::from_u32(0x00002460),
0x7E62 => char::from_u32(0x00002461),
0x7E63 => char::from_u32(0x00002462),
0x7E64 => char::from_u32(0x00002463),
0x7E65 => char::from_u32(0x00002464),
0x7E66 => char::from_u32(0x00002465),
0x7E67 => char::from_u32(0x00002466),
0x7E68 => char::from_u32(0x00002467),
0x7E69 => char::from_u32(0x00002468),
0x7E6A => char::from_u32(0x00002469),
0x7E6B => char::from_u32(0x0000246A),
0x7E6C => char::from_u32(0x0000246B),
0x7E6D => char::from_u32(0x0000246C),
0x7E6E => char::from_u32(0x0000246D),
0x7E6F => char::from_u32(0x0000246E),
0x7E70 => char::from_u32(0x0000246F),
0x7E71 => char::from_u32(0x00002776),
0x7E72 => char::from_u32(0x00002777),
0x7E73 => char::from_u32(0x00002778),
0x7E74 => char::from_u32(0x00002779),
0x7E75 => char::from_u32(0x0000277A),
0x7E76 => char::from_u32(0x0000277B),
0x7E77 => char::from_u32(0x0000277C),
0x7E78 => char::from_u32(0x0000277D),
0x7E79 => char::from_u32(0x0000277E),
0x7E7A => char::from_u32(0x0000277F),
0x7E7B => char::from_u32(0x000024EB),
0x7E7C => char::from_u32(0x000024EC),
0x7E7D => char::from_u32(0x0000325B),
0x7521 => char::from_u32(0x00003402),
0x7522 => char::from_u32(0x00020158),
0x7523 => char::from_u32(0x00004EFD),
0x7524 => char::from_u32(0x00004EFF),
0x7525 => char::from_u32(0x00004F9A),
0x7526 => char::from_u32(0x00004FC9),
0x7527 => char::from_u32(0x0000509C),
0x7528 => char::from_u32(0x0000511E),
0x7529 => char::from_u32(0x000051BC),
0x752A => char::from_u32(0x0000351F),
0x752B => char::from_u32(0x00005307),
0x752C => char::from_u32(0x00005361),
0x752D => char::from_u32(0x0000536C),
0x752E => char::from_u32(0x00008A79),
0x752F => char::from_u32(0x00020BB7),
0x7530 => char::from_u32(0x0000544D),
0x7531 => char::from_u32(0x00005496),
0x7532 => char::from_u32(0x0000549C),
0x7533 => char::from_u32(0x000054A9),
0x7534 => char::from_u32(0x0000550E),
0x7535 => char::from_u32(0x0000554A),
0x7536 => char::from_u32(0x00005672),
0x7537 => char::from_u32(0x000056E4),
0x7538 => char::from_u32(0x00005733),
0x7539 => char::from_u32(0x00005734),
0x753A => char::from_u32(0x0000FA10),
0x753B => char::from_u32(0x00005880),
0x753C => char::from_u32(0x000059E4),
0x753D => char::from_u32(0x00005A23),
0x753E => char::from_u32(0x00005A55),
0x753F => char::from_u32(0x00005BEC),
0x7540 => char::from_u32(0x0000FA11),
0x7541 => char::from_u32(0x000037E2),
0x7542 => char::from_u32(0x00005EAC),
0x7543 => char::from_u32(0x00005F34),
0x7544 => char::from_u32(0x00005F45),
0x7545 => char::from_u32(0x00005FB7),
0x7546 => char::from_u32(0x00006017),
0x7547 => char::from_u32(0x0000FA6B),
0x7548 => char::from_u32(0x00006130),
0x7549 => char::from_u32(0x00006624),
0x754A => char::from_u32(0x000066C8),
0x754B => char::from_u32(0x000066D9),
0x754C => char::from_u32(0x000066FA),
0x754D => char::from_u32(0x000066FB),
0x754E => char::from_u32(0x00006852),
0x754F => char::from_u32(0x00009FC4),
0x7550 => char::from_u32(0x00006911),
0x7551 => char::from_u32(0x0000693B),
0x7552 => char::from_u32(0x00006A45),
0x7553 => char::from_u32(0x00006A91),
0x7554 => char::from_u32(0x00006ADB),
0x7555 => char::from_u32(0x000233CC),
0x7556 => char::from_u32(0x000233FE),
0x7557 => char::from_u32(0x000235C4),
0x7558 => char::from_u32(0x00006BF1),
0x7559 => char::from_u32(0x00006CE0),
0x755A => char::from_u32(0x00006D2E),
0x755B => char::from_u32(0x0000FA45),
0x755C => char::from_u32(0x00006DBF),
0x755D => char::from_u32(0x00006DCA),
0x755E => char::from_u32(0x00006DF8),
0x755F => char::from_u32(0x0000FA46),
0x7560 => char::from_u32(0x00006F5E),
0x7561 => char::from_u32(0x00006FF9),
0x7562 => char::from_u32(0x00007064),
0x7563 => char::from_u32(0x0000FA6C),
0x7564 => char::from_u32(0x000242EE),
0x7565 => char::from_u32(0x00007147),
0x7566 => char::from_u32(0x000071C1),
0x7567 => char::from_u32(0x00007200),
0x7568 => char::from_u32(0x0000739F),
0x7569 => char::from_u32(0x000073A8),
0x756A => char::from_u32(0x000073C9),
0x756B => char::from_u32(0x000073D6),
0x756C => char::from_u32(0x0000741B),
0x756D => char::from_u32(0x00007421),
0x756E => char::from_u32(0x0000FA4A),
0x756F => char::from_u32(0x00007426),
0x7570 => char::from_u32(0x0000742A),
0x7571 => char::from_u32(0x0000742C),
0x7572 => char::from_u32(0x00007439),
0x7573 => char::from_u32(0x0000744B),
0x7574 => char::from_u32(0x00003EDA),
0x7575 => char::from_u32(0x00007575),
0x7576 => char::from_u32(0x00007581),
0x7577 => char::from_u32(0x00007772),
0x7578 => char::from_u32(0x00004093),
0x7579 => char::from_u32(0x000078C8),
0x757A => char::from_u32(0x000078E0),
0x757B => char::from_u32(0x00007947),
0x757C => char::from_u32(0x000079AE),
0x757D => char::from_u32(0x00009FC6),
0x757E => char::from_u32(0x00004103),
0x7621 => char::from_u32(0x00009FC5),
0x7622 => char::from_u32(0x000079DA),
0x7623 => char::from_u32(0x00007A1E),
0x7624 => char::from_u32(0x00007B7F),
0x7625 => char::from_u32(0x00007C31),
0x7626 => char::from_u32(0x00004264),
0x7627 => char::from_u32(0x00007D8B),
0x7628 => char::from_u32(0x00007FA1),
0x7629 => char::from_u32(0x00008118),
0x762A => char::from_u32(0x0000813A),
0x762B => char::from_u32(0x0000FA6D),
0x762C => char::from_u32(0x000082AE),
0x762D => char::from_u32(0x0000845B),
0x762E => char::from_u32(0x000084DC),
0x762F => char::from_u32(0x000084EC),
0x7630 => char::from_u32(0x00008559),
0x7631 => char::from_u32(0x000085CE),
0x7632 => char::from_u32(0x00008755),
0x7633 => char::from_u32(0x000087EC),
0x7634 => char::from_u32(0x0000880B),
0x7635 => char::from_u32(0x000088F5),
0x7636 => char::from_u32(0x000089D2),
0x7637 => char::from_u32(0x00008AF6),
0x7638 => char::from_u32(0x00008DCE),
0x7639 => char::from_u32(0x00008FBB),
0x763A => char::from_u32(0x00008FF6),
0x763B => char::from_u32(0x000090DD),
0x763C => char::from_u32(0x00009127),
0x763D => char::from_u32(0x0000912D),
0x763E => char::from_u32(0x000091B2),
0x763F => char::from_u32(0x00009233),
0x7640 => char::from_u32(0x00009288),
0x7641 => char::from_u32(0x00009321),
0x7642 => char::from_u32(0x00009348),
0x7643 => char::from_u32(0x00009592),
0x7644 => char::from_u32(0x000096DE),
0x7645 => char::from_u32(0x00009903),
0x7646 => char::from_u32(0x00009940),
0x7647 => char::from_u32(0x00009AD9),
0x7648 => char::from_u32(0x00009BD6),
0x7649 => char::from_u32(0x00009DD7),
0x764A => char::from_u32(0x00009EB4),
0x764B => char::from_u32(0x00009EB5),
_ => None,
}
}

View File

@@ -5,12 +5,13 @@
use std::collections::{BTreeMap, HashMap, HashSet}; use std::collections::{BTreeMap, HashMap, HashSet};
use encoding_rs::EUC_JP;
use mirakurun_types::{ use mirakurun_types::{
Program, ProgramGenre, ProgramVideo, ProgramVideoResolution, ProgramVideoType, program_id, Program, ProgramAudio, ProgramGenre, ProgramRelatedItem, ProgramRelatedItemType, ProgramSeries,
ProgramVideo, ProgramVideoResolution, ProgramVideoType, program_id,
}; };
use crate::{ use crate::{
arib::decode as decode_arib,
filter::SectionAssembler, filter::SectionAssembler,
ts::{Packet, PacketFramer, crc32_mpeg2}, ts::{Packet, PacketFramer, crc32_mpeg2},
}; };
@@ -22,6 +23,7 @@ pub struct EitCollector {
framer: PacketFramer, framer: PacketFramer,
assembler: SectionAssembler, assembler: SectionAssembler,
programs: HashMap<u64, Program>, programs: HashMap<u64, Program>,
states: HashMap<u64, ProgramState>,
sections_seen: HashSet<(u8, u16, u8, u8)>, sections_seen: HashSet<(u8, u16, u8, u8)>,
packet_count: u64, packet_count: u64,
section_count: u64, section_count: u64,
@@ -40,7 +42,7 @@ impl EitCollector {
continue; continue;
} }
for section in self.assembler.push(packet) { 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(( && self.sections_seen.insert((
section[0], section[0],
u16::from_be_bytes([section[3], section[4]]), 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 if section.len() < 18
|| !matches!(section[0], 0x4e..=0x6f) || !matches!(section[0], 0x4e..=0x6f)
|| section[5] & 0x01 == 0 || section[5] & 0x01 == 0
@@ -80,10 +136,14 @@ fn parse_eit_section(section: &[u8], programs: &mut HashMap<u64, Program>) -> bo
{ {
return false; return false;
} }
let table_id = section[0];
let version = (section[5] >> 1) & 0x1f;
let is_present_following = matches!(section[0], 0x4e | 0x4f); let is_present_following = matches!(section[0], 0x4e | 0x4f);
if is_present_following && section[6] > 1 { if is_present_following && section[6] > 1 {
return false; 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 service_id = u16::from_be_bytes([section[3], section[4]]);
let network_id = u16::from_be_bytes([section[10], section[11]]); let network_id = u16::from_be_bytes([section[10], section[11]]);
let Some(end) = section.len().checked_sub(4) else { let Some(end) = section.len().checked_sub(4) else {
@@ -105,14 +165,18 @@ fn parse_eit_section(section: &[u8], programs: &mut HashMap<u64, Program>) -> bo
if descriptor_end > end { if descriptor_end > end {
return false; return false;
} }
if let Some(start_at) = decode_mjd_time(start) {
let id = program_id(network_id, service_id, event_id); 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 { let program = programs.entry(id).or_insert_with(|| Program {
id, id,
event_id, event_id,
service_id, service_id,
network_id, network_id,
start_at, start_at: start_at.unwrap_or_default(),
duration: decode_bcd_duration(duration).unwrap_or(1), duration: decode_bcd_duration(duration).unwrap_or(1),
is_free: section[offset + 10] & 0x10 == 0, is_free: section[offset + 10] & 0x10 == 0,
name: None, name: None,
@@ -124,17 +188,41 @@ fn parse_eit_section(section: &[u8], programs: &mut HashMap<u64, Program>) -> bo
extended: None, extended: None,
related_items: 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.start_at = start_at;
program.duration = decode_bcd_duration(duration).unwrap_or(1); program.duration = decode_bcd_duration(duration).unwrap_or(1);
program.is_free = section[offset + 10] & 0x10 == 0; program.is_free = section[offset + 10] & 0x10 == 0;
parse_descriptors(&section[offset + 12..descriptor_end], program);
} }
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; offset = descriptor_end;
} }
true 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 { while descriptors.len() >= 2 {
let length = usize::from(descriptors[1]); let length = usize::from(descriptors[1]);
let Some(end) = 2usize.checked_add(length) else { 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]; let body = &descriptors[2..end];
match descriptors[0] { match descriptors[0] {
0x4d => parse_short_event(body, program), 0x4d if state.short.should_update(table_id, version) => {
0x4e => parse_extended_event(body, program), if let Some((name, description)) = parse_short_event(body) {
0x50 => parse_video_component(body, program), state.short.mark(table_id, version);
0x54 => parse_content(body, program), 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..]; 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 { if body.len() < 5 {
return; return None;
} }
let name_length = usize::from(body[3]); let name_length = usize::from(body[3]);
let Some(text_length_offset) = 4usize.checked_add(name_length) else { let text_length_offset = 4usize.checked_add(name_length)?;
return;
};
if text_length_offset >= body.len() { if text_length_offset >= body.len() {
return; return None;
} }
let text_length = usize::from(body[text_length_offset]); let text_length = usize::from(body[text_length_offset]);
let text_start = text_length_offset + 1; let text_start = text_length_offset + 1;
let Some(text_end) = text_start.checked_add(text_length) else { let text_end = text_start.checked_add(text_length)?;
return;
};
if text_end > body.len() { if text_end > body.len() {
return; return None;
} }
program.name = Some(decode_arib(&body[4..text_length_offset])); Some((
program.description = Some(decode_arib(&body[text_start..text_end])); 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 { if body.len() < 6 {
return; 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 items_length = usize::from(body[4]);
let Some(items_end) = 5usize.checked_add(items_length) else { let Some(items_end) = 5usize.checked_add(items_length) else {
return; return;
@@ -189,44 +315,78 @@ fn parse_extended_event(body: &[u8], program: &mut Program) {
if items_end > body.len() { if items_end > body.len() {
return; return;
} }
let extended = program.extended.get_or_insert_with(BTreeMap::new); let Some(items) = parse_extended_items(&body[5..items_end]) else {
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; return;
}; };
if description_end >= items.len() { state.parts.entry(descriptor_number).or_insert(items);
let Some(last) = state.last_descriptor else {
return; 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 description_end = 1usize.checked_add(description_length)?;
if description_end >= items.len() {
return None;
} }
let item_length = usize::from(items[description_end]); let item_length = usize::from(items[description_end]);
let item_start = description_end + 1; let item_start = description_end + 1;
let Some(item_end) = item_start.checked_add(item_length) else { let item_end = item_start.checked_add(item_length)?;
return;
};
if item_end > items.len() { if item_end > items.len() {
return; return None;
} }
let decoded_key = decode_arib(&items[1..description_end]); parsed.push(ExtendedItem {
let key = if decoded_key.is_empty() { description: items[1..description_end].to_vec(),
previous_key.clone() value: items[item_start..item_end].to_vec(),
} 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..]; 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 { if body.len() < 2 {
return; return None;
} }
let stream_content = body[0] & 0x0f; let stream_content = body[0] & 0x0f;
let component_type = body[1]; let component_type = body[1];
@@ -234,7 +394,7 @@ fn parse_video_component(body: &[u8], program: &mut Program) {
1 => ProgramVideoType::Mpeg2, 1 => ProgramVideoType::Mpeg2,
5 => ProgramVideoType::H264, 5 => ProgramVideoType::H264,
9 => ProgramVideoType::H265, 9 => ProgramVideoType::H265,
_ => return, _ => return None,
}; };
let resolution = match component_type { let resolution = match component_type {
0x01..=0x04 => ProgramVideoResolution::I480, 0x01..=0x04 => ProgramVideoResolution::I480,
@@ -245,17 +405,17 @@ fn parse_video_component(body: &[u8], program: &mut Program) {
0xc1..=0xc4 => ProgramVideoResolution::P720, 0xc1..=0xc4 => ProgramVideoResolution::P720,
0xd1..=0xd4 => ProgramVideoResolution::P240, 0xd1..=0xd4 => ProgramVideoResolution::P240,
0xe1..=0xe4 => ProgramVideoResolution::P1080, 0xe1..=0xe4 => ProgramVideoResolution::P1080,
_ => return, _ => return None,
}; };
program.video = Some(ProgramVideo { Some(ProgramVideo {
video_type, video_type,
resolution, resolution,
stream_content, stream_content,
component_type, component_type,
}); })
} }
fn parse_content(body: &[u8], program: &mut Program) { fn parse_content(body: &[u8]) -> Option<Vec<ProgramGenre>> {
let genres = body let genres = body
.chunks_exact(2) .chunks_exact(2)
.map(|content| ProgramGenre { .map(|content| ProgramGenre {
@@ -265,9 +425,133 @@ fn parse_content(body: &[u8], program: &mut Program) {
un2: content[1] & 0x0f, un2: content[1] & 0x0f,
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
if !genres.is_empty() { (!genres.is_empty()).then_some(genres)
program.genres = 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> { 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() 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> { fn decode_bcd_duration(value: &[u8]) -> Option<u64> {
if value.len() != 3 || value.iter().all(|byte| *byte == 0xff) { if value.len() != 3 || value.iter().all(|byte| *byte == 0xff) {
return None; return None;
@@ -312,152 +607,12 @@ fn decode_bcd(value: u8) -> Option<u8> {
(high <= 9 && low <= 9).then_some(high * 10 + low) (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)] #[cfg(test)]
mod tests { mod tests {
use crate::ts::{PACKET_SIZE, crc32_mpeg2}; 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] #[test]
fn decodes_common_arib_character_sets() { fn decodes_common_arib_character_sets() {
@@ -506,4 +661,108 @@ mod tests {
assert_eq!(programs[0].duration, 3_600_000); assert_eq!(programs[0].duration, 3_600_000);
assert!(programs[0].is_free); 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
}
} }

View File

@@ -3,10 +3,12 @@
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
mod arib;
pub mod config; pub mod config;
pub mod epg; pub mod epg;
pub mod error; pub mod error;
pub mod filter; pub mod filter;
pub mod logo;
pub mod persistence; pub mod persistence;
pub mod service; pub mod service;
pub mod ts; pub mod ts;

View File

@@ -0,0 +1,637 @@
// 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::{HashMap, HashSet};
use crate::{
filter::SectionAssembler,
ts::{Packet, PacketFramer, crc32_mpeg2},
};
const PID_CDT: u16 = 0x0029;
const TABLE_ID_CDT: u8 = 0xc8;
const TABLE_ID_DII: u8 = 0x3b;
const TABLE_ID_DDB: u8 = 0x3c;
const MAX_LOGO_MODULE_SIZE: usize = 64 * 1024 * 1024;
/// A decoded type-5 broadcaster logo and the services which reference it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LogoData {
pub network_id: u16,
pub logo_id: u16,
pub services: Vec<(u16, u16)>,
pub png: Vec<u8>,
}
#[derive(Debug)]
struct Download {
module_id: u16,
module_version: u8,
block_size: usize,
bytes: Vec<u8>,
received_blocks: HashSet<u16>,
received_bytes: usize,
}
/// Collects terrestrial CDT and satellite DSM-CC logo data from a transport stream.
#[derive(Debug, Default)]
pub struct LogoCollector {
framer: PacketFramer,
cdt_assembler: SectionAssembler,
dsmcc_assemblers: HashMap<u16, SectionAssembler>,
downloads: HashMap<u32, Download>,
logos: HashMap<(u16, u16), LogoData>,
}
impl LogoCollector {
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 {
let Ok(packet) = Packet::new(&packet_bytes) else {
continue;
};
if packet.pid() == PID_CDT {
for section in self.cdt_assembler.push(packet) {
if let Some(logo) = parse_cdt(&section) {
self.insert_logo(logo);
}
}
continue;
}
let pid = packet.pid();
if !self.dsmcc_assemblers.contains_key(&pid) && !is_dsmcc_start(packet) {
continue;
}
let sections = self.dsmcc_assemblers.entry(pid).or_default().push(packet);
for section in sections {
self.push_dsmcc(&section);
}
}
}
#[must_use]
pub fn has_logo(&self) -> bool {
!self.logos.is_empty()
}
#[must_use]
pub fn contains(&self, network_id: u16, logo_id: u16) -> bool {
self.logos.contains_key(&(network_id, logo_id))
}
#[must_use]
pub fn into_logos(self) -> Vec<LogoData> {
let mut logos = self.logos.into_values().collect::<Vec<_>>();
logos.sort_by_key(|logo| (logo.network_id, logo.logo_id));
logos
}
fn push_dsmcc(&mut self, section: &[u8]) {
if section.len() < 12 || crc32_mpeg2(section) != 0 || section[5] & 0x01 == 0 {
return;
}
match section[0] {
TABLE_ID_DII => {
if let Some((download_id, download)) = parse_dii(section) {
self.downloads.entry(download_id).or_insert(download);
}
}
TABLE_ID_DDB => {
let Some(block) = parse_ddb(section) else {
return;
};
let Some(download) = self.downloads.get_mut(&block.download_id) else {
return;
};
if download.module_id != block.module_id
|| download.module_version != block.module_version
|| !download.received_blocks.insert(block.block_number)
{
return;
}
let offset = download
.block_size
.saturating_mul(usize::from(block.block_number));
if offset >= download.bytes.len() {
return;
}
let copy_len = block.data.len().min(download.bytes.len() - offset);
download.bytes[offset..offset + copy_len].copy_from_slice(&block.data[..copy_len]);
download.received_bytes = download.received_bytes.saturating_add(copy_len);
if download.received_bytes < download.bytes.len() {
return;
}
let Some(download) = self.downloads.remove(&block.download_id) else {
return;
};
for logo in parse_logo_module(&download.bytes) {
self.insert_logo(logo);
}
}
_ => {}
}
}
fn insert_logo(&mut self, mut logo: LogoData) {
let key = (logo.network_id, logo.logo_id);
if let Some(previous) = self.logos.remove(&key) {
logo.services.extend(previous.services);
logo.services.sort_unstable();
logo.services.dedup();
}
self.logos.insert(key, logo);
}
}
fn is_dsmcc_start(packet: Packet<'_>) -> bool {
if !packet.payload_unit_start() {
return false;
}
let Some(payload) = packet.payload() else {
return false;
};
let Some(table_offset) = payload
.first()
.copied()
.map(usize::from)
.and_then(|pointer| 1usize.checked_add(pointer))
else {
return false;
};
payload
.get(table_offset)
.is_some_and(|table_id| matches!(*table_id, TABLE_ID_DII | TABLE_ID_DDB))
}
fn parse_cdt(section: &[u8]) -> Option<LogoData> {
if section.len() < 24
|| section[0] != TABLE_ID_CDT
|| section[5] & 0x01 == 0
|| crc32_mpeg2(section) != 0
|| section[10] != 0x01
{
return None;
}
let network_id = u16::from_be_bytes([section[8], section[9]]);
let descriptors_length = (usize::from(section[11] & 0x0f) << 8) | usize::from(section[12]);
let module_start = 13usize.checked_add(descriptors_length)?;
let module_end = section.len().checked_sub(4)?;
let module = section.get(module_start..module_end)?;
if module.len() < 7 || module[0] != 0x05 {
return None;
}
let logo_id = (u16::from(module[1] & 0x01) << 8) | u16::from(module[2]);
let data_size = usize::from(u16::from_be_bytes([module[5], module[6]]));
let data_end = 7usize.checked_add(data_size)?;
let png = decode_indexed_png(module.get(7..data_end)?)?;
Some(LogoData {
network_id,
logo_id,
services: Vec::new(),
png,
})
}
fn parse_dii(section: &[u8]) -> Option<(u32, Download)> {
if section.first().copied() != Some(TABLE_ID_DII) {
return None;
}
let body = section.get(8..section.len().checked_sub(4)?)?;
if body.len() < 34 || u16::from_be_bytes([body[2], body[3]]) != 0x1002 {
return None;
}
let adaptation_length = usize::from(body[9]);
let mut offset = 12usize.checked_add(adaptation_length)?;
let download_id = read_u32(body, offset)?;
offset += 4;
let block_size = usize::from(read_u16(body, offset)?);
offset += 12;
let compatibility_length = usize::from(read_u16(body, offset)?);
offset = offset.checked_add(2)?.checked_add(compatibility_length)?;
let module_count = usize::from(read_u16(body, offset)?);
offset += 2;
for _ in 0..module_count {
let module_id = read_u16(body, offset)?;
let module_size = usize::try_from(read_u32(body, offset + 2)?).ok()?;
let module_version = *body.get(offset + 6)?;
let info_length = usize::from(*body.get(offset + 7)?);
let info_start = offset + 8;
let info_end = info_start.checked_add(info_length)?;
let info = body.get(info_start..info_end)?;
if module_size <= MAX_LOGO_MODULE_SIZE && is_logo_module(info) {
return Some((
download_id,
Download {
module_id,
module_version,
block_size: block_size.max(1),
bytes: vec![0; module_size],
received_blocks: HashSet::new(),
received_bytes: 0,
},
));
}
offset = info_end;
}
None
}
fn is_logo_module(mut descriptors: &[u8]) -> bool {
while descriptors.len() >= 2 {
let end = 2usize.saturating_add(usize::from(descriptors[1]));
if end > descriptors.len() {
return false;
}
if descriptors[0] == 0x02 && matches!(&descriptors[2..end], b"LOGO-05" | b"CS_LOGO-05") {
return true;
}
descriptors = &descriptors[end..];
}
false
}
struct DownloadBlock<'a> {
download_id: u32,
module_id: u16,
module_version: u8,
block_number: u16,
data: &'a [u8],
}
fn parse_ddb(section: &[u8]) -> Option<DownloadBlock<'_>> {
if section.first().copied() != Some(TABLE_ID_DDB) {
return None;
}
let body = section.get(8..section.len().checked_sub(4)?)?;
if body.len() < 18 || u16::from_be_bytes([body[2], body[3]]) != 0x1003 {
return None;
}
let download_id = read_u32(body, 4)?;
let adaptation_length = usize::from(body[9]);
let offset = 12usize.checked_add(adaptation_length)?;
Some(DownloadBlock {
download_id,
module_id: read_u16(body, offset)?,
module_version: *body.get(offset + 2)?,
block_number: read_u16(body, offset + 4)?,
data: body.get(offset + 6..)?,
})
}
fn parse_logo_module(module: &[u8]) -> Vec<LogoData> {
if module.len() < 3 || module[0] != 0x05 {
return Vec::new();
}
let count = usize::from(u16::from_be_bytes([module[1], module[2]]));
let mut offset = 3;
let mut output = Vec::new();
for _ in 0..count {
let Some(logo_bits) = module.get(offset..offset + 2) else {
break;
};
let logo_id = (u16::from(logo_bits[0] & 0x01) << 8) | u16::from(logo_bits[1]);
let Some(&service_count) = module.get(offset + 2) else {
break;
};
offset += 3;
let mut services = Vec::new();
for _ in 0..service_count {
let Some(service) = module.get(offset..offset + 6) else {
return output;
};
services.push((
u16::from_be_bytes([service[0], service[1]]),
u16::from_be_bytes([service[4], service[5]]),
));
offset += 6;
}
let Some(data_size) = module.get(offset..offset + 2) else {
break;
};
let data_size = usize::from(u16::from_be_bytes([data_size[0], data_size[1]]));
offset += 2;
let Some(data) = module.get(offset..offset.saturating_add(data_size)) else {
break;
};
offset += data_size;
let Some(png) = decode_indexed_png(data) else {
continue;
};
for network_id in services
.iter()
.map(|(network_id, _)| *network_id)
.collect::<HashSet<_>>()
{
output.push(LogoData {
network_id,
logo_id,
services: services
.iter()
.copied()
.filter(|(service_network_id, _)| *service_network_id == network_id)
.collect(),
png: png.clone(),
});
}
}
output
}
fn decode_indexed_png(data: &[u8]) -> Option<Vec<u8>> {
const PNG_SIGNATURE: &[u8; 8] = b"\x89PNG\r\n\x1a\n";
if data.len() < 33 || data.get(..8)? != PNG_SIGNATURE || data.get(25).copied()? != 3 {
return None;
}
let palette = logo_palette();
let mut plte = Vec::with_capacity(palette.len() * 3);
let mut trns = Vec::with_capacity(palette.len());
for [red, green, blue, alpha] in palette {
plte.extend_from_slice(&[red, green, blue]);
trns.push(alpha);
}
let plte = png_chunk(*b"PLTE", &plte);
let trns = png_chunk(*b"tRNS", &trns);
let mut png = Vec::with_capacity(data.len() + plte.len() + trns.len());
png.extend_from_slice(&data[..33]);
png.extend_from_slice(&plte);
png.extend_from_slice(&trns);
png.extend_from_slice(&data[33..]);
Some(png)
}
fn png_chunk(kind: [u8; 4], data: &[u8]) -> Vec<u8> {
let mut chunk = Vec::with_capacity(12 + data.len());
chunk.extend_from_slice(&u32::try_from(data.len()).unwrap_or(u32::MAX).to_be_bytes());
chunk.extend_from_slice(&kind);
chunk.extend_from_slice(data);
let checksum = crc32_ieee(&chunk[4..]);
chunk.extend_from_slice(&checksum.to_be_bytes());
chunk
}
fn crc32_ieee(data: &[u8]) -> u32 {
let mut crc = u32::MAX;
for byte in data {
crc ^= u32::from(*byte);
for _ in 0..8 {
crc = (crc >> 1) ^ (0xedb8_8320 & 0u32.wrapping_sub(crc & 1));
}
}
!crc
}
fn logo_palette() -> Vec<[u8; 4]> {
const COLORS: [[u8; 3]; 65] = [
[0, 0, 0],
[255, 0, 0],
[0, 255, 0],
[255, 255, 0],
[0, 0, 255],
[255, 0, 255],
[0, 255, 255],
[255, 255, 255],
[0, 0, 0],
[170, 0, 0],
[0, 170, 0],
[170, 170, 0],
[0, 0, 170],
[170, 0, 170],
[0, 170, 170],
[170, 170, 170],
[0, 0, 85],
[0, 85, 0],
[0, 85, 85],
[0, 85, 170],
[0, 85, 255],
[0, 170, 85],
[0, 170, 255],
[0, 255, 85],
[0, 255, 170],
[85, 0, 0],
[85, 0, 85],
[85, 0, 170],
[85, 0, 255],
[85, 85, 0],
[85, 85, 85],
[85, 85, 170],
[85, 85, 255],
[85, 170, 0],
[85, 170, 85],
[85, 170, 170],
[85, 170, 255],
[85, 255, 0],
[85, 255, 85],
[85, 255, 170],
[85, 255, 255],
[170, 0, 85],
[170, 0, 255],
[170, 85, 0],
[170, 85, 85],
[170, 85, 170],
[170, 85, 255],
[170, 170, 85],
[170, 170, 255],
[170, 255, 0],
[170, 255, 85],
[170, 255, 170],
[170, 255, 255],
[255, 0, 85],
[255, 0, 255],
[255, 85, 0],
[255, 85, 85],
[255, 85, 170],
[255, 85, 255],
[255, 170, 0],
[255, 170, 85],
[255, 170, 170],
[255, 170, 255],
[255, 255, 85],
[255, 255, 255],
];
let mut palette = Vec::with_capacity(128);
palette.extend(COLORS.into_iter().enumerate().map(|(index, color)| {
[
color[0],
color[1],
color[2],
if index == 8 { 0 } else { 255 },
]
}));
palette.extend(
COLORS
.into_iter()
.enumerate()
.filter(|(index, _)| *index != 8)
.map(|(_, color)| [color[0], color[1], color[2], 128]),
);
palette
}
fn read_u16(bytes: &[u8], offset: usize) -> Option<u16> {
Some(u16::from_be_bytes([
*bytes.get(offset)?,
*bytes.get(offset + 1)?,
]))
}
fn read_u32(bytes: &[u8], offset: usize) -> Option<u32> {
Some(u32::from_be_bytes([
*bytes.get(offset)?,
*bytes.get(offset + 1)?,
*bytes.get(offset + 2)?,
*bytes.get(offset + 3)?,
]))
}
#[cfg(test)]
mod tests {
use crate::ts::{PACKET_SIZE, crc32_mpeg2};
use super::{LogoCollector, decode_indexed_png, parse_logo_module};
fn indexed_png() -> Vec<u8> {
let mut png = vec![0; 33];
png[..8].copy_from_slice(b"\x89PNG\r\n\x1a\n");
png[8..12].copy_from_slice(&13u32.to_be_bytes());
png[12..16].copy_from_slice(b"IHDR");
png[25] = 3;
png.extend_from_slice(&[0, 0, 0, 0, b'I', b'E', b'N', b'D', 0, 0, 0, 0]);
png
}
fn finish_section(mut section: Vec<u8>) -> Vec<u8> {
let section_length = section.len() - 3 + 4;
section[1] = 0xf0 | 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());
section
}
fn logo_module() -> Vec<u8> {
let source = indexed_png();
let mut module = vec![
0x05, 0x00, 0x01, 0x00, 0x09, 0x01, 0x00, 0x04, 0x00, 0x01, 0x00, 0x65,
];
module.extend_from_slice(
&u16::try_from(source.len())
.expect("logo data length")
.to_be_bytes(),
);
module.extend_from_slice(&source);
module
}
#[test]
fn injects_arib_palette_into_indexed_png() {
let decoded = decode_indexed_png(&indexed_png()).expect("decode PNG");
assert_eq!(&decoded[37..41], b"PLTE");
assert_eq!(&decoded[436..440], b"tRNS");
assert_eq!(decoded.len(), indexed_png().len() + 399 + 141);
}
#[test]
fn collects_type_five_logo_from_cdt() {
let source = indexed_png();
let mut section = vec![
0xc8, 0xf0, 0x00, 0x00, 0x01, 0xc1, 0x00, 0x00, 0x7f, 0xf0, 0x01, 0xf0, 0x00, 0x05,
0x00, 0x07, 0x00, 0x01,
];
section.extend_from_slice(
&u16::try_from(source.len())
.expect("logo data length")
.to_be_bytes(),
);
section.extend_from_slice(&source);
let section = finish_section(section);
let mut packet = [0xff; PACKET_SIZE];
packet[0] = 0x47;
packet[1] = 0x40;
packet[2] = 0x29;
packet[3] = 0x10;
packet[4] = 0;
packet[5..5 + section.len()].copy_from_slice(&section);
let mut collector = LogoCollector::default();
collector.push(&packet);
let logos = collector.into_logos();
assert_eq!(logos.len(), 1);
assert_eq!(logos[0].network_id, 0x7ff0);
assert_eq!(logos[0].logo_id, 7);
assert_eq!(&logos[0].png[37..41], b"PLTE");
}
#[test]
fn parses_satellite_logo_service_associations() {
let logos = parse_logo_module(&logo_module());
assert_eq!(logos.len(), 1);
assert_eq!(logos[0].network_id, 4);
assert_eq!(logos[0].logo_id, 9);
assert_eq!(logos[0].services, vec![(4, 101)]);
}
#[test]
fn reassembles_satellite_logo_from_dii_and_ddb() {
let module = logo_module();
let download_id = 0x1122_3344_u32;
let mut dii_body = vec![
0x11, 0x03, 0x10, 0x02, 0x00, 0x00, 0x00, 0x01, 0xff, 0x00, 0x00, 0x00,
];
dii_body.extend_from_slice(&download_id.to_be_bytes());
dii_body.extend_from_slice(&4066_u16.to_be_bytes());
dii_body.extend_from_slice(&[0, 0]);
dii_body.extend_from_slice(&[0; 8]);
dii_body.extend_from_slice(&0_u16.to_be_bytes());
dii_body.extend_from_slice(&1_u16.to_be_bytes());
dii_body.extend_from_slice(&1_u16.to_be_bytes());
dii_body.extend_from_slice(
&u32::try_from(module.len())
.expect("module size")
.to_be_bytes(),
);
dii_body.extend_from_slice(&[1, 9, 0x02, 7]);
dii_body.extend_from_slice(b"LOGO-05");
dii_body.extend_from_slice(&0_u16.to_be_bytes());
let mut dii = vec![0x3b, 0xf0, 0x00, 0x00, 0x00, 0xc1, 0x00, 0x00];
dii.extend_from_slice(&dii_body);
let dii = finish_section(dii);
let message_length = u16::try_from(module.len() + 6).expect("DDB message size");
let mut ddb_body = vec![
0x11,
0x03,
0x10,
0x03,
0x11,
0x22,
0x33,
0x44,
0xff,
0x00,
(message_length >> 8) as u8,
(message_length & 0xff) as u8,
0x00,
0x01,
0x01,
0xff,
0x00,
0x00,
];
ddb_body.extend_from_slice(&module);
let mut ddb = vec![0x3c, 0xf0, 0x00, 0x00, 0x00, 0xc1, 0x00, 0x00];
ddb.extend_from_slice(&ddb_body);
let ddb = finish_section(ddb);
let mut collector = LogoCollector::default();
collector.push_dsmcc(&dii);
collector.push_dsmcc(&ddb);
let logos = collector.into_logos();
assert_eq!(logos.len(), 1);
assert_eq!(logos[0].network_id, 4);
assert_eq!(logos[0].logo_id, 9);
}
}

View File

@@ -8,22 +8,26 @@ use std::collections::{HashMap, HashSet};
use mirakurun_types::{Channel, ConfigChannel, Service, service_item_id}; use mirakurun_types::{Channel, ConfigChannel, Service, service_item_id};
use crate::{ use crate::{
epg::decode_arib, arib::decode as decode_arib,
filter::SectionAssembler, filter::SectionAssembler,
ts::{Packet, PacketFramer, crc32_mpeg2}, ts::{Packet, PacketFramer, crc32_mpeg2},
}; };
const PID_SDT: u16 = 0x0011; const PID_SDT: u16 = 0x0011;
const PID_NIT: u16 = 0x0010;
const TABLE_ID_NIT_ACTUAL: u8 = 0x40;
const TABLE_ID_SDT_ACTUAL: u8 = 0x42; const TABLE_ID_SDT_ACTUAL: u8 = 0x42;
#[derive(Debug)] #[derive(Debug)]
pub struct ServiceCollector { pub struct ServiceCollector {
channel: ConfigChannel, channel: ConfigChannel,
framer: PacketFramer, framer: PacketFramer,
assembler: SectionAssembler, sdt_assembler: SectionAssembler,
nit_assembler: SectionAssembler,
services: HashMap<u64, Service>, services: HashMap<u64, Service>,
sections_seen: HashSet<(u8, u8)>, sections_seen: HashSet<(u8, u8)>,
last_section: Option<u8>, last_section: Option<u8>,
network: Option<(u16, Option<u8>)>,
packet_count: u64, packet_count: u64,
} }
@@ -33,10 +37,12 @@ impl ServiceCollector {
Self { Self {
channel, channel,
framer: PacketFramer::default(), framer: PacketFramer::default(),
assembler: SectionAssembler::default(), sdt_assembler: SectionAssembler::default(),
nit_assembler: SectionAssembler::default(),
services: HashMap::new(), services: HashMap::new(),
sections_seen: HashSet::new(), sections_seen: HashSet::new(),
last_section: None, last_section: None,
network: None,
packet_count: 0, packet_count: 0,
} }
} }
@@ -49,10 +55,16 @@ impl ServiceCollector {
let Ok(packet) = Packet::new(&packet_bytes) else { let Ok(packet) = Packet::new(&packet_bytes) else {
continue; continue;
}; };
if packet.pid() != PID_SDT { match packet.pid() {
continue; PID_NIT => {
for section in self.nit_assembler.push(packet) {
if let Some(network) = parse_nit_section(&section) {
self.network = Some(network);
} }
for section in self.assembler.push(packet) { }
}
PID_SDT => {
for section in self.sdt_assembler.push(packet) {
let Some(parsed) = parse_sdt_section(&section, &self.channel) else { let Some(parsed) = parse_sdt_section(&section, &self.channel) else {
continue; continue;
}; };
@@ -64,11 +76,15 @@ impl ServiceCollector {
} }
} }
} }
_ => {}
}
}
} }
#[must_use] #[must_use]
pub fn is_complete(&self) -> bool { pub fn is_complete(&self) -> bool {
self.last_section.is_some_and(|last| { self.network.is_some()
&& self.last_section.is_some_and(|last| {
(0..=last).all(|section| { (0..=last).all(|section| {
self.sections_seen self.sections_seen
.iter() .iter()
@@ -83,13 +99,77 @@ impl ServiceCollector {
} }
#[must_use] #[must_use]
pub fn into_services(self) -> Vec<Service> { pub fn into_services(mut self) -> Vec<Service> {
if let Some((network_id, Some(remote_control_key_id))) = self.network {
for service in self.services.values_mut() {
if service.network_id == network_id {
service.remote_control_key_id = Some(remote_control_key_id);
}
}
}
let mut services = self.services.into_values().collect::<Vec<_>>(); let mut services = self.services.into_values().collect::<Vec<_>>();
services.sort_by_key(|service| service.service_id); services.sort_by_key(|service| service.service_id);
services services
} }
} }
fn parse_nit_section(section: &[u8]) -> Option<(u16, Option<u8>)> {
if section.len() < 16
|| section[0] != TABLE_ID_NIT_ACTUAL
|| section[5] & 0x01 == 0
|| crc32_mpeg2(section) != 0
{
return None;
}
let network_id = u16::from_be_bytes([section[3], section[4]]);
let end = section.len().checked_sub(4)?;
let network_descriptors_length =
(usize::from(section[8] & 0x0f) << 8) | usize::from(section[9]);
let transport_length_offset = 10usize.checked_add(network_descriptors_length)?;
if transport_length_offset + 2 > end {
return None;
}
let transport_loop_length = (usize::from(section[transport_length_offset] & 0x0f) << 8)
| usize::from(section[transport_length_offset + 1]);
let mut offset = transport_length_offset + 2;
let transport_end = offset.checked_add(transport_loop_length)?;
if transport_end > end {
return None;
}
let mut remote_control_key_id = None;
while offset + 6 <= transport_end {
let descriptors_length =
(usize::from(section[offset + 4] & 0x0f) << 8) | usize::from(section[offset + 5]);
let descriptors_start = offset + 6;
let descriptors_end = descriptors_start.checked_add(descriptors_length)?;
if descriptors_end > transport_end {
return None;
}
remote_control_key_id =
parse_remote_control_key_id(&section[descriptors_start..descriptors_end]);
if remote_control_key_id.is_some() {
break;
}
offset = descriptors_end;
}
Some((network_id, remote_control_key_id))
}
fn parse_remote_control_key_id(mut descriptors: &[u8]) -> Option<u8> {
while descriptors.len() >= 2 {
let length = usize::from(descriptors[1]);
let end = 2usize.checked_add(length)?;
if end > descriptors.len() {
return None;
}
if descriptors[0] == 0xcd && length >= 1 {
return Some(descriptors[2]);
}
descriptors = &descriptors[end..];
}
None
}
struct ParsedSdt { struct ParsedSdt {
version: u8, version: u8,
section_number: u8, section_number: u8,
@@ -122,7 +202,7 @@ fn parse_sdt_section(section: &[u8], channel: &ConfigChannel) -> Option<ParsedSd
.service_id .service_id
.is_none_or(|configured| configured == service_id) .is_none_or(|configured| configured == service_id)
{ {
if let Some((service_type, name)) = if let Some((service_type, name, logo_id)) =
parse_service_descriptor(&section[descriptor_start..descriptor_end]) parse_service_descriptor(&section[descriptor_start..descriptor_end])
{ {
services.push(Service { services.push(Service {
@@ -131,7 +211,7 @@ fn parse_sdt_section(section: &[u8], channel: &ConfigChannel) -> Option<ParsedSd
network_id, network_id,
name, name,
service_type, service_type,
logo_id: None, logo_id,
has_logo_data: None, has_logo_data: None,
remote_control_key_id: None, remote_control_key_id: None,
epg_ready: None, epg_ready: None,
@@ -155,15 +235,18 @@ fn parse_sdt_section(section: &[u8], channel: &ConfigChannel) -> Option<ParsedSd
}) })
} }
fn parse_service_descriptor(mut descriptors: &[u8]) -> Option<(u8, String)> { fn parse_service_descriptor(mut descriptors: &[u8]) -> Option<(u8, String, Option<u16>)> {
let mut service = None;
let mut logo_id = None;
while descriptors.len() >= 2 { while descriptors.len() >= 2 {
let length = usize::from(descriptors[1]); let length = usize::from(descriptors[1]);
let end = 2usize.checked_add(length)?; let end = 2usize.checked_add(length)?;
if end > descriptors.len() { if end > descriptors.len() {
return None; return None;
} }
if descriptors[0] == 0x48 {
let body = &descriptors[2..end]; let body = &descriptors[2..end];
match descriptors[0] {
0x48 => {
if body.len() < 3 { if body.len() < 3 {
return None; return None;
} }
@@ -178,11 +261,16 @@ fn parse_service_descriptor(mut descriptors: &[u8]) -> Option<(u8, String)> {
if name_end > body.len() { if name_end > body.len() {
return None; return None;
} }
return Some((body[0], decode_arib(&body[name_start..name_end]))); service = Some((body[0], decode_arib(&body[name_start..name_end])));
}
0xcf if body.len() >= 3 && matches!(body[0], 1 | 2) => {
logo_id = Some((u16::from(body[1] & 0x01) << 8) | u16::from(body[2]));
}
_ => {}
} }
descriptors = &descriptors[end..]; descriptors = &descriptors[end..];
} }
None service.map(|(service_type, name)| (service_type, name, logo_id))
} }
#[cfg(test)] #[cfg(test)]
@@ -193,29 +281,45 @@ mod tests {
use super::ServiceCollector; use super::ServiceCollector;
#[test] fn section_packet(pid: u16, section: &[u8]) -> [u8; PACKET_SIZE] {
fn discovers_service_from_sdt() { let mut packet = [0xff; PACKET_SIZE];
let descriptor = [ packet[0] = 0x47;
0x48, 0x0c, 0x01, 0x00, 0x09, 0x0e, b'T', b'e', b's', b't', b' ', b'T', b'V', b'!', packet[1] = 0x40 | u8::try_from(pid >> 8).expect("PID");
]; packet[2] = u8::try_from(pid & 0xff).expect("PID");
let mut section = vec![ packet[3] = 0x10;
0x42, 0xf0, 0x00, 0x00, 0x01, 0xc1, 0x00, 0x00, 0x7f, 0xf0, 0xff, 0x00, 0x65, 0xfc, packet[4] = 0;
0x80, 0x0e, packet[5..5 + section.len()].copy_from_slice(section);
]; packet
section.extend_from_slice(&descriptor); }
fn finish_section(mut section: Vec<u8>) -> Vec<u8> {
let section_length = section.len() - 3 + 4; let section_length = section.len() - 3 + 4;
section[1] = 0xf0 | u8::try_from(section_length >> 8).expect("section length"); section[1] = 0xf0 | u8::try_from(section_length >> 8).expect("section length");
section[2] = u8::try_from(section_length & 0xff).expect("section length"); section[2] = u8::try_from(section_length & 0xff).expect("section length");
let crc = crc32_mpeg2(&section); let crc = crc32_mpeg2(&section);
section.extend_from_slice(&crc.to_be_bytes()); section.extend_from_slice(&crc.to_be_bytes());
section
}
let mut packet = [0xff; PACKET_SIZE]; #[test]
packet[0] = 0x47; fn discovers_service_from_sdt() {
packet[1] = 0x40; let descriptor = [
packet[2] = 0x11; 0x48, 0x0c, 0x01, 0x00, 0x09, 0x0e, b'T', b'e', b's', b't', b' ', b'T', b'V', b'!',
packet[3] = 0x10; 0xcf, 0x03, 0x02, 0x00, 0x07,
packet[4] = 0; ];
packet[5..5 + section.len()].copy_from_slice(&section); let section = finish_section(
vec![
0x42, 0xf0, 0x00, 0x00, 0x01, 0xc1, 0x00, 0x00, 0x7f, 0xf0, 0xff, 0x00, 0x65, 0xfc,
0x80, 0x13,
]
.into_iter()
.chain(descriptor)
.collect(),
);
let nit = finish_section(vec![
0x40, 0xf0, 0x00, 0x7f, 0xf0, 0xc1, 0x00, 0x00, 0xf0, 0x00, 0xf0, 0x09, 0x00, 0x01,
0x7f, 0xf0, 0xf0, 0x03, 0xcd, 0x01, 0x05,
]);
let mut collector = ServiceCollector::new(ConfigChannel { let mut collector = ServiceCollector::new(ConfigChannel {
name: "channel".into(), name: "channel".into(),
@@ -231,12 +335,15 @@ mod tests {
freq: None, freq: None,
polarity: None, polarity: None,
}); });
collector.push(&packet); collector.push(&section_packet(0x10, &nit));
collector.push(&section_packet(0x11, &section));
assert!(collector.is_complete()); assert!(collector.is_complete());
let services = collector.into_services(); let services = collector.into_services();
assert_eq!(services.len(), 1); assert_eq!(services.len(), 1);
assert_eq!(services[0].name, "Test TV!"); assert_eq!(services[0].name, "Test TV!");
assert_eq!(services[0].network_id, 0x7ff0); assert_eq!(services[0].network_id, 0x7ff0);
assert_eq!(services[0].service_id, 101); assert_eq!(services[0].service_id, 101);
assert_eq!(services[0].logo_id, Some(7));
assert_eq!(services[0].remote_control_key_id, Some(5));
} }
} }

View File

@@ -68,23 +68,31 @@ impl Drop for TunerSubscription {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct TunerManager { pub struct TunerManager {
devices: Arc<Vec<DeviceHandle>>, devices: Arc<Vec<DeviceHandle>>,
respawn_count: Arc<AtomicU64>,
} }
impl TunerManager { impl TunerManager {
#[must_use] #[must_use]
pub fn new(configs: &[ConfigTuner]) -> Self { pub fn new(configs: &[ConfigTuner]) -> Self {
let respawn_count = Arc::new(AtomicU64::new(0));
let devices = configs let devices = configs
.iter() .iter()
.filter(|config| config.is_disabled != Some(true)) .filter(|config| config.is_disabled != Some(true))
.cloned() .cloned()
.enumerate() .enumerate()
.map(|(index, config)| DeviceHandle::spawn(index, config)) .map(|(index, config)| DeviceHandle::spawn(index, config, respawn_count.clone()))
.collect(); .collect();
Self { Self {
devices: Arc::new(devices), devices: Arc::new(devices),
respawn_count,
} }
} }
#[must_use]
pub fn respawn_count(&self) -> u64 {
self.respawn_count.load(Ordering::Relaxed)
}
/// Subscribes to a compatible tuner, sharing an existing multiplex when possible. /// Subscribes to a compatible tuner, sharing an existing multiplex when possible.
/// ///
/// # Errors /// # Errors
@@ -173,11 +181,11 @@ struct DeviceHandle {
} }
impl DeviceHandle { impl DeviceHandle {
fn spawn(index: usize, config: ConfigTuner) -> Self { fn spawn(index: usize, config: ConfigTuner, respawn_count: Arc<AtomicU64>) -> Self {
let (command, receiver) = mpsc::channel(DEVICE_COMMAND_CAPACITY); let (command, receiver) = mpsc::channel(DEVICE_COMMAND_CAPACITY);
let actor_command = command.clone(); let actor_command = command.clone();
tokio::spawn(async move { tokio::spawn(async move {
DeviceActor::new(index, config, actor_command) DeviceActor::new(index, config, actor_command, respawn_count)
.run(receiver) .run(receiver)
.await; .await;
}); });
@@ -265,10 +273,16 @@ struct DeviceActor {
fault: bool, fault: bool,
fatal_count: u8, fatal_count: u8,
generation: u64, generation: u64,
respawn_count: Arc<AtomicU64>,
} }
impl DeviceActor { impl DeviceActor {
fn new(index: usize, config: ConfigTuner, command_sender: mpsc::Sender<DeviceCommand>) -> Self { fn new(
index: usize,
config: ConfigTuner,
command_sender: mpsc::Sender<DeviceCommand>,
respawn_count: Arc<AtomicU64>,
) -> Self {
Self { Self {
index, index,
config, config,
@@ -282,6 +296,7 @@ impl DeviceActor {
fault: false, fault: false,
fatal_count: 0, fatal_count: 0,
generation: 0, generation: 0,
respawn_count,
} }
} }
@@ -461,6 +476,7 @@ impl DeviceActor {
self.release(); self.release();
return; return;
}; };
self.respawn_count.fetch_add(1, Ordering::Relaxed);
tracing::warn!(device = self.index, "respawning tuner after unexpected EOF"); tracing::warn!(device = self.index, "respawning tuner after unexpected EOF");
if let Err(error) = self.start_process(channel).await { if let Err(error) = self.start_process(channel).await {
tracing::error!(device = self.index, %error, "failed to respawn tuner"); tracing::error!(device = self.index, %error, "failed to respawn tuner");

View File

@@ -6,9 +6,11 @@
use std::{ use std::{
collections::{BTreeMap, HashMap}, collections::{BTreeMap, HashMap},
convert::Infallible, convert::Infallible,
future::pending,
io, io,
process::Stdio, process::Stdio,
sync::{Arc, atomic::Ordering}, sync::{Arc, atomic::Ordering},
time::Duration,
}; };
use axum::{ use axum::{
@@ -751,6 +753,8 @@ pub async fn build_status(state: &AppState) -> Status {
}, },
error_count: ErrorCount { error_count: ErrorCount {
buffer_overflow: state.buffer_overflow_count.load(Ordering::Relaxed), buffer_overflow: state.buffer_overflow_count.load(Ordering::Relaxed),
tuner_device_respawn: state.tuners.respawn_count(),
decoder_respawn: state.decoder_respawn_count.load(Ordering::Relaxed),
..ErrorCount::default() ..ErrorCount::default()
}, },
timer_accuracy: TimerAccuracy::default(), timer_accuracy: TimerAccuracy::default(),
@@ -1278,6 +1282,7 @@ async fn head_program_stream(
} }
} }
#[allow(clippy::too_many_lines)]
async fn stream_channel( async fn stream_channel(
state: Arc<AppState>, state: Arc<AppState>,
channel: ConfigChannel, channel: ConfigChannel,
@@ -1305,62 +1310,193 @@ async fn stream_channel(
.await .await
.map_err(|error| ApiFailure::unavailable(error.to_string()))?; .map_err(|error| ApiFailure::unavailable(error.to_string()))?;
let user_id = subscription.user_id.clone(); let user_id = subscription.user_id.clone();
let mut decoder = subscription let decoder_command = subscription.decoder.clone();
.decoder if let Some(command) = decoder_command.as_deref() {
.as_deref() let _ = parse_process_command(command)?;
.map(spawn_decoder) }
.transpose()?;
let stream_state = state.clone(); let stream_state = state.clone();
let output = async_stream::stream! { let output = async_stream::stream! {
let _guard = StreamGuard::new(stream_state.clone()); let _guard = StreamGuard::new(stream_state.clone());
let _decoder_guard = decoder let _decoder_guard = decoder_command
.as_ref() .as_ref()
.map(|_| DecoderGuard::new(stream_state.clone())); .map(|_| DecoderGuard::new(stream_state.clone()));
let mut service_filter = service_id.map(mirakurun_core::filter::ServiceFilter::new); let mut service_filter = service_id.map(mirakurun_core::filter::ServiceFilter::new);
let mut decoder_buffer = vec![0_u8; 32 * 1024]; let mut decoder_buffer = vec![0_u8; 32 * 1024];
let mut decoder: Option<DecoderProcess> = None;
let mut decoder_dead_count = 0_u8;
let mut decoder_fallback = decoder_command.is_none();
let mut decoder_retry_at = decoder_command
.as_ref()
.map(|_| tokio::time::Instant::now());
let mut decoder_response_deadline = None;
let mut decoder_waiting_for_output = false;
let mut source_open = true; let mut source_open = true;
loop { loop {
if stream_expired(end_at) { if stream_expired(end_at) {
break; break;
} }
if let Some(process) = decoder.as_mut() { if let Some(process) = decoder.as_mut() {
if source_open { let event = if source_open {
tokio::select! { tokio::select! {
source = subscription.receiver.recv() => { source = subscription.receiver.recv() => {
match source { DecoderStreamEvent::Source(source)
Ok(chunk) => {
let filtered = filter_stream_chunk(&mut service_filter, &chunk);
if !filtered.is_empty()
&& process.stdin.write_all(&filtered).await.is_err()
{
break;
}
}
Err(broadcast::error::RecvError::Lagged(skipped)) => {
stream_state.buffer_overflow_count.fetch_add(skipped, Ordering::Relaxed);
break;
}
Err(broadcast::error::RecvError::Closed) => {
source_open = false;
let _ = process.stdin.shutdown().await;
}
}
} }
decoder_read = process.stdout.read(&mut decoder_buffer) => { decoder_read = process.stdout.read(&mut decoder_buffer) => {
match decoder_read { DecoderStreamEvent::Output(decoder_read)
Ok(0) | Err(_) => break,
Ok(length) => {
yield Ok::<Bytes, io::Error>(Bytes::copy_from_slice(&decoder_buffer[..length]));
}
} }
() = wait_for_decoder_deadline(decoder_response_deadline) => {
DecoderStreamEvent::NoResponse
} }
} }
} else { } else {
match process.stdout.read(&mut decoder_buffer).await { tokio::select! {
Ok(0) | Err(_) => break, decoder_read = process.stdout.read(&mut decoder_buffer) => {
Ok(length) => { DecoderStreamEvent::Output(decoder_read)
}
() = wait_for_decoder_deadline(decoder_response_deadline) => {
DecoderStreamEvent::NoResponse
}
}
};
let mut decoder_failure = None;
match event {
DecoderStreamEvent::Source(Ok(chunk)) => {
let filtered = filter_stream_chunk(&mut service_filter, &chunk);
if !filtered.is_empty() {
let first_input = !decoder_waiting_for_output;
if first_input {
decoder_waiting_for_output = true;
decoder_response_deadline =
Some(tokio::time::Instant::now() + DECODER_RESPONSE_TIMEOUT);
}
let write_deadline = decoder_response_deadline
.unwrap_or_else(|| {
tokio::time::Instant::now() + DECODER_RESPONSE_TIMEOUT
});
match tokio::time::timeout_at(
write_deadline,
process.stdin.write_all(&filtered),
)
.await
{
Ok(Ok(())) => {}
Ok(Err(_)) => decoder_failure = Some("stdin closed"),
Err(_) => decoder_failure = Some("stdin write timed out"),
}
}
}
DecoderStreamEvent::Source(Err(broadcast::error::RecvError::Lagged(skipped))) => {
stream_state.buffer_overflow_count.fetch_add(skipped, Ordering::Relaxed);
service_filter =
service_id.map(mirakurun_core::filter::ServiceFilter::new);
decoder_failure = Some("source buffer overflow");
}
DecoderStreamEvent::Source(Err(broadcast::error::RecvError::Closed)) => {
source_open = false;
let _ = process.stdin.shutdown().await;
decoder_response_deadline =
Some(tokio::time::Instant::now() + DECODER_RESPONSE_TIMEOUT);
}
DecoderStreamEvent::Output(Ok(0)) => {
decoder_failure = Some("stdout closed");
}
DecoderStreamEvent::Output(Err(_)) => {
decoder_failure = Some("stdout read failed");
}
DecoderStreamEvent::Output(Ok(length)) => {
decoder_waiting_for_output = false;
decoder_response_deadline = if source_open {
None
} else {
Some(tokio::time::Instant::now() + DECODER_RESPONSE_TIMEOUT)
};
yield Ok::<Bytes, io::Error>(Bytes::copy_from_slice(&decoder_buffer[..length])); yield Ok::<Bytes, io::Error>(Bytes::copy_from_slice(&decoder_buffer[..length]));
} }
DecoderStreamEvent::NoResponse => {
decoder_failure = Some("no response");
}
}
if let Some(reason) = decoder_failure {
decoder = None;
decoder_response_deadline = None;
decoder_waiting_for_output = false;
if !source_open {
break;
}
if record_decoder_failure(&mut decoder_dead_count) {
decoder_fallback = true;
stream_state
.log(format!(
"decoder failed ({reason}); falling back to pass-through"
))
.await;
} else {
decoder_retry_at =
Some(tokio::time::Instant::now() + DECODER_RESPAWN_DELAY);
stream_state
.log(format!(
"decoder failed ({reason}); scheduling respawn {decoder_dead_count}/{MAX_DECODER_RESPAWNS}"
))
.await;
}
}
} else if !decoder_fallback {
let retry_at = decoder_retry_at.unwrap_or_else(tokio::time::Instant::now);
tokio::select! {
biased;
() = tokio::time::sleep_until(retry_at) => {
if decoder_dead_count > 0 {
stream_state
.decoder_respawn_count
.fetch_add(1, Ordering::Relaxed);
}
let command = decoder_command.as_deref().unwrap_or_default();
match spawn_decoder(command) {
Ok(process) => {
decoder = Some(process);
decoder_retry_at = None;
decoder_response_deadline = None;
decoder_waiting_for_output = false;
}
Err(error) => {
let reason = error
.reason
.as_deref()
.unwrap_or("failed to start decoder")
.to_owned();
if record_decoder_failure(&mut decoder_dead_count) {
decoder_fallback = true;
stream_state
.log(format!(
"decoder failed ({reason}); falling back to pass-through"
))
.await;
} else {
decoder_retry_at =
Some(tokio::time::Instant::now() + DECODER_RESPAWN_DELAY);
stream_state
.log(format!(
"decoder failed ({reason}); scheduling respawn {decoder_dead_count}/{MAX_DECODER_RESPAWNS}"
))
.await;
}
}
}
}
source = subscription.receiver.recv() => {
match source {
Ok(chunk) => {
let _ = filter_stream_chunk(&mut service_filter, &chunk);
}
Err(broadcast::error::RecvError::Lagged(skipped)) => {
stream_state.buffer_overflow_count.fetch_add(skipped, Ordering::Relaxed);
service_filter =
service_id.map(mirakurun_core::filter::ServiceFilter::new);
}
Err(broadcast::error::RecvError::Closed) => break,
}
} }
} }
} else { } else {
@@ -1373,7 +1509,8 @@ async fn stream_channel(
} }
Err(broadcast::error::RecvError::Lagged(skipped)) => { Err(broadcast::error::RecvError::Lagged(skipped)) => {
stream_state.buffer_overflow_count.fetch_add(skipped, Ordering::Relaxed); stream_state.buffer_overflow_count.fetch_add(skipped, Ordering::Relaxed);
break; service_filter =
service_id.map(mirakurun_core::filter::ServiceFilter::new);
} }
Err(broadcast::error::RecvError::Closed) => break, Err(broadcast::error::RecvError::Closed) => break,
} }
@@ -1383,6 +1520,29 @@ async fn stream_channel(
Ok(stream_response(Body::from_stream(output), &user_id)) Ok(stream_response(Body::from_stream(output), &user_id))
} }
const MAX_DECODER_RESPAWNS: u8 = 3;
const DECODER_RESPAWN_DELAY: Duration = Duration::from_millis(1500);
const DECODER_RESPONSE_TIMEOUT: Duration = Duration::from_millis(1500);
enum DecoderStreamEvent {
Source(Result<Bytes, broadcast::error::RecvError>),
Output(io::Result<usize>),
NoResponse,
}
async fn wait_for_decoder_deadline(deadline: Option<tokio::time::Instant>) {
if let Some(deadline) = deadline {
tokio::time::sleep_until(deadline).await;
} else {
pending::<()>().await;
}
}
fn record_decoder_failure(dead_count: &mut u8) -> bool {
*dead_count = dead_count.saturating_add(1);
*dead_count > MAX_DECODER_RESPAWNS
}
fn stream_expired(end_at: Option<u64>) -> bool { fn stream_expired(end_at: Option<u64>) -> bool {
end_at.is_some_and(|end_at| AppState::now_ms() >= end_at) end_at.is_some_and(|end_at| AppState::now_ms() >= end_at)
} }
@@ -1698,3 +1858,37 @@ where
{ {
Json(value).into_response() Json(value).into_response()
} }
#[cfg(test)]
mod tests {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use super::{MAX_DECODER_RESPAWNS, record_decoder_failure, spawn_decoder};
#[test]
fn decoder_falls_back_after_three_respawns() {
let mut dead_count = 0;
for expected in 1..=MAX_DECODER_RESPAWNS {
assert!(!record_decoder_failure(&mut dead_count));
assert_eq!(dead_count, expected);
}
assert!(record_decoder_failure(&mut dead_count));
}
#[tokio::test]
async fn decoder_process_round_trips_transport_data() {
let mut decoder = spawn_decoder("/bin/cat").expect("start pass-through decoder");
decoder
.stdin
.write_all(b"transport-stream")
.await
.expect("write decoder input");
let mut output = vec![0; 16];
decoder
.stdout
.read_exact(&mut output)
.await
.expect("read decoder output");
assert_eq!(&output, b"transport-stream");
}
}

View File

@@ -6,14 +6,15 @@
use std::{ use std::{
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
sync::Arc, sync::Arc,
time::Duration, time::{Duration, SystemTime},
}; };
use anyhow::{Result, bail}; use anyhow::{Result, bail};
use chrono::{DateTime, Datelike, Local, Timelike}; use chrono::{DateTime, Datelike, Local, Timelike};
use mirakurun_core::{ use mirakurun_core::{
epg::EitCollector, epg::EitCollector,
persistence::{channels_integrity, save_json_db}, logo::{LogoCollector, LogoData},
persistence::{atomic_write, channels_integrity, ensure_parent, save_json_db},
service::ServiceCollector, service::ServiceCollector,
tuner::StreamRequest, tuner::StreamRequest,
}; };
@@ -340,8 +341,9 @@ async fn epg_gather(state: &Arc<AppState>) -> Result<()> {
.unwrap_or(10 * 60 * 1000); .unwrap_or(10 * 60 * 1000);
for (network_id, channel) in targets { for (network_id, channel) in targets {
let _guard = GatheringNetworkGuard::new(state.clone(), network_id).await; let _guard = GatheringNetworkGuard::new(state.clone(), network_id).await;
let programs = gather_network(state, channel, retrieval_time).await?; let gathered = gather_network(state, network_id, channel, retrieval_time).await?;
merge_epg(state, network_id, programs).await?; merge_logo_data(state, gathered.logos).await?;
merge_epg(state, network_id, gathered.programs).await?;
} }
Ok(()) Ok(())
} }
@@ -421,8 +423,12 @@ async fn merge_services(
.collect::<HashMap<_, _>>(); .collect::<HashMap<_, _>>();
for service in &mut discovered { for service in &mut discovered {
if let Some(previous) = old_on_channel.get(&service.id) { if let Some(previous) = old_on_channel.get(&service.id) {
if service.logo_id.is_none() {
service.logo_id = previous.logo_id; service.logo_id = previous.logo_id;
}
if service.remote_control_key_id.is_none() {
service.remote_control_key_id = previous.remote_control_key_id; service.remote_control_key_id = previous.remote_control_key_id;
}
service.epg_ready = previous.epg_ready; service.epg_ready = previous.epg_ready;
service.epg_updated_at = previous.epg_updated_at; service.epg_updated_at = previous.epg_updated_at;
} }
@@ -517,9 +523,10 @@ async fn epg_targets(state: &Arc<AppState>) -> Vec<(u16, ConfigChannel)> {
async fn gather_network( async fn gather_network(
state: &Arc<AppState>, state: &Arc<AppState>,
network_id: u16,
channel: ConfigChannel, channel: ConfigChannel,
retrieval_time_ms: u64, retrieval_time_ms: u64,
) -> Result<Vec<mirakurun_types::Program>> { ) -> Result<GatheredNetwork> {
let mut subscription = state let mut subscription = state
.tuners .tuners
.subscribe(StreamRequest { .subscribe(StreamRequest {
@@ -537,10 +544,17 @@ async fn gather_network(
let mut last_section = started; let mut last_section = started;
let mut section_count = 0; let mut section_count = 0;
let mut collector = EitCollector::default(); let mut collector = EitCollector::default();
let mut logo_collector = LogoCollector::default();
let expected_logos = stale_logo_keys(state, network_id).await;
loop { loop {
let now = Instant::now(); let now = Instant::now();
let logos_complete = expected_logos
.iter()
.all(|(network_id, logo_id)| logo_collector.contains(*network_id, *logo_id));
if now >= deadline if now >= deadline
|| (section_count > 0 && now.duration_since(last_section) >= Duration::from_secs(3)) || (section_count > 0
&& now.duration_since(last_section) >= Duration::from_secs(3)
&& logos_complete)
{ {
break; break;
} }
@@ -550,6 +564,7 @@ async fn gather_network(
match tokio::time::timeout(wait, subscription.receiver.recv()).await { match tokio::time::timeout(wait, subscription.receiver.recv()).await {
Ok(Ok(chunk)) => { Ok(Ok(chunk)) => {
collector.push(&chunk); collector.push(&chunk);
logo_collector.push(&chunk);
if collector.section_count() != section_count { if collector.section_count() != section_count {
section_count = collector.section_count(); section_count = collector.section_count();
last_section = Instant::now(); last_section = Instant::now();
@@ -569,7 +584,95 @@ async fn gather_network(
if collector.section_count() == 0 { if collector.section_count() == 0 {
bail!("no EIT sections received before the EPG retrieval timeout"); bail!("no EIT sections received before the EPG retrieval timeout");
} }
Ok(collector.into_programs()) Ok(GatheredNetwork {
programs: collector.into_programs(),
logos: logo_collector.into_logos(),
})
}
struct GatheredNetwork {
programs: Vec<mirakurun_types::Program>,
logos: Vec<LogoData>,
}
async fn stale_logo_keys(state: &AppState, network_id: u16) -> HashSet<(u16, u16)> {
let interval = state
.config
.read()
.await
.server
.logo_data_interval
.unwrap_or(7 * DAY_MS);
let services = state.services.read().await.clone();
let mut pending_keys = HashSet::new();
for service in services
.iter()
.filter(|service| service.network_id == network_id)
{
let Some(logo_id) = service.logo_id else {
continue;
};
let path = state
.paths
.logo_data_dir
.join(format!("{}_{}.png", service.network_id, logo_id));
let fresh = tokio::fs::metadata(path)
.await
.ok()
.and_then(|metadata| metadata.modified().ok())
.and_then(|modified| SystemTime::now().duration_since(modified).ok())
.is_some_and(|age| u64::try_from(age.as_millis()).unwrap_or(u64::MAX) <= interval);
if !fresh {
pending_keys.insert((service.network_id, logo_id));
}
}
pending_keys
}
async fn merge_logo_data(state: &Arc<AppState>, logos: Vec<LogoData>) -> Result<()> {
if logos.is_empty() {
return Ok(());
}
let mut services = state.services.read().await.clone();
let mut updated_service_ids = HashSet::new();
for logo in &logos {
let path = state
.paths
.logo_data_dir
.join(format!("{}_{}.png", logo.network_id, logo.logo_id));
ensure_parent(&path, "logo data").await?;
atomic_write(&path, &logo.png, "logo data").await?;
for &(network_id, service_id) in &logo.services {
let service = services.iter_mut().find(|service| {
service.network_id == network_id && service.service_id == service_id
});
if let Some(service) = service {
if service.logo_id != Some(logo.logo_id) {
service.logo_id = Some(logo.logo_id);
updated_service_ids.insert(service.id);
}
}
}
}
if !updated_service_ids.is_empty() {
let channels = state.config.read().await.channels.clone();
let integrity = channels_integrity(&channels)?;
save_json_db(&state.paths.services_db, &services, &integrity).await?;
state.services.write().await.clone_from(&services);
for service in services
.iter()
.filter(|service| updated_service_ids.contains(&service.id))
{
state
.emit_event(EventResource::Service, EventType::Update, service)
.await;
}
}
state
.log(format!("stored {} broadcast logo images", logos.len()))
.await;
Ok(())
} }
async fn merge_epg( async fn merge_epg(
@@ -588,7 +691,7 @@ async fn merge_epg(
.collect::<HashSet<_>>(); .collect::<HashSet<_>>();
for program in &incoming { for program in &incoming {
if let Some(current) = programs.iter_mut().find(|current| current.id == program.id) { if let Some(current) = programs.iter_mut().find(|current| current.id == program.id) {
current.clone_from(program); merge_program(current, program);
} else { } else {
programs.push(program.clone()); programs.push(program.clone());
} }
@@ -638,6 +741,36 @@ async fn merge_epg(
Ok(()) Ok(())
} }
fn merge_program(current: &mut mirakurun_types::Program, incoming: &mirakurun_types::Program) {
current.start_at = incoming.start_at;
current.duration = incoming.duration;
current.is_free = incoming.is_free;
if incoming.name.is_some() {
current.name.clone_from(&incoming.name);
}
if incoming.description.is_some() {
current.description.clone_from(&incoming.description);
}
if incoming.genres.is_some() {
current.genres.clone_from(&incoming.genres);
}
if incoming.video.is_some() {
current.video.clone_from(&incoming.video);
}
if incoming.audios.is_some() {
current.audios.clone_from(&incoming.audios);
}
if incoming.series.is_some() {
current.series.clone_from(&incoming.series);
}
if incoming.extended.is_some() {
current.extended.clone_from(&incoming.extended);
}
if incoming.related_items.is_some() {
current.related_items.clone_from(&incoming.related_items);
}
}
struct GatheringNetworkGuard { struct GatheringNetworkGuard {
state: Arc<AppState>, state: Arc<AppState>,
network_id: u16, network_id: u16,

View File

@@ -55,6 +55,7 @@ pub struct AppState {
pub stream_count: AtomicU64, pub stream_count: AtomicU64,
pub decoder_count: AtomicU64, pub decoder_count: AtomicU64,
pub buffer_overflow_count: AtomicU64, pub buffer_overflow_count: AtomicU64,
pub decoder_respawn_count: AtomicU64,
pub shutdown: CancellationToken, pub shutdown: CancellationToken,
pub restart_requested: AtomicBool, pub restart_requested: AtomicBool,
} }
@@ -100,6 +101,7 @@ impl AppState {
stream_count: AtomicU64::new(0), stream_count: AtomicU64::new(0),
decoder_count: AtomicU64::new(0), decoder_count: AtomicU64::new(0),
buffer_overflow_count: AtomicU64::new(0), buffer_overflow_count: AtomicU64::new(0),
decoder_respawn_count: AtomicU64::new(0),
shutdown: CancellationToken::new(), shutdown: CancellationToken::new(),
restart_requested: AtomicBool::new(false), restart_requested: AtomicBool::new(false),
})) }))