First commit

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

View File

@@ -0,0 +1,242 @@
// 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 mirakurun_types::{Channel, ConfigChannel, Service, service_item_id};
use crate::{
epg::decode_arib,
filter::SectionAssembler,
ts::{Packet, PacketFramer, crc32_mpeg2},
};
const PID_SDT: u16 = 0x0011;
const TABLE_ID_SDT_ACTUAL: u8 = 0x42;
#[derive(Debug)]
pub struct ServiceCollector {
channel: ConfigChannel,
framer: PacketFramer,
assembler: SectionAssembler,
services: HashMap<u64, Service>,
sections_seen: HashSet<(u8, u8)>,
last_section: Option<u8>,
packet_count: u64,
}
impl ServiceCollector {
#[must_use]
pub fn new(channel: ConfigChannel) -> Self {
Self {
channel,
framer: PacketFramer::default(),
assembler: SectionAssembler::default(),
services: HashMap::new(),
sections_seen: HashSet::new(),
last_section: None,
packet_count: 0,
}
}
pub fn push(&mut self, chunk: &[u8]) {
let mut packets = Vec::new();
self.framer.push(chunk, |packet| packets.push(*packet));
for packet_bytes in packets {
self.packet_count = self.packet_count.saturating_add(1);
let Ok(packet) = Packet::new(&packet_bytes) else {
continue;
};
if packet.pid() != PID_SDT {
continue;
}
for section in self.assembler.push(packet) {
let Some(parsed) = parse_sdt_section(&section, &self.channel) else {
continue;
};
self.last_section = Some(parsed.last_section);
self.sections_seen
.insert((parsed.version, parsed.section_number));
for service in parsed.services {
self.services.insert(service.id, service);
}
}
}
}
#[must_use]
pub fn is_complete(&self) -> bool {
self.last_section.is_some_and(|last| {
(0..=last).all(|section| {
self.sections_seen
.iter()
.any(|(_, seen_section)| *seen_section == section)
})
})
}
#[must_use]
pub const fn packet_count(&self) -> u64 {
self.packet_count
}
#[must_use]
pub fn into_services(self) -> Vec<Service> {
let mut services = self.services.into_values().collect::<Vec<_>>();
services.sort_by_key(|service| service.service_id);
services
}
}
struct ParsedSdt {
version: u8,
section_number: u8,
last_section: u8,
services: Vec<Service>,
}
fn parse_sdt_section(section: &[u8], channel: &ConfigChannel) -> Option<ParsedSdt> {
if section.len() < 15
|| section[0] != TABLE_ID_SDT_ACTUAL
|| section[5] & 0x01 == 0
|| crc32_mpeg2(section) != 0
{
return None;
}
let network_id = u16::from_be_bytes([section[8], section[9]]);
let end = section.len().checked_sub(4)?;
let mut services = Vec::new();
let mut offset = 11;
while offset + 5 <= end {
let service_id = u16::from_be_bytes([section[offset], section[offset + 1]]);
let descriptors_length =
(usize::from(section[offset + 3] & 0x0f) << 8) | usize::from(section[offset + 4]);
let descriptor_start = offset.checked_add(5)?;
let descriptor_end = descriptor_start.checked_add(descriptors_length)?;
if descriptor_end > end {
return None;
}
if channel
.service_id
.is_none_or(|configured| configured == service_id)
{
if let Some((service_type, name)) =
parse_service_descriptor(&section[descriptor_start..descriptor_end])
{
services.push(Service {
id: service_item_id(network_id, service_id),
service_id,
network_id,
name,
service_type,
logo_id: None,
has_logo_data: None,
remote_control_key_id: None,
epg_ready: None,
epg_updated_at: None,
channel: Some(Box::new(Channel {
channel_type: channel.channel_type,
channel: channel.channel.clone(),
name: Some(channel.name.clone()),
services: None,
})),
});
}
}
offset = descriptor_end;
}
Some(ParsedSdt {
version: (section[5] >> 1) & 0x1f,
section_number: section[6],
last_section: section[7],
services,
})
}
fn parse_service_descriptor(mut descriptors: &[u8]) -> Option<(u8, String)> {
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] == 0x48 {
let body = &descriptors[2..end];
if body.len() < 3 {
return None;
}
let provider_length = usize::from(body[1]);
let name_length_offset = 2usize.checked_add(provider_length)?;
if name_length_offset >= body.len() {
return None;
}
let name_length = usize::from(body[name_length_offset]);
let name_start = name_length_offset + 1;
let name_end = name_start.checked_add(name_length)?;
if name_end > body.len() {
return None;
}
return Some((body[0], decode_arib(&body[name_start..name_end])));
}
descriptors = &descriptors[end..];
}
None
}
#[cfg(test)]
mod tests {
use mirakurun_types::{ChannelType, ConfigChannel};
use crate::ts::{PACKET_SIZE, crc32_mpeg2};
use super::ServiceCollector;
#[test]
fn discovers_service_from_sdt() {
let descriptor = [
0x48, 0x0c, 0x01, 0x00, 0x09, 0x0e, b'T', b'e', b's', b't', b' ', b'T', b'V', b'!',
];
let mut section = vec![
0x42, 0xf0, 0x00, 0x00, 0x01, 0xc1, 0x00, 0x00, 0x7f, 0xf0, 0xff, 0x00, 0x65, 0xfc,
0x80, 0x0e,
];
section.extend_from_slice(&descriptor);
let section_length = section.len() - 3 + 4;
section[1] = 0xf0 | u8::try_from(section_length >> 8).expect("section length");
section[2] = u8::try_from(section_length & 0xff).expect("section length");
let crc = crc32_mpeg2(&section);
section.extend_from_slice(&crc.to_be_bytes());
let mut packet = [0xff; PACKET_SIZE];
packet[0] = 0x47;
packet[1] = 0x40;
packet[2] = 0x11;
packet[3] = 0x10;
packet[4] = 0;
packet[5..5 + section.len()].copy_from_slice(&section);
let mut collector = ServiceCollector::new(ConfigChannel {
name: "channel".into(),
channel_type: ChannelType::Gr,
channel: "27".into(),
service_id: None,
tsmf_rel_ts: None,
command_vars: None,
is_disabled: None,
satelite: None,
satellite: None,
space: None,
freq: None,
polarity: None,
});
collector.push(&packet);
assert!(collector.is_complete());
let services = collector.into_services();
assert_eq!(services.len(), 1);
assert_eq!(services[0].name, "Test TV!");
assert_eq!(services[0].network_id, 0x7ff0);
assert_eq!(services[0].service_id, 101);
}
}