Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
<module>sip-transaction</module>
<module>sip-dialog</module>
<module>sip-ua</module>
<module>sip-gb28181</module>
<module>sip-compliance-tests</module>
</modules>

Expand Down Expand Up @@ -92,6 +93,11 @@
<artifactId>sip-ua</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.sip</groupId>
<artifactId>sip-gb28181</artifactId>
<version>${project.version}</version>
</dependency>

<!-- runtime: ONLY slf4j is allowed in protocol core -->
<dependency>
Expand Down
64 changes: 64 additions & 0 deletions sip-gb28181/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>com.sip</groupId>
<artifactId>sip-stack-parent</artifactId>
<version>0.1.0-SNAPSHOT</version>
</parent>

<artifactId>sip-gb28181</artifactId>
<name>SIP :: GB28181 Application Layer</name>
<description>
GB/T 28181-2016 video surveillance application-layer messages
(MANSCDP XML over MESSAGE / NOTIFY) and supporting types.
Builds on sip-message + sip-codec; consumes sip-ua for register flow.
</description>

<dependencies>
<dependency>
<groupId>com.sip</groupId>
<artifactId>sip-message</artifactId>
</dependency>
<dependency>
<groupId>com.sip</groupId>
<artifactId>sip-codec</artifactId>
</dependency>
<dependency>
<groupId>com.sip</groupId>
<artifactId>sip-ua</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<compilerArgs>
<!-- The module name "com.sip.gb28181" reflects the
national-standard identifier (GB/T 28181). JPMS
warns about terminal digits, but that convention
does not apply here. -->
<arg>-Xlint:all,-serial,-processing,-module</arg>
<arg>-Werror</arg>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
</project>
60 changes: 60 additions & 0 deletions sip-gb28181/src/main/java/com/sip/gb28181/GbCommand.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.sip.gb28181;

/**
* GB/T 28181-2016 {@code CmdType} values used in the MANSCDP envelope.
*
* <p>Each enum constant matches the literal token expected on the wire
* (e.g. {@code "Catalog"}, {@code "Keepalive"}). When sent in XML, the
* value is wrapped in a {@code <CmdType>...</CmdType>} element.</p>
*/
public enum GbCommand {

/** Heartbeat — device → platform, periodic alive notification. */
KEEPALIVE("Keepalive"),

/** Catalog query (platform → device) and catalog response (device → platform). */
CATALOG("Catalog"),

/** Device basic info query / response. */
DEVICE_INFO("DeviceInfo"),

/** Device status query / response. */
DEVICE_STATUS("DeviceStatus"),

/** Device control (PTZ, recording, reboot, …). */
DEVICE_CONTROL("DeviceControl"),

/** Device configuration query / response. */
DEVICE_CONFIG("DeviceConfig"),

/** Alarm notification (device → platform). */
ALARM("Alarm"),

/** Mobile-position notification (subscribed location stream). */
MOBILE_POSITION("MobilePosition"),

/** Record history query / response. */
RECORD_INFO("RecordInfo"),

/** Broadcast invite (platform → device, voice broadcast). */
BROADCAST("Broadcast");

private final String wireName;

GbCommand(String wireName) {
this.wireName = wireName;
}

public String wireName() {
return wireName;
}

public static GbCommand of(String wireName) {
for (GbCommand c : values()) {
if (c.wireName.equalsIgnoreCase(wireName)) {
return c;
}
}
throw new IllegalArgumentException("unknown GB28181 CmdType: '" + wireName + "'");
}
}
98 changes: 98 additions & 0 deletions sip-gb28181/src/main/java/com/sip/gb28181/GbMessages.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package com.sip.gb28181;

import com.sip.message.SipMethod;
import com.sip.message.SipRequest;
import com.sip.message.SipVersion;
import com.sip.message.header.Headers;
import com.sip.message.uri.SipUri;
import com.sip.ua.Branch;
import com.sip.ua.SipStackInfo;

import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicInteger;

