commit baedec4c102a6e02db8236e10c2733080f2ba3e2
parent c2edff6142980892930d56b4d97f7aa4a0650f4f
Author: Oliver Lowe <o@olowe.co>
Date: Fri, 5 Jul 2024 15:41:47 +1000
sdp: Parse option media description fields
The parsing logic has become pretty tricky! Tests pass, so refactoring
can be done pretty safely as long as we still get the Session struct
out that we expect.
Diffstat:
| M | sdp/sdp.go | | | 111 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------ |
| M | sdp/sdp_test.go | | | 28 | ++++++++++++++++++++++++---- |
2 files changed, 119 insertions(+), 20 deletions(-)
diff --git a/sdp/sdp.go b/sdp/sdp.go
@@ -17,12 +17,14 @@ type Session struct {
Origin Origin
Name string
- Info string
- URI *url.URL
- Email *mail.Address
- Phone string
- Bandwidth *Bandwidth
- Media []Media
+ Info string
+ URI *url.URL
+ Email *mail.Address
+ Phone string
+ Connection *ConnInfo
+ Bandwidth *Bandwidth
+ Media []Media
+ Attributes []string
// TODO(otl): add rest of fields
}
@@ -34,7 +36,9 @@ type Origin struct {
Address string // IPv4, IPv6 literal or a hostname
}
-var fchars = [...]string{"i", "u", "e", "p", "c", "b", "t", "r", "z", "m"}
+var fchars = [...]string{"i", "u", "e", "p", "c", "b", "t", "r", "z", "m", "a"}
+
+var mchars = [...]string{"i", "c", "b", "a", "m"}
func ReadSession(rd io.Reader) (*Session, error) {
session, sc, err := readSession(rd)
@@ -44,7 +48,7 @@ func ReadSession(rd io.Reader) (*Session, error) {
// Time for optional fields. We keep a slice...
// TODO(otl): document in plain language what's going on here.
- onext := fchars[:]
+ next := fchars[:]
for sc.Scan() {
if sc.Text() == "" {
return nil, fmt.Errorf("illegal empty line")
@@ -55,52 +59,121 @@ func ReadSession(rd io.Reader) (*Session, error) {
}
var allowed bool
- for i := range onext {
- if onext[i] == k {
+ for i := range next {
+ if next[i] == k {
allowed = true
}
}
if !allowed {
- return nil, fmt.Errorf("unexpected field %q: expected one of %q", k, onext)
+ return nil, fmt.Errorf("unexpected field %q: expected one of %q", k, next)
}
switch k {
case "i":
session.Info = v
- onext = fchars[1:]
+ next = fchars[1:]
case "u":
u, err := url.Parse(v)
if err != nil {
return nil, fmt.Errorf("parse uri: %w", err)
}
session.URI = u
- onext = fchars[2:]
+ next = fchars[2:]
case "e":
addr, err := parseEmail(v)
if err != nil {
return nil, fmt.Errorf("parse email: %w", err)
}
session.Email = addr
- onext = fchars[3:]
+ next = fchars[3:]
case "p":
session.Phone = cleanPhone(v)
- onext = fchars[4:]
+ next = fchars[4:]
+ case "c":
+ conn, err := parseConnInfo(v)
+ if err != nil {
+ return nil, fmt.Errorf("parse connection info: %w", err)
+ }
+ session.Connection = &conn
+ next = fchars[5:]
case "b":
bw, err := parseBandwidth(v)
if err != nil {
return nil, fmt.Errorf("parse bandwidth line %q: %w", v, err)
}
session.Bandwidth = &bw
- onext = fchars[5:]
+ next = fchars[6:]
+ case "a":
+ session.Attributes = strings.Fields(v)
+ next = fchars[7:]
case "m":
m, err := parseMedia(v)
if err != nil {
return nil, fmt.Errorf("parse media info from %q: %w", v, err)
}
session.Media = append(session.Media, m)
+ next = mchars[:]
+ goto Media
}
}
+Media:
+ var media *Media
+ if len(session.Media) > 0 {
+ media = &session.Media[len(session.Media)-1]
+ }
+ for sc.Scan() {
+ if sc.Text() == "" {
+ return nil, fmt.Errorf("illegal empty line")
+ }
+ k, v, found := strings.Cut(sc.Text(), "=")
+ if !found {
+ return nil, fmt.Errorf("parse field %q: missing %q", k, "=")
+ }
+
+ var allowed bool
+ for i := range next {
+ if next[i] == k {
+ allowed = true
+ }
+ }
+ if !allowed {
+ return nil, fmt.Errorf("unexpected field %q: expected one of %q", k, next)
+ }
+
+ switch k {
+ case "i":
+ media.Title = v
+ next = mchars[1:]
+ case "c":
+ conn, err := parseConnInfo(v)
+ if err != nil {
+ return nil, fmt.Errorf("parse connection info: %w", err)
+ }
+ media.Connection = &conn
+ next = mchars[2:]
+ case "b":
+ bw, err := parseBandwidth(v)
+ if err != nil {
+ return nil, fmt.Errorf("parse bandwidth: %w", err)
+ }
+ media.Bandwidth = &bw
+ next = mchars[3:]
+ case "a":
+ media.Attributes = strings.Fields(v)
+ next = mchars[4:]
+ case "m":
+ m, err := parseMedia(v)
+ if err != nil {
+ return nil, fmt.Errorf("parse media description: %w", err)
+ }
+ session.Media = append(session.Media, m)
+ media = &session.Media[len(session.Media)-1]
+ next = mchars[:]
+ default:
+ return nil, fmt.Errorf("unsupported field char %s", k)
+ }
+ }
return session, sc.Err()
}
@@ -227,6 +300,12 @@ type Media struct {
PortCount int
Protocol uint8
Format []string
+ // Optional fields
+ Title string
+ Connection *ConnInfo
+ Bandwidth *Bandwidth
+ // TODO(otl): store as k, v pairs
+ Attributes []string
}
const (
diff --git a/sdp/sdp_test.go b/sdp/sdp_test.go
@@ -27,10 +27,31 @@ func TestReadSession(t *testing.T) {
},
Email: &mail.Address{"Jane Doe", "jane@jdoe.example.com"},
Phone: "+16175556011",
+ Connection: &ConnInfo{
+ Type: "IP4",
+ Address: "198.51.100.1",
+ },
Media: []Media{
- {"audio", 49170, 0, ProtoRTP, []string{"0"}},
- {"audio", 49180, 0, ProtoRTP, []string{"0"}},
- {"video", 51372, 0, ProtoRTP, []string{"99"}},
+ Media{
+ Type: "audio",
+ Port: 49170,
+ Protocol: ProtoRTP,
+ Format: []string{"0"},
+ },
+ Media{
+ Type: "audio",
+ Port: 49180,
+ Protocol: ProtoRTP,
+ Format: []string{"0"},
+ },
+ Media{
+ Type: "video",
+ Port: 51372,
+ Protocol: ProtoRTP,
+ Format: []string{"99"},
+ Connection: &ConnInfo{"IP6", "2001:db8::2", 0, 0},
+ Attributes: []string{"rtpmap:99", "h263-1998/90000"},
+ },
},
},
},
@@ -71,7 +92,6 @@ func TestReadSession(t *testing.T) {
if !reflect.DeepEqual(*session, tt.want) {
t.Errorf("got %+v\nwant %+v\n", *session, tt.want)
}
- t.Errorf("TODO still not parsing all fields")
})
}
}