2019-04-16 16:17:13 +03:00
|
|
|
package rtsp
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bufio"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
|
|
|
"net"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
)
|
|
|
|
|
2019-04-16 17:02:50 +03:00
|
|
|
// rtsp protocol described by:
|
|
|
|
// https://tools.ietf.org/html/rfc7826
|
2019-04-16 16:17:13 +03:00
|
|
|
|
2019-04-16 17:02:50 +03:00
|
|
|
// RTSP methods.
|
2019-04-16 16:17:13 +03:00
|
|
|
const (
|
2019-04-16 17:02:50 +03:00
|
|
|
describe = "DESCRIBE"
|
|
|
|
options = "OPTIONS"
|
|
|
|
play = "PLAY"
|
|
|
|
setup = "SETUP"
|
2019-04-16 16:17:13 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
type Session struct {
|
|
|
|
cSeq int
|
|
|
|
conn net.Conn
|
|
|
|
session string
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewSession() *Session {
|
|
|
|
return &Session{}
|
|
|
|
}
|
|
|
|
|
2019-04-17 02:35:34 +03:00
|
|
|
func (s *Session) Describe(urlStr string) (*Response, error) {
|
|
|
|
return s.writeRequest(describe, urlStr, func(req *request) { req.Header.Add("Accept", "application/sdp") }, nil)
|
2019-04-16 16:17:13 +03:00
|
|
|
}
|
|
|
|
|
2019-04-17 02:35:34 +03:00
|
|
|
func (s *Session) Options(urlStr string) (*Response, error) {
|
|
|
|
return s.writeRequest(options, urlStr, nil, nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Session) Setup(urlStr, transport string) (*Response, error) {
|
|
|
|
return s.writeRequest(
|
|
|
|
setup,
|
|
|
|
urlStr,
|
|
|
|
func(req *request) {
|
|
|
|
req.Header.Add("Transport", transport)
|
|
|
|
},
|
|
|
|
func(resp *Response) {
|
|
|
|
s.session = resp.Header.Get("Session")
|
|
|
|
},
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Session) Play(urlStr, sessionId string) (*Response, error) {
|
|
|
|
return s.writeRequest(play, urlStr, func(req *request) { req.Header.Add("Session", s.session) }, nil)
|
|
|
|
}
|
|
|
|
func (s *Session) writeRequest(method, url string, headerOp func(*request), respOp func(*Response)) (*Response, error) {
|
|
|
|
req, err := newRequest(describe, url, s.nextCSeq(), nil)
|
2019-04-16 16:17:13 +03:00
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
2019-04-17 02:35:34 +03:00
|
|
|
if headerOp != nil {
|
|
|
|
headerOp(req)
|
|
|
|
}
|
2019-04-16 16:17:13 +03:00
|
|
|
|
|
|
|
if s.conn == nil {
|
|
|
|
s.conn, err = net.Dial("tcp", req.URL.Host)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err = io.WriteString(s.conn, req.String())
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2019-04-17 02:35:34 +03:00
|
|
|
res, err := ReadResponse(s.conn)
|
2019-04-16 16:17:13 +03:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2019-04-17 02:35:34 +03:00
|
|
|
if respOp != nil {
|
|
|
|
respOp(res)
|
2019-04-16 16:17:13 +03:00
|
|
|
}
|
2019-04-17 02:35:34 +03:00
|
|
|
return res, nil
|
|
|
|
}
|
2019-04-16 16:17:13 +03:00
|
|
|
|
2019-04-17 02:35:34 +03:00
|
|
|
func (s *Session) nextCSeq() string {
|
|
|
|
s.cSeq++
|
|
|
|
return strconv.Itoa(s.cSeq)
|
|
|
|
}
|
2019-04-16 16:17:13 +03:00
|
|
|
|
2019-04-17 02:35:34 +03:00
|
|
|
type request http.Request
|
2019-04-16 16:17:13 +03:00
|
|
|
|
2019-04-17 02:35:34 +03:00
|
|
|
func newRequest(method, urlStr, cSeq string, body io.ReadCloser) (*request, error) {
|
|
|
|
u, err := url.Parse(urlStr)
|
2019-04-16 16:17:13 +03:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2019-04-17 02:35:34 +03:00
|
|
|
req := &http.Request{
|
|
|
|
Method: method,
|
|
|
|
URL: u,
|
|
|
|
Proto: "RTSP",
|
|
|
|
ProtoMajor: 1,
|
|
|
|
ProtoMinor: 0,
|
|
|
|
Header: map[string][]string{"CSeq": []string{cSeq}},
|
|
|
|
Body: body,
|
2019-04-16 16:17:13 +03:00
|
|
|
}
|
2019-04-17 02:35:34 +03:00
|
|
|
return (*request)(req), nil
|
|
|
|
}
|
2019-04-16 16:17:13 +03:00
|
|
|
|
2019-04-17 02:35:34 +03:00
|
|
|
func (r *request) String() string {
|
|
|
|
s := fmt.Sprintf("%s %s %s/%d.%d\r\n", r.Method, r.URL, r.Proto, r.ProtoMajor, r.ProtoMinor)
|
|
|
|
for k, v := range r.Header {
|
|
|
|
for _, v := range v {
|
|
|
|
s += fmt.Sprintf("%s: %s\r\n", k, v)
|
2019-04-16 16:17:13 +03:00
|
|
|
}
|
|
|
|
}
|
2019-04-17 02:35:34 +03:00
|
|
|
s += "\r\n"
|
|
|
|
if r.Body != nil {
|
|
|
|
str, _ := ioutil.ReadAll(r.Body)
|
|
|
|
s += string(str)
|
2019-04-16 16:17:13 +03:00
|
|
|
}
|
2019-04-17 02:35:34 +03:00
|
|
|
return s
|
2019-04-16 16:17:13 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
type closer struct {
|
|
|
|
*bufio.Reader
|
|
|
|
r io.Reader
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c closer) Close() error {
|
|
|
|
if c.Reader == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
defer func() {
|
|
|
|
c.Reader = nil
|
|
|
|
c.r = nil
|
|
|
|
}()
|
|
|
|
if r, ok := c.r.(io.ReadCloser); ok {
|
|
|
|
return r.Close()
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func ParseRTSPVersion(s string) (proto string, major int, minor int, err error) {
|
|
|
|
parts := strings.SplitN(s, "/", 2)
|
|
|
|
proto = parts[0]
|
|
|
|
parts = strings.SplitN(parts[1], ".", 2)
|
|
|
|
if major, err = strconv.Atoi(parts[0]); err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if minor, err = strconv.Atoi(parts[0]); err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
type Response struct {
|
|
|
|
Proto string
|
|
|
|
ProtoMajor int
|
|
|
|
ProtoMinor int
|
|
|
|
|
|
|
|
StatusCode int
|
|
|
|
Status string
|
|
|
|
|
|
|
|
ContentLength int64
|
|
|
|
|
|
|
|
Header http.Header
|
|
|
|
Body io.ReadCloser
|
|
|
|
}
|
|
|
|
|
|
|
|
func (res Response) String() string {
|
|
|
|
s := fmt.Sprintf("%s/%d.%d %d %s\n", res.Proto, res.ProtoMajor, res.ProtoMinor, res.StatusCode, res.Status)
|
|
|
|
for k, v := range res.Header {
|
|
|
|
for _, v := range v {
|
|
|
|
s += fmt.Sprintf("%s: %s\n", k, v)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return s
|
|
|
|
}
|
|
|
|
|
|
|
|
func ReadResponse(r io.Reader) (res *Response, err error) {
|
|
|
|
res = new(Response)
|
|
|
|
res.Header = make(map[string][]string)
|
|
|
|
|
|
|
|
b := bufio.NewReader(r)
|
|
|
|
var s string
|
|
|
|
|
|
|
|
// TODO: allow CR, LF, or CRLF
|
|
|
|
if s, err = b.ReadString('\n'); err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
parts := strings.SplitN(s, " ", 3)
|
|
|
|
res.Proto, res.ProtoMajor, res.ProtoMinor, err = ParseRTSPVersion(parts[0])
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if res.StatusCode, err = strconv.Atoi(parts[1]); err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
res.Status = strings.TrimSpace(parts[2])
|
|
|
|
|
|
|
|
// read headers
|
|
|
|
for {
|
|
|
|
if s, err = b.ReadString('\n'); err != nil {
|
|
|
|
return
|
|
|
|
} else if s = strings.TrimRight(s, "\r\n"); s == "" {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
parts := strings.SplitN(s, ":", 2)
|
|
|
|
res.Header.Add(strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]))
|
|
|
|
}
|
|
|
|
|
|
|
|
res.ContentLength, _ = strconv.ParseInt(res.Header.Get("Content-Length"), 10, 64)
|
|
|
|
|
|
|
|
res.Body = closer{b, r}
|
|
|
|
return
|
|
|
|
}
|