feat: make gb32960 archive history query production ready
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
package com.lingniu.ingest.protocol.jt1078.codec.parser;
|
||||
|
||||
import com.lingniu.ingest.protocol.jt1078.model.Jt1078MessageId;
|
||||
import com.lingniu.ingest.protocol.jt808.codec.BodyParser;
|
||||
import com.lingniu.ingest.protocol.jt808.model.Jt808Body;
|
||||
import com.lingniu.ingest.protocol.jt808.model.Jt808Header;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/** 0x1205 终端上传文件列表,摘要字段结构化,明细原始 body 保留。 */
|
||||
public final class FileListBodyParser implements BodyParser {
|
||||
@Override public int messageId() { return Jt1078MessageId.TERMINAL_FILE_LIST; }
|
||||
|
||||
@Override
|
||||
public Jt808Body parse(Jt808Header header, ByteBuffer body) {
|
||||
byte[] raw = new byte[body.remaining()];
|
||||
body.get(raw);
|
||||
ByteBuffer b = ByteBuffer.wrap(raw);
|
||||
int responseSerialNo = b.remaining() >= 2 ? b.getShort() & 0xFFFF : 0;
|
||||
long fileCount = b.remaining() >= 4 ? b.getInt() & 0xFFFFFFFFL : 0L;
|
||||
return new Jt808Body.FileList(responseSerialNo, fileCount, raw);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.lingniu.ingest.protocol.jt1078.codec.parser;
|
||||
|
||||
import com.lingniu.ingest.protocol.jt1078.model.Jt1078MessageId;
|
||||
import com.lingniu.ingest.protocol.jt808.codec.BodyParser;
|
||||
import com.lingniu.ingest.protocol.jt808.model.Jt808Body;
|
||||
import com.lingniu.ingest.protocol.jt808.model.Jt808Header;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/** 0x1206 文件上传完成通知。 */
|
||||
public final class FileUploadCompleteBodyParser implements BodyParser {
|
||||
@Override public int messageId() { return Jt1078MessageId.TERMINAL_FILE_UPLOAD_DONE; }
|
||||
|
||||
@Override
|
||||
public Jt808Body parse(Jt808Header header, ByteBuffer body) {
|
||||
byte[] raw = new byte[body.remaining()];
|
||||
body.get(raw);
|
||||
ByteBuffer b = ByteBuffer.wrap(raw);
|
||||
int responseSerialNo = b.remaining() >= 2 ? b.getShort() & 0xFFFF : 0;
|
||||
int result = b.hasRemaining() ? b.get() & 0xFF : 0;
|
||||
return new Jt808Body.FileUploadComplete(responseSerialNo, result, raw);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.lingniu.ingest.protocol.jt1078.codec.parser;
|
||||
|
||||
import com.lingniu.ingest.protocol.jt1078.model.Jt1078MessageId;
|
||||
import com.lingniu.ingest.protocol.jt808.codec.BodyParser;
|
||||
import com.lingniu.ingest.protocol.jt808.model.Jt808Body;
|
||||
import com.lingniu.ingest.protocol.jt808.model.Jt808Header;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/** 0x1003 终端上传音视频属性。 */
|
||||
public final class MediaAttributesBodyParser implements BodyParser {
|
||||
@Override public int messageId() { return Jt1078MessageId.TERMINAL_MEDIA_ATTRS_REPORT; }
|
||||
|
||||
@Override
|
||||
public Jt808Body parse(Jt808Header header, ByteBuffer body) {
|
||||
byte[] raw = new byte[body.remaining()];
|
||||
body.get(raw);
|
||||
ByteBuffer b = ByteBuffer.wrap(raw);
|
||||
return new Jt808Body.MediaAttributes(
|
||||
readU8(b),
|
||||
readU8(b),
|
||||
readU8(b),
|
||||
readU8(b),
|
||||
readU16(b),
|
||||
readU8(b) == 1,
|
||||
readU8(b),
|
||||
readU8(b),
|
||||
readU8(b),
|
||||
raw);
|
||||
}
|
||||
|
||||
private static int readU8(ByteBuffer b) {
|
||||
return b.hasRemaining() ? b.get() & 0xFF : 0;
|
||||
}
|
||||
|
||||
private static int readU16(ByteBuffer b) {
|
||||
return b.remaining() >= 2 ? b.getShort() & 0xFFFF : 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.lingniu.ingest.protocol.jt1078.codec.parser;
|
||||
|
||||
import com.lingniu.ingest.codec.BcdCodec;
|
||||
import com.lingniu.ingest.protocol.jt1078.model.Jt1078MessageId;
|
||||
import com.lingniu.ingest.protocol.jt808.codec.BodyParser;
|
||||
import com.lingniu.ingest.protocol.jt808.model.Jt808Body;
|
||||
import com.lingniu.ingest.protocol.jt808.model.Jt808Header;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/** 0x1005 终端上传乘客流量,字段结构化后仍按可查询透传事件入库。 */
|
||||
public final class PassengerVolumeBodyParser implements BodyParser {
|
||||
@Override public int messageId() { return Jt1078MessageId.TERMINAL_PASSENGER_VOLUME; }
|
||||
|
||||
@Override
|
||||
public Jt808Body parse(Jt808Header header, ByteBuffer body) {
|
||||
byte[] raw = new byte[body.remaining()];
|
||||
body.get(raw);
|
||||
ByteBuffer b = ByteBuffer.wrap(raw);
|
||||
return new Jt808Body.PassengerVolume(
|
||||
readU8(b),
|
||||
readBcdTime(b),
|
||||
readBcdTime(b),
|
||||
readU16(b),
|
||||
readU16(b),
|
||||
raw);
|
||||
}
|
||||
|
||||
private static int readU8(ByteBuffer b) {
|
||||
return b.hasRemaining() ? b.get() & 0xFF : 0;
|
||||
}
|
||||
|
||||
private static int readU16(ByteBuffer b) {
|
||||
return b.remaining() >= 2 ? b.getShort() & 0xFFFF : 0;
|
||||
}
|
||||
|
||||
private static String readBcdTime(ByteBuffer b) {
|
||||
if (b.remaining() < 6) {
|
||||
byte[] tail = new byte[b.remaining()];
|
||||
b.get(tail);
|
||||
return "";
|
||||
}
|
||||
byte[] time = new byte[6];
|
||||
b.get(time);
|
||||
String ts = BcdCodec.decode(time);
|
||||
if (ts.length() != 12) {
|
||||
return "";
|
||||
}
|
||||
return "20" + ts.substring(0, 2)
|
||||
+ "-" + ts.substring(2, 4)
|
||||
+ "-" + ts.substring(4, 6)
|
||||
+ "T" + ts.substring(6, 8)
|
||||
+ ":" + ts.substring(8, 10)
|
||||
+ ":" + ts.substring(10, 12);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package com.lingniu.ingest.protocol.jt1078.config;
|
||||
|
||||
import com.lingniu.ingest.core.concurrency.DisruptorEventBus;
|
||||
import com.lingniu.ingest.core.dispatcher.Dispatcher;
|
||||
import com.lingniu.ingest.identity.VehicleIdentityResolver;
|
||||
import com.lingniu.ingest.protocol.jt1078.codec.parser.FileUploadCompleteBodyParser;
|
||||
import com.lingniu.ingest.protocol.jt1078.codec.parser.FileListBodyParser;
|
||||
import com.lingniu.ingest.protocol.jt1078.codec.parser.MediaAttributesBodyParser;
|
||||
import com.lingniu.ingest.protocol.jt1078.codec.parser.PassengerVolumeBodyParser;
|
||||
import com.lingniu.ingest.protocol.jt1078.handler.Jt1078MalformedRtpHandler;
|
||||
import com.lingniu.ingest.protocol.jt1078.media.Jt1078MalformedRtpEventPublisher;
|
||||
import com.lingniu.ingest.protocol.jt1078.media.Jt1078MediaArchiveService;
|
||||
import com.lingniu.ingest.protocol.jt1078.media.Jt1078MediaStreamServer;
|
||||
import com.lingniu.ingest.protocol.jt1078.media.Jt1078UdpMediaStreamServer;
|
||||
import com.lingniu.ingest.protocol.jt808.codec.BodyParser;
|
||||
import com.lingniu.ingest.protocol.jt808.config.Jt808AutoConfiguration;
|
||||
import com.lingniu.ingest.sink.archive.ArchiveStore;
|
||||
import com.lingniu.ingest.sink.archive.config.SinkArchiveAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
/**
|
||||
* jt1078 在 jt808 之前装配它自己的 Parser Bean,Spring 会把它们注入到 jt808 的
|
||||
* {@code BodyParserRegistry} 里,实现"同一个 Netty 端口复用、消息集热插拔"。
|
||||
*/
|
||||
@AutoConfiguration(before = Jt808AutoConfiguration.class, after = SinkArchiveAutoConfiguration.class)
|
||||
@EnableConfigurationProperties(Jt1078Properties.class)
|
||||
@ConditionalOnProperty(prefix = "lingniu.ingest.jt1078", name = "enabled", havingValue = "true")
|
||||
public class Jt1078AutoConfiguration {
|
||||
|
||||
@Bean
|
||||
public BodyParser jt1078MediaAttributesBodyParser() {
|
||||
return new MediaAttributesBodyParser();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public BodyParser jt1078PassengerVolumeBodyParser() {
|
||||
return new PassengerVolumeBodyParser();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public BodyParser jt1078FileListBodyParser() {
|
||||
return new FileListBodyParser();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public BodyParser jt1078FileUploadCompleteBodyParser() {
|
||||
return new FileUploadCompleteBodyParser();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnBean(ArchiveStore.class)
|
||||
public Jt1078MediaArchiveService jt1078MediaArchiveService(ArchiveStore archive,
|
||||
VehicleIdentityResolver identityResolver,
|
||||
DisruptorEventBus eventBus,
|
||||
Jt1078Properties props) {
|
||||
return new Jt1078MediaArchiveService(
|
||||
archive, identityResolver, eventBus::publish, props.getMediaStream().getSegmentSeconds());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public Jt1078MalformedRtpEventPublisher jt1078MalformedRtpEventPublisher(VehicleIdentityResolver identityResolver,
|
||||
Dispatcher dispatcher) {
|
||||
return new Jt1078MalformedRtpEventPublisher(identityResolver, dispatcher::dispatch);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public Jt1078MalformedRtpHandler jt1078MalformedRtpHandler(VehicleIdentityResolver identityResolver) {
|
||||
return new Jt1078MalformedRtpHandler(identityResolver);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnBean(Jt1078MediaArchiveService.class)
|
||||
@ConditionalOnExpression("${lingniu.ingest.jt1078.media-stream.enabled:true} "
|
||||
+ "&& ${lingniu.ingest.jt1078.media-stream.tcp-enabled:true}")
|
||||
public Jt1078MediaStreamServer jt1078MediaStreamServer(Jt1078Properties props,
|
||||
Jt1078MediaArchiveService archiveService,
|
||||
Jt1078MalformedRtpEventPublisher malformedPublisher) {
|
||||
Jt1078Properties.MediaStream media = props.getMediaStream();
|
||||
return new Jt1078MediaStreamServer(
|
||||
media.getPort(), media.getWorkerThreads(), media.getMaxPacketLength(), archiveService, malformedPublisher);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnBean(Jt1078MediaArchiveService.class)
|
||||
@ConditionalOnExpression("${lingniu.ingest.jt1078.media-stream.enabled:true} "
|
||||
+ "&& ${lingniu.ingest.jt1078.media-stream.udp-enabled:false}")
|
||||
public Jt1078UdpMediaStreamServer jt1078UdpMediaStreamServer(Jt1078Properties props,
|
||||
Jt1078MediaArchiveService archiveService,
|
||||
Jt1078MalformedRtpEventPublisher malformedPublisher) {
|
||||
Jt1078Properties.MediaStream media = props.getMediaStream();
|
||||
return new Jt1078UdpMediaStreamServer(
|
||||
media.getUdpPort(), media.getWorkerThreads(), media.getMaxPacketLength(), archiveService, malformedPublisher);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.lingniu.ingest.protocol.jt1078.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
@ConfigurationProperties(prefix = "lingniu.ingest.jt1078")
|
||||
public class Jt1078Properties {
|
||||
|
||||
private boolean enabled = false;
|
||||
private MediaStream mediaStream = new MediaStream();
|
||||
|
||||
public boolean isEnabled() { return enabled; }
|
||||
public void setEnabled(boolean enabled) { this.enabled = enabled; }
|
||||
public MediaStream getMediaStream() { return mediaStream; }
|
||||
public void setMediaStream(MediaStream mediaStream) { this.mediaStream = mediaStream; }
|
||||
|
||||
public static class MediaStream {
|
||||
private boolean enabled = true;
|
||||
private boolean tcpEnabled = true;
|
||||
private boolean udpEnabled = false;
|
||||
private int port = 11078;
|
||||
private int udpPort = 11078;
|
||||
private int workerThreads = 0;
|
||||
private int maxPacketLength = 1024 * 1024;
|
||||
private int segmentSeconds = 300;
|
||||
|
||||
public boolean isEnabled() { return enabled; }
|
||||
public void setEnabled(boolean enabled) { this.enabled = enabled; }
|
||||
public boolean isTcpEnabled() { return tcpEnabled; }
|
||||
public void setTcpEnabled(boolean tcpEnabled) { this.tcpEnabled = tcpEnabled; }
|
||||
public boolean isUdpEnabled() { return udpEnabled; }
|
||||
public void setUdpEnabled(boolean udpEnabled) { this.udpEnabled = udpEnabled; }
|
||||
public int getPort() { return port; }
|
||||
public void setPort(int port) { this.port = port; }
|
||||
public int getUdpPort() { return udpPort; }
|
||||
public void setUdpPort(int udpPort) { this.udpPort = udpPort; }
|
||||
public int getWorkerThreads() { return workerThreads; }
|
||||
public void setWorkerThreads(int workerThreads) { this.workerThreads = workerThreads; }
|
||||
public int getMaxPacketLength() { return maxPacketLength; }
|
||||
public void setMaxPacketLength(int maxPacketLength) { this.maxPacketLength = maxPacketLength; }
|
||||
public int getSegmentSeconds() { return segmentSeconds; }
|
||||
public void setSegmentSeconds(int segmentSeconds) { this.segmentSeconds = segmentSeconds; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.lingniu.ingest.protocol.jt1078.config;
|
||||
|
||||
import com.lingniu.ingest.protocol.jt1078.handler.Jt1078MediaSignalHandler;
|
||||
import com.lingniu.ingest.protocol.jt808.config.Jt808AutoConfiguration;
|
||||
import com.lingniu.ingest.protocol.jt808.mapper.Jt808EventMapper;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
/**
|
||||
* 1078 信令复用 JT808 解码和 mapper,必须在 JT808 AutoConfiguration 之后判断。
|
||||
*/
|
||||
@AutoConfiguration(after = Jt808AutoConfiguration.class)
|
||||
@ConditionalOnProperty(prefix = "lingniu.ingest.jt1078", name = "enabled", havingValue = "true")
|
||||
@ConditionalOnBean(Jt808EventMapper.class)
|
||||
public class Jt1078SignalAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public Jt1078MediaSignalHandler jt1078MediaSignalHandler(Jt808EventMapper mapper) {
|
||||
return new Jt1078MediaSignalHandler(mapper);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
package com.lingniu.ingest.protocol.jt1078.downlink;
|
||||
|
||||
import com.lingniu.ingest.protocol.jt1078.model.Jt1078MessageId;
|
||||
import com.lingniu.ingest.protocol.jt808.downlink.Jt808Commands;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
/** JT/T 1078 下行信令 body 构造器,外层帧仍由 JT808 通道发送。 */
|
||||
public final class Jt1078Commands {
|
||||
|
||||
private static final Charset GBK = Charset.forName("GBK");
|
||||
|
||||
private Jt1078Commands() {}
|
||||
|
||||
/** 0x1003 查询终端音视频属性。 */
|
||||
public static Jt808Commands.DownlinkCommand queryMediaAttributes() {
|
||||
return new Jt808Commands.DownlinkCommand(Jt1078MessageId.PLATFORM_QUERY_MEDIA_ATTRS, new byte[0]);
|
||||
}
|
||||
|
||||
/** 0x9101 实时音视频传输请求。 */
|
||||
public static Jt808Commands.DownlinkCommand realtimePlay(String ip,
|
||||
int tcpPort,
|
||||
int udpPort,
|
||||
int channelNo,
|
||||
int mediaType,
|
||||
int streamType) {
|
||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||
writeStringWithU8Length(os, ip);
|
||||
writeU16(os, tcpPort);
|
||||
writeU16(os, udpPort);
|
||||
writeU8(os, channelNo);
|
||||
writeU8(os, mediaType);
|
||||
writeU8(os, streamType);
|
||||
return new Jt808Commands.DownlinkCommand(Jt1078MessageId.PLATFORM_REALTIME_PLAY, os.toByteArray());
|
||||
}
|
||||
|
||||
/** 0x9102 音视频实时传输控制。 */
|
||||
public static Jt808Commands.DownlinkCommand realtimeControl(int channelNo,
|
||||
int command,
|
||||
int closeType,
|
||||
int streamType) {
|
||||
ByteArrayOutputStream os = new ByteArrayOutputStream(4);
|
||||
writeU8(os, channelNo);
|
||||
writeU8(os, command);
|
||||
writeU8(os, closeType);
|
||||
writeU8(os, streamType);
|
||||
return new Jt808Commands.DownlinkCommand(Jt1078MessageId.PLATFORM_REALTIME_CONTROL, os.toByteArray());
|
||||
}
|
||||
|
||||
/** 0x9201 平台下发远程录像回放请求。 */
|
||||
public static Jt808Commands.DownlinkCommand historyPlay(String ip,
|
||||
int tcpPort,
|
||||
int udpPort,
|
||||
int channelNo,
|
||||
int mediaType,
|
||||
int streamType,
|
||||
int storageType,
|
||||
int playbackMode,
|
||||
int playbackSpeed,
|
||||
String startTime,
|
||||
String endTime) {
|
||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||
writeStringWithU8Length(os, ip);
|
||||
writeU16(os, tcpPort);
|
||||
writeU16(os, udpPort);
|
||||
writeU8(os, channelNo);
|
||||
writeU8(os, mediaType);
|
||||
writeU8(os, streamType);
|
||||
writeU8(os, storageType);
|
||||
writeU8(os, playbackMode);
|
||||
writeU8(os, playbackSpeed);
|
||||
writeBcdTime(os, startTime);
|
||||
writeBcdTime(os, endTime);
|
||||
return new Jt808Commands.DownlinkCommand(Jt1078MessageId.PLATFORM_HISTORY_PLAY, os.toByteArray());
|
||||
}
|
||||
|
||||
/** 0x9202 平台下发远程录像回放控制。 */
|
||||
public static Jt808Commands.DownlinkCommand historyControl(int channelNo,
|
||||
int playbackMode,
|
||||
int playbackSpeed,
|
||||
String playbackTime) {
|
||||
ByteArrayOutputStream os = new ByteArrayOutputStream(9);
|
||||
writeU8(os, channelNo);
|
||||
writeU8(os, playbackMode);
|
||||
writeU8(os, playbackSpeed);
|
||||
writeBcdTime(os, playbackTime);
|
||||
return new Jt808Commands.DownlinkCommand(Jt1078MessageId.PLATFORM_HISTORY_CONTROL, os.toByteArray());
|
||||
}
|
||||
|
||||
/** 0x9205 查询资源列表。 */
|
||||
public static Jt808Commands.DownlinkCommand resourceSearch(int channelNo,
|
||||
String startTime,
|
||||
String endTime,
|
||||
long warnBit1,
|
||||
long warnBit2,
|
||||
int mediaType,
|
||||
int streamType,
|
||||
int storageType) {
|
||||
ByteArrayOutputStream os = new ByteArrayOutputStream(24);
|
||||
writeU8(os, channelNo);
|
||||
writeBcdTime(os, startTime);
|
||||
writeBcdTime(os, endTime);
|
||||
writeU32(os, warnBit1);
|
||||
writeU32(os, warnBit2);
|
||||
writeU8(os, mediaType);
|
||||
writeU8(os, streamType);
|
||||
writeU8(os, storageType);
|
||||
return new Jt808Commands.DownlinkCommand(Jt1078MessageId.PLATFORM_RESOURCE_SEARCH, os.toByteArray());
|
||||
}
|
||||
|
||||
/** 0x9206 文件上传指令。 */
|
||||
public static Jt808Commands.DownlinkCommand fileUpload(String ip,
|
||||
int port,
|
||||
String username,
|
||||
String password,
|
||||
String path,
|
||||
int channelNo,
|
||||
String startTime,
|
||||
String endTime,
|
||||
long warnBit1,
|
||||
long warnBit2,
|
||||
int mediaType,
|
||||
int streamType,
|
||||
int storageType,
|
||||
int condition) {
|
||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||
writeStringWithU8Length(os, ip);
|
||||
writeU16(os, port);
|
||||
writeStringWithU8Length(os, username);
|
||||
writeStringWithU8Length(os, password);
|
||||
writeStringWithU8Length(os, path);
|
||||
writeU8(os, channelNo);
|
||||
writeBcdTime(os, startTime);
|
||||
writeBcdTime(os, endTime);
|
||||
writeU32(os, warnBit1);
|
||||
writeU32(os, warnBit2);
|
||||
writeU8(os, mediaType);
|
||||
writeU8(os, streamType);
|
||||
writeU8(os, storageType);
|
||||
writeU8(os, condition);
|
||||
return new Jt808Commands.DownlinkCommand(Jt1078MessageId.PLATFORM_FILE_UPLOAD, os.toByteArray());
|
||||
}
|
||||
|
||||
/** 0x9207 文件上传控制。 */
|
||||
public static Jt808Commands.DownlinkCommand fileUploadControl(int responseSerialNo, int command) {
|
||||
ByteArrayOutputStream os = new ByteArrayOutputStream(3);
|
||||
writeU16(os, responseSerialNo);
|
||||
writeU8(os, command);
|
||||
return new Jt808Commands.DownlinkCommand(Jt1078MessageId.PLATFORM_FILE_UPLOAD_CONTROL, os.toByteArray());
|
||||
}
|
||||
|
||||
private static void writeStringWithU8Length(ByteArrayOutputStream os, String value) {
|
||||
byte[] bytes = value == null ? new byte[0] : value.getBytes(GBK);
|
||||
if (bytes.length > 255) {
|
||||
throw new IllegalArgumentException("jt1078 string field too long: " + bytes.length);
|
||||
}
|
||||
writeU8(os, bytes.length);
|
||||
os.write(bytes, 0, bytes.length);
|
||||
}
|
||||
|
||||
private static void writeBcdTime(ByteArrayOutputStream os, String value) {
|
||||
String digits = value == null ? "" : value.replaceAll("\\D", "");
|
||||
if (digits.length() == 14 && digits.startsWith("20")) {
|
||||
digits = digits.substring(2);
|
||||
}
|
||||
if (digits.isBlank()) {
|
||||
digits = "000000000000";
|
||||
}
|
||||
if (digits.length() != 12) {
|
||||
throw new IllegalArgumentException("jt1078 time must be YYMMDDHHMMSS or yyyy-MM-dd HH:mm:ss: " + value);
|
||||
}
|
||||
for (int i = 0; i < digits.length(); i += 2) {
|
||||
int high = Character.digit(digits.charAt(i), 10);
|
||||
int low = Character.digit(digits.charAt(i + 1), 10);
|
||||
if (high < 0 || low < 0) {
|
||||
throw new IllegalArgumentException("jt1078 time contains non-decimal digit: " + value);
|
||||
}
|
||||
os.write((high << 4) | low);
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeU8(ByteArrayOutputStream os, int v) {
|
||||
os.write(v & 0xFF);
|
||||
}
|
||||
|
||||
private static void writeU16(ByteArrayOutputStream os, int v) {
|
||||
os.write((v >> 8) & 0xFF);
|
||||
os.write(v & 0xFF);
|
||||
}
|
||||
|
||||
private static void writeU32(ByteArrayOutputStream os, long v) {
|
||||
os.write((int) ((v >> 24) & 0xFF));
|
||||
os.write((int) ((v >> 16) & 0xFF));
|
||||
os.write((int) ((v >> 8) & 0xFF));
|
||||
os.write((int) (v & 0xFF));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.lingniu.ingest.protocol.jt1078.handler;
|
||||
|
||||
import com.lingniu.ingest.api.ProtocolId;
|
||||
import com.lingniu.ingest.api.annotation.EventEmit;
|
||||
import com.lingniu.ingest.api.annotation.MessageMapping;
|
||||
import com.lingniu.ingest.api.annotation.ProtocolHandler;
|
||||
import com.lingniu.ingest.api.event.VehicleEvent;
|
||||
import com.lingniu.ingest.identity.InMemoryVehicleIdentityService;
|
||||
import com.lingniu.ingest.identity.VehicleIdentity;
|
||||
import com.lingniu.ingest.identity.VehicleIdentityLookup;
|
||||
import com.lingniu.ingest.identity.VehicleIdentityResolver;
|
||||
import com.lingniu.ingest.identity.VehicleIdentitySource;
|
||||
import com.lingniu.ingest.protocol.jt1078.media.Jt1078MalformedRtpPacket;
|
||||
import com.lingniu.ingest.protocol.jt1078.media.Jt1078MediaMessageId;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@ProtocolHandler(protocol = ProtocolId.JT1078)
|
||||
public final class Jt1078MalformedRtpHandler {
|
||||
|
||||
private final VehicleIdentityResolver identityResolver;
|
||||
|
||||
public Jt1078MalformedRtpHandler() {
|
||||
this(new InMemoryVehicleIdentityService());
|
||||
}
|
||||
|
||||
public Jt1078MalformedRtpHandler(VehicleIdentityResolver identityResolver) {
|
||||
this.identityResolver = identityResolver == null ? new InMemoryVehicleIdentityService() : identityResolver;
|
||||
}
|
||||
|
||||
@MessageMapping(command = Jt1078MediaMessageId.MALFORMED_RTP, desc = "JT1078 RTP 入口坏包兜底")
|
||||
@EventEmit(VehicleEvent.Passthrough.class)
|
||||
public List<VehicleEvent> onMalformedRtp(Jt1078MalformedRtpPacket malformed) {
|
||||
if (malformed == null) {
|
||||
return List.of();
|
||||
}
|
||||
Instant eventTime = malformed.receivedAt() == null ? Instant.now() : malformed.receivedAt();
|
||||
IdentityResolution identity = resolveIdentity(malformed);
|
||||
Map<String, String> metadata = new HashMap<>();
|
||||
metadata.put("transport", safe(malformed.transport()));
|
||||
metadata.put("peer", safe(malformed.peer()));
|
||||
metadata.put("sim", safe(malformed.sim()));
|
||||
metadata.put("vin", identity.identity().vin());
|
||||
metadata.put("identityResolved", Boolean.toString(identity.identity().resolved()));
|
||||
metadata.put("identitySource", identity.identity().source().name());
|
||||
if (identity.errorMessage() != null) {
|
||||
metadata.put("identityError", "true");
|
||||
metadata.put("identityErrorMessage", identity.errorMessage());
|
||||
}
|
||||
metadata.put("malformedRtp", "true");
|
||||
metadata.put("parseError", "true");
|
||||
metadata.put("parseErrorMessage", safe(malformed.reason()));
|
||||
metadata.put("packetLength", Integer.toString(malformed.packetLength()));
|
||||
metadata.put("reason", safe(malformed.reason()));
|
||||
return List.of(new VehicleEvent.Passthrough(
|
||||
UUID.randomUUID().toString(),
|
||||
identity.identity().vin(),
|
||||
ProtocolId.JT1078,
|
||||
eventTime,
|
||||
Instant.now(),
|
||||
null,
|
||||
Map.copyOf(metadata),
|
||||
Jt1078MediaMessageId.MALFORMED_RTP,
|
||||
malformed.rawBytes() == null ? new byte[0] : malformed.rawBytes()));
|
||||
}
|
||||
|
||||
private IdentityResolution resolveIdentity(Jt1078MalformedRtpPacket malformed) {
|
||||
try {
|
||||
return new IdentityResolution(identityResolver.resolve(new VehicleIdentityLookup(
|
||||
ProtocolId.JT1078, "", safe(malformed.sim()), "", "")), null);
|
||||
} catch (RuntimeException e) {
|
||||
return new IdentityResolution(
|
||||
new VehicleIdentity("unknown", false, VehicleIdentitySource.UNKNOWN),
|
||||
e.getMessage() == null ? "" : e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static String safe(String value) {
|
||||
return value == null ? "" : value;
|
||||
}
|
||||
|
||||
private record IdentityResolution(VehicleIdentity identity, String errorMessage) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.lingniu.ingest.protocol.jt1078.handler;
|
||||
|
||||
import com.lingniu.ingest.api.ProtocolId;
|
||||
import com.lingniu.ingest.api.annotation.EventEmit;
|
||||
import com.lingniu.ingest.api.annotation.MessageMapping;
|
||||
import com.lingniu.ingest.api.annotation.ProtocolHandler;
|
||||
import com.lingniu.ingest.api.event.VehicleEvent;
|
||||
import com.lingniu.ingest.protocol.jt1078.model.Jt1078MessageId;
|
||||
import com.lingniu.ingest.protocol.jt808.mapper.Jt808EventMapper;
|
||||
import com.lingniu.ingest.protocol.jt808.model.Jt808Message;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* JT/T 1078 信令复用 JT808 包头、连接与 dispatcher,但路由独立放在 1078 模块内。
|
||||
*/
|
||||
@ProtocolHandler(protocol = ProtocolId.JT808)
|
||||
public final class Jt1078MediaSignalHandler {
|
||||
|
||||
private final Jt808EventMapper mapper;
|
||||
|
||||
public Jt1078MediaSignalHandler(Jt808EventMapper mapper) {
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
@MessageMapping(command = {
|
||||
Jt1078MessageId.TERMINAL_MEDIA_ATTRS_REPORT,
|
||||
Jt1078MessageId.TERMINAL_PASSENGER_VOLUME,
|
||||
Jt1078MessageId.TERMINAL_FILE_LIST,
|
||||
Jt1078MessageId.TERMINAL_FILE_UPLOAD_DONE
|
||||
}, desc = "JT/T 1078 音视频信令")
|
||||
@EventEmit({VehicleEvent.MediaMeta.class, VehicleEvent.Passthrough.class})
|
||||
public List<VehicleEvent> onMediaSignal(Jt808Message msg) {
|
||||
return mapper.toEvents(msg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.lingniu.ingest.protocol.jt1078.media;
|
||||
|
||||
import com.lingniu.ingest.api.ProtocolId;
|
||||
import com.lingniu.ingest.api.pipeline.RawFrame;
|
||||
import com.lingniu.ingest.identity.InMemoryVehicleIdentityService;
|
||||
import com.lingniu.ingest.identity.VehicleIdentity;
|
||||
import com.lingniu.ingest.identity.VehicleIdentityLookup;
|
||||
import com.lingniu.ingest.identity.VehicleIdentityResolver;
|
||||
import com.lingniu.ingest.identity.VehicleIdentitySource;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public final class Jt1078MalformedRtpEventPublisher {
|
||||
|
||||
private final VehicleIdentityResolver identityResolver;
|
||||
private final Consumer<RawFrame> dispatcher;
|
||||
|
||||
public Jt1078MalformedRtpEventPublisher(Consumer<RawFrame> dispatcher) {
|
||||
this(new InMemoryVehicleIdentityService(), dispatcher);
|
||||
}
|
||||
|
||||
public Jt1078MalformedRtpEventPublisher(VehicleIdentityResolver identityResolver,
|
||||
Consumer<RawFrame> dispatcher) {
|
||||
this.identityResolver = identityResolver == null ? new InMemoryVehicleIdentityService() : identityResolver;
|
||||
this.dispatcher = dispatcher;
|
||||
}
|
||||
|
||||
public void publish(Jt1078MalformedRtpPacket malformed) {
|
||||
if (malformed == null) {
|
||||
return;
|
||||
}
|
||||
Instant now = malformed.receivedAt() == null ? Instant.now() : malformed.receivedAt();
|
||||
IdentityResolution identity = resolveIdentity(malformed);
|
||||
Map<String, String> metadata = new HashMap<>();
|
||||
metadata.put("transport", safe(malformed.transport()));
|
||||
metadata.put("peer", safe(malformed.peer()));
|
||||
metadata.put("sim", safe(malformed.sim()));
|
||||
metadata.put("identityResolved", Boolean.toString(identity.identity().resolved()));
|
||||
metadata.put("identitySource", identity.identity().source().name());
|
||||
if (identity.errorMessage() != null) {
|
||||
metadata.put("identityError", "true");
|
||||
metadata.put("identityErrorMessage", identity.errorMessage());
|
||||
}
|
||||
metadata.put("malformedRtp", "true");
|
||||
metadata.put("parseError", "true");
|
||||
metadata.put("parseErrorMessage", safe(malformed.reason()));
|
||||
metadata.put("packetLength", Integer.toString(malformed.packetLength()));
|
||||
metadata.put("reason", safe(malformed.reason()));
|
||||
metadata.put("vin", identity.identity().vin());
|
||||
dispatcher.accept(new RawFrame(
|
||||
ProtocolId.JT1078,
|
||||
Jt1078MediaMessageId.MALFORMED_RTP,
|
||||
0,
|
||||
malformed,
|
||||
malformed.rawBytes() == null ? new byte[0] : malformed.rawBytes(),
|
||||
Map.copyOf(metadata),
|
||||
now
|
||||
));
|
||||
}
|
||||
|
||||
private IdentityResolution resolveIdentity(Jt1078MalformedRtpPacket malformed) {
|
||||
try {
|
||||
return new IdentityResolution(identityResolver.resolve(new VehicleIdentityLookup(
|
||||
ProtocolId.JT1078, "", malformed.sim(), "", "")), null);
|
||||
} catch (RuntimeException e) {
|
||||
return new IdentityResolution(
|
||||
new VehicleIdentity("unknown", false, VehicleIdentitySource.UNKNOWN),
|
||||
e.getMessage() == null ? "" : e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static String safe(String value) {
|
||||
return value == null ? "" : value;
|
||||
}
|
||||
|
||||
private record IdentityResolution(VehicleIdentity identity, String errorMessage) {}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.lingniu.ingest.protocol.jt1078.media;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
public record Jt1078MalformedRtpPacket(
|
||||
String transport,
|
||||
String peer,
|
||||
String sim,
|
||||
byte[] rawBytes,
|
||||
int packetLength,
|
||||
String reason,
|
||||
Instant receivedAt
|
||||
) {
|
||||
public Jt1078MalformedRtpPacket(String transport,
|
||||
String peer,
|
||||
byte[] rawBytes,
|
||||
int packetLength,
|
||||
String reason,
|
||||
Instant receivedAt) {
|
||||
this(transport, peer, "", rawBytes, packetLength, reason, receivedAt);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
package com.lingniu.ingest.protocol.jt1078.media;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import com.lingniu.ingest.api.ProtocolId;
|
||||
import com.lingniu.ingest.api.event.VehicleEvent;
|
||||
import com.lingniu.ingest.identity.VehicleIdentity;
|
||||
import com.lingniu.ingest.identity.VehicleIdentityLookup;
|
||||
import com.lingniu.ingest.identity.VehicleIdentityResolver;
|
||||
import com.lingniu.ingest.identity.VehicleIdentitySource;
|
||||
import com.lingniu.ingest.sink.archive.ArchiveStore;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public final class Jt1078MediaArchiveService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(Jt1078MediaArchiveService.class);
|
||||
private static final DateTimeFormatter DAY_FMT = DateTimeFormatter.ofPattern("yyyy/MM/dd").withZone(ZoneId.systemDefault());
|
||||
|
||||
private final ArchiveStore archive;
|
||||
private final VehicleIdentityResolver identityResolver;
|
||||
private final Consumer<VehicleEvent> eventPublisher;
|
||||
private final int segmentSeconds;
|
||||
private final Cache<String, String> publishedSegmentRefs = Caffeine.newBuilder()
|
||||
.expireAfterAccess(Duration.ofHours(2))
|
||||
.maximumSize(200_000)
|
||||
.build();
|
||||
|
||||
public Jt1078MediaArchiveService(ArchiveStore archive,
|
||||
VehicleIdentityResolver identityResolver,
|
||||
Consumer<VehicleEvent> eventPublisher,
|
||||
int segmentSeconds) {
|
||||
this.archive = archive;
|
||||
this.identityResolver = identityResolver;
|
||||
this.eventPublisher = eventPublisher;
|
||||
this.segmentSeconds = Math.max(60, segmentSeconds);
|
||||
}
|
||||
|
||||
public void archive(Jt1078RtpPacket packet, String peer) {
|
||||
if (packet == null || packet.payload() == null || packet.payload().length == 0) {
|
||||
return;
|
||||
}
|
||||
IdentityResolution identity = resolveIdentity(packet);
|
||||
String vin = identity.identity().vin();
|
||||
long segment = segment(packet.timestamp());
|
||||
String key = key(vin, packet, segment);
|
||||
try {
|
||||
String uri = archive.append(key, packet.payload());
|
||||
long segmentSizeBytes = archiveSize(key, packet.payload().length);
|
||||
publishOnce(packet, peer, identity, vin, segment, key, uri, segmentSizeBytes);
|
||||
} catch (Exception e) {
|
||||
log.warn("[jt1078] media archive failed sim={} channel={} sequence={} peer={}",
|
||||
packet.sim(), packet.channelId(), packet.sequence(), peer, e);
|
||||
publishArchiveFailure(packet, peer, identity, vin, segment, key, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void publishArchiveFailure(Jt1078RtpPacket packet,
|
||||
String peer,
|
||||
IdentityResolution identity,
|
||||
String vin,
|
||||
long segment,
|
||||
String archiveKey,
|
||||
Exception error) {
|
||||
Instant now = Instant.now();
|
||||
Map<String, String> metadata = baseMetadata(packet, peer, identity);
|
||||
metadata.put("sequence", Integer.toString(packet.sequence()));
|
||||
metadata.put("segment", Long.toString(segment));
|
||||
metadata.put("archiveKey", archiveKey == null ? "" : archiveKey);
|
||||
metadata.put("archiveError", "true");
|
||||
metadata.put("archiveErrorMessage", error.getMessage() == null ? "" : error.getMessage());
|
||||
eventPublisher.accept(new VehicleEvent.Passthrough(
|
||||
UUID.randomUUID().toString(),
|
||||
vin,
|
||||
ProtocolId.JT1078,
|
||||
now,
|
||||
now,
|
||||
null,
|
||||
Map.copyOf(metadata),
|
||||
packet.dataType(),
|
||||
packet.payload()));
|
||||
}
|
||||
|
||||
private void publishOnce(Jt1078RtpPacket packet,
|
||||
String peer,
|
||||
IdentityResolution identity,
|
||||
String vin,
|
||||
long segment,
|
||||
String archiveKey,
|
||||
String uri,
|
||||
long segmentSizeBytes) {
|
||||
String segmentKey = archiveKey == null || archiveKey.isBlank()
|
||||
? packet.sim() + ':' + packet.channelId() + ':' + packet.dataType() + ':' + segment
|
||||
: archiveKey;
|
||||
String old = publishedSegmentRefs.getIfPresent(segmentKey);
|
||||
if (old != null) {
|
||||
return;
|
||||
}
|
||||
publishedSegmentRefs.put(segmentKey, uri);
|
||||
Instant now = Instant.now();
|
||||
Map<String, String> metadata = baseMetadata(packet, peer, identity);
|
||||
metadata.put("segment", Long.toString(segment));
|
||||
metadata.put("sequence", Integer.toString(packet.sequence()));
|
||||
metadata.put("segmentSizeBytes", Long.toString(segmentSizeBytes));
|
||||
metadata.put("archiveKey", archiveKey == null ? "" : archiveKey);
|
||||
eventPublisher.accept(new VehicleEvent.MediaMeta(
|
||||
UUID.randomUUID().toString(),
|
||||
vin,
|
||||
ProtocolId.JT1078,
|
||||
now,
|
||||
now,
|
||||
null,
|
||||
Map.copyOf(metadata),
|
||||
packet.sim() + "-" + packet.channelId() + "-" + segment,
|
||||
mediaType(packet),
|
||||
segmentSizeBytes,
|
||||
uri));
|
||||
}
|
||||
|
||||
private long archiveSize(String key, int fallbackBytes) {
|
||||
try {
|
||||
return archive.size(key);
|
||||
} catch (Exception e) {
|
||||
return Math.max(0, fallbackBytes);
|
||||
}
|
||||
}
|
||||
|
||||
private IdentityResolution resolveIdentity(Jt1078RtpPacket packet) {
|
||||
try {
|
||||
return new IdentityResolution(identityResolver.resolve(new VehicleIdentityLookup(
|
||||
ProtocolId.JT1078, "", packet.sim(), "", "")), null);
|
||||
} catch (RuntimeException e) {
|
||||
return new IdentityResolution(
|
||||
new VehicleIdentity("unknown", false, VehicleIdentitySource.UNKNOWN),
|
||||
e.getMessage() == null ? "" : e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, String> baseMetadata(Jt1078RtpPacket packet,
|
||||
String peer,
|
||||
IdentityResolution identity) {
|
||||
Map<String, String> metadata = new LinkedHashMap<>();
|
||||
metadata.put("sim", packet.sim());
|
||||
metadata.put("channelId", Integer.toString(packet.channelId()));
|
||||
metadata.put("dataType", Integer.toString(packet.dataType()));
|
||||
metadata.put("packetType", Integer.toString(packet.packetType()));
|
||||
metadata.put("peer", peer == null ? "" : peer);
|
||||
metadata.put("vin", identity.identity().vin());
|
||||
metadata.put("identityResolved", Boolean.toString(identity.identity().resolved()));
|
||||
metadata.put("identitySource", identity.identity().source().name());
|
||||
if (identity.errorMessage() != null) {
|
||||
metadata.put("identityError", "true");
|
||||
metadata.put("identityErrorMessage", identity.errorMessage());
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private long segment(long timestamp) {
|
||||
long seconds = timestamp > 10_000_000_000L ? timestamp / 1000 : timestamp;
|
||||
return seconds - Math.floorMod(seconds, segmentSeconds);
|
||||
}
|
||||
|
||||
private static String key(String vin, Jt1078RtpPacket packet, long segment) {
|
||||
String day = DAY_FMT.format(Instant.ofEpochSecond(Math.max(0, segment)));
|
||||
String safeVin = safe(vin);
|
||||
return "jt1078/" + day + "/" + safeVin + "/ch-" + packet.channelId()
|
||||
+ "/dt-" + packet.dataType() + "/" + segment + ".media";
|
||||
}
|
||||
|
||||
private static String mediaType(Jt1078RtpPacket packet) {
|
||||
return "rtp,dataType=" + packet.dataType()
|
||||
+ ",packetType=" + packet.packetType()
|
||||
+ ",channel=" + packet.channelId();
|
||||
}
|
||||
|
||||
private static String safe(String raw) {
|
||||
if (raw == null || raw.isBlank()) {
|
||||
return "unknown";
|
||||
}
|
||||
return raw.replaceAll("[^A-Za-z0-9_.-]", "_");
|
||||
}
|
||||
|
||||
private record IdentityResolution(VehicleIdentity identity, String errorMessage) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.lingniu.ingest.protocol.jt1078.media;
|
||||
|
||||
public final class Jt1078MediaMessageId {
|
||||
|
||||
/** 内部消息:JT1078 RTP 入口坏包,走 Dispatcher 统一 RawArchive + Passthrough 链路。 */
|
||||
public static final int MALFORMED_RTP = 0x72747065; // "rtpe"
|
||||
|
||||
private Jt1078MediaMessageId() {}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.lingniu.ingest.protocol.jt1078.media;
|
||||
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.SimpleChannelInboundHandler;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
public final class Jt1078MediaStreamHandler extends SimpleChannelInboundHandler<Object> {
|
||||
|
||||
private final Jt1078MediaArchiveService archiveService;
|
||||
private final Jt1078MalformedRtpEventPublisher malformedPublisher;
|
||||
|
||||
public Jt1078MediaStreamHandler(Jt1078MediaArchiveService archiveService,
|
||||
Jt1078MalformedRtpEventPublisher malformedPublisher) {
|
||||
this.archiveService = archiveService;
|
||||
this.malformedPublisher = malformedPublisher;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void channelRead0(ChannelHandlerContext ctx, Object msg) {
|
||||
if (msg instanceof Jt1078RtpPacket packet) {
|
||||
archiveService.archive(packet, addr(ctx));
|
||||
} else if (msg instanceof Jt1078MalformedRtpPacket malformed) {
|
||||
malformedPublisher.publish(malformed);
|
||||
} else {
|
||||
ctx.fireChannelRead(msg);
|
||||
}
|
||||
}
|
||||
|
||||
private static String addr(ChannelHandlerContext ctx) {
|
||||
if (ctx.channel().remoteAddress() instanceof InetSocketAddress i) {
|
||||
return i.getAddress().getHostAddress() + ":" + i.getPort();
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.lingniu.ingest.protocol.jt1078.media;
|
||||
|
||||
import io.netty.bootstrap.ServerBootstrap;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import io.netty.channel.ChannelOption;
|
||||
import io.netty.channel.EventLoopGroup;
|
||||
import io.netty.channel.nio.NioEventLoopGroup;
|
||||
import io.netty.channel.socket.SocketChannel;
|
||||
import io.netty.channel.socket.nio.NioServerSocketChannel;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
|
||||
public final class Jt1078MediaStreamServer implements InitializingBean, DisposableBean {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(Jt1078MediaStreamServer.class);
|
||||
|
||||
private final int port;
|
||||
private final int workerThreads;
|
||||
private final int maxPacketLength;
|
||||
private final Jt1078MediaArchiveService archiveService;
|
||||
private final Jt1078MalformedRtpEventPublisher malformedPublisher;
|
||||
|
||||
private EventLoopGroup boss;
|
||||
private EventLoopGroup workers;
|
||||
private Channel serverChannel;
|
||||
|
||||
public Jt1078MediaStreamServer(int port,
|
||||
int workerThreads,
|
||||
int maxPacketLength,
|
||||
Jt1078MediaArchiveService archiveService,
|
||||
Jt1078MalformedRtpEventPublisher malformedPublisher) {
|
||||
this.port = port;
|
||||
this.workerThreads = workerThreads;
|
||||
this.maxPacketLength = maxPacketLength;
|
||||
this.archiveService = archiveService;
|
||||
this.malformedPublisher = malformedPublisher;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
ThreadFactory bossTf = r -> new Thread(r, "jt1078-media-boss");
|
||||
ThreadFactory wkTf = r -> new Thread(r, "jt1078-media-worker");
|
||||
this.boss = new NioEventLoopGroup(1, bossTf);
|
||||
this.workers = new NioEventLoopGroup(
|
||||
workerThreads == 0 ? Runtime.getRuntime().availableProcessors() : workerThreads,
|
||||
wkTf);
|
||||
|
||||
ServerBootstrap b = new ServerBootstrap();
|
||||
b.group(boss, workers)
|
||||
.channel(NioServerSocketChannel.class)
|
||||
.option(ChannelOption.SO_BACKLOG, 1024)
|
||||
.childOption(ChannelOption.TCP_NODELAY, true)
|
||||
.childOption(ChannelOption.SO_KEEPALIVE, true)
|
||||
.childHandler(new ChannelInitializer<SocketChannel>() {
|
||||
@Override
|
||||
protected void initChannel(SocketChannel ch) {
|
||||
ch.pipeline()
|
||||
.addLast("rtp", new Jt1078RtpPacketDecoder(maxPacketLength))
|
||||
.addLast("archive", new Jt1078MediaStreamHandler(archiveService, malformedPublisher));
|
||||
}
|
||||
});
|
||||
this.serverChannel = b.bind(port).sync().channel();
|
||||
log.info("[jt1078] media stream server listening on :{}", port);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
try {
|
||||
if (serverChannel != null) serverChannel.close().sync();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
} finally {
|
||||
if (boss != null) boss.shutdownGracefully();
|
||||
if (workers != null) workers.shutdownGracefully();
|
||||
log.info("[jt1078] media stream server stopped");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.lingniu.ingest.protocol.jt1078.media;
|
||||
|
||||
import io.netty.channel.SimpleChannelInboundHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
|
||||
public final class Jt1078ReceivedMediaStreamHandler extends SimpleChannelInboundHandler<Object> {
|
||||
|
||||
private final Jt1078MediaArchiveService archiveService;
|
||||
private final Jt1078MalformedRtpEventPublisher malformedPublisher;
|
||||
|
||||
public Jt1078ReceivedMediaStreamHandler(Jt1078MediaArchiveService archiveService,
|
||||
Jt1078MalformedRtpEventPublisher malformedPublisher) {
|
||||
this.archiveService = archiveService;
|
||||
this.malformedPublisher = malformedPublisher;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void channelRead0(ChannelHandlerContext ctx, Object msg) {
|
||||
if (msg instanceof Jt1078ReceivedRtpPacket received) {
|
||||
archiveService.archive(received.packet(), received.peer());
|
||||
} else if (msg instanceof Jt1078MalformedRtpPacket malformed) {
|
||||
malformedPublisher.publish(malformed);
|
||||
} else {
|
||||
ctx.fireChannelRead(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.lingniu.ingest.protocol.jt1078.media;
|
||||
|
||||
public record Jt1078ReceivedRtpPacket(
|
||||
Jt1078RtpPacket packet,
|
||||
String peer
|
||||
) {
|
||||
public Jt1078ReceivedRtpPacket {
|
||||
peer = peer == null ? "" : peer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.lingniu.ingest.protocol.jt1078.media;
|
||||
|
||||
public record Jt1078RtpPacket(
|
||||
int sequence,
|
||||
String sim,
|
||||
int channelId,
|
||||
int dataType,
|
||||
int packetType,
|
||||
long timestamp,
|
||||
byte[] payload
|
||||
) {}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.lingniu.ingest.protocol.jt1078.media;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.ByteToMessageDecoder;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* JT/T 1078 TCP RTP 包解码器。
|
||||
*
|
||||
* <p>包头固定魔数 0x30316364,header 固定 28 字节,随后是 bodyLength 指定的媒体负载。
|
||||
*/
|
||||
public final class Jt1078RtpPacketDecoder extends ByteToMessageDecoder {
|
||||
|
||||
private final int maxPacketLength;
|
||||
|
||||
public Jt1078RtpPacketDecoder(int maxPacketLength) {
|
||||
this.maxPacketLength = maxPacketLength;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
|
||||
while (true) {
|
||||
if (in.readableBytes() < Jt1078RtpPacketParser.HEADER_LEN) {
|
||||
return;
|
||||
}
|
||||
int frameStart = findMagic(in);
|
||||
if (frameStart < 0) {
|
||||
in.skipBytes(Math.max(0, in.readableBytes() - 3));
|
||||
return;
|
||||
}
|
||||
if (frameStart > in.readerIndex()) {
|
||||
in.skipBytes(frameStart - in.readerIndex());
|
||||
}
|
||||
if (in.readableBytes() < Jt1078RtpPacketParser.HEADER_LEN) {
|
||||
return;
|
||||
}
|
||||
int length = in.getUnsignedShort(in.readerIndex() + 26);
|
||||
int packetLength = Jt1078RtpPacketParser.HEADER_LEN + length;
|
||||
if (packetLength > maxPacketLength) {
|
||||
String sim = Jt1078RtpPacketParser.peekSim(in);
|
||||
int available = in.readableBytes();
|
||||
int rawLength = Math.min(available, packetLength);
|
||||
byte[] raw = new byte[rawLength];
|
||||
in.readBytes(raw);
|
||||
out.add(new Jt1078MalformedRtpPacket(
|
||||
"tcp",
|
||||
peer(ctx),
|
||||
sim,
|
||||
raw,
|
||||
packetLength,
|
||||
"jt1078 rtp packet too large: " + packetLength,
|
||||
Instant.now()));
|
||||
return;
|
||||
}
|
||||
if (in.readableBytes() < packetLength) {
|
||||
return;
|
||||
}
|
||||
ByteBuf packet = in.readSlice(packetLength);
|
||||
out.add(Jt1078RtpPacketParser.parse(packet));
|
||||
}
|
||||
}
|
||||
|
||||
private static String peer(ChannelHandlerContext ctx) {
|
||||
if (ctx == null || ctx.channel() == null || ctx.channel().remoteAddress() == null) {
|
||||
return "";
|
||||
}
|
||||
return ctx.channel().remoteAddress().toString();
|
||||
}
|
||||
|
||||
private static int findMagic(ByteBuf in) {
|
||||
int start = in.readerIndex();
|
||||
int end = in.writerIndex() - 3;
|
||||
for (int i = start; i < end; i++) {
|
||||
if (in.getInt(i) == Jt1078RtpPacketParser.MAGIC) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.lingniu.ingest.protocol.jt1078.media;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.handler.codec.CorruptedFrameException;
|
||||
|
||||
final class Jt1078RtpPacketParser {
|
||||
|
||||
static final int MAGIC = 0x30316364;
|
||||
static final int HEADER_LEN = 28;
|
||||
|
||||
private Jt1078RtpPacketParser() {}
|
||||
|
||||
static Jt1078RtpPacket parse(ByteBuf in) {
|
||||
int magic = in.readInt();
|
||||
if (magic != MAGIC) {
|
||||
throw new CorruptedFrameException("jt1078 invalid magic: 0x" + Integer.toHexString(magic));
|
||||
}
|
||||
int sequence = in.readUnsignedShort();
|
||||
String sim = readBcd(in, 6);
|
||||
int channelId = in.readUnsignedByte();
|
||||
int dataAndPacketType = in.readUnsignedByte();
|
||||
int dataType = (dataAndPacketType >>> 4) & 0x0F;
|
||||
int packetType = dataAndPacketType & 0x0F;
|
||||
long timestamp = in.readLong();
|
||||
in.skipBytes(4);
|
||||
int bodyLength = in.readUnsignedShort();
|
||||
if (bodyLength > in.readableBytes()) {
|
||||
throw new CorruptedFrameException("jt1078 body length exceeds readable bytes: "
|
||||
+ bodyLength + " > " + in.readableBytes());
|
||||
}
|
||||
byte[] payload = new byte[bodyLength];
|
||||
in.readBytes(payload);
|
||||
return new Jt1078RtpPacket(sequence, sim, channelId, dataType, packetType, timestamp, payload);
|
||||
}
|
||||
|
||||
static String peekSim(ByteBuf in) {
|
||||
if (in == null || in.readableBytes() < HEADER_LEN || in.getInt(in.readerIndex()) != MAGIC) {
|
||||
return "";
|
||||
}
|
||||
StringBuilder sb = new StringBuilder(12);
|
||||
int simOffset = in.readerIndex() + 6;
|
||||
for (int i = 0; i < 6; i++) {
|
||||
int b = in.getUnsignedByte(simOffset + i);
|
||||
sb.append((b >>> 4) & 0x0F);
|
||||
sb.append(b & 0x0F);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static String readBcd(ByteBuf in, int len) {
|
||||
StringBuilder sb = new StringBuilder(len * 2);
|
||||
for (int i = 0; i < len; i++) {
|
||||
int b = in.readUnsignedByte();
|
||||
sb.append((b >>> 4) & 0x0F);
|
||||
sb.append(b & 0x0F);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.lingniu.ingest.protocol.jt1078.media;
|
||||
|
||||
import io.netty.bootstrap.Bootstrap;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import io.netty.channel.ChannelOption;
|
||||
import io.netty.channel.EventLoopGroup;
|
||||
import io.netty.channel.nio.NioEventLoopGroup;
|
||||
import io.netty.channel.socket.DatagramChannel;
|
||||
import io.netty.channel.socket.nio.NioDatagramChannel;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
|
||||
public final class Jt1078UdpMediaStreamServer implements InitializingBean, DisposableBean {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(Jt1078UdpMediaStreamServer.class);
|
||||
|
||||
private final int port;
|
||||
private final int workerThreads;
|
||||
private final int maxPacketLength;
|
||||
private final Jt1078MediaArchiveService archiveService;
|
||||
private final Jt1078MalformedRtpEventPublisher malformedPublisher;
|
||||
|
||||
private EventLoopGroup workers;
|
||||
private Channel serverChannel;
|
||||
|
||||
public Jt1078UdpMediaStreamServer(int port,
|
||||
int workerThreads,
|
||||
int maxPacketLength,
|
||||
Jt1078MediaArchiveService archiveService,
|
||||
Jt1078MalformedRtpEventPublisher malformedPublisher) {
|
||||
this.port = port;
|
||||
this.workerThreads = workerThreads;
|
||||
this.maxPacketLength = maxPacketLength;
|
||||
this.archiveService = archiveService;
|
||||
this.malformedPublisher = malformedPublisher;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
ThreadFactory wkTf = r -> new Thread(r, "jt1078-media-udp-worker");
|
||||
int threads = workerThreads == 0 ? Runtime.getRuntime().availableProcessors() : workerThreads;
|
||||
this.workers = new NioEventLoopGroup(threads, wkTf);
|
||||
|
||||
Bootstrap b = new Bootstrap();
|
||||
b.group(workers)
|
||||
.channel(NioDatagramChannel.class)
|
||||
.option(ChannelOption.SO_BROADCAST, false)
|
||||
.option(ChannelOption.SO_RCVBUF, Math.max(1024 * 1024, maxPacketLength * 2))
|
||||
.handler(new ChannelInitializer<DatagramChannel>() {
|
||||
@Override
|
||||
protected void initChannel(DatagramChannel ch) {
|
||||
ch.pipeline()
|
||||
.addLast("rtp", new Jt1078UdpRtpPacketDecoder(maxPacketLength))
|
||||
.addLast("archive", new Jt1078ReceivedMediaStreamHandler(archiveService, malformedPublisher));
|
||||
}
|
||||
});
|
||||
this.serverChannel = b.bind(port).sync().channel();
|
||||
log.info("[jt1078] udp media stream server listening on :{} workers={} maxPacketLength={}",
|
||||
port, threads, maxPacketLength);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
try {
|
||||
if (serverChannel != null) serverChannel.close().sync();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
} finally {
|
||||
if (workers != null) workers.shutdownGracefully();
|
||||
log.info("[jt1078] udp media stream server stopped");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.lingniu.ingest.protocol.jt1078.media;
|
||||
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.socket.DatagramPacket;
|
||||
import io.netty.handler.codec.CorruptedFrameException;
|
||||
import io.netty.handler.codec.MessageToMessageDecoder;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
public final class Jt1078UdpRtpPacketDecoder extends MessageToMessageDecoder<DatagramPacket> {
|
||||
|
||||
private final int maxPacketLength;
|
||||
|
||||
public Jt1078UdpRtpPacketDecoder(int maxPacketLength) {
|
||||
this.maxPacketLength = maxPacketLength;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void decode(ChannelHandlerContext ctx, DatagramPacket msg, List<Object> out) throws Exception {
|
||||
int packetLength = msg.content().readableBytes();
|
||||
if (packetLength > maxPacketLength) {
|
||||
out.add(malformed(msg, packetLength, "jt1078 udp rtp packet too large: " + packetLength));
|
||||
return;
|
||||
}
|
||||
if (packetLength < Jt1078RtpPacketParser.HEADER_LEN) {
|
||||
out.add(malformed(msg, packetLength, "jt1078 udp rtp packet too short: " + packetLength));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
out.add(new Jt1078ReceivedRtpPacket(
|
||||
Jt1078RtpPacketParser.parse(msg.content().slice()),
|
||||
peer(msg.sender())));
|
||||
} catch (CorruptedFrameException | IndexOutOfBoundsException e) {
|
||||
out.add(malformed(msg, packetLength, e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
private static Jt1078MalformedRtpPacket malformed(DatagramPacket msg, int packetLength, String reason) {
|
||||
byte[] raw = new byte[Math.max(0, msg.content().readableBytes())];
|
||||
msg.content().getBytes(msg.content().readerIndex(), raw);
|
||||
return new Jt1078MalformedRtpPacket(
|
||||
"udp",
|
||||
peer(msg.sender()),
|
||||
Jt1078RtpPacketParser.peekSim(msg.content().slice()),
|
||||
raw,
|
||||
packetLength,
|
||||
reason == null ? "" : reason,
|
||||
Instant.now());
|
||||
}
|
||||
|
||||
private static String peer(InetSocketAddress sender) {
|
||||
if (sender == null) {
|
||||
return "";
|
||||
}
|
||||
return sender.getAddress().getHostAddress() + ":" + sender.getPort();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.lingniu.ingest.protocol.jt1078.model;
|
||||
|
||||
/** JT/T 1078 消息 ID 常量(核心信令子集)。 */
|
||||
public final class Jt1078MessageId {
|
||||
|
||||
private Jt1078MessageId() {}
|
||||
|
||||
public static final int TERMINAL_MEDIA_ATTRS_REPORT = 0x1003;
|
||||
public static final int TERMINAL_PASSENGER_VOLUME = 0x1005;
|
||||
public static final int TERMINAL_FILE_LIST = 0x1205;
|
||||
public static final int TERMINAL_FILE_UPLOAD_DONE = 0x1206;
|
||||
|
||||
public static final int PLATFORM_QUERY_MEDIA_ATTRS = 0x1003;
|
||||
public static final int PLATFORM_REALTIME_PLAY = 0x9101;
|
||||
public static final int PLATFORM_REALTIME_CONTROL = 0x9102;
|
||||
public static final int PLATFORM_HISTORY_PLAY = 0x9201;
|
||||
public static final int PLATFORM_HISTORY_CONTROL = 0x9202;
|
||||
public static final int PLATFORM_RESOURCE_SEARCH = 0x9205;
|
||||
public static final int PLATFORM_FILE_UPLOAD = 0x9206;
|
||||
public static final int PLATFORM_FILE_UPLOAD_CONTROL = 0x9207;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
com.lingniu.ingest.protocol.jt1078.config.Jt1078AutoConfiguration
|
||||
com.lingniu.ingest.protocol.jt1078.config.Jt1078SignalAutoConfiguration
|
||||
Reference in New Issue
Block a user