second
This commit is contained in:
parent
be1994a49b
commit
ab6a068d45
510
client.go
510
client.go
@ -3,22 +3,26 @@ package rtsp
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"bytes"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"strings"
|
"strings"
|
||||||
"strconv"
|
"strconv"
|
||||||
"bufio"
|
"bufio"
|
||||||
"net/textproto"
|
"net/textproto"
|
||||||
"net/url"
|
"net/url"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"encoding/binary"
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"github.com/nareix/av"
|
"github.com/nareix/av"
|
||||||
|
"github.com/nareix/codec/h264parser"
|
||||||
|
"github.com/nareix/rtsp/sdp"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
DebugConn bool
|
DebugConn bool
|
||||||
url *url.URL
|
url *url.URL
|
||||||
conn net.Conn
|
conn net.Conn
|
||||||
|
rconn io.Reader
|
||||||
requestUri string
|
requestUri string
|
||||||
cseq uint
|
cseq uint
|
||||||
streams []*Stream
|
streams []*Stream
|
||||||
@ -27,6 +31,23 @@ type Client struct {
|
|||||||
body io.Reader
|
body io.Reader
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Request struct {
|
||||||
|
Header []string
|
||||||
|
Uri string
|
||||||
|
Method string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
BlockLength int
|
||||||
|
Block []byte
|
||||||
|
BlockNo int
|
||||||
|
|
||||||
|
StatusCode int
|
||||||
|
Header textproto.MIMEHeader
|
||||||
|
ContentLength int
|
||||||
|
Body []byte
|
||||||
|
}
|
||||||
|
|
||||||
func Connect(uri string) (self *Client, err error) {
|
func Connect(uri string) (self *Client, err error) {
|
||||||
var URL *url.URL
|
var URL *url.URL
|
||||||
if URL, err = url.Parse(uri); err != nil {
|
if URL, err = url.Parse(uri); err != nil {
|
||||||
@ -44,6 +65,7 @@ func Connect(uri string) (self *Client, err error) {
|
|||||||
|
|
||||||
self = &Client{
|
self = &Client{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
|
rconn: conn,
|
||||||
url: URL,
|
url: URL,
|
||||||
requestUri: u2.String(),
|
requestUri: u2.String(),
|
||||||
}
|
}
|
||||||
@ -52,20 +74,20 @@ func Connect(uri string) (self *Client, err error) {
|
|||||||
|
|
||||||
func (self *Client) writeLine(line string) (err error) {
|
func (self *Client) writeLine(line string) (err error) {
|
||||||
if self.DebugConn {
|
if self.DebugConn {
|
||||||
fmt.Print(line)
|
fmt.Print("> ", line)
|
||||||
}
|
}
|
||||||
_, err = fmt.Fprint(self.conn, line)
|
_, err = fmt.Fprint(self.conn, line)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *Client) WriteRequest(method string, uri string, headers []string) (err error) {
|
func (self *Client) WriteRequest(req Request) (err error) {
|
||||||
self.cseq++
|
self.cseq++
|
||||||
headers = append(headers, fmt.Sprintf("CSeq: %d", self.cseq))
|
req.Header = append(req.Header, fmt.Sprintf("CSeq: %d", self.cseq))
|
||||||
if err = self.writeLine(fmt.Sprintf("%s %s RTSP/1.0\r\n", method, uri)); err != nil {
|
if err = self.writeLine(fmt.Sprintf("%s %s RTSP/1.0\r\n", req.Method, req.Uri)); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, header := range headers {
|
for _, v := range req.Header {
|
||||||
if err = self.writeLine(header+"\r\n"); err != nil {
|
if err = self.writeLine(fmt.Sprint(v, "\r\n")); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -75,8 +97,50 @@ func (self *Client) WriteRequest(method string, uri string, headers []string) (e
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *Client) ReadResponse() (statusCode int, body io.Reader, err error) {
|
func (self *Client) ReadResponse() (res Response, err error) {
|
||||||
br := bufio.NewReader(self.conn)
|
var br *bufio.Reader
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if br != nil {
|
||||||
|
buf, _ := br.Peek(br.Buffered())
|
||||||
|
self.rconn = io.MultiReader(bytes.NewReader(buf), self.rconn)
|
||||||
|
}
|
||||||
|
if res.StatusCode == 200 {
|
||||||
|
if res.ContentLength > 0 {
|
||||||
|
res.Body = make([]byte, res.ContentLength)
|
||||||
|
if _, err = io.ReadFull(self.rconn, res.Body); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if res.BlockLength > 0 {
|
||||||
|
res.Block = make([]byte, res.BlockLength)
|
||||||
|
if _, err = io.ReadFull(self.rconn, res.Block); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
var h [4]byte
|
||||||
|
if _, err = io.ReadFull(self.rconn, h[:]); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if h[0] == 36 {
|
||||||
|
// $
|
||||||
|
res.BlockLength = int(h[2])<<8+int(h[3])
|
||||||
|
res.BlockNo = int(h[1])
|
||||||
|
if self.DebugConn {
|
||||||
|
fmt.Println("block: len", res.BlockLength, "no", res.BlockNo)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
} else if h[0] == 82 {
|
||||||
|
// RTSP 200 OK
|
||||||
|
self.rconn = io.MultiReader(bytes.NewReader(h[:]), self.rconn)
|
||||||
|
} else {
|
||||||
|
err = fmt.Errorf("invalid response bytes=%v", h)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
br = bufio.NewReader(self.rconn)
|
||||||
tp := textproto.NewReader(br)
|
tp := textproto.NewReader(br)
|
||||||
|
|
||||||
var line string
|
var line string
|
||||||
@ -84,29 +148,33 @@ func (self *Client) ReadResponse() (statusCode int, body io.Reader, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if self.DebugConn {
|
if self.DebugConn {
|
||||||
fmt.Println(line)
|
fmt.Println("<", line)
|
||||||
}
|
}
|
||||||
|
|
||||||
fline := strings.SplitN(line, " ", 3)
|
fline := strings.SplitN(line, " ", 3)
|
||||||
if len(fline) < 2 {
|
if len(fline) < 2 {
|
||||||
err = fmt.Errorf("malformed RTSP response")
|
err = fmt.Errorf("malformed RTSP response line")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if statusCode, err = strconv.Atoi(fline[1]); err != nil {
|
if res.StatusCode, err = strconv.Atoi(fline[1]); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if statusCode != 200 && statusCode != 401 {
|
|
||||||
err = fmt.Errorf("statusCode(%d) invalid", statusCode)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var header textproto.MIMEHeader
|
var header textproto.MIMEHeader
|
||||||
if header, err = tp.ReadMIMEHeader(); err != nil {
|
if header, err = tp.ReadMIMEHeader(); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if statusCode == 401 {
|
if self.DebugConn {
|
||||||
|
fmt.Println("<", header)
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.StatusCode != 200 && res.StatusCode != 401 {
|
||||||
|
err = fmt.Errorf("StatusCode=%d invalid", res.StatusCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.StatusCode == 401 {
|
||||||
/*
|
/*
|
||||||
RTSP/1.0 401 Unauthorized
|
RTSP/1.0 401 Unauthorized
|
||||||
CSeq: 2
|
CSeq: 2
|
||||||
@ -149,7 +217,7 @@ func (self *Client) ReadResponse() (statusCode int, body io.Reader, err error) {
|
|||||||
hs2 := md5hash("DESCRIBE:"+self.requestUri)
|
hs2 := md5hash("DESCRIBE:"+self.requestUri)
|
||||||
response := md5hash(hs1+":"+nonce+":"+hs2)
|
response := md5hash(hs1+":"+nonce+":"+hs2)
|
||||||
self.authorization = fmt.Sprintf(
|
self.authorization = fmt.Sprintf(
|
||||||
`Authorization: Digest username="%s", realm="%s", nonce="%s", uri="%s", response="%s"`,
|
`Digest username="%s", realm="%s", nonce="%s", uri="%s", response="%s"`,
|
||||||
username, realm, nonce, self.requestUri, response)
|
username, realm, nonce, self.requestUri, response)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -161,29 +229,25 @@ func (self *Client) ReadResponse() (statusCode int, body io.Reader, err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
clen, _ := strconv.Atoi(header.Get("Content-Length"))
|
res.ContentLength, _ = strconv.Atoi(header.Get("Content-Length"))
|
||||||
|
|
||||||
if statusCode == 200 {
|
|
||||||
if clen > 0 {
|
|
||||||
body = io.LimitReader(br, int64(clen))
|
|
||||||
} else {
|
|
||||||
body = io.MultiReader(io.LimitReader(br, int64(br.Buffered())), self.conn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *Client) Setup(streams []int) (err error) {
|
func (self *Client) Setup(streams []int) (err error) {
|
||||||
for _, si := range streams {
|
for _, si := range streams {
|
||||||
reqhdr := []string{fmt.Sprintf("Transport: RTP/AVP/TCP;unicast;interleaved=%d-%d", si*2, si*2+1)}
|
req := Request{
|
||||||
if self.session != "" {
|
Method: "SETUP",
|
||||||
reqhdr = append(reqhdr, "Session: "+self.session)
|
Uri: self.requestUri+"/"+self.streams[si].Sdp.Control,
|
||||||
}
|
}
|
||||||
if err = self.WriteRequest("SETUP", self.requestUri+"/"+self.streams[si].control, reqhdr); err != nil {
|
req.Header = append(req.Header, fmt.Sprintf("Transport: RTP/AVP/TCP;unicast;interleaved=%d-%d", si*2, si*2+1))
|
||||||
|
if self.session != "" {
|
||||||
|
req.Header = append(req.Header, "Session: "+self.session)
|
||||||
|
}
|
||||||
|
if err = self.WriteRequest(req); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, _, err = self.ReadResponse(); err != nil {
|
if _, err = self.ReadResponse(); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -195,124 +259,356 @@ func md5hash(s string) string {
|
|||||||
return hex.EncodeToString(h[:])
|
return hex.EncodeToString(h[:])
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *Client) Describe() (streams []*Stream, err error) {
|
func (self *Client) Describe() (streams []av.Stream, err error) {
|
||||||
var body io.Reader
|
var res Response
|
||||||
var statusCode int
|
|
||||||
|
|
||||||
for i := 0; i < 2; i++ {
|
for i := 0; i < 2; i++ {
|
||||||
reqhdr := []string{}
|
req := Request{
|
||||||
|
Method: "DESCRIBE",
|
||||||
|
Uri: self.requestUri,
|
||||||
|
}
|
||||||
if self.authorization != "" {
|
if self.authorization != "" {
|
||||||
reqhdr = append(reqhdr, self.authorization)
|
req.Header = append(req.Header, "Authorization: "+self.authorization)
|
||||||
}
|
}
|
||||||
if err = self.WriteRequest("DESCRIBE", self.requestUri, reqhdr); err != nil {
|
if err = self.WriteRequest(req); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if statusCode, body, err = self.ReadResponse(); err != nil {
|
if res, err = self.ReadResponse(); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if statusCode == 200 {
|
if res.StatusCode == 200 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if body == nil {
|
if res.ContentLength == 0 {
|
||||||
err = fmt.Errorf("Describe failed")
|
err = fmt.Errorf("Describe failed")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
br := bufio.NewReader(body)
|
body := string(res.Body)
|
||||||
tp := textproto.NewReader(br)
|
|
||||||
var stream *Stream
|
|
||||||
|
|
||||||
for {
|
if self.DebugConn {
|
||||||
line, err := tp.ReadLine()
|
fmt.Println("<", body)
|
||||||
if err != nil {
|
}
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.DebugConn {
|
self.streams = []*Stream{}
|
||||||
fmt.Println(line)
|
for _, info := range sdp.Decode(body) {
|
||||||
}
|
stream := &Stream{Sdp: info}
|
||||||
|
stream.SetType(info.Type)
|
||||||
|
|
||||||
typeval := strings.SplitN(line, "=", 2)
|
switch info.Type {
|
||||||
if len(typeval) == 2 {
|
case av.H264:
|
||||||
fields := strings.Split(typeval[1], " ")
|
var sps, pps []byte
|
||||||
switch typeval[0] {
|
for _, nalu := range info.SpropParameterSets {
|
||||||
case "m":
|
if len(nalu) > 0 {
|
||||||
if len(fields) > 0 {
|
switch nalu[0]&0x1f {
|
||||||
switch fields[0] {
|
case 7:
|
||||||
case "audio", "video":
|
sps = nalu
|
||||||
stream = &Stream{typestr: fields[0]}
|
case 8:
|
||||||
self.streams = append(self.streams, stream)
|
pps = nalu
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case "a":
|
|
||||||
if stream != nil {
|
|
||||||
for _, field := range fields {
|
|
||||||
keyval := strings.Split(field, ":")
|
|
||||||
if len(keyval) >= 2 {
|
|
||||||
key := keyval[0]
|
|
||||||
val := keyval[1]
|
|
||||||
if key == "control" {
|
|
||||||
stream.control = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if len(sps) > 0 && len(pps) > 0 {
|
||||||
|
codecData, _ := h264parser.CreateCodecDataBySPSAndPPS(sps, pps)
|
||||||
|
if err = stream.SetCodecData(codecData); err != nil {
|
||||||
|
err = fmt.Errorf("h264 sps/pps invalid: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = fmt.Errorf("h264 sdp sprop-parameter-sets invalid: missing sps or pps")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
case av.AAC:
|
||||||
|
if len(info.Config) == 0 {
|
||||||
|
err = fmt.Errorf("aac sdp config missing")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = stream.SetCodecData(info.Config); err != nil {
|
||||||
|
err = fmt.Errorf("aac sdp config invalid: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.streams = append(self.streams, stream)
|
||||||
}
|
}
|
||||||
|
|
||||||
streams = self.streams
|
for _, stream := range self.streams {
|
||||||
|
streams = append(streams, stream)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *Client) Options() (err error) {
|
func (self *Client) Options() (err error) {
|
||||||
if err = self.WriteRequest("OPTIONS", self.requestUri, []string{}); err != nil {
|
if err = self.WriteRequest(Request{
|
||||||
|
Method: "OPTIONS",
|
||||||
|
Uri: self.requestUri,
|
||||||
|
}); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, _, err = self.ReadResponse(); err != nil {
|
if _, err = self.ReadResponse(); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *Client) readBlock() (err error) {
|
func (self *Stream) handleH264Payload(naluType byte, timestamp uint32, packet []byte) (err error) {
|
||||||
var h [4]byte
|
/*
|
||||||
for {
|
Table 7-1 – NAL unit type codes
|
||||||
if _, err = io.ReadFull(self.body, h[:]); err != nil {
|
1 Coded slice of a non-IDR picture
|
||||||
return
|
5 Coded slice of an IDR picture
|
||||||
}
|
6 Supplemental enhancement information (SEI)
|
||||||
if h[0] != 36 {
|
7 Sequence parameter set
|
||||||
err = fmt.Errorf("block not start with $")
|
8 Picture parameter set
|
||||||
fmt.Println(h)
|
*/
|
||||||
return
|
switch naluType {
|
||||||
}
|
case 7,8:
|
||||||
length := int(h[2])<<8+int(h[3])
|
// sps/pps
|
||||||
|
|
||||||
if self.DebugConn {
|
default:
|
||||||
fmt.Println("packet", length, h[1])
|
if naluType == 5 {
|
||||||
}
|
self.pkt.IsKeyFrame = true
|
||||||
|
|
||||||
if _, err = io.CopyN(ioutil.Discard, self.body, int64(length)); err != nil {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
self.gotpkt = true
|
||||||
|
self.pkt.Data = packet
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *Client) ReadHeader() (streams []av.Stream, err error) {
|
func (self *Stream) handlePacket(timestamp uint32, packet []byte) (err error) {
|
||||||
if err = self.WriteRequest("PLAY", self.requestUri, []string{"Session: "+self.session}); err != nil {
|
switch self.Type() {
|
||||||
return
|
case av.AAC:
|
||||||
}
|
|
||||||
if _, self.body, err = self.ReadResponse(); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
case av.H264:
|
||||||
if err = self.readBlock(); err != nil {
|
/*
|
||||||
|
+---------------+
|
||||||
|
|0|1|2|3|4|5|6|7|
|
||||||
|
+-+-+-+-+-+-+-+-+
|
||||||
|
|F|NRI| Type |
|
||||||
|
+---------------+
|
||||||
|
*/
|
||||||
|
naluType := packet[0]&0x1f
|
||||||
|
|
||||||
|
/*
|
||||||
|
NAL Unit Packet Packet Type Name Section
|
||||||
|
Type Type
|
||||||
|
-------------------------------------------------------------
|
||||||
|
0 reserved -
|
||||||
|
1-23 NAL unit Single NAL unit packet 5.6
|
||||||
|
24 STAP-A Single-time aggregation packet 5.7.1
|
||||||
|
25 STAP-B Single-time aggregation packet 5.7.1
|
||||||
|
26 MTAP16 Multi-time aggregation packet 5.7.2
|
||||||
|
27 MTAP24 Multi-time aggregation packet 5.7.2
|
||||||
|
28 FU-A Fragmentation unit 5.8
|
||||||
|
29 FU-B Fragmentation unit 5.8
|
||||||
|
30-31 reserved -
|
||||||
|
*/
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case naluType >= 1 && naluType <= 23:
|
||||||
|
if err = self.handleH264Payload(naluType, timestamp, packet); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
case naluType == 28:
|
||||||
|
/*
|
||||||
|
0 1 2 3
|
||||||
|
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
| FU indicator | FU header | |
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
|
||||||
|
| |
|
||||||
|
| FU payload |
|
||||||
|
| |
|
||||||
|
| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
| :...OPTIONAL RTP padding |
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
Figure 14. RTP payload format for FU-A
|
||||||
|
|
||||||
|
The FU indicator octet has the following format:
|
||||||
|
+---------------+
|
||||||
|
|0|1|2|3|4|5|6|7|
|
||||||
|
+-+-+-+-+-+-+-+-+
|
||||||
|
|F|NRI| Type |
|
||||||
|
+---------------+
|
||||||
|
|
||||||
|
|
||||||
|
The FU header has the following format:
|
||||||
|
+---------------+
|
||||||
|
|0|1|2|3|4|5|6|7|
|
||||||
|
+-+-+-+-+-+-+-+-+
|
||||||
|
|S|E|R| Type |
|
||||||
|
+---------------+
|
||||||
|
|
||||||
|
S: 1 bit
|
||||||
|
When set to one, the Start bit indicates the start of a fragmented
|
||||||
|
NAL unit. When the following FU payload is not the start of a
|
||||||
|
fragmented NAL unit payload, the Start bit is set to zero.
|
||||||
|
|
||||||
|
E: 1 bit
|
||||||
|
When set to one, the End bit indicates the end of a fragmented NAL
|
||||||
|
unit, i.e., the last byte of the payload is also the last byte of
|
||||||
|
the fragmented NAL unit. When the following FU payload is not the
|
||||||
|
last fragment of a fragmented NAL unit, the End bit is set to
|
||||||
|
zero.
|
||||||
|
|
||||||
|
R: 1 bit
|
||||||
|
The Reserved bit MUST be equal to 0 and MUST be ignored by the
|
||||||
|
receiver.
|
||||||
|
|
||||||
|
Type: 5 bits
|
||||||
|
The NAL unit payload type as defined in table 7-1 of [1].
|
||||||
|
*/
|
||||||
|
fuIndicator := packet[0]
|
||||||
|
fuHeader := packet[1]
|
||||||
|
isStart := fuHeader&0x80!=0
|
||||||
|
isEnd := fuHeader&0x40!=0
|
||||||
|
naluType := fuHeader&0x1f
|
||||||
|
if isStart {
|
||||||
|
self.fuBuffer = []byte{fuIndicator&0xe0|fuHeader&0x1f}
|
||||||
|
}
|
||||||
|
self.fuBuffer = append(self.fuBuffer, packet[2:]...)
|
||||||
|
if isEnd {
|
||||||
|
if err = self.handleH264Payload(naluType, timestamp, self.fuBuffer); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("unsupported H264 naluType=%d", naluType)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (self *Client) parseBlock(blockNo int, packet []byte) (streamIndex int, err error) {
|
||||||
|
if blockNo % 2 != 0 {
|
||||||
|
// rtcp block
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
0 1 2 3
|
||||||
|
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
|V=2|P|X| CC |M| PT | sequence number |
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
| timestamp |
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
| synchronization source (SSRC) identifier |
|
||||||
|
+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
|
||||||
|
| contributing source (CSRC) identifiers |
|
||||||
|
| .... |
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
*/
|
||||||
|
|
||||||
|
if len(packet) < 8 {
|
||||||
|
err = fmt.Errorf("rtp packet too short")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
payloadOffset := 12+int(packet[0]&0xf)*4
|
||||||
|
if payloadOffset+2 > len(packet) {
|
||||||
|
err = fmt.Errorf("rtp packet too short")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
timestamp := binary.BigEndian.Uint32(packet[4:8])
|
||||||
|
payload := packet[payloadOffset:]
|
||||||
|
|
||||||
|
/*
|
||||||
|
PT Encoding Name Audio/Video (A/V) Clock Rate (Hz) Channels Reference
|
||||||
|
0 PCMU A 8000 1 [RFC3551]
|
||||||
|
1 Reserved
|
||||||
|
2 Reserved
|
||||||
|
3 GSM A 8000 1 [RFC3551]
|
||||||
|
4 G723 A 8000 1 [Vineet_Kumar][RFC3551]
|
||||||
|
5 DVI4 A 8000 1 [RFC3551]
|
||||||
|
6 DVI4 A 16000 1 [RFC3551]
|
||||||
|
7 LPC A 8000 1 [RFC3551]
|
||||||
|
8 PCMA A 8000 1 [RFC3551]
|
||||||
|
9 G722 A 8000 1 [RFC3551]
|
||||||
|
10 L16 A 44100 2 [RFC3551]
|
||||||
|
11 L16 A 44100 1 [RFC3551]
|
||||||
|
12 QCELP A 8000 1 [RFC3551]
|
||||||
|
13 CN A 8000 1 [RFC3389]
|
||||||
|
14 MPA A 90000 [RFC3551][RFC2250]
|
||||||
|
15 G728 A 8000 1 [RFC3551]
|
||||||
|
16 DVI4 A 11025 1 [Joseph_Di_Pol]
|
||||||
|
17 DVI4 A 22050 1 [Joseph_Di_Pol]
|
||||||
|
18 G729 A 8000 1 [RFC3551]
|
||||||
|
19 Reserved A
|
||||||
|
20 Unassigned A
|
||||||
|
21 Unassigned A
|
||||||
|
22 Unassigned A
|
||||||
|
23 Unassigned A
|
||||||
|
24 Unassigned V
|
||||||
|
25 CelB V 90000 [RFC2029]
|
||||||
|
26 JPEG V 90000 [RFC2435]
|
||||||
|
27 Unassigned V
|
||||||
|
28 nv V 90000 [RFC3551]
|
||||||
|
29 Unassigned V
|
||||||
|
30 Unassigned V
|
||||||
|
31 H261 V 90000 [RFC4587]
|
||||||
|
32 MPV V 90000 [RFC2250]
|
||||||
|
33 MP2T AV 90000 [RFC2250]
|
||||||
|
34 H263 V 90000 [Chunrong_Zhu]
|
||||||
|
35-71 Unassigned ?
|
||||||
|
72-76 Reserved for RTCP conflict avoidance [RFC3551]
|
||||||
|
77-95 Unassigned ?
|
||||||
|
96-127 dynamic ? [RFC3551]
|
||||||
|
*/
|
||||||
|
//payloadType := packet[1]&0x7f
|
||||||
|
|
||||||
|
streamIndex = blockNo/2
|
||||||
|
stream := self.streams[streamIndex]
|
||||||
|
|
||||||
|
if self.DebugConn {
|
||||||
|
//fmt.Println("packet:", stream.Type(), "offset", payloadOffset, "pt", payloadType)
|
||||||
|
if len(packet)>24 {
|
||||||
|
fmt.Println(hex.Dump(packet[:24]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = stream.handlePacket(timestamp, payload); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Client) Play() (err error) {
|
||||||
|
req := Request{
|
||||||
|
Method: "PLAY",
|
||||||
|
Uri: self.requestUri,
|
||||||
|
}
|
||||||
|
req.Header = append(req.Header, "Session: "+self.session)
|
||||||
|
if err = self.WriteRequest(req); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Client) ReadPacket() (streamIndex int, pkt av.Packet, err error) {
|
||||||
|
for {
|
||||||
|
var res Response
|
||||||
|
if res, err = self.ReadResponse(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if res.BlockLength > 0 {
|
||||||
|
if streamIndex, err = self.parseBlock(res.BlockNo, res.Block); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stream := self.streams[streamIndex]
|
||||||
|
if stream.gotpkt {
|
||||||
|
pkt = stream.pkt
|
||||||
|
stream.gotpkt = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
94
sdp/parser.go
Normal file
94
sdp/parser.go
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
package sdp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"strconv"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/base64"
|
||||||
|
"github.com/nareix/av"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Info struct {
|
||||||
|
AVType string
|
||||||
|
Type av.CodecType
|
||||||
|
TimeScale int
|
||||||
|
Control string
|
||||||
|
Rtpmap int
|
||||||
|
Config []byte
|
||||||
|
SpropParameterSets [][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func Decode(content string) (infos []Info) {
|
||||||
|
var info *Info
|
||||||
|
|
||||||
|
for _, line := range strings.Split(content, "\n") {
|
||||||
|
line = strings.Trim(line, "\r")
|
||||||
|
typeval := strings.SplitN(line, "=", 2)
|
||||||
|
if len(typeval) == 2 {
|
||||||
|
fields := strings.Split(typeval[1], " ")
|
||||||
|
|
||||||
|
switch typeval[0] {
|
||||||
|
case "m":
|
||||||
|
if len(fields) > 0 {
|
||||||
|
switch fields[0] {
|
||||||
|
case "audio", "video":
|
||||||
|
infos = append(infos, Info{AVType: fields[0]})
|
||||||
|
info = &infos[len(infos)-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case "a":
|
||||||
|
if info != nil {
|
||||||
|
for _, field := range fields {
|
||||||
|
keyval := strings.Split(field, ":")
|
||||||
|
if len(keyval) >= 2 {
|
||||||
|
key := keyval[0]
|
||||||
|
val := keyval[1]
|
||||||
|
switch key {
|
||||||
|
case "control":
|
||||||
|
info.Control = val
|
||||||
|
case "rtpmap":
|
||||||
|
info.Rtpmap, _ = strconv.Atoi(val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
keyval = strings.Split(field, "/")
|
||||||
|
if len(keyval) >= 2 {
|
||||||
|
key := keyval[0]
|
||||||
|
switch key {
|
||||||
|
case "MPEG4-GENERIC":
|
||||||
|
info.Type = av.AAC
|
||||||
|
info.TimeScale, _ = strconv.Atoi(keyval[1])
|
||||||
|
case "H264":
|
||||||
|
info.Type = av.H264
|
||||||
|
info.TimeScale, _ = strconv.Atoi(keyval[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
keyval = strings.Split(field, ";")
|
||||||
|
if len(keyval) > 1 {
|
||||||
|
for _, field := range keyval {
|
||||||
|
keyval := strings.SplitN(field, "=", 2)
|
||||||
|
if len(keyval) == 2 {
|
||||||
|
key := keyval[0]
|
||||||
|
val := keyval[1]
|
||||||
|
switch key {
|
||||||
|
case "config":
|
||||||
|
info.Config, _ = hex.DecodeString(val)
|
||||||
|
case "sprop-parameter-sets":
|
||||||
|
fields := strings.Split(val, ",")
|
||||||
|
for _, field := range fields {
|
||||||
|
val, _ := base64.StdEncoding.DecodeString(field)
|
||||||
|
info.SpropParameterSets = append(info.SpropParameterSets, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
36
sdp/parser_test.go
Normal file
36
sdp/parser_test.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package sdp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParse(t *testing.T) {
|
||||||
|
infos := Decode(`
|
||||||
|
v=0
|
||||||
|
o=- 1459325504777324 1 IN IP4 192.168.0.123
|
||||||
|
s=RTSP/RTP stream from Network Video Server
|
||||||
|
i=mpeg4cif
|
||||||
|
t=0 0
|
||||||
|
a=tool:LIVE555 Streaming Media v2009.09.28
|
||||||
|
a=type:broadcast
|
||||||
|
a=control:*
|
||||||
|
a=range:npt=0-
|
||||||
|
a=x-qt-text-nam:RTSP/RTP stream from Network Video Server
|
||||||
|
a=x-qt-text-inf:mpeg4cif
|
||||||
|
m=video 0 RTP/AVP 96
|
||||||
|
c=IN IP4 0.0.0.0
|
||||||
|
b=AS:300
|
||||||
|
a=rtpmap:96 H264/90000
|
||||||
|
a=fmtp:96 packetization-mode=1;profile-level-id=640028;sprop-parameter-sets=Z2QAKK2EBUViuKxUdCAqKxXFYqOhAVFYrisVHQgKisVxWKjoQFRWK4rFR0ICorFcVio6ECSFITk8nyfk/k/J8nm5s00IEkKQnJ5Pk/J/J+T5PNzZprQFoe0qQAAAHgAABDgYEABJPAAUmW974XhEI1A=,aO48sA==;config=0000000167640028ad84054562b8ac5474202a2b15c562a3a1015158ae2b151d080a8ac57158a8e84054562b8ac5474202a2b15c562a3a10248521393c9f27e4fe4fc9f279b9b34d081242909c9e4f93f27f27e4f93cdcd9a6b405a1ed2a4000001e00000438181000493c0014996f7be1784423500000000168ee3cb0
|
||||||
|
a=x-dimensions: 720, 480
|
||||||
|
a=x-framerate: 15
|
||||||
|
a=control:track1
|
||||||
|
m=audio 0 RTP/AVP 96
|
||||||
|
c=IN IP4 0.0.0.0
|
||||||
|
b=AS:256
|
||||||
|
a=rtpmap:96 MPEG4-GENERIC/16000/2
|
||||||
|
a=fmtp:96 streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1408
|
||||||
|
a=control:track2
|
||||||
|
`)
|
||||||
|
t.Logf("%v", infos)
|
||||||
|
}
|
16
stream.go
16
stream.go
@ -2,19 +2,27 @@ package rtsp
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/nareix/av"
|
"github.com/nareix/av"
|
||||||
|
"github.com/nareix/rtsp/sdp"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Stream struct {
|
type Stream struct {
|
||||||
av.StreamCommon
|
av.StreamCommon
|
||||||
typestr string
|
Sdp sdp.Info
|
||||||
control string
|
|
||||||
|
// h264
|
||||||
|
fuBuffer []byte
|
||||||
|
sps []byte
|
||||||
|
pps []byte
|
||||||
|
|
||||||
|
gotpkt bool
|
||||||
|
pkt av.Packet
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self Stream) IsAudio() bool {
|
func (self Stream) IsAudio() bool {
|
||||||
return self.typestr == "audio"
|
return self.Sdp.AVType == "audio"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self Stream) IsVideo() bool {
|
func (self Stream) IsVideo() bool {
|
||||||
return self.typestr == "video"
|
return self.Sdp.AVType == "video"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user