// Copyright 2020, Chef. All rights reserved. // https://github.com/q191201771/lal // // Use of this source code is governed by a MIT-style license // that can be found in the License file. // // Author: Chef (191201771@qq.com) package rtsp import ( "fmt" "net" "strconv" "strings" "github.com/q191201771/naza/pkg/nazaerrors" "github.com/q191201771/lal/pkg/base" "github.com/q191201771/lal/pkg/rtprtcp" "github.com/q191201771/naza/pkg/nazanet" ) // TODO chef // - out缺少主动发送sr // - pull session回调有observer interface和on func回调两种方式,是否需要统一 // - [refactor] BaseInSession和BaseOutSession有不少重复内容 // - [refactor] PullSession和PushSession有不少重复内容 const ( MethodOptions = "OPTIONS" MethodAnnounce = "ANNOUNCE" MethodDescribe = "DESCRIBE" MethodSetup = "SETUP" MethodRecord = "RECORD" MethodPlay = "PLAY" MethodTeardown = "TEARDOWN" MethodGetParameter = "GET_PARAMETER" ) const ( // HeaderAccept header key HeaderAccept = "Accept" HeaderUserAgent = "User-Agent" HeaderCSeq = "CSeq" HeaderContentLength = "Content-Length" HeaderTransport = "Transport" HeaderSession = "Session" HeaderRange = "Range" HeaderWwwAuthenticate = "WWW-Authenticate" HeaderAuthorization = "Authorization" HeaderPublic = "Public" // HeaderAcceptApplicationSdp header value HeaderAcceptApplicationSdp = "application/sdp" HeaderRangeDefault = "npt=0.000-" HeaderTransportClientPlayTmpl = "RTP/AVP/UDP;unicast;client_port=%d-%d" // localRtpPort, localRtcpPort HeaderTransportClientPlayTcpTmpl = "RTP/AVP/TCP;unicast;interleaved=%d-%d" // rtpChannel, rtcpChannel HeaderTransportClientRecordTmpl = "RTP/AVP/UDP;unicast;client_port=%d-%d;mode=record" HeaderTransportClientRecordTcpTmpl = "RTP/AVP/TCP;unicast;interleaved=%d-%d;mode=record" HeaderTransportServerPlayTmpl = "RTP/AVP/UDP;unicast;client_port=%d-%d;server_port=%d-%d" //HeaderTransportServerPlayTCPTmpl = "RTP/AVP/TCP;unicast;interleaved=%d-%d" HeaderTransportServerRecordTmpl = "RTP/AVP/UDP;unicast;client_port=%d-%d;server_port=%d-%d;mode=record" //HeaderTransportServerRecordTCPTmpl = "RTP/AVP/TCP;unicast;interleaved=%d-%d;mode=record" ) const ( TransportFieldClientPort = "client_port" TransportFieldServerPort = "server_port" TransportFieldInterleaved = "interleaved" ) const ( Interleaved = uint8(0x24) ) var ( // TODO chef: 参考协议标准,不要使用固定值 sessionId = "191201771" minServerPort = uint16(30000) maxServerPort = uint16(60000) unpackerItemMaxSize = 1024 serverCommandSessionReadBufSize = 256 serverCommandSessionWriteChanSize = 1024 dummyRtpPacket = []byte{ 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, } dummyRtcpPacket = []byte{ 0x80, 0xc9, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, } ) type IInterleavedPacketWriter interface { WriteInterleavedPacket(packet []byte, channel int) error } var availUdpConnPool *nazanet.AvailUdpConnPool // 传入远端IP,RtpPort,RtcpPort,创建两个对应的RTP和RTCP的UDP连接对象,以及对应的本端端口 func initConnWithClientPort(rHost string, rRtpPort, rRtcpPort uint16) (rtpConn, rtcpConn *nazanet.UdpConnection, lRtpPort, lRtcpPort uint16, err error) { // NOTICE // 处理Pub时, // 一路流的rtp端口和rtcp端口必须不同。 // 我尝试给ffmpeg返回rtp和rtcp同一个端口,结果ffmpeg依然使用rtp+1作为rtcp的端口。 // 又尝试给ffmpeg返回rtp:a和rtcp:a+2的端口,结果ffmpeg依然使用a和a+1端口。 // 也即是说,ffmpeg默认认为rtcp的端口是rtp的端口+1。而不管SETUP RESPONSE的rtcp端口是多少。 // 我目前在Acquire2这个函数里做了保证,绑定两个可用且连续的端口。 var rtpc, rtcpc *net.UDPConn rtpc, lRtpPort, rtcpc, lRtcpPort, err = availUdpConnPool.Acquire2() if err != nil { return } rtpConn, err = nazanet.NewUdpConnection(func(option *nazanet.UdpConnectionOption) { option.Conn = rtpc option.RAddr = net.JoinHostPort(rHost, fmt.Sprintf("%d", rRtpPort)) option.MaxReadPacketSize = rtprtcp.MaxRtpRtcpPacketSize }) if err != nil { return } rtcpConn, err = nazanet.NewUdpConnection(func(option *nazanet.UdpConnectionOption) { option.Conn = rtcpc option.RAddr = net.JoinHostPort(rHost, fmt.Sprintf("%d", rRtcpPort)) option.MaxReadPacketSize = rtprtcp.MaxRtpRtcpPacketSize }) return } // 从setup消息的header中解析rtp rtcp channel func parseRtpRtcpChannel(setupTransport string) (rtp, rtcp uint16, err error) { return parseTransport(setupTransport, TransportFieldInterleaved) } // 从setup消息的header中解析rtp rtcp 端口 func parseClientPort(setupTransport string) (rtp, rtcp uint16, err error) { return parseTransport(setupTransport, TransportFieldClientPort) } func parseServerPort(setupTransport string) (rtp, rtcp uint16, err error) { return parseTransport(setupTransport, TransportFieldServerPort) } func parseTransport(setupTransport string, key string) (first, second uint16, err error) { var clientPort string items := strings.Split(setupTransport, ";") for _, item := range items { if strings.HasPrefix(item, key) { kv := strings.Split(item, "=") if len(kv) != 2 { continue } clientPort = kv[1] } } items = strings.Split(clientPort, "-") if len(items) != 2 { return 0, 0, nazaerrors.Wrap(base.ErrRtsp) } iFirst, err := strconv.Atoi(items[0]) if err != nil { return 0, 0, err } iSecond, err := strconv.Atoi(items[1]) if err != nil { return 0, 0, err } return uint16(iFirst), uint16(iSecond), err } func makeSetupUri(urlCtx base.UrlContext, aControl string) string { if strings.HasPrefix(aControl, "rtsp://") { return aControl } return fmt.Sprintf("%s/%s", urlCtx.RawUrlWithoutUserInfo, aControl) } func init() { availUdpConnPool = nazanet.NewAvailUdpConnPool(minServerPort, maxServerPort) } // --------------------------------------------------------------------------------------------------------------------- // PUB // ffmpeg -re -stream_loop -1 -i /Volumes/Data/tmp/wontcry.flv -acodec copy -vcodec copy -f rtsp rtsp://localhost:5544/live/test110 // read http request. method=OPTIONS, uri=rtsp://localhost:5544/live/test110, headers=map[CSeq:1 User-Agent:Lavf57.83.100], body= - server.go:95 // read http request. method=ANNOUNCE, uri=rtsp://localhost:5544/live/test110, headers=map[CSeq:2 Content-Length:490 Content-Type:application/sdp User-Agent:Lavf57.83.100], body=v=0 // o=- 0 0 IN IP4 127.0.0.1 // s=No Name // c=IN IP4 127.0.0.1 // t=0 0 // a=tool:libavformat 57.83.100 // m=video 0 RTP/AVP 96 // a=rtpmap:96 H264/90000 // a=fmtp:96 packetization-mode=1; sprop-parameter-sets=Z2QAFqyyAUBf8uAiAAADAAIAAAMAPB4sXJA=,aOvDyyLA; profile-level-id=640016 // a=control:streamid=0 // m=audio 0 RTP/AVP 97 // b=AS:128 // a=rtpmap:97 MPEG4-GENERIC/44100/2 // a=fmtp:97 profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3; config=121056E500 // a=control:streamid=1 // - server.go:95 // read http request. method=SETUP, uri=rtsp://localhost:5544/live/test110/streamid=0, headers=map[CSeq:3 Transport:RTP/AVP/UDP;unicast;client_port=32182-32183;mode=record User-Agent:Lavf57.83.100], body= - server.go:95 // read http request. method=SETUP, uri=rtsp://localhost:5544/live/test110/streamid=1, headers=map[CSeq:4 Session:191201771 Transport:RTP/AVP/UDP;unicast;client_port=32184-32185;mode=record User-Agent:Lavf57.83.100], body= - server.go:95 // read http request. method=RECORD, uri=rtsp://localhost:5544/live/test110, headers=map[CSeq:5 Range:npt=0.000- Session:191201771 User-Agent:Lavf57.83.100], body= - server.go:95 // read http request. method=TEARDOWN, uri=rtsp://localhost:5544/live/test110, headers=map[CSeq:6 Session:191201771 User-Agent:Lavf57.83.100], body= - server.go:95 // --------------------------------------------------------------------------------------------------------------------- // --------------------------------------------------------------------------------------------------------------------- // PUB(rtp over tcp) // ffmpeg -re -stream_loop -1 -i /Volumes/Data/tmp/wontcry.flv -acodec copy -vcodec copy -rtsp_transport tcp -f rtsp rtsp://localhost:5544/live/test110 // // read http request. method=OPTIONS, uri=rtsp://localhost:5544/live/test110, headers=map[CSeq:1 User-Agent:Lavf57.83.100], body= - server.go:137 // read http request. method=ANNOUNCE, uri=rtsp://localhost:5544/live/test110, headers=map[CSeq:2 Content-Length:478 Content-Type:application/sdp User-Agent:Lavf57.83.100], body=v=0 // o=- 0 0 IN IP6 ::1 // s=No Name // c=IN IP6 ::1 // t=0 0 // a=tool:libavformat 57.83.100 // m=video 0 RTP/AVP 96 // a=rtpmap:96 H264/90000 // a=fmtp:96 packetization-mode=1; sprop-parameter-sets=Z2QAFqyyAUBf8uAiAAADAAIAAAMAPB4sXJA=,aOvDyyLA; profile-level-id=640016 // a=control:streamid=0 // m=audio 0 RTP/AVP 97 // b=AS:128 // a=rtpmap:97 MPEG4-GENERIC/44100/2 // a=fmtp:97 profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3; config=121056E500 // a=control:streamid=1 // - server.go:137 // read http request. method=SETUP, uri=rtsp://localhost:5544/live/test110/streamid=0, headers=map[CSeq:3 Transport:RTP/AVP/TCP;unicast;interleaved=0-1;mode=record User-Agent:Lavf57.83.100], body= - server.go:137 // read http request. method=SETUP, uri=rtsp://localhost:5544/live/test110/streamid=1, headers=map[CSeq:4 Session:191201771 Transport:RTP/AVP/TCP;unicast;interleaved=2-3;mode=record User-Agent:Lavf57.83.100], body= - server.go:137 // read http request. method=RECORD, uri=rtsp://localhost:5544/live/test110, headers=map[CSeq:5 Range:npt=0.000- Session:191201771 User-Agent:Lavf57.83.100], body= - server.go:137 // --------------------------------------------------------------------------------------------------------------------- // --------------------------------------------------------------------------------------------------------------------- // SUB // // read http request. method=OPTIONS, uri=rtsp://localhost:5544/live/test110, headers=map[CSeq:1 User-Agent:Lavf57.83.100], body= - server.go:108 // read http request. method=DESCRIBE, uri=rtsp://localhost:5544/live/test110, headers=map[Accept:application/sdp CSeq:2 User-Agent:Lavf57.83.100], body= - server.go:108 // read http request. method=SETUP, uri=rtsp://localhost:5544/live/test110/streamid=0, headers=map[CSeq:3 Transport:RTP/AVP/UDP;unicast;client_port=15690-15691 User-Agent:Lavf57.83.100], body= - server.go:108 // read http request. method=SETUP, uri=rtsp://localhost:5544/live/test110/streamid=1, headers=map[CSeq:4 Session:191201771 Transport:RTP/AVP/UDP;unicast;client_port=15692-15693 User-Agent:Lavf57.83.100], body= - server.go:108 // read http request. method=PLAY, uri=rtsp://localhost:5544/live/test110, headers=map[CSeq:5 Range:npt=0.000- Session:191201771 User-Agent:Lavf57.83.100], body= - server.go:108 // --------------------------------------------------------------------------------------------------------------------- // --------------------------------------------------------------------------------------------------------------------- // SUB(rtp over tcp) // // read http request. method=OPTIONS, uri=rtsp://localhost:5544/live/test110, headers=map[CSeq:1 User-Agent:Lavf57.83.100], body= - server_command_session.go:136 // read http request. method=DESCRIBE, uri=rtsp://localhost:5544/live/test110, headers=map[Accept:application/sdp CSeq:2 User-Agent:Lavf57.83.100], body= - server_command_session.go:136 // read http request. method=SETUP, uri=rtsp://localhost:5544/live/test110/streamid=0, headers=map[CSeq:3 Transport:RTP/AVP/TCP;unicast;interleaved=0-1 User-Agent:Lavf57.83.100], body= - server_command_session.go:136 // read http request. method=SETUP, uri=rtsp://localhost:5544/live/test110/streamid=1, headers=map[CSeq:4 Session:191201771 Transport:RTP/AVP/TCP;unicast;interleaved=2-3 User-Agent:Lavf57.83.100], body= - server_command_session.go:136 // read http request. method=PLAY, uri=rtsp://localhost:5544/live/test110, headers=map[CSeq:5 Range:npt=0.000- Session:191201771 User-Agent:Lavf57.83.100], body= - server_command_session.go:136 // --------------------------------------------------------------------------------------------------------------------- // 8000 video rtp // 8001 video rtcp // 8002 audio rtp // 8003 audio rtcp