├── .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 --------------------------------------------------------------------------------