import java.io.ByteArrayOutputStream;
import java.net.*;
import java.util.*;
/**
* UdpTsInspector
* 监听 UDP 组播 TS,解析 PAT/PMT/SDT,节目号/节目名/各节目流速率统计
* JDK 1.6 兼容版
*/
public class UdpTsInspector {
// ==== 配置 ====
static final String MCAST_IP = "236.78.78.1";
static final int PORT = 59000;
static final int MTU = 7 * 188; // UDP 一帧里常见多个 TS 包
// PSI 常量
static final int PID_PAT = 0x0000;
static final int PID_SDT = 0x0011; // SDT/BAT
static final int PID_NULL = 0x1FFF;
// 节目与 PID 的关系
static final Map<Integer, Integer> progToPmtPid = new HashMap<Integer, Integer>(); // program_number -> PMT PID
static final Map<Integer, Set<Integer>> programEsPids = new HashMap<Integer, Set<Integer>>(); // program_number -> ES PIDs
static final Map<Integer, String> programNames = new HashMap<Integer, String>(); // program_number -> service_name
// PID → 节目 的反查(便于统计)
static final Map<Integer, Integer> pidToProgram = new HashMap<Integer, Integer>();
// PID 字节计数(用于码率)
static final Map<Integer, Long> pidBytesCounter = new HashMap<Integer, Long>();
static long lastReportNs = System.nanoTime();
static final long REPORT_INTERVAL_NS = 3000000000L; // 3s
// PSI 重组缓存:一个 PID 里可能会分段
static final Map<Integer, SectionAssembler> assemblers = new HashMap<Integer, SectionAssembler>();
// 每个节目:详细 ES 信息(pid + stream_type + 粗分类 + 友好名)
static final Map<Integer, List<EsInfo>> programEsInfo = new HashMap<Integer, List<EsInfo>>();
static class EsInfo {
int pid;
int streamType;
String kind; // "video" / "audio" / "other"
String codec; // "H.264/AVC" / "HEVC" / "AAC" / "AC-3" ...
EsInfo(int pid, int st, String kind, String codec) {
this.pid = pid; this.streamType = st; this.kind = kind; this.codec = codec;
}
}
public static void main(String[] args) throws Exception {
System.out.println("Listen UDP multicast: " + MCAST_IP + ":" + PORT);
MulticastSocket sock = new MulticastSocket(PORT);
try {
InetAddress grp = InetAddress.getByName(MCAST_IP);
sock.joinGroup(grp);
} catch (Throwable t) {
System.err.println("Join group failed: " + t);
}
byte[] buf = new byte[MTU];
DatagramPacket pkt = new DatagramPacket(buf, buf.length);
while (true) {
sock.receive(pkt);
int len = pkt.getLength();
int off = 0;
while (off + 188 <= len) {
parseTsPacket(buf, off);
off += 188;
}
maybeReport();
}
}
/* 解析一个 188B TS 包 */
static void parseTsPacket(byte[] b, int off) {
if ((b[off] & 0xFF) != 0x47) return; // sync
int tei = (b[off+1] & 0x80) >>> 7;
int pusi = (b[off+1] & 0x40) >>> 6;
int pid = ((b[off+1] & 0x1F) << 8) | (b[off+2] & 0xFF);
int afc = (b[off+3] & 0x30) >>> 4;
if (tei == 1) return; // 传输错误,丢弃
// 统计:把 TS 包字节计入对应 PID(过滤 null)
if (pid != PID_NULL) {
Long old = pidBytesCounter.get(pid);
pidBytesCounter.put(pid, (old == null ? 0L : old) + 188L);
}
int i = off + 4;
if (afc == 2) return; // 只有 AF,无 payload
if (afc == 3) { // 跳过 AF
int afl = b[i] & 0xFF;
i += 1 + afl;
}
if (i >= off + 188) return;
// 仅处理 PSI(PAT/PMT/SDT)以获取节目/名称
if (pid == PID_PAT || pid == PID_SDT || containsValue(progToPmtPid, pid)) {
SectionAssembler sa = assemblers.get(pid);
if (sa == null) {
sa = new SectionAssembler();
assemblers.put(pid, sa);
}
sa.push(b, i, off + 188 - i, pusi == 1, (b[off+3] & 0x0F)); // continuity 不做严格校验
while (sa.hasSection()) {
byte[] sec = sa.pollSection();
parseSection(pid, sec);
}
}
}
// JDK6无Map.containsValue泛型重载,自己写
static boolean containsValue(Map<Integer, Integer> map, int val) {
for (Integer v : map.values()) if (v != null && v.intValue() == val) return true;
return false;
}
/* 解析一个 PSI section(含 table_id、section_length 等完整体) */
static void parseSection(int pid, byte[] sec) {
if (sec.length < 3) return;
int tableId = sec[0] & 0xFF;
int sectionLength = ((sec[1] & 0x0F) << 8) | (sec[2] & 0xFF);
if (sectionLength + 3 != sec.length) return; // 粗略校验
if (pid == PID_PAT && tableId == 0x00) {
parsePAT(sec);
} else if (pid == PID_SDT && (tableId == 0x42 || tableId == 0x46)) {
parseSDT(sec);
} else if (tableId == 0x02) {
parsePMT(sec);
}
}
static void parsePAT(byte[] sec) {
// 解析PAT头部信息
int sectionSyntaxIndicator = (sec[1] & 0x80) >>> 7;
int sectionLength = ((sec[1] & 0x0F) << 8) | (sec[2] & 0xFF);
int transportStreamId = ((sec[3] & 0xFF) << 8) | (sec[4] & 0xFF);
int versionNumber = (sec[5] & 0x3E) >>> 1;
int currentNextIndicator = sec[5] & 0x01;
int sectionNumber = sec[6] & 0xFF;
int lastSectionNumber = sec[7] & 0xFF;
System.out.printf("[PAT] TS ID: %d, Ver: %d, Sec: %d/%d, Length: %d%n",
transportStreamId, versionNumber, sectionNumber, lastSectionNumber, sectionLength);
int pos = 8; // 跳过0..7字段
int end = sec.length - 4; // 减去CRC32
// 计算节目数量
int programCount = (end - pos) / 4;
System.out.printf("[PAT] 节目数量: %d%n", programCount);
while (pos + 4 <= end) {
int programNumber = ((sec[pos] & 0xFF) << 8) | (sec[pos+1] & 0xFF);
int pmtPid = ((sec[pos+2] & 0x1F) << 8) | (sec[pos+3] & 0xFF);
pos += 4;
if (programNumber == 0) {
// NIT PID
System.out.printf("[PAT] NIT -> PID %d%n", pmtPid);
pidToProgram.put(pmtPid, 0); // NIT的节目号为0
continue;
}
Integer old = progToPmtPid.put(programNumber, pmtPid);
if (old == null || !old.equals(pmtPid)) {
System.out.printf("[PAT] 节目 %d -> PMT PID %d%n", programNumber, pmtPid);
pidToProgram.put(pmtPid, programNumber);
}
}
// 打印CRC32校验码
int crc32 = ((sec[end] & 0xFF) << 24) |
((sec[end+1] & 0xFF) << 16) |
((sec[end+2] & 0xFF) << 8) |
(sec[end+3] & 0xFF);
System.out.printf("[PAT] CRC32: 0x%08X%n", crc32);
}
static void parsePMT(byte[] sec) {
int tableId = sec[0] & 0xFF;
int sectionSyntaxIndicator = (sec[1] & 0x80) >>> 7;
int sectionLength = ((sec[1] & 0x0F) << 8) | (sec[2] & 0xFF);
int programNumber = ((sec[3] & 0xFF) << 8) | (sec[4] & 0xFF);
int versionNumber = (sec[5] & 0x3E) >>> 1;
int currentNextIndicator = sec[5] & 0x01;
int sectionNumber = sec[6] & 0xFF;
int lastSectionNumber = sec[7] & 0xFF;
int pcrPid = ((sec[8] & 0x1F) << 8) | (sec[9] & 0xFF);
int progInfoLen = ((sec[10] & 0x0F) << 8) | (sec[11] & 0xFF);
System.out.printf("[PMT] 节目 %d, PCR PID: %d, 节目信息长度: %d%n",
programNumber, pcrPid, progInfoLen);
int pos = 12 + progInfoLen;
int end = sec.length - 4;
// 旧:仅收集 ES PID;新:同时收集详细信息
Set<Integer> es = new HashSet<Integer>();
List<EsInfo> list = new ArrayList<EsInfo>();
while (pos + 5 <= end) {
int streamType = sec[pos] & 0xFF;
int elementaryPid = ((sec[pos+1] & 0x1F) << 8) | (sec[pos+2] & 0xFF);
int esInfoLen = ((sec[pos+3] & 0x0F) << 8) | (sec[pos+4] & 0xFF);
// 取 ES 描述符(可能用于识别 AC-3/E-AC-3/DTS)
byte[] esDesc = null;
if (pos + 5 + esInfoLen <= end && esInfoLen > 0) {
esDesc = new byte[esInfoLen];
System.arraycopy(sec, pos + 5, esDesc, 0, esInfoLen);
}
String[] cls = classifyStream(streamType, esDesc); // 见③
list.add(new EsInfo(elementaryPid, streamType, cls[0], cls[1]));
es.add(elementaryPid);
pidToProgram.put(elementaryPid, programNumber);
System.out.printf("[PMT] ES PID %d, 类型: 0x%02X (%s - %s), 描述符长度: %d%n",
elementaryPid, streamType, cls[0], cls[1], esInfoLen);
pos += 5 + esInfoLen;
}
programEsPids.put(programNumber, es); // 兼容你之前的用法
programEsInfo.put(programNumber, list); // 新增:带类型的清单
System.out.printf("[PMT] 节目 %d ES PIDs %s%n", programNumber, es);
}
static String[] classifyStream(int streamType, byte[] esDesc) {
// 返回 [kind, codec];kind: video/audio/other
switch (streamType & 0xFF) {
case 0x01: return new String[]{"video", "MPEG-1 Video"};
case 0x02: return new String[]{"video", "MPEG-2 Video"};
case 0x10: return new String[]{"video", "MPEG-4 Video"};
case 0x1B: return new String[]{"video", "H.264/AVC"};
case 0x24: return new String[]{"video", "HEVC/H.265"};
case 0x03: return new String[]{"audio", "MPEG-1 Layer II"};
case 0x04: return new String[]{"audio", "MPEG-2 Audio"};
case 0x0F: return new String[]{"audio", "AAC"};
case 0x11: return new String[]{"audio", "LATM/LOAS AAC"};
case 0x06:
// 0x06(私有数据)常见通过描述符判定 AC-3 / E-AC-3 / DTS
if (hasDescriptor(esDesc, (byte)0x6A)) return new String[]{"audio", "AC-3"};
if (hasDescriptor(esDesc, (byte)0x7A)) return new String[]{"audio", "E-AC-3"};
if (hasDescriptor(esDesc, (byte)0x7B)) return new String[]{"audio", "DTS"};
return new String[]{"other", "Private data"};
default:
return new String[]{"other", String.format("stream_type 0x%02X", streamType & 0xFF)};
}
}
static boolean hasDescriptor(byte[] esDesc, byte tag) {
if (esDesc == null) return false;
int i = 0, n = esDesc.length;
while (i + 2 <= n) {
int t = esDesc[i] & 0xFF;
int l = esDesc[i+1] & 0xFF;
if (i + 2 + l > n) break;
if ((byte)t == tag) return true;
i += 2 + l;
}
return false;
}
static void parseSDT(byte[] sec) {
int tableId = sec[0] & 0xFF;
int sectionSyntaxIndicator = (sec[1] & 0x80) >>> 7;
int sectionLength = ((sec[1] & 0x0F) << 8) | (sec[2] & 0xFF);
int transportStreamId = ((sec[3] & 0xFF) << 8) | (sec[4] & 0xFF);
int versionNumber = (sec[5] & 0x3E) >>> 1;
int currentNextIndicator = sec[5] & 0x01;
int sectionNumber = sec[6] & 0xFF;
int lastSectionNumber = sec[7] & 0xFF;
int originalNetworkId = ((sec[8] & 0xFF) << 8) | (sec[9] & 0xFF);
int reservedFutureUse = sec[10] & 0xFF;
System.out.printf("[SDT] TS ID: %d, ON ID: %d, Ver: %d, Sec: %d/%d%n",
transportStreamId, originalNetworkId, versionNumber, sectionNumber, lastSectionNumber);
int pos = 11;
int end = sec.length - 4;
while (pos + 5 <= end) {
int serviceId = ((sec[pos] & 0xFF) << 8) | (sec[pos+1] & 0xFF);
int reservedFutureUse2 = (sec[pos+2] & 0xFC) >>> 2;
int eitScheduleFlag = (sec[pos+2] & 0x02) >>> 1;
int eitPresentFollowingFlag = sec[pos+2] & 0x01;
int runningStatus = (sec[pos+3] & 0xE0) >>> 5;
int freeCaMode = (sec[pos+3] & 0x10) >>> 4;
int descriptorsLoopLen = ((sec[pos+3] & 0x0F) << 8) | (sec[pos+4] & 0xFF);
int descPos = pos + 5;
int descEnd = descPos + descriptorsLoopLen;
String name = null;
System.out.printf("[SDT] 服务 ID: %d, 运行状态: %d, CA模式: %d, 描述符长度: %d%n",
serviceId, runningStatus, freeCaMode, descriptorsLoopLen);
while (descPos + 2 <= descEnd && descEnd <= end) {
int tag = sec[descPos] & 0xFF;
int len = sec[descPos+1] & 0xFF;
if (descPos + 2 + len > descEnd) break;
if (tag == 0x48 && len >= 3) { // service_descriptor
int serviceType = sec[descPos+2] & 0xFF;
int serviceProviderNameLen = sec[descPos+3] & 0xFF;
int nameLenPos = descPos + 4 + serviceProviderNameLen;
if (nameLenPos + 1 <= descPos + 2 + len) {
int serviceNameLen = sec[nameLenPos] & 0xFF;
int nameStart = nameLenPos + 1;
int nameEnd = Math.min(nameStart + serviceNameLen, descPos + 2 + len);
// JDK6没有Arrays.copyOfRange
byte[] nameBytes = new byte[nameEnd - nameStart];
System.arraycopy(sec, nameStart, nameBytes, 0, nameEnd - nameStart);
name = decodeDvbString(nameBytes);
}
System.out.printf("[SDT] 服务类型: 0x%02X, 名称: %s%n", serviceType, name);
}
descPos += 2 + len;
}
if (name != null && name.length() > 0) {
programNames.put(serviceId, name);
System.out.printf("[SDT] 节目 %d 名称='%s'%n", serviceId, name);
}
pos = descEnd;
}
// 打印CRC32校验码
int crc32 = ((sec[end] & 0xFF) << 24) |
((sec[end+1] & 0xFF) << 16) |
((sec[end+2] & 0xFF) << 8) |
(sec[end+3] & 0xFF);
System.out.printf("[SDT] CRC32: 0x%08X%n", crc32);
}
static String decodeDvbString(byte[] bs) {
if (bs == null || bs.length == 0) return "";
int offset = 0;
String charset = "GB18030"; // 国内台普遍用
int first = bs[0] & 0xFF;
if (first == 0x10 && bs.length >= 3) {
charset = "UTF-16BE";
offset = 1;
} else if (first == 0x15 || first == 0x14) {
charset = "GB2312";
offset = 1;
} else if (first == 0x1F) {
charset = "UTF-8";
offset = 1;
} else if (first < 0x20) {
// 还有其它DVB编码,通常国内不用
offset = 1;
} else {
// 无编码标志,直接用GB18030尝试
offset = 0;
}
try {
return new String(bs, offset, bs.length - offset, charset).trim();
} catch (Exception e) {
try { return new String(bs, offset, bs.length - offset, "GB2312").trim(); } catch (Exception ignore) {}
try { return new String(bs, offset, bs.length - offset, "UTF-8").trim(); } catch (Exception ignore) {}
return "";
}
}
static void maybeReport() {
long now = System.nanoTime();
if (now - lastReportNs < REPORT_INTERVAL_NS) return;
long dtNs = now - lastReportNs;
lastReportNs = now;
Map<Integer, Long> programBytes = new HashMap<Integer, Long>();
for (Map.Entry<Integer, Long> e : pidBytesCounter.entrySet()) {
int pid = e.getKey().intValue();
long bytes = e.getValue().longValue();
Integer prog = pidToProgram.get(pid);
if (prog != null) {
Long old = programBytes.get(prog);
programBytes.put(prog, (old == null ? 0L : old) + bytes);
}
}
pidBytesCounter.clear();
if (programBytes.isEmpty()) return;
System.out.println("----- 3s 统计 -----");
for (Map.Entry<Integer, Long> e : programBytes.entrySet()) {
int program = e.getKey().intValue();
long bytes = e.getValue().longValue();
double mbps = (bytes * 8.0) / (dtNs / 1e9) / 1_000_000.0;
String name = programNames.get(program);
if (name == null) name = "(解析中…)";
// PMT PID 来自 PAT
Integer pmtPid = progToPmtPid.get(program);
// 选第一个视频、前两个音频
EsInfo v = null, a1 = null, a2 = null;
List<EsInfo> list = programEsInfo.get(program);
if (list != null) {
for (int i = 0; i < list.size(); i++) {
EsInfo es = list.get(i);
if ("video".equals(es.kind) && v == null) {
v = es;
} else if ("audio".equals(es.kind)) {
if (a1 == null) a1 = es;
else if (a2 == null) a2 = es;
}
}
}
System.out.printf(
"Program %d %-16s Rate: %.3f Mbit/s PMT:%s V:%s(%s) A1:%s(%s) A2:%s(%s)%n",
program, name, mbps,
(pmtPid == null ? "-" : String.valueOf(pmtPid)),
(v==null ? "-" : String.valueOf(v.pid)), (v==null ? "-" : v.codec),
(a1==null? "-" : String.valueOf(a1.pid)), (a1==null? "-" : a1.codec),
(a2==null? "-" : String.valueOf(a2.pid)), (a2==null? "-" : a2.codec)
);
}
}
// ===== PSI Section 重组器 =====
static class SectionAssembler {
private final ByteArrayOutputStream cur = new ByteArrayOutputStream();
private Integer expectedLen = null;
private final Queue<byte[]> ready = new LinkedList<byte[]>();
void push(byte[] src, int off, int len, boolean payloadStart, int continuityCounter) {
int i = off;
if (payloadStart) {
int pointer = src[i] & 0xFF;
i += 1;
while (pointer > 0 && i < off + len) { i++; pointer--; }
startNew();
}
while (i < off + len) {
if (expectedLen == null) {
if (off + len - i < 3) { // 不够section头
write(src, i, off + len - i);
return;
}
int sl = ((src[i+1] & 0x0F) << 8) | (src[i+2] & 0xFF);
expectedLen = sl + 3;
}
int remaining = expectedLen.intValue() - cur.size();
int copy = Math.min(remaining, off + len - i);
write(src, i, copy);
i += copy;
if (cur.size() == expectedLen.intValue()) {
ready.add(cur.toByteArray());
startNew();
}
}
}
boolean hasSection() { return !ready.isEmpty(); }
byte[] pollSection() { return ready.poll(); }
private void startNew() { cur.reset(); expectedLen = null; }
private void write(byte[] src, int off, int len) { cur.write(src, off, len); }
}
} 帮我修改代码 解析 打印PAT表主要内容