mirror of
https://github.com/aler9/gortsplib
synced 2025-12-24 13:38:08 +08:00
initial commit
This commit is contained in:
1
.dockerignore
Normal file
1
.dockerignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.git
|
||||||
14
.travis.yml
Normal file
14
.travis.yml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
language: go
|
||||||
|
|
||||||
|
go:
|
||||||
|
- "1.12.x"
|
||||||
|
- "1.13.x"
|
||||||
|
|
||||||
|
env:
|
||||||
|
- GO111MODULE=on
|
||||||
|
|
||||||
|
before_install:
|
||||||
|
- go mod download
|
||||||
|
|
||||||
|
script:
|
||||||
|
- make test-nodocker
|
||||||
45
Makefile
Normal file
45
Makefile
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
|
||||||
|
.PHONY: $(shell ls)
|
||||||
|
|
||||||
|
BASE_IMAGE = amd64/golang:1.13-alpine3.10
|
||||||
|
|
||||||
|
help:
|
||||||
|
@echo "usage: make [action]"
|
||||||
|
@echo ""
|
||||||
|
@echo "available actions:"
|
||||||
|
@echo ""
|
||||||
|
@echo " mod-tidy run go mod tidy"
|
||||||
|
@echo " format format source files"
|
||||||
|
@echo " test run available tests"
|
||||||
|
@echo ""
|
||||||
|
|
||||||
|
mod-tidy:
|
||||||
|
docker run --rm -it -v $(PWD):/s $(BASE_IMAGE) \
|
||||||
|
sh -c "apk add git && cd /s && go get && go mod tidy"
|
||||||
|
|
||||||
|
format:
|
||||||
|
docker run --rm -it -v $(PWD):/s $(BASE_IMAGE) \
|
||||||
|
sh -c "cd /s && find . -type f -name '*.go' | xargs gofmt -l -w -s"
|
||||||
|
|
||||||
|
define DOCKERFILE_TEST
|
||||||
|
FROM $(BASE_IMAGE)
|
||||||
|
RUN apk add --no-cache make git
|
||||||
|
WORKDIR /s
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
COPY . ./
|
||||||
|
endef
|
||||||
|
export DOCKERFILE_TEST
|
||||||
|
|
||||||
|
test:
|
||||||
|
echo "$$DOCKERFILE_TEST" | docker build -q . -f - -t temp
|
||||||
|
docker run --rm -it \
|
||||||
|
--name temp \
|
||||||
|
temp \
|
||||||
|
make test-nodocker
|
||||||
|
|
||||||
|
IMAGES = $(shell echo test-images/*/ | xargs -n1 basename)
|
||||||
|
|
||||||
|
test-nodocker:
|
||||||
|
$(eval export CGO_ENABLED = 0)
|
||||||
|
go test -v .
|
||||||
10
README.md
10
README.md
@@ -1,2 +1,10 @@
|
|||||||
|
|
||||||
# gortsplib
|
# gortsplib
|
||||||
RTSP primitives for the go programming language
|
|
||||||
|
[](https://godoc.org/github.com/aler9/gortsplib)
|
||||||
|
[](https://goreportcard.com/report/github.com/aler9/gortsplib)
|
||||||
|
[](https://travis-ci.org/aler9/gortsplib)
|
||||||
|
|
||||||
|
RTSP primitives for the Go programming language.
|
||||||
|
|
||||||
|
See [rtsp-simple-server](https://github.com/aler9/rtsp-simple-server) and [rtsp-simple-proxy](https://github.com/aler9/rtsp-simple-proxy) for examples on how to use this library.
|
||||||
|
|||||||
90
conn.go
Normal file
90
conn.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package gortsplib
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
5
go.mod
Normal file
5
go.mod
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module github.com/aler9/gortsplib
|
||||||
|
|
||||||
|
go 1.13
|
||||||
|
|
||||||
|
require github.com/stretchr/testify v1.4.0
|
||||||
11
go.sum
Normal file
11
go.sum
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
88
request.go
Normal file
88
request.go
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
package gortsplib
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
134
request_test.go
Normal file
134
request_test.go
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
package gortsplib
|
||||||
|
|
||||||
|
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())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
95
response.go
Normal file
95
response.go
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
package gortsplib
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
105
response_test.go
Normal file
105
response_test.go
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
package gortsplib
|
||||||
|
|
||||||
|
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())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
161
utils.go
Normal file
161
utils.go
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
package gortsplib
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user