├── .classpath
├── .myeclipse
└── profiler
│ └── SpsUtils.xml
├── .project
├── .settings
├── org.eclipse.core.resources.prefs
├── org.eclipse.jdt.core.prefs
└── org.eclipse.m2e.core.prefs
├── README.md
├── README.md.bak
├── dependency-reduced-pom.xml
├── pom.xml
├── readme.docx
├── src
├── main
│ ├── java
│ │ └── com
│ │ │ └── zhuyun
│ │ │ ├── RtspNettyServer.java
│ │ │ ├── handler
│ │ │ ├── HeartBeatServerHandler.java
│ │ │ ├── RtcpHandler.java
│ │ │ ├── RtpHandler.java
│ │ │ └── RtspHandler.java
│ │ │ ├── media
│ │ │ └── MediaSdpInfo.java
│ │ │ ├── rtcp
│ │ │ ├── AudioRRRtcp.java
│ │ │ ├── AudioSRRtcp.java
│ │ │ ├── VideoRRRtcp.java
│ │ │ └── VideoSRRtcp.java
│ │ │ ├── rtp
│ │ │ ├── FileUtils.java
│ │ │ ├── NaluType.java
│ │ │ ├── RtpUtils.java
│ │ │ ├── SpsBitStream.java
│ │ │ ├── SpsInfoStruct.java
│ │ │ └── SpsUtils.java
│ │ │ ├── streamhub
│ │ │ ├── StreamFrame.java
│ │ │ ├── StreamFrameSink.java
│ │ │ └── StreamHub.java
│ │ │ ├── transform
│ │ │ └── RetransmissionRequesterDelegate.java
│ │ │ └── utils
│ │ │ ├── Command.java
│ │ │ ├── HttpConnection.java
│ │ │ ├── HttpConnectionManager.java
│ │ │ ├── ReadFromFile.java
│ │ │ └── SdpParser.java
│ └── resources
│ │ ├── logback.xml
│ │ └── rtsp-server.properties
└── test
│ └── java
│ └── com
│ └── zhuyun
│ ├── FileUtilsTest.java
│ └── HttpTest.java
└── target
├── classes
├── com
│ └── zhuyun
│ │ ├── RtspNettyServer$1.class
│ │ ├── RtspNettyServer$2.class
│ │ ├── RtspNettyServer$3.class
│ │ ├── RtspNettyServer.class
│ │ ├── handler
│ │ ├── HeartBeatServerHandler.class
│ │ ├── RtcpHandler.class
│ │ ├── RtpHandler.class
│ │ ├── RtspHandler$1.class
│ │ ├── RtspHandler$2.class
│ │ ├── RtspHandler$3.class
│ │ ├── RtspHandler$4.class
│ │ ├── RtspHandler$5.class
│ │ └── RtspHandler.class
│ │ ├── media
│ │ └── MediaSdpInfo.class
│ │ ├── rtp
│ │ ├── FileUtils.class
│ │ ├── NaluType.class
│ │ ├── RtpUtils.class
│ │ ├── SpsBitStream.class
│ │ ├── SpsInfoStruct.class
│ │ └── SpsUtils.class
│ │ ├── streamhub
│ │ ├── StreamFrame.class
│ │ ├── StreamFrameSink.class
│ │ └── StreamHub.class
│ │ └── utils
│ │ ├── Command.class
│ │ ├── HttpConnection.class
│ │ ├── HttpConnectionManager.class
│ │ ├── ReadFromFile.class
│ │ └── SdpParser.class
├── logback.xml
└── rtsp-server.properties
├── maven-archiver
└── pom.properties
├── maven-status
└── maven-compiler-plugin
│ ├── compile
│ └── default-compile
│ │ ├── createdFiles.lst
│ │ └── inputFiles.lst
│ └── testCompile
│ └── default-testCompile
│ ├── createdFiles.lst
│ └── inputFiles.lst
├── original-rtsp-netty-server-1.0-SNAPSHOT.jar
├── rtsp-netty-server-1.0-SNAPSHOT.jar
└── test-classes
└── com
└── zhuyun
├── FileUtilsTest.class
└── HttpTest.class
/.classpath:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/.myeclipse/profiler/SpsUtils.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | SpsUtils Launch Configuration
5 |
6 | com.zhuyun.*
7 | 10
8 |
9 | 1000
10 | false
11 | com.zhuyun.**
12 |
13 | false
14 | 0
15 | false
16 | 100
17 | true
18 |
19 | 1000
20 | true
21 | true
22 | false
23 |
24 |
--------------------------------------------------------------------------------
/.project:
--------------------------------------------------------------------------------
1 |
2 |
3 | rtsp-netty-server
4 |
5 |
6 |
7 |
8 |
9 | org.eclipse.m2e.core.maven2Builder
10 |
11 |
12 |
13 |
14 |
15 | org.eclipse.m2e.core.maven2Nature
16 | org.eclipse.jdt.core.javanature
17 | org.eclipse.wst.common.project.facet.core.nature
18 |
19 |
20 |
--------------------------------------------------------------------------------
/.settings/org.eclipse.core.resources.prefs:
--------------------------------------------------------------------------------
1 | eclipse.preferences.version=1
2 | encoding//src/main/java=UTF-8
3 | encoding//src/main/java/com/zhuyun/rtp/NaluType.java=GBK
4 | encoding//src/main/java/com/zhuyun/utils/ReadFromFile.java=GBK
5 | encoding//src/test/java=UTF-8
6 | encoding/=UTF-8
7 |
--------------------------------------------------------------------------------
/.settings/org.eclipse.jdt.core.prefs:
--------------------------------------------------------------------------------
1 | eclipse.preferences.version=1
2 | org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
3 | org.eclipse.jdt.core.compiler.compliance=1.8
4 | org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
5 | org.eclipse.jdt.core.compiler.source=1.8
6 |
--------------------------------------------------------------------------------
/.settings/org.eclipse.m2e.core.prefs:
--------------------------------------------------------------------------------
1 | activeProfiles=
2 | eclipse.preferences.version=1
3 | resolveWorkspaceProjects=true
4 | version=1
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # rtsp-netty-server
2 | 一个使用netty写的rtsp服务器。
3 |
4 | 目前支持H264、H265、 AAC格式的流文件上传与存储。
5 | H264、H265、AAC格式流文件的播放。
6 |
--------------------------------------------------------------------------------
/README.md.bak:
--------------------------------------------------------------------------------
1 | # rtsp-netty-server
2 | 一个使用netty写的rtsp服务器。
3 |
4 | 目前支持H264、 AAC格式的流文件上传与存储。
5 | H264、AAC格式流文件的播放。
6 |
--------------------------------------------------------------------------------
/dependency-reduced-pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 4.0.0
4 | com.zhuyun
5 | rtsp-netty-server
6 | rtsp-netty-server
7 | 1.0-SNAPSHOT
8 | http://www.example.com
9 |
10 |
11 |
12 |
13 | maven-clean-plugin
14 | 3.0.0
15 |
16 |
17 | maven-resources-plugin
18 | 3.0.2
19 |
20 |
21 | maven-compiler-plugin
22 | 3.7.0
23 |
24 |
25 | maven-surefire-plugin
26 | 2.20.1
27 |
28 |
29 | maven-jar-plugin
30 | 3.0.2
31 |
32 |
33 | maven-install-plugin
34 | 2.5.2
35 |
36 |
37 |
38 |
39 |
40 | maven-shade-plugin
41 | 2.4.3
42 |
43 |
44 | package
45 |
46 | shade
47 |
48 |
49 |
50 |
51 |
52 | com.zhuyun.App
53 | 123
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | junit
66 | junit
67 | 4.11
68 | test
69 |
70 |
71 | hamcrest-core
72 | org.hamcrest
73 |
74 |
75 |
76 |
77 |
78 | 1.8
79 | UTF-8
80 | 1.8
81 |
82 |
83 |
84 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 | 4.0.0
6 |
7 | com.zhuyun
8 | rtsp-netty-server
9 | 1.0-SNAPSHOT
10 |
11 | rtsp-netty-server
12 |
13 | http://www.example.com
14 |
15 |
16 | UTF-8
17 | 1.8
18 | 1.8
19 |
20 |
21 |
22 |
23 |
24 | io.netty
25 | netty-all
26 | 4.1.25.Final
27 |
28 |
29 |
30 |
31 | net.sf.json-lib
32 | json-lib
33 | 2.4
34 | jdk15
35 |
36 |
37 |
38 |
39 | junit
40 | junit
41 | 4.11
42 | test
43 |
44 |
45 |
46 |
47 | org.apache.httpcomponents
48 | httpclient
49 | 4.5.6
50 |
51 |
52 |
53 |
54 | com.alibaba
55 | fastjson
56 | 1.2.47
57 |
58 |
59 |
60 |
61 | ch.qos.logback
62 | logback-classic
63 | 1.2.3
64 |
65 |
66 |
67 |
68 | org.bouncycastle
69 | bcprov-jdk15on
70 | 1.54
71 |
72 |
73 |
74 | org.bouncycastle
75 | bcpkix-jdk15on
76 | 1.54
77 |
78 |
79 |
80 | org.jitsi
81 | bccontrib
82 | 1.0
83 |
84 |
85 |
86 | org.jetbrains
87 | annotations
88 | 20.0.0
89 |
90 |
91 |
92 |
93 |
94 |
95 | org.apache.maven.plugins
96 | maven-shade-plugin
97 | 2.4.3
98 |
99 |
100 | package
101 |
102 | shade
103 |
104 |
105 |
106 |
107 |
108 | com.zhuyun.App
109 | 123
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 | maven-clean-plugin
126 | 3.0.0
127 |
128 |
129 |
130 | maven-resources-plugin
131 | 3.0.2
132 |
133 |
134 | maven-compiler-plugin
135 | 3.7.0
136 |
137 |
138 | maven-surefire-plugin
139 | 2.20.1
140 |
141 |
142 | maven-jar-plugin
143 | 3.0.2
144 |
145 |
146 | maven-install-plugin
147 | 2.5.2
148 |
149 |
153 |
154 |
155 |
156 |
157 |
--------------------------------------------------------------------------------
/readme.docx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhouyinfei/rtsp-netty-server/d572bcf78d610b7126590d501c800ca30e602e1d/readme.docx
--------------------------------------------------------------------------------
/src/main/java/com/zhuyun/RtspNettyServer.java:
--------------------------------------------------------------------------------
1 | package com.zhuyun;
2 |
3 | import io.netty.bootstrap.Bootstrap;
4 | import io.netty.bootstrap.ServerBootstrap;
5 | import io.netty.channel.Channel;
6 | import io.netty.channel.ChannelFuture;
7 | import io.netty.channel.ChannelInitializer;
8 | import io.netty.channel.ChannelOption;
9 | import io.netty.channel.EventLoopGroup;
10 | import io.netty.channel.WriteBufferWaterMark;
11 | import io.netty.channel.nio.NioEventLoopGroup;
12 | import io.netty.channel.socket.SocketChannel;
13 | import io.netty.channel.socket.nio.NioDatagramChannel;
14 | import io.netty.channel.socket.nio.NioServerSocketChannel;
15 | import io.netty.handler.codec.http.HttpObjectAggregator;
16 | import io.netty.handler.codec.rtsp.RtspDecoder;
17 | import io.netty.handler.codec.rtsp.RtspEncoder;
18 | import io.netty.handler.timeout.IdleStateHandler;
19 | import io.netty.util.ResourceLeakDetector;
20 |
21 | import java.io.BufferedReader;
22 | import java.io.FileReader;
23 | import java.io.IOException;
24 | import java.util.Properties;
25 | import java.util.concurrent.ExecutorService;
26 | import java.util.concurrent.Executors;
27 | import java.util.concurrent.LinkedBlockingQueue;
28 | import java.util.concurrent.ScheduledExecutorService;
29 | import java.util.concurrent.ThreadPoolExecutor;
30 | import java.util.concurrent.TimeUnit;
31 |
32 | import org.jitsi.utils.TimeProvider;
33 | import org.slf4j.Logger;
34 | import org.slf4j.LoggerFactory;
35 |
36 | import com.zhuyun.handler.HeartBeatServerHandler;
37 | import com.zhuyun.handler.RtcpHandler;
38 | import com.zhuyun.handler.RtpHandler;
39 | import com.zhuyun.handler.RtspHandler;
40 | import com.zhuyun.transform.RetransmissionRequesterDelegate;
41 |
42 | /**
43 | *
44 | *
45 | */
46 | public class RtspNettyServer {
47 | public static final Logger log = LoggerFactory.getLogger(RtspNettyServer.class);
48 | private static Bootstrap udpRtpstrap = new Bootstrap();
49 | private static Bootstrap udpRtcpstrap = new Bootstrap();
50 | public static Channel rtpChannel;
51 | public static Channel rtcpChannel;
52 | public static int RTSP_IDLE_TIME; //读和写的超时时间
53 | public static int RTCP_IDLE_TIME;
54 | public static int RTP_IDLE_TIME;
55 | public static ExecutorService EXECUTOR; //处理的线程池
56 | public static int WORKER_GROUP; //worker的线程数
57 | public static ScheduledExecutorService SCHEDULED_EXECUTOR; //定时线程,用来定时发送RTCP包
58 | public static int SCHEDULE_RTCP_SR_TIME; //定时发送RTCP SR的间隔时间
59 | public static String NEWTON_URL;
60 |
61 | public static int rtpPort = 54000;
62 | public static int rtspPort = 554;
63 | public static String outputPath = null;
64 | public static RetransmissionRequesterDelegate retransmissionRequesterDelegate;
65 |
66 | public static void initUdp(EventLoopGroup group)
67 | {
68 | udpRtpstrap.group(group)
69 | .channel(NioDatagramChannel.class)
70 | .option(ChannelOption.SO_SNDBUF, 1024*1024*2)
71 | .option(ChannelOption.SO_RCVBUF, 1024*1024*2)
72 | .handler(new ChannelInitializer() {
73 | @Override
74 | protected void initChannel(NioDatagramChannel nioDatagramChannel) throws Exception {
75 | nioDatagramChannel.pipeline().addLast(new RtpHandler());
76 | }
77 | })
78 | .option(ChannelOption.SO_BROADCAST, false);
79 |
80 | udpRtcpstrap.group(group)
81 | .channel(NioDatagramChannel.class)
82 | .option(ChannelOption.SO_SNDBUF, 1024*1024)
83 | .option(ChannelOption.SO_RCVBUF, 1024*1024)
84 | .handler(new ChannelInitializer() {
85 | @Override
86 | protected void initChannel(NioDatagramChannel nioDatagramChannel) throws Exception {
87 | nioDatagramChannel.pipeline().addLast(new RtcpHandler());
88 | }
89 | })
90 | .option(ChannelOption.SO_BROADCAST, false);
91 | }
92 |
93 | public static void createUdp(int port)
94 | {
95 | try
96 | {
97 | log.info("start udp bind {} ", port);
98 | rtpChannel = udpRtpstrap.bind(port).sync().channel();
99 | rtcpChannel = udpRtcpstrap.bind(port+1).sync().channel();
100 |
101 | log.info("end udp bind {}", port);
102 | }
103 | catch (InterruptedException e)
104 | {
105 | }
106 | }
107 |
108 | public static void main(String[] args) {
109 | try {
110 | Properties properties = new Properties();
111 | BufferedReader bufferedReader = new BufferedReader(new FileReader(
112 | // "C:/Users/zhouyinfei/Desktop/SVN/dsp/my_newtonGW/trunk/src/watt/rtsp-netty-server/src/main/resources/rtsp-server.properties"));
113 | "/home/zhou/rtsp-netty-server/rtsp-server.properties"));
114 | properties.load(bufferedReader);
115 | rtpPort = Integer.parseInt(properties.getProperty("rtp.port"));
116 | rtspPort = Integer.parseInt(properties.getProperty("rtsp.port"));
117 | outputPath = properties.getProperty("output.path");
118 | RTSP_IDLE_TIME = Integer.parseInt(properties.getProperty("rtsp.idle.time"));
119 | RTCP_IDLE_TIME = Integer.parseInt(properties.getProperty("rtcp.idle.time"));
120 | RTP_IDLE_TIME = Integer.parseInt(properties.getProperty("rtp.idle.time"));
121 | EXECUTOR = new ThreadPoolExecutor(5, Integer.parseInt(properties.getProperty("executor.threadpool")), 600, TimeUnit.SECONDS, new LinkedBlockingQueue());
122 | SCHEDULED_EXECUTOR = Executors.newScheduledThreadPool(Integer.parseInt(properties.getProperty("schedule.executor")));
123 | SCHEDULE_RTCP_SR_TIME = Integer.parseInt(properties.getProperty("schedule.rtcp.time"));
124 | WORKER_GROUP = Integer.parseInt(properties.getProperty("worker.group"));
125 | NEWTON_URL = properties.getProperty("newton.url");
126 |
127 | //定时重传NACK
128 | retransmissionRequesterDelegate = new RetransmissionRequesterDelegate(null, new TimeProvider());
129 | SCHEDULED_EXECUTOR.scheduleWithFixedDelay(retransmissionRequesterDelegate, 250, 250, TimeUnit.MILLISECONDS);
130 | } catch (NumberFormatException | IOException e1) {
131 | e1.printStackTrace();
132 | }
133 |
134 | ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.ADVANCED);
135 |
136 | EventLoopGroup listenGrp = new NioEventLoopGroup(1);
137 | EventLoopGroup workGrp = new NioEventLoopGroup(WORKER_GROUP);
138 | initUdp(workGrp);
139 | createUdp(rtpPort);
140 |
141 | try {
142 | ServerBootstrap rtspstrap = new ServerBootstrap();
143 | rtspstrap.group(listenGrp, workGrp)
144 | .channel(NioServerSocketChannel.class)
145 | .option(ChannelOption.SO_BACKLOG, 1024)
146 | .option(ChannelOption.SO_REUSEADDR, true)
147 | .childOption(ChannelOption.SO_RCVBUF, 64 * 1024)
148 | .childOption(ChannelOption.SO_SNDBUF, 64 * 1024)
149 | .childOption(ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark(64 * 1024 / 2, 64 * 1024))
150 | .childHandler(new ChannelInitializer() {
151 | @Override
152 | protected void initChannel(SocketChannel socketChannel) throws Exception {
153 | socketChannel.pipeline()
154 | .addLast(new IdleStateHandler(0, 0, RTSP_IDLE_TIME, TimeUnit.SECONDS))//5秒内既没有读,也没有写,则关闭连接
155 | .addLast(new RtspDecoder())
156 | .addLast(new RtspEncoder())
157 | .addLast(new HttpObjectAggregator(64 * 1024))
158 | .addLast(new RtspHandler())
159 | .addLast(new HeartBeatServerHandler());
160 | }
161 | });
162 |
163 |
164 | ChannelFuture rtspFuture = rtspstrap.bind(rtspPort).sync();
165 |
166 | log.info("RtspNettyServer start success ...");
167 |
168 | rtspFuture.channel().closeFuture().sync();
169 | } catch (InterruptedException e) {
170 | e.printStackTrace();
171 | } finally {
172 | listenGrp.shutdownGracefully();
173 | workGrp.shutdownGracefully();
174 | }
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/src/main/java/com/zhuyun/handler/HeartBeatServerHandler.java:
--------------------------------------------------------------------------------
1 | package com.zhuyun.handler;
2 |
3 | import org.slf4j.Logger;
4 | import org.slf4j.LoggerFactory;
5 |
6 |
7 |
8 | import com.zhuyun.RtspNettyServer;
9 |
10 | import io.netty.channel.ChannelHandlerContext;
11 | import io.netty.channel.ChannelInboundHandlerAdapter;
12 | import io.netty.handler.timeout.IdleState;
13 | import io.netty.handler.timeout.IdleStateEvent;
14 |
15 | /**
16 | *
17 | * @author zhouyinfei
18 | *
19 | */
20 | public class HeartBeatServerHandler extends ChannelInboundHandlerAdapter {
21 | public static final Logger log = LoggerFactory.getLogger(HeartBeatServerHandler.class);
22 |
23 | @Override
24 | public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
25 | log.debug("已经{}秒未收到或未发送给客户端的消息了!", RtspNettyServer.RTSP_IDLE_TIME);
26 |
27 | if (evt instanceof IdleStateEvent){
28 | IdleStateEvent event = (IdleStateEvent)evt;
29 | if (event.state()== IdleState.ALL_IDLE){
30 | log.debug("关闭这个不活跃通道!");
31 | ctx.channel().close();
32 | }
33 | }else {
34 | super.userEventTriggered(ctx,evt);
35 | }
36 | }
37 |
38 | @Override
39 | public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
40 | log.debug("client says: {}", msg.toString());
41 | }
42 |
43 | @Override
44 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
45 | ctx.close();
46 | }
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/src/main/java/com/zhuyun/handler/RtcpHandler.java:
--------------------------------------------------------------------------------
1 | package com.zhuyun.handler;
2 |
3 | import io.netty.buffer.ByteBuf;
4 | import io.netty.channel.ChannelHandlerContext;
5 | import io.netty.channel.SimpleChannelInboundHandler;
6 | import io.netty.channel.socket.DatagramPacket;
7 |
8 | import java.net.InetSocketAddress;
9 |
10 | import net.sf.fmj.media.rtp.RTCPPacket;
11 |
12 | import org.jitsi.impl.neomedia.rtcp.RTCPFBPacket;
13 | import org.jitsi.service.neomedia.ByteArrayBuffer;
14 | import org.jitsi.service.neomedia.RawPacket;
15 | import org.jitsi.util.RTPUtils;
16 | import org.slf4j.Logger;
17 | import org.slf4j.LoggerFactory;
18 |
19 | public class RtcpHandler extends SimpleChannelInboundHandler
20 | {
21 | public static final Logger log = LoggerFactory.getLogger(RtcpHandler.class);
22 |
23 | @Override
24 | protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket msg)
25 | {
26 | ByteBuf content = msg.content();
27 | byte[] dst = new byte[content.readableBytes()];
28 | content.readBytes(dst);
29 |
30 | RawPacket rawPacket = new RawPacket(dst, 0, dst.length);
31 |
32 | try {
33 | if (!rawPacket.isInvalid()) {
34 | String rtcpSsrc = null;
35 | switch (rawPacket.getRTCPPacketType()) {
36 | case RTCPPacket.SR: //发送端报告200
37 | rtcpSsrc = String.valueOf(rawPacket.getRTCPSSRC());
38 | break;
39 | case RTCPPacket.RR: //接收端报告201
40 | rtcpSsrc = String.valueOf(RTCPFBPacket.getSourceSSRC(rawPacket));
41 | break;
42 | case RTCPFBPacket.RTPFB: //RTPFB 205, FMT是1,NACK; FMT是15, transport-cc
43 | rtcpSsrc = String.valueOf(RTCPFBPacket.getSourceSSRC(rawPacket));
44 | int fmt = (rawPacket.getBuffer()[0] & 0x1F);
45 | if (fmt == 1) { //NACK
46 |
47 | } else if(fmt == 15){ //transport-cc
48 |
49 | }
50 | break;
51 | case RTCPFBPacket.PSFB: //PSFB 206, FMT是1, PLI; FMT是4, FIR; FMT是15, REMB
52 | rtcpSsrc = String.valueOf(RTCPFBPacket.getSourceSSRC(rawPacket));
53 | int fmt2 = (rawPacket.getBuffer()[0] & 0x1F);
54 | if (fmt2 == 1) { //PLI
55 |
56 | }
57 | break;
58 | default:
59 | break;
60 | }
61 | RtspHandler rtspHandler = RtpHandler.rtspHandlerMap.get(rtcpSsrc);
62 | if (rtspHandler != null) {
63 | log.debug("put rtcpQueue length= {}" , rtspHandler.rtcpQueue.size());
64 | rtspHandler.rtcpQueue.offer(rawPacket);
65 | }
66 | } else { //不是rtp包
67 | String destIp = msg.sender().getAddress().getHostAddress();
68 | int destPort = msg.sender().getPort();
69 |
70 | byte sign = dst[0];
71 | int ssrc = ((dst[1]&0xFF)<<24) + ((dst[2]&0xFF)<<16) + ((dst[3]&0xFF)<<8) + (dst[4]&0xFF);
72 | InetSocketAddress dstAddr = new InetSocketAddress(destIp, destPort);
73 | RtspHandler rtspHandler2 = RtpHandler.rtspHandlerMap.get(String.valueOf(ssrc));
74 | if (rtspHandler2 != null && destIp.equals(rtspHandler2.strremoteip)) {
75 | if (sign == 0x2) { //视频RTCP探测
76 | rtspHandler2.dstVideoRtcpAddr = dstAddr;
77 | rtspHandler2.isVideoRtcpDetected = true;
78 | } if (sign == 0x3) { //音频RTCP探测
79 | rtspHandler2.dstAudioRtcpAddr = dstAddr;
80 | }
81 | }
82 | }
83 | } catch (Exception e) {
84 | e.printStackTrace();
85 | }
86 |
87 |
88 | }
89 |
90 | @Override
91 | public void channelActive(ChannelHandlerContext ctx) throws Exception
92 | {
93 | super.channelActive(ctx);
94 | log.info("rtcp handler active {}", ctx.channel().id().asShortText());
95 | }
96 |
97 | @Override
98 | public void channelInactive(ChannelHandlerContext ctx) throws Exception
99 | {
100 | super.channelInactive(ctx);
101 | log.info("rtcp handler inactive {}", ctx.channel().id().asShortText());
102 | }
103 |
104 | private static final char Hex_Char_Arr[] = {'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'};
105 |
106 | public static String byteArrToHex(byte[] btArr) {
107 | char strArr[] = new char[btArr.length * 2];
108 | int i = 0;
109 | for (byte bt : btArr) {
110 | strArr[i++] = Hex_Char_Arr[bt>>>4 & 0xf];
111 | strArr[i++] = Hex_Char_Arr[bt & 0xf];
112 | }
113 | return new String(strArr);
114 | }
115 |
116 | @Override
117 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception
118 | {
119 | log.error("{}", cause.getMessage());
120 | ctx.channel().close();
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/src/main/java/com/zhuyun/handler/RtpHandler.java:
--------------------------------------------------------------------------------
1 | package com.zhuyun.handler;
2 |
3 | import io.netty.buffer.ByteBuf;
4 | import io.netty.buffer.Unpooled;
5 | import io.netty.channel.ChannelHandlerContext;
6 | import io.netty.channel.SimpleChannelInboundHandler;
7 | import io.netty.channel.socket.DatagramPacket;
8 |
9 | import java.net.InetSocketAddress;
10 | import java.util.Map;
11 | import java.util.concurrent.ArrayBlockingQueue;
12 | import java.util.concurrent.ConcurrentHashMap;
13 |
14 | import org.jitsi.service.neomedia.RawPacket;
15 | import org.slf4j.Logger;
16 | import org.slf4j.LoggerFactory;
17 |
18 |
19 | public class RtpHandler extends SimpleChannelInboundHandler
20 | {
21 | public static final Logger log = LoggerFactory.getLogger(RtpHandler.class);
22 | //key是ssrc。同一个channel内,audio和video的ssrc不同,但是Queue是同一个
23 | public static Map rtspHandlerMap =
24 | new ConcurrentHashMap(5000);
25 | // public static ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue(500000);
26 |
27 | @Override
28 | protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket msg)
29 | {
30 | ByteBuf content = msg.content();
31 | byte[] dst = new byte[content.readableBytes()];
32 | content.readBytes(dst);
33 |
34 | // byte[] b = new byte[11];
35 | // System.arraycopy(dst, 0, b, 0, 11);
36 | //如果udp的前面11个字节内容是"zhuyun-stun",则返回port, 2个字节
37 | // System.out.println("b=" + new String(b));
38 | // if ("zhuyun-stun".equals(new String(b))) {
39 | // String destIp = msg.sender().getAddress().getHostAddress();
40 | // int destPort = msg.sender().getPort();
41 | // System.out.println("destPort=" + destPort + ",destIp=" + destIp);
42 | //
43 | // InetSocketAddress dstAddr = new InetSocketAddress(destIp, destPort);;
44 | // ByteBuf byteBuf = Unpooled.buffer(2);
45 | // byteBuf.writeShort(destPort);
46 | // RtspNettyServer.rtpChannel.writeAndFlush(new DatagramPacket(byteBuf, dstAddr));
47 | // } else {
48 | // arrayBlockingQueue.offer(dst);
49 |
50 | try {
51 | //ssrc校验
52 | RawPacket rawPacket = new RawPacket(dst, 0, dst.length);
53 |
54 | if (!rawPacket.isInvalid()) {
55 | RtspHandler rtspHandler = rtspHandlerMap.get(String.valueOf(rawPacket.getSSRC()));
56 | if (rtspHandler != null) {
57 | rtspHandler.rtpQueue.offer(dst);
58 | }
59 | } else { //不是rtp包
60 | String destIp = msg.sender().getAddress().getHostAddress();
61 | int destPort = msg.sender().getPort();
62 |
63 | byte sign = dst[0];
64 | int ssrc = ((dst[1]&0xFF)<<24) + ((dst[2]&0xFF)<<16) + ((dst[3]&0xFF)<<8) + (dst[4]&0xFF);
65 | InetSocketAddress dstAddr = new InetSocketAddress(destIp, destPort);
66 | RtspHandler rtspHandler2 = rtspHandlerMap.get(String.valueOf(ssrc));
67 | if (rtspHandler2 != null && destIp.equals(rtspHandler2.strremoteip)) {
68 | if (sign == 0x0) { //视频RTP探测
69 | rtspHandler2.dstVideoRtpAddr = dstAddr;
70 | rtspHandler2.isVideoRtpDetected = true;
71 | } else if (sign == 0x1) { //音频RTP探测
72 | rtspHandler2.dstAudioRtpAddr = dstAddr;
73 | }
74 | }
75 |
76 | }
77 | } catch (Exception e) {
78 | e.printStackTrace();
79 | }
80 |
81 | }
82 |
83 | @Override
84 | public void channelActive(ChannelHandlerContext ctx) throws Exception
85 | {
86 | super.channelActive(ctx);
87 | log.info("rtp handler active {}", ctx.channel().id().asShortText());
88 | }
89 |
90 | @Override
91 | public void channelInactive(ChannelHandlerContext ctx) throws Exception
92 | {
93 | super.channelInactive(ctx);
94 | log.info("rtp handler inactive {}", ctx.channel().id().asShortText());
95 | }
96 |
97 | private static final char Hex_Char_Arr[] = {'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'};
98 |
99 | public static String byteArrToHex(byte[] btArr) {
100 | char strArr[] = new char[btArr.length * 2];
101 | int i = 0;
102 | for (byte bt : btArr) {
103 | strArr[i++] = Hex_Char_Arr[bt>>>4 & 0xf];
104 | strArr[i++] = Hex_Char_Arr[bt & 0xf];
105 | }
106 | return new String(strArr);
107 | }
108 |
109 | @Override
110 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception
111 | {
112 | log.error("", cause.getMessage());
113 | ctx.channel().close();
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/src/main/java/com/zhuyun/handler/RtspHandler.java:
--------------------------------------------------------------------------------
1 | package com.zhuyun.handler;
2 |
3 | import io.netty.buffer.ByteBuf;
4 | import io.netty.buffer.Unpooled;
5 | import io.netty.channel.Channel;
6 | import io.netty.channel.ChannelFutureListener;
7 | import io.netty.channel.ChannelHandlerContext;
8 | import io.netty.channel.SimpleChannelInboundHandler;
9 | import io.netty.channel.socket.DatagramPacket;
10 | import io.netty.handler.codec.http.DefaultFullHttpResponse;
11 | import io.netty.handler.codec.http.FullHttpRequest;
12 | import io.netty.handler.codec.http.FullHttpResponse;
13 | import io.netty.handler.codec.http.HttpUtil;
14 | import io.netty.handler.codec.http.QueryStringDecoder;
15 | import io.netty.handler.codec.rtsp.RtspHeaderNames;
16 | import io.netty.handler.codec.rtsp.RtspHeaderValues;
17 | import io.netty.handler.codec.rtsp.RtspMethods;
18 | import io.netty.handler.codec.rtsp.RtspResponseStatuses;
19 | import io.netty.handler.codec.rtsp.RtspVersions;
20 | import io.netty.util.CharsetUtil;
21 | import io.netty.util.internal.StringUtil;
22 |
23 | import java.io.BufferedInputStream;
24 | import java.io.BufferedOutputStream;
25 | import java.io.File;
26 | import java.io.FileInputStream;
27 | import java.io.FileNotFoundException;
28 | import java.io.FileOutputStream;
29 | import java.io.IOException;
30 | import java.io.OutputStream;
31 | import java.net.InetSocketAddress;
32 | import java.net.URLEncoder;
33 | import java.text.SimpleDateFormat;
34 | import java.util.Collection;
35 | import java.util.Date;
36 | import java.util.HashMap;
37 | import java.util.List;
38 | import java.util.Map;
39 | import java.util.Map.Entry;
40 | import java.util.TreeMap;
41 | import java.util.concurrent.ArrayBlockingQueue;
42 | import java.util.concurrent.ExecutorService;
43 | import java.util.concurrent.Executors;
44 | import java.util.concurrent.ScheduledExecutorService;
45 | import java.util.concurrent.TimeUnit;
46 |
47 | import net.sf.fmj.media.rtp.RTCPPacket;
48 |
49 | import org.apache.commons.lang.StringUtils;
50 | import org.apache.commons.lang.math.RandomUtils;
51 | import org.bouncycastle.util.Strings;
52 | import org.jitsi.impl.neomedia.rtcp.NACKPacket;
53 | import org.jitsi.impl.neomedia.rtcp.RTCPFBPacket;
54 | import org.jitsi.service.neomedia.ByteArrayBuffer;
55 | import org.jitsi.service.neomedia.RawPacket;
56 | import org.slf4j.Logger;
57 | import org.slf4j.LoggerFactory;
58 |
59 | import com.alibaba.fastjson.JSONObject;
60 | import com.zhuyun.RtspNettyServer;
61 | import com.zhuyun.media.MediaSdpInfo;
62 | import com.zhuyun.rtcp.AudioSRRtcp;
63 | import com.zhuyun.rtcp.VideoSRRtcp;
64 | import com.zhuyun.rtp.RtpUtils;
65 | import com.zhuyun.transform.RetransmissionRequesterDelegate;
66 | import com.zhuyun.utils.HttpConnection;
67 | import com.zhuyun.utils.SdpParser;
68 |
69 | public class RtspHandler extends SimpleChannelInboundHandler
70 | {
71 | public static final Logger log = LoggerFactory.getLogger(RtspHandler.class);
72 | private int remoteVideoRtpPort = 0; //客户端Video的RTP端口
73 | private int remoteVideoRtcpPort = 0; //客户端Video的RTCP端口
74 | private int remoteAudioRtpPort = 0; //客户端Audio的RTP端口
75 | private int remoteAudioRtcpPort = 0; //客户端Audio的RTCP端口
76 | private int videoSsrc = 0; //如果是record,则由客户端带上来。如果是play,则由服务器下发下去
77 | private int audioSsrc = 0; //如果是record,则由客户端带上来。如果是play,则由服务器下发下去
78 | public String strremoteip; //客户端的IP地址
79 | private String session;
80 | public InetSocketAddress dstVideoRtpAddr = null; //Video目的客户端地址
81 | public InetSocketAddress dstVideoRtcpAddr = null; //Video目的客户端地址
82 | public InetSocketAddress dstAudioRtpAddr = null; //Audio目的客户端地址
83 | public InetSocketAddress dstAudioRtcpAddr = null; //Audio目的客户端地址
84 | private volatile int isRtspAlive = 1; //rtsp连接是否存在,如果不存在,则停止发送udp
85 | private int fps = 25; //默认帧率
86 | private String media = "h265"; //默认媒体类型
87 | private Map mediaSdpInfoMap = null;
88 | private String keyhash = "";
89 | private Channel chn; //RTSP channel
90 | public ArrayBlockingQueue rtpQueue; //存放音视频数据的阻塞队列
91 | public TreeMap rtpCacheQueue; //存放音视频播放的时候的发包历史数据
92 | public ArrayBlockingQueue rtcpQueue; //存放rtcp的阻塞队列
93 | public boolean isVideoRtpDetected = false; //是否收到Video的udp探测包
94 | public boolean isVideoRtcpDetected = false; //是否收到Video的udp rtcp探测包
95 | private VideoSRRtcp videoSRRtcp; //视频发送端报告
96 | private AudioSRRtcp audioSRRtcp; //音频发送端报告
97 | private RetransmissionRequesterDelegate retransmissionRequesterDelegate;
98 |
99 | @Override
100 | public void channelActive(ChannelHandlerContext ctx) throws Exception
101 | {
102 | chn = ctx.channel();
103 | log.debug("{} new connection {}", chn.id(), Thread.currentThread().getName());
104 |
105 | rtcpQueue = new ArrayBlockingQueue(50);
106 | retransmissionRequesterDelegate = RtspNettyServer.retransmissionRequesterDelegate;
107 |
108 | }
109 |
110 | public void closeThisClient()
111 | {
112 | if (this.chn.isActive())
113 | {
114 | log.debug("close this client {}", this.chn);
115 | this.chn.close();
116 | }
117 | }
118 |
119 | @Override
120 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception
121 | {
122 | log.debug("{}", cause.getMessage());
123 | ctx.channel().close();
124 | }
125 |
126 | @Override
127 | public void channelInactive(ChannelHandlerContext ctx) throws Exception
128 | {
129 | super.channelInactive(ctx);
130 | log.debug("{} i am dead", ctx.channel().id());
131 |
132 | if (!StringUtil.isNullOrEmpty(keyhash))
133 | {
134 | // from stream hub clear this info
135 | log.debug("{} will leave stream {} ", chn.id(), keyhash);
136 | keyhash = "";
137 | }
138 |
139 | RtpHandler.rtspHandlerMap.remove(String.valueOf(videoSsrc));
140 | RtpHandler.rtspHandlerMap.remove(String.valueOf(audioSsrc));
141 |
142 | isRtspAlive = 0;
143 | }
144 |
145 | private boolean checkUrl(FullHttpRequest r)
146 | {
147 | if (!StringUtil.isNullOrEmpty(keyhash))
148 | {
149 | return true;
150 | }
151 |
152 | QueryStringDecoder uri = new QueryStringDecoder(r.uri());
153 |
154 | if (!uri.path().endsWith("/live") || !uri.parameters().containsKey("keyhash"))
155 | {
156 | return false;
157 | }
158 |
159 | keyhash = (Strings.split(uri.toString(), '?')[1].split("&"))[0].substring(8);
160 |
161 | //设备keyhash,必须
162 | if (StringUtil.isNullOrEmpty(keyhash))
163 | {
164 | return false;
165 | }
166 |
167 | //媒体类型,非必须,值目前可选范围:264、aac
168 | if (uri.parameters().get("media") != null) {
169 | media = uri.parameters().get("media").get(0);
170 | }
171 | if (!"h264".equals(media) && !"h265".equals(media)) {
172 | return false;
173 | }
174 |
175 | //帧率,非必须
176 | String fpsString = null;
177 | if (uri.parameters().get("fps") != null) {
178 | fpsString = uri.parameters().get("fps").get(0);
179 | }
180 | if (fpsString != null && StringUtils.isNumeric(fpsString)) {
181 | fps = Integer.parseInt(fpsString);
182 | }
183 |
184 | return true;
185 | }
186 |
187 | @Override
188 | protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest r) throws Exception
189 | {
190 | FullHttpResponse o = new DefaultFullHttpResponse(RtspVersions.RTSP_1_0, RtspResponseStatuses.OK);
191 | if (!r.decoderResult().isSuccess())
192 | {
193 | log.error("error: decode error, invalid rtsp request");
194 | closeThisClient();
195 | return;
196 | }
197 | if (false == checkUrl(r))
198 | {
199 | log.error("error: check url error, 451, Parameter Not Understood");
200 | o.setStatus(RtspResponseStatuses.PARAMETER_NOT_UNDERSTOOD); //Parameter Not Understood,参数无效
201 | sendAnswer(ctx, r, o);
202 | closeThisClient();
203 | return;
204 | }
205 |
206 | if (r.method() == RtspMethods.OPTIONS) {
207 | log.debug("options");
208 | o.headers().add(RtspHeaderValues.PUBLIC, "DESCRIBE, SETUP, PLAY, TEARDOWN, ANNOUNCE, RECORD, GET_PARAMETER");
209 | } else if (r.method() == RtspMethods.DESCRIBE) {
210 | log.debug("describe");
211 | InetSocketAddress addr = (InetSocketAddress) ctx.channel().localAddress();
212 |
213 | //默认是H265
214 | String sdp = String.format("c=IN IP4 %s \nm=video 0 RTP/AVP 96\na=rtpmap:96 H265/90000\n"
215 | + "a=fmtp:96 packetization-mode=1; sprop-parameter-sets=Z0IAH5Y1QKALdNwEBAQI,aM4xsg==; profile-level-id=42001F\n"
216 | + "a=control:streamid=0\n"
217 | + "m=audio 0 RTP/AVP 97\na=rtpmap:97 MPEG4-GENERIC/16000\n"
218 | + "a=fmtp:97 streamtype=5; profile-level-id=15; mode=AAC-hbr; config=140856e500; sizeLength=13; indexLength=3; indexDeltaLength=3; Profile=1;\n"
219 | + "a=control:streamid=1\n", addr.getHostString());
220 | if ("h264".equals(media)) {
221 | sdp = String.format("c=IN IP4 %s \nm=video 0 RTP/AVP 96\na=rtpmap:96 H264/90000\n"
222 | + "a=fmtp:96 packetization-mode=1; sprop-parameter-sets=Z0IAH5Y1QKALdNwEBAQI,aM4xsg==; profile-level-id=42001F\n"
223 | + "a=control:streamid=0\n"
224 | + "m=audio 0 RTP/AVP 97\na=rtpmap:97 MPEG4-GENERIC/16000\n"
225 | + "a=fmtp:97 streamtype=5; profile-level-id=15; mode=AAC-hbr; config=140856e500; sizeLength=13; indexLength=3; indexDeltaLength=3; Profile=1;\n"
226 | + "a=control:streamid=1\n", addr.getHostString());
227 | } else if ("h265".equals(media)) {
228 |
229 | }
230 |
231 | o.headers().add(RtspHeaderNames.CONTENT_TYPE, "application/sdp");
232 | o.content().writeCharSequence(sdp, CharsetUtil.UTF_8);
233 | o.headers().add(RtspHeaderNames.CONTENT_LENGTH, o.content().writerIndex());
234 | } else if (r.method() == RtspMethods.SETUP) {
235 | log.debug("setup");
236 |
237 | String transport = r.headers().get(RtspHeaderNames.TRANSPORT);
238 | transport = transport.toLowerCase();
239 |
240 | String uri = r.uri();
241 | //streamid=0、streamid=1
242 |
243 | String[] strlist = transport.split(";");
244 | if (strlist.length > 0 && strlist[0].contains("rtp/avp"))
245 | {
246 | for(String i : strlist)
247 | {
248 | if (i.startsWith("client_port")) {
249 | if ((mediaSdpInfoMap != null && mediaSdpInfoMap.containsKey("video") && uri.endsWith(mediaSdpInfoMap.get("video").getControl()))
250 | || (mediaSdpInfoMap == null && uri.endsWith("streamid=0"))) { //视频流
251 | String[] strclientport = i.split("=|-");
252 |
253 | remoteVideoRtpPort = Integer.parseInt(strclientport[1]);
254 | remoteVideoRtcpPort = Integer.parseInt(strclientport[2]);
255 | strremoteip = ((InetSocketAddress) ctx.channel().remoteAddress()).getHostString();
256 |
257 | videoSsrc = RandomUtils.nextInt();
258 | //如果在ssrcMap中已存在该ssrc,则重新生成
259 | if (RtpHandler.rtspHandlerMap.containsKey(String.valueOf(videoSsrc))) {
260 | videoSsrc = RandomUtils.nextInt();
261 | }
262 | RtpHandler.rtspHandlerMap.put(String.valueOf(videoSsrc), this);
263 |
264 | if (null == dstVideoRtpAddr)
265 | {
266 | dstVideoRtpAddr = new InetSocketAddress(strremoteip, remoteVideoRtpPort);
267 | dstVideoRtcpAddr = new InetSocketAddress(strremoteip, remoteVideoRtcpPort);
268 | }
269 | o.headers().add(RtspHeaderNames.TRANSPORT,
270 | r.headers().get(RtspHeaderNames.TRANSPORT)+String.format(";server_port=%d-%d", RtspNettyServer.rtpPort, RtspNettyServer.rtpPort+1)+";ssrc=" + videoSsrc);
271 | break;
272 | } else if ((mediaSdpInfoMap != null && mediaSdpInfoMap.containsKey("audio") && uri.endsWith(mediaSdpInfoMap.get("audio").getControl()))
273 | || (mediaSdpInfoMap == null && uri.endsWith("streamid=1"))) { //音频流
274 | String[] strclientport = i.split("=|-");
275 |
276 | remoteAudioRtpPort = Integer.parseInt(strclientport[1]);
277 | remoteAudioRtcpPort = Integer.parseInt(strclientport[2]);
278 | strremoteip = ((InetSocketAddress) ctx.channel().remoteAddress()).getHostString();
279 |
280 | audioSsrc = RandomUtils.nextInt();
281 | //如果在ssrcMap中已存在该ssrc,则重新生成
282 | if (RtpHandler.rtspHandlerMap.containsKey(String.valueOf(audioSsrc))) {
283 | audioSsrc = RandomUtils.nextInt();
284 | }
285 | RtpHandler.rtspHandlerMap.put(String.valueOf(audioSsrc), this);
286 |
287 | if (null == dstAudioRtpAddr)
288 | {
289 | dstAudioRtpAddr = new InetSocketAddress(strremoteip, remoteAudioRtpPort);
290 | dstAudioRtcpAddr = new InetSocketAddress(strremoteip, remoteAudioRtcpPort);
291 | }
292 | o.headers().add(RtspHeaderNames.TRANSPORT,
293 | r.headers().get(RtspHeaderNames.TRANSPORT)+String.format(";server_port=%d-%d", RtspNettyServer.rtpPort, RtspNettyServer.rtpPort+1)+";ssrc=" + audioSsrc);
294 | }
295 | }
296 | }
297 |
298 | } else{
299 | log.error("error: SETUP error transport, must contains rtp/avp. 461, Unsupported transport");
300 | o.setStatus(RtspResponseStatuses.UNSUPPORTED_TRANSPORT); //不支持的transport
301 | sendAnswer(ctx, r, o);
302 | closeThisClient();
303 | return;
304 | }
305 |
306 | session = r.headers().get(RtspHeaderNames.SESSION);
307 | if (session == null) //如果为空,则将channel id返回
308 | {
309 | session = chn.id().toString();
310 | o.headers().add(RtspHeaderNames.SESSION, session);
311 | }
312 | else { //如果不为空,则判断是否等于当前channel id
313 | if (!session.equals(chn.id().toString())) { //如果不等于当前channel id,则关闭连接
314 | log.error("error: SETUP session incorrect, 454, Session Not Found");
315 | o.setStatus(RtspResponseStatuses.SESSION_NOT_FOUND); //Session未找到
316 | sendAnswer(ctx, r, o);
317 | closeThisClient();
318 | return;
319 | }
320 | }
321 |
322 | log.debug("transport request=" + transport + ",channel=" + ctx.channel().id());
323 | log.debug("transport response=" + o.headers().get(RtspHeaderNames.TRANSPORT)+ ",channel=" + ctx.channel().id());
324 | } else if (r.method() == RtspMethods.PLAY) {
325 | // send rtp and rtcp to client
326 | log.debug("play");
327 |
328 | //校验session是否存在
329 | session = r.headers().get(RtspHeaderNames.SESSION);
330 | if (session == null || !session.equals(chn.id().toString())) {
331 | log.error("error: PLAY session incorrect, 454, Session Not Found");
332 | o.setStatus(RtspResponseStatuses.SESSION_NOT_FOUND); //Session未找到
333 | sendAnswer(ctx, r, o);
334 | closeThisClient();
335 | return;
336 | } else {
337 | ///////////用户、设备鉴权/////////////
338 | String tenantId = null;
339 | String token = null;
340 | String mediaName = null;
341 |
342 | QueryStringDecoder uri = new QueryStringDecoder(r.uri());
343 | //参数检测
344 | if (uri.parameters().get("tenantId") == null ||
345 | uri.parameters().get("token") == null ||
346 | uri.parameters().get("mediaName") == null) {
347 | log.error("error: PLAY invalid params, 451, Parameter Not Understood");
348 | o.setStatus(RtspResponseStatuses.PARAMETER_NOT_UNDERSTOOD); //参数无效
349 | sendAnswer(ctx, r, o);
350 | closeThisClient();
351 | return;
352 | }
353 |
354 | //如果未收到视频的udp探测包,或者视频的rtcp探测包,则返回错误
355 | if (isVideoRtpDetected == false || isVideoRtcpDetected == false) {
356 | log.error("error: PLAY not received detect packets, 462, Destination unreachable");
357 | o.setStatus(RtspResponseStatuses.DESTINATION_UNREACHABLE);
358 | sendAnswer(ctx, r, o);
359 | closeThisClient();
360 | return;
361 | }
362 |
363 | tenantId = uri.parameters().get("tenantId").get(0);
364 | token = uri.parameters().get("token").get(0);
365 | mediaName = uri.parameters().get("mediaName").get(0);
366 |
367 | //newton鉴权
368 | String url = RtspNettyServer.NEWTON_URL + "getPayInfoIsExpire?keyhash=" + keyhash
369 | + "&tenantId=" + tenantId + "&token=" + token
370 | + "&chargeMark=3&version=1.0";
371 | String result = HttpConnection.get(url);
372 | // String result = "{\"retcode\": \"200\"}";
373 |
374 | log.debug("PLAY: keyhash={}, tenantId={}, token={}, mediaName={}, newtonUrl={}, result={}", keyhash, tenantId, token, mediaName, url, result);
375 | JSONObject jsonObject = JSONObject.parseObject(result);
376 | if (jsonObject == null) { //连接newtonWeb失败
377 | log.error("error: PLAY connect to newton failed, 400, Bad Request");
378 | o.setStatus(RtspResponseStatuses.BAD_REQUEST); //请求失败
379 | o.content().writeCharSequence("{\"retcode\": \"215\"}", CharsetUtil.UTF_8);
380 | sendAnswer(ctx, r, o);
381 | closeThisClient();
382 | return;
383 | }
384 | String retcode = jsonObject.getString("retcode");
385 | if (!"200".equals(retcode)) { //如果返回码不是200,则直接返回newton的错误码,关闭连接
386 | log.error("error: PLAY newton retcode not 200, 400, Bad Request");
387 | o.setStatus(RtspResponseStatuses.BAD_REQUEST); //请求失败
388 | o.content().writeCharSequence(jsonObject.toString(), CharsetUtil.UTF_8);
389 | sendAnswer(ctx, r, o);
390 | closeThisClient();
391 | return;
392 | }
393 | ///////////////////////////////
394 |
395 | //根据rtcp消息作为心跳,一段时间内未收到rtcp包,则关闭rtsp连接,并停止发送音视频数据。
396 | RtspNettyServer.EXECUTOR.execute(new Runnable() {
397 | @Override
398 | public void run() { //处理rtcp请求
399 | dealWithRtcp();
400 | }
401 | });
402 |
403 | rtpCacheQueue = new TreeMap();
404 |
405 | //根据文件名获取路径信息
406 | SimpleDateFormat formatter= new SimpleDateFormat("yyyyMMdd");
407 | Date date = new Date(Long.parseLong(mediaName)*1000);
408 | String today = formatter.format(date);
409 |
410 | if ("h264".equals(media)) {
411 | //播放h264视频文件、aac音频文件
412 | String videoFilename = RtspNettyServer.outputPath + keyhash + "/" + today + "/" + mediaName + ".h264";
413 | File videoFile = new File(videoFilename);
414 | if (!videoFile.exists()) {
415 | log.error("error: PLAY h264 file not exist, 404, Not Found");
416 | o.setStatus(RtspResponseStatuses.NOT_FOUND); //文件不存在
417 | sendAnswer(ctx, r, o);
418 | closeThisClient();
419 | throw new FileNotFoundException(videoFilename);
420 | }
421 |
422 | String audioFilename = RtspNettyServer.outputPath + keyhash + "/" + today + "/" + mediaName + ".aac";
423 | File audioFile = new File(audioFilename);
424 | if (!audioFile.exists()) {
425 | log.error("error: PLAY aac file not exist, 404, Not Found");
426 | o.setStatus(RtspResponseStatuses.NOT_FOUND); //文件不存在
427 | sendAnswer(ctx, r, o);
428 | closeThisClient();
429 | throw new FileNotFoundException(audioFilename);
430 | }
431 |
432 | sendAnswer(ctx, r, o);
433 | // int rtpTimestamp = (int) (System.currentTimeMillis()/1000); //音视频初始的RTP时间戳
434 | RtspNettyServer.EXECUTOR.execute(new Runnable() {
435 | @Override
436 | public void run() {
437 | // playH264(videoFile, rtpTimestamp);
438 | playH264(videoFile);
439 | // sendVideoSenderReport(); //发送视频的SR报告
440 | }
441 | });
442 | RtspNettyServer.EXECUTOR.execute(new Runnable() {
443 | @Override
444 | public void run() {
445 | // playAac(audioFile, rtpTimestamp);
446 | playAac(audioFile);
447 | // sendAudioSenderReport(); //发送音频的SR报告
448 | }
449 | });
450 | return;
451 | } else if ("h265".equals(media)) {
452 | //播放h265视频文件、aac音频文件
453 | String videoFilename = RtspNettyServer.outputPath + keyhash + "/" + today + "/" + mediaName + ".h265";;
454 | File videoFile = new File(videoFilename);
455 | if (!videoFile.exists()) {
456 | log.error("error: PLAY h265 file not exist, 404, Not Found");
457 | o.setStatus(RtspResponseStatuses.NOT_FOUND);
458 | sendAnswer(ctx, r, o);
459 | closeThisClient();
460 | throw new FileNotFoundException(videoFilename);
461 | }
462 |
463 | String audioFilename = RtspNettyServer.outputPath + keyhash + "/" + today + "/" + mediaName + ".aac";
464 | File audioFile = new File(audioFilename);
465 | if (!audioFile.exists()) {
466 | log.error("error: PLAY acc file not exist, 404, Not Found");
467 | o.setStatus(RtspResponseStatuses.NOT_FOUND);
468 | sendAnswer(ctx, r, o);
469 | closeThisClient();
470 | throw new FileNotFoundException(audioFilename);
471 | }
472 |
473 | sendAnswer(ctx, r, o);
474 | // int rtpTimestamp = (int) (System.currentTimeMillis()/1000); //音视频初始的RTP时间戳
475 | RtspNettyServer.EXECUTOR.execute(new Runnable() {
476 | @Override
477 | public void run() {
478 | // playH265(videoFile, rtpTimestamp);
479 | playH265(videoFile);
480 | // sendVideoSenderReport(); //发送视频的SR报告
481 | }
482 | });
483 | RtspNettyServer.EXECUTOR.execute(new Runnable() {
484 | @Override
485 | public void run() {
486 | // playAac(audioFile, rtpTimestamp);
487 | playAac(audioFile);
488 | // sendAudioSenderReport(); //发送音频的SR报告
489 | }
490 | });
491 | return;
492 | }
493 |
494 | }
495 |
496 | } else if (r.method() == RtspMethods.TEARDOWN) {
497 | log.debug("teardown");
498 |
499 | //校验session是否存在
500 | session = r.headers().get(RtspHeaderNames.SESSION);
501 | if (session == null || !session.equals(chn.id().toString())) {
502 | log.error("error: TEARDOWN rtspSession is null or invalid, 454, Session Not Found");
503 | o.setStatus(RtspResponseStatuses.SESSION_NOT_FOUND); //Session未找到
504 | sendAnswer(ctx, r, o);
505 | closeThisClient();
506 | return;
507 | } else {
508 | sendAnswer(ctx, r, o);
509 | closeThisClient();
510 | return;
511 | }
512 | } else if (r.method() == RtspMethods.GET_PARAMETER) {
513 | log.debug("get_parameter");
514 |
515 | //校验session是否存在
516 | session = r.headers().get(RtspHeaderNames.SESSION);
517 | if (session == null || !session.equals(chn.id().toString())) {
518 | log.error("error: GET_PARAMETER rtspSession is null or invalid, 454, Session Not Found");
519 | o.setStatus(RtspResponseStatuses.SESSION_NOT_FOUND); //Session未找到
520 | sendAnswer(ctx, r, o);
521 | closeThisClient();
522 | return;
523 | }
524 | } else if (r.method() == RtspMethods.ANNOUNCE) {
525 | log.debug("announce");
526 |
527 | ByteBuf content = r.content();
528 | byte[] sdp = new byte[content.readableBytes()];
529 | content.readBytes(sdp);
530 |
531 | mediaSdpInfoMap = SdpParser.parse(new String(sdp)); //解析出音视频相关参数
532 | if (mediaSdpInfoMap == null || mediaSdpInfoMap.size() == 0 ||
533 | ((mediaSdpInfoMap != null && mediaSdpInfoMap.containsKey("video")
534 | && !mediaSdpInfoMap.get("video").getCodec().equals("H264"))
535 | && !mediaSdpInfoMap.get("video").getCodec().equals("H265")) ||
536 | ((mediaSdpInfoMap != null && mediaSdpInfoMap.containsKey("audio")
537 | && !mediaSdpInfoMap.get("audio").getCodec().equals("MPEG4-GENERIC")))) {
538 | log.error("error: ANNOUNCE 415, Unsupported Media Type");
539 | o.setStatus(RtspResponseStatuses.UNSUPPORTED_MEDIA_TYPE); //不支持的媒体类型
540 | sendAnswer(ctx, r, o);
541 | closeThisClient();
542 | return;
543 | }
544 | } else if (r.method() == RtspMethods.RECORD) {
545 | log.debug("record");
546 |
547 | //校验session是否存在
548 | session = r.headers().get(RtspHeaderNames.SESSION);
549 | if (session == null || !session.equals(chn.id().toString())) {
550 | log.error("error: RECORD rtspSession is null or invalid, 454, Session Not Found");
551 | o.setStatus(RtspResponseStatuses.SESSION_NOT_FOUND); //Session未找到
552 | sendAnswer(ctx, r, o);
553 | closeThisClient();
554 | return;
555 | } else {
556 | ///////////设备鉴权/////////////
557 | String content = null; //keyhash+aeskey拼接后的字符串,再进行MD5运算后的值。
558 | QueryStringDecoder uri = new QueryStringDecoder(r.uri());
559 | if (uri.parameters().get("content") == null) { //content不能为空,否则报错
560 | log.error("error: RECORD content param cannot be null, 451, Parameter Not Understood");
561 | o.setStatus(RtspResponseStatuses.PARAMETER_NOT_UNDERSTOOD); //参数无效
562 | sendAnswer(ctx, r, o);
563 | closeThisClient();
564 | return;
565 | } else {
566 | content = uri.parameters().get("content").get(0);
567 | }
568 |
569 | String url = RtspNettyServer.NEWTON_URL + "getPayInfoIsExpire2?keyhash=" + URLEncoder.encode(keyhash)
570 | + "&content=" + content + "&chargeMark=3&version=1.0";
571 | String result = HttpConnection.get(url);
572 |
573 | log.debug("RECORD: keyhash={}, content={}, newtonUrl={}, result={}", keyhash, content, url, result);
574 | // String result = "{\"retcode\": \"200\"}";
575 | JSONObject jsonObject = JSONObject.parseObject(result);
576 | if (jsonObject == null) { //连接newtonWeb失败
577 | log.error("error: RECORD connect to newton failed, 400, Bad Request");
578 | o.setStatus(RtspResponseStatuses.BAD_REQUEST); //请求失败
579 | o.content().writeCharSequence("{\"retcode\": \"215\"}", CharsetUtil.UTF_8);
580 | sendAnswer(ctx, r, o);
581 | closeThisClient();
582 | return;
583 | }
584 | String retcode = jsonObject.getString("retcode");
585 | if (!"200".equals(retcode)) { //如果返回码不是200,则直接返回newton的错误码,关闭连接
586 | log.error("error: RECORD newton retcode not 200, 400, Bad Request");
587 | o.setStatus(RtspResponseStatuses.BAD_REQUEST); //请求失败
588 | o.content().writeCharSequence(jsonObject.toString(), CharsetUtil.UTF_8);
589 | sendAnswer(ctx, r, o);
590 | closeThisClient();
591 | return;
592 | }
593 | ///////////////////////////////
594 |
595 | long now = System.currentTimeMillis();
596 | o.headers().add("filename", now/1000);
597 | sendAnswer(ctx, r, o);
598 |
599 | rtpQueue = new ArrayBlockingQueue(1000); //初始化rtp队列
600 |
601 | RtspNettyServer.EXECUTOR.execute(new Runnable() {
602 | @Override
603 | public void run() {
604 | OutputStream videoOutputStream = null;
605 | OutputStream audioOutputStream = null;
606 | int videoPayloadType = mediaSdpInfoMap.containsKey("video")?mediaSdpInfoMap.get("video").getRtpPayloadType():0;
607 | String videoCodec = mediaSdpInfoMap.containsKey("video")?mediaSdpInfoMap.get("video").getCodec():null;
608 | int audioPayloadType = mediaSdpInfoMap.containsKey("audio")?mediaSdpInfoMap.get("audio").getRtpPayloadType():0;
609 | String audioCodec = mediaSdpInfoMap.containsKey("audio")?mediaSdpInfoMap.get("audio").getCodec():null;
610 | try {
611 | SimpleDateFormat formatter= new SimpleDateFormat("yyyyMMdd");
612 | Date date = new Date(now);
613 | String today = formatter.format(date);
614 | String keyhashDir = keyhash.replace('/', '-'); //keyhash中的/符号需要转换成-号
615 |
616 | //H264
617 | if ("H264".equals(videoCodec)) {
618 | File file = new File(RtspNettyServer.outputPath + keyhashDir + "/" + today + "/" + (now/1000) + ".h264");
619 | if (!file.getParentFile().exists()) { //如果目录不存在,则创建
620 | file.getParentFile().mkdirs();
621 | }
622 | videoOutputStream = new BufferedOutputStream(new FileOutputStream(file, true));
623 | //H265
624 | } else if ("H265".equals(videoCodec)) {
625 | File file = new File(RtspNettyServer.outputPath + keyhashDir + "/" + today + "/" + (now/1000) + ".h265");
626 | if (!file.getParentFile().exists()) { //如果目录不存在,则创建
627 | file.getParentFile().mkdirs();
628 | }
629 | videoOutputStream = new BufferedOutputStream(new FileOutputStream(file, true));
630 | }
631 | //AAC
632 | if("MPEG4-GENERIC".equals(audioCodec)) {
633 | File file = new File(RtspNettyServer.outputPath + keyhashDir + "/" + today + "/" + (now/1000) + ".aac");
634 | if (!file.getParentFile().exists()) { //如果目录不存在,则创建
635 | file.getParentFile().mkdirs();
636 | }
637 | audioOutputStream = new BufferedOutputStream(new FileOutputStream(file, true));
638 | }
639 |
640 | TreeMap map = new TreeMap();
641 |
642 | int count = 0;
643 | while (true) {
644 | if (isRtspAlive == 0) { //如果rtsp连接中断,则停止接收udp
645 | //将map中剩下的所有RTP包都写入文件,foreach循环是有序的
646 | for (Entry entrySet : map.entrySet()) {
647 | recordMedia(entrySet.getValue(), videoPayloadType, videoCodec, videoOutputStream, audioPayloadType, audioCodec, audioOutputStream);
648 | }
649 | break;
650 | }
651 |
652 | byte[] take = rtpQueue.poll(RtspNettyServer.RTP_IDLE_TIME, TimeUnit.SECONDS);
653 | if (take == null) {
654 | //将map中剩下的所有RTP包都写入文件,foreach循环是有序的
655 | for (Entry entrySet : map.entrySet()) {
656 | recordMedia(entrySet.getValue(), videoPayloadType, videoCodec, videoOutputStream, audioPayloadType, audioCodec, audioOutputStream);
657 | }
658 | break;
659 | }
660 |
661 | System.out.println("count=" + count++);
662 | RawPacket rawPacket = new RawPacket(take, 0, take.length);
663 | retransmissionRequesterDelegate.packetReceived(rawPacket.getSSRCAsLong(), rawPacket.getSequenceNumber());
664 |
665 | ///////////////////
666 | // recordMedia(rawPacket, videoPayloadType, videoCodec, videoOutputStream, audioPayloadType, audioCodec, audioOutputStream);
667 | //////////////////
668 |
669 | map.put(rawPacket.getSequenceNumber(), rawPacket);
670 | if (map.size() >= 300) { //大概是3秒时间内产生的RTP数量
671 | for (int i = 0; i < 100; i++) { //取第一秒的RTP包
672 | RawPacket minimumPacket = map.remove(map.firstKey()); //每次取第一个
673 | recordMedia(minimumPacket, videoPayloadType, videoCodec, videoOutputStream, audioPayloadType, audioCodec, audioOutputStream);
674 | }
675 | }
676 |
677 | }
678 | } catch (InterruptedException | IOException e) {
679 | e.printStackTrace();
680 | } finally {
681 | try {
682 | if (videoOutputStream != null) {
683 | videoOutputStream.close();
684 | }
685 | if (audioOutputStream != null) {
686 | audioOutputStream.close();
687 | }
688 | } catch (IOException e) {
689 | e.printStackTrace();
690 | }
691 | }
692 | return;
693 | }
694 | });
695 | return;
696 | }
697 | } else {
698 | log.error("error: unknown message, 405, Method Not Allowed");
699 | o.setStatus(RtspResponseStatuses.METHOD_NOT_ALLOWED);
700 | sendAnswer(ctx, r, o);
701 | closeThisClient();
702 | return;
703 | }
704 | sendAnswer(ctx, r, o);
705 | }
706 |
707 |
708 | private void sendAnswer(ChannelHandlerContext ctx, FullHttpRequest req, FullHttpResponse rep)
709 | {
710 | final String cseq = req.headers().get(RtspHeaderNames.CSEQ);
711 | if (cseq != null)
712 | {
713 | rep.headers().add(RtspHeaderNames.CSEQ, cseq);
714 | }
715 | final String session = req.headers().get(RtspHeaderNames.SESSION);
716 | if (session != null)
717 | {
718 | rep.headers().add(RtspHeaderNames.SESSION, session);
719 | }
720 | if (!HttpUtil.isKeepAlive(req))
721 | {
722 | ctx.writeAndFlush(rep).addListener(ChannelFutureListener.CLOSE);
723 | } else
724 | {
725 | rep.headers().set(RtspHeaderNames.CONNECTION, RtspHeaderValues.KEEP_ALIVE);
726 | ctx.writeAndFlush(rep);
727 | }
728 | }
729 |
730 | // public void playH264(File f, int rtpTimestamp){
731 | public void playH264(File f){
732 | // int time = rtpTimestamp;
733 | int time = 0;
734 | //播放h264视频文件
735 | BufferedInputStream in = null;
736 | try {
737 | in = new BufferedInputStream(new FileInputStream(f));
738 | int buf_size = 64*1024;
739 | byte[] buffer = new byte[buf_size]; //从文件读的字节存入的地方
740 | byte[] nalu; //临时存储一个nalu单元内容
741 | byte[] firstHalfNalu = null; //nalu前半段
742 | byte[] secondHalfNalu = null; //nalu后半段
743 | int len = 0; //每次从文件读的字节数
744 | int state = 0; //状态机,值范围:0、1、2、3、4
745 | int first = 1; //是否是第一个起始码
746 | int cross = 0; //某个nalu是否跨buffer
747 | RtpUtils rtpUtils = new RtpUtils();
748 |
749 | while (-1 != (len = in.read(buffer, 0, buf_size))) {
750 | if (isRtspAlive == 0) { //如果rtsp连接中断,则停止发送udp
751 | break;
752 | }
753 |
754 | int start = 0; //第一个nalu的起始位置
755 | int offset = 0; //当前循环中的偏移量
756 | while (offset <= len-4) {
757 | if (state == 0) { //没有遗留状态
758 | if (buffer[offset] == 0x00 &&
759 | buffer[offset + 1] == 0x00 &&
760 | buffer[offset + 2] == 0x00 &&
761 | buffer[offset + 3] == 0x01) {
762 | if (cross == 1) { //跨buffer
763 | if (first == 0) { //不是第一个起始码
764 | secondHalfNalu = new byte[offset]; //拿到后半段内容
765 | System.arraycopy(buffer, start, secondHalfNalu, 0, secondHalfNalu.length);
766 |
767 | //拼接前半段与后半段内容, 拷贝到新的数组中
768 | int naluSize = firstHalfNalu.length + secondHalfNalu.length;
769 | nalu = new byte[naluSize];
770 | System.arraycopy(firstHalfNalu, 0, nalu, 0, firstHalfNalu.length);
771 | System.arraycopy(secondHalfNalu, 0, nalu, firstHalfNalu.length, secondHalfNalu.length);
772 |
773 | // List rtpList = rtpUtils.naluToRtpPack(nalu, videoSsrc, fps, time);
774 | Map rtpMap = rtpUtils.naluToRtpPack(nalu, videoSsrc, fps, time);
775 | for (Entry entry: rtpMap.entrySet()) {
776 | ByteBuf byteBuf = Unpooled.buffer(entry.getValue().length);
777 | byteBuf.writeBytes(entry.getValue());
778 | RtspNettyServer.rtpChannel.writeAndFlush(new DatagramPacket(byteBuf, this.dstVideoRtpAddr));
779 |
780 | //存入历史记录中,用于NACK重传。超过800时,则先删除旧的,再插入
781 | if (rtpCacheQueue.size() > 800) {
782 | rtpCacheQueue.pollFirstEntry();
783 | }
784 | rtpCacheQueue.put(entry.getKey(), entry.getValue());
785 | }
786 | time += (90000/fps); //rtp时间戳刻度递增
787 | }
788 |
789 | offset += 4;
790 | state = 0;
791 | first = 0;
792 | start = offset; //当前位置变成新的起始位置
793 | cross = 0; //跨buffer标志位重置成0
794 | } else { //没有跨buffer
795 | if (first == 0) { //不是第一个起始码
796 | int naluSize = offset - start;
797 | nalu = new byte[naluSize];
798 | System.arraycopy(buffer, start, nalu, 0, naluSize);
799 |
800 | Map rtpMap = rtpUtils.naluToRtpPack(nalu, videoSsrc, fps, time);
801 | for (Entry entry: rtpMap.entrySet()) {
802 | ByteBuf byteBuf = Unpooled.buffer(entry.getValue().length);
803 | byteBuf.writeBytes(entry.getValue());
804 | RtspNettyServer.rtpChannel.writeAndFlush(new DatagramPacket(byteBuf, this.dstVideoRtpAddr));
805 |
806 | //存入历史记录中,用于NACK重传。超过800时,则先删除旧的,再插入
807 | if (rtpCacheQueue.size() > 800) {
808 | rtpCacheQueue.pollFirstEntry();
809 | }
810 | rtpCacheQueue.put(entry.getKey(), entry.getValue());
811 | }
812 | time += (90000/fps); //rtp时间戳刻度递增
813 | }
814 |
815 | offset += 4;
816 | state = 0;
817 | first = 0;
818 | start = offset; //当前位置变成新的起始位置
819 | }
820 |
821 |
822 | } else {
823 | state = 0;
824 | offset ++;
825 | }
826 | } else if (state == 1) {
827 | if (buffer[offset] == 0x00 &&
828 | buffer[offset + 1] == 0x00 &&
829 | buffer[offset + 2] == 0x01) {
830 |
831 | //拿到两个起始码之间的一个nalu的数据
832 | int naluSize = firstHalfNalu.length - 1;
833 | nalu = new byte[naluSize];
834 | System.arraycopy(firstHalfNalu, 0, nalu, 0, naluSize);
835 |
836 | Map rtpMap = rtpUtils.naluToRtpPack(nalu, videoSsrc, fps, time);
837 | for (Entry entry: rtpMap.entrySet()) {
838 | ByteBuf byteBuf = Unpooled.buffer(entry.getValue().length);
839 | byteBuf.writeBytes(entry.getValue());
840 | RtspNettyServer.rtpChannel.writeAndFlush(new DatagramPacket(byteBuf, this.dstVideoRtpAddr));
841 |
842 | //存入历史记录中,用于NACK重传。超过800时,则先删除旧的,再插入
843 | if (rtpCacheQueue.size() > 800) {
844 | rtpCacheQueue.pollFirstEntry();
845 | }
846 | rtpCacheQueue.put(entry.getKey(), entry.getValue());
847 | }
848 | time += (90000/fps); //rtp时间戳刻度递增
849 |
850 | offset += 3;
851 | state = 0;
852 | start = offset; //当前位置变成新的起始位置
853 | } else {
854 | state = 0;
855 | offset ++;
856 | }
857 | } else if (state == 2) {
858 | if (buffer[offset] == 0x00 &&
859 | buffer[offset + 1] == 0x01) {
860 |
861 | //拿到两个起始码之间的一个nalu的数据
862 | int naluSize = firstHalfNalu.length - 2;
863 | nalu = new byte[naluSize];
864 | System.arraycopy(firstHalfNalu, 0, nalu, 0, naluSize);
865 |
866 | Map rtpMap = rtpUtils.naluToRtpPack(nalu, videoSsrc, fps, time);
867 | for (Entry entry: rtpMap.entrySet()) {
868 | ByteBuf byteBuf = Unpooled.buffer(entry.getValue().length);
869 | byteBuf.writeBytes(entry.getValue());
870 | RtspNettyServer.rtpChannel.writeAndFlush(new DatagramPacket(byteBuf, this.dstVideoRtpAddr));
871 |
872 | //存入历史记录中,用于NACK重传。超过800时,则先删除旧的,再插入
873 | if (rtpCacheQueue.size() > 800) {
874 | rtpCacheQueue.pollFirstEntry();
875 | }
876 | rtpCacheQueue.put(entry.getKey(), entry.getValue());
877 | }
878 | time += (90000/fps); //rtp时间戳刻度递增
879 |
880 | offset += 2;
881 | state = 0;
882 | start = offset; //当前位置变成新的起始位置
883 | } else {
884 | state = 0;
885 | offset ++;
886 | }
887 | } else if (state == 3) {
888 | if (buffer[offset] == 0x01) {
889 | //拿到两个起始码之间的一个nalu的数据
890 | int naluSize = firstHalfNalu.length - 3;
891 | nalu = new byte[naluSize];
892 | System.arraycopy(firstHalfNalu, 0, nalu, 0, naluSize);
893 |
894 | Map rtpMap = rtpUtils.naluToRtpPack(nalu, videoSsrc, fps, time);
895 | for (Entry entry: rtpMap.entrySet()) {
896 | ByteBuf byteBuf = Unpooled.buffer(entry.getValue().length);
897 | byteBuf.writeBytes(entry.getValue());
898 | RtspNettyServer.rtpChannel.writeAndFlush(new DatagramPacket(byteBuf, this.dstVideoRtpAddr));
899 |
900 | //存入历史记录中,用于NACK重传。超过800时,则先删除旧的,再插入
901 | if (rtpCacheQueue.size() > 800) {
902 | rtpCacheQueue.pollFirstEntry();
903 | }
904 | rtpCacheQueue.put(entry.getKey(), entry.getValue());
905 | }
906 | time += (90000/fps); //rtp时间戳刻度递增
907 |
908 | offset += 1;
909 | state = 0;
910 | start = offset; //当前位置变成新的起始位置
911 | } else {
912 | state = 0;
913 | offset ++;
914 | }
915 | }
916 | }
917 |
918 |
919 | //指针指向最后3位时
920 | if (offset == len-3) {
921 | if (buffer[offset] == 0x00 &&
922 | buffer[offset + 1] == 0x00 &&
923 | buffer[offset + 2] == 0x00) {
924 | state = 3;
925 | } else if (buffer[offset + 1] == 0x00 &&
926 | buffer[offset + 2] == 0x00) {
927 | state = 2;
928 | } else if (buffer[offset + 2] == 0x00) {
929 | state = 1;
930 | }
931 | cross = 1; //一定会跨buffer
932 | firstHalfNalu = new byte[offset + 3 - start]; //初始化前半段nalu数组,将前半段内容放进去
933 | System.arraycopy(buffer, start, firstHalfNalu, 0, firstHalfNalu.length);
934 | }
935 |
936 | }
937 |
938 | } catch (Exception e) {
939 | e.printStackTrace();
940 | } finally {
941 | try {
942 | in.close();
943 | } catch (IOException e) {
944 | e.printStackTrace();
945 | }
946 | }
947 | }
948 |
949 | // public void playH265(File f, int rtpTimestamp){
950 | public void playH265(File f){
951 | // int time = rtpTimestamp;
952 | int time = 0;
953 | //播放h265视频文件
954 | BufferedInputStream in = null;
955 | try {
956 | in = new BufferedInputStream(new FileInputStream(f));
957 | int buf_size = 64*1024;
958 | byte[] buffer = new byte[buf_size]; //从文件读的字节存入的地方
959 | byte[] nalu; //临时存储一个nalu单元内容
960 | byte[] firstHalfNalu = null; //nalu前半段
961 | byte[] secondHalfNalu = null; //nalu后半段
962 | int len = 0; //每次从文件读的字节数
963 | int state = 0; //状态机,值范围:0、1、2、3、4
964 | int first = 1; //是否是第一个起始码
965 | int cross = 0; //某个nalu是否跨buffer
966 | RtpUtils rtpUtils = new RtpUtils();
967 |
968 | while (-1 != (len = in.read(buffer, 0, buf_size))) {
969 | if (isRtspAlive == 0) { //如果rtsp连接中断,则停止发送udp
970 | break;
971 | }
972 |
973 | int start = 0; //第一个nalu的起始位置
974 | int offset = 0; //当前循环中的偏移量
975 | while (offset <= len-4) {
976 | if (state == 0) { //没有遗留状态
977 | if (buffer[offset] == 0x00 &&
978 | buffer[offset + 1] == 0x00 &&
979 | buffer[offset + 2] == 0x00 &&
980 | buffer[offset + 3] == 0x01) {
981 | if (cross == 1) { //跨buffer
982 | if (first == 0) { //不是第一个起始码
983 | secondHalfNalu = new byte[offset]; //拿到后半段内容
984 | System.arraycopy(buffer, start, secondHalfNalu, 0, secondHalfNalu.length);
985 |
986 | //拼接前半段与后半段内容, 拷贝到新的数组中
987 | int naluSize = firstHalfNalu.length + secondHalfNalu.length;
988 | nalu = new byte[naluSize];
989 | System.arraycopy(firstHalfNalu, 0, nalu, 0, firstHalfNalu.length);
990 | System.arraycopy(secondHalfNalu, 0, nalu, firstHalfNalu.length, secondHalfNalu.length);
991 |
992 | Map rtpMap = rtpUtils.naluH265ToRtpPack(nalu, videoSsrc, fps, time);
993 | for (Entry entry: rtpMap.entrySet()) {
994 | ByteBuf byteBuf = Unpooled.buffer(entry.getValue().length);
995 | byteBuf.writeBytes(entry.getValue());
996 | RtspNettyServer.rtpChannel.writeAndFlush(new DatagramPacket(byteBuf, this.dstVideoRtpAddr));
997 |
998 | //存入历史记录中,用于NACK重传。超过800时,则先删除旧的,再插入
999 | if (rtpCacheQueue.size() > 800) {
1000 | rtpCacheQueue.pollFirstEntry();
1001 | }
1002 | rtpCacheQueue.put(entry.getKey(), entry.getValue());
1003 | }
1004 | time += (90000/fps); //rtp时间戳刻度递增
1005 | }
1006 |
1007 | offset += 4;
1008 | state = 0;
1009 | first = 0;
1010 | start = offset; //当前位置变成新的起始位置
1011 | cross = 0; //跨buffer标志位重置成0
1012 | } else { //没有跨buffer
1013 | if (first == 0) { //不是第一个起始码
1014 | int naluSize = offset - start;
1015 | nalu = new byte[naluSize];
1016 | System.arraycopy(buffer, start, nalu, 0, naluSize);
1017 |
1018 | Map rtpMap = rtpUtils.naluH265ToRtpPack(nalu, videoSsrc, fps, time);
1019 | for (Entry entry: rtpMap.entrySet()) {
1020 | ByteBuf byteBuf = Unpooled.buffer(entry.getValue().length);
1021 | byteBuf.writeBytes(entry.getValue());
1022 | RtspNettyServer.rtpChannel.writeAndFlush(new DatagramPacket(byteBuf, this.dstVideoRtpAddr));
1023 |
1024 | //存入历史记录中,用于NACK重传。超过800时,则先删除旧的,再插入
1025 | if (rtpCacheQueue.size() > 800) {
1026 | rtpCacheQueue.pollFirstEntry();
1027 | }
1028 | rtpCacheQueue.put(entry.getKey(), entry.getValue());
1029 | }
1030 | time += (90000/fps); //rtp时间戳刻度递增
1031 | }
1032 |
1033 | offset += 4;
1034 | state = 0;
1035 | first = 0;
1036 | start = offset; //当前位置变成新的起始位置
1037 | }
1038 |
1039 |
1040 | } else {
1041 | state = 0;
1042 | offset ++;
1043 | }
1044 | } else if (state == 1) {
1045 | if (buffer[offset] == 0x00 &&
1046 | buffer[offset + 1] == 0x00 &&
1047 | buffer[offset + 2] == 0x01) {
1048 |
1049 | //拿到两个起始码之间的一个nalu的数据
1050 | int naluSize = firstHalfNalu.length - 1;
1051 | nalu = new byte[naluSize];
1052 | System.arraycopy(firstHalfNalu, 0, nalu, 0, naluSize);
1053 |
1054 | Map rtpMap = rtpUtils.naluH265ToRtpPack(nalu, videoSsrc, fps, time);
1055 | for (Entry entry: rtpMap.entrySet()) {
1056 | ByteBuf byteBuf = Unpooled.buffer(entry.getValue().length);
1057 | byteBuf.writeBytes(entry.getValue());
1058 | RtspNettyServer.rtpChannel.writeAndFlush(new DatagramPacket(byteBuf, this.dstVideoRtpAddr));
1059 |
1060 | //存入历史记录中,用于NACK重传。超过800时,则先删除旧的,再插入
1061 | if (rtpCacheQueue.size() > 800) {
1062 | rtpCacheQueue.pollFirstEntry();
1063 | }
1064 | rtpCacheQueue.put(entry.getKey(), entry.getValue());
1065 | }
1066 | time += (90000/fps); //rtp时间戳刻度递增
1067 |
1068 | offset += 3;
1069 | state = 0;
1070 | start = offset; //当前位置变成新的起始位置
1071 | } else {
1072 | state = 0;
1073 | offset ++;
1074 | }
1075 | } else if (state == 2) {
1076 | if (buffer[offset] == 0x00 &&
1077 | buffer[offset + 1] == 0x01) {
1078 |
1079 | //拿到两个起始码之间的一个nalu的数据
1080 | int naluSize = firstHalfNalu.length - 2;
1081 | nalu = new byte[naluSize];
1082 | System.arraycopy(firstHalfNalu, 0, nalu, 0, naluSize);
1083 |
1084 | Map rtpMap = rtpUtils.naluH265ToRtpPack(nalu, videoSsrc, fps, time);
1085 | for (Entry entry: rtpMap.entrySet()) {
1086 | ByteBuf byteBuf = Unpooled.buffer(entry.getValue().length);
1087 | byteBuf.writeBytes(entry.getValue());
1088 | RtspNettyServer.rtpChannel.writeAndFlush(new DatagramPacket(byteBuf, this.dstVideoRtpAddr));
1089 |
1090 | // //存入历史记录中,用于NACK重传。超过800时,则先删除旧的,再插入
1091 | if (rtpCacheQueue.size() > 800) {
1092 | rtpCacheQueue.pollFirstEntry();
1093 | }
1094 | rtpCacheQueue.put(entry.getKey(), entry.getValue());
1095 | }
1096 | time += (90000/fps); //rtp时间戳刻度递增
1097 |
1098 | offset += 2;
1099 | state = 0;
1100 | start = offset; //当前位置变成新的起始位置
1101 | } else {
1102 | state = 0;
1103 | offset ++;
1104 | }
1105 | } else if (state == 3) {
1106 | if (buffer[offset] == 0x01) {
1107 | //拿到两个起始码之间的一个nalu的数据
1108 | int naluSize = firstHalfNalu.length - 3;
1109 | nalu = new byte[naluSize];
1110 | System.arraycopy(firstHalfNalu, 0, nalu, 0, naluSize);
1111 |
1112 | Map rtpMap = rtpUtils.naluH265ToRtpPack(nalu, videoSsrc, fps, time);
1113 | for (Entry entry: rtpMap.entrySet()) {
1114 | ByteBuf byteBuf = Unpooled.buffer(entry.getValue().length);
1115 | byteBuf.writeBytes(entry.getValue());
1116 | RtspNettyServer.rtpChannel.writeAndFlush(new DatagramPacket(byteBuf, this.dstVideoRtpAddr));
1117 |
1118 | //存入历史记录中,用于NACK重传。超过800时,则先删除旧的,再插入
1119 | if (rtpCacheQueue.size() > 800) {
1120 | rtpCacheQueue.pollFirstEntry();
1121 | }
1122 | rtpCacheQueue.put(entry.getKey(), entry.getValue());
1123 | }
1124 | time += (90000/fps); //rtp时间戳刻度递增
1125 |
1126 | offset += 1;
1127 | state = 0;
1128 | start = offset; //当前位置变成新的起始位置
1129 | } else {
1130 | state = 0;
1131 | offset ++;
1132 | }
1133 | }
1134 | }
1135 |
1136 |
1137 | //指针指向最后3位时
1138 | if (offset == len-3) {
1139 | if (buffer[offset] == 0x00 &&
1140 | buffer[offset + 1] == 0x00 &&
1141 | buffer[offset + 2] == 0x00) {
1142 | state = 3;
1143 | } else if (buffer[offset + 1] == 0x00 &&
1144 | buffer[offset + 2] == 0x00) {
1145 | state = 2;
1146 | } else if (buffer[offset + 2] == 0x00) {
1147 | state = 1;
1148 | }
1149 | cross = 1; //一定会跨buffer
1150 | firstHalfNalu = new byte[offset + 3 - start]; //初始化前半段nalu数组,将前半段内容放进去
1151 | System.arraycopy(buffer, start, firstHalfNalu, 0, firstHalfNalu.length);
1152 | }
1153 |
1154 | }
1155 |
1156 | } catch (Exception e) {
1157 | e.printStackTrace();
1158 | } finally {
1159 | try {
1160 | in.close();
1161 | } catch (IOException e) {
1162 | e.printStackTrace();
1163 | }
1164 | }
1165 | }
1166 |
1167 | // public void playAac(File f, int rtpTimestamp){
1168 | public void playAac(File f){
1169 | // int time = rtpTimestamp;
1170 | int time = 0;
1171 | BufferedInputStream in = null;
1172 | try {
1173 | in = new BufferedInputStream(new FileInputStream(f));
1174 | int seqNum = 1; //rtp的seqnum
1175 | int len = 0; //每次从文件读的实际字节数
1176 | int aacDataLen = 0; //aac data长度
1177 | int sampling = 16000; //采样率,默认值16000
1178 | byte[] adtsHeaderBuffer = new byte[7]; //临时存储adts头
1179 | byte[] aacDataBuffer; //临时存储aac data
1180 | RtpUtils rtpUtils = new RtpUtils();
1181 |
1182 | int isHeader = 1; //标志位,表示当前读取的是adts头还是aac data
1183 | len = in.read(adtsHeaderBuffer, 0, 7); //刚开始读取adts头
1184 | while (len != -1) {
1185 | if (isRtspAlive == 0) { //如果rtsp连接中断,则停止发送udp
1186 | break;
1187 | }
1188 |
1189 | if (isHeader == 1) { //adts头,下一个就是aac data
1190 | aacDataLen = ((adtsHeaderBuffer[3]&0x03)<<11) //获取aac data长度
1191 | + (adtsHeaderBuffer[4]<<3)
1192 | + ((adtsHeaderBuffer[5]&0xE0)>>5) - 7;
1193 | byte samp = (byte) (adtsHeaderBuffer[2]&0x3C); //获取采样率
1194 | sampling = RtpUtils.getSampling(samp);
1195 | isHeader = 0;
1196 | } else { //aac data,下一个就是adts头
1197 | aacDataBuffer = new byte[aacDataLen];
1198 | len = in.read(aacDataBuffer, 0, aacDataLen); //读取aac data
1199 | byte[] rptPackage = rtpUtils.aacToRtpPack(aacDataBuffer, seqNum, audioSsrc, time);
1200 | ByteBuf byteBuf = Unpooled.buffer(rptPackage.length);
1201 | byteBuf.writeBytes(rptPackage);
1202 | RtspNettyServer.rtpChannel.writeAndFlush(new DatagramPacket(byteBuf, this.dstAudioRtpAddr));
1203 | time += 1024; //rtp时间戳刻度递增, 音频固定递增1024
1204 | seqNum ++;
1205 |
1206 | Thread.sleep(1024*1000/sampling); //延时发送帧
1207 | len = in.read(adtsHeaderBuffer, 0, 7);
1208 | isHeader = 1;
1209 | }
1210 | }
1211 | } catch (IOException | InterruptedException e) {
1212 | e.printStackTrace();
1213 | } finally{
1214 | try {
1215 | if (in != null) {
1216 | in.close();
1217 | }
1218 | } catch (IOException e) {
1219 | e.printStackTrace();
1220 | }
1221 | }
1222 | }
1223 |
1224 | //保存音视频
1225 | public void recordMedia(RawPacket rawPacket, int videoPayloadType, String videoCodec
1226 | , OutputStream videoOutputStream, int audioPayloadType, String audioCodec, OutputStream audioOutputStream) throws IOException{
1227 | byte type = rawPacket.getPayloadType();
1228 | if (type == videoPayloadType) {
1229 | if ("H264".equals(videoCodec)) { //如果sdp中是H264,就按照H264规则解码
1230 | byte[] b = RtpUtils.rtpToNaluPack(rawPacket);
1231 | if (b != null && b.length != 0) {
1232 | videoOutputStream.write(b);
1233 | }
1234 | } else if ("H265".equals(videoCodec)) { //如果sdp中是H265,就按照H265规则解码
1235 | byte[] b = RtpUtils.rtpToNaluH265Pack(rawPacket);
1236 | if (b != null && b.length != 0) {
1237 | videoOutputStream.write(b);
1238 | }
1239 | }
1240 | } else if (type == audioPayloadType) {
1241 | if ("MPEG4-GENERIC".equals(audioCodec)) { //如果sdp中是MPEG4-GENERIC,就按照AAC规则解码
1242 | List adtsList = RtpUtils.rtpToAdtsPack(rawPacket, mediaSdpInfoMap.get("audio").getClockRate());
1243 | if (adtsList != null && adtsList.size() != 0) {
1244 | for (byte[] b : adtsList) {
1245 | audioOutputStream.write(b);
1246 | }
1247 | }
1248 | }
1249 | }
1250 | }
1251 |
1252 | //处理RTCP
1253 | public void dealWithRtcp(){
1254 | try {
1255 | while (true) {
1256 | RawPacket poll = rtcpQueue.poll(RtspNettyServer.RTCP_IDLE_TIME, TimeUnit.SECONDS);
1257 | log.debug("get rtcpQueue length = {}", rtcpQueue.size());
1258 | if (poll == null) {
1259 | closeThisClient();
1260 | break;
1261 | }
1262 |
1263 | if (!poll.isInvalid()) {
1264 | switch (poll.getRTCPPacketType()) {
1265 | case RTCPPacket.SR: //发送端报告200
1266 | break;
1267 | case RTCPPacket.RR: //接收端报告201
1268 | break;
1269 | case RTCPFBPacket.RTPFB: //RTPFB 205, FMT是1,NACK; FMT是15, transport-cc
1270 | int fmt = (poll.getBuffer()[0] & 0x1F);
1271 | if (fmt == 1) { //NACK,从历史记录中找到丢的包并重传
1272 | Collection lostPackets = NACKPacket.getLostPackets(poll);
1273 | for (Integer seqnum : lostPackets) {
1274 | byte[] lostPacket = rtpCacheQueue.get(seqnum);
1275 | if (lostPacket == null) { //如果不存在于队列中,则跳过
1276 | continue;
1277 | }
1278 | ByteBuf byteBuf = Unpooled.buffer(lostPacket.length);
1279 | byteBuf.writeBytes(lostPacket);
1280 | RtspNettyServer.rtpChannel.writeAndFlush(new DatagramPacket(byteBuf, this.dstVideoRtpAddr));
1281 | }
1282 | }
1283 | break;
1284 | case RTCPFBPacket.PSFB: //PSFB 206, FMT是1, PLI; FMT是4, FIR; FMT是15, REMB
1285 | break;
1286 | default:
1287 | break;
1288 | }
1289 | }
1290 | }
1291 | } catch (InterruptedException e) {
1292 | e.printStackTrace();
1293 | }
1294 | }
1295 |
1296 | }
--------------------------------------------------------------------------------
/src/main/java/com/zhuyun/media/MediaSdpInfo.java:
--------------------------------------------------------------------------------
1 | package com.zhuyun.media;
2 |
3 | public class MediaSdpInfo {
4 | private String media; //媒体类型,可选范围:audio、video
5 | private String codec; //编解码格式,例如:H264、H265、MPEG4-GENERIC
6 | private int rtpPayloadType; //rtp包的payload类型,例如:96、97
7 | private int clockRate; //采样率
8 | private String control;
9 |
10 | public MediaSdpInfo() {
11 | super();
12 | }
13 |
14 | public MediaSdpInfo(String media, String codec, int rtpPayloadType,
15 | int clockRate, String control) {
16 | super();
17 | this.media = media;
18 | this.codec = codec;
19 | this.rtpPayloadType = rtpPayloadType;
20 | this.clockRate = clockRate;
21 | this.control = control;
22 | }
23 |
24 | public String getMedia() {
25 | return media;
26 | }
27 |
28 | public void setMedia(String media) {
29 | this.media = media;
30 | }
31 |
32 | public String getCodec() {
33 | return codec;
34 | }
35 |
36 | public void setCodec(String codec) {
37 | this.codec = codec;
38 | }
39 |
40 | public int getRtpPayloadType() {
41 | return rtpPayloadType;
42 | }
43 |
44 | public void setRtpPayloadType(int rtpPayloadType) {
45 | this.rtpPayloadType = rtpPayloadType;
46 | }
47 |
48 | public int getClockRate() {
49 | return clockRate;
50 | }
51 |
52 | public void setClockRate(int clockRate) {
53 | this.clockRate = clockRate;
54 | }
55 |
56 | public String getControl() {
57 | return control;
58 | }
59 |
60 | public void setControl(String control) {
61 | this.control = control;
62 | }
63 |
64 | @Override
65 | public String toString() {
66 | return "MediaSdpInfo [media=" + media + ", codec=" + codec
67 | + ", rtpPayloadType=" + rtpPayloadType + ", clockRate="
68 | + clockRate + ", control=" + control + "]";
69 | }
70 |
71 | }
72 |
--------------------------------------------------------------------------------
/src/main/java/com/zhuyun/rtcp/AudioRRRtcp.java:
--------------------------------------------------------------------------------
1 | package com.zhuyun.rtcp;
2 |
3 | public class AudioRRRtcp {
4 |
5 | }
6 |
--------------------------------------------------------------------------------
/src/main/java/com/zhuyun/rtcp/AudioSRRtcp.java:
--------------------------------------------------------------------------------
1 | package com.zhuyun.rtcp;
2 |
3 | /**
4 | * 音频发送端报告
5 | * @author zhouyinfei
6 | *
7 | */
8 | public class AudioSRRtcp {
9 | private int mswNtpTimestamp; //NTP时间戳
10 | private int lswNtpTimestamp; //NTP时间戳
11 | private int rtpTimestamp; //RTP时间戳
12 | private int senderPacketCount; //从开始传输到此 SR 包产生时该发送者发送的 RTP 数据包总数
13 | private int senderOctetCount; //从开始传输到此 SR 包产生时该发送者在 RTP 数据包发送的字节总数
14 |
15 | public AudioSRRtcp() {
16 | super();
17 | }
18 |
19 | public int getMswNtpTimestamp() {
20 | return mswNtpTimestamp;
21 | }
22 |
23 | public void setMswNtpTimestamp(int mswNtpTimestamp) {
24 | this.mswNtpTimestamp = mswNtpTimestamp;
25 | }
26 |
27 | public int getLswNtpTimestamp() {
28 | return lswNtpTimestamp;
29 | }
30 |
31 | public void setLswNtpTimestamp(int lswNtpTimestamp) {
32 | this.lswNtpTimestamp = lswNtpTimestamp;
33 | }
34 |
35 | public int getRtpTimestamp() {
36 | return rtpTimestamp;
37 | }
38 |
39 | public void setRtpTimestamp(int rtpTimestamp) {
40 | this.rtpTimestamp = rtpTimestamp;
41 | }
42 |
43 | public int getSenderPacketCount() {
44 | return senderPacketCount;
45 | }
46 |
47 | public void setSenderPacketCount(int senderPacketCount) {
48 | this.senderPacketCount = senderPacketCount;
49 | }
50 |
51 | public int getSenderOctetCount() {
52 | return senderOctetCount;
53 | }
54 |
55 | public void setSenderOctetCount(int senderOctetCount) {
56 | this.senderOctetCount = senderOctetCount;
57 | }
58 |
59 | @Override
60 | public String toString() {
61 | return "VideoSRRtcp [mswNtpTimestamp=" + mswNtpTimestamp
62 | + ", lswNtpTimestamp=" + lswNtpTimestamp + ", rtpTimestamp="
63 | + rtpTimestamp + ", senderPacketCount=" + senderPacketCount
64 | + ", senderOctetCount=" + senderOctetCount + "]";
65 | }
66 |
67 |
68 | }
69 |
--------------------------------------------------------------------------------
/src/main/java/com/zhuyun/rtcp/VideoRRRtcp.java:
--------------------------------------------------------------------------------
1 | package com.zhuyun.rtcp;
2 |
3 | public class VideoRRRtcp {
4 |
5 | }
6 |
--------------------------------------------------------------------------------
/src/main/java/com/zhuyun/rtcp/VideoSRRtcp.java:
--------------------------------------------------------------------------------
1 | package com.zhuyun.rtcp;
2 |
3 | /**
4 | * 视频发送端报告
5 | * @author zhouyinfei
6 | *
7 | */
8 | public class VideoSRRtcp {
9 | private int mswNtpTimestamp; //NTP时间戳
10 | private int lswNtpTimestamp; //NTP时间戳
11 | private int rtpTimestamp; //RTP时间戳
12 | private int senderPacketCount; //从开始传输到此 SR 包产生时该发送者发送的 RTP 数据包总数
13 | private int senderOctetCount; //从开始传输到此 SR 包产生时该发送者在 RTP 数据包发送的字节总数
14 |
15 | public VideoSRRtcp() {
16 | super();
17 | }
18 |
19 | public int getMswNtpTimestamp() {
20 | return mswNtpTimestamp;
21 | }
22 |
23 | public void setMswNtpTimestamp(int mswNtpTimestamp) {
24 | this.mswNtpTimestamp = mswNtpTimestamp;
25 | }
26 |
27 | public int getLswNtpTimestamp() {
28 | return lswNtpTimestamp;
29 | }
30 |
31 | public void setLswNtpTimestamp(int lswNtpTimestamp) {
32 | this.lswNtpTimestamp = lswNtpTimestamp;
33 | }
34 |
35 | public int getRtpTimestamp() {
36 | return rtpTimestamp;
37 | }
38 |
39 | public void setRtpTimestamp(int rtpTimestamp) {
40 | this.rtpTimestamp = rtpTimestamp;
41 | }
42 |
43 | public int getSenderPacketCount() {
44 | return senderPacketCount;
45 | }
46 |
47 | public void setSenderPacketCount(int senderPacketCount) {
48 | this.senderPacketCount = senderPacketCount;
49 | }
50 |
51 | public int getSenderOctetCount() {
52 | return senderOctetCount;
53 | }
54 |
55 | public void setSenderOctetCount(int senderOctetCount) {
56 | this.senderOctetCount = senderOctetCount;
57 | }
58 |
59 | @Override
60 | public String toString() {
61 | return "VideoSRRtcp [mswNtpTimestamp=" + mswNtpTimestamp
62 | + ", lswNtpTimestamp=" + lswNtpTimestamp + ", rtpTimestamp="
63 | + rtpTimestamp + ", senderPacketCount=" + senderPacketCount
64 | + ", senderOctetCount=" + senderOctetCount + "]";
65 | }
66 |
67 | }
68 |
--------------------------------------------------------------------------------
/src/main/java/com/zhuyun/rtp/FileUtils.java:
--------------------------------------------------------------------------------
1 | package com.zhuyun.rtp;
2 |
3 | import java.io.BufferedInputStream;
4 | import java.io.BufferedOutputStream;
5 | import java.io.ByteArrayOutputStream;
6 | import java.io.File;
7 | import java.io.FileInputStream;
8 | import java.io.FileNotFoundException;
9 | import java.io.FileOutputStream;
10 | import java.io.IOException;
11 | import java.io.OutputStream;
12 | import java.io.RandomAccessFile;
13 | import java.nio.ByteBuffer;
14 | import java.nio.MappedByteBuffer;
15 | import java.nio.channels.FileChannel;
16 | import java.nio.channels.FileChannel.MapMode;
17 |
18 | public class FileUtils {
19 |
20 | /**
21 | * the traditional io way
22 | *
23 | * @param filename
24 | * @return
25 | * @throws IOException
26 | */
27 | public static byte[] toByteArray(String filename) throws IOException {
28 |
29 | File f = new File(filename);
30 | if (!f.exists()) {
31 | throw new FileNotFoundException(filename);
32 | }
33 |
34 | ByteArrayOutputStream bos = new ByteArrayOutputStream((int) f.length());
35 | BufferedInputStream in = null;
36 | try {
37 | in = new BufferedInputStream(new FileInputStream(f));
38 | int buf_size = 1024;
39 | byte[] buffer = new byte[buf_size];
40 | int len = 0;
41 | while (-1 != (len = in.read(buffer, 0, buf_size))) {
42 | bos.write(buffer, 0, len);
43 | }
44 | return bos.toByteArray();
45 | } catch (IOException e) {
46 | e.printStackTrace();
47 | throw e;
48 | } finally {
49 | try {
50 | in.close();
51 | } catch (IOException e) {
52 | e.printStackTrace();
53 | }
54 | bos.close();
55 | }
56 | }
57 |
58 | /**
59 | * NIO way
60 | *
61 | * @param filename
62 | * @return
63 | * @throws IOException
64 | */
65 | public static byte[] toByteArray2(String filename) throws IOException {
66 |
67 | File f = new File(filename);
68 | if (!f.exists()) {
69 | throw new FileNotFoundException(filename);
70 | }
71 |
72 | FileChannel channel = null;
73 | FileInputStream fs = null;
74 | try {
75 | fs = new FileInputStream(f);
76 | channel = fs.getChannel();
77 | ByteBuffer byteBuffer = ByteBuffer.allocate((int) channel.size());
78 | while ((channel.read(byteBuffer)) > 0) {
79 | // do nothing
80 | // System.out.println("reading");
81 | }
82 | return byteBuffer.array();
83 | } catch (IOException e) {
84 | e.printStackTrace();
85 | throw e;
86 | } finally {
87 | try {
88 | channel.close();
89 | } catch (IOException e) {
90 | e.printStackTrace();
91 | }
92 | try {
93 | fs.close();
94 | } catch (IOException e) {
95 | e.printStackTrace();
96 | }
97 | }
98 | }
99 |
100 | /**
101 | * Mapped File way MappedByteBuffer 可以在处理大文件时,提升性能
102 | *
103 | * @param filename
104 | * @return
105 | * @throws IOException
106 | */
107 | public static byte[] toByteArray3(String filename) throws IOException {
108 |
109 | FileChannel fc = null;
110 | try {
111 | fc = new RandomAccessFile(filename, "r").getChannel();
112 | MappedByteBuffer byteBuffer = fc.map(MapMode.READ_ONLY, 0,
113 | fc.size()).load();
114 | System.out.println(byteBuffer.isLoaded());
115 | byte[] result = new byte[(int) fc.size()];
116 | if (byteBuffer.remaining() > 0) {
117 | // System.out.println("remain");
118 | byteBuffer.get(result, 0, byteBuffer.remaining());
119 |
120 | }
121 | return result;
122 | } catch (IOException e) {
123 | e.printStackTrace();
124 | throw e;
125 | } finally {
126 | try {
127 | fc.close();
128 | } catch (IOException e) {
129 | e.printStackTrace();
130 | }
131 | }
132 | }
133 |
134 |
135 | }
136 |
--------------------------------------------------------------------------------
/src/main/java/com/zhuyun/rtp/NaluType.java:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhouyinfei/rtsp-netty-server/d572bcf78d610b7126590d501c800ca30e602e1d/src/main/java/com/zhuyun/rtp/NaluType.java
--------------------------------------------------------------------------------
/src/main/java/com/zhuyun/rtp/RtpUtils.java:
--------------------------------------------------------------------------------
1 | package com.zhuyun.rtp;
2 |
3 | import java.nio.ByteBuffer;
4 | import java.util.ArrayList;
5 | import java.util.HashMap;
6 | import java.util.List;
7 | import java.util.Map;
8 | import java.util.Map.Entry;
9 |
10 | import org.jitsi.service.neomedia.RawPacket;
11 | import org.slf4j.Logger;
12 | import org.slf4j.LoggerFactory;
13 |
14 |
15 | public class RtpUtils {
16 | public static final Logger log = LoggerFactory.getLogger(RtpUtils.class);
17 | private int intervel = 40; //默认发送间隔40ms,帧率1000/40=25
18 | private int seqNum = 1; //h264 rtp的序列号,play的时候使用
19 |
20 | //rtp拆包成nalu
21 | public static byte[] rtpToNaluPack(RawPacket rtpPacket){
22 | //h264码流处理
23 | // if (rtpPacket.getPayloadType() == 96) { //以下处理仅针对H264码流
24 | ByteBuffer bb = null; //存放RTP解析后的NALU的数据
25 |
26 | byte[] rtpPayload = rtpPacket.getPayload();
27 | byte fu_indicator = rtpPayload[0];
28 | byte fu_header = rtpPayload[1];
29 | byte nalu_type = (byte) (fu_indicator & 0x1f);
30 |
31 | if (nalu_type == NaluType.FU_A.getType()) { //FU-A //分片封包模式
32 | byte start_flag = (byte) (fu_header & 0x80);
33 | byte end_flag = (byte) (fu_header & 0x40);
34 | byte nalu_header = (byte) ((fu_indicator & 0xe0) | (fu_header & 0x1f)); //根据fu_indicator和fu_header来重构出nalu_header
35 | if (start_flag != 0) { //第一个分片
36 | bb = ByteBuffer.allocate(rtpPayload.length + 3);
37 | bb.put(new byte[]{0x0, 0x0, 0x0, 0x1});
38 | bb.put(nalu_header);
39 | byte[] dest = new byte[rtpPayload.length-2];
40 | System.arraycopy(rtpPayload, 2, dest, 0, rtpPayload.length-2);
41 | bb.put(dest);
42 | } else if (end_flag != 0) { //最后一个分片
43 | bb = ByteBuffer.allocate(rtpPayload.length-2);
44 | byte[] dest = new byte[rtpPayload.length-2];
45 | System.arraycopy(rtpPayload, 2, dest, 0, rtpPayload.length-2);
46 | bb.put(dest);
47 | } else { //中间分片
48 | bb = ByteBuffer.allocate(rtpPayload.length-2);
49 | byte[] dest = new byte[rtpPayload.length-2];
50 | System.arraycopy(rtpPayload, 2, dest, 0, rtpPayload.length-2);
51 | bb.put(dest);
52 | }
53 | } else if (nalu_type == NaluType.STAP_A.getType()) { //STAP-A //组合封包模式
54 | int srcOffset = 1; //第一个字节是STAP-A头,跳过
55 | int bufferLen = 0;
56 | //先计算需要的ByteBuffer长度,再将内容放进去
57 | while ((rtpPayload.length - srcOffset) > 2) //循环解析RTP,将组合后的NALU取出来,再加上起始码
58 | {
59 | int size = 0; //NALU的长度,2个字节
60 | size |= rtpPayload[srcOffset] << 8;
61 | size |= rtpPayload[srcOffset + 1];
62 |
63 | srcOffset += 2; //将NALU header和NALU payload一起放进去,然后进入下一个循环
64 | bufferLen += (4+size);
65 | srcOffset += size;
66 | }
67 |
68 | srcOffset = 1;
69 | bb = ByteBuffer.allocate(bufferLen);
70 | while ((rtpPayload.length - srcOffset) > 2) //循环解析RTP,将组合后的NALU取出来,再加上起始码
71 | {
72 | int size = 0; //NALU的长度,2个字节
73 | size |= rtpPayload[srcOffset] << 8;
74 | size |= rtpPayload[srcOffset + 1];
75 |
76 | srcOffset += 2; //将NALU header和NALU payload一起放进去,然后进入下一个循环
77 | byte[] dest = new byte[size];
78 | System.arraycopy(rtpPayload, srcOffset, dest, 0, size);
79 |
80 | bb.put(new byte[]{0x0, 0x0, 0x0, 0x1}); //NALU的起始码
81 | bb.put(dest);
82 |
83 | srcOffset += size;
84 | }
85 |
86 | } else if (nalu_type >= 1 && nalu_type <= 23) { //单一NAL 单元模式
87 | bb = ByteBuffer.allocate(rtpPayload.length + 4); //将整个rtpPayload一起放进去
88 | bb.put(new byte[]{0x0, 0x0, 0x0, 0x1});
89 | bb.put(rtpPayload);
90 | } else {
91 | log.debug("rtpToNaluPack-----Unsupport nalu type! {}", nalu_type);
92 | }
93 |
94 | if (bb != null) {
95 | return bb.array();
96 | }
97 | // }
98 | return null;
99 | }
100 |
101 | //rtp拆包成nalu h265
102 | public static byte[] rtpToNaluH265Pack(RawPacket rtpPacket){
103 | //h265码流处理
104 | // if (rtpPacket.getPayloadType() == 96) { //以下处理仅针对H265码流
105 | ByteBuffer bb = null; //存放RTP解析后的NALU的数据
106 |
107 | byte[] rtpPayload = rtpPacket.getPayload();
108 | byte fu_header0 = rtpPayload[0];
109 | byte nalu_type = (byte) ((fu_header0>>1) & 0x3f); //获取NALU TYPE
110 |
111 | // System.out.println("nalu_type=" + nalu_type);
112 | if (nalu_type == 49) { //分片封包模式
113 | byte fu_header2 = rtpPayload[2];
114 | byte start_flag = (byte) (fu_header2 & 0x80); //是否起始片
115 | byte end_flag = (byte) (fu_header2 & 0x40); //是否结束片
116 | nalu_type = (byte) (fu_header2&0x3F); //nalu type
117 | byte nalu_header0 = (byte) (nalu_type<<1);
118 | byte nalu_header1 = 0x01; //固定值
119 | if (start_flag != 0) { //第一个分片
120 | bb = ByteBuffer.allocate(rtpPayload.length + 3);
121 | bb.put(new byte[]{0x0, 0x0, 0x0, 0x1});
122 | bb.put(nalu_header0);
123 | bb.put(nalu_header1);
124 | byte[] dest = new byte[rtpPayload.length-3];
125 | System.arraycopy(rtpPayload, 3, dest, 0, rtpPayload.length-3);
126 | bb.put(dest);
127 | } else if (end_flag != 0) { //最后一个分片
128 | bb = ByteBuffer.allocate(rtpPayload.length-3);
129 | byte[] dest = new byte[rtpPayload.length-3];
130 | System.arraycopy(rtpPayload, 3, dest, 0, rtpPayload.length-3);
131 | bb.put(dest);
132 | } else { //中间分片
133 | bb = ByteBuffer.allocate(rtpPayload.length-3);
134 | byte[] dest = new byte[rtpPayload.length-3];
135 | System.arraycopy(rtpPayload, 3, dest, 0, rtpPayload.length-3);
136 | bb.put(dest);
137 | }
138 | } else if (nalu_type == 48) { //组合封包模式
139 | int srcOffset = 2; //第一个字节是STAP-A头,跳过
140 | int bufferLen = 0;
141 | //先计算需要的ByteBuffer长度,再将内容放进去
142 | while ((rtpPayload.length - srcOffset) > 2) //循环解析RTP,将组合后的NALU取出来,再加上起始码
143 | {
144 | int size = 0; //NALU的长度,2个字节
145 | size |= rtpPayload[srcOffset] << 8;
146 | size |= rtpPayload[srcOffset + 1];
147 |
148 | srcOffset += 2; //将NALU header和NALU payload一起放进去,然后进入下一个循环
149 | bufferLen += (4+size);
150 | srcOffset += size;
151 | }
152 |
153 | srcOffset = 2;
154 | bb = ByteBuffer.allocate(bufferLen);
155 | while ((rtpPayload.length - srcOffset) > 2) //循环解析RTP,将组合后的NALU取出来,再加上起始码
156 | {
157 | int size = 0; //NALU的长度,2个字节
158 | size |= rtpPayload[srcOffset] << 8;
159 | size |= rtpPayload[srcOffset + 1];
160 |
161 | srcOffset += 2; //将NALU header和NALU payload一起放进去,然后进入下一个循环
162 | byte[] dest = new byte[size];
163 | System.arraycopy(rtpPayload, srcOffset, dest, 0, size);
164 |
165 | bb.put(new byte[]{0x0, 0x0, 0x0, 0x1}); //NALU的起始码
166 | bb.put(dest);
167 |
168 | srcOffset += size;
169 | }
170 |
171 | } else if (nalu_type == 1 || nalu_type == 19 || nalu_type == 32 || nalu_type == 33 ||
172 | nalu_type == 34 || nalu_type == 39) { //单一NAL 单元模式
173 | bb = ByteBuffer.allocate(rtpPayload.length + 4); //将整个rtpPayload一起放进去
174 | bb.put(new byte[]{0x0, 0x0, 0x0, 0x1});
175 | bb.put(rtpPayload);
176 | } else {
177 | log.debug("rtpToNaluH265Pack-----Unsupport nalu type!");
178 | }
179 |
180 | if (bb != null) {
181 | return bb.array();
182 | }
183 | // }
184 | return null;
185 | }
186 |
187 | //rtp拆包成ADTS列表
188 | public static List rtpToAdtsPack(RawPacket rtpPacket, int clockRate){
189 | //aac码流处理
190 | // if (rtpPacket.getPayloadType() == 96) { //以下处理仅针对H264码流
191 | byte[] rtpPayload = rtpPacket.getPayload();
192 | int aUHeadersLength = ((rtpPayload[0]&0xFF)<<16) + (rtpPayload[1]&0xFF);
193 | int auCount = aUHeadersLength/16; //AAC帧的数量
194 | int index = 2 + auCount*2; //上一次解析到的位置
195 | List adtsList = new ArrayList(); //ADTS帧列表
196 |
197 | for (int i = 2; i < 2+2*auCount; i+=2) { //遍历所有auHeader
198 | int aacDataLen = ((rtpPayload[i]&0xFF)<<5) + ((rtpPayload[i+1]&0xF8)>>3); //AAC的数据长度(不包括header)
199 | byte[] aacData = new byte[aacDataLen]; //AAC的数据
200 | System.arraycopy(rtpPayload, index, aacData, 0, aacDataLen);
201 | index += aacDataLen;
202 | byte[] adts = addAdtsHeader(aacData, clockRate);
203 | adtsList.add(adts);
204 | }
205 |
206 | return adtsList;
207 | // }
208 | // return null;
209 | }
210 |
211 | //nalu封装成rtp
212 | public Map naluToRtpPack(byte[] nalu, int ssrc, int fps, int rtpTimestamp){
213 | byte[] pcData = nalu; //两个起始码(00 00 00 01)之间的NALU数据
214 | int mtu = 1400; //最大传输单元
215 | int iLen = pcData.length; //NALU总长度
216 | ByteBuffer bb = null;
217 | // List rtpList = new ArrayList(); //封装后的rtp包集合
218 | Map rtpMap = new HashMap(); //封装后的rtp包集合
219 |
220 | if (iLen > mtu) { //超过MTU 分片封包模式
221 | byte start_flag = (byte) 0x80;
222 | byte mid_flag = 0x00;
223 | byte end_flag = 0x40;
224 |
225 | //获取帧头数据,1byte
226 | byte nalu_type = (byte) (pcData[0] & 0x1f); //获取NALU的5bit 帧类型
227 | byte nal_ref_idc = (byte) (pcData[0] & 0x60); //获取NALU的2bit 帧重要程度 00 可以丢 11不能丢
228 |
229 | //组装FU-A帧头数据 2byte
230 | byte fu_identifier = (byte) (nal_ref_idc + 28); //F为0 1bit,nri上面获取到2bit,28为FU-A分片类型5bit
231 | byte fu_header = nalu_type; //低5位和naluHeader相同
232 | boolean bFirst = true; //是否是第一个分片
233 | boolean mark = false; //是否是最后一个分片
234 | int nOffset = 1; //偏移量,跳过第一个字节naluHeader
235 | while (!mark) {
236 | if (seqNum == 65535) { //如果超过2个字节的上限,则重置为1
237 | seqNum = 1;
238 | }
239 | bb = ByteBuffer.allocate(mtu + 2);
240 | if (iLen < nOffset + mtu) { //是否拆分结束, 最后一个分片
241 | mtu = iLen - nOffset;
242 |
243 | mark = true;
244 | fu_header = (byte) (end_flag + nalu_type);
245 | } else {
246 | if (bFirst == true) { //第一个分片
247 | fu_header = (byte) (start_flag + nalu_type);
248 | bFirst = false;
249 | } else { //或者中间分片
250 | fu_header = (byte) (mid_flag + nalu_type);
251 | }
252 | }
253 | bb.put(fu_identifier);
254 | bb.put(fu_header);
255 | byte[] dest = new byte[mtu];
256 | System.arraycopy(pcData, nOffset, dest, 0, mtu);
257 | bb.put(dest);
258 | nOffset += mtu;
259 | byte[] rtpPackage = makeH264Rtp(bb.array(), mark, seqNum, rtpTimestamp, ssrc);
260 | // rtpList.add(rtpPackage);
261 | rtpMap.put(seqNum, rtpPackage);
262 | seqNum ++;
263 | }
264 | } else { //单一NAL 单元模式, 不使用组合模式。mark始终为false
265 | //从SPS中获取帧率
266 | // byte nalu_type = (byte) (pcData[0] & 0x1f); //获取NALU的5bit 帧类型
267 | // if (nalu_type == 7) { //如果是sps
268 | // SpsInfoStruct info = new SpsInfoStruct();
269 | // SpsUtils.h264ParseSps(nalu, nalu.length, info);
270 | // if (info.fps != 0) { //如果sps中存在帧率信息,则使用。如果没有,则使用默认帧率
271 | // intervel = 1000/info.fps;
272 | // }
273 | // }
274 |
275 | if (seqNum == 65535) { //如果超过2个字节的上限,则重置为1
276 | seqNum = 1;
277 | }
278 |
279 | //根据rtsp传过来的fps参数
280 | if (fps != 0) {
281 | intervel = 1000/fps;
282 | }
283 |
284 | byte[] rtpPackage = makeH264Rtp(pcData, false, seqNum, rtpTimestamp, ssrc);
285 | // rtpList.add(rtpPackage);
286 | rtpMap.put(seqNum, rtpPackage);
287 | seqNum ++;
288 | }
289 |
290 | try {
291 | Thread.sleep(intervel); //根据帧率延时发送
292 | } catch (InterruptedException e) {
293 | e.printStackTrace();
294 | }
295 | return rtpMap;
296 | }
297 |
298 | //nalu h265封装成rtp
299 | public Map naluH265ToRtpPack(byte[] nalu, int ssrc, int fps, int rtpTimestamp){
300 | byte[] pcData = nalu; //两个起始码(00 00 00 01)之间的NALU数据
301 | int mtu = 1400; //最大传输单元
302 | int iLen = pcData.length; //NALU总长度
303 | ByteBuffer bb = null;
304 |
305 | // List rtpList = new ArrayList(); //封装后的rtp包集合
306 | Map rtpMap = new HashMap(); //封装后的rtp包集合
307 | int seqNum = 1; //h264 rtp的序列号,play的时候使用
308 | int intervel = 40; //默认发送间隔40ms,帧率1000/40=25
309 |
310 | if (iLen > mtu) { //超过MTU 分片封包模式
311 | byte start_flag = (byte) 0x80;
312 | byte mid_flag = 0x00;
313 | byte end_flag = 0x40;
314 |
315 | byte nalu_type = (byte) ((pcData[0]>>1) & 0x3f); //获取NALU的6bit 帧类型
316 |
317 | //组装FU-A帧头数据 3byte
318 | byte fu_header0 = 0x62; //第一字节固定
319 | byte fu_header1 = 0x01; //第二字节固定
320 | byte fu_header2 = nalu_type; //第三字节的低6位是nalu_type
321 |
322 | boolean bFirst = true; //是否是第一个分片
323 | boolean mark = false; //是否是最后一个分片
324 | int nOffset = 2; //偏移量,跳过2个字节naluHeader
325 | while (!mark) {
326 | if (seqNum == 65535) { //如果超过2个字节的上限,则重置为1
327 | seqNum = 1;
328 | }
329 |
330 | bb = ByteBuffer.allocate(mtu + 3);
331 | if (iLen < nOffset + mtu) { //是否拆分结束, 最后一个分片
332 | mtu = iLen - nOffset;
333 |
334 | mark = true;
335 | fu_header2 = (byte) (end_flag + nalu_type);
336 | } else {
337 | if (bFirst == true) { //第一个分片
338 | fu_header2 = (byte) (start_flag + nalu_type);
339 | bFirst = false;
340 | } else { //或者中间分片
341 | fu_header2 = (byte) (mid_flag + nalu_type);
342 | }
343 | }
344 | bb.put(fu_header0);
345 | bb.put(fu_header1);
346 | bb.put(fu_header2);
347 | byte[] dest = new byte[mtu];
348 | System.arraycopy(pcData, nOffset, dest, 0, mtu);
349 | bb.put(dest);
350 | nOffset += mtu;
351 | byte[] rtpPackage = makeH264Rtp(bb.array(), mark, seqNum, rtpTimestamp, ssrc);
352 | // rtpList.add(rtpPackage);
353 | rtpMap.put(seqNum, rtpPackage);
354 | seqNum ++;
355 | }
356 | } else { //单一NAL 单元模式, 不使用组合模式。mark始终为true
357 | //从SPS中获取帧率
358 | // byte nalu_type = (byte) (pcData[0] & 0x1f); //获取NALU的5bit 帧类型
359 | // if (nalu_type == 7) { //如果是sps
360 | // SpsInfoStruct info = new SpsInfoStruct();
361 | // SpsUtils.h264ParseSps(nalu, nalu.length, info);
362 | // if (info.fps != 0) { //如果sps中存在帧率信息,则使用。如果没有,则使用默认帧率
363 | // intervel = 1000/info.fps;
364 | // }
365 | // }
366 |
367 | if (seqNum == 65535) { //如果超过2个字节的上限,则重置为1
368 | seqNum = 1;
369 | }
370 |
371 | //根据rtsp传过来的fps参数
372 | if (fps != 0) {
373 | intervel = 1000/fps;
374 | }
375 |
376 | byte[] rtpPackage = makeH264Rtp(pcData, true, seqNum, rtpTimestamp, ssrc);
377 | // rtpList.add(rtpPackage);
378 | rtpMap.put(seqNum, rtpPackage);
379 | seqNum ++;
380 | }
381 |
382 | try {
383 | Thread.sleep(intervel); //根据帧率延时发送
384 | } catch (InterruptedException e) {
385 | e.printStackTrace();
386 | }
387 | return rtpMap;
388 | }
389 |
390 | //aac data封装成rtp
391 | public byte[] aacToRtpPack(byte[] aacData, int seqNum, int ssrc, int rtpTimestamp){
392 | int aacLen = aacData.length;
393 | ByteBuffer bb = ByteBuffer.allocate(aacData.length + 4);
394 | bb.put((byte) 0x00);
395 | bb.put((byte) 0x10);
396 | bb.put((byte) ((aacLen>>5)&0xFF)); //取长度的高8位
397 | bb.put((byte) ((aacLen&0x1F)<<3)); //取长度的低5位
398 | bb.put(aacData);
399 |
400 | return makeAacRtp(bb.array(), true, seqNum, rtpTimestamp, ssrc);
401 | }
402 |
403 | public byte[] makeH264Rtp(byte[] pcData, boolean mark, int seqNum, int timestamp, int ssrc){
404 | byte b;
405 | if (mark) {
406 | b = (byte) 0xE0;
407 | } else {
408 | b = (byte) 0x60;
409 | }
410 |
411 | ByteBuffer bb = ByteBuffer.allocate(pcData.length + 12);
412 | bb.put((byte) 0x80); //V、P、X、CC, 1000 0000
413 | bb.put(b); //mark 、payloadType(96)
414 | bb.putShort((short) seqNum);
415 | bb.putInt(timestamp);
416 | bb.putInt(ssrc);
417 | bb.put(pcData);
418 | return bb.array();
419 | }
420 |
421 | public byte[] makeAacRtp(byte[] pcData, boolean mark, int seqNum, int timestamp, int ssrc){
422 | byte b;
423 | if (mark) { //aac payload类型是97
424 | b = (byte) 0xE1;
425 | } else {
426 | b = (byte) 0x61;
427 | }
428 |
429 | ByteBuffer bb = ByteBuffer.allocate(pcData.length + 12);
430 | bb.put((byte) 0x80); //V、P、X、CC, 1000 0000
431 | bb.put(b); //mark 、payloadType(96)
432 | bb.putShort((short) seqNum);
433 | bb.putInt(timestamp);
434 | bb.putInt(ssrc);
435 | bb.put(pcData);
436 | return bb.array();
437 | }
438 |
439 | //构造ADTS帧,添加ADTS头
440 | public static byte[] addAdtsHeader(byte[] aacData, int clockRate){
441 | byte samplingIndex = getSamplingIndex(clockRate); //采样率下标
442 |
443 | ByteBuffer bb = ByteBuffer.allocate(aacData.length + 7);
444 | bb.put((byte) 0xFF);
445 | bb.put((byte) 0xF1);
446 | bb.put((byte) ((0x01<<6) + (samplingIndex<<2)));
447 |
448 | short aacFrameLength = (short) ((aacData.length + 7)&0x1FFF); //取低13位
449 | bb.put((byte) (((aacFrameLength>>11)&0x0F) + 0x40));
450 | bb.put((byte) ((aacFrameLength>>3)&0xFF)); //取低3位往后数8位
451 | bb.put((byte) (((aacFrameLength&0x07)<<5) + 0x02)); //取低3位作为结果的高3位,后5位固定:0 0010
452 |
453 | bb.put((byte) 0x40);
454 | bb.put(aacData);
455 | return bb.array();
456 | }
457 |
458 | //获取采样率
459 | public static int getSampling(byte samp){
460 | int sampling = 16000;
461 | switch (samp) {
462 | case 0x0:
463 | sampling = 96000;
464 | break;
465 | case 0x1:
466 | sampling = 88200;
467 | break;
468 | case 0x2:
469 | sampling = 64000;
470 | break;
471 | case 0x3:
472 | sampling = 48000;
473 | break;
474 | case 0x4:
475 | sampling = 44100;
476 | break;
477 | case 0x5:
478 | sampling = 32000;
479 | break;
480 | case 0x6:
481 | sampling = 24000;
482 | break;
483 | case 0x7:
484 | sampling = 22050;
485 | break;
486 | case 0x8:
487 | sampling = 16000;
488 | break;
489 | case 0x9:
490 | sampling = 12000;
491 | break;
492 | case 0xa:
493 | sampling = 11025;
494 | break;
495 | case 0xb:
496 | sampling = 8000;
497 | break;
498 | case 0xc:
499 | sampling = 7350;
500 | break;
501 | default:
502 | break;
503 | }
504 | return sampling;
505 | }
506 |
507 | //获取采样率index
508 | public static byte getSamplingIndex(int sampling){
509 | byte index = 0x8;
510 | switch (sampling) {
511 | case 96000:
512 | index = 0x0;
513 | break;
514 | case 88200:
515 | index = 0x1;
516 | break;
517 | case 64000:
518 | index = 0x2;
519 | break;
520 | case 48000:
521 | index = 0x3;
522 | break;
523 | case 44100:
524 | index = 0x4;
525 | break;
526 | case 32000:
527 | index = 0x5;
528 | break;
529 | case 24000:
530 | index = 0x6;
531 | break;
532 | case 22050:
533 | index = 0x7;
534 | break;
535 | case 16000:
536 | index = 0x8;
537 | break;
538 | case 12000:
539 | index = 0x9;
540 | break;
541 | case 11025:
542 | index = 0xa;
543 | break;
544 | case 8000:
545 | index = 0xb;
546 | break;
547 | case 7350:
548 | index = 0xc;
549 | break;
550 | default:
551 | break;
552 | }
553 | return index;
554 | }
555 |
556 | }
557 |
--------------------------------------------------------------------------------
/src/main/java/com/zhuyun/rtp/SpsBitStream.java:
--------------------------------------------------------------------------------
1 | package com.zhuyun.rtp;
2 |
3 | import java.util.Arrays;
4 |
5 | /**
6 | * SPS帧
7 | * @author zhouyinfei
8 | *
9 | */
10 | public class SpsBitStream {
11 | byte[] data; //sps数据
12 | int size; //sps数据大小
13 | int index; //当前计算位所在的位置标记
14 |
15 | @Override
16 | public String toString() {
17 | return "SpsBitStream [data=" + Arrays.toString(data) + ", size=" + size
18 | + ", index=" + index + "]";
19 | }
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/java/com/zhuyun/rtp/SpsInfoStruct.java:
--------------------------------------------------------------------------------
1 | package com.zhuyun.rtp;
2 |
3 | public class SpsInfoStruct {
4 | int profile_idc;
5 | int level_idc;
6 |
7 | int width;
8 | int height;
9 | int fps; //SPS中可能不包含FPS信息
10 |
11 | @Override
12 | public String toString() {
13 | return "SpsInfoStruct [profile_idc=" + profile_idc + ", level_idc="
14 | + level_idc + ", width=" + width + ", height=" + height
15 | + ", fps=" + fps + "]";
16 | }
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/src/main/java/com/zhuyun/rtp/SpsUtils.java:
--------------------------------------------------------------------------------
1 | package com.zhuyun.rtp;
2 |
3 |
4 | public class SpsUtils {
5 |
6 | //视频帧率(FPS)获取
7 | /**
8 | * 视频帧率信息在SPS的VUI parameters syntax中,需要根据time_scale、fixed_frame_rate_flag计算得到:
9 | * fps = time_scale / num_units_in_tick。但是需要判断参数timing_info_present_flag是否存在,
10 | * 若不存在表示FPS在信息流中无法获取。同时还存在另外一种情况:fixed_frame_rate_flag为1时,
11 | * 两个连续图像的HDR输出时间频率为单位,获取的fps是实际的2倍。
12 | */
13 | public static int getFPS(SpsBitStream bs, SpsInfoStruct info){
14 | int timing_info_present_flag = u(bs, 1);
15 | if (timing_info_present_flag != 0) {
16 | int num_units_in_tick = u(bs, 32);
17 | int time_scale = u(bs, 32);
18 | int fixed_frame_rate_flag = u(bs, 1);
19 | info.fps = (int)((float)time_scale / (float)num_units_in_tick);
20 | if (fixed_frame_rate_flag != 0) {
21 | info.fps = info.fps/2;
22 | }
23 | }
24 | return 0;
25 | }
26 |
27 | /**
28 | 移除H264的NAL防竞争码(0x03)
29 | @param data sps数据
30 | @param dataSize sps数据大小
31 | */
32 | public static void delEmulationPrevention(byte[] data, int dataSize)
33 | {
34 | int dataSizeTemp = dataSize;
35 | for (int i=0, j=0; i<(dataSizeTemp-2); i++) {
36 | int val = (data[i]^0x0) + (data[i+1]^0x0) + (data[i+2]^0x3); //检测是否是竞争码
37 | if (val == 0) {
38 | for (j=i+2; j= (bs.size * 8); //位偏移已经超出数据
65 | }
66 |
67 | /**
68 | 读取从起始位开始的BitCount个位所表示的值
69 | @param bs sps_bit_stream数据
70 | @param bitCount bit位个数(从低到高)
71 | @return value
72 | */
73 | public static int u(SpsBitStream bs, int bitCount)
74 | {
75 | int val = 0;
76 | for (int i=0; i> (bs.index % 8))) != 0) { //计算index所在的位是否为1
82 | val |= 1;
83 | }
84 | bs.index++; //递增当前起始位(表示该位已经被计算,在后面的计算过程中不需要再次去计算所在的起始位索引,缺点:后面每个bit位都需要去位移)
85 | }
86 |
87 | return val;
88 | }
89 |
90 | /**
91 | 读取无符号哥伦布编码值(UE)
92 | #2^LeadingZeroBits - 1 + (xxx)
93 | @param bs sps_bit_stream数据
94 | @return value
95 | */
96 | public static int ue(SpsBitStream bs)
97 | {
98 | int zeroNum = 0;
99 | while (u(bs, 1) == 0 && !eof(bs) && zeroNum < 32) {
100 | zeroNum ++;
101 | }
102 |
103 | return (int)((1 << zeroNum) - 1 + u(bs, zeroNum));
104 | }
105 |
106 | /**
107 | 读取有符号哥伦布编码值(SE)
108 | #(-1)^(k+1) * Ceil(k/2)
109 |
110 | @param bs sps_bit_stream数据
111 | @return value
112 | */
113 | public static int se(SpsBitStream bs)
114 | {
115 | int ueVal = (int)ue(bs);
116 | double k = ueVal;
117 |
118 | int seVal = (int)Math.ceil(k / 2); //ceil:返回大于或者等于指定表达式的最小整数
119 | if (ueVal % 2 == 0) { //偶数取反,即(-1)^(k+1)
120 | seVal = -seVal;
121 | }
122 |
123 | return seVal;
124 | }
125 |
126 | /**
127 | 视频可用性信息(Video usability information)解析
128 | @param bs sps_bit_stream数据
129 | @param info sps解析之后的信息数据及结构体
130 | @see E.1.1 VUI parameters syntax
131 | */
132 | public static void vuiParaParse(SpsBitStream bs, SpsInfoStruct info)
133 | {
134 | int aspect_ratio_info_present_flag = u(bs, 1);
135 | if (aspect_ratio_info_present_flag != 0) {
136 | int aspect_ratio_idc = u(bs, 8);
137 | if (aspect_ratio_idc == 255) { //Extended_SAR
138 | u(bs, 16); //sar_width
139 | u(bs, 16); //sar_height
140 | }
141 | }
142 |
143 | int overscan_info_present_flag = u(bs, 1);
144 | if (overscan_info_present_flag != 0) {
145 | u(bs, 1); //overscan_appropriate_flag
146 | }
147 |
148 | int video_signal_type_present_flag = u(bs, 1);
149 | if (video_signal_type_present_flag != 0) {
150 | u(bs, 3); //video_format
151 | u(bs, 1); //video_full_range_flag
152 | int colour_description_present_flag = u(bs, 1);
153 | if (colour_description_present_flag != 0) {
154 | u(bs, 8); //colour_primaries
155 | u(bs, 8); //transfer_characteristics
156 | u(bs, 8); //matrix_coefficients
157 | }
158 | }
159 |
160 | int chroma_loc_info_present_flag = u(bs, 1);
161 | if (chroma_loc_info_present_flag != 0) {
162 | ue(bs); //chroma_sample_loc_type_top_field
163 | ue(bs); //chroma_sample_loc_type_bottom_field
164 | }
165 |
166 | int timing_info_present_flag = u(bs, 1);
167 | if (timing_info_present_flag != 0) {
168 | int num_units_in_tick = u(bs, 32);
169 | int time_scale = u(bs, 32);
170 | int fixed_frame_rate_flag = u(bs, 1);
171 |
172 | info.fps = (int)((float)time_scale / (float)num_units_in_tick);
173 | if (fixed_frame_rate_flag != 0) {
174 | info.fps = info.fps/2;
175 | }
176 | }
177 |
178 | int nal_hrd_parameters_present_flag = u(bs, 1);
179 | if (nal_hrd_parameters_present_flag != 0) {
180 | //hrd_parameters() //see E.1.2 HRD parameters syntax
181 | }
182 |
183 | //后面代码需要hrd_parameters()函数接口实现才有用
184 | int vcl_hrd_parameters_present_flag = u(bs, 1);
185 | if (vcl_hrd_parameters_present_flag != 0) {
186 | //hrd_parameters()
187 | }
188 | if ((nal_hrd_parameters_present_flag !=0) || (vcl_hrd_parameters_present_flag != 0)) {
189 | u(bs, 1); //low_delay_hrd_flag
190 | }
191 |
192 | u(bs, 1); //pic_struct_present_flag
193 | int bitstream_restriction_flag = u(bs, 1);
194 | if (bitstream_restriction_flag != 0) {
195 | u(bs, 1); //motion_vectors_over_pic_boundaries_flag
196 | ue(bs); //max_bytes_per_pic_denom
197 | ue(bs); //max_bits_per_mb_denom
198 | ue(bs); //log2_max_mv_length_horizontal
199 | ue(bs); //log2_max_mv_length_vertical
200 | ue(bs); //max_num_reorder_frames
201 | ue(bs); //max_dec_frame_buffering
202 | }
203 | }
204 |
205 | //See 7.3.1 NAL unit syntax
206 | //See 7.3.2.1.1 Sequence parameter set data syntax
207 | public static int h264ParseSps(byte[] data, int dataSize, SpsInfoStruct info)
208 | {
209 | if ((data == null) || dataSize <= 0 || (info== null)) return 0;
210 | int ret = 0;
211 |
212 | byte[] dataBuf = new byte[dataSize];
213 | System.arraycopy(data, 0, dataBuf, 0, dataSize); //重新拷贝一份数据,防止移除竞争码时对原数据造成影响
214 |
215 | delEmulationPrevention(dataBuf, dataSize);
216 |
217 | // sps_bit_stream bs = {0};
218 |
219 | SpsBitStream bs = new SpsBitStream();
220 | spsBsInit(bs, dataBuf, dataSize); //初始化SPS数据流结构体
221 |
222 | u(bs, 1); //forbidden_zero_bit
223 | u(bs, 2); //nal_ref_idc
224 | int nal_unit_type = u(bs, 5);
225 |
226 | if (nal_unit_type == 0x7) { //Nal SPS Flag
227 | info.profile_idc = u(bs, 8);
228 | u(bs, 1); //constraint_set0_flag
229 | u(bs, 1); //constraint_set1_flag
230 | u(bs, 1); //constraint_set2_flag
231 | u(bs, 1); //constraint_set3_flag
232 | u(bs, 1); //constraint_set4_flag
233 | u(bs, 1); //constraint_set4_flag
234 | u(bs, 2); //reserved_zero_2bits
235 | info.level_idc = u(bs, 8);
236 |
237 | ue(bs); //seq_parameter_set_id
238 |
239 | int chroma_format_idc = 1; //摄像机出图大部分格式是4:2:0
240 | if (info.profile_idc == 100 || info.profile_idc == 110 || info.profile_idc == 122 ||
241 | info.profile_idc == 244 || info.profile_idc == 44 || info.profile_idc == 83 ||
242 | info.profile_idc == 86 || info.profile_idc == 118 || info.profile_idc == 128 ||
243 | info.profile_idc == 138 || info.profile_idc == 139 || info.profile_idc == 134 || info.profile_idc == 135) {
244 | chroma_format_idc = ue(bs);
245 | if (chroma_format_idc == 3) {
246 | u(bs, 1); //separate_colour_plane_flag
247 | }
248 |
249 | ue(bs); //bit_depth_luma_minus8
250 | ue(bs); //bit_depth_chroma_minus8
251 | u(bs, 1); //qpprime_y_zero_transform_bypass_flag
252 | int seq_scaling_matrix_present_flag = u(bs, 1);
253 | if (seq_scaling_matrix_present_flag != 0) {
254 | int[] seq_scaling_list_present_flag = new int[8];
255 | for (int i=0; i<((chroma_format_idc != 3)?8:12); i++) {
256 | seq_scaling_list_present_flag[i] = u(bs, 1);
257 | if (seq_scaling_list_present_flag[i] != 0) {
258 | if (i < 6) { //scaling_list(ScalingList4x4[i], 16, UseDefaultScalingMatrix4x4Flag[i])
259 | } else { //scaling_list(ScalingList8x8[i − 6], 64, UseDefaultScalingMatrix8x8Flag[i − 6] )
260 | }
261 | }
262 | }
263 | }
264 | }
265 |
266 | ue(bs); //log2_max_frame_num_minus4
267 | int pic_order_cnt_type = ue(bs);
268 | if (pic_order_cnt_type == 0) {
269 | ue(bs); //log2_max_pic_order_cnt_lsb_minus4
270 | } else if (pic_order_cnt_type == 1) {
271 | u(bs, 1); //delta_pic_order_always_zero_flag
272 | se(bs); //offset_for_non_ref_pic
273 | se(bs); //offset_for_top_to_bottom_field
274 |
275 | int num_ref_frames_in_pic_order_cnt_cycle = ue(bs);
276 | int[] offset_for_ref_frame = new int[num_ref_frames_in_pic_order_cnt_cycle * 4];
277 | for (int i = 0; i> sinkmap = new ConcurrentHashMap<>();
9 |
10 | public static Map GetStream(String name){
11 | if (sinkmap.containsKey(name)){
12 | return sinkmap.get(name);
13 | }
14 | Map map = new ConcurrentHashMap<>();
15 | Map retmap = sinkmap.putIfAbsent(name, map);
16 | return retmap == null ? map : retmap;
17 | }
18 |
19 | public static void EnterStream(String name, StreamFrameSink sink){
20 | GetStream(name).put(sink, sink);
21 | }
22 | public static void LeaveStream(String name, StreamFrameSink sink){
23 | GetStream(name).remove(sink);
24 | }
25 |
26 | public static void WriteFrame(Map map, StreamFrame frame){
27 | //System.out.printf("input thread %s\n", Thread.currentThread().getName());
28 | for(StreamFrameSink key : map.keySet()){
29 | key.writeFrame(frame);
30 | }
31 | }
32 | public static Set GetStreams()
33 | {
34 | return sinkmap.keySet();
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/main/java/com/zhuyun/transform/RetransmissionRequesterDelegate.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright @ 2017 Atlassian Pty Ltd
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.zhuyun.transform;
17 |
18 | import io.netty.buffer.ByteBuf;
19 | import io.netty.buffer.Unpooled;
20 | import io.netty.channel.Channel;
21 | import io.netty.channel.socket.DatagramPacket;
22 |
23 | import java.io.*;
24 | import java.net.InetSocketAddress;
25 | import java.util.*;
26 | import java.util.concurrent.ConcurrentHashMap;
27 |
28 | import org.jetbrains.annotations.*;
29 | import org.jitsi.impl.neomedia.rtcp.*;
30 | import org.jitsi.service.neomedia.*;
31 | import org.jitsi.util.*;
32 | import org.jitsi.utils.*;
33 | import org.jitsi.utils.concurrent.*;
34 | import org.jitsi.utils.logging.*;
35 |
36 | import com.zhuyun.RtspNettyServer;
37 | import com.zhuyun.handler.RtpHandler;
38 | import com.zhuyun.handler.RtspHandler;
39 |
40 | /**
41 | * Detects lost RTP packets for a particular RtpChannel and requests
42 | * their retransmission by sending RTCP NACK packets.
43 | *
44 | * @author Boris Grozev
45 | * @author George Politis
46 | * @author bbaldino
47 | */
48 | public class RetransmissionRequesterDelegate
49 | implements RecurringRunnable
50 | {
51 | /**
52 | * If more than MAX_MISSING consecutive packets are lost, we will
53 | * not request retransmissions for them, but reset our state instead.
54 | */
55 | public static final int MAX_MISSING = 100;
56 |
57 | /**
58 | * The maximum number of retransmission requests to be sent for a single
59 | * RTP packet.
60 | */
61 | // public static final int MAX_REQUESTS = 10;
62 | public static final int MAX_REQUESTS = 3;
63 |
64 | /**
65 | * The interval after which another retransmission request will be sent
66 | * for a packet, unless it arrives. Ideally this should not be a constant,
67 | * but should be based on the RTT to the endpoint.
68 | */
69 | // public static final int RE_REQUEST_AFTER_MILLIS = 150;
70 | public static final int RE_REQUEST_AFTER_MILLIS = RtspNettyServer.SCHEDULE_RTCP_SR_TIME;
71 |
72 | /**
73 | * The interval we'll ask the {@link RecurringRunnableExecutor} to check back
74 | * in if there is no current work
75 | * TODO(brian): i think we should actually be able to get rid of this and
76 | * just rely on scheduled work and the 'work ready now' callback
77 | */
78 | public static final long WAKEUP_INTERVAL_MILLIS = 1000;
79 |
80 | /**
81 | * The Logger used by the RetransmissionRequesterDelegate class
82 | * and its instances to print debug information.
83 | */
84 | private static final Logger logger
85 | = Logger.getLogger(RetransmissionRequesterDelegate.class);
86 |
87 | /**
88 | * Maps an SSRC to the Requester instance corresponding to it.
89 | * TODO: purge these somehow (RTCP BYE? Timeout?)
90 | */
91 | public final Map requesters = new ConcurrentHashMap<>();
92 |
93 | /**
94 | * The {@link MediaStream} that this instance belongs to.
95 | */
96 | // private final MediaStream stream;
97 |
98 |
99 | /**
100 | * The SSRC which will be used as Packet Sender SSRC in NACK packets sent
101 | * by this {@code RetransmissionRequesterDelegate}.
102 | */
103 | private long senderSsrc = -1;
104 |
105 |
106 | protected final TimeProvider timeProvider;
107 |
108 | /**
109 | * A callback which allows this class to signal it has nack work that is ready
110 | * to be run
111 | */
112 | protected Runnable workReadyCallback = null;
113 |
114 | /**
115 | * Initializes a new RetransmissionRequesterDelegate for the given
116 | * RtpChannel.
117 | * @param stream the {@link MediaStream} that the instance belongs to.
118 | */
119 | public RetransmissionRequesterDelegate(MediaStream stream, TimeProvider timeProvider)
120 | {
121 | // this.stream = stream;
122 | this.timeProvider = timeProvider;
123 | }
124 |
125 | /**
126 | * Notify this requester that a packet has been received
127 | */
128 | public void packetReceived(long ssrc, int seqNum)
129 | {
130 | // TODO(gp) Don't NACK higher temporal layers.
131 | Requester requester = getOrCreateRequester(ssrc);
132 | // If the reception of this packet resulted in there being work that
133 | // is ready to be done now, fire the work ready callback
134 | if (requester.received(seqNum))
135 | {
136 | if (workReadyCallback != null)
137 | {
138 | workReadyCallback.run();
139 | }
140 | }
141 | }
142 |
143 | /**
144 | * {@inheritDoc}
145 | */
146 | @Override
147 | public long getTimeUntilNextRun()
148 | {
149 | long now = timeProvider.currentTimeMillis();
150 | Requester nextDueRequester = getNextDueRequester();
151 | if (nextDueRequester == null)
152 | {
153 | return WAKEUP_INTERVAL_MILLIS;
154 | }
155 | else
156 | {
157 | if (logger.isTraceEnabled())
158 | {
159 | logger.trace(hashCode() + ": Next nack is scheduled for ssrc " +
160 | nextDueRequester.ssrc + " at " +
161 | Math.max(nextDueRequester.nextRequestAt, 0) +
162 | ". (current time is " + now + ")");
163 | }
164 | return Math.max(nextDueRequester.nextRequestAt - now, 0);
165 | }
166 | }
167 |
168 | public void setWorkReadyCallback(Runnable workReadyCallback)
169 | {
170 | this.workReadyCallback = workReadyCallback;
171 | }
172 |
173 | /**
174 | * {@inheritDoc}
175 | */
176 | @Override
177 | public void run()
178 | {
179 | long now = timeProvider.currentTimeMillis();
180 | if (logger.isTraceEnabled())
181 | {
182 | logger.trace(hashCode() + " running at " + now);
183 | }
184 | List dueRequesters = getDueRequesters(now);
185 | if (logger.isTraceEnabled())
186 | {
187 | logger.trace(hashCode() + " has " + dueRequesters.size() + " due requesters");
188 | }
189 | if (!dueRequesters.isEmpty())
190 | {
191 | List nackPackets = createNackPackets(now, dueRequesters);
192 | if (logger.isTraceEnabled())
193 | {
194 | logger.trace(hashCode() + " injecting " + nackPackets.size() + " nack packets");
195 | }
196 | if (!nackPackets.isEmpty())
197 | {
198 | injectNackPackets(nackPackets);
199 | }
200 | }
201 | }
202 |
203 | private Requester getOrCreateRequester(long ssrc)
204 | {
205 | Requester requester;
206 | synchronized (requesters)
207 | {
208 | requester = requesters.get(ssrc);
209 | if (requester == null)
210 | {
211 | if (logger.isDebugEnabled())
212 | {
213 | logger.debug(
214 | "Creating new Requester for SSRC " + ssrc);
215 | }
216 | requester = new Requester(ssrc);
217 | requesters.put(ssrc, requester);
218 | }
219 | }
220 | return requester;
221 | }
222 |
223 | private Requester getNextDueRequester()
224 | {
225 | Requester nextDueRequester = null;
226 | synchronized (requesters)
227 | {
228 | for (Requester requester : requesters.values())
229 | {
230 | if (requester.nextRequestAt != -1 &&
231 | (nextDueRequester == null || requester.nextRequestAt < nextDueRequester.nextRequestAt))
232 | {
233 | nextDueRequester = requester;
234 | }
235 | }
236 | }
237 | return nextDueRequester;
238 | }
239 |
240 | /**
241 | * Get a list of the requesters (not necessarily in sorted order)
242 | * which are due to request as of the given time
243 | *
244 | * @param currentTime the current time
245 | * @return a list of the requesters (not necessarily in sorted order)
246 | * which are due to request as of the given time
247 | */
248 | private List getDueRequesters(long currentTime)
249 | {
250 | List dueRequesters = new ArrayList<>();
251 | synchronized (requesters)
252 | {
253 | for (Requester requester : requesters.values())
254 | {
255 | if (requester.isDue(currentTime))
256 | {
257 | if (logger.isTraceEnabled())
258 | {
259 | logger.trace(hashCode() + " requester for ssrc " +
260 | requester.ssrc + " has work due at " +
261 | requester.nextRequestAt +
262 | " (now = " + currentTime + ") and is missing packets: " +
263 | requester.getMissingSeqNums());
264 | }
265 | dueRequesters.add(requester);
266 | }
267 | }
268 | }
269 | return dueRequesters;
270 | }
271 |
272 | /**
273 | * Inject the given nack packets into the outgoing stream
274 | * @param nackPackets the nack packets to inject
275 | */
276 | private void injectNackPackets(List nackPackets)
277 | {
278 | for (NACKPacket nackPacket : nackPackets)
279 | {
280 | // try
281 | // {
282 | RawPacket packet;
283 | try
284 | {
285 | packet = nackPacket.toRawPacket();
286 | }
287 | catch (IOException ioe)
288 | {
289 | logger.warn("Failed to create a NACK packet: " + ioe);
290 | continue;
291 | }
292 |
293 | if (logger.isTraceEnabled())
294 | {
295 | logger.trace("Sending a NACK: " + nackPacket);
296 | }
297 |
298 | // stream.injectPacket(packet, /* data */ false, /* after */ null);
299 |
300 | ByteBuf byteBuf = Unpooled.buffer(packet.getBuffer().length);
301 | byteBuf.writeBytes(packet.getBuffer());
302 | RtspHandler rtspHandler = RtpHandler.rtspHandlerMap.get(String.valueOf(packet.getSSRC()));
303 | if (rtspHandler == null) {
304 | break;
305 | }
306 | RtspNettyServer.rtcpChannel.writeAndFlush(new DatagramPacket(byteBuf, rtspHandler.dstVideoRtcpAddr));
307 | // }
308 | // catch (TransmissionFailedException e)
309 | // {
310 | // logger.warn(
311 | // "Failed to inject packet in MediaStream: ", e.getCause());
312 | // }
313 | }
314 | }
315 |
316 | /**
317 | * Gather the packets currently marked as missing and create
318 | * NACKs for them
319 | * @param dueRequesters the requesters which are due to have nack packets
320 | * generated
321 | */
322 | protected List createNackPackets(long now, List dueRequesters)
323 | {
324 | Map> packetsToRequest = new HashMap<>();
325 |
326 | for (Requester dueRequester : dueRequesters)
327 | {
328 | synchronized (dueRequester)
329 | {
330 | Set missingPackets = dueRequester.getMissingSeqNums();
331 | if (!missingPackets.isEmpty())
332 | {
333 | if (logger.isTraceEnabled())
334 | {
335 | logger.trace(
336 | hashCode() + " Sending nack with packets "
337 | + missingPackets
338 | + " for ssrc " + dueRequester.ssrc);
339 |
340 | }
341 | packetsToRequest.put(dueRequester.ssrc, missingPackets);
342 | dueRequester.notifyNackCreated(now, missingPackets);
343 | }
344 | }
345 | }
346 |
347 | List nackPackets = new ArrayList<>();
348 | for (Map.Entry> entry : packetsToRequest.entrySet())
349 | {
350 | long sourceSsrc = entry.getKey();
351 | Set missingPackets = entry.getValue();
352 | NACKPacket nack
353 | = new NACKPacket(senderSsrc, sourceSsrc, missingPackets);
354 | nackPackets.add(nack);
355 | }
356 | return nackPackets;
357 | }
358 |
359 | /**
360 | * Handles packets for a single SSRC.
361 | */
362 | private class Requester
363 | {
364 | /**
365 | * The SSRC for this instance.
366 | */
367 | private final long ssrc;
368 |
369 | /**
370 | * The highest received RTP sequence number.
371 | */
372 | private int lastReceivedSeq = -1;
373 |
374 | /**
375 | * The time that the next request for this SSRC should be sent.
376 | */
377 | private long nextRequestAt = -1;
378 |
379 | /**
380 | * The set of active requests for this SSRC. The keys are the sequence
381 | * numbers.
382 | */
383 | private final Map requests = new HashMap<>();
384 |
385 | /**
386 | * Initializes a new Requester instance for the given SSRC.
387 | */
388 | private Requester(long ssrc)
389 | {
390 | this.ssrc = ssrc;
391 | }
392 |
393 | /**
394 | * Check if this {@link Requester} is due to send a nack
395 | * @param currentTime the current time, in ms
396 | * @return true if this {@link Requester} is due to send a nack, false
397 | * otherwise
398 | */
399 | public boolean isDue(long currentTime)
400 | {
401 | return nextRequestAt != -1 && nextRequestAt <= currentTime;
402 | }
403 |
404 | /**
405 | * Handles a received RTP packet with a specific sequence number.
406 | * @param seq the RTP sequence number of the received packet.
407 | *
408 | * @return true if there is work for this requester ready to be
409 | * done now, false otherwise
410 | */
411 | synchronized private boolean received(int seq)
412 | {
413 | if (lastReceivedSeq == -1)
414 | {
415 | lastReceivedSeq = seq;
416 | return false;
417 | }
418 |
419 | int diff = RTPUtils.getSequenceNumberDelta(seq, lastReceivedSeq);
420 | if (diff <= 0)
421 | {
422 | // An older packet, possibly already requested.
423 | Request r = requests.remove(seq);
424 | if (requests.isEmpty())
425 | {
426 | nextRequestAt = -1;
427 | }
428 |
429 | // if (r != null && logger.isDebugEnabled())
430 | // {
431 | // long rtt
432 | // = stream.getMediaStreamStats().getSendStats().getRtt();
433 | // if (rtt > 0)
434 | // {
435 | //
436 | // // firstRequestSentAt is if we created a Request, but
437 | // // haven't yet sent a NACK. Assume a delta of 0 in that
438 | // // case.
439 | // long firstRequestSentAt = r.firstRequestSentAt;
440 | // long delta
441 | // = firstRequestSentAt > 0
442 | // ? timeProvider.currentTimeMillis() - r.firstRequestSentAt
443 | // : 0;
444 | //
445 | // logger.debug(Logger.Category.STATISTICS,
446 | // "retr_received,stream=" + stream
447 | // .hashCode() +
448 | // " delay=" + delta +
449 | // ",rtt=" + rtt);
450 | // }
451 | // }
452 | }
453 | else if (diff == 1)
454 | {
455 | // The very next packet, as expected.
456 | lastReceivedSeq = seq;
457 | }
458 | else if (diff <= MAX_MISSING)
459 | {
460 | for (int missing = (lastReceivedSeq + 1) % (1<<16);
461 | missing != seq;
462 | missing = (missing + 1) % (1<<16))
463 | {
464 | Request request = new Request(missing);
465 | requests.put(missing, request);
466 | }
467 |
468 | lastReceivedSeq = seq;
469 | nextRequestAt = 0;
470 |
471 | return true;
472 | }
473 | else // if (diff > MAX_MISSING)
474 | {
475 | // Too many packets missing. Reset.
476 | lastReceivedSeq = seq;
477 | if (logger.isDebugEnabled())
478 | {
479 | logger.debug("Resetting retransmission requester state. "
480 | + "SSRC: " + ssrc
481 | + ", last received: " + lastReceivedSeq
482 | + ", current: " + seq
483 | + ". Removing " + requests.size()
484 | + " unsatisfied requests.");
485 | }
486 | requests.clear();
487 | nextRequestAt = -1;
488 | }
489 |
490 | // logger.error("丢包总数为: " + requests.size());
491 |
492 | return false;
493 | }
494 |
495 | /**
496 | * Returns a set of RTP sequence numbers which are considered still MIA,
497 | * and for which a retransmission request needs to be sent.
498 | * Assumes that the returned set of sequence numbers will be requested
499 | * immediately and updates the state accordingly (i.e. increments the
500 | * timesRequested counters and sets the time of next request).
501 | *
502 | * @return a set of RTP sequence numbers which are considered still MIA,
503 | * and for which a retransmission request needs to be sent.
504 | */
505 | synchronized private @NotNull Set getMissingSeqNums()
506 | {
507 | return new HashSet<>(requests.keySet());
508 | }
509 |
510 | /**
511 | * Notify this requester that a nack was sent at the given time
512 | * @param time the time at which the nack was sent
513 | */
514 | public synchronized void notifyNackCreated(long time, Collection sequenceNumbers)
515 | {
516 | for (Integer seqNum : sequenceNumbers)
517 | {
518 | Request request = requests.get(seqNum);
519 | request.timesRequested++;
520 | if (request.timesRequested == MAX_REQUESTS)
521 | {
522 | if (logger.isDebugEnabled())
523 | {
524 | logger.debug(
525 | "Generated the last NACK for SSRC=" + ssrc + " seq="
526 | + request.seq + ". "
527 | + "Time since the first request: "
528 | + (time - request.firstRequestSentAt));
529 | }
530 | requests.remove(seqNum);
531 | continue;
532 | }
533 | if (request.timesRequested == 1)
534 | {
535 | request.firstRequestSentAt = time;
536 | }
537 | }
538 | nextRequestAt = (requests.size() > 0) ? time + RE_REQUEST_AFTER_MILLIS : -1;
539 | }
540 |
541 | }
542 |
543 | /**
544 | * Represents a request for the retransmission of a specific RTP packet.
545 | */
546 | private static class Request
547 | {
548 | /**
549 | * The RTP sequence number.
550 | */
551 | final int seq;
552 |
553 | /**
554 | * The system time at the moment a retransmission request for this
555 | * packet was first sent.
556 | */
557 | long firstRequestSentAt = -1;
558 |
559 | /**
560 | * The number of times that a retransmission request for this packet
561 | * has been sent.
562 | */
563 | int timesRequested = 0;
564 |
565 | /**
566 | * Initializes a new Request instance with the given RTP
567 | * sequence number.
568 | * @param seq the RTP sequence number.
569 | */
570 | Request(int seq)
571 | {
572 | this.seq = seq;
573 | }
574 | }
575 |
576 | /**
577 | * {@inheritDoc}
578 | */
579 | public void setSenderSsrc(long ssrc)
580 | {
581 | senderSsrc = ssrc;
582 | }
583 | }
584 |
--------------------------------------------------------------------------------
/src/main/java/com/zhuyun/utils/Command.java:
--------------------------------------------------------------------------------
1 | package com.zhuyun.utils;
2 |
3 | import java.io.BufferedReader;
4 | import java.io.InputStreamReader;
5 | public class Command {
6 | public static void exeCmd(String commandStr) {
7 | BufferedReader br = null;
8 | try {
9 | Process p = Runtime.getRuntime().exec(commandStr);
10 | br = new BufferedReader(new InputStreamReader(p.getInputStream()));
11 | String line = null;
12 | while ((line = br.readLine()) != null) {
13 | System.out.println(line);
14 | }
15 | } catch (Exception e) {
16 | e.printStackTrace();
17 | } finally {
18 | if (br != null)
19 | {
20 | try {
21 | br.close();
22 | } catch (Exception e) {
23 | e.printStackTrace();
24 | }
25 | }
26 | }
27 | }
28 |
29 | public static void main(String[] args) {
30 | String commandStr = "ping www.baidu.com";
31 | //String commandStr = "ipconfig";
32 | Command.exeCmd(commandStr);
33 | }
34 | }
35 |
36 |
--------------------------------------------------------------------------------
/src/main/java/com/zhuyun/utils/HttpConnection.java:
--------------------------------------------------------------------------------
1 | package com.zhuyun.utils;
2 |
3 | import org.apache.http.HttpEntity;
4 | import org.apache.http.client.config.RequestConfig;
5 | import org.apache.http.client.methods.CloseableHttpResponse;
6 | import org.apache.http.client.methods.HttpGet;
7 | import org.apache.http.client.methods.HttpPost;
8 | import org.apache.http.entity.StringEntity;
9 | import org.apache.http.impl.client.CloseableHttpClient;
10 | import org.apache.http.impl.client.HttpClientBuilder;
11 | import org.apache.http.util.EntityUtils;
12 | import org.slf4j.Logger;
13 | import org.slf4j.LoggerFactory;
14 |
15 | import java.io.BufferedReader;
16 | import java.io.IOException;
17 | import java.io.InputStream;
18 | import java.io.InputStreamReader;
19 | import java.nio.charset.Charset;
20 |
21 | /**
22 | * @author CC
23 | * 2018年6月19日 下午6:18:58
24 | */
25 | public class HttpConnection {
26 | private static final Logger LOGGER = LoggerFactory.getLogger(HttpConnectionManager.class);
27 |
28 |
29 | public static String post(String path) {
30 | CloseableHttpClient httpClient = HttpClientBuilder.create().build();
31 | CloseableHttpResponse response = null;
32 | HttpPost httpPost = new HttpPost(path);
33 | httpPost.setHeader("Content-Type", "application/json;charset=utf-8");
34 | httpPost.setConfig(RequestConfig.custom().setConnectTimeout(10000).setConnectionRequestTimeout(10000).setSocketTimeout(10000).build());
35 | try {
36 | response = httpClient.execute(httpPost);
37 | HttpEntity httpEntity = response.getEntity();
38 | return EntityUtils.toString(httpEntity);
39 | } catch (IOException e) {
40 | LOGGER.error("系统错误", e);
41 | }finally {
42 | if (response != null) {
43 | try {
44 | response.close();
45 | } catch (IOException e) {
46 | LOGGER.error("系统错误", e);
47 | }
48 | }
49 | }
50 | return "";
51 | }
52 |
53 | public static String post(String path, String params) {
54 | String result="";
55 | CloseableHttpClient httpClient = HttpClientBuilder.create().build();
56 | CloseableHttpResponse response = null;
57 | HttpPost httpPost = new HttpPost(path);
58 | httpPost.setHeader("Content-Type", "application/json;charset=utf-8");
59 | httpPost.setConfig(RequestConfig.custom().setConnectTimeout(10000).setConnectionRequestTimeout(10000).setSocketTimeout(10000).build());
60 | StringEntity entity = new StringEntity(params, Charset.forName("UTF-8"));
61 | httpPost.setEntity(entity);
62 | try {
63 | response = httpClient.execute(httpPost);
64 | HttpEntity httpEntity = response.getEntity();
65 | result = EntityUtils.toString(httpEntity, "utf-8");
66 | //System.out.println(result);
67 | } catch (IOException e) {
68 | LOGGER.error("系统错误", e);
69 | }finally {
70 | if (response != null) {
71 | try {
72 | response.close();
73 | } catch (IOException e) {
74 | LOGGER.error("系统错误", e);
75 | }
76 | }
77 | }
78 | return result;
79 | }
80 | public static String get(String path) {
81 | String result = "";
82 | CloseableHttpClient httpClient = HttpConnectionManager.getHttpClient();
83 | HttpGet httpget = new HttpGet(path);
84 | RequestConfig requestConfig = RequestConfig.custom()
85 | .setConnectTimeout(10000).setConnectionRequestTimeout(10000)
86 | .setSocketTimeout(10000).build();
87 | httpget.setConfig(requestConfig);
88 | CloseableHttpResponse response = null;
89 | try {
90 | response = httpClient.execute(httpget);
91 | InputStream in = response.getEntity().getContent();
92 |
93 | BufferedReader br = new BufferedReader(new InputStreamReader(in, "UTF-8"));
94 | // 存放数据
95 | StringBuffer sbf = new StringBuffer();
96 | String temp = null;
97 | while ((temp = br.readLine()) != null) {
98 | sbf.append(temp);
99 | }
100 | result = sbf.toString();
101 |
102 | in.close();
103 | } catch (UnsupportedOperationException e) {
104 | LOGGER.error("系统错误", e);
105 | } catch (IOException e) {
106 | LOGGER.error("系统错误", e);
107 | } finally {
108 | if (response != null) {
109 | try {
110 | response.close();
111 | } catch (IOException e) {
112 | LOGGER.error("系统错误", e);
113 | }
114 | }
115 | }
116 | return result;
117 | }
118 |
119 | }
120 |
--------------------------------------------------------------------------------
/src/main/java/com/zhuyun/utils/HttpConnectionManager.java:
--------------------------------------------------------------------------------
1 | package com.zhuyun.utils;
2 |
3 | import org.apache.http.config.Registry;
4 | import org.apache.http.config.RegistryBuilder;
5 | import org.apache.http.conn.socket.ConnectionSocketFactory;
6 | import org.apache.http.conn.socket.LayeredConnectionSocketFactory;
7 | import org.apache.http.conn.socket.PlainConnectionSocketFactory;
8 | import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
9 | import org.apache.http.impl.client.CloseableHttpClient;
10 | import org.apache.http.impl.client.HttpClients;
11 | import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
12 | import org.slf4j.Logger;
13 | import org.slf4j.LoggerFactory;
14 |
15 | import javax.annotation.PostConstruct;
16 | import javax.net.ssl.SSLContext;
17 | import java.security.NoSuchAlgorithmException;
18 |
19 | /**
20 | *
21 | * @author CC
22 | * 2018年6月19日 下午6:16:34
23 | */
24 | public class HttpConnectionManager
25 | {
26 | static PoolingHttpClientConnectionManager cm = null;
27 |
28 | private static final Logger LOGGER = LoggerFactory.getLogger(HttpConnectionManager.class);
29 |
30 | @PostConstruct
31 | public void init() {
32 | LayeredConnectionSocketFactory sslsf = null;
33 | try {
34 | sslsf = new SSLConnectionSocketFactory(SSLContext.getDefault());
35 | } catch (NoSuchAlgorithmException e) {
36 | LOGGER.error("系统错误", e);
37 | }
38 |
39 | Registry socketFactoryRegistry = RegistryBuilder. create()
40 | .register("https", sslsf)
41 | .register("http", new PlainConnectionSocketFactory())
42 | .build();
43 | cm =new PoolingHttpClientConnectionManager(socketFactoryRegistry);
44 | cm.setMaxTotal(500);
45 | cm.setDefaultMaxPerRoute(500);
46 | }
47 |
48 | public static CloseableHttpClient getHttpClient() {
49 | CloseableHttpClient httpClient = HttpClients.custom()
50 | .setConnectionManager(cm)
51 | .build();
52 | /*CloseableHttpClient httpClient = HttpClients.createDefault();//如果不采用连接池就是这种方式获取连接*/
53 | return httpClient;
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/main/java/com/zhuyun/utils/ReadFromFile.java:
--------------------------------------------------------------------------------
1 | package com.zhuyun.utils;
2 |
3 | import java.io.BufferedInputStream;
4 | import java.io.ByteArrayOutputStream;
5 | import java.io.File;
6 | import java.io.FileInputStream;
7 | import java.io.FileNotFoundException;
8 | import java.io.IOException;
9 |
10 | /**
11 | *
12 | * @author zhouyinfei
13 | *
14 | */
15 | public class ReadFromFile {
16 | public static byte[] toByteArray(String filename) throws IOException {
17 |
18 | File f = new File(filename);
19 | if (!f.exists()) {
20 | throw new FileNotFoundException(filename);
21 | }
22 |
23 | ByteArrayOutputStream bos = new ByteArrayOutputStream((int) f.length());
24 | BufferedInputStream in = null;
25 | try {
26 | in = new BufferedInputStream(new FileInputStream(f));
27 | int bufSize = 1024;
28 | byte[] buffer = new byte[bufSize];
29 | int len = 0;
30 | while (-1 != (len = in.read(buffer, 0, bufSize))) {
31 | bos.write(buffer, 0, len);
32 | }
33 | return bos.toByteArray();
34 | } catch (IOException e) {
35 | e.printStackTrace();
36 | throw e;
37 | } finally {
38 | try {
39 | in.close();
40 | } catch (IOException e) {
41 | e.printStackTrace();
42 | }
43 | bos.close();
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/main/java/com/zhuyun/utils/SdpParser.java:
--------------------------------------------------------------------------------
1 | package com.zhuyun.utils;
2 |
3 | import java.util.HashMap;
4 | import java.util.Map;
5 |
6 | import org.apache.commons.lang.StringUtils;
7 |
8 | import com.zhuyun.media.MediaSdpInfo;
9 |
10 | /**
11 | * sdp解析
12 | * @author zhouyinfei
13 | *
14 | */
15 | public class SdpParser {
16 |
17 | //从sdp中提取video、audio的内容,包括
18 | public static Map parse(String sdp){
19 | // v=0
20 | // o=- 0 0 IN IP4 127.0.0.1
21 | // s=No Name
22 | // c=IN IP4 127.0.0.1
23 | // t=0 0
24 | // a=tool:libavformat 58.35.101
25 | // m=video 0 RTP/AVP 96
26 | // b=AS:1893
27 | // a=rtpmap:96 H264/90000
28 | // a=fmtp:96 packetization-mode=1; sprop-parameter-sets=Z0IAH5Y1QKALdNwEBAQI,aM4xsg==; profile-level-id=42001F
29 | // a=control:streamid=0
30 | // m=audio 0 RTP/AVP 97
31 | // b=AS:22
32 | // a=rtpmap:97 MPEG4-GENERIC/16000/1
33 | // a=fmtp:97 profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3; config=1408
34 | // a=control:streamid=1
35 |
36 | Map mediaSdpInfoMap = new HashMap();
37 | String[] lines = StringUtils.split(sdp, "\n");
38 | String lastMedia = null; //最近一次解析到的媒体类型
39 | MediaSdpInfo videoSdpInfo = null;
40 | MediaSdpInfo audioSdpInfo = null;
41 | for (String line : lines) {
42 | if ("m".equals(StringUtils.split(line, "=")[0])) { //=前面的标签如果是m
43 | String mediaInfo = StringUtils.split(line.trim(), "=")[1];
44 | if (mediaInfo.startsWith("video")) { //视频参数
45 | videoSdpInfo = new MediaSdpInfo();
46 | String[] videoInfo = StringUtils.split(mediaInfo, " "); //一个空格分隔
47 | videoSdpInfo.setMedia("video");
48 | videoSdpInfo.setRtpPayloadType(Integer.parseInt(videoInfo[3])); //最后一个字段是payload类型
49 | lastMedia = "video";
50 | }else if (mediaInfo.startsWith("audio")) { //音频参数
51 | audioSdpInfo = new MediaSdpInfo();
52 | audioSdpInfo.setCodec("MPEG4-GENERIC"); //设置音频的默认编码
53 | String[] audioInfo = StringUtils.split(mediaInfo, " "); //一个空格分隔
54 | audioSdpInfo.setMedia("audio");
55 | audioSdpInfo.setRtpPayloadType(Integer.parseInt(audioInfo[3])); //最后一个字段是payload类型
56 | lastMedia = "audio";
57 | }
58 | }
59 |
60 | if ("a".equals(StringUtils.split(line, "=")[0])) { //=前面的标签如果是a,后面是rtpmap
61 | String mediaInfo = line.trim().substring(2);
62 | if (mediaInfo.startsWith("rtpmap")) { //视频参数
63 | String[] mediaInfoList = StringUtils.split(mediaInfo, " "); //一个空格分隔
64 | if ("video".equals(lastMedia)) {
65 | videoSdpInfo.setCodec(mediaInfoList[1].split("/")[0]); //设置video的编解码格式
66 | videoSdpInfo.setClockRate(Integer.parseInt(mediaInfoList[1].split("/")[1])); //设置采样率
67 | } else if ("audio".equals(lastMedia)) {
68 | audioSdpInfo.setCodec(mediaInfoList[1].split("/")[0]); //设置audio的编解码格式
69 | audioSdpInfo.setClockRate(Integer.parseInt(mediaInfoList[1].split("/")[1])); //设置采样率
70 | }
71 |
72 | } else if (mediaInfo.startsWith("control")) { //stream流
73 | String[] mediaInfoList = StringUtils.split(mediaInfo, ":"); //;分隔
74 | if ("video".equals(lastMedia)) {
75 | videoSdpInfo.setControl(mediaInfoList[1]);
76 | } else if ("audio".equals(lastMedia)) {
77 | audioSdpInfo.setControl(mediaInfoList[1]);
78 | }
79 | }
80 | }
81 | }
82 |
83 | if(videoSdpInfo != null){
84 | mediaSdpInfoMap.put("video", videoSdpInfo);
85 | }
86 |
87 | if (audioSdpInfo != null) {
88 | mediaSdpInfoMap.put("audio", audioSdpInfo);
89 | }
90 | return mediaSdpInfoMap;
91 |
92 | }
93 |
94 | public static void main(String[] args) {
95 | String sdp = "v=0\n" +
96 | "o=- 0 0 IN IP4 127.0.0.1\n" +
97 | "s=No Name\n" +
98 | "c=IN IP4 127.0.0.1\n" +
99 | "t=0 0\n" +
100 | "a=tool:libavformat 58.35.101\n" +
101 | "m=video 0 RTP/AVP 96\n" +
102 | "b=AS:1893\n" +
103 | "a=rtpmap:96 H264/90000\n" +
104 | "a=fmtp:96 packetization-mode=1; sprop-parameter-sets=Z0IAH5Y1QKALdNwEBAQI,aM4xsg==; profile-level-id=42001F\n" +
105 | "a=control:streamid=0\n" +
106 | "m=audio 0 RTP/AVP 97\n" +
107 | "b=AS:22\n" +
108 | "a=rtpmap:97 MPEG4-GENERIC/16000/1\n" +
109 | "a=fmtp:97 profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3; config=1408\n" +
110 | "a=control:streamid=1";
111 | Map parser = parse(sdp);
112 | System.out.println(parser);
113 | }
114 |
115 |
116 | }
117 |
--------------------------------------------------------------------------------
/src/main/resources/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | UTF-8
9 |
10 |
11 | %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
12 |
13 |
14 |
15 |
16 |
17 | UTF-8
18 |
19 |
20 | ${LOG_HOME}/rtsp-netty-server_%d{yyyy-MM-dd}.%i.log
21 | 30
22 | 100MB
23 |
24 |
25 |
26 | %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/src/main/resources/rtsp-server.properties:
--------------------------------------------------------------------------------
1 | #音视频文件存储路径
2 | output.path=C:/Users/zhouyinfei/Desktop/H264_H265/medias/
3 | #output.path=/home/zhou/rtsp-netty-server/media/
4 | rtp.port=54000
5 | rtsp.port=554
6 | #rtsp超时时间,这段时间内未收到rtsp请求,则断开连接,停止发送或接受流
7 | rtsp.idle.time=600
8 | #rtcp超时时间,这段时间内未收到rtcp请求,则断开连接,停止发送或接受流
9 | rtcp.idle.time=10
10 | #rtp超时时间,这段时间内未收到rtp数据,则断开连接,停止发送或接受流
11 | rtp.idle.time=10
12 | #执行任务线程池大小
13 | executor.threadpool=5
14 | #定时任务线程池大小
15 | schedule.executor=10
16 | #定时发送rtcp的间隔
17 | schedule.rtcp.time=250
18 | #netty worker线程池大小
19 | worker.group=30
20 | #内部业务,自己在代码中将其改掉,返回值改成{"retcode": "200"}
21 | newton.url=http\://localhost\:9385/newton/
22 |
--------------------------------------------------------------------------------
/src/test/java/com/zhuyun/FileUtilsTest.java:
--------------------------------------------------------------------------------
1 | package com.zhuyun;
2 |
3 | import org.apache.commons.lang.math.RandomUtils;
4 |
5 | public class FileUtilsTest {
6 | public static void main(String[] args) {
7 | for (int i = 0; i < 1000; i++) {
8 | System.out.println(RandomUtils.nextInt());
9 | }
10 | }
11 |
12 | }
--------------------------------------------------------------------------------
/src/test/java/com/zhuyun/HttpTest.java:
--------------------------------------------------------------------------------
1 | package com.zhuyun;
2 |
3 | import com.zhuyun.utils.HttpConnection;
4 |
5 | public class HttpTest {
6 |
7 | public static void main(String[] args) {
8 | for (int i = 0; i < 100; i++) {
9 | String string = HttpConnection.get("http://localhost:8080/springmvc/eco?username=a&password=b");
10 | System.out.println(string);
11 | }
12 |
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/target/classes/com/zhuyun/RtspNettyServer$1.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhouyinfei/rtsp-netty-server/d572bcf78d610b7126590d501c800ca30e602e1d/target/classes/com/zhuyun/RtspNettyServer$1.class
--------------------------------------------------------------------------------
/target/classes/com/zhuyun/RtspNettyServer$2.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhouyinfei/rtsp-netty-server/d572bcf78d610b7126590d501c800ca30e602e1d/target/classes/com/zhuyun/RtspNettyServer$2.class
--------------------------------------------------------------------------------
/target/classes/com/zhuyun/RtspNettyServer$3.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhouyinfei/rtsp-netty-server/d572bcf78d610b7126590d501c800ca30e602e1d/target/classes/com/zhuyun/RtspNettyServer$3.class
--------------------------------------------------------------------------------
/target/classes/com/zhuyun/RtspNettyServer.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhouyinfei/rtsp-netty-server/d572bcf78d610b7126590d501c800ca30e602e1d/target/classes/com/zhuyun/RtspNettyServer.class
--------------------------------------------------------------------------------
/target/classes/com/zhuyun/handler/HeartBeatServerHandler.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhouyinfei/rtsp-netty-server/d572bcf78d610b7126590d501c800ca30e602e1d/target/classes/com/zhuyun/handler/HeartBeatServerHandler.class
--------------------------------------------------------------------------------
/target/classes/com/zhuyun/handler/RtcpHandler.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhouyinfei/rtsp-netty-server/d572bcf78d610b7126590d501c800ca30e602e1d/target/classes/com/zhuyun/handler/RtcpHandler.class
--------------------------------------------------------------------------------
/target/classes/com/zhuyun/handler/RtpHandler.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhouyinfei/rtsp-netty-server/d572bcf78d610b7126590d501c800ca30e602e1d/target/classes/com/zhuyun/handler/RtpHandler.class
--------------------------------------------------------------------------------
/target/classes/com/zhuyun/handler/RtspHandler$1.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhouyinfei/rtsp-netty-server/d572bcf78d610b7126590d501c800ca30e602e1d/target/classes/com/zhuyun/handler/RtspHandler$1.class
--------------------------------------------------------------------------------
/target/classes/com/zhuyun/handler/RtspHandler$2.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhouyinfei/rtsp-netty-server/d572bcf78d610b7126590d501c800ca30e602e1d/target/classes/com/zhuyun/handler/RtspHandler$2.class
--------------------------------------------------------------------------------
/target/classes/com/zhuyun/handler/RtspHandler$3.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhouyinfei/rtsp-netty-server/d572bcf78d610b7126590d501c800ca30e602e1d/target/classes/com/zhuyun/handler/RtspHandler$3.class
--------------------------------------------------------------------------------
/target/classes/com/zhuyun/handler/RtspHandler$4.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhouyinfei/rtsp-netty-server/d572bcf78d610b7126590d501c800ca30e602e1d/target/classes/com/zhuyun/handler/RtspHandler$4.class
--------------------------------------------------------------------------------
/target/classes/com/zhuyun/handler/RtspHandler$5.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhouyinfei/rtsp-netty-server/d572bcf78d610b7126590d501c800ca30e602e1d/target/classes/com/zhuyun/handler/RtspHandler$5.class
--------------------------------------------------------------------------------
/target/classes/com/zhuyun/handler/RtspHandler.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhouyinfei/rtsp-netty-server/d572bcf78d610b7126590d501c800ca30e602e1d/target/classes/com/zhuyun/handler/RtspHandler.class
--------------------------------------------------------------------------------
/target/classes/com/zhuyun/media/MediaSdpInfo.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhouyinfei/rtsp-netty-server/d572bcf78d610b7126590d501c800ca30e602e1d/target/classes/com/zhuyun/media/MediaSdpInfo.class
--------------------------------------------------------------------------------
/target/classes/com/zhuyun/rtp/FileUtils.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhouyinfei/rtsp-netty-server/d572bcf78d610b7126590d501c800ca30e602e1d/target/classes/com/zhuyun/rtp/FileUtils.class
--------------------------------------------------------------------------------
/target/classes/com/zhuyun/rtp/NaluType.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhouyinfei/rtsp-netty-server/d572bcf78d610b7126590d501c800ca30e602e1d/target/classes/com/zhuyun/rtp/NaluType.class
--------------------------------------------------------------------------------
/target/classes/com/zhuyun/rtp/RtpUtils.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhouyinfei/rtsp-netty-server/d572bcf78d610b7126590d501c800ca30e602e1d/target/classes/com/zhuyun/rtp/RtpUtils.class
--------------------------------------------------------------------------------
/target/classes/com/zhuyun/rtp/SpsBitStream.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhouyinfei/rtsp-netty-server/d572bcf78d610b7126590d501c800ca30e602e1d/target/classes/com/zhuyun/rtp/SpsBitStream.class
--------------------------------------------------------------------------------
/target/classes/com/zhuyun/rtp/SpsInfoStruct.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhouyinfei/rtsp-netty-server/d572bcf78d610b7126590d501c800ca30e602e1d/target/classes/com/zhuyun/rtp/SpsInfoStruct.class
--------------------------------------------------------------------------------
/target/classes/com/zhuyun/rtp/SpsUtils.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhouyinfei/rtsp-netty-server/d572bcf78d610b7126590d501c800ca30e602e1d/target/classes/com/zhuyun/rtp/SpsUtils.class
--------------------------------------------------------------------------------
/target/classes/com/zhuyun/streamhub/StreamFrame.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhouyinfei/rtsp-netty-server/d572bcf78d610b7126590d501c800ca30e602e1d/target/classes/com/zhuyun/streamhub/StreamFrame.class
--------------------------------------------------------------------------------
/target/classes/com/zhuyun/streamhub/StreamFrameSink.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhouyinfei/rtsp-netty-server/d572bcf78d610b7126590d501c800ca30e602e1d/target/classes/com/zhuyun/streamhub/StreamFrameSink.class
--------------------------------------------------------------------------------
/target/classes/com/zhuyun/streamhub/StreamHub.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhouyinfei/rtsp-netty-server/d572bcf78d610b7126590d501c800ca30e602e1d/target/classes/com/zhuyun/streamhub/StreamHub.class
--------------------------------------------------------------------------------
/target/classes/com/zhuyun/utils/Command.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhouyinfei/rtsp-netty-server/d572bcf78d610b7126590d501c800ca30e602e1d/target/classes/com/zhuyun/utils/Command.class
--------------------------------------------------------------------------------
/target/classes/com/zhuyun/utils/HttpConnection.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhouyinfei/rtsp-netty-server/d572bcf78d610b7126590d501c800ca30e602e1d/target/classes/com/zhuyun/utils/HttpConnection.class
--------------------------------------------------------------------------------
/target/classes/com/zhuyun/utils/HttpConnectionManager.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhouyinfei/rtsp-netty-server/d572bcf78d610b7126590d501c800ca30e602e1d/target/classes/com/zhuyun/utils/HttpConnectionManager.class
--------------------------------------------------------------------------------
/target/classes/com/zhuyun/utils/ReadFromFile.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhouyinfei/rtsp-netty-server/d572bcf78d610b7126590d501c800ca30e602e1d/target/classes/com/zhuyun/utils/ReadFromFile.class
--------------------------------------------------------------------------------
/target/classes/com/zhuyun/utils/SdpParser.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhouyinfei/rtsp-netty-server/d572bcf78d610b7126590d501c800ca30e602e1d/target/classes/com/zhuyun/utils/SdpParser.class
--------------------------------------------------------------------------------
/target/classes/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | UTF-8
9 |
10 |
11 | %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
12 |
13 |
14 |
15 |
16 |
17 | UTF-8
18 |
19 |
20 | ${LOG_HOME}/rtsp-netty-server_%d{yyyy-MM-dd}.%i.log
21 | 30
22 | 100MB
23 |
24 |
25 |
26 | %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/target/classes/rtsp-server.properties:
--------------------------------------------------------------------------------
1 | output.path=C:/Users/zhouyinfei/Desktop/H264_H265/medias/
2 | #output.path=/home/zhou/rtsp-netty-server/media/
3 | rtp.port=54000
4 | rtsp.port=554
5 | rtsp.idle.time=60
6 | rtcp.idle.time=10
7 | rtp.idle.time=10
8 | executor.threadpool=50
9 | worker.group=30
10 | newton.url=http\://localhost\:9385/newton/
--------------------------------------------------------------------------------
/target/maven-archiver/pom.properties:
--------------------------------------------------------------------------------
1 | #Created by Apache Maven 3.5.0
2 | version=1.0-SNAPSHOT
3 | groupId=com.zhuyun
4 | artifactId=rtsp-netty-server
5 |
--------------------------------------------------------------------------------
/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst:
--------------------------------------------------------------------------------
1 | com\zhuyun\handler\RtspHandler$4.class
2 | com\zhuyun\utils\Command.class
3 | com\zhuyun\rtp\SpsInfoStruct.class
4 | com\zhuyun\rtp\SpsBitStream.class
5 | com\zhuyun\utils\ReadFromFile.class
6 | com\zhuyun\RtspNettyServer.class
7 | com\zhuyun\rtp\FileUtils.class
8 | com\zhuyun\RtspNettyServer$1.class
9 | com\zhuyun\handler\RtspHandler$2.class
10 | com\zhuyun\RtspNettyServer$3.class
11 | com\zhuyun\rtp\RtpUtils.class
12 | com\zhuyun\handler\RtspHandler$5.class
13 | com\zhuyun\handler\RtpHandler.class
14 | com\zhuyun\utils\HttpConnection.class
15 | com\zhuyun\handler\HeartBeatServerHandler.class
16 | com\zhuyun\streamhub\StreamFrame.class
17 | com\zhuyun\utils\HttpConnectionManager.class
18 | com\zhuyun\handler\RtspHandler$3.class
19 | com\zhuyun\rtp\NaluType.class
20 | com\zhuyun\RtspNettyServer$2.class
21 | com\zhuyun\streamhub\StreamHub.class
22 | com\zhuyun\streamhub\StreamFrameSink.class
23 | com\zhuyun\handler\RtspHandler.class
24 | com\zhuyun\handler\RtcpHandler.class
25 | com\zhuyun\media\MediaSdpInfo.class
26 | com\zhuyun\handler\RtspHandler$1.class
27 | com\zhuyun\rtp\SpsUtils.class
28 | com\zhuyun\utils\SdpParser.class
29 |
--------------------------------------------------------------------------------
/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst:
--------------------------------------------------------------------------------
1 | C:\Users\zhouyinfei\Desktop\SVN\dsp\my_newtonGW\trunk\src\watt\rtsp-netty-server\src\main\java\com\zhuyun\handler\HeartBeatServerHandler.java
2 | C:\Users\zhouyinfei\Desktop\SVN\dsp\my_newtonGW\trunk\src\watt\rtsp-netty-server\src\main\java\com\zhuyun\rtp\SpsInfoStruct.java
3 | C:\Users\zhouyinfei\Desktop\SVN\dsp\my_newtonGW\trunk\src\watt\rtsp-netty-server\src\main\java\com\zhuyun\utils\HttpConnectionManager.java
4 | C:\Users\zhouyinfei\Desktop\SVN\dsp\my_newtonGW\trunk\src\watt\rtsp-netty-server\src\main\java\com\zhuyun\handler\RtpHandler.java
5 | C:\Users\zhouyinfei\Desktop\SVN\dsp\my_newtonGW\trunk\src\watt\rtsp-netty-server\src\main\java\com\zhuyun\handler\RtspHandler.java
6 | C:\Users\zhouyinfei\Desktop\SVN\dsp\my_newtonGW\trunk\src\watt\rtsp-netty-server\src\main\java\com\zhuyun\utils\HttpConnection.java
7 | C:\Users\zhouyinfei\Desktop\SVN\dsp\my_newtonGW\trunk\src\watt\rtsp-netty-server\src\main\java\com\zhuyun\rtp\RtpUtils.java
8 | C:\Users\zhouyinfei\Desktop\SVN\dsp\my_newtonGW\trunk\src\watt\rtsp-netty-server\src\main\java\com\zhuyun\rtp\SpsBitStream.java
9 | C:\Users\zhouyinfei\Desktop\SVN\dsp\my_newtonGW\trunk\src\watt\rtsp-netty-server\src\main\java\com\zhuyun\media\MediaSdpInfo.java
10 | C:\Users\zhouyinfei\Desktop\SVN\dsp\my_newtonGW\trunk\src\watt\rtsp-netty-server\src\main\java\com\zhuyun\rtp\SpsUtils.java
11 | C:\Users\zhouyinfei\Desktop\SVN\dsp\my_newtonGW\trunk\src\watt\rtsp-netty-server\src\main\java\com\zhuyun\utils\ReadFromFile.java
12 | C:\Users\zhouyinfei\Desktop\SVN\dsp\my_newtonGW\trunk\src\watt\rtsp-netty-server\src\main\java\com\zhuyun\utils\SdpParser.java
13 | C:\Users\zhouyinfei\Desktop\SVN\dsp\my_newtonGW\trunk\src\watt\rtsp-netty-server\src\main\java\com\zhuyun\rtp\FileUtils.java
14 | C:\Users\zhouyinfei\Desktop\SVN\dsp\my_newtonGW\trunk\src\watt\rtsp-netty-server\src\main\java\com\zhuyun\streamhub\StreamFrameSink.java
15 | C:\Users\zhouyinfei\Desktop\SVN\dsp\my_newtonGW\trunk\src\watt\rtsp-netty-server\src\main\java\com\zhuyun\rtp\NaluType.java
16 | C:\Users\zhouyinfei\Desktop\SVN\dsp\my_newtonGW\trunk\src\watt\rtsp-netty-server\src\main\java\com\zhuyun\streamhub\StreamHub.java
17 | C:\Users\zhouyinfei\Desktop\SVN\dsp\my_newtonGW\trunk\src\watt\rtsp-netty-server\src\main\java\com\zhuyun\handler\RtcpHandler.java
18 | C:\Users\zhouyinfei\Desktop\SVN\dsp\my_newtonGW\trunk\src\watt\rtsp-netty-server\src\main\java\com\zhuyun\streamhub\StreamFrame.java
19 | C:\Users\zhouyinfei\Desktop\SVN\dsp\my_newtonGW\trunk\src\watt\rtsp-netty-server\src\main\java\com\zhuyun\utils\Command.java
20 | C:\Users\zhouyinfei\Desktop\SVN\dsp\my_newtonGW\trunk\src\watt\rtsp-netty-server\src\main\java\com\zhuyun\RtspNettyServer.java
21 |
--------------------------------------------------------------------------------
/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst:
--------------------------------------------------------------------------------
1 | com\zhuyun\FileUtilsTest.class
2 | com\zhuyun\HttpTest.class
3 |
--------------------------------------------------------------------------------
/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst:
--------------------------------------------------------------------------------
1 | C:\Users\zhouyinfei\Desktop\SVN\dsp\my_newtonGW\trunk\src\watt\rtsp-netty-server\src\test\java\com\zhuyun\FileUtilsTest.java
2 | C:\Users\zhouyinfei\Desktop\SVN\dsp\my_newtonGW\trunk\src\watt\rtsp-netty-server\src\test\java\com\zhuyun\HttpTest.java
3 |
--------------------------------------------------------------------------------
/target/original-rtsp-netty-server-1.0-SNAPSHOT.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhouyinfei/rtsp-netty-server/d572bcf78d610b7126590d501c800ca30e602e1d/target/original-rtsp-netty-server-1.0-SNAPSHOT.jar
--------------------------------------------------------------------------------
/target/rtsp-netty-server-1.0-SNAPSHOT.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhouyinfei/rtsp-netty-server/d572bcf78d610b7126590d501c800ca30e602e1d/target/rtsp-netty-server-1.0-SNAPSHOT.jar
--------------------------------------------------------------------------------
/target/test-classes/com/zhuyun/FileUtilsTest.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhouyinfei/rtsp-netty-server/d572bcf78d610b7126590d501c800ca30e602e1d/target/test-classes/com/zhuyun/FileUtilsTest.class
--------------------------------------------------------------------------------
/target/test-classes/com/zhuyun/HttpTest.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhouyinfei/rtsp-netty-server/d572bcf78d610b7126590d501c800ca30e602e1d/target/test-classes/com/zhuyun/HttpTest.class
--------------------------------------------------------------------------------