JavaScript Object Properties —— Enumeration, Types of Properties

本文探讨了JavaScript中对象属性的枚举方式及其特性,并介绍了数据属性与访问器属性的区别及应用场景。

原创转载请注明出处:http://agilestyle.iteye.com/blog/2341899 

 

Enumeration

All object properties are enumerable by default, which means that they will appear in a for-in loop or be retrieved by Object.keys().

var listProperties1 = function (object) {
    var property;

    for (property in object) {
        console.log("Name: " + property);
        console.log("Value: " + object[property]);
    }
}

var listProperties2 = function (object) {
    var properties = Object.keys(object);

    var i, len;
    for (i = 0, len = properties.length; i < len; i++) {
        console.log("Name: " + properties[i]);
        console.log("Value: " + object[properties[i]]);
    }
}

var person1 = {
    name: "Nicholas"
};

listProperties1(person1);
listProperties2(person1);

console.log("name" in person1);                         // true
console.log(person1.propertyIsEnumerable("name"));      // true
var properties = Object.keys(person1);
console.log("length" in properties);                    // true
console.log(properties.propertyIsEnumerable("length")); // false

Note:

Keep in mind that not all properties are enumerable. In fact, most of the native methods on objects have their [[Enumerable]] attribute set to false. You can check whether a property is enumerable by using the propertyIsEnumerable() method, which is present on every object. 

 

Types of Properties

There are two types of properties: data properties and accessor properties.

Data properties are placeholders for values, and you can read from and write to them. When a data property holds a function value, the property is considered a method of the object.

Unlike data properties, accessor properties don’t store values on their own; they use a combination of getters and setters to perform specific actions. You can create both data properties and accessor properties directly using object literal notation.

var person1 = {
    _name: "Kobe",

    get name() {
        console.log("Reading name");
        return this._name;
    },
    set name(value) {
        console.log("Setting name to %s", value);
        this._name = value;
    }
};

console.log(person1.name);  // "Reading name" then outputs "Kobe"
person1.name = "Bryant";
console.log(person1.name);  // "Setting name to Bryant" then outputs "Bryant"

This example defines an accessor property called name. There is a data property called _name that contains the actual value for the property. (The leading underscore is a common convention to indicate that the property is considered to be private, though in reality it is still public). The syntax used to define the getter and setter for name looks a lot like a function but without the function keyword. The special keywords get and set are used before the accessor property name, followed by parentheses and a function body. Getters are expected to return a value, while setters receive the value being assigned to the property as an argument.

Even though this example uses _name to store the property data, you could just as easily store the data in a variable or even in another object. This example simply adds logging to the behavior of the property; there's usually no reason to use accessor properties if you are only storing the data in another property - just use the property itself. Accessor properties are most useful when you want the assignment of a value to trigger some sort of behavior, or when reading a value requires the calculation of the desired return value. 

 

Reference

Leanpub.Principles.of.Object-Oriented.Programming.in.JavaScript.Jun.2014

 

 

 

 

/* ================== 单 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
最新发布
11-22
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值