streaming

Media streaming and broadcast systems in Go
Log | Files | Refs | README | LICENSE

commit e3016940734400ce1534d0b208b08f301cb9a3b7
parent b77e66b7ee9f5346dd68fe44fd01aff3e36b1eeb
Author: Oliver Lowe <o@olowe.co>
Date:   Sat, 29 Jun 2024 22:56:49 +1000

sdp: add package for Session Description Protocol (SDP)

Incomplete. Don't know how to handle optional fields in a description
yet.

Diffstat:
Asdp/sdp.go | 132+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asdp/sdp_test.go | 31+++++++++++++++++++++++++++++++
2 files changed, 163 insertions(+), 0 deletions(-)

diff --git a/sdp/sdp.go b/sdp/sdp.go @@ -0,0 +1,132 @@ +// Package sdp implements a decoder and encoder for +// Session Description Protocol formatted data as specified in +// RFC 8866. +package sdp + +import ( + "bufio" + "fmt" + "io" + "net/mail" + "net/url" + "strconv" + "strings" +) + +type Session struct { + Version int + Origin Origin + Name string + + Info string + URI *url.URL + Email *mail.Address + Phone string + // TODO(otl): add rest of fields +} + +type Origin struct { + Username string + ID int + Version int + Network string // TODO(otl): only "IN" is valid... so int type? + AddressType string // TODO(otl): only "IP4", "IP6" valid... new int type? + Address string +} + +func ReadSession(rd io.Reader) (*Session, error) { + sc := bufio.NewScanner(rd) + next := "v" + var session Session + for sc.Scan() { + if sc.Text() == "" { + continue // TODO(otl): empty lines allowed? + } + k, v, found := strings.Cut(sc.Text(), "=") + if !found { + return nil, fmt.Errorf("parse field %q: missing %q", next, "=") + } + if k != next { + return nil, fmt.Errorf("expected field %q, found %q", next, k) + } + switch k { + case "v": + i, err := strconv.Atoi(v) + if err != nil { + return nil, fmt.Errorf("parse version: %w", err) + } + if i != 0 { + return nil, fmt.Errorf("parse version: unsupported version %d", i) + } + session.Version = i + next = "o" + case "o": + o, err := parseOrigin(v) + if err != nil { + return nil, fmt.Errorf("parse origin: %w", err) + } + session.Origin = o + next = "s" + case "s": + if v == "" { + return nil, fmt.Errorf("empty name") + } + session.Name = v + case "i": + session.Info = v + // TODO(otl): what do we expect next? + case "u": + u, err := url.Parse(v) + if err != nil { + return nil, fmt.Errorf("parse uri: %w", err) + } + session.URI = u + // TODO(otl): what do we expect next? + case "e": + addr, err := parseEmail(v) + if err != nil { + return nil, fmt.Errorf("parse email: %w", err) + } + session.Email = addr + // TODO(otl): what do we expect next? + default: + return &session, fmt.Errorf("unsupported field %q", k) + } + } + return &session, sc.Err() +} + +func parseOrigin(line string) (Origin, error) { + fields := strings.Fields(line) + if len(fields) != 6 { + return Origin{}, fmt.Errorf("need %d fields but only have %d", 6, len(fields)) + } + o := Origin{Username: fields[0]} + var err error + o.ID, err = strconv.Atoi(fields[1]) + if err != nil { + return o, fmt.Errorf("parse session id: %w", err) + } + o.Version, err = strconv.Atoi(fields[2]) + if err != nil { + return o, fmt.Errorf("parse version: %w", err) + } + o.Network = fields[3] + o.AddressType = fields[4] + o.Address = fields[5] + return o, nil +} + +// parseEmail returns the parsed email address from s. +// Addresses should be in RFC 5322 form, for instance +// "Oliver Lowe <o@olowe.co>" or just "o@olowe.co". +// They can also be in the form detailed in the SDP specification, +// for instance +// "Oliver Lowe (o@olowe.co)". +func parseEmail(s string) (*mail.Address, error) { + // Oliver Lowe (o@olowe.co) to RFC 5322 format + // Oliver Lowe <o@olowe.co> + s = strings.ReplaceAll(s, "(", "<") + s = strings.ReplaceAll(s, ")", ">") + return mail.ParseAddress(s) +} diff --git a/sdp/sdp_test.go b/sdp/sdp_test.go @@ -0,0 +1,31 @@ +package sdp + +import ( + "strings" + "testing" +) + +func TestReadSession(t *testing.T) { + s := ` +v=0 +o=jdoe 3724394400 3724394405 IN IP4 198.51.100.1 +s=Call to John Smith +i=SDP Offer #1 +u=http://www.jdoe.example.com/home.html +e=Jane Doe <jane@jdoe.example.com> +p=+1 617 555-6011 +c=IN IP4 198.51.100.1 +t=0 0 +m=audio 49170 RTP/AVP 0 +m=audio 49180 RTP/AVP 0 +m=video 51372 RTP/AVP 99 +c=IN IP6 2001:db8::2 +a=rtpmap:99 h263-1998/90000` + session, err := ReadSession(strings.NewReader(s)) + if err != nil { + t.Fatalf("read session: %v", err) + } + if session.Name != "Call to John Smith" { + t.Errorf("bad name bla bla TODO") + } +}