|
|
@@ -1,8 +1,16 @@
|
|
|
package com.jttserver.relay;
|
|
|
|
|
|
+import java.util.Map;
|
|
|
+import java.util.concurrent.ConcurrentHashMap;
|
|
|
+
|
|
|
+import org.slf4j.Logger;
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
+
|
|
|
+import com.jttserver.codec.FlvPacketizer;
|
|
|
import com.jttserver.protocol.Jtt1078PacketParams;
|
|
|
import com.jttserver.service.publisher.PublishServer;
|
|
|
import com.jttserver.service.receiver.RecvSever;
|
|
|
+import com.jttserver.utils.SimCardUtils;
|
|
|
|
|
|
import io.netty.channel.Channel;
|
|
|
|
|
|
@@ -11,6 +19,8 @@ import io.netty.channel.Channel;
|
|
|
*/
|
|
|
public abstract class StreamRelay {
|
|
|
|
|
|
+ private static final Logger logger = LoggerFactory.getLogger(StreamRelay.class);
|
|
|
+
|
|
|
// 单个nalu解析结果
|
|
|
protected static class NaluSegment {
|
|
|
final byte[] payload; // 解析出的NALU负载(不含起始码)
|
|
|
@@ -31,6 +41,12 @@ public abstract class StreamRelay {
|
|
|
// 路径前缀
|
|
|
protected String prefix;
|
|
|
|
|
|
+ // 为每个 channelId 缓存已计算的 streamId,避免重复计算
|
|
|
+ protected Map<String, String> channelIdToStreamId = new ConcurrentHashMap<>();
|
|
|
+
|
|
|
+ // 使用单实例 FlvPacketizer,内部以 channelId 维护编解码器信息
|
|
|
+ protected FlvPacketizer packetizer;
|
|
|
+
|
|
|
/*
|
|
|
* 构造函数
|
|
|
*/
|
|
|
@@ -45,6 +61,12 @@ public abstract class StreamRelay {
|
|
|
this.publishServer = publishServer;
|
|
|
this.receiveServer = receiveServer;
|
|
|
this.prefix = prefix;
|
|
|
+
|
|
|
+ // 当前仅支持 FLV 打包
|
|
|
+ this.packetizer = new FlvPacketizer();
|
|
|
+ if (this.publishServer != null) {
|
|
|
+ publishServer.addPrefixRelay(prefix, this);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
public void setPublishServer(PublishServer publishServer) {
|
|
|
@@ -69,26 +91,180 @@ public abstract class StreamRelay {
|
|
|
/*
|
|
|
* 推流视频数据
|
|
|
*/
|
|
|
- public abstract void publishVideo(String channelId, byte[] nalu, Jtt1078PacketParams params,long timestampMs);
|
|
|
+ public void publishVideo(String channelId, byte[] nalu, Jtt1078PacketParams params,
|
|
|
+ long timestampMs) {
|
|
|
+ if (nalu == null || nalu.length == 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // 逐段解析并发布单个NALU单元(不含起始码)
|
|
|
+ int offset = 0;
|
|
|
+ int totalLength = nalu.length;
|
|
|
+ while (offset < totalLength) {
|
|
|
+ // 提取从当前偏移开始的下一个NALU单元(不含起始码),并返回本次消耗的字节数
|
|
|
+ NaluSegment segment = getNaluSingle(nalu, offset);
|
|
|
+ if (segment.consumedBytes <= 0) { // 未能有效解析出片段,结束循环
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ // 有效负载才进行发布
|
|
|
+ if (segment.payload != null && segment.payload.length > 0) {
|
|
|
+ publishSingleNalu(channelId, segment.payload, params, timestampMs);
|
|
|
+ }
|
|
|
+ // 增加偏移到下一个片段起始位置
|
|
|
+ offset += segment.consumedBytes;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /*
|
|
|
+ * 推流单个NALU单元
|
|
|
+ */
|
|
|
+ public void publishSingleNalu(String channelId, byte[] nalu, Jtt1078PacketParams params,
|
|
|
+ long timestampMs) {
|
|
|
+ if (nalu == null || nalu.length == 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理单个NALU数据
|
|
|
+ byte[] tag = packetizer.processVideoNalu(channelId, nalu, params, timestampMs);
|
|
|
+ if (tag != null && tag.length > 0) {
|
|
|
+ // 通过构造时缓存的 publishServer 引用进行广播
|
|
|
+ if (publishServer != null) {
|
|
|
+ broadcastStreamData(channelId, tag, params);
|
|
|
+ } else {
|
|
|
+ logger.warn("publishServer is null, cannot broadcast stream data");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /*
|
|
|
+ * 广播逻辑
|
|
|
+ */
|
|
|
+ private void broadcastStreamData(String channelId, byte[] data, Jtt1078PacketParams params) {
|
|
|
+ // 先尝试复用缓存的 streamId
|
|
|
+ String streamId = channelIdToStreamId.get(channelId);
|
|
|
+ if (streamId == null || streamId.isEmpty()) {
|
|
|
+ // 首次或未缓存,计算并建立映射
|
|
|
+ streamId = SimCardUtils.buildStreamId(params.simCardNumberStr, params.logicChannelNumber);
|
|
|
+ if (streamId != null && !streamId.isEmpty()) {
|
|
|
+ logger.info("channelId: {}, streamId: {}", channelId, streamId);
|
|
|
+ channelIdToStreamId.put(channelId, streamId);
|
|
|
+ // 同步建立映射关系
|
|
|
+ publishServer.mapStreamToChannel(streamId, channelId, prefix);
|
|
|
+
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 使用 streamId
|
|
|
+ if (streamId != null && !streamId.isEmpty()) {
|
|
|
+ threadBocastStreamData(publishServer, channelId, streamId, data, prefix);
|
|
|
+ } else {
|
|
|
+ logger.warn("streamId为空,无法广播数据");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 提取从指定偏移开始的单个NALU负载(不含起始码),返回同时包含本次消耗的字节数
|
|
|
+ private NaluSegment getNaluSingle(byte[] data, int offset) {
|
|
|
+ int len = (data == null) ? 0 : data.length;
|
|
|
+ if (data == null || offset >= len) {
|
|
|
+ return new NaluSegment(new byte[0], 0);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 查找当前或之后的起始码(00 00 00 01)
|
|
|
+ int start = -1;
|
|
|
+ for (int i = offset; i <= len - 4; i++) {
|
|
|
+ if (data[i] == 0x00 && data[i + 1] == 0x00 && data[i + 2] == 0x00 && data[i + 3] == 0x01) {
|
|
|
+ start = i;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (start < 0) {
|
|
|
+ // 未检测到起始码:将剩余数据视为单个NALU负载进行处理
|
|
|
+ int remaining = len - offset;
|
|
|
+ byte[] payload = new byte[remaining];
|
|
|
+ System.arraycopy(data, offset, payload, 0, remaining);
|
|
|
+ return new NaluSegment(payload, remaining);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 负载从起始码之后开始
|
|
|
+ int payloadStart = start + 4;
|
|
|
+
|
|
|
+ // 查找下一个起始码,用于确定本片段的结束位置
|
|
|
+ int nextStart = -1;
|
|
|
+ for (int j = payloadStart; j <= len - 4; j++) {
|
|
|
+ if (data[j] == 0x00 && data[j + 1] == 0x00 && data[j + 2] == 0x00 && data[j + 3] == 0x01) {
|
|
|
+ nextStart = j;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ int end = (nextStart >= 0) ? nextStart : len;
|
|
|
+
|
|
|
+ // 复制负载数据(不含起始码)
|
|
|
+
|
|
|
+ // 负载为起始码之后到下一个起始码(或数据末尾)之间的数据
|
|
|
+ int payloadLen = end - payloadStart;
|
|
|
+ if (payloadLen <= 0) {
|
|
|
+ return new NaluSegment(new byte[0], 0);
|
|
|
+ }
|
|
|
+ byte[] payload = new byte[payloadLen];
|
|
|
+
|
|
|
+ System.arraycopy(data, payloadStart, payload, 0, payloadLen);
|
|
|
+
|
|
|
+ // 本次消耗的字节数 = 起始码长度(4) + 负载长度
|
|
|
+ int consumed = 4 + payloadLen;
|
|
|
+ return new NaluSegment(payload, consumed);
|
|
|
+ }
|
|
|
|
|
|
/*
|
|
|
* 推流音频数据
|
|
|
*/
|
|
|
- public abstract void publishAudio(String channelId, byte[] audio, Jtt1078PacketParams params, long timestampMs);
|
|
|
+ public void publishAudio(String channelId, byte[] audio, Jtt1078PacketParams params,
|
|
|
+ long timestampMs) {
|
|
|
+ if (audio == null || audio.length == 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ byte[] tag = packetizer.processAudioNalu(channelId, audio, params, timestampMs);
|
|
|
+ if (tag != null && tag.length > 0) {
|
|
|
+ // 通过构造时缓存的 publishServer 引用进行广播
|
|
|
+ if (publishServer != null) {
|
|
|
+ // 如果是channel第一次广播音频,先广播AAC序列头
|
|
|
+ byte[] aacSeqHeader = packetizer.getOrCreateAacSequenceHeader(channelId, timestampMs);
|
|
|
+ if (aacSeqHeader != null && aacSeqHeader.length > 0) {
|
|
|
+ broadcastStreamData(channelId, aacSeqHeader, params);
|
|
|
+ }
|
|
|
+
|
|
|
+ broadcastStreamData(channelId, tag, params);
|
|
|
+ } else {
|
|
|
+ logger.warn("ws is null, cannot broadcast stream data");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
/*
|
|
|
* 关闭并清理指定通道的资源
|
|
|
*/
|
|
|
- public abstract void closeChannel(String channelId);
|
|
|
+ public void closeChannel(String channelId) {
|
|
|
+ // 同步清理 FlvPacketizer 的该通道信息
|
|
|
+ packetizer.clearChannel(channelId);
|
|
|
+ // 同步移除映射
|
|
|
+ if (publishServer != null) {
|
|
|
+ publishServer.removeChannelMapping(channelId);
|
|
|
+ }
|
|
|
+ // 同步清理本地缓存的 streamId
|
|
|
+ channelIdToStreamId.remove(channelId);
|
|
|
+ }
|
|
|
|
|
|
|
|
|
/*
|
|
|
* 初始化通道连接(订阅时调用一次,用于补发数据等)
|
|
|
*/
|
|
|
- public abstract void initChannelConn(String channelId, Channel ch);
|
|
|
+ public abstract void initChannelConn(String channelId, String streamId, Channel ch);
|
|
|
|
|
|
/**
|
|
|
* 结束通道连接(断开时调用一次,清理资源)
|
|
|
*/
|
|
|
- public abstract void destroyChannelDisconn(String channelId);
|
|
|
+ public abstract void destroyChannelDisconn(String channelId, String streamId);
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 线程中广播流数据
|
|
|
+ */
|
|
|
+ public abstract void threadBocastStreamData(PublishServer publishServer, String channelId, String streamId, byte[] data, String prefix);
|
|
|
}
|