| | |
| | | import cn.org.hentai.jtt1078.util.ByteUtils; |
| | | import cn.org.hentai.jtt1078.util.Packet; |
| | | |
| | | /** |
| | | * Created by matrixy on 2019/4/9. |
| | | */ |
| | | public class Jtt1078Decoder |
| | | { |
| | | //4096 |
| | | ByteHolder buffer = new ByteHolder(4096); |
| | | import java.time.Instant; |
| | | import java.time.ZoneOffset; |
| | | import java.time.ZonedDateTime; |
| | | import java.time.format.DateTimeFormatter; |
| | | import java.util.Arrays; |
| | | |
| | | public void write(byte[] block) |
| | | { |
| | | /** |
| | | * JTT1078 Protocol Decoder |
| | | */ |
| | | |
| | | public class Jtt1078Decoder { |
| | | private ByteHolder buffer = new ByteHolder(4096); |
| | | private int simLen = 0; |
| | | private String sim = ""; |
| | | private byte channel = 0; |
| | | private static final byte[] HEAD1078 = {0x30, 0x31, 0x63, 0x64}; |
| | | |
| | | public void write(byte[] block) { |
| | | buffer.write(block); |
| | | } |
| | | |
| | | public void write(byte[] block, int startIndex, int length) |
| | | { |
| | | public void write(byte[] block, int startIndex, int length) { |
| | | byte[] buff = new byte[length]; |
| | | System.arraycopy(block, startIndex, buff, 0, length); |
| | | write(buff); |
| | | } |
| | | |
| | | public Packet decode() |
| | | { |
| | | if (this.buffer.size() < 30) return null; |
| | | public Packet decode() { |
| | | if (buffer.size() < 4) return null; |
| | | |
| | | if ((buffer.getInt(0) & 0x7fffffff) != 0x30316364) |
| | | { |
| | | String header = ByteUtils.toString(buffer.array(30)); |
| | | throw new RuntimeException("invalid protocol header: " + header); |
| | | // Check header |
| | | if (!Arrays.equals(buffer.array(4), HEAD1078)) { |
| | | // log.warn("Invalid protocol header, expected:30316364, actual:{}", |
| | | // ByteUtils.toHexString(buffer.array(4))); |
| | | buffer.clear(); |
| | | return null; |
| | | } |
| | | |
| | | int lengthOffset = 28; |
| | | int dataType = (this.buffer.get(15) >> 4) & 0x0f; |
| | | // 透传数据类型:0100,没有后面的时间以及Last I Frame Interval和Last Frame Interval字段 |
| | | if (dataType == 0x04) lengthOffset = 28 - 8 - 2 - 2; |
| | | else if (dataType == 0x03) lengthOffset = 28 - 4; |
| | | int bodyLength = this.buffer.getShort(lengthOffset); |
| | | |
| | | int packetLength = bodyLength + lengthOffset + 2; |
| | | |
| | | if (this.buffer.size() < packetLength) return null; |
| | | byte[] block = new byte[packetLength]; |
| | | this.buffer.sliceInto(block, packetLength); |
| | | return Packet.create(block); |
| | | // Try to determine protocol version if not set |
| | | if (simLen == 0) { |
| | | // Try version 16 (2013 protocol) |
| | | Packet packet = decodePacket(16); |
| | | if (packet != null) { |
| | | Analyze analyze = parse16(packet.getBytes()); |
| | | sim = analyze.getSim(); |
| | | channel = analyze.getCh(); |
| | | simLen = 6; |
| | | System.out.printf("%s-%d device started output%n", sim, channel); |
| | | return packet; |
| | | } |
| | | |
| | | // Try version 19 (2019 protocol) |
| | | packet = decodePacket(19); |
| | | if (packet != null) { |
| | | Analyze analyze = parse19(packet.getBytes()); |
| | | sim = analyze.getSim(); |
| | | channel = analyze.getCh(); |
| | | simLen = 10; |
| | | System.out.printf("%s-%d device started output%n", sim, channel); |
| | | return packet; |
| | | } |
| | | |
| | | return null; |
| | | } else { |
| | | return decodePacket(simLen == 6 ? 16 : 19); |
| | | } |
| | | } |
| | | |
| | | private Packet decodePacket(int version) { |
| | | int dataTypeIndex = version == 16 ? 15 : 19; |
| | | if (buffer.size() < dataTypeIndex + 1) return null; |
| | | |
| | | byte dataType = (byte) ((buffer.get(dataTypeIndex) >> 4) & 0x0F); |
| | | int dataLenIndex; |
| | | |
| | | switch (dataType) { |
| | | case 3: |
| | | dataLenIndex = dataTypeIndex + 9; |
| | | break; |
| | | case 4: |
| | | dataLenIndex = dataTypeIndex + 1; |
| | | break; |
| | | default: |
| | | dataLenIndex = dataTypeIndex + 13; |
| | | } |
| | | //log.info("dataType:"+dataType); |
| | | if (buffer.size() < dataLenIndex + 2) return null; |
| | | int dataLen = buffer.getShort(dataLenIndex) & 0xFFFF; |
| | | int packetLength = dataLenIndex + 2 + dataLen; |
| | | |
| | | if (buffer.size() < packetLength) return null; |
| | | |
| | | // Check next packet header |
| | | if (buffer.size() >= packetLength + 4) { |
| | | byte[] nextHeader = new byte[4]; |
| | | for (int i = 0; i < 4; i++) { |
| | | nextHeader[i] = buffer.get(packetLength + i); |
| | | } |
| | | if (!Arrays.equals(nextHeader, HEAD1078)) { |
| | | // log.warn("Invalid data type: {}", dataType); |
| | | buffer.clear(); |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | byte[] packetData = new byte[packetLength]; |
| | | buffer.sliceInto(packetData, packetLength); |
| | | // printPacketInfo(packetData, version); |
| | | return Packet.create(packetData); |
| | | } |
| | | |
| | | private Analyze parse16(byte[] data) { |
| | | Analyze analyze = new Analyze(); |
| | | analyze.setPt((byte) (data[5] & 0x7F)); |
| | | analyze.setSn((short) (((data[6] & 0xFF) << 8) | (data[7] & 0xFF))); |
| | | |
| | | StringBuilder simBuilder = new StringBuilder(); |
| | | for (int i = 0; i < 6; i++) { |
| | | simBuilder.append(nextBcd(data, 8 + i)); |
| | | } |
| | | analyze.setSim(simBuilder.toString()); |
| | | |
| | | analyze.setCh(data[14]); |
| | | analyze.setDt((byte) ((data[15] >> 4) & 0x0F)); |
| | | analyze.setFi((byte) (data[15] & 0x0F)); |
| | | |
| | | int dataLenIndex; |
| | | switch (analyze.getDt()) { |
| | | case 3: |
| | | analyze.setTimestamp(setTimestamp(data, 16)); |
| | | dataLenIndex = 24; |
| | | break; |
| | | case 4: |
| | | dataLenIndex = 16; |
| | | break; |
| | | default: |
| | | analyze.setTimestamp(setTimestamp(data, 16)); |
| | | analyze.setLastIInterval((short) (((data[24] & 0xFF) << 8) | (data[25] & 0xFF))); |
| | | analyze.setLastInterval((short) (((data[26] & 0xFF) << 8) | (data[27] & 0xFF))); |
| | | dataLenIndex = 28; |
| | | } |
| | | |
| | | analyze.setDataLen((short) (((data[dataLenIndex] & 0xFF) << 8) | (data[dataLenIndex + 1] & 0xFF))); |
| | | byte[] payload = new byte[analyze.getDataLen()]; |
| | | System.arraycopy(data, dataLenIndex + 2, payload, 0, payload.length); |
| | | analyze.setData(payload); |
| | | |
| | | return analyze; |
| | | } |
| | | |
| | | private Analyze parse19(byte[] data) { |
| | | Analyze analyze = new Analyze(); |
| | | analyze.setPt((byte) (data[5] & 0x7F)); |
| | | analyze.setSn((short) (((data[6] & 0xFF) << 8) | (data[7] & 0xFF))); |
| | | |
| | | StringBuilder simBuilder = new StringBuilder(); |
| | | for (int i = 0; i < 10; i++) { |
| | | simBuilder.append(nextBcd(data, 8 + i)); |
| | | } |
| | | analyze.setSim(simBuilder.toString()); |
| | | |
| | | analyze.setCh(data[18]); |
| | | analyze.setDt((byte) ((data[19] >> 4) & 0x0F)); |
| | | analyze.setFi((byte) (data[19] & 0x0F)); |
| | | |
| | | int dataLenIndex; |
| | | switch (analyze.getDt()) { |
| | | case 3: |
| | | analyze.setTimestamp(setTimestamp(data, 20)); |
| | | dataLenIndex = 28; |
| | | break; |
| | | case 4: |
| | | dataLenIndex = 20; |
| | | break; |
| | | default: |
| | | analyze.setTimestamp(setTimestamp(data, 20)); |
| | | analyze.setLastIInterval((short) (((data[28] & 0xFF) << 8) | (data[29] & 0xFF))); |
| | | analyze.setLastInterval((short) (((data[30] & 0xFF) << 8) | (data[31] & 0xFF))); |
| | | dataLenIndex = 32; |
| | | } |
| | | |
| | | analyze.setDataLen((short) (((data[dataLenIndex] & 0xFF) << 8) | (data[dataLenIndex + 1] & 0xFF))); |
| | | byte[] payload = new byte[analyze.getDataLen()]; |
| | | System.arraycopy(data, dataLenIndex + 2, payload, 0, payload.length); |
| | | analyze.setData(payload); |
| | | |
| | | return analyze; |
| | | } |
| | | |
| | | private long setTimestamp(byte[] data, int offset) { |
| | | long timestamp = 0; |
| | | for (int i = 0; i < 8; i++) { |
| | | timestamp = (timestamp << 8) | (data[offset + i] & 0xFF); |
| | | } |
| | | return timestamp; |
| | | } |
| | | |
| | | private String nextBcd(byte[] data, int offset) { |
| | | byte val = data[offset]; |
| | | int ch1 = (val >> 4) & 0x0F; |
| | | int ch2 = val & 0x0F; |
| | | return String.format("%d%d", ch1, ch2); |
| | | } |
| | | |
| | | private void printPacketInfo(byte[] packet, int version) { |
| | | System.out.println("\n=== JTT1078 " + (version == 16 ? "2013" : "2019") + " Protocol Packet ==="); |
| | | System.out.println("Header: " + ByteUtils.toHexString(packet, 0, 4)); |
| | | |
| | | Analyze analyze = version == 16 ? parse16(packet) : parse19(packet); |
| | | System.out.println("Payload Type: " + analyze.getPt()); |
| | | System.out.println("Sequence Number: " + analyze.getSn()); |
| | | System.out.println("SIM: " + analyze.getSim()); |
| | | System.out.println("Channel: " + analyze.getCh()); |
| | | System.out.println("Data Type: " + getDataTypeDescription(analyze.getDt())); |
| | | System.out.println("Frame Info: " + getFrameInfoDescription(analyze.getFi())); |
| | | |
| | | if (analyze.getTimestamp() != 0) { |
| | | Instant instant = Instant.ofEpochMilli(analyze.getTimestamp()); |
| | | ZonedDateTime beijingTime = instant.atZone(ZoneOffset.ofHours(8)); |
| | | DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); |
| | | System.out.println("Timestamp: " + formatter.format(beijingTime)); |
| | | } |
| | | |
| | | if (analyze.getLastIInterval() != 0) { |
| | | System.out.println("Last I Frame Interval: " + analyze.getLastIInterval()); |
| | | } |
| | | if (analyze.getLastInterval() != 0) { |
| | | System.out.println("Last Frame Interval: " + analyze.getLastInterval()); |
| | | } |
| | | |
| | | System.out.println("Data Length: " + analyze.getDataLen()); |
| | | System.out.println("=================================\n"); |
| | | } |
| | | |
| | | private String getDataTypeDescription(int dataType) { |
| | | switch (dataType) { |
| | | case 0: return "Video I Frame"; |
| | | case 1: return "Video P Frame"; |
| | | case 2: return "Video B Frame"; |
| | | case 3: return "Audio Frame"; |
| | | case 4: return "Transparent Data"; |
| | | default: return "Unknown (" + dataType + ")"; |
| | | } |
| | | } |
| | | |
| | | private String getFrameInfoDescription(int fi) { |
| | | switch (fi) { |
| | | case 0: return "Atomic Packet"; |
| | | case 1: return "First Packet"; |
| | | case 2: return "Last Packet"; |
| | | case 3: return "Middle Packet"; |
| | | default: return "Unknown (" + fi + ")"; |
| | | } |
| | | } |
| | | } |
| | | |
| | | class Analyze { |
| | | private byte pt; |
| | | private short sn; |
| | | private String sim; |
| | | private byte ch; |
| | | private byte dt; |
| | | private byte fi; |
| | | private long timestamp; |
| | | private short lastIInterval; |
| | | private short lastInterval; |
| | | private short dataLen; |
| | | private byte[] data; |
| | | |
| | | // Getters and setters |
| | | public byte getPt() { return pt; } |
| | | public void setPt(byte pt) { this.pt = pt; } |
| | | |
| | | public short getSn() { return sn; } |
| | | public void setSn(short sn) { this.sn = sn; } |
| | | |
| | | public String getSim() { return sim; } |
| | | public void setSim(String sim) { this.sim = sim; } |
| | | |
| | | public byte getCh() { return ch; } |
| | | public void setCh(byte ch) { this.ch = ch; } |
| | | |
| | | public byte getDt() { return dt; } |
| | | public void setDt(byte dt) { this.dt = dt; } |
| | | |
| | | public byte getFi() { return fi; } |
| | | public void setFi(byte fi) { this.fi = fi; } |
| | | |
| | | public long getTimestamp() { return timestamp; } |
| | | public void setTimestamp(long timestamp) { this.timestamp = timestamp; } |
| | | |
| | | public short getLastIInterval() { return lastIInterval; } |
| | | public void setLastIInterval(short lastIInterval) { this.lastIInterval = lastIInterval; } |
| | | |
| | | public short getLastInterval() { return lastInterval; } |
| | | public void setLastInterval(short lastInterval) { this.lastInterval = lastInterval; } |
| | | |
| | | public short getDataLen() { return dataLen; } |
| | | public void setDataLen(short dataLen) { this.dataLen = dataLen; } |
| | | |
| | | public byte[] getData() { return data; } |
| | | public void setData(byte[] data) { this.data = data; } |
| | | } |