/* ================== 单 WebSocket 聚合所有通道 ================== */
let ws = null;
let wsConnected = false;
// 构造要解析的全部通道清单
function buildAllChannelsPayload() {
const list = groups
.filter(g => g && g.ip && g.port)
.map(g => ({
id: g.id ?? '',
name: g.name ?? '',
ip: String(g.ip),
port: String(g.port)
}));
return { action: "start", channels: list };
}
// 根据 key 更新对应 group 的节目与速率,并刷新 UI
function applyWsUpdate(msg) {
// msg 形如:{ key:"ip:port", programs:[ {name,no,rate,pmt,vid,vtype,a1,a1t,a2,a2t}, ... ] }
const key = String(msg.key || '');
const [ip, port] = key.split(':');
if (!ip || !port) return;
const g = groups.find(x => String(x.ip) === ip && String(x.port) === port);
if (!g) return;
const progs = Array.isArray(msg.programs) ? msg.programs : [];
g.programs = progs.map(p => ({
name: p.name,
no: p.no,
rate: Number(p.rate) || 0,
pmt: p.pmt ?? '-',
vid: p.vid ?? '-',
vtype: p.vtype ?? '-',
a1: p.a1 ?? '-',
a1t: p.a1t ?? '-',
a2: p.a2 ?? '-',
a2t: p.a2t ?? '-'
}));
// 直接重渲染 summary 的速率与节目表
renderTree();
updateStatus && updateStatus('解析节目成功', 'ok');
}
function openWsAndStartAll() {
const url = (location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host + '/ws/parse-all';
// 如果已连,直接发送一次 start(支持重复覆盖订阅)
if (ws && wsConnected) {
try { ws.send(JSON.stringify(buildAllChannelsPayload())); } catch(e){ console.error(e); }
return;
}
try { ws?.close(); } catch(_) {}
ws = new WebSocket(url);
wsConnected = false;
ws.onopen = () => {
wsConnected = true;
ws.send(JSON.stringify(buildAllChannelsPayload()));
};
ws.onmessage = (ev) => {
const msg = JSON.parse(ev.data);
// 后端返回错误格式: {"ok":false,"error":"java.lang.IllegalArgumentException: Group not a multicast address"}
if (msg && msg.ok === false && msg.error) {
if (msg.error.includes("Group not a multicast address")) {
alert("解析失败:地址不是组播地址,请检查输入的 IP 是否在 224.0.0.0 – 239.255.255.255 范围内。");
} else {
alert("解析失败:" + msg.error);
}
return;
}
try {
// const msg = JSON.parse(ev.data);
if (Array.isArray(msg)) {
msg.forEach(applyWsUpdate); // 批量
} else {
applyWsUpdate(msg); // 单条
}
} catch (e) {
console.error('WS parse error', e, ev.data);
}
};
ws.onerror = (e) => {
console.error('WS error', e);
};
ws.onclose = () => {
wsConnected = false;
// 简单重连(可加指数回退)
setTimeout(() => openWsAndStartAll(), 2000);
};
}
package com.taixin.dvbc2;
import com.google.gson.*;
import com.google.gson.reflect.TypeToken;
import javax.servlet.*;
import javax.servlet.http.*;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.*;
import java.lang.reflect.Type;
import java.net.*;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.channels.spi.SelectorProvider;
import java.sql.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import static java.net.StandardProtocolFamily.INET;
import static java.net.StandardSocketOptions.SO_REUSEADDR;
/**
* 单机 Reactor 解析 + SSE(单路调试) + WebSocket 聚合(/ws/parse-all)
*
* 前端(你提供的)会连接 /ws/parse-all,并发送:
* { "action":"start", "channels":[ {"id":"","name":"","ip":"239.1.1.1","port":"59000"}, ... ] }
*
* 后端会为这些 (ip,port) 启/复用解析器,并持续向该 session 推送:
* { "key":"ip:port", "programs":[ {name,no,rate,pmt,vid,vtype,a1,a1t,a2,a2t}, ... ] }
*/
public class LoadProgramServlet extends HttpServlet {
// ===== 配置 =====
static final Gson GSON = new Gson();
static final long REPORT_INTERVAL_NS = TimeUnit.SECONDS.toNanos(8);
static final long HEARTBEAT_INTERVAL_NS = TimeUnit.SECONDS.toNanos(15);
static final int BUF_SIZE = 2048;
static final int TS_SIZE = 188;
// 共享 Reactor(NIO 线程 + 定时器 + 通道状态)
static final Reactor REACTOR = new Reactor();
// ========== (可选) 兼容原先的 SSE 单路调试入口 ==========
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
final String ip = req.getParameter("ip");
final String pstr = req.getParameter("port");
final String idStr= req.getParameter("id");
final String name = req.getParameter("name");
if (ip == null || pstr == null) {
resp.setStatus(400);
resp.setContentType("application/json;charset=UTF-8");
resp.getWriter().write("{\"error\":\"缺少参数 ip 或 port\"}");
return;
}
final int port = Integer.parseInt(pstr);
final Integer inputChannelId = (idStr == null || idStr.isEmpty()) ? null : Integer.valueOf(idStr);
// SSE 响应
resp.setStatus(200);
resp.setHeader("Cache-Control", "no-cache");
resp.setHeader("Connection", "keep-alive");
resp.setHeader("X-Accel-Buffering", "no");
resp.setContentType("text/event-stream;charset=UTF-8");
resp.setCharacterEncoding("UTF-8");
final AsyncContext ac = req.startAsync();
ac.setTimeout(0);
final PrintWriter writer = resp.getWriter();
final SseSubscriber sub = new SseSubscriber(writer, ac);
final ChannelKey key = new ChannelKey(ip, port);
try {
REACTOR.ensureChannel(key, inputChannelId, name);
REACTOR.addSseSubscriber(key, sub);
} catch (Exception e) {
safeSendSSE(sub, "{\"error\":\"通道启动失败: " + safeMsg(e) + "\"}");
completeQuietly(ac);
return;
}
ac.addListener(new AsyncListener() {
@Override public void onComplete(AsyncEvent event) { cleanup(); }
@Override public void onTimeout(AsyncEvent event) { cleanup(); }
@Override public void onError(AsyncEvent event) { cleanup(); }
@Override public void onStartAsync(AsyncEvent event) {}
private void cleanup() { REACTOR.removeSseSubscriber(key, sub); }
});
safeSendSSE(sub, "{\"status\":\"connected\"}");
}
/* ============= SSE 辅助 ============= */
static final class SseSubscriber {
final PrintWriter writer;
final AsyncContext ac;
volatile long lastHeartbeatNs = System.nanoTime();
SseSubscriber(PrintWriter w, AsyncContext ac) { this.writer = w; this.ac = ac; }
synchronized void send(String json) {
if (writer.checkError()) { complete(); return; }
writer.write("data: " + json + "\n\n");
writer.flush();
if (writer.checkError()) { complete(); }
}
synchronized void heartbeatIfNeeded(long now) {
if (now - lastHeartbeatNs < HEARTBEAT_INTERVAL_NS) return;
if (writer.checkError()) { complete(); return; }
writer.write(": keepalive\n\n");
writer.flush();
lastHeartbeatNs = now;
if (writer.checkError()) { complete(); }
}
synchronized void complete() { completeQuietly(ac); }
}
/* ============= 统一通道键 ============= */
static final class ChannelKey {
final String ip; final int port;
ChannelKey(String ip, int port){ this.ip=ip; this.port=port; }
@Override public String toString(){ return ip + ":" + port; }
@Override public boolean equals(Object o){
if (!(o instanceof ChannelKey)) return false;
ChannelKey k=(ChannelKey)o;
return port==k.port && ip.equals(k.ip);
}
@Override public int hashCode(){ return ip.hashCode()*31 + port; }
}
/* ============= Reactor:NIO 收包 + 定时聚合广播 ============= */
static final class Reactor {
private final Selector selector;
private final Thread ioThread;
private final ScheduledExecutorService scheduler =
Executors.newSingleThreadScheduledExecutor(r -> { Thread t = new Thread(r, "ReportScheduler"); t.setDaemon(true); return t; });
// 每个通道的注册信息
private static final class Reg {
final DatagramChannel ch;
final ChannelState st;
final Set<SseSubscriber> sseSubs = ConcurrentHashMap.newKeySet(); // 单路调试用
final AtomicBoolean alive = new AtomicBoolean(true);
final NetworkInterface nif;
Reg(DatagramChannel ch, ChannelState st, NetworkInterface nif){ this.ch = ch; this.st = st; this.nif = nif; }
}
// key -> 通道注册
private final Map<ChannelKey, Reg> regs = new ConcurrentHashMap<>();
// WebSocket 订阅关系:某 session 订阅了哪些 key;某 key 有哪些 session
private final Map<Session, Set<ChannelKey>> wsSubsBySession = new ConcurrentHashMap<>();
private final Map<ChannelKey, Set<Session>> wsSessionsByKey = new ConcurrentHashMap<>();
Reactor() {
try { this.selector = SelectorProvider.provider().openSelector(); }
catch (IOException e) { throw new RuntimeException(e); }
// I/O 线程:收 UDP -> 解析 TS
this.ioThread = new Thread(this::ioLoop, "UdpReactor");
this.ioThread.setDaemon(true);
this.ioThread.start();
// 定时器:聚合 -> 推送(SSE+WS)
long periodMs = Math.max(1000, TimeUnit.NANOSECONDS.toMillis(REPORT_INTERVAL_NS/2));
scheduler.scheduleAtFixedRate(this::tick, periodMs, periodMs, TimeUnit.MILLISECONDS);
}
/* 公开:确保通道存在(若无则创建并 join 组播) */
void ensureChannel(ChannelKey key, Integer inputChannelId, String inputChannelName) {
regs.computeIfAbsent(key, k -> {
try {
DatagramChannel ch = DatagramChannel.open(INET);
ch.setOption(SO_REUSEADDR, true);
ch.configureBlocking(false);
ch.bind(new InetSocketAddress(k.port));
NetworkInterface nif = pickMulticastInterface();
InetAddress group = InetAddress.getByName(k.ip);
ch.join(group, nif);
ch.register(selector, SelectionKey.OP_READ, k);
ChannelState st = new ChannelState(k);
st.inputChannelId = inputChannelId;
st.inputChannelName = inputChannelName;
wsSessionsByKey.putIfAbsent(k, ConcurrentHashMap.newKeySet());
return new Reg(ch, st, nif);
} catch (Exception e) {
throw new RuntimeException(e);
}
});
// 补齐 meta
Reg reg = regs.get(key);
if (reg != null) {
if (reg.st.inputChannelId == null && inputChannelId != null) reg.st.inputChannelId = inputChannelId;
if ((reg.st.inputChannelName == null || reg.st.inputChannelName.isEmpty()) && inputChannelName != null)
reg.st.inputChannelName = inputChannelName;
}
}
/* SSE 订阅管理(可选) */
void addSseSubscriber(ChannelKey key, SseSubscriber sub) { regs.get(key).sseSubs.add(sub); }
void removeSseSubscriber(ChannelKey key, SseSubscriber sub) {
Reg reg = regs.get(key); if (reg!=null) reg.sseSubs.remove(sub);
}
/* WS 订阅管理 */
void wsSubscribe(Session s, Collection<ChannelKey> keys) {
wsSubsBySession.computeIfAbsent(s, k -> ConcurrentHashMap.newKeySet()).clear();
for (ChannelKey key : keys) {
ensureChannel(key, null, null);
wsSubsBySession.get(s).add(key);
wsSessionsByKey.computeIfAbsent(key, k -> ConcurrentHashMap.newKeySet()).add(s);
}
}
void wsUnsubscribeAll(Session s) {
Set<ChannelKey> ks = wsSubsBySession.remove(s);
if (ks != null) {
for (ChannelKey k : ks) {
Set<Session> set = wsSessionsByKey.get(k);
if (set != null) set.remove(s);
}
}
}
/* UDP -> TS 解析 */
private void ioLoop() {
ByteBuffer buf = ByteBuffer.allocateDirect(BUF_SIZE);
while (true) {
try {
selector.select(500);
Set<SelectionKey> keys = selector.selectedKeys();
if (keys.isEmpty()) continue;
Iterator<SelectionKey> it = keys.iterator();
while (it.hasNext()) {
SelectionKey sk = it.next(); it.remove();
if (!sk.isValid() || !sk.isReadable()) continue;
DatagramChannel ch = (DatagramChannel) sk.channel();
ChannelKey key = (ChannelKey) sk.attachment();
Reg reg = regs.get(key);
if (reg == null || !reg.alive.get()) continue;
buf.clear();
if (ch.receive(buf) == null) continue;
int n = buf.position();
if (n <= 0) continue;
buf.flip();
byte[] arr = new byte[n];
buf.get(arr);
int off = 0;
if (n >= 13 && arr[0] != 0x47 && arr[12] == 0x47) { off = 12; }
int left = n - off;
if (left > 0 && arr[off] != 0x47) {
int sync = findTsSync(arr, n, off);
if (sync >= 0) off = sync; else continue;
left = n - off;
}
int aligned = left - (left % TS_SIZE);
for (int i = 0; i + TS_SIZE <= aligned; i += TS_SIZE) {
TsParsers.parseTsPacket(reg.st, arr, off + i);
}
}
} catch (Throwable t) {
t.printStackTrace();
}
}
}
/* 定时聚合 + 广播(SSE 心跳 + WS 推送) */
private void tick() {
long now = System.nanoTime();
regs.forEach((key, reg) -> {
// SSE 心跳
for (SseSubscriber s : reg.sseSubs) s.heartbeatIfNeeded(now);
// 到点聚合
if (reg.st.needReport(now, REPORT_INTERVAL_NS)) {
// 生成该通道的节目数组 JSON(字符串)
String progJson = reg.st.buildProgramArrayJson(REPORT_INTERVAL_NS);
reg.st.afterReport(now);
if (progJson != null) {
// SSE:按你原来的格式(数组)发
for (SseSubscriber s : reg.sseSubs) s.send(progJson);
// WS:包装成 {key, programs:[...]} 发给订阅了该 key 的所有 session
Set<Session> sessions = wsSessionsByKey.get(key);
if (sessions != null && !sessions.isEmpty()) {
String wrapped = "{\"key\":\"" + key.toString() + "\",\"programs\":" + progJson + "}";
// 清理死 session
List<Session> dead = new ArrayList<>();
for (Session ss : sessions) {
if (ss == null || !ss.isOpen()) { dead.add(ss); continue; }
try { ss.getAsyncRemote().sendText(wrapped); }
catch (Throwable t) { dead.add(ss); }
}
sessions.removeAll(dead);
}
}
}
});
}
// 选择一个可用的组播网卡
private NetworkInterface pickMulticastInterface() throws SocketException {
Enumeration<NetworkInterface> ifs = NetworkInterface.getNetworkInterfaces();
while (ifs.hasMoreElements()) {
NetworkInterface nif = ifs.nextElement();
if (!nif.isUp() || nif.isLoopback() || !nif.supportsMulticast()) continue;
return nif;
}
return NetworkInterface.getByInetAddress(InetAddress.getLoopbackAddress());
}
}
/* ============= 通道解析状态与输出 ============= */
static final class ChannelState {
final ChannelKey key;
volatile Integer inputChannelId;
volatile String inputChannelName;
final Map<Integer, Integer> progToPmtPid = new HashMap<>();
final Map<Integer, String> programNames = new HashMap<>();
final Map<Integer, Integer> pidToProgram = new HashMap<>();
final Map<Integer, Long> pidBytes = new HashMap<>();
final Map<Integer, SectionAssembler> assemblers = new HashMap<>();
final Map<Integer, List<EsInfo>> programEsInfo = new HashMap<>();
volatile String lastSavedSignature = null;
private long lastReportTs = System.nanoTime();
ChannelState(ChannelKey key){ this.key = key; }
boolean needReport(long now, long intervalNs){ return now - lastReportTs >= intervalNs; }
void afterReport(long now){ lastReportTs = now; }
// 输出:返回“节目数组”的 JSON 字符串(例如:[ {...} ]),给 WS 再包一层
String buildProgramArrayJson(long intervalNs) {
if (pidBytes.isEmpty()) return null;
Map<Integer, Long> programBytes = new HashMap<>();
for (Map.Entry<Integer, Long> e : pidBytes.entrySet()) {
int pid = e.getKey();
long bytes = e.getValue();
Integer prog = pidToProgram.get(pid);
if (prog != null) programBytes.merge(prog, bytes, Long::sum);
}
pidBytes.clear();
if (programBytes.isEmpty()) return null;
List<Map<String,Object>> programs = new ArrayList<>();
for (Map.Entry<Integer, Long> e : programBytes.entrySet()) {
int program = e.getKey();
long bytes = e.getValue();
double mbps = (bytes * 8.0) / (intervalNs / 1e9) / 1_000_000.0;
String name = programNames.getOrDefault(program, "(解析中…)");
Integer pmtPid = progToPmtPid.get(program);
EsInfo v = null, a1 = null, a2 = null;
List<EsInfo> list = programEsInfo.get(program);
if (list != null) {
for (EsInfo es : list) {
if ("video".equals(es.kind)) { if (v == null) v = es; }
else if ("audio".equals(es.kind)) {
if (a1 == null) a1 = es; else if (a2 == null) a2 = es;
}
}
}
Map<String, Object> one = new LinkedHashMap<>();
one.put("name", name);
one.put("no", program);
one.put("rate", mbps);
one.put("pmt", pmtPid == null ? null : pmtPid);
one.put("vid", v == null ? null : v.pid);
one.put("vtype", v == null ? null : v.codec);
one.put("a1", a1 == null ? null : a1.pid);
one.put("a1t", a1 == null ? null : a1.codec);
one.put("a2", a2 == null ? null : a2.pid);
one.put("a2t", a2 == null ? null : a2.codec);
programs.add(one);
// —— 入库去重:仅在结构/类型/PID变化时 ——(只保存第一个节目)
final String sig = makeSignature(
name, program, pmtPid,
(v==null?null:v.pid),(v==null?null:v.codec),
(a1==null?null:a1.pid),(a1==null?null:a1.codec),
(a2==null?null:a2.pid),(a2==null?null:a2.codec)
);
if (lastSavedSignature == null || !lastSavedSignature.equals(sig)) {
lastSavedSignature = sig;
try {
// ✅ 兜底:名字没传时用 ip:port,避免 NULL 破坏存储过程
final String safeName = (inputChannelName == null || inputChannelName.isEmpty())
? (key.ip + ":" + key.port)
: inputChannelName;
SaveProgramData(
inputChannelId, safeName, // <== 使用 safeName
key.ip, key.port,
name, program, pmtPid,
(v==null?null:v.pid),(v==null?null:v.codec),
(a1==null?null:a1.pid),(a1==null?null:a1.codec),
(a2==null?null:a2.pid),(a2==null?null:a2.codec)
);
} catch (Exception ex) {
System.err.println("[SaveProgramData][ERROR] " + ex.getMessage());
}
}
break; // 一个输入通道只有一个节目
}
return GSON.toJson(programs);
}
/* 解析结构 */
static final class EsInfo { final int pid, streamType; final String kind, codec;
EsInfo(int pid, int st, String kind, String codec){ this.pid=pid; this.streamType=st; this.kind=kind; this.codec=codec; }
}
static final class SectionAssembler {
private final ByteArrayOutputStream cur = new ByteArrayOutputStream();
private Integer expectedLen = null;
private final Queue<byte[]> ready = new ArrayDeque<>();
void push(byte[] src, int off, int len, boolean payloadStart, int cc) {
final int end = off + len; int i = off;
if (payloadStart) {
if (i >= end) return;
int pointer = src[i] & 0xFF; i += 1;
if (i + pointer > end) return;
i += pointer; startNew();
}
while (i < end) {
int remaining = end - i;
if (expectedLen == null) {
if (remaining < 3) { write(src, i, remaining); return; }
int sl = ((src[i+1] & 0x0F) << 8) | (src[i+2] & 0xFF);
expectedLen = sl + 3;
}
int need = expectedLen - cur.size();
if (need <= 0) { startNew(); continue; }
int copy = Math.min(need, remaining);
write(src, i, copy); i += copy;
if (cur.size() == expectedLen) { ready.add(cur.toByteArray()); startNew(); }
}
}
private void write(byte[] src, int off, int len){ if (len>0) cur.write(src, off, len); }
boolean hasSection(){ return !ready.isEmpty(); }
byte[] pollSection(){ return ready.poll(); }
private void startNew(){ cur.reset(); expectedLen = null; }
}
private static String makeSignature(String name, Integer progNo,
Integer pmt, Integer vid, String vtype,
Integer a1, String a1t,
Integer a2, String a2t) {
return String.valueOf(Objects.hash(safe(name), progNo, pmt, vid, safe(vtype), a1, safe(a1t), a2, safe(a2t)));
}
private static String safe(String s){ return (s==null?"":s); }
}
/* ============= TS 解析器(与你之前一致) ============= */
static final class TsParsers {
static void parseTsPacket(ChannelState st, byte[] buf, int off) {
if ((buf[off] & 0xFF) != 0x47) return;
int tei = (buf[off + 1] & 0x80) >>> 7;
int pusi = (buf[off + 1] & 0x40) >>> 6;
int pid = ((buf[off + 1] & 0x1F) << 8) | (buf[off + 2] & 0xFF);
int afc = (buf[off + 3] & 0x30) >>> 4;
if (tei == 1) return;
if (pid != 0x1FFF) st.pidBytes.merge(pid, 188L, Long::sum);
int i = off + 4;
if (afc == 2) return;
if (afc == 3) {
int afl = buf[i] & 0xFF; i += 1 + afl;
}
if (i >= off + 188) return;
if (pid == 0x0000 || pid == 0x0011 || containsValue(st.progToPmtPid, pid)) {
ChannelState.SectionAssembler sa = st.assemblers.get(pid);
if (sa == null) { sa = new ChannelState.SectionAssembler(); st.assemblers.put(pid, sa); }
sa.push(buf, i, off + 188 - i, pusi == 1, (buf[off + 3] & 0x0F));
while (sa.hasSection()) {
byte[] sec = sa.pollSection();
parseSection(st, pid, sec);
}
}
}
private static boolean containsValue(Map<Integer,Integer> map, int val){
for (Integer v : map.values()) if (v != null && v == val) return true;
return false;
}
private static void parseSection(ChannelState st, 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 == 0x0000 && tableId == 0x00) parsePAT(st, sec);
else if (pid == 0x0011 && (tableId == 0x42 || tableId == 0x46)) parseSDT(st, sec);
else if (tableId == 0x02) parsePMT(st, sec);
}
private static void parsePAT(ChannelState st, byte[] sec) {
int pos = 8, end = sec.length - 4;
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) continue;
st.progToPmtPid.put(programNumber, pmtPid);
st.pidToProgram.put(pmtPid, programNumber);
}
}
private static void parsePMT(ChannelState st, byte[] sec) {
int programNumber = ((sec[3] & 0xFF) << 8) | (sec[4] & 0xFF);
if (!st.programNames.containsKey(programNumber)) st.programNames.put(programNumber, "Program " + programNumber);
int progInfoLen = ((sec[10] & 0x0F) << 8) | (sec[11] & 0xFF);
int pos = 12 + progInfoLen;
int end = sec.length - 4;
List<ChannelState.EsInfo> list = new ArrayList<>();
while (pos + 5 <= end) {
int streamType = sec[pos] & 0xFF;
int esPid = ((sec[pos + 1] & 0x1F) << 8) | (sec[pos + 2] & 0xFF);
int esInfoLen = ((sec[pos + 3] & 0x0F) << 8) | (sec[pos + 4] & 0xFF);
pos += 5;
byte[] esDesc = null;
if (esInfoLen > 0 && pos + esInfoLen <= end) esDesc = Arrays.copyOfRange(sec, pos, pos + esInfoLen);
pos += esInfoLen;
st.pidToProgram.put(esPid, programNumber);
String[] cls = classifyStream(streamType, esDesc);
list.add(new ChannelState.EsInfo(esPid, streamType, cls[0], cls[1]));
}
st.programEsInfo.put(programNumber, list);
}
private static void parseSDT(ChannelState st, byte[] sec) {
int pos = 11, end = sec.length - 4;
while (pos + 5 <= end) {
int serviceId = ((sec[pos] & 0xFF) << 8) | (sec[pos + 1] & 0xFF);
int descriptorsLoopLen = ((sec[pos + 3] & 0x0F) << 8) | (sec[pos + 4] & 0xFF);
int descPos = pos + 5;
int descEnd = descPos + descriptorsLoopLen;
String name = null;
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 >= 5) {
int base = descPos + 2;
int provLen = sec[base + 1] & 0xFF;
int nameLenPos = base + 2 + provLen;
if (nameLenPos < descPos + 2 + len) {
int nameLen = sec[nameLenPos] & 0xFF;
int nameStart = nameLenPos + 1;
int nameEnd = Math.min(nameStart + nameLen, descPos + 2 + len);
byte[] nameBytes = Arrays.copyOfRange(sec, nameStart, nameEnd);
name = decodeDvbString(nameBytes);
}
}
descPos += 2 + len;
}
if (name != null && !name.isEmpty()) st.programNames.put(serviceId, name);
pos = descEnd;
}
}
private static String[] classifyStream(int streamType, byte[] esDesc) {
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 Visual"};
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:
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)};
}
}
private 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;
}
private 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) { offset = 1; }
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 "";
}
}
}
/* ============= WebSocket 端点:/ws/parse-all ============= */
@ServerEndpoint(value = "/ws/parse-all")
public static class ParseAllEndpoint {
// 复用 REACTOR
private static final Type START_PAYLOAD_TYPE = new TypeToken<Map<String, Object>>(){}.getType();
@OnOpen
public void onOpen(Session session) {
// nothing
}
@OnMessage
public void onMessage(String text, Session session) {
try {
Map<String, Object> root = GSON.fromJson(text, START_PAYLOAD_TYPE);
String action = String.valueOf(root.getOrDefault("action", "start"));
if (!"start".equalsIgnoreCase(action)) return;
Object chs = root.get("channels");
if (!(chs instanceof List)) return;
List<Map<String, Object>> channels = (List<Map<String, Object>>) chs;
if (channels.isEmpty()) return;
List<ChannelKey> keys = new ArrayList<>();
for (Map<String, Object> c : channels) {
String ipStr = c.get("ip") == null ? null : String.valueOf(c.get("ip")).trim();
String portStr = c.get("port") == null ? null : String.valueOf(c.get("port")).trim();
if (ipStr == null || portStr == null) continue;
Integer id = parseIntOrNull(c.get("id"));
String name = c.get("name") == null ? null : String.valueOf(c.get("name")).trim();
try {
int p = Integer.parseInt(portStr);
ChannelKey key = new ChannelKey(ipStr, p);
keys.add(key);
// ✅ 关键:把 meta 写进解析器(首次创建或补齐空白)
REACTOR.ensureChannel(key, id, name);
} catch (NumberFormatException ignore) {}
}
// 覆盖订阅
REACTOR.wsSubscribe(session, keys);
session.getAsyncRemote().sendText("{\"ok\":true,\"subscribed\":" + keys.size() + "}");
} catch (Throwable t) {
try { session.getAsyncRemote().sendText("{\"ok\":false,\"error\":\"" + safeMsg(t) + "\"}"); } catch (Exception ignore){}
}
}
// 工具:容忍 id 传字符串或数字
private static Integer parseIntOrNull(Object v) {
if (v == null) return null;
if (v instanceof Number) return ((Number)v).intValue();
try { return Integer.valueOf(String.valueOf(v).trim()); } catch (Exception e) { return null; }
}
@OnClose
public void onClose(Session session, CloseReason reason) {
REACTOR.wsUnsubscribeAll(session);
}
@OnError
public void onError(Session session, Throwable thr) {
REACTOR.wsUnsubscribeAll(session);
}
}
/* ============= 通用工具 ============= */
private static void safeSendSSE(SseSubscriber s, String json) {
try { s.send(json); } catch (Throwable ignore) {}
}
private static void completeQuietly(AsyncContext ac) { try { ac.complete(); } catch (Throwable ignore) {} }
private static String safeMsg(Throwable t) {
String m = t.getMessage(); return (m == null ? t.getClass().getSimpleName() : m).replace("\"", "'");
}
private static int findTsSync(byte[] b, int n, int start) {
for (int i = start; i + TS_SIZE < n; ++i) {
if (b[i] != 0x47) continue;
if (i + 2*TS_SIZE < n) {
if (b[i + TS_SIZE] == 0x47 && b[i + 2*TS_SIZE] == 0x47) return i;
} else return i;
}
return -1;
}
/* ============= 入库:保持你原来的存储过程调用 ============= */
static void SaveProgramData(Integer inputChannelId,
String inputChannelName,
String ip, int port,
String name, Integer progNo,
Integer pmtPid,
Integer videoPid, String videoType,
Integer audio1Pid, String audio1Type,
Integer audio2Pid, String audio2Type) throws Exception {
Properties props = new Properties();
try {
InputStream in = LoadProgramServlet.class.getClassLoader().getResourceAsStream("jdbc.properties");
if (in != null) {
props.load(in);
String DB_URL = props.getProperty("jdbc.url");
String DB_USER = props.getProperty("jdbc.username");
String DB_PWD = props.getProperty("jdbc.password");
Class.forName(props.getProperty("jdbc.driverClassName"));
Connection c = null; CallableStatement cs = null;
try {
c = DriverManager.getConnection(DB_URL, DB_USER, DB_PWD);
String sql = "{call sp_SaveProgramData(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)}";
cs = c.prepareCall(sql);
cs.setString(1, inputChannelName);
cs.setInt(2, port);
cs.setString(3, ip);
cs.setString(4, name);
cs.setInt(5, progNo==null?0:progNo);
cs.setInt(6, pmtPid==null?0:pmtPid);
cs.setInt(7, videoPid==null?0:videoPid);
cs.setString(8, videoType==null?"-":videoType);
cs.setInt(9, audio1Pid==null?0:audio1Pid);
cs.setString(10, audio1Type==null?"-":audio1Type);
cs.setInt(11, audio2Pid==null?0:audio2Pid);
cs.setString(12, audio2Type==null?"-":audio2Type);
cs.registerOutParameter(13, Types.INTEGER);
cs.execute();
int ret = cs.getInt(13);
// ret==1 插入成功;保留静默
} finally {
try { if (cs!=null) cs.close(); } catch (Exception ignore){}
try { if (c!=null) c.close(); } catch (Exception ignore){}
}
}
} catch (Exception e) {
throw new ServletException("Failed to load database properties", e);
}
}
} /* ================== 事件绑定:点击“解析节目” => 单 WS 解析全部 ================== */
document.getElementById('btnParse')?.addEventListener('click', () => {
if (!groups.length) { alert('没有可解析的输入通道'); return; }
openWsAndStartAll();
});
点击解析节目 获取不到 rate
最新发布