| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
446f86682a | Merge branch 'master' of http://git.ht-atia.cn/qinyanlei/inspect-tcpserver | 3 weeks ago |
|
|
640c732b2b | /*SIP协议对接音频协议推流相关的尝试和修改*/ | 3 weeks ago |
| @ -0,0 +1,45 @@ | |||||
| package com.inspect.tcpserver.sip.gb28181; | |||||
| import java.io.ByteArrayOutputStream; | |||||
| import java.nio.ByteBuffer; | |||||
| public class Gb28181PsMuxer { | |||||
| private boolean headerSent = false; | |||||
| public byte[] muxH264(byte[] annexB, long pts90k) { | |||||
| try { | |||||
| ByteArrayOutputStream out = new ByteArrayOutputStream(); | |||||
| if (!headerSent) { | |||||
| out.write(PsHeader.packHeader()); | |||||
| out.write(PsHeader.systemHeader()); | |||||
| out.write(PsHeader.psmHeader()); | |||||
| headerSent = true; | |||||
| } | |||||
| out.write(PesPacket.buildH264Pes(annexB, pts90k)); | |||||
| return out.toByteArray(); | |||||
| } catch (Exception e) { | |||||
| return null; | |||||
| } | |||||
| } | |||||
| public byte[] muxOneFrame(byte[] h264, long pts90k) { | |||||
| try { | |||||
| ByteArrayOutputStream out = new ByteArrayOutputStream(); | |||||
| // ⚠️ 每一帧都必须有 | |||||
| out.write(PsHeader.packHeader()); | |||||
| out.write(PsHeader.systemHeader()); | |||||
| out.write(PsHeader.psmHeader()); | |||||
| out.write(PesPacket.buildH264Pes(h264, pts90k)); | |||||
| return out.toByteArray(); | |||||
| } catch (Exception e) { | |||||
| return null; | |||||
| } | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,71 @@ | |||||
| package com.inspect.tcpserver.sip.gb28181; | |||||
| import org.bytedeco.ffmpeg.avcodec.AVPacket; | |||||
| import org.bytedeco.javacpp.BytePointer; | |||||
| import java.nio.ByteBuffer; | |||||
| public class H264AnnexBExtractor { | |||||
| private static final byte[] START_CODE = {0, 0, 0, 1}; | |||||
| /** | |||||
| * 从 AVPacket 中提取 Annex-B H264 | |||||
| */ | |||||
| public static byte[] extractFromAvPacket(AVPacket pkt) { | |||||
| if (pkt == null || pkt.size() <= 0 || pkt.data() == null) { | |||||
| return null; | |||||
| } | |||||
| BytePointer ptr = pkt.data(); | |||||
| int size = pkt.size(); | |||||
| byte[] raw = new byte[size]; | |||||
| ptr.get(raw); | |||||
| // 已经是 Annex-B | |||||
| if (isAnnexB(raw)) { | |||||
| return raw; | |||||
| } | |||||
| // AVCC → Annex-B | |||||
| return avccToAnnexB(raw); | |||||
| } | |||||
| /** | |||||
| * 判断是否 Annex-B | |||||
| */ | |||||
| private static boolean isAnnexB(byte[] data) { | |||||
| if (data.length < 4) return false; | |||||
| return data[0] == 0 && data[1] == 0 && data[2] == 0 && data[3] == 1; | |||||
| } | |||||
| /** | |||||
| * AVCC(length-prefixed)→ Annex-B(start code) | |||||
| */ | |||||
| private static byte[] avccToAnnexB(byte[] avcc) { | |||||
| ByteBuffer in = ByteBuffer.wrap(avcc); | |||||
| ByteBuffer out = ByteBuffer.allocate(avcc.length + 64); | |||||
| while (in.remaining() >= 4) { | |||||
| int nalLen = | |||||
| ((in.get() & 0xff) << 24) | | |||||
| ((in.get() & 0xff) << 16) | | |||||
| ((in.get() & 0xff) << 8) | | |||||
| (in.get() & 0xff); | |||||
| if (nalLen <= 0 || nalLen > in.remaining()) { | |||||
| break; | |||||
| } | |||||
| out.put(START_CODE); | |||||
| out.put(in.array(), in.position(), nalLen); | |||||
| in.position(in.position() + nalLen); | |||||
| } | |||||
| byte[] annexB = new byte[out.position()]; | |||||
| out.flip(); | |||||
| out.get(annexB); | |||||
| return annexB; | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,46 @@ | |||||
| package com.inspect.tcpserver.sip.gb28181; | |||||
| import java.net.*; | |||||
| import java.util.concurrent.atomic.AtomicBoolean; | |||||
| public class MediaSession { | |||||
| private final String remoteIp; | |||||
| private final int remotePort; | |||||
| private final int ssrc; | |||||
| private DatagramSocket socket; | |||||
| private final AtomicBoolean running = new AtomicBoolean(false); | |||||
| private final RtpSender rtpSender; | |||||
| private final PsMuxer psMuxer; | |||||
| public MediaSession(String ip, int port, int ssrc) throws Exception { | |||||
| this.remoteIp = ip; | |||||
| this.remotePort = port; | |||||
| this.ssrc = ssrc; | |||||
| this.socket = new DatagramSocket(); | |||||
| this.rtpSender = new RtpSender(socket, InetAddress.getByName(ip), port, ssrc); | |||||
| this.psMuxer = new PsMuxer(); | |||||
| } | |||||
| public void start() { | |||||
| running.set(true); | |||||
| } | |||||
| public void stop() { | |||||
| running.set(false); | |||||
| socket.close(); | |||||
| } | |||||
| // RTSP Client 回调此方法 | |||||
| public void onH264Frame(byte[] annexB, long pts90k) throws Exception { | |||||
| if (!running.get()) return; | |||||
| byte[] ps = psMuxer.muxH264(annexB, pts90k); | |||||
| rtpSender.send(ps, true); | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,28 @@ | |||||
| package com.inspect.tcpserver.sip.gb28181; | |||||
| import java.net.DatagramPacket; | |||||
| import java.net.DatagramSocket; | |||||
| import java.net.InetSocketAddress; | |||||
| public class MediaSessionEx { | |||||
| private final RtpSenderEx rtpSenderEx; | |||||
| private final Gb28181PsMuxer psMuxer; | |||||
| public MediaSessionEx(String remoteIp, int remotePort, int ssrc) throws Exception { | |||||
| this.rtpSenderEx = new RtpSenderEx(remoteIp, remotePort, ssrc, 96); | |||||
| this.psMuxer = new Gb28181PsMuxer(); | |||||
| } | |||||
| /** RTSP Client 回调 */ | |||||
| public void onH264Frame(byte[] annexB, long pts90k) { | |||||
| byte[] ps = psMuxer.muxOneFrame(annexB, pts90k); | |||||
| if (ps != null) { | |||||
| rtpSenderEx.sendOne(ps, pts90k); | |||||
| } | |||||
| } | |||||
| public void close() { | |||||
| rtpSenderEx.close(); | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,30 @@ | |||||
| package com.inspect.tcpserver.sip.gb28181; | |||||
| import java.io.ByteArrayOutputStream; | |||||
| public class PesPacket { | |||||
| public static byte[] buildH264Pes(byte[] h264, long pts90k) throws Exception { | |||||
| ByteArrayOutputStream out = new ByteArrayOutputStream(); | |||||
| out.write(new byte[]{ | |||||
| 0x00,0x00,0x01,(byte)0xE0, | |||||
| 0x00,0x00, | |||||
| (byte)0x80,(byte)0x80,0x05 | |||||
| }); | |||||
| writePts(out, pts90k); | |||||
| out.write(h264); | |||||
| return out.toByteArray(); | |||||
| } | |||||
| private static void writePts(ByteArrayOutputStream out, long pts) { | |||||
| long val = pts & 0x1FFFFFFFFL; | |||||
| out.write((byte)(0x21 | ((val >> 29) & 0x0E))); | |||||
| out.write((byte)(val >> 22)); | |||||
| out.write((byte)(0x01 | ((val >> 14) & 0xFE))); | |||||
| out.write((byte)(val >> 7)); | |||||
| out.write((byte)(0x01 | ((val << 1) & 0xFE))); | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,35 @@ | |||||
| package com.inspect.tcpserver.sip.gb28181; | |||||
| public class PsHeader { | |||||
| public static byte[] packHeader() { | |||||
| return new byte[]{ | |||||
| 0x00,0x00,0x01,(byte)0xBA, | |||||
| 0x44,0x00,0x04,0x00, | |||||
| 0x04,0x01,(byte)0x89,(byte)0xC3, | |||||
| (byte)0xF8 | |||||
| }; | |||||
| } | |||||
| public static byte[] systemHeader() { | |||||
| return new byte[]{ | |||||
| 0x00,0x00,0x01,(byte)0xBB, | |||||
| 0x00,0x0C, | |||||
| (byte)0x80,0x04, | |||||
| (byte)0xE0,0x07, | |||||
| (byte)0xE0,(byte)0xC0, | |||||
| 0x20,(byte)0xBD,(byte)0xE0 | |||||
| }; | |||||
| } | |||||
| public static byte[] psmHeader() { | |||||
| return new byte[]{ | |||||
| 0x00,0x00,0x01,(byte)0xBC, | |||||
| 0x00,0x12, | |||||
| 0x00,0x00, | |||||
| (byte)0xE0,0x00, | |||||
| 0x1B,0x00,0x00, | |||||
| 0x00,0x00 | |||||
| }; | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,21 @@ | |||||
| package com.inspect.tcpserver.sip.gb28181; | |||||
| import java.io.*; | |||||
| public class PsMuxer { | |||||
| private boolean sentHeader = false; | |||||
| public byte[] muxH264(byte[] annexB, long pts90k) throws IOException { | |||||
| ByteArrayOutputStream out = new ByteArrayOutputStream(); | |||||
| if (!sentHeader) { | |||||
| out.write(PsPackHeader.packHeader()); | |||||
| out.write(PsSystemHeader.systemHeader()); | |||||
| out.write(PsProgramStreamMap.psm()); | |||||
| sentHeader = true; | |||||
| } | |||||
| out.write(PsPesPacket.pes(annexB, pts90k)); | |||||
| return out.toByteArray(); | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,10 @@ | |||||
| package com.inspect.tcpserver.sip.gb28181; | |||||
| public class PsPackHeader { | |||||
| public static byte[] packHeader() { | |||||
| return new byte[]{ | |||||
| 0x00,0x00,0x01,(byte)0xBA, | |||||
| 0x44,0x00,0x04,0x00,0x04,0x01,0x00,0x01 | |||||
| }; | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,28 @@ | |||||
| package com.inspect.tcpserver.sip.gb28181; | |||||
| import java.io.*; | |||||
| public class PsPesPacket { | |||||
| public static byte[] pes(byte[] h264, long pts) throws IOException { | |||||
| ByteArrayOutputStream out = new ByteArrayOutputStream(); | |||||
| out.write(new byte[]{0x00,0x00,0x01,(byte)0xE0}); | |||||
| int len = h264.length + 13; | |||||
| out.write((len >> 8) & 0xFF); | |||||
| out.write(len & 0xFF); | |||||
| out.write(new byte[]{(byte)0x80,(byte)0x80,0x05}); | |||||
| writePts(out, pts); | |||||
| out.write(h264); | |||||
| return out.toByteArray(); | |||||
| } | |||||
| private static void writePts(ByteArrayOutputStream out, long pts) throws IOException { | |||||
| long val = pts & 0x1FFFFFFFFL; | |||||
| out.write((byte)(0x21 | (((val >> 30) & 0x07) << 1))); | |||||
| out.write((byte)((val >> 22) & 0xFF)); | |||||
| out.write((byte)((((val >> 15) & 0x7F) << 1) | 1)); | |||||
| out.write((byte)((val >> 7) & 0xFF)); | |||||
| out.write((byte)(((val & 0x7F) << 1) | 1)); | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,12 @@ | |||||
| package com.inspect.tcpserver.sip.gb28181; | |||||
| public class PsProgramStreamMap { | |||||
| public static byte[] psm() { | |||||
| return new byte[]{ | |||||
| 0x00,0x00,0x01,(byte)0xBC, | |||||
| 0x00,0x12, | |||||
| (byte)0xE0,0x00,0x00,0x00, | |||||
| 0x01,(byte)0xE0,0x00,0x00 | |||||
| }; | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,12 @@ | |||||
| package com.inspect.tcpserver.sip.gb28181; | |||||
| public class PsSystemHeader { | |||||
| public static byte[] systemHeader() { | |||||
| return new byte[]{ | |||||
| 0x00,0x00,0x01,(byte)0xBB, | |||||
| 0x00,0x0C, | |||||
| (byte)0x80,(byte)0x04,(byte)0xE4,(byte)0x7F, | |||||
| (byte)0xE0,0x07,(byte)0xE1,0x07 | |||||
| }; | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,60 @@ | |||||
| package com.inspect.tcpserver.sip.gb28181; | |||||
| import java.net.*; | |||||
| import java.util.concurrent.atomic.AtomicInteger; | |||||
| public class RtpSender { | |||||
| private static final int RTP_HEADER_SIZE = 12; | |||||
| private static final int MTU = 1400; | |||||
| private final DatagramSocket socket; | |||||
| private final InetAddress remote; | |||||
| private final int port; | |||||
| private final int ssrc; | |||||
| private final AtomicInteger seq = new AtomicInteger(0); | |||||
| private int timestamp = 0; | |||||
| public RtpSender(DatagramSocket socket, InetAddress remote, int port, int ssrc) { | |||||
| this.socket = socket; | |||||
| this.remote = remote; | |||||
| this.port = port; | |||||
| this.ssrc = ssrc; | |||||
| } | |||||
| public void send(byte[] ps, boolean marker) throws Exception { | |||||
| int offset = 0; | |||||
| while (offset < ps.length) { | |||||
| int size = Math.min(MTU, ps.length - offset); | |||||
| byte[] pkt = new byte[RTP_HEADER_SIZE + size]; | |||||
| pkt[0] = (byte) 0x80; | |||||
| pkt[1] = (byte) (96 | (marker ? 0x80 : 0x00)); | |||||
| pkt[2] = (byte) (seq.get() >> 8); | |||||
| pkt[3] = (byte) (seq.getAndIncrement()); | |||||
| timestamp += 3600; | |||||
| pkt[4] = (byte) (timestamp >> 24); | |||||
| pkt[5] = (byte) (timestamp >> 16); | |||||
| pkt[6] = (byte) (timestamp >> 8); | |||||
| pkt[7] = (byte) (timestamp); | |||||
| pkt[8] = (byte) (ssrc >> 24); | |||||
| pkt[9] = (byte) (ssrc >> 16); | |||||
| pkt[10] = (byte) (ssrc >> 8); | |||||
| pkt[11] = (byte) (ssrc); | |||||
| System.arraycopy(ps, offset, pkt, RTP_HEADER_SIZE, size); | |||||
| socket.send(new DatagramPacket(pkt, pkt.length, remote, port)); | |||||
| offset += size; | |||||
| } | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,84 @@ | |||||
| package com.inspect.tcpserver.sip.gb28181; | |||||
| import java.net.*; | |||||
| import java.util.concurrent.atomic.AtomicInteger; | |||||
| public class RtpSenderEx { | |||||
| private final DatagramSocket socket; | |||||
| private final InetSocketAddress target; | |||||
| private final int ssrc; | |||||
| private final int payloadType; | |||||
| private final AtomicInteger seq = new AtomicInteger(0); | |||||
| public RtpSenderEx(String ip, int port, int ssrc, int pt) throws Exception { | |||||
| this.socket = new DatagramSocket(); | |||||
| this.target = new InetSocketAddress(ip, port); | |||||
| this.ssrc = ssrc; | |||||
| this.payloadType = pt; | |||||
| } | |||||
| // public void send(byte[] ps, long ts90k) { | |||||
| // int offset = 0; | |||||
| // while (offset < ps.length) { | |||||
| // int len = Math.min(1400, ps.length - offset); | |||||
| // byte[] rtp = buildRtp(ps, offset, len, ts90k); | |||||
| // socketSend(rtp); | |||||
| // offset += len; | |||||
| // } | |||||
| // } | |||||
| // | |||||
| // private byte[] buildRtp(byte[] ps, int off, int len, long ts) { | |||||
| // byte[] pkt = new byte[12 + len]; | |||||
| // pkt[0] = (byte) 0x80; | |||||
| // pkt[1] = (byte) payloadType; | |||||
| // pkt[2] = (byte) (seq.getAndIncrement() >> 8); | |||||
| // pkt[3] = (byte) seq.get(); | |||||
| // pkt[4] = (byte) (ts >> 24); | |||||
| // pkt[5] = (byte) (ts >> 16); | |||||
| // pkt[6] = (byte) (ts >> 8); | |||||
| // pkt[7] = (byte) ts; | |||||
| // pkt[8] = (byte) (ssrc >> 24); | |||||
| // pkt[9] = (byte) (ssrc >> 16); | |||||
| // pkt[10] = (byte) (ssrc >> 8); | |||||
| // pkt[11] = (byte) ssrc; | |||||
| // | |||||
| // System.arraycopy(ps, off, pkt, 12, len); | |||||
| // return pkt; | |||||
| // } | |||||
| public void sendOne(byte[] ps, long ts90k) { | |||||
| byte[] rtp = buildRtp(ps, ts90k, true); | |||||
| socketSend(rtp); | |||||
| } | |||||
| private byte[] buildRtp(byte[] payload, long ts, boolean marker) { | |||||
| byte[] pkt = new byte[12 + payload.length]; | |||||
| pkt[0] = (byte) 0x80; | |||||
| pkt[1] = (byte) (payloadType | (marker ? 0x80 : 0)); | |||||
| pkt[2] = (byte) (seq.get() >> 8); | |||||
| pkt[3] = (byte) seq.getAndIncrement(); | |||||
| pkt[4] = (byte) (ts >> 24); | |||||
| pkt[5] = (byte) (ts >> 16); | |||||
| pkt[6] = (byte) (ts >> 8); | |||||
| pkt[7] = (byte) ts; | |||||
| pkt[8] = (byte) (ssrc >> 24); | |||||
| pkt[9] = (byte) (ssrc >> 16); | |||||
| pkt[10] = (byte) (ssrc >> 8); | |||||
| pkt[11] = (byte) ssrc; | |||||
| System.arraycopy(payload, 0, pkt, 12, payload.length); | |||||
| return pkt; | |||||
| } | |||||
| private void socketSend(byte[] pkt) { | |||||
| try { | |||||
| socket.send(new DatagramPacket(pkt, pkt.length, target)); | |||||
| } catch (Exception ignored) {} | |||||
| } | |||||
| public void close() { | |||||
| socket.close(); | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,74 @@ | |||||
| package com.inspect.tcpserver.sip.gb28181; | |||||
| import org.bytedeco.ffmpeg.avcodec.AVPacket; | |||||
| import org.bytedeco.javacv.FFmpegFrameGrabber; | |||||
| import java.util.concurrent.atomic.AtomicBoolean; | |||||
| import java.util.function.BiConsumer; | |||||
| public class RtspClient { | |||||
| private final String url; | |||||
| private FFmpegFrameGrabber grabber; | |||||
| private final AtomicBoolean running = new AtomicBoolean(false); | |||||
| public RtspClient(String url) { | |||||
| this.url = url; | |||||
| } | |||||
| /** | |||||
| * @param onH264 callback(h264AnnexB, pts90k) | |||||
| */ | |||||
| public void start(BiConsumer<byte[], Long> onH264) throws Exception { | |||||
| grabber = new FFmpegFrameGrabber(url); | |||||
| grabber.setOption("rtsp_transport", "tcp"); | |||||
| grabber.setOption("stimeout", "5000000"); | |||||
| grabber.start(); | |||||
| running.set(true); | |||||
| new Thread(() -> { | |||||
| try { | |||||
| while (running.get()) { | |||||
| AVPacket pkt = grabber.grabPacket(); | |||||
| if (pkt == null || pkt.size() <= 0) { | |||||
| continue; | |||||
| } | |||||
| // 1️⃣ 提取 Annex-B H264 | |||||
| byte[] annexB = H264AnnexBExtractor.extractFromAvPacket(pkt); | |||||
| if (annexB == null) { | |||||
| continue; | |||||
| } | |||||
| // 2️⃣ PTS → 90kHz | |||||
| long pts90k = ptsTo90k(pkt.pts(), grabber.getTimestamp()); | |||||
| onH264.accept(annexB, pts90k); | |||||
| } | |||||
| } catch (Exception e) { | |||||
| e.printStackTrace(); | |||||
| } finally { | |||||
| stop(); | |||||
| } | |||||
| }, "RtspClient-Thread").start(); | |||||
| } | |||||
| private long ptsTo90k(long pts, double timeBase) { | |||||
| if (pts <= 0 || timeBase <= 0) { | |||||
| return System.nanoTime() / 11_111; // fallback | |||||
| } | |||||
| return (long) (pts * timeBase * 90_000); | |||||
| } | |||||
| public void stop() { | |||||
| running.set(false); | |||||
| try { | |||||
| if (grabber != null) { | |||||
| grabber.stop(); | |||||
| grabber.release(); | |||||
| } | |||||
| } catch (Exception ignored) { | |||||
| } | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,45 @@ | |||||
| package com.inspect.tcpserver.sip.media; | |||||
| import com.inspect.tcpserver.sip.media.rtcp.RtcpSrSender; | |||||
| import com.inspect.tcpserver.sip.media.rtp.RtpPacketizer; | |||||
| import com.inspect.tcpserver.sip.media.rtp.RtpSender; | |||||
| import com.inspect.tcpserver.sip.media.rtp.RtpTcpSender; | |||||
| import com.inspect.tcpserver.sip.media.rtp.RtpUdpSender; | |||||
| import com.inspect.tcpserver.sip.media.rtsp.RtspClient; | |||||
| public class MediaSession { | |||||
| private final String remoteIp; | |||||
| private final int remotePort; | |||||
| private final long ssrc; | |||||
| private final boolean tcp; | |||||
| private final String rtspUrl; | |||||
| public MediaSession(String remoteIp, int remotePort, long ssrc, boolean tcp, String rtspUrl) { | |||||
| this.remoteIp = remoteIp; | |||||
| this.remotePort = remotePort; | |||||
| this.ssrc = ssrc; | |||||
| this.tcp = tcp; | |||||
| this.rtspUrl = rtspUrl; | |||||
| } | |||||
| public void start() throws Exception { | |||||
| RtpSender sender = tcp | |||||
| ? new RtpTcpSender(remoteIp, remotePort) | |||||
| : new RtpUdpSender(remoteIp, remotePort); | |||||
| // 先发 RTCP SR(B.6.1) | |||||
| RtcpSrSender.send(sender.getControlStream(), ssrc); | |||||
| // 启动 RTSP 拉流 → RTP | |||||
| RtspClient rtspClient = new RtspClient(rtspUrl); | |||||
| rtspClient.start(nal -> { | |||||
| try { | |||||
| RtpPacketizer.packetizerAndSend(nal, ssrc, sender); | |||||
| } catch (Exception e) { | |||||
| e.printStackTrace(); | |||||
| } | |||||
| }); | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,138 @@ | |||||
| package com.inspect.tcpserver.sip.media; | |||||
| import java.util.HashMap; | |||||
| import java.util.Map; | |||||
| public class SdpInfo { | |||||
| /** 会话级 IP(c= 行) */ | |||||
| private String remoteIp; | |||||
| /** 媒体端口(m=video) */ | |||||
| private int mediaPort; | |||||
| /** RTP / TCP / UDP */ | |||||
| private Transport transport; | |||||
| /** payload type,如 98 */ | |||||
| private int payloadType; | |||||
| /** 编码,如 H264 */ | |||||
| private String codec; | |||||
| /** 时钟频率,如 90000 */ | |||||
| private int clockRate; | |||||
| /** SDP setup 属性(RFC4145) */ | |||||
| private String setup; // active / passive | |||||
| /** SDP connection 属性 */ | |||||
| private String connection; // new | |||||
| /** y= 行里的 SSRC */ | |||||
| private long ssrc; | |||||
| /** 原始属性缓存(方便扩展) */ | |||||
| private final Map<String, String> attributes = new HashMap<>(); | |||||
| public enum Transport { | |||||
| RTP_UDP, | |||||
| RTP_TCP | |||||
| } | |||||
| public String getRemoteIp() { | |||||
| return remoteIp; | |||||
| } | |||||
| public void setRemoteIp(String remoteIp) { | |||||
| this.remoteIp = remoteIp; | |||||
| } | |||||
| public int getMediaPort() { | |||||
| return mediaPort; | |||||
| } | |||||
| public void setMediaPort(int mediaPort) { | |||||
| this.mediaPort = mediaPort; | |||||
| } | |||||
| public Transport getTransport() { | |||||
| return transport; | |||||
| } | |||||
| public void setTransport(Transport transport) { | |||||
| this.transport = transport; | |||||
| } | |||||
| public int getPayloadType() { | |||||
| return payloadType; | |||||
| } | |||||
| public void setPayloadType(int payloadType) { | |||||
| this.payloadType = payloadType; | |||||
| } | |||||
| public String getCodec() { | |||||
| return codec; | |||||
| } | |||||
| public void setCodec(String codec) { | |||||
| this.codec = codec; | |||||
| } | |||||
| public int getClockRate() { | |||||
| return clockRate; | |||||
| } | |||||
| public void setClockRate(int clockRate) { | |||||
| this.clockRate = clockRate; | |||||
| } | |||||
| public String getSetup() { | |||||
| return setup; | |||||
| } | |||||
| public void setSetup(String setup) { | |||||
| this.setup = setup; | |||||
| } | |||||
| public String getConnection() { | |||||
| return connection; | |||||
| } | |||||
| public void setConnection(String connection) { | |||||
| this.connection = connection; | |||||
| } | |||||
| public long getSsrc() { | |||||
| return ssrc; | |||||
| } | |||||
| public void setSsrc(long ssrc) { | |||||
| this.ssrc = ssrc; | |||||
| } | |||||
| public void putAttribute(String key, String value) { | |||||
| attributes.put(key, value); | |||||
| } | |||||
| public String getAttribute(String key) { | |||||
| return attributes.get(key); | |||||
| } | |||||
| @Override | |||||
| public String toString() { | |||||
| return "SdpInfo{" + | |||||
| "remoteIp='" + remoteIp + '\'' + | |||||
| ", mediaPort=" + mediaPort + | |||||
| ", transport=" + transport + | |||||
| ", payloadType=" + payloadType + | |||||
| ", codec='" + codec + '\'' + | |||||
| ", clockRate=" + clockRate + | |||||
| ", setup='" + setup + '\'' + | |||||
| ", connection='" + connection + '\'' + | |||||
| ", ssrc=" + ssrc + | |||||
| '}'; | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,92 @@ | |||||
| package com.inspect.tcpserver.sip.media; | |||||
| import java.util.StringTokenizer; | |||||
| public class SdpParser { | |||||
| public static SdpInfo parse(String sdp) { | |||||
| SdpInfo info = new SdpInfo(); | |||||
| String[] lines = sdp.split("\\r?\\n"); | |||||
| for (String line : lines) { | |||||
| line = line.trim(); | |||||
| if (line.isEmpty()) continue; | |||||
| // ---------- 会话级 ---------- | |||||
| if (line.startsWith("c=")) { | |||||
| // c=IN IP4 192.168.1.116 | |||||
| String[] parts = line.split("\\s+"); | |||||
| if (parts.length >= 3) { | |||||
| info.setRemoteIp(parts[2]); | |||||
| } | |||||
| } | |||||
| // ---------- media ---------- | |||||
| else if (line.startsWith("m=video")) { | |||||
| // m=video 50000 TCP/AVP 98 | |||||
| StringTokenizer st = new StringTokenizer(line); | |||||
| st.nextToken(); // m=video | |||||
| info.setMediaPort(Integer.parseInt(st.nextToken())); | |||||
| String proto = st.nextToken(); | |||||
| if (proto.toUpperCase().contains("TCP")) { | |||||
| info.setTransport(SdpInfo.Transport.RTP_TCP); | |||||
| } else { | |||||
| info.setTransport(SdpInfo.Transport.RTP_UDP); | |||||
| } | |||||
| info.setPayloadType(Integer.parseInt(st.nextToken())); | |||||
| } | |||||
| // ---------- rtpmap ---------- | |||||
| else if (line.startsWith("a=rtpmap")) { | |||||
| // a=rtpmap:98 H264/90000 | |||||
| String value = line.substring("a=rtpmap:".length()); | |||||
| String[] parts = value.split("\\s+"); | |||||
| if (parts.length == 2) { | |||||
| String[] codecInfo = parts[1].split("/"); | |||||
| info.setCodec(codecInfo[0]); | |||||
| info.setClockRate(Integer.parseInt(codecInfo[1])); | |||||
| } | |||||
| } | |||||
| // ---------- setup ---------- | |||||
| else if (line.startsWith("a=setup:")) { | |||||
| info.setSetup(line.substring("a=setup:".length())); | |||||
| } | |||||
| // ---------- connection ---------- | |||||
| else if (line.startsWith("a=connection:")) { | |||||
| info.setConnection(line.substring("a=connection:".length())); | |||||
| } | |||||
| // ---------- fmtp ---------- | |||||
| else if (line.startsWith("a=fmtp:")) { | |||||
| // 可留作扩展 | |||||
| info.putAttribute("fmtp", line); | |||||
| } | |||||
| // ---------- y= (SSRC) ---------- | |||||
| else if (line.startsWith("y=")) { | |||||
| // y=0700000038 | |||||
| info.setSsrc(Long.parseLong(line.substring(2))); | |||||
| } | |||||
| // ---------- 其他属性 ---------- | |||||
| else if (line.startsWith("a=")) { | |||||
| int idx = line.indexOf(':'); | |||||
| if (idx > 0) { | |||||
| info.putAttribute( | |||||
| line.substring(2, idx), | |||||
| line.substring(idx + 1) | |||||
| ); | |||||
| } | |||||
| } | |||||
| } | |||||
| return info; | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,24 @@ | |||||
| package com.inspect.tcpserver.sip.media.rtcp; | |||||
| import java.nio.ByteBuffer; | |||||
| public class RtcpPacket { | |||||
| public static byte[] buildSr(long ssrc) { | |||||
| ByteBuffer buf = ByteBuffer.allocate(28); | |||||
| buf.put((byte) 0x80); | |||||
| buf.put((byte) 200); | |||||
| buf.putShort((short) 6); | |||||
| buf.putInt((int) ssrc); | |||||
| long ntp = System.currentTimeMillis() * 2208988800L; | |||||
| buf.putLong(ntp); | |||||
| buf.putInt(0); | |||||
| buf.putInt(0); | |||||
| buf.putInt(0); | |||||
| return buf.array(); | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,13 @@ | |||||
| package com.inspect.tcpserver.sip.media.rtcp; | |||||
| import java.io.OutputStream; | |||||
| public class RtcpSrSender { | |||||
| public static void send(OutputStream out, long ssrc) throws Exception { | |||||
| byte[] sr = RtcpPacket.buildSr(ssrc); | |||||
| out.write(sr); | |||||
| out.flush(); | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,37 @@ | |||||
| package com.inspect.tcpserver.sip.media.rtp; | |||||
| public class RtpPacket { | |||||
| public static byte[] build( | |||||
| int payloadType, | |||||
| int seq, | |||||
| int timestamp, | |||||
| long ssrc, | |||||
| byte[] payload, | |||||
| boolean marker | |||||
| ) { | |||||
| int headerSize = 12; | |||||
| byte[] packet = new byte[headerSize + payload.length]; | |||||
| packet[0] = (byte) 0x80; | |||||
| packet[1] = (byte) (payloadType & 0x7F); | |||||
| if (marker) { | |||||
| packet[1] |= 0x80; | |||||
| } | |||||
| packet[2] = (byte) (seq >> 8); | |||||
| packet[3] = (byte) seq; | |||||
| packet[4] = (byte) (timestamp >> 24); | |||||
| packet[5] = (byte) (timestamp >> 16); | |||||
| packet[6] = (byte) (timestamp >> 8); | |||||
| packet[7] = (byte) timestamp; | |||||
| packet[8] = (byte) (ssrc >> 24); | |||||
| packet[9] = (byte) (ssrc >> 16); | |||||
| packet[10] = (byte) (ssrc >> 8); | |||||
| packet[11] = (byte) ssrc; | |||||
| System.arraycopy(payload, 0, packet, headerSize, payload.length); | |||||
| return packet; | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,48 @@ | |||||
| package com.inspect.tcpserver.sip.media.rtp; | |||||
| public class RtpPacketizer { | |||||
| private static final int MTU = 1400; | |||||
| private static int seq = 0; | |||||
| private static int timestamp = 0; | |||||
| public static void packetizerAndSend(byte[] nal, long ssrc, RtpSender sender) throws Exception { | |||||
| if (nal.length <= MTU) { | |||||
| byte[] pkt = RtpPacket.build( | |||||
| 98, seq++, timestamp, ssrc, nal, true | |||||
| ); | |||||
| sender.send(pkt, pkt.length); | |||||
| } else { | |||||
| // FU-A | |||||
| int nalHeader = nal[0] & 0xFF; | |||||
| int fuIndicator = (nalHeader & 0xE0) | 28; | |||||
| int fuHeaderStart = 0x80 | (nalHeader & 0x1F); | |||||
| int fuHeaderMid = nalHeader & 0x1F; | |||||
| int fuHeaderEnd = 0x40 | (nalHeader & 0x1F); | |||||
| int offset = 1; | |||||
| boolean start = true; | |||||
| while (offset < nal.length) { | |||||
| int size = Math.min(MTU - 2, nal.length - offset); | |||||
| byte[] payload = new byte[size + 2]; | |||||
| payload[0] = (byte) fuIndicator; | |||||
| payload[1] = (byte) (start ? fuHeaderStart : | |||||
| (offset + size >= nal.length ? fuHeaderEnd : fuHeaderMid)); | |||||
| System.arraycopy(nal, offset, payload, 2, size); | |||||
| offset += size; | |||||
| start = false; | |||||
| byte[] pkt = RtpPacket.build( | |||||
| 98, seq++, timestamp, ssrc, payload, offset >= nal.length | |||||
| ); | |||||
| sender.send(pkt, pkt.length); | |||||
| } | |||||
| } | |||||
| timestamp += 3600; | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,11 @@ | |||||
| package com.inspect.tcpserver.sip.media.rtp; | |||||
| import java.io.OutputStream; | |||||
| public interface RtpSender { | |||||
| void send(byte[] data, int len) throws Exception; | |||||
| OutputStream getControlStream(); | |||||
| } | |||||
| @ -0,0 +1,31 @@ | |||||
| package com.inspect.tcpserver.sip.media.rtp; | |||||
| import java.io.OutputStream; | |||||
| import java.net.Socket; | |||||
| import java.nio.ByteBuffer; | |||||
| public class RtpTcpSender implements RtpSender { | |||||
| private final Socket socket; | |||||
| private final OutputStream out; | |||||
| public RtpTcpSender(String ip, int port) throws Exception { | |||||
| this.socket = new Socket(ip, port); | |||||
| this.out = socket.getOutputStream(); | |||||
| } | |||||
| @Override | |||||
| public void send(byte[] data, int len) throws Exception { | |||||
| ByteBuffer buf = ByteBuffer.allocate(len + 2); | |||||
| buf.putShort((short) len); | |||||
| buf.put(data, 0, len); | |||||
| out.write(buf.array()); | |||||
| out.flush(); | |||||
| } | |||||
| @Override | |||||
| public OutputStream getControlStream() { | |||||
| return out; | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,32 @@ | |||||
| package com.inspect.tcpserver.sip.media.rtp; | |||||
| import java.io.ByteArrayOutputStream; | |||||
| import java.io.OutputStream; | |||||
| import java.net.DatagramPacket; | |||||
| import java.net.DatagramSocket; | |||||
| import java.net.InetAddress; | |||||
| public class RtpUdpSender implements RtpSender { | |||||
| private final DatagramSocket socket; | |||||
| private final InetAddress addr; | |||||
| private final int port; | |||||
| public RtpUdpSender(String ip, int port) throws Exception { | |||||
| this.socket = new DatagramSocket(); | |||||
| this.addr = InetAddress.getByName(ip); | |||||
| this.port = port; | |||||
| } | |||||
| @Override | |||||
| public void send(byte[] data, int len) throws Exception { | |||||
| DatagramPacket packet = new DatagramPacket(data, len, addr, port); | |||||
| socket.send(packet); | |||||
| } | |||||
| @Override | |||||
| public OutputStream getControlStream() { | |||||
| return new ByteArrayOutputStream(); // UDP 场景可忽略 | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,21 @@ | |||||
| package com.inspect.tcpserver.sip.media.rtsp; | |||||
| import org.bytedeco.javacv.Frame; | |||||
| import java.nio.ByteBuffer; | |||||
| public class H264AnnexBExtractor { | |||||
| public static byte[] extract(Frame frame) { | |||||
| if (frame.image == null || frame.image.length == 0) { | |||||
| return null; | |||||
| } | |||||
| ByteBuffer buffer = (ByteBuffer) frame.image[0]; | |||||
| byte[] data = new byte[buffer.remaining()]; | |||||
| buffer.get(data); | |||||
| return data; | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,33 @@ | |||||
| package com.inspect.tcpserver.sip.media.rtsp; | |||||
| import org.bytedeco.javacv.FFmpegFrameGrabber; | |||||
| import org.bytedeco.javacv.Frame; | |||||
| import java.util.function.Consumer; | |||||
| public class RtspClient { | |||||
| private final String url; | |||||
| public RtspClient(String url) { | |||||
| this.url = url; | |||||
| } | |||||
| public void start(Consumer<byte[]> onNal) throws Exception { | |||||
| FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(url); | |||||
| grabber.setOption("rtsp_transport", "tcp"); | |||||
| grabber.start(); | |||||
| while (true) { | |||||
| Frame frame = grabber.grabImage(); | |||||
| if (frame == null || frame.image == null) { | |||||
| continue; | |||||
| } | |||||
| byte[] annexB = H264AnnexBExtractor.extract(frame); | |||||
| if (annexB != null) { | |||||
| onNal.accept(annexB); | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,35 @@ | |||||
| package com.inspect.tcpserver.sip.stream; | |||||
| import org.bytedeco.ffmpeg.avcodec.AVPacket; | |||||
| import org.bytedeco.javacpp.BytePointer; | |||||
| public class H264AnnexBExtractor { | |||||
| private static final byte[] START_CODE = {0x00, 0x00, 0x00, 0x01}; | |||||
| public static byte[] extractFromAvPacket(AVPacket pkt) { | |||||
| BytePointer dataPtr = pkt.data(); | |||||
| int size = pkt.size(); | |||||
| if (dataPtr == null || size <= 0) { | |||||
| return null; | |||||
| } | |||||
| byte[] raw = new byte[size]; | |||||
| dataPtr.get(raw); | |||||
| // 如果已经是 Annex-B(00 00 00 01) | |||||
| if (raw.length >= 4 && | |||||
| raw[0] == 0x00 && raw[1] == 0x00 && | |||||
| raw[2] == 0x00 && raw[3] == 0x01) { | |||||
| return raw; | |||||
| } | |||||
| // AVCC → Annex-B(简单处理:前置 start code) | |||||
| byte[] annexB = new byte[START_CODE.length + raw.length]; | |||||
| System.arraycopy(START_CODE, 0, annexB, 0, START_CODE.length); | |||||
| System.arraycopy(raw, 0, annexB, START_CODE.length, raw.length); | |||||
| return annexB; | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,35 @@ | |||||
| package com.inspect.tcpserver.sip.stream; | |||||
| import com.inspect.tcpserver.sip.stream.ps.PsMuxer; | |||||
| public class MediaSession { | |||||
| private final PsMuxer psMuxer; | |||||
| private final RtpSender rtpSender; | |||||
| private RtspClient rtspClient; | |||||
| public MediaSession(String remoteIp, int remotePort, int ssrc) throws Exception { | |||||
| this.psMuxer = new PsMuxer(); | |||||
| this.rtpSender = new RtpSender(remoteIp, remotePort, ssrc); | |||||
| } | |||||
| public void startRtsp(String rtspUrl) throws Exception { | |||||
| rtspClient = new RtspClient(rtspUrl); | |||||
| rtspClient.start(h264 -> { | |||||
| try { | |||||
| byte[] ps = psMuxer.mux(h264); | |||||
| rtpSender.sendPs(ps); | |||||
| } catch (Exception e) { | |||||
| e.printStackTrace(); | |||||
| } | |||||
| }); | |||||
| } | |||||
| public void stop() { | |||||
| if (rtspClient != null) { | |||||
| rtspClient.stop(); | |||||
| } | |||||
| rtpSender.close(); | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,15 @@ | |||||
| package com.inspect.tcpserver.sip.stream; | |||||
| public class RtpClock { | |||||
| private static final long CLOCK_90K = 90000; | |||||
| private static final long FRAME_INTERVAL = 3600; // 40ms | |||||
| private long timestamp = 0; | |||||
| public long next() { | |||||
| timestamp += FRAME_INTERVAL; | |||||
| return timestamp; | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,39 @@ | |||||
| package com.inspect.tcpserver.sip.stream; | |||||
| public class RtpPacket { | |||||
| public static final int HEADER_SIZE = 12; | |||||
| public byte[] buffer; | |||||
| public int length; | |||||
| public RtpPacket(int payloadSize) { | |||||
| buffer = new byte[HEADER_SIZE + payloadSize]; | |||||
| } | |||||
| public void setHeader( | |||||
| int payloadType, | |||||
| int seq, | |||||
| long timestamp, | |||||
| int ssrc, | |||||
| boolean marker | |||||
| ) { | |||||
| buffer[0] = (byte) 0x80; | |||||
| buffer[1] = (byte) (payloadType & 0x7F); | |||||
| if (marker) buffer[1] |= 0x80; | |||||
| buffer[2] = (byte) (seq >> 8); | |||||
| buffer[3] = (byte) (seq); | |||||
| buffer[4] = (byte) (timestamp >> 24); | |||||
| buffer[5] = (byte) (timestamp >> 16); | |||||
| buffer[6] = (byte) (timestamp >> 8); | |||||
| buffer[7] = (byte) (timestamp); | |||||
| buffer[8] = (byte) (ssrc >> 24); | |||||
| buffer[9] = (byte) (ssrc >> 16); | |||||
| buffer[10] = (byte) (ssrc >> 8); | |||||
| buffer[11] = (byte) (ssrc); | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,46 @@ | |||||
| package com.inspect.tcpserver.sip.stream; | |||||
| import lombok.extern.slf4j.Slf4j; | |||||
| @Slf4j | |||||
| public class RtpSender { | |||||
| private static final int MTU = 1400; | |||||
| private static final int PAYLOAD_TYPE = 98; | |||||
| private int seq = 0; | |||||
| private final int ssrc; | |||||
| private final RtpSocket socket; | |||||
| private final RtpClock clock = new RtpClock(); | |||||
| public RtpSender(String ip, int port, int ssrc) throws Exception { | |||||
| this.ssrc = ssrc; | |||||
| this.socket = new RtpSocket(ip, port); | |||||
| } | |||||
| public void sendPs(byte[] ps) throws Exception { | |||||
| long ts = clock.next(); | |||||
| int offset = 0; | |||||
| while (offset < ps.length) { | |||||
| int size = Math.min(MTU, ps.length - offset); | |||||
| boolean marker = (offset + size) >= ps.length; | |||||
| RtpPacket pkt = new RtpPacket(size); | |||||
| pkt.setHeader(PAYLOAD_TYPE, seq++, ts, ssrc, marker); | |||||
| System.arraycopy(ps, offset, pkt.buffer, RtpPacket.HEADER_SIZE, size); | |||||
| pkt.length = RtpPacket.HEADER_SIZE + size; | |||||
| socket.send(pkt.buffer, pkt.length); | |||||
| offset += size; | |||||
| log.info("sendPs offset = {}", offset); | |||||
| } | |||||
| } | |||||
| public void close() { | |||||
| socket.close(); | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,26 @@ | |||||
| package com.inspect.tcpserver.sip.stream; | |||||
| import java.net.DatagramPacket; | |||||
| import java.net.DatagramSocket; | |||||
| import java.net.InetSocketAddress; | |||||
| public class RtpSocket { | |||||
| private final DatagramSocket socket; | |||||
| private final InetSocketAddress remote; | |||||
| public RtpSocket(String ip, int port) throws Exception { | |||||
| socket = new DatagramSocket(); | |||||
| remote = new InetSocketAddress(ip, port); | |||||
| } | |||||
| public void send(byte[] data, int len) throws Exception { | |||||
| DatagramPacket packet = new DatagramPacket(data, len, remote); | |||||
| socket.send(packet); | |||||
| } | |||||
| public void close() { | |||||
| socket.close(); | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,59 @@ | |||||
| package com.inspect.tcpserver.sip.stream; | |||||
| import org.bytedeco.ffmpeg.avcodec.AVPacket; | |||||
| import org.bytedeco.javacv.FFmpegFrameGrabber; | |||||
| import java.nio.ByteBuffer; | |||||
| import java.util.concurrent.atomic.AtomicBoolean; | |||||
| import java.util.function.Consumer; | |||||
| public class RtspClient { | |||||
| private final String url; | |||||
| private FFmpegFrameGrabber grabber; | |||||
| private final AtomicBoolean running = new AtomicBoolean(false); | |||||
| public RtspClient(String url) { | |||||
| this.url = url; | |||||
| } | |||||
| public void start(Consumer<byte[]> onH264) throws Exception { | |||||
| grabber = new FFmpegFrameGrabber(url); | |||||
| grabber.setOption("rtsp_transport", "tcp"); | |||||
| grabber.setOption("stimeout", "5000000"); | |||||
| grabber.start(); | |||||
| running.set(true); | |||||
| new Thread(() -> { | |||||
| try { | |||||
| while (running.get()) { | |||||
| AVPacket pkt = grabber.grabPacket(); | |||||
| if (pkt == null || pkt.size() <= 0) { | |||||
| continue; | |||||
| } | |||||
| byte[] annexB = H264AnnexBExtractor.extractFromAvPacket(pkt); | |||||
| if (annexB != null) { | |||||
| onH264.accept(annexB); | |||||
| } | |||||
| } | |||||
| } catch (Exception e) { | |||||
| e.printStackTrace(); | |||||
| } finally { | |||||
| stop(); | |||||
| } | |||||
| }, "RtspClient-Thread").start(); | |||||
| } | |||||
| public void stop() { | |||||
| running.set(false); | |||||
| try { | |||||
| if (grabber != null) { | |||||
| grabber.stop(); | |||||
| grabber.release(); | |||||
| } | |||||
| } catch (Exception ignored) { | |||||
| } | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,15 @@ | |||||
| package com.inspect.tcpserver.sip.stream.ps; | |||||
| public final class PsConstants { | |||||
| public static final int PACK_START_CODE = 0x000001BA; | |||||
| public static final int SYSTEM_HEADER_START_CODE = 0x000001BB; | |||||
| public static final int PROGRAM_STREAM_MAP = 0x000001BC; | |||||
| public static final int PES_VIDEO_START_CODE = 0x000001E0; | |||||
| public static final int STREAM_ID_VIDEO = 0xE0; | |||||
| public static final long CLOCK_90K = 90000L; | |||||
| private PsConstants() {} | |||||
| } | |||||
| @ -0,0 +1,28 @@ | |||||
| package com.inspect.tcpserver.sip.stream.ps; | |||||
| import java.io.ByteArrayOutputStream; | |||||
| public class PsMuxer { | |||||
| private boolean headerSent = false; | |||||
| private long scr = 0; | |||||
| public byte[] mux(byte[] h264) throws Exception { | |||||
| ByteArrayOutputStream out = new ByteArrayOutputStream(); | |||||
| scr += 3600; | |||||
| if (!headerSent) { | |||||
| out.write(PsPackHeader.create(scr)); | |||||
| out.write(PsSystemHeader.create()); | |||||
| out.write(PsProgramStreamMap.create()); | |||||
| headerSent = true; | |||||
| } | |||||
| out.write(PsPackHeader.create(scr)); | |||||
| out.write(PsPesPacket.create(h264, scr)); | |||||
| return out.toByteArray(); | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,27 @@ | |||||
| package com.inspect.tcpserver.sip.stream.ps; | |||||
| import java.io.ByteArrayOutputStream; | |||||
| public class PsPackHeader { | |||||
| public static byte[] create(long scr) throws Exception { | |||||
| ByteArrayOutputStream out = new ByteArrayOutputStream(); | |||||
| out.write(new byte[]{0, 0, 1, (byte) 0xBA}); | |||||
| long b = scr & 0x1FFFFFFFFL; | |||||
| out.write((byte) (0x44 | ((b >> 27) & 0x38))); | |||||
| out.write((byte) (b >> 19)); | |||||
| out.write((byte) (0x04 | ((b >> 14) & 0xF8))); | |||||
| out.write((byte) (b >> 6)); | |||||
| out.write((byte) (0x04 | ((b << 2) & 0xF8))); | |||||
| out.write(0x01); | |||||
| out.write(new byte[]{0x00, 0x01, (byte) 0x89, (byte) 0xC3}); | |||||
| out.write(0xF8); | |||||
| return out.toByteArray(); | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,35 @@ | |||||
| package com.inspect.tcpserver.sip.stream.ps; | |||||
| import java.io.ByteArrayOutputStream; | |||||
| public class PsPesPacket { | |||||
| public static byte[] create(byte[] es, long pts) throws Exception { | |||||
| ByteArrayOutputStream out = new ByteArrayOutputStream(); | |||||
| out.write(new byte[]{0, 0, 1, (byte) 0xE0}); | |||||
| int len = es.length + 8; | |||||
| out.write((len >> 8) & 0xFF); | |||||
| out.write(len & 0xFF); | |||||
| out.write(0x80); | |||||
| out.write(0x80); | |||||
| out.write(0x05); | |||||
| writePts(out, pts); | |||||
| out.write(es); | |||||
| return out.toByteArray(); | |||||
| } | |||||
| private static void writePts(ByteArrayOutputStream out, long pts) { | |||||
| long v = pts & 0x1FFFFFFFFL; | |||||
| out.write((byte) (0x21 | ((v >> 29) & 0x0E))); | |||||
| out.write((byte) (v >> 22)); | |||||
| out.write((byte) (0x01 | ((v >> 14) & 0xFE))); | |||||
| out.write((byte) (v >> 7)); | |||||
| out.write((byte) (0x01 | ((v << 1) & 0xFE))); | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,26 @@ | |||||
| package com.inspect.tcpserver.sip.stream.ps; | |||||
| import java.io.ByteArrayOutputStream; | |||||
| public class PsProgramStreamMap { | |||||
| public static byte[] create() throws Exception { | |||||
| ByteArrayOutputStream out = new ByteArrayOutputStream(); | |||||
| out.write(new byte[]{0, 0, 1, (byte) 0xBC}); | |||||
| out.write(new byte[]{0x00, 0x12}); | |||||
| out.write(0xE0); | |||||
| out.write(new byte[]{0x00, 0x00}); | |||||
| out.write(new byte[]{0x00, 0x08}); | |||||
| out.write(0x1B); // H264 | |||||
| out.write(PsConstants.STREAM_ID_VIDEO); | |||||
| out.write(new byte[]{0x00, 0x00}); | |||||
| out.write(new byte[]{0, 0, 0, 0}); // CRC | |||||
| return out.toByteArray(); | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,21 @@ | |||||
| package com.inspect.tcpserver.sip.stream.ps; | |||||
| import java.io.ByteArrayOutputStream; | |||||
| public class PsSystemHeader { | |||||
| public static byte[] create() throws Exception { | |||||
| ByteArrayOutputStream out = new ByteArrayOutputStream(); | |||||
| out.write(new byte[]{0, 0, 1, (byte) 0xBB}); | |||||
| out.write(new byte[]{0x00, 0x0C}); | |||||
| out.write(new byte[]{(byte) 0x80, 0x04, (byte) 0xE4}); | |||||
| out.write(0x04); | |||||
| out.write(0xE1); | |||||
| out.write(new byte[]{0x7F, (byte) 0xE0, 0x7F}); | |||||
| return out.toByteArray(); | |||||
| } | |||||
| } | |||||