浏览代码

调整2019版本视频

kwl 2 周之前
父节点
当前提交
cc8e326ffe

+ 20 - 2
src/main/java/com/jttserver/Server.java

@@ -8,6 +8,8 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.jttserver.config.ConfigManager;
+import com.jttserver.protocol.JttConstants;
+import com.jttserver.relay.StreamRelay;
 import com.jttserver.relay.StreamRelayType;
 import com.jttserver.service.ManagerWebServer;
 import com.jttserver.service.publisher.PublishServer;
@@ -45,6 +47,14 @@ public class Server {
         int realtimePort = Integer.parseInt(com.jttserver.config.ConfigManager.get("server.realtime.port", "18080"));
         int playbackPort = Integer.parseInt(com.jttserver.config.ConfigManager.get("server.playback.port", "18081"));
 
+        int realtimePort2019 = Integer.parseInt(com.jttserver.config.ConfigManager.get("server.realtime2019.port", "18180"));
+        int playbackPort2019 = Integer.parseInt(com.jttserver.config.ConfigManager.get("server.playback2019.port", "18181"));
+
+        String prefixRetime = com.jttserver.config.ConfigManager.get("server.realtime.prefix", "/realtime/");
+        String prefixPlayback = com.jttserver.config.ConfigManager.get("server.playback.prefix", "/playback/");
+        String prefixRetime2019 = com.jttserver.config.ConfigManager.get("server.realtime2019.prefix", "/realtime2019/");
+        String prefixPlayback2019 = com.jttserver.config.ConfigManager.get("server.playback2019.prefix", "/playback2019/");
+
 
         PublishServer wsServer = new WebsockServer(wsPort);
 
@@ -52,13 +62,19 @@ public class Server {
             wsServer.start();
             ManagerWebServer webServer = new ManagerWebServer();
 
-            RecvSever readTimeServer = new JttVideoRecvServer(wsServer, realtimePort, "/realtime/", StreamRelayType.FLV_REALTIME);
-            RecvSever playbackServer = new JttVideoRecvServer(wsServer, playbackPort, "/playback/", StreamRelayType.FLV_PLAYBACK);
+            RecvSever readTimeServer = new JttVideoRecvServer(wsServer, realtimePort, prefixRetime, StreamRelayType.FLV_REALTIME);
+            RecvSever playbackServer = new JttVideoRecvServer(wsServer, playbackPort, prefixPlayback, StreamRelayType.FLV_PLAYBACK);
+
+            RecvSever readTimeServer2019 = new JttVideoRecvServer(wsServer, realtimePort2019, prefixRetime2019, StreamRelayType.FLV_REALTIME, JttConstants.TYPE_JTT808_2019);
+            RecvSever playbackServer2019 = new JttVideoRecvServer(wsServer, playbackPort2019, prefixPlayback2019, StreamRelayType.FLV_PLAYBACK, JttConstants.TYPE_JTT808_2019);
+
 
             // 当程序正常退出时自动调用stop()方法来关闭服务器。
             Runtime.getRuntime().addShutdownHook(new Thread(() -> {
                 readTimeServer.stop();
                 playbackServer.stop();
+                readTimeServer2019.stop();
+                playbackServer2019.stop();
                 webServer.stop();
                 wsServer.stop();
             }));
@@ -66,6 +82,8 @@ public class Server {
             webServer.start();
             readTimeServer.start();
             playbackServer.start();
+            readTimeServer2019.start();
+            playbackServer2019.start();
 
             wsServer.waitForShutdown();
             

+ 25 - 8
src/main/java/com/jttserver/codec/Jtt1078MessageDecoder.java

@@ -6,6 +6,8 @@ import io.netty.handler.codec.ByteToMessageDecoder;
 
 import java.util.List;
 
+import com.jttserver.protocol.JttConstants;
+
 /**
  * JTT1078协议消息解码器
  * 处理TCP粘包和拆包问题
@@ -18,11 +20,26 @@ public class Jtt1078MessageDecoder extends ByteToMessageDecoder {
     private static final int MIN_DATA_LENGTH = 19; //最小为透传数据18+1个字节
     private static final int MIN_HEADER_LENGTH = 18;
     private static final int MAX_HEADER_LENGTH = 30;
+    private final int jtt808Type;
+    private int jttOffset = 0;  // JTT808协议偏移量,2013版为0,2019版为4 设备sim的BCD字段多占4字节
+
+
+    // 默认构造函数,使用JTT808 2013版
+    public Jtt1078MessageDecoder() {
+        this.jtt808Type = JttConstants.TYPE_JTT808_2013;
+    }
+
+    public Jtt1078MessageDecoder(int jtt808Type) {
+        this.jtt808Type = jtt808Type;
+        if (this.jtt808Type == JttConstants.TYPE_JTT808_2019) {
+            jttOffset = 4;
+        }
+    }
 
     @Override
     protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
         // 循环处理,直到没有完整数据包可读(至少需要最小头部长度+NALU数据长度)
-        while (in.isReadable(MIN_DATA_LENGTH)) {
+        while (in.isReadable(MIN_DATA_LENGTH + jttOffset)) {
 
             // 查找数据包头部
             int headerIndex = findHeader(in);
@@ -38,7 +55,7 @@ public class Jtt1078MessageDecoder extends ByteToMessageDecoder {
             }
 
             // 确保有足够的数据进行头部解析
-            if (in.readableBytes() < MIN_HEADER_LENGTH) {
+            if (in.readableBytes() < MIN_HEADER_LENGTH + jttOffset) {
                 return;
             }
 
@@ -49,13 +66,13 @@ public class Jtt1078MessageDecoder extends ByteToMessageDecoder {
             }
 
             // 确保有足够的数据读取到第15字节(已经读取了4字节header)
-            if (in.readableBytes() < 12) {
+            if (in.readableBytes() < 12 + jttOffset) {
                 in.resetReaderIndex();
                 return;
             }
 
             // 跳过前11字节到达第15字节 (已读取4字节header,还需跳过11字节到达第15字节)
-            in.skipBytes(11);
+            in.skipBytes(11 + jttOffset);
 
             // 读取第15字节并获取前4位
             byte fifteenthByte = in.readByte();
@@ -67,13 +84,13 @@ public class Jtt1078MessageDecoder extends ByteToMessageDecoder {
                 case 0x0: // 0000
                 case 0x1: // 0001
                 case 0x2: // 0010
-                    headerLength = MAX_HEADER_LENGTH;
+                    headerLength = MAX_HEADER_LENGTH + jttOffset;
                     break;
                 case 0x3: // 0011
-                    headerLength = 26;
+                    headerLength = 26 + jttOffset;
                     break;
                 case 0x4: // 0100
-                    headerLength = 18;
+                    headerLength = 18 + jttOffset;
                     break;
                 default:
                     // 未知类型, 报错
@@ -81,7 +98,7 @@ public class Jtt1078MessageDecoder extends ByteToMessageDecoder {
             }
 
             // 回到包头开始位置
-            in.readerIndex(in.readerIndex() - 16); // 回退16字节
+            in.readerIndex(in.readerIndex() - 16 - jttOffset); // 回退16字节
 
             // 确保有足够的数据读取整个包头
             if (in.readableBytes() < headerLength) {

+ 18 - 5
src/main/java/com/jttserver/protocol/Jtt1078PacketParser.java

@@ -205,14 +205,26 @@ public class Jtt1078PacketParser {
         return dataType == 0x00 || dataType == 0x01 || dataType == 0x02;
     }
 
+
     /**
-     * 解析JTT1078协议数据包
+     * 解析JTT1078协议数据包 默认2013版JTT808偏移
      * 
      * @param buf 包含完整JTT1078数据包的ByteBuf
      * @return 解析后的数据包对象
      */
     public static Jtt1078Packet parse(ByteBuf buf) {
+        return parse(buf, JttConstants.TYPE_JTT808_2013);
+    }
+
+    /**
+     * 解析JTT1078协议数据包
+     * 
+     * @param buf 包含完整JTT1078数据包的ByteBuf
+     * @return 解析后的数据包对象
+     */
+    public static Jtt1078Packet parse(ByteBuf buf, int jtt808Type) {
         Jtt1078Packet packet = new Jtt1078Packet();
+        int jttOffset = (jtt808Type == JttConstants.TYPE_JTT808_2019) ? 4 : 0;
 
         // 保存readerIndex以便恢复
         int startIndex = buf.readerIndex();
@@ -232,26 +244,27 @@ public class Jtt1078PacketParser {
             buf.readerIndex(startIndex + 6); // 起始字节:6
             packet.packetSequenceNumber = buf.readUnsignedShort();
 
-            // 解析第8-13字节:SIM卡号(BCD[6]格式)
+            // 解析第8-13字节:SIM卡号(BCD[6]格式), 注意JTT808 2019版多占4字节
             buf.readerIndex(startIndex + 8); // 起始字节:8
             packet.simCardNumber = new byte[6];
+            buf.skipBytes(jttOffset);
             buf.readBytes(packet.simCardNumber);
 
             // 立即计算并缓存SIM卡号字符串,避免后续重复计算
             packet.simCardNumberStr = SimCardUtils.toStandardString(packet.simCardNumber);
 
             // 解析:逻辑通道号
-            buf.readerIndex(startIndex + 14); // 起始字节:14
+            buf.readerIndex(startIndex + 14 + jttOffset); // 起始字节:14
             packet.logicChannelNumber = buf.readByte();
 
             // 解析:数据类型(高4位)和分包处理标记(低4位)
-            buf.readerIndex(startIndex + 15); // 起始字节:15
+            buf.readerIndex(startIndex + 15 + jttOffset); // 起始字节:15
             byte fifteenthByte = buf.readByte();
             packet.dataType = (byte) ((fifteenthByte >> 4) & 0x0F);
             packet.subpackageFlag = (byte) (fifteenthByte & 0x0F);
 
             // 最后偏移位置,因为类型不同,有的字段可能没有
-            short lastOffset = 16;
+            short lastOffset = (short) (16 + jttOffset);
 
             // 解析第16-23字节:时间戳(当数据类型为透传数据0100时不存在)
             if (packet.dataType != 0x04) { // 0100(二进制) = 4(十进制)

+ 4 - 0
src/main/java/com/jttserver/protocol/JttConstants.java

@@ -2,6 +2,10 @@ package com.jttserver.protocol;
 
 public class JttConstants {
 
+    /****** 对应808协议类型 *******/
+    public static final int TYPE_JTT808_2013= 0;
+    public static final int TYPE_JTT808_2019= 1;
+
     /****** AAC数据类型 *******/
     public static final byte AAC_SEQUENCE_HEADER = 0; // 序列头
     public static final byte AAC_SEQUENCE_DATA = 1; // 序列数据

+ 2 - 0
src/main/java/com/jttserver/relay/FlvRealtimeStreamRelay.java

@@ -87,6 +87,8 @@ public class FlvRealtimeStreamRelay extends StreamRelay {
 
             if (initVideoSegment != null && initVideoSegment.length > 0) {
                 ch.writeAndFlush(new BinaryWebSocketFrame(Unpooled.wrappedBuffer(initVideoSegment)));
+            }else {
+                logger.warn("无法获取初始化视频段,可能视频设备尚未连接: channelId={}", channelId);
             }
 
             if (initAudioSegment != null && initAudioSegment.length > 0) {

+ 15 - 5
src/main/java/com/jttserver/service/receiver/JttVideoRecvServer.java

@@ -15,6 +15,7 @@ import com.jttserver.device.DeviceManager;
 import com.jttserver.protocol.Jtt1078NaluPacket;
 import com.jttserver.protocol.Jtt1078PacketParams;
 import com.jttserver.protocol.Jtt1078PacketParser;
+import com.jttserver.protocol.JttConstants;
 import com.jttserver.relay.StreamRelay;
 import com.jttserver.relay.StreamRelayType;
 import com.jttserver.relay.workerthreads.BroadcastWorker;
@@ -54,8 +55,14 @@ public class JttVideoRecvServer extends RecvSever {
     // 存储每个连接的Channel
     private static final Map<String, Channel> channelIdToCtxMap = new ConcurrentHashMap<>();
 
+    // 默认2013版JTT808协议
     public JttVideoRecvServer(PublishServer publishServer, int port, String prefix, StreamRelayType relayType) {
-        super(publishServer, port, prefix, relayType);
+        this(publishServer, port, prefix, relayType, JttConstants.TYPE_JTT808_2013);
+    }
+
+    // 手动指定JTT808协议
+    public JttVideoRecvServer(PublishServer publishServer, int port, String prefix, StreamRelayType relayType, int jtt808Type) {
+        super(publishServer, port, prefix, relayType, jtt808Type);
     }
 
     /**
@@ -75,11 +82,11 @@ public class JttVideoRecvServer extends RecvSever {
                 .childHandler(new ChannelInitializer<SocketChannel>() {
                     @Override
                     protected void initChannel(SocketChannel ch) throws Exception {
-                        ch.pipeline().addLast(new Jtt1078MessageDecoder()); // 添加JTT1078解码器处理粘包拆包
+                        ch.pipeline().addLast(new Jtt1078MessageDecoder(jtt808Type)); // 添加JTT1078解码器处理粘包拆包
 
                         // 添加处理视频流数据的处理器
                         ch.pipeline().addLast(
-                                new VideoStreamHandler(deviceManagementEnabled, streamRelay, publishServer, prefix));
+                                new VideoStreamHandler(deviceManagementEnabled, streamRelay, publishServer, prefix, jtt808Type));
                     }
                 })
                 .option(ChannelOption.SO_BACKLOG, 128)
@@ -161,6 +168,8 @@ public class JttVideoRecvServer extends RecvSever {
 
         private final String prefix;
 
+        private final int jtt808Type;
+
         // 定时器配置
         private static final int DEFAULT_CHECK_INTERVAL = 5; // 默认检查间隔5秒
         private static final int DEFAULT_TIMEOUT = 30; // 默认超时时间30秒
@@ -173,11 +182,12 @@ public class JttVideoRecvServer extends RecvSever {
         private final Map<String, Long> channelStartTimes = new ConcurrentHashMap<>();
 
         public VideoStreamHandler(boolean deviceManagementEnabled, StreamRelay streamRelay,
-                PublishServer publishServer, String prefix) {
+                PublishServer publishServer, String prefix, int jtt808Type) {
             this.deviceManagementEnabled = deviceManagementEnabled;
             this.streamRelay = streamRelay;
             this.publishServer = publishServer;
             this.prefix = prefix;
+            this.jtt808Type = jtt808Type;
         }
 
         @Override
@@ -237,7 +247,7 @@ public class JttVideoRecvServer extends RecvSever {
             }
             // 解析JTT1078协议数据包
             try {
-                Jtt1078PacketParser.Jtt1078Packet packet = Jtt1078PacketParser.parse(buf);
+                Jtt1078PacketParser.Jtt1078Packet packet = Jtt1078PacketParser.parse(buf, jtt808Type);
                 // System.out.println("解析到JTT1078数据包: " + packet.toString());
 
                 // 更新设备信息(受功能开关控制)

+ 5 - 1
src/main/java/com/jttserver/service/receiver/RecvSever.java

@@ -15,6 +15,9 @@ public abstract class RecvSever {
     // 流转发器
     protected StreamRelay streamRelay;
 
+    // 808协议类型
+    protected int jtt808Type = -1;
+
     // 接收服务器监听端口,-1表示未配置
     protected int port = -1;
 
@@ -31,11 +34,12 @@ public abstract class RecvSever {
     /**
      * 构造函数
      */
-    public RecvSever(PublishServer publishServer, int port, String prefix, StreamRelayType relayType) {
+    public RecvSever(PublishServer publishServer, int port, String prefix, StreamRelayType relayType, int jtt808Type) {
         this.publishServer = publishServer;
         this.port = port;
         this.prefix = prefix;
         this.streamRelay = relayType.create(publishServer, this, prefix);
+        this.jtt808Type = jtt808Type;
     }
 
     /*

+ 258 - 0
src/test/java/com/jttserver/codec/Jtt1078MessageDecoder2019Test.java

@@ -0,0 +1,258 @@
+package com.jttserver.codec;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.embedded.EmbeddedChannel;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import com.jttserver.protocol.JttConstants;
+
+public class Jtt1078MessageDecoder2019Test {
+
+    @Test
+    void testSingleCompletePacket2019() {
+        // 测试2019版本单个完整的数据包
+        // 2019版本在第8字节开始的SIM卡号比2013版本多了4个字节(00 00 00 00)
+
+        // 创建嵌入式通道用于测试,使用2019版本
+        EmbeddedChannel channel = new EmbeddedChannel(new Jtt1078MessageDecoder(JttConstants.TYPE_JTT808_2019));
+
+        // 构造一个完整的JTT1078 2019版本数据包
+        // 头部: 0x30316364 (4字节)
+        // 包头(总共22字节 = 18字节 + 4字节SIM卡号扩展)
+        // 数据长度字段为2字节,位于包头的最后2个字节,设为5
+        // 数据部分: 5字节
+        byte[] packet = new byte[] {
+                0x30, 0x31, 0x63, 0x64, // 头部标识 0x30316364 (字节0-3)
+                0x01, 0x02, 0x03, 0x04, // 字节4-7
+                0x00, 0x00, 0x00, 0x00, // 2019版本新增的4字节SIM卡号扩展 (字节8-11)
+                0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, // 7字节填充 (字节12-18)
+                (byte) 0x40, // 字节19,前4位为0100,表示包头长度为22字节 (18+4)
+                0x00, 0x05, // 数据长度字段,表示5字节数据 (位于包头最后2个字节,大端序)
+                0x11, 0x22, 0x33, 0x44, 0x55 // 5字节实际数据
+        };
+
+        // 写入完整数据包
+        channel.writeInbound(Unpooled.copiedBuffer(packet));
+
+        // 读取解码后的数据
+        Object result = channel.readInbound();
+
+        // 验证解码结果
+        assertNotNull(result, "Result should not be null");
+        assertTrue(result instanceof byte[], "Result should be byte[]");
+        byte[] decodedPacket = (byte[]) result;
+        assertEquals(packet.length, decodedPacket.length);
+        assertArrayEquals(packet, decodedPacket);
+
+        // 验证没有更多数据
+        assertNull(channel.readInbound());
+        assertFalse(channel.finish()); // 通道应该没有更多数据,所以finish应该返回false
+    }
+
+    @Test
+    void testStickyPackets2019() {
+        // 测试2019版本粘包情况:两个完整数据包连接在一起
+        EmbeddedChannel channel = new EmbeddedChannel(new Jtt1078MessageDecoder(JttConstants.TYPE_JTT808_2019));
+
+        // 构造两个2019版本数据包 (包头长度22字节 = 18字节 + 4字节SIM卡号扩展)
+        byte[] packet1 = new byte[] {
+                0x30, 0x31, 0x63, 0x64, // 头部标识 (字节0-3)
+                0x01, 0x02, 0x03, 0x04, // 字节4-7
+                0x00, 0x00, 0x00, 0x00, // 2019版本新增的4字节SIM卡号扩展 (字节8-11)
+                0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, // 7字节填充 (字节12-18)
+                (byte) 0x40, // 字节19,前4位为0100,表示包头长度为22字节
+                0x00, 0x03, // 数据长度: 3字节 (位于包头最后2个字节,大端序)
+                0x11, 0x22, 0x33 // 数据 (3字节)
+        };
+
+        byte[] packet2 = new byte[] {
+                0x30, 0x31, 0x63, 0x64, // 头部标识 (字节0-3)
+                0x01, 0x02, 0x03, 0x04, // 字节4-7
+                0x00, 0x00, 0x00, 0x00, // 2019版本新增的4字节SIM卡号扩展 (字节8-11)
+                0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, // 7字节填充 (字节12-18)
+                (byte) 0x40, // 字节19,前4位为0100,表示包头长度为22字节
+                0x00, 0x02, // 数据长度: 2字节 (位于包头最后2个字节,大端序)
+                0x44, 0x55 // 数据 (2字节)
+        };
+
+        // 将两个包粘在一起
+        byte[] stickyPacket = new byte[packet1.length + packet2.length];
+        System.arraycopy(packet1, 0, stickyPacket, 0, packet1.length);
+        System.arraycopy(packet2, 0, stickyPacket, packet1.length, packet2.length);
+
+        // 写入粘包数据
+        channel.writeInbound(Unpooled.copiedBuffer(stickyPacket));
+
+        // 应该能正确解析出两个数据包
+        Object result1 = channel.readInbound();
+        Object result2 = channel.readInbound();
+
+        // 验证第一个包
+        assertNotNull(result1, "First packet should not be null");
+        assertTrue(result1 instanceof byte[], "First packet should be byte[]");
+        byte[] decodedPacket1 = (byte[]) result1;
+        assertEquals(packet1.length, decodedPacket1.length);
+        assertArrayEquals(packet1, decodedPacket1);
+
+        // 验证第二个包
+        assertNotNull(result2, "Second packet should not be null");
+        assertTrue(result2 instanceof byte[], "Second packet should be byte[]");
+        byte[] decodedPacket2 = (byte[]) result2;
+        assertEquals(packet2.length, decodedPacket2.length);
+        assertArrayEquals(packet2, decodedPacket2);
+
+        // 验证没有更多数据
+        assertNull(channel.readInbound());
+        assertFalse(channel.finish()); // 通道应该没有更多数据,所以finish应该返回false
+    }
+
+    @Test
+    void testFragmentedPacket2019() {
+        // 测试2019版本拆包情况:一个数据包分两次到达
+        EmbeddedChannel channel = new EmbeddedChannel(new Jtt1078MessageDecoder(JttConstants.TYPE_JTT808_2019));
+
+        // 构造一个完整2019版本数据包
+        byte[] fullPacket = new byte[] {
+                0x30, 0x31, 0x63, 0x64, // 头部标识 (字节0-3)
+                0x01, 0x02, 0x03, 0x04, // 字节4-7
+                0x00, 0x00, 0x00, 0x00, // 2019版本新增的4字节SIM卡号扩展 (字节8-11)
+                0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, // 7字节填充 (字节12-18)
+                (byte) 0x40, // 字节19,前4位为0100,表示包头长度为22字节
+                0x00, 0x04, // 数据长度: 4字节 (位于包头最后2个字节,大端序)
+                0x11, 0x22, 0x33, 0x44 // 数据 (4字节)
+        };
+
+        // 第一次只发送前12字节
+        ByteBuf firstPart = Unpooled.copiedBuffer(fullPacket, 0, 12);
+        channel.writeInbound(firstPart); // 不应该产生输出
+
+        // 验证还没有解码出数据
+        assertNull(channel.readInbound(), "Should not have decoded data after first part");
+
+        // 第二次发送剩余部分
+        ByteBuf secondPart = Unpooled.copiedBuffer(fullPacket, 12, fullPacket.length - 12);
+        channel.writeInbound(secondPart);
+
+        // 现在应该能读取到解码后的数据
+        Object result = channel.readInbound();
+        assertNotNull(result, "Result should not be null after second write");
+        assertTrue(result instanceof byte[], "Result should be byte[]");
+        byte[] decodedPacket = (byte[]) result;
+        assertEquals(fullPacket.length, decodedPacket.length);
+        assertArrayEquals(fullPacket, decodedPacket);
+
+        // 验证没有更多数据
+        assertNull(channel.readInbound());
+        assertFalse(channel.finish()); // 通道应该没有更多数据,所以finish应该返回false
+    }
+
+    @Test
+    void testMultipleFragmentedPackets2019() {
+        // 测试2019版本多个拆包情况:第一个包分两段,第二个包完整
+        EmbeddedChannel channel = new EmbeddedChannel(new Jtt1078MessageDecoder(JttConstants.TYPE_JTT808_2019));
+
+        // 构造两个2019版本数据包
+        byte[] packet1 = new byte[] {
+                0x30, 0x31, 0x63, 0x64, // 头部标识 (字节0-3)
+                0x01, 0x02, 0x03, 0x04, // 字节4-7
+                0x00, 0x00, 0x00, 0x00, // 2019版本新增的4字节SIM卡号扩展 (字节8-11)
+                0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, // 7字节填充 (字节12-18)
+                (byte) 0x40, // 字节19,前4位为0100,表示包头长度为22字节
+                0x00, 0x02, // 数据长度: 2字节 (位于包头最后2个字节,大端序)
+                0x11, 0x22 // 数据 (2字节)
+        };
+
+        byte[] packet2 = new byte[] {
+                0x30, 0x31, 0x63, 0x64, // 头部标识 (字节0-3)
+                0x01, 0x02, 0x03, 0x04, // 字节4-7
+                0x00, 0x00, 0x00, 0x00, // 2019版本新增的4字节SIM卡号扩展 (字节8-11)
+                0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, // 7字节填充 (字节12-18)
+                (byte) 0x40, // 字节19,前4位为0100,表示包头长度为22字节
+                0x00, 0x03, // 数据长度: 3字节 (位于包头最后2个字节,大端序)
+                0x33, 0x44, 0x55 // 数据 (3字节)
+        };
+
+        // 第一次发送第一个包的前16字节
+        byte[] firstChunk = new byte[16];
+        System.arraycopy(packet1, 0, firstChunk, 0, 16); // packet1的前16字节
+
+        channel.writeInbound(Unpooled.copiedBuffer(firstChunk));
+
+        // 第二次发送第一个包的剩余部分 + 第二个包的完整数据
+        byte[] secondChunk = new byte[packet1.length - 16 + packet2.length];
+        System.arraycopy(packet1, 16, secondChunk, 0, packet1.length - 16); // packet1的剩余部分
+        System.arraycopy(packet2, 0, secondChunk, packet1.length - 16, packet2.length); // 完整的packet2
+
+        channel.writeInbound(Unpooled.copiedBuffer(secondChunk));
+
+        // 应该能正确解析出两个数据包
+        Object result1 = channel.readInbound();
+        Object result2 = channel.readInbound();
+
+        // 验证第一个包
+        assertNotNull(result1, "First packet should not be null");
+        assertTrue(result1 instanceof byte[], "First packet should be byte[]");
+        byte[] decodedPacket1 = (byte[]) result1;
+        assertEquals(packet1.length, decodedPacket1.length);
+        assertArrayEquals(packet1, decodedPacket1);
+
+        // 验证第二个包
+        assertNotNull(result2, "Second packet should not be null");
+        assertTrue(result2 instanceof byte[], "Second packet should be byte[]");
+        byte[] decodedPacket2 = (byte[]) result2;
+        assertEquals(packet2.length, decodedPacket2.length);
+        assertArrayEquals(packet2, decodedPacket2);
+
+        // 验证没有更多数据
+        assertNull(channel.readInbound());
+        assertFalse(channel.finish()); // 通道应该没有更多数据,所以finish应该返回false
+    }
+
+    @Test
+    void testRealPacket2019() {
+        // 测试真实的2019版本数据包
+        // 数据包: "30316364816200000000000001501122334401010000019ac52ea5c003e801e0001c0000000167640029ac154a0b01269a8101012000007d00000ea60080"
+        EmbeddedChannel channel = new EmbeddedChannel(new Jtt1078MessageDecoder(JttConstants.TYPE_JTT808_2019));
+
+        // 将十六进制字符串转换为字节数组
+        String hexString = "30316364816200000000000001501122334401010000019ac52ea5c003e801e0001c0000000167640029ac154a0b01269a8101012000007d00000ea60080";
+        byte[] realPacket = hexStringToBytes(hexString);
+
+        // 写入真实数据包
+        channel.writeInbound(Unpooled.copiedBuffer(realPacket));
+
+        // 读取解码后的数据
+        Object result = channel.readInbound();
+
+        // 验证解码结果
+        assertNotNull(result, "Real packet should not be null");
+        assertTrue(result instanceof byte[], "Real packet should be byte[]");
+        byte[] decodedPacket = (byte[]) result;
+        assertEquals(realPacket.length, decodedPacket.length);
+        assertArrayEquals(realPacket, decodedPacket);
+
+        // 验证没有更多数据
+        assertNull(channel.readInbound());
+        assertFalse(channel.finish());
+    }
+
+    /**
+     * 将十六进制字符串转换为字节数组
+     * 
+     * @param hexString 十六进制字符串
+     * @return 字节数组
+     */
+    private byte[] hexStringToBytes(String hexString) {
+        int len = hexString.length();
+        byte[] data = new byte[len / 2];
+        for (int i = 0; i < len; i += 2) {
+            data[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4)
+                                 + Character.digit(hexString.charAt(i + 1), 16));
+        }
+        return data;
+    }
+
+}