use std::collections::HashMap;
use std::process::ExitCode;
use dvb_si::collect::{CompleteSdt, CompleteSdtService, SectionSetCollector};
use dvb_si::demux::SiDemux;
use dvb_si::descriptors::AnyDescriptor;
use dvb_si::tables::RunningStatus;
use dvb_si::TableId;
use crate::util::{for_each_packet, read_file};
const SDT_TABLE_IDS: [u8; 2] = [
TableId::ServiceDescriptionActual as u8,
TableId::ServiceDescriptionOther as u8,
];
const NIT_TABLE_IDS: [u8; 2] = [
TableId::NetworkInformationActual as u8,
TableId::NetworkInformationOther as u8,
];
struct ServiceRow {
service_id: u16,
type_name: &'static str,
name: Option<String>,
running_status: RunningStatus,
}
fn collect_event(
collector: &mut SectionSetCollector,
pid: u16,
bytes: &[u8],
) -> Option<dvb_si::collect::CompleteSectionSet> {
collector
.push_section_with_pid(Some(pid), bytes)
.ok()
.flatten()
}
fn service_descriptor_view(service: &CompleteSdtService<'_>) -> (Option<String>, &'static str) {
let mut name = None;
let mut type_name = "reserved/unknown";
for item in service.descriptors.descriptors() {
if let Ok(AnyDescriptor::Service(sd)) = item {
name = Some(sd.service_name.to_string());
type_name = sd.service_type.name();
break;
}
}
(name, type_name)
}
fn lookup_lcn(map: &HashMap<u16, u16>, service_id: u16) -> Option<u16> {
map.get(&service_id).copied()
}
fn print_row(row: &ServiceRow, lcn: Option<u16>) {
let lcn_str = match lcn {
Some(n) => format!("{n:>4}"),
None => " -".to_string(),
};
let type_name = row.type_name;
let name = row.name.as_deref().unwrap_or("(no service descriptor)");
println!(
"LCN {lcn_str} service 0x{:04X} {type_name:<32} \"{name}\" running={}",
row.service_id,
row.running_status.name()
);
}
pub fn run(args: &[String]) -> ExitCode {
let mut path: Option<String> = None;
for arg in args {
match arg.as_str() {
"-h" | "--help" => {
eprintln!("usage: dvb-tools services <file.ts>");
return ExitCode::SUCCESS;
}
other if other.starts_with('-') => {
eprintln!("dvb-tools services: unknown option {other}");
return ExitCode::FAILURE;
}
other => path = Some(other.to_string()),
}
}
let Some(path) = path else {
eprintln!("usage: dvb-tools services <file.ts>");
return ExitCode::FAILURE;
};
let data = match read_file(&path, "dvb-tools services") {
Ok(d) => d,
Err(code) => return code,
};
let mut demux = SiDemux::builder().build();
let mut collector = SectionSetCollector::new();
let mut services: HashMap<u16, ServiceRow> = HashMap::new();
let mut lcn_map: HashMap<u16, u16> = HashMap::new();
let mut sdt_seen = 0u32;
let mut nit_seen = 0u32;
for packet in for_each_packet(&data) {
for event in demux.feed(packet) {
let table_id = event.table_id();
let bytes = event.bytes().to_vec();
let pid_u16 = u16::from(event.pid());
if SDT_TABLE_IDS.contains(&table_id) {
if let Some(complete) = collect_event(&mut collector, pid_u16, &bytes) {
sdt_seen += 1;
if let Ok(sdt) = complete.sdt() {
absorb_sdt(&sdt, &mut services);
}
}
} else if NIT_TABLE_IDS.contains(&table_id) {
if let Some(complete) = collect_event(&mut collector, pid_u16, &bytes) {
nit_seen += 1;
if let Ok(nit) = complete.nit() {
absorb_nit(&nit, &mut lcn_map);
}
}
}
}
}
if sdt_seen == 0 {
eprintln!("dvb-tools services: no SDT seen (stream is empty or has no SDT)");
return ExitCode::SUCCESS;
}
let rows: Vec<(Option<u16>, u16, &ServiceRow)> = services
.values()
.map(|row| (lookup_lcn(&lcn_map, row.service_id), row.service_id, row))
.collect();
let mut rows = rows;
rows.sort_by(|a, b| match (a.0, b.0) {
(Some(x), Some(y)) => x.cmp(&y).then(a.1.cmp(&b.1)),
(Some(_), None) => std::cmp::Ordering::Less,
(None, Some(_)) => std::cmp::Ordering::Greater,
(None, None) => a.1.cmp(&b.1),
});
let with_lcn = rows.iter().filter(|(lcn, _, _)| lcn.is_some()).count();
for (_, _, row) in &rows {
print_row(row, lookup_lcn(&lcn_map, row.service_id));
}
eprintln!(
"-- services={} with_lcn={} sdt_collected={} nit_collected={}",
services.len(),
with_lcn,
sdt_seen,
nit_seen
);
ExitCode::SUCCESS
}
fn absorb_sdt<'a>(sdt: &'a CompleteSdt<'a>, services: &mut HashMap<u16, ServiceRow>) {
for service in &sdt.services {
let (name, type_name) = service_descriptor_view(service);
services.insert(
service.service_id,
ServiceRow {
service_id: service.service_id,
type_name,
name,
running_status: service.running_status,
},
);
}
}
fn absorb_nit<'a>(nit: &'a dvb_si::collect::CompleteNit<'a>, lcn_map: &mut HashMap<u16, u16>) {
for ts in &nit.transport_streams {
for item in ts.descriptors.descriptors() {
if let Ok(AnyDescriptor::LogicalChannel(lcd)) = item {
for entry in &lcd.entries {
lcn_map.insert(entry.service_id, entry.logical_channel_number);
}
}
}
}
}