implement
This commit is contained in:
486
crates/mirakurun-core/src/arib.rs
Normal file
486
crates/mirakurun-core/src/arib.rs
Normal 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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
548
crates/mirakurun-core/src/arib_symbols.rs
Normal file
548
crates/mirakurun-core/src/arib_symbols.rs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(§ion, &mut self.programs)
|
if parse_eit_section(§ion, &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(§ion[offset + 12..descriptor_end], program);
|
|
||||||
}
|
}
|
||||||
|
state.initialized = true;
|
||||||
|
state.present = is_present;
|
||||||
|
state.following = is_following;
|
||||||
|
state.event.mark(table_id, version);
|
||||||
|
}
|
||||||
|
parse_descriptors(
|
||||||
|
§ion[offset + 12..descriptor_end],
|
||||||
|
program,
|
||||||
|
state,
|
||||||
|
table_id,
|
||||||
|
version,
|
||||||
|
);
|
||||||
offset = descriptor_end;
|
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(§ion);
|
||||||
|
section.extend_from_slice(&crc.to_be_bytes());
|
||||||
|
|
||||||
|
let mut packet = [0xff; PACKET_SIZE];
|
||||||
|
packet[0] = 0x47;
|
||||||
|
packet[1] = 0x40;
|
||||||
|
packet[2] = 0x12;
|
||||||
|
packet[3] = 0x10;
|
||||||
|
packet[4] = 0;
|
||||||
|
packet[5..5 + section.len()].copy_from_slice(§ion);
|
||||||
|
packet
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
637
crates/mirakurun-core/src/logo.rs
Normal file
637
crates/mirakurun-core/src/logo.rs
Normal 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(§ion) {
|
||||||
|
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(§ion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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(§ion);
|
||||||
|
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(§ion);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(§ion) {
|
||||||
|
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(§ion, &self.channel) else {
|
let Some(parsed) = parse_sdt_section(§ion, &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(§ion[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(§ion[descriptor_start..descriptor_end])
|
parse_service_descriptor(§ion[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(§ion);
|
let crc = crc32_mpeg2(§ion);
|
||||||
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(§ion);
|
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(§ion_packet(0x10, &nit));
|
||||||
|
collector.push(§ion_packet(0x11, §ion));
|
||||||
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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),
|
||||||
}))
|
}))
|
||||||
|
|||||||
Reference in New Issue
Block a user