243 lines
7.7 KiB
Rust
243 lines
7.7 KiB
Rust
// 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(§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<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(§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);
|
|
}
|
|
}
|