/**
* Builders for the common GB28181 message bodies and matching SIP
* envelopes.
*
* <p>All MANSCDP bodies are emitted as UTF-8 and carried inside a SIP
* {@code MESSAGE} request with
* {@code Content-Type: Application/MANSCDP+xml}.</p>
*/
public final class GbMessages {

/** MIME media type for MANSCDP XML bodies (GB/T 28181-2016 §9.1). */
public static final String MANSCDP_CONTENT_TYPE = "Application/MANSCDP+xml";

private static final AtomicInteger SN = new AtomicInteger();

private GbMessages() { }

/** Allocate a unique serial number for a query/response correlation pair. */
public static int nextSerialNumber() {
return SN.incrementAndGet();
}

/** Build the XML body for a Keepalive notification. */
public static String keepaliveXml(int sn, String deviceId, String status) {
return GbXmlBuilder.envelope("Notify")
.text("CmdType", GbCommand.KEEPALIVE.wireName())
.text("SN", Integer.toString(sn))
.text("DeviceID", deviceId)
.text("Status", status == null ? "OK" : status)
.build();
}

/** Build the XML body for a Catalog query (platform → device). */
public static String catalogQueryXml(int sn, String deviceId) {
return GbXmlBuilder.envelope("Query")
.text("CmdType", GbCommand.CATALOG.wireName())
.text("SN", Integer.toString(sn))
.text("DeviceID", deviceId)
.build();
}

/** Build the XML body for a DeviceInfo query (platform → device). */
public static String deviceInfoQueryXml(int sn, String deviceId) {
return GbXmlBuilder.envelope("Query")
.text("CmdType", GbCommand.DEVICE_INFO.wireName())
.text("SN", Integer.toString(sn))
.text("DeviceID", deviceId)
.build();
}

/** Build the XML body for a DeviceStatus query. */
public static String deviceStatusQueryXml(int sn, String deviceId) {
return GbXmlBuilder.envelope("Query")
.text("CmdType", GbCommand.DEVICE_STATUS.wireName())
.text("SN", Integer.toString(sn))
.text("DeviceID", deviceId)
.build();
}

/**
* Wrap an arbitrary MANSCDP XML payload in a SIP MESSAGE request
* suitable for sending to {@code targetAor} from {@code localAor}.
*/
public static SipRequest manscdpMessage(SipUri targetAor, SipUri localAor,
SipUri requestUri,
String fromTag, String callId,
int cseqNumber, String xmlBody,
String viaSentBy) {
byte[] body = xmlBody.getBytes(StandardCharsets.UTF_8);
Headers headers = Headers.builder()
.add("Via", "SIP/2.0/UDP " + viaSentBy
+ ";branch=" + Branch.newBranch() + ";rport")
.add("Max-Forwards", "70")
.add("From", "<" + localAor.asWire() + ">;tag=" + fromTag)
.add("To", "<" + targetAor.asWire() + ">")
.add("Call-ID", callId)
.add("CSeq", cseqNumber + " MESSAGE")
.add("Content-Type", MANSCDP_CONTENT_TYPE)
.add("Content-Length", Integer.toString(body.length))
.add("User-Agent", SipStackInfo.userAgent())
.build();
return new SipRequest(SipMethod.MESSAGE, requestUri,
SipVersion.SIP_2_0, headers, body);
}
}
69 changes: 69 additions & 0 deletions sip-gb28181/src/main/java/com/sip/gb28181/GbXmlBuilder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.sip.gb28181;

import java.util.LinkedHashMap;
import java.util.Map;

/**
* Tiny element-and-text XML builder tuned for the patterns GB28181 uses.
*
* <p>GB28181 MANSCDP bodies are flat element trees with no attributes,
* no namespaces, and CDATA-free text. A hand-rolled builder is faster
* and less error-prone than dragging in JAXB just for this.</p>
*
* <pre>{@code
* String xml = GbXmlBuilder.envelope("Query")
* .text("CmdType", "Catalog")
* .text("SN", "1")
* .text("DeviceID", "34020000001320000001")
* .build();
* }</pre>
*
* <p>Output is always declared as UTF-8 and ends with a single newline.</p>
*/
public final class GbXmlBuilder {

private final String rootElement;
private final Map<String, String> children = new LinkedHashMap<>();

private GbXmlBuilder(String rootElement) {
this.rootElement = rootElement;
}

public static GbXmlBuilder envelope(String rootElement) {
return new GbXmlBuilder(rootElement);
}

public GbXmlBuilder text(String element, String value) {
children.put(element, value == null ? "" : value);
return this;
}

public String build() {
StringBuilder sb = new StringBuilder(256);
sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
sb.append('<').append(rootElement).append(">\n");
for (var e : children.entrySet()) {
sb.append(" <").append(e.getKey()).append('>')
.append(escape(e.getValue()))
.append("</").append(e.getKey()).append(">\n");
}
sb.append("</").append(rootElement).append(">\n");
return sb.toString();
}

private static String escape(String s) {
StringBuilder sb = new StringBuilder(s.length());
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
switch (c) {
case '<' -> sb.append("&lt;");
case '>' -> sb.append("&gt;");
case '&' -> sb.append("&amp;");
case '"' -> sb.append("&quot;");
case '\'' -> sb.append("&apos;");
default -> sb.append(c);
}
}
return sb.toString();
}
}
Loading
Loading