Java NALU 转 JT/T 1078 媒体包工具实现

说明

JT1078 媒体包结构(视频包固定头部24字节 + NALU负载):

  1. 4字节魔数 0x30 0x31 0x63 0x64
  2. 1字节RTP标志位 V=2
  3. 2字节序列号
  4. 6字节BCD格式SIM卡号
  5. 1字节逻辑通道号
  6. 1字节帧分片标记(0不分片/1首包/3中间/2尾包)
  7. 8字节时间戳(毫秒)+帧间隔
  8. 2字节负载长度
  9. NALU裸流数据

完整工具类

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;

import java.nio.ByteOrder;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * NALU -> JT/T 1078 媒体数据包封装工具
 * 支持:不分包、大包自动分片(MTU 950)
 */
public class Jt1078NaluEncoder {
    // JT1078魔数 0x30316364
    private static final int MAGIC = 0x30316364;
    // JT视频固定头部长度 24字节
    private static final int JT_HEAD_LEN = 24;
    // 单包最大NALU负载长度(留头部空间)
    private static final int MAX_NALU_PER_PACK = 950;
    // 全局自增序列号
    private final AtomicInteger seqGen = new AtomicInteger(0);

    // SIM卡号BCD 12位字符串转6字节BCD数组
    public static byte[] simToBcd(String sim) {
        if (sim.length() != 12) {
            throw new IllegalArgumentException("SIM必须12位数字");
        }
        byte[] bcd = new byte[6];
        for (int i = 0; i < 6; i++) {
            char c1 = sim.charAt(i * 2);
            char c2 = sim.charAt(i * 2 + 1);
            byte h = (byte) (c1 - '0');
            byte l = (byte) (c2 - '0');
            bcd[i] = (byte) ((h << 4) | l);
        }
        return bcd;
    }

    /**
     * 封装NALU为JT1078媒体包列表(自动分片)
     * @param nalu H264/H265完整NALU裸流
     * @param simBcd 6字节BCD SIM卡
     * @param channel 逻辑通道号 0~99
     * @param frameInterval 帧间隔ms(如33)
     * @return 多个JT1078数据包ByteBuf
     */
    public ByteBuf[] encodeNaluToJt1078(byte[] nalu, byte[] simBcd, int channel, int frameInterval) {
        int naluLen = nalu.length;
        int sliceCount = (naluLen + MAX_NALU_PER_PACK - 1) / MAX_NALU_PER_PACK;
        ByteBuf[] result = new ByteBuf[sliceCount];

        long nowTs = System.currentTimeMillis();
        for (int i = 0; i < sliceCount; i++) {
            int offset = i * MAX_NALU_PER_PACK;
            int sliceLen = Math.min(MAX_NALU_PER_PACK, naluLen - offset);
            byte subFlag;
            if (sliceCount == 1) {
                subFlag = 0x00; // 不分包
            } else if (i == 0) {
                subFlag = 0x01; // 首包
            } else if (i == sliceCount - 1) {
                subFlag = 0x02; // 尾包
            } else {
                subFlag = 0x03; // 中间包
            }
            result[i] = buildSingleJtPacket(nalu, offset, sliceLen, simBcd, channel, subFlag, nowTs, frameInterval);
        }
        return result;
    }

