// 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, sections_seen: HashSet<(u8, u8)>, last_section: Option, 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(§ion, &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 { let mut services = self.services.into_values().collect::>(); services.sort_by_key(|service| service.service_id); services } } struct ParsedSdt { version: u8, section_number: u8, last_section: u8, services: Vec, } fn parse_sdt_section(section: &[u8], channel: &ConfigChannel) -> Option { 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(§ion[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(§ion); 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(§ion); 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); } }