12 changed files with 27 additions and 725 deletions
@ -1,11 +1,11 @@
@@ -1,11 +1,11 @@
|
||||
module rtsp-server |
||||
module rtsp-simple-server |
||||
|
||||
go 1.13 |
||||
|
||||
require ( |
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect |
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect |
||||
github.com/stretchr/testify v1.4.0 |
||||
github.com/aler9/gortsplib v0.0.0-20200120091821-97304167de21 |
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6 |
||||
gortc.io/sdp v0.17.0 |
||||
) |
||||
|
@ -1,90 +0,0 @@
@@ -1,90 +0,0 @@
|
||||
package rtsp |
||||
|
||||
import ( |
||||
"encoding/binary" |
||||
"fmt" |
||||
"io" |
||||
"net" |
||||
) |
||||
|
||||
type Conn struct { |
||||
c net.Conn |
||||
writeBuf []byte |
||||
} |
||||
|
||||
func NewConn(c net.Conn) *Conn { |
||||
return &Conn{ |
||||
c: c, |
||||
writeBuf: make([]byte, 2048), |
||||
} |
||||
} |
||||
|
||||
func (c *Conn) Close() error { |
||||
return c.c.Close() |
||||
} |
||||
|
||||
func (c *Conn) RemoteAddr() net.Addr { |
||||
return c.c.RemoteAddr() |
||||
} |
||||
|
||||
func (c *Conn) ReadRequest() (*Request, error) { |
||||
return requestDecode(c.c) |
||||
} |
||||
|
||||
func (c *Conn) WriteRequest(req *Request) error { |
||||
return requestEncode(c.c, req) |
||||
} |
||||
|
||||
func (c *Conn) ReadResponse() (*Response, error) { |
||||
return responseDecode(c.c) |
||||
} |
||||
|
||||
func (c *Conn) WriteResponse(res *Response) error { |
||||
return responseEncode(c.c, res) |
||||
} |
||||
|
||||
func (c *Conn) ReadInterleavedFrame(frame []byte) (int, int, error) { |
||||
var header [4]byte |
||||
_, err := io.ReadFull(c.c, header[:]) |
||||
if err != nil { |
||||
return 0, 0, err |
||||
} |
||||
|
||||
// connection terminated
|
||||
if header[0] == 0x54 { |
||||
return 0, 0, io.EOF |
||||
} |
||||
|
||||
if header[0] != 0x24 { |
||||
return 0, 0, fmt.Errorf("wrong magic byte (0x%.2x)", header[0]) |
||||
} |
||||
|
||||
framelen := binary.BigEndian.Uint16(header[2:]) |
||||
if framelen > 2048 { |
||||
return 0, 0, fmt.Errorf("frame length greater than 2048") |
||||
} |
||||
|
||||
_, err = io.ReadFull(c.c, frame[:framelen]) |
||||
if err != nil { |
||||
return 0, 0, err |
||||
} |
||||
|
||||
return int(header[1]), int(framelen), nil |
||||
} |
||||
|
||||
func (c *Conn) WriteInterleavedFrame(channel int, frame []byte) error { |
||||
c.writeBuf[0] = 0x24 |
||||
c.writeBuf[1] = byte(channel) |
||||
binary.BigEndian.PutUint16(c.writeBuf[2:], uint16(len(frame))) |
||||
n := copy(c.writeBuf[4:], frame) |
||||
|
||||
_, err := c.c.Write(c.writeBuf[:4+n]) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func (c *Conn) Read(buf []byte) (int, error) { |
||||
return c.c.Read(buf) |
||||
} |
@ -1,88 +0,0 @@
@@ -1,88 +0,0 @@
|
||||
package rtsp |
||||
|
||||
import ( |
||||
"bufio" |
||||
"fmt" |
||||
"io" |
||||
) |
||||
|
||||
type Request struct { |
||||
Method string |
||||
Url string |
||||
Headers map[string]string |
||||
Content []byte |
||||
} |
||||
|
||||
func requestDecode(r io.Reader) (*Request, error) { |
||||
rb := bufio.NewReader(r) |
||||
|
||||
req := &Request{} |
||||
|
||||
byts, err := readBytesLimited(rb, ' ', 255) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
req.Method = string(byts[:len(byts)-1]) |
||||
|
||||
if len(req.Method) == 0 { |
||||
return nil, fmt.Errorf("empty method") |
||||
} |
||||
|
||||
byts, err = readBytesLimited(rb, ' ', 255) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
req.Url = string(byts[:len(byts)-1]) |
||||
|
||||
if len(req.Url) == 0 { |
||||
return nil, fmt.Errorf("empty path") |
||||
} |
||||
|
||||
byts, err = readBytesLimited(rb, '\r', 255) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
proto := string(byts[:len(byts)-1]) |
||||
|
||||
if proto != _RTSP_PROTO { |
||||
return nil, fmt.Errorf("expected '%s', got '%s'", _RTSP_PROTO, proto) |
||||
} |
||||
|
||||
err = readByteEqual(rb, '\n') |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
req.Headers, err = readHeaders(rb) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
req.Content, err = readContent(rb, req.Headers) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return req, nil |
||||
} |
||||
|
||||
func requestEncode(w io.Writer, req *Request) error { |
||||
wb := bufio.NewWriter(w) |
||||
|
||||
_, err := wb.Write([]byte(req.Method + " " + req.Url + " " + _RTSP_PROTO + "\r\n")) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = writeHeaders(wb, req.Headers) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = writeContent(wb, req.Content) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
return wb.Flush() |
||||
} |
@ -1,134 +0,0 @@
@@ -1,134 +0,0 @@
|
||||
package rtsp |
||||
|
||||
import ( |
||||
"bytes" |
||||
"testing" |
||||
|
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
var casesRequest = []struct { |
||||
name string |
||||
byts []byte |
||||
req *Request |
||||
}{ |
||||
{ |
||||
"options", |
||||
[]byte("OPTIONS rtsp://example.com/media.mp4 RTSP/1.0\r\n" + |
||||
"CSeq: 1\r\n" + |
||||
"Proxy-Require: gzipped-messages\r\n" + |
||||
"Require: implicit-play\r\n" + |
||||
"\r\n"), |
||||
&Request{ |
||||
Method: "OPTIONS", |
||||
Url: "rtsp://example.com/media.mp4", |
||||
Headers: map[string]string{ |
||||
"CSeq": "1", |
||||
"Require": "implicit-play", |
||||
"Proxy-Require": "gzipped-messages", |
||||
}, |
||||
}, |
||||
}, |
||||
{ |
||||
"describe", |
||||
[]byte("DESCRIBE rtsp://example.com/media.mp4 RTSP/1.0\r\n" + |
||||
"CSeq: 2\r\n" + |
||||
"\r\n"), |
||||
&Request{ |
||||
Method: "DESCRIBE", |
||||
Url: "rtsp://example.com/media.mp4", |
||||
Headers: map[string]string{ |
||||
"CSeq": "2", |
||||
}, |
||||
}, |
||||
}, |
||||
{ |
||||
"announce", |
||||
[]byte("ANNOUNCE rtsp://example.com/media.mp4 RTSP/1.0\r\n" + |
||||
"CSeq: 7\r\n" + |
||||
"Content-Length: 306\r\n" + |
||||
"Content-Type: application/sdp\r\n" + |
||||
"Date: 23 Jan 1997 15:35:06 GMT\r\n" + |
||||
"Session: 12345678\r\n" + |
||||
"\r\n" + |
||||
"v=0\n" + |
||||
"o=mhandley 2890844526 2890845468 IN IP4 126.16.64.4\n" + |
||||
"s=SDP Seminar\n" + |
||||
"i=A Seminar on the session description protocol\n" + |
||||
"u=http://www.cs.ucl.ac.uk/staff/M.Handley/sdp.03.ps\n" + |
||||
"e=mjh@isi.edu (Mark Handley)\n" + |
||||
"c=IN IP4 224.2.17.12/127\n" + |
||||
"t=2873397496 2873404696\n" + |
||||
"a=recvonly\n" + |
||||
"m=audio 3456 RTP/AVP 0\n" + |
||||
"m=video 2232 RTP/AVP 31\n"), |
||||
&Request{ |
||||
Method: "ANNOUNCE", |
||||
Url: "rtsp://example.com/media.mp4", |
||||
Headers: map[string]string{ |
||||
"CSeq": "7", |
||||
"Date": "23 Jan 1997 15:35:06 GMT", |
||||
"Session": "12345678", |
||||
"Content-Type": "application/sdp", |
||||
"Content-Length": "306", |
||||
}, |
||||
Content: []byte("v=0\n" + |
||||
"o=mhandley 2890844526 2890845468 IN IP4 126.16.64.4\n" + |
||||
"s=SDP Seminar\n" + |
||||
"i=A Seminar on the session description protocol\n" + |
||||
"u=http://www.cs.ucl.ac.uk/staff/M.Handley/sdp.03.ps\n" + |
||||
"e=mjh@isi.edu (Mark Handley)\n" + |
||||
"c=IN IP4 224.2.17.12/127\n" + |
||||
"t=2873397496 2873404696\n" + |
||||
"a=recvonly\n" + |
||||
"m=audio 3456 RTP/AVP 0\n" + |
||||
"m=video 2232 RTP/AVP 31\n", |
||||
), |
||||
}, |
||||
}, |
||||
{ |
||||
"get_parameter", |
||||
[]byte("GET_PARAMETER rtsp://example.com/media.mp4 RTSP/1.0\r\n" + |
||||
"CSeq: 9\r\n" + |
||||
"Content-Length: 24\r\n" + |
||||
"Content-Type: text/parameters\r\n" + |
||||
"Session: 12345678\r\n" + |
||||
"\r\n" + |
||||
"packets_received\n" + |
||||
"jitter\n"), |
||||
&Request{ |
||||
Method: "GET_PARAMETER", |
||||
Url: "rtsp://example.com/media.mp4", |
||||
Headers: map[string]string{ |
||||
"CSeq": "9", |
||||
"Content-Type": "text/parameters", |
||||
"Session": "12345678", |
||||
"Content-Length": "24", |
||||
}, |
||||
Content: []byte("packets_received\n" + |
||||
"jitter\n", |
||||
), |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
func TestRequestDecode(t *testing.T) { |
||||
for _, c := range casesRequest { |
||||
t.Run(c.name, func(t *testing.T) { |
||||
req, err := requestDecode(bytes.NewBuffer(c.byts)) |
||||
require.NoError(t, err) |
||||
require.Equal(t, c.req, req) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestRequestEncode(t *testing.T) { |
||||
for _, c := range casesRequest { |
||||
t.Run(c.name, func(t *testing.T) { |
||||
var buf bytes.Buffer |
||||
err := requestEncode(&buf, c.req) |
||||
require.NoError(t, err) |
||||
require.Equal(t, c.byts, buf.Bytes()) |
||||
}) |
||||
} |
||||
} |
@ -1,95 +0,0 @@
@@ -1,95 +0,0 @@
|
||||
package rtsp |
||||
|
||||
import ( |
||||
"bufio" |
||||
"fmt" |
||||
"io" |
||||
"strconv" |
||||
) |
||||
|
||||
type Response struct { |
||||
StatusCode int |
||||
Status string |
||||
Headers map[string]string |
||||
Content []byte |
||||
} |
||||
|
||||
func responseDecode(r io.Reader) (*Response, error) { |
||||
rb := bufio.NewReader(r) |
||||
|
||||
res := &Response{} |
||||
|
||||
byts, err := readBytesLimited(rb, ' ', 255) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
proto := string(byts[:len(byts)-1]) |
||||
|
||||
if proto != _RTSP_PROTO { |
||||
return nil, fmt.Errorf("expected '%s', got '%s'", _RTSP_PROTO, proto) |
||||
} |
||||
|
||||
byts, err = readBytesLimited(rb, ' ', 4) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
statusCodeStr := string(byts[:len(byts)-1]) |
||||
|
||||
statusCode64, err := strconv.ParseInt(statusCodeStr, 10, 32) |
||||
res.StatusCode = int(statusCode64) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("unable to parse status code") |
||||
} |
||||
|
||||
byts, err = readBytesLimited(rb, '\r', 255) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
res.Status = string(byts[:len(byts)-1]) |
||||
|
||||
if len(res.Status) == 0 { |
||||
return nil, fmt.Errorf("empty status") |
||||
} |
||||
|
||||
err = readByteEqual(rb, '\n') |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
res.Headers, err = readHeaders(rb) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
res.Content, err = readContent(rb, res.Headers) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return res, nil |
||||
} |
||||
|
||||
func responseEncode(w io.Writer, res *Response) error { |
||||
wb := bufio.NewWriter(w) |
||||
|
||||
_, err := wb.Write([]byte(_RTSP_PROTO + " " + strconv.FormatInt(int64(res.StatusCode), 10) + " " + res.Status + "\r\n")) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
if len(res.Content) != 0 { |
||||
res.Headers["Content-Length"] = strconv.FormatInt(int64(len(res.Content)), 10) |
||||
} |
||||
|
||||
err = writeHeaders(wb, res.Headers) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = writeContent(wb, res.Content) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
return wb.Flush() |
||||
} |
@ -1,105 +0,0 @@
@@ -1,105 +0,0 @@
|
||||
package rtsp |
||||
|
||||
import ( |
||||
"bytes" |
||||
"testing" |
||||
|
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
var casesResponse = []struct { |
||||
name string |
||||
byts []byte |
||||
res *Response |
||||
}{ |
||||
{ |
||||
"ok", |
||||
[]byte("RTSP/1.0 200 OK\r\n" + |
||||
"CSeq: 1\r\n" + |
||||
"Public: DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE\r\n" + |
||||
"\r\n", |
||||
), |
||||
&Response{ |
||||
StatusCode: 200, |
||||
Status: "OK", |
||||
Headers: map[string]string{ |
||||
"CSeq": "1", |
||||
"Public": "DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE", |
||||
}, |
||||
}, |
||||
}, |
||||
{ |
||||
"ok with content", |
||||
[]byte("RTSP/1.0 200 OK\r\n" + |
||||
"CSeq: 2\r\n" + |
||||
"Content-Base: rtsp://example.com/media.mp4\r\n" + |
||||
"Content-Length: 444\r\n" + |
||||
"Content-Type: application/sdp\r\n" + |
||||
"\r\n" + |
||||
"m=video 0 RTP/AVP 96\n" + |
||||
"a=control:streamid=0\n" + |
||||
"a=range:npt=0-7.741000\n" + |
||||
"a=length:npt=7.741000\n" + |
||||
"a=rtpmap:96 MP4V-ES/5544\n" + |
||||
"a=mimetype:string;\"video/MP4V-ES\"\n" + |
||||
"a=AvgBitRate:integer;304018\n" + |
||||
"a=StreamName:string;\"hinted video track\"\n" + |
||||
"m=audio 0 RTP/AVP 97\n" + |
||||
"a=control:streamid=1\n" + |
||||
"a=range:npt=0-7.712000\n" + |
||||
"a=length:npt=7.712000\n" + |
||||
"a=rtpmap:97 mpeg4-generic/32000/2\n" + |
||||
"a=mimetype:string;\"audio/mpeg4-generic\"\n" + |
||||
"a=AvgBitRate:integer;65790\n" + |
||||
"a=StreamName:string;\"hinted audio track\"\n", |
||||
), |
||||
&Response{ |
||||
StatusCode: 200, |
||||
Status: "OK", |
||||
Headers: map[string]string{ |
||||
"Content-Base": "rtsp://example.com/media.mp4", |
||||
"Content-Length": "444", |
||||
"Content-Type": "application/sdp", |
||||
"CSeq": "2", |
||||
}, |
||||
Content: []byte("m=video 0 RTP/AVP 96\n" + |
||||
"a=control:streamid=0\n" + |
||||
"a=range:npt=0-7.741000\n" + |
||||
"a=length:npt=7.741000\n" + |
||||
"a=rtpmap:96 MP4V-ES/5544\n" + |
||||
"a=mimetype:string;\"video/MP4V-ES\"\n" + |
||||
"a=AvgBitRate:integer;304018\n" + |
||||
"a=StreamName:string;\"hinted video track\"\n" + |
||||
"m=audio 0 RTP/AVP 97\n" + |
||||
"a=control:streamid=1\n" + |
||||
"a=range:npt=0-7.712000\n" + |
||||
"a=length:npt=7.712000\n" + |
||||
"a=rtpmap:97 mpeg4-generic/32000/2\n" + |
||||
"a=mimetype:string;\"audio/mpeg4-generic\"\n" + |
||||
"a=AvgBitRate:integer;65790\n" + |
||||
"a=StreamName:string;\"hinted audio track\"\n", |
||||
), |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
func TestResponseDecode(t *testing.T) { |
||||
for _, c := range casesResponse { |
||||
t.Run(c.name, func(t *testing.T) { |
||||
res, err := responseDecode(bytes.NewBuffer(c.byts)) |
||||
require.NoError(t, err) |
||||
require.Equal(t, c.res, res) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestResponseEncode(t *testing.T) { |
||||
for _, c := range casesResponse { |
||||
t.Run(c.name, func(t *testing.T) { |
||||
var buf bytes.Buffer |
||||
err := responseEncode(&buf, c.res) |
||||
require.NoError(t, err) |
||||
require.Equal(t, c.byts, buf.Bytes()) |
||||
}) |
||||
} |
||||
} |
@ -1,161 +0,0 @@
@@ -1,161 +0,0 @@
|
||||
package rtsp |
||||
|
||||
import ( |
||||
"bufio" |
||||
"fmt" |
||||
"io" |
||||
"sort" |
||||
"strconv" |
||||
) |
||||
|
||||
const ( |
||||
_RTSP_PROTO = "RTSP/1.0" |
||||
_MAX_HEADER_COUNT = 255 |
||||
_MAX_HEADER_KEY_LENGTH = 255 |
||||
_MAX_HEADER_VALUE_LENGTH = 255 |
||||
_MAX_CONTENT_LENGTH = 4096 |
||||
) |
||||
|
||||
func readBytesLimited(rb *bufio.Reader, delim byte, n int) ([]byte, error) { |
||||
for i := 1; i <= n; i++ { |
||||
byts, err := rb.Peek(i) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
if byts[len(byts)-1] == delim { |
||||
rb.Discard(len(byts)) |
||||
return byts, nil |
||||
} |
||||
} |
||||
return nil, fmt.Errorf("buffer length exceeds %d", n) |
||||
} |
||||
|
||||
func readByteEqual(rb *bufio.Reader, cmp byte) error { |
||||
byt, err := rb.ReadByte() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
if byt != cmp { |
||||
return fmt.Errorf("expected '%c', got '%c'", cmp, byt) |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func readHeaders(rb *bufio.Reader) (map[string]string, error) { |
||||
ret := make(map[string]string) |
||||
|
||||
for { |
||||
byt, err := rb.ReadByte() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
if byt == '\r' { |
||||
err := readByteEqual(rb, '\n') |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
break |
||||
} |
||||
|
||||
if len(ret) >= _MAX_HEADER_COUNT { |
||||
return nil, fmt.Errorf("headers count exceeds %d", _MAX_HEADER_COUNT) |
||||
} |
||||
|
||||
key := string([]byte{byt}) |
||||
byts, err := readBytesLimited(rb, ':', _MAX_HEADER_KEY_LENGTH-1) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
key += string(byts[:len(byts)-1]) |
||||
|
||||
err = readByteEqual(rb, ' ') |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
byts, err = readBytesLimited(rb, '\r', _MAX_HEADER_VALUE_LENGTH) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
val := string(byts[:len(byts)-1]) |
||||
|
||||
if len(val) == 0 { |
||||
return nil, fmt.Errorf("empty header value") |
||||
} |
||||
|
||||
err = readByteEqual(rb, '\n') |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
ret[key] = val |
||||
} |
||||
|
||||
return ret, nil |
||||
} |
||||
|
||||
func writeHeaders(wb *bufio.Writer, headers map[string]string) error { |
||||
// sort headers by key
|
||||
// in order to obtain deterministic results
|
||||
var keys []string |
||||
for key := range headers { |
||||
keys = append(keys, key) |
||||
} |
||||
sort.Strings(keys) |
||||
|
||||
for _, key := range keys { |
||||
_, err := wb.Write([]byte(key + ": " + headers[key] + "\r\n")) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
|
||||
_, err := wb.Write([]byte("\r\n")) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func readContent(rb *bufio.Reader, headers map[string]string) ([]byte, error) { |
||||
cls, ok := headers["Content-Length"] |
||||
if !ok { |
||||
return nil, nil |
||||
} |
||||
|
||||
cl, err := strconv.ParseInt(cls, 10, 64) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("invalid Content-Length") |
||||
} |
||||
|
||||
if cl > _MAX_CONTENT_LENGTH { |
||||
return nil, fmt.Errorf("Content-Length exceeds %d", _MAX_CONTENT_LENGTH) |
||||
} |
||||
|
||||
ret := make([]byte, cl) |
||||
n, err := io.ReadFull(rb, ret) |
||||
if err != nil && n != len(ret) { |
||||
return nil, err |
||||
} |
||||
|
||||
return ret, nil |
||||
} |
||||
|
||||
func writeContent(wb *bufio.Writer, content []byte) error { |
||||
if len(content) == 0 { |
||||
return nil |
||||
} |
||||
|
||||
_, err := wb.Write(content) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
} |
Loading…
Reference in new issue