    /**
     * 构造单个JT1078媒体包
     */
    private ByteBuf buildSingleJtPacket(byte[] fullNalu, int offset, int sliceLen,
                                        byte[] simBcd, int channel, byte subFlag,
                                        long tsMs, int frameInterval) {
        int totalPacketLen = JT_HEAD_LEN + sliceLen;
        ByteBuf buf = Unpooled.buffer(totalPacketLen).order(ByteOrder.BIG_ENDIAN);

        // 1. 4字节魔数
        buf.writeInt(MAGIC);
        // 2. 1字节RTP标志 V=2
        buf.writeByte(0x80);
        // 3. 2字节序列号
        int seq = seqGen.getAndIncrement() & 0xFFFF;
        buf.writeShort(seq);
        // 4. 6字节SIM BCD
        buf.writeBytes(simBcd);
        // 5. 1字节逻辑通道
        buf.writeByte(channel);
        // 6. 1字节分片标记(低4位分片,高4位帧类型0视频)
        buf.writeByte(subFlag);
        // 7. 8字节:时间戳(6)+帧间隔(2)
        buf.writeLong((tsMs << 16) | (frameInterval & 0xFFFF));
        // 8. 2字节NALU分片长度
        buf.writeShort(sliceLen);
        // 9. NALU分片数据
        buf.writeBytes(fullNalu, offset, sliceLen);
        return buf;
    }

    // 测试示例
    public static void main(String[] args) {
        Jt1078NaluEncoder encoder = new Jt1078NaluEncoder();
        String sim12 = "13800138000";
        byte[] simBcd = simToBcd(sim12);
        // 模拟一段H264 NALU数据
        byte[] testNalu = new byte[]{0x00,0x00,0x01,0x67,0x42,(byte)0x80,0x1E,0xDA,0x01};
        ByteBuf[] jtPackets = encoder.encodeNaluToJt1078(testNalu, simBcd, 0, 33);

        for (ByteBuf pkt : jtPackets) {
            byte[] raw = new byte[pkt.readableBytes()];
            pkt.readBytes(raw);
            System.out.println("JT包长度:" + raw.length);
            pkt.release();
        }
    }
}

配套UDP发送工具(发给JT1078平台)

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;

public class Jt1078UdpSender {
    private final DatagramSocket socket;
    private final InetSocketAddress target;

    public Jt1078UdpSender(String host, int port) throws Exception {
        socket = new DatagramSocket();
        target = new InetSocketAddress(host, port);
    }

    public void sendPacket(byte[] jtData) throws Exception {
        DatagramPacket dp = new DatagramPacket(jtData, jtData.length, target);
        socket.send(dp);
    }

    public void close() {
        socket.close();
    }
}

使用流程示例

// 1. 初始化编码器
Jt1078NaluEncoder encoder = new Jt1078NaluEncoder();
byte[] simBcd = Jt1078NaluEncoder.simToBcd("13912345678");
// 2. UDP发送器,连接JT1078流媒体服务端口
Jt1078UdpSender udpSender = new Jt1078UdpSender("127.0.0.1", 8000);

// 循环收到H264 NALU裸流 byte[] nalu
while (true) {
    byte[] nalu = getH264Nalu();
    // 封装为JT1078包数组(自动分片)
    ByteBuf[] jtBufs = encoder.encodeNaluToJt1078(nalu, simBcd, 0, 33);
    for (ByteBuf buf : jtBufs) {
        byte[] pkt = new byte[buf.readableBytes()];
        buf.readBytes(pkt);
        udpSender.sendPacket(pkt);
        buf.release();
    }
    Thread.sleep(30);
}

关键适配要点

  1. 分片机制:NALU超过950字节自动拆分包,头部标记首/中/尾包,平台才能拼接完整帧;
  2. SIM卡必须12位数字转6字节BCD,JT1078强制;
  3. 输出UDP包可直接发给支持JT1078的流媒体服务(ZLMediaKit JT1078端口);
  4. 序列号全局自增,uint16循环溢出自动处理;
  5. 时间戳使用系统毫秒,兼容国标时间格式;
  6. 若需要反向 JT1078包解析为NALU,可以配套写解码工具。

依赖

仅依赖 Netty buffer(工程已有Netty直接用,无Netty可替换byte数组手动拼接)

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-buffer</artifactId>
    <version>4.1.94.Final</version>
</dependency>

无Netty纯原生byte数组版本

如需脱离Netty,我可以给你纯byte[]拼接版本,不引入任何第三方包。

更多推荐