package com.taixin.dvbc2;
import com.google.gson.Gson;
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.net.*;
import java.sql.*;
import java.util.*;
import java.util.concurrent.*;
/**
* SSE + Servlet 3.0 异步版本(含 PMT/ES 细节、入库去重)
* - 每个 (ip,port) 唯一一个 ChannelWorker
* - 多个订阅者共享该 worker 的统计输出
* - 输出字段:name/no/rate + pmt/vid/vtype/a1/a1t/a2/a2t
* - 仅当节目结构或PID/类型变化时触发 SaveProgramData(签名去重)
*/
public class LoadProgramServlet extends HttpServlet {
private static final Gson GSON = new Gson();
// 推送间隔(纳秒):8 秒
private static final long REPORT_INTERVAL_NS = TimeUnit.SECONDS.toNanos(8);
// 心跳间隔:15 秒(SSE 注释行)
private static final long HEARTBEAT_INTERVAL_NS = TimeUnit.SECONDS.toNanos(15);
// 所有工作线程:Key -> ChannelWorker
private static final Map<ChannelKey, ChannelWorker> WORKERS = new ConcurrentHashMap<>();
// ===== Servlet 入口 =====
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
final String ip = req.getParameter("ip");
final String portStr = req.getParameter("port");
final String idStr = req.getParameter("id");
final String inputChannelName = req.getParameter("name");
if (ip == null || portStr == null) {
resp.setStatus(400);
resp.setContentType("application/json;charset=UTF-8");
resp.getWriter().write("{\"error\":\"缺少参数 ip 或 port\"}");
return;
}
final int port = Integer.parseInt(portStr);
final Integer inputChannelId = (idStr == null || idStr.trim().isEmpty()) ? null : Integer.valueOf(idStr);
final ChannelKey key = new ChannelKey(ip, port);
// 设置 SSE 头
resp.setStatus(200);
resp.setContentType("text/event-stream;charset=UTF-8");
resp.setCharacterEncoding("UTF-8");
resp.setHeader("Cache-Control", "no-cache");
resp.setHeader("Connection", "keep-alive");
resp.setHeader("X-Accel-Buffering", "no");
// 开启异步
final AsyncContext ac = req.startAsync(req, resp);
ac.setTimeout(0); // 永不超时
final PrintWriter writer = resp.getWriter();
final Subscriber sub = new Subscriber(writer, ac);
// 获取/创建 worker,并登记订阅者
ChannelWorker worker = WORKERS.compute(key, (k, w) -> {
if (w == null || !w.isAliveWorker()) {
ChannelWorker nw = new ChannelWorker(
k,
REPORT_INTERVAL_NS,
HEARTBEAT_INTERVAL_NS,
inputChannelId,
inputChannelName
);
try {
nw.start();
} catch (Exception e) {
safeSendSSE(sub, "{\"error\":\"通道启动失败: " + e.getMessage() + "\"}");
completeQuietly(ac);
return null;
}
w = nw;
} else {
// 已有 worker,补齐一次元信息(为空时才设置,避免覆盖)
w.setChannelMetaIfEmpty(inputChannelId, inputChannelName);
}
w.addSubscriber(sub);
return w;
});
if (worker == null) {
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() {
ChannelWorker w = WORKERS.get(key);
if (w != null) {
w.removeSubscriber(sub);
if (w.subscriberCount() == 0) {
w.stopWorker();
WORKERS.remove(key);
}
}
}
});
// 立即发一条“已连接”
safeSendSSE(sub, "{\"status\":\"connected\"}");
}
// ====== SSE 订阅者对象 ======
static final class Subscriber {
final PrintWriter writer;
final AsyncContext ac;
volatile long lastHeartbeatNs = System.nanoTime();
Subscriber(PrintWriter writer, AsyncContext ac) {
this.writer = writer;
this.ac = ac;
}
synchronized void sendData(String json) {
if (writer.checkError()) { complete(); return; }
writer.write("data: " + json + "\n\n"); // SSE 以空行分隔
writer.flush();
if (writer.checkError()) { complete(); }
}
synchronized void heartbeatIfNeeded(long now, long heartbeatIntervalNs) {
if (now - lastHeartbeatNs < heartbeatIntervalNs) 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 boolean equals(Object o) {
if (!(o instanceof ChannelKey)) return false;
ChannelKey that = (ChannelKey)o;
return this.port == that.port && this.ip.equals(that.ip);
}
@Override public int hashCode() { return ip.hashCode() * 31 + port; }
@Override public String toString() { return ip + ":" + port; }
}
// ====== 每个通道一个工作线程:接收 TS、解析 PSI、聚合速率、广播 ======
static final class ChannelWorker extends Thread {
private final ChannelKey key;
private final long reportIntervalNs;
private final long heartbeatIntervalNs;
private final Set<Subscriber> subscribers = ConcurrentHashMap.newKeySet();
// —— 请求元数据(来自 doGet)——
private volatile Integer inputChannelId; // InputChannelId
private volatile String inputChannelName; // InputChannelName
// —— 每个通道独有的解析状态(不要 static)——
private final Map<Integer, Integer> progToPmtPid = new HashMap<>(); // program -> PMT PID
private final Map<Integer, String> programNames = new HashMap<>(); // program -> name(优先 SDT)
private final Map<Integer, Integer> pidToProgram = new HashMap<>(); // pid -> program(用于速率归集)
private final Map<Integer, Long> pidBytesCounter = new HashMap<>(); // pid -> bytes since last report
private final Map<Integer, SectionAssembler> assemblers = new HashMap<>();
// 新增:每个节目下的 ES 列表(带类型/编码)
private final Map<Integer, List<EsInfo>> programEsInfo = new HashMap<>();
// 去重签名:记录“最近一次已保存”的结构快照(字符串签名)
private volatile String lastSavedSignature = null;
// —— ES 描述结构 ——
static final class EsInfo {
final int pid;
final int streamType;
final String kind; // video / audio / other
final String codec; // H.264/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;
}
}
private volatile boolean running = true;
private MulticastSocket sock;
private InetAddress group;
ChannelWorker(ChannelKey key,
long reportIntervalNs,
long heartbeatIntervalNs,
Integer inputChannelId,
String inputChannelName) {
super("ChannelWorker-" + key);
this.key = key;
this.reportIntervalNs = reportIntervalNs;
this.heartbeatIntervalNs = heartbeatIntervalNs;
this.inputChannelId = inputChannelId;
this.inputChannelName = inputChannelName;
setDaemon(true);
}
// 复用已存在的 worker 时补齐元信息
void setChannelMetaIfEmpty(Integer id, String name) {
if (this.inputChannelId == null && id != null) this.inputChannelId = id;
if ((this.inputChannelName == null || this.inputChannelName.isEmpty()) && name != null) {
this.inputChannelName = name;
}
}
boolean isAliveWorker() { return running && isAlive(); }
void addSubscriber(Subscriber s) { subscribers.add(s); }
void removeSubscriber(Subscriber s) { subscribers.remove(s); }
int subscriberCount() { return subscribers.size(); }
void stopWorker() {
running = false;
try { if (sock != null) sock.close(); } catch (Throwable ignore) {}
}
@Override public void run() {
try {
sock = new MulticastSocket(key.port);
group = InetAddress.getByName(key.ip);
sock.joinGroup(group);
final int MTU = 7 * 188;
byte[] buf = new byte[MTU];
DatagramPacket pkt = new DatagramPacket(buf, buf.length);
long lastReport = System.nanoTime();
while (running) {
if (subscribers.isEmpty()) { Thread.sleep(80); continue; }
sock.receive(pkt);
int len = pkt.getLength();
int off = 0;
while (off + 188 <= len) {
parseTsPacket(buf, off);
off += 188;
}
long now = System.nanoTime();
// 心跳
for (Subscriber s : subscribers) s.heartbeatIfNeeded(now, heartbeatIntervalNs);
// 定时汇报
if (now - lastReport >= reportIntervalNs) {
String json = buildProgramReportJson(reportIntervalNs);
if (json != null) broadcast(json);
lastReport = now;
}
}
} catch (SocketException ignore) {
// close() 时会来这
} catch (Throwable t) {
broadcast("{\"error\":\"worker-crashed: " + safeMsg(t) + "\"}");
} finally {
try { if (group != null && sock != null) sock.leaveGroup(group); } catch (Throwable ignore) {}
try { if (sock != null) sock.close(); } catch (Throwable ignore) {}
for (Subscriber s : subscribers) completeQuietly(s.ac);
subscribers.clear();
}
}
private void broadcast(String json) {
for (Subscriber s : subscribers) s.sendData(json);
}
// === TS/PSI 解析 ===
private void parseTsPacket(byte[] buf, int off) {
if ((buf[off] & 0xFF) != 0x47) return; // sync
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) {
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 = buf[i] & 0xFF;
i += 1 + afl;
}
if (i >= off + 188) return;
// 只重组 PAT/PMT/SDT
if (pid == 0x0000 || pid == 0x0011 || containsValue(progToPmtPid, pid)) {
SectionAssembler sa = assemblers.get(pid);
if (sa == null) { sa = new SectionAssembler(); 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(pid, sec);
}
}
}
private boolean containsValue(Map<Integer, Integer> map, int val) {
for (Integer v : map.values()) if (v != null && v == val) return true;
return false;
}
private 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 == 0x0000 && tableId == 0x00) {
parsePAT(sec);
} else if (pid == 0x0011 && (tableId == 0x42 || tableId == 0x46)) {
parseSDT(sec);
} else if (tableId == 0x02) {
parsePMT(sec);
}
}
private void parsePAT(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;
progToPmtPid.put(programNumber, pmtPid);
pidToProgram.put(pmtPid, programNumber);
}
}
private void parsePMT(byte[] sec) {
int programNumber = ((sec[3] & 0xFF) << 8) | (sec[4] & 0xFF);
// 占位名(后续由 SDT 覆盖)
if (!programNames.containsKey(programNumber)) {
programNames.put(programNumber, "Program " + programNumber);
}
int progInfoLen = ((sec[10] & 0x0F) << 8) | (sec[11] & 0xFF);
int pos = 12 + progInfoLen;
int end = sec.length - 4;
List<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;
// ES 描述符区域
byte[] esDesc = null;
if (esInfoLen > 0 && pos + esInfoLen <= end) {
esDesc = Arrays.copyOfRange(sec, pos, pos + esInfoLen);
}
pos += esInfoLen;
// 归属 program,便于速率归集
pidToProgram.put(esPid, programNumber);
// 识别类型/编码
String[] cls = classifyStream(streamType, esDesc);
list.add(new EsInfo(esPid, streamType, cls[0], cls[1]));
}
programEsInfo.put(programNumber, list);
}
private void parseSDT(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) { // service_descriptor
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()) {
programNames.put(serviceId, name);
}
pos = descEnd;
}
}
// === 码率汇总 + JSON 输出(含“变更才保存”) ===
private String buildProgramReportJson(long intervalNs) {
if (pidBytesCounter.isEmpty()) return null;
Map<Integer, Long> programBytes = new HashMap<>();
for (Map.Entry<Integer, Long> e : pidBytesCounter.entrySet()) {
int pid = e.getKey();
long bytes = e.getValue();
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 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.get(program);
if (name == null) name = "(解析中…)";
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);
// ==== 入库去重:仅当签名变化时才保存 ====
// 只保存“首个节目”(你的需求:一个输入通道只有一个节目)
final String signature = 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)
);
// System.out.println(signature);
// 比较并保存
if (lastSavedSignature == null || !signature.equals(lastSavedSignature)) {
lastSavedSignature = signature; // 先更新,避免并发重复
try {
SaveProgramData(
this.inputChannelId,
this.inputChannelName,
this.key.ip,
this.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) {
// 保存失败不影响 SSE
System.err.println("[SaveProgramData][ERROR] " + ex.getMessage());
}
}
// 一个输入通道只有一个节目——只处理第一个
break;
}
return GSON.toJson(programs);
}
// 生成签名(只关注结构/类型/PID变化,速率不参与)
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); }
// === stream_type + 描述符 → 类型/编码 ===
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;
}
// === DVB 字符串解码 ===
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 "";
}
}
// === 安全的 section 重组器 ===
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 continuityCounter) {
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; // total including header
}
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 void safeSendSSE(Subscriber s, String json) {
try { s.sendData(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("\"","'");
}
// ====== 实际入库(你可以在此调用存储过程) ======
// 这里默认只打印,避免直接写库;你可以改为JDBC调用:
// CallableStatement c = conn.prepareCall("{call dbo.sp_UpsertProgram_ByChannel(?,?,?,?,?,?,?,?,?,?,?,?,?,?)}");
// ... set 参数 ...
// c.execute();
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 inputStream = LoadProgramServlet.class.getClassLoader().getResourceAsStream("jdbc.properties");
if (inputStream != null) {
props.load(inputStream);
String DB_URL = props.getProperty("jdbc.url");
String DB_USER = props.getProperty("jdbc.username");
String DB_PASSWORD = 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_PASSWORD);
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); // 当 out_mod 为 256 时
cs.setInt(5, progNo); // 当 out_mod 为 256 时
cs.setInt(6, pmtPid); // 当 out_mod 为 256 时
cs.setInt(7, videoPid); // 当 out_mod 为 256 时
cs.setString(8, videoType); // 当 out_mod 为 256 时
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); // @ReturnValue
cs.execute();
// 获取输出参数
int returnValue = cs.getInt(13); // 读取 @ReturnValue
if (returnValue == 1) {
// 插入成功
// System.out.println("保存"+name+"成功");
} else {
}
} catch (SQLException e) {
} finally {
// 资源释放
try {
if (cs != null) cs.close();
} catch (Exception ignore) {
}
try {
if (c != null) c.close();
} catch (Exception ignore) {
}
}
} else {
}
} catch (Exception e) {
throw new ServletException("Failed to load database properties", e);
}
}
}
function getRateMbps(p) {
if (typeof p?.rate === 'number' && isFinite(p.rate)) return p.rate;
if (typeof p?.rate === 'string') {
const n = parseFloat(p.rate);
return isFinite(n) ? n : 0;
}
return 0;
}
function formatRate(mbps) {
if (!isFinite(mbps) || mbps <= 0) return '0';
if (mbps >= 1) return (mbps).toFixed(2) + ' M';
if (mbps >= 0.001) return (mbps * 1000).toFixed(2) + ' K';
return Math.round(mbps * 1e6) + ' bps';
}
// 给每个 group 挂一个 es 实例 & 正在解析标记
function startParseForGroup(group) {
// 已经自动启动过并且连接还活着,直接返回,避免重复创建
if (group._autostarted && group.es && group.es.readyState === 1) {
return;
}
group._autostarted = true;
// 已有连接先关(防止旧连接残留)
if (group.es) {
try { group.es.close(); } catch (_) {}
group.es = null;
}
group.parsing = true;
// ⚠️ 确认你的后端路径是否真叫 loadPramgermServlet,别拼错了
const url = `loadPramgermServlet?ip=${encodeURIComponent(group.ip)}&port=${encodeURIComponent(group.port)}&id=${encodeURIComponent(group.id)}&name=${encodeURIComponent(group.name)}`;
const es = new EventSource(url);
group.es = es;
es.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
if (!Array.isArray(data)) return;
group.programs = data.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 ?? '-'
}));
group.parsing = false;
renderTree();
} catch (e) {
console.error('解析 SSE 数据失败:', e, event.data);
}
};
es.onerror = (err) => {
console.error('SSE 连接出错', err);
group.parsing = false;
try { es.close(); } catch (_) {}
group.es = null;
// 失败后允许再次自动启动
group._autostarted = false;
};
}
// (可选)为了节省资源,开始解析前关闭其它通道的 SSE
function stopOtherParsers(exceptId) {
groups.forEach(g => {
if (String(g.id) !== String(exceptId) && g.es) {
try { g.es.close(); } catch (_) {}
g.es = null;
g.parsing = false;
}
});
}
// ✅ 按钮事件:只启动选中通道的解析,且只更新该通道
document.getElementById('btnParse').addEventListener('click', () => {
const selectedGroup = groups.find(g => g.selected);
if (!selectedGroup) {
alert('请先选择一个输入通道');
return;
}
// // 可选:关掉其它通道的 SSE,避免多路同时开
// stopOtherParsers(selectedGroup.id);
// 启动这个通道的解析
startParseForGroup(selectedGroup);
});
如果我有好几百个通道 一个通道一个节目100多个通道 我需要每个都展示速率 怎么才能坐到 而且不崩