streaming

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

commit 439c6395b12db4f7329a94e472639a5b55169cf0
parent baedec4c102a6e02db8236e10c2733080f2ba3e2
Author: Oliver Lowe <o@olowe.co>
Date:   Fri,  5 Jul 2024 16:51:16 +1000

sdp: manage parsing state in a parser struct

The ReadSession and readSession functions were getting pretty long,
with break labels and even a goto. Managing the state in the parser
obviates that stuff. It also let us deduplicate much of the scanning
logic e.g. checking for empty/invalid lines.

Diffstat:
Asdp/parser.go | 185+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msdp/sdp.go | 187+++----------------------------------------------------------------------------
2 files changed, 190 insertions(+), 182 deletions(-)

diff --git a/sdp/parser.go b/sdp/parser.go @@ -0,0 +1,184 @@ +package sdp + +import ( + "bufio" + "fmt" + "net/url" + "strconv" + "strings" +) + +type parser struct { + *bufio.Scanner + err error + // Field name and value from the current line. + // TODO(otl): rename? key, value is very non-specific... + key, value string + next []string // expected next field names + + session Session +} + +var ftab = [...]string{"i", "u", "e", "p", "c", "b", "t", "r", "z", "m", "a"} + +var mtab = [...]string{"i", "c", "b", "a", "m"} + +func (p *parser) scan() bool { + if !p.Scan() { + p.err = p.Err() + return false + } + line := strings.TrimSpace(p.Text()) + if line == "" { + p.err = fmt.Errorf("illegal empty line") + return false + } + k, v, found := strings.Cut(line, "=") + if !found { + p.err = fmt.Errorf("parse field %q, missing %q", k, "=") + return false + } + p.key = k + p.value = v + + if p.next != nil { + for i := range p.next { + if p.next[i] == p.key { + return true + } + } + p.err = fmt.Errorf("unexpected field %q: expected one of %q", k, p.next) + return false + } + return true +} + +func (p *parser) parse() error { + next := "v" + for p.scan() { + if p.key != next { + return fmt.Errorf("expected key %q, found %q", next, p.key) + } + switch p.key { + case "v": + i, err := strconv.Atoi(p.value) + if err != nil { + return fmt.Errorf("parse version: %w", err) + } + if i != 0 { + return fmt.Errorf("unsupported version %d", i) + } + next = "o" + case "o": + o, err := parseOrigin(p.value) + if err != nil { + return fmt.Errorf("parse origin: %w", err) + } + p.session.Origin = o + next = "s" + case "s": + if p.value == "" { + return fmt.Errorf("empty name") + } + p.session.Name = p.value + return p.parseOptional() + } + } + return p.err +} + +func (p *parser) parseOptional() error { + p.next = ftab[:] + for p.scan() { + switch p.key { + case "i": + p.session.Info = p.value + p.next = ftab[1:] + case "u": + u, err := url.Parse(p.value) + if err != nil { + return fmt.Errorf("parse uri: %w", err) + } + p.session.URI = u + p.next = ftab[2:] + case "e": + addr, err := parseEmail(p.value) + if err != nil { + return fmt.Errorf("parse email: %w", err) + } + p.session.Email = addr + p.next = ftab[3:] + case "p": + p.session.Phone = cleanPhone(p.value) + p.next = ftab[4:] + case "c": + conn, err := parseConnInfo(p.value) + if err != nil { + return fmt.Errorf("parse connection info: %w", err) + } + p.session.Connection = &conn + p.next = ftab[5:] + case "b": + bw, err := parseBandwidth(p.value) + if err != nil { + return fmt.Errorf("parse bandwidth line %q: %w", p.value, err) + } + p.session.Bandwidth = &bw + p.next = ftab[6:] + case "a": + p.session.Attributes = strings.Fields(p.value) + p.next = ftab[7:] + case "m": + m, err := parseMedia(p.value) + if err != nil { + return fmt.Errorf("parse media info from %q: %w", p.value, err) + } + p.session.Media = append(p.session.Media, m) + p.next = mtab[:] + return p.parseMedia() + } + } + return p.err +} + +func (p *parser) parseMedia() error { + var media *Media + if len(p.session.Media) > 0 { + media = &p.session.Media[len(p.session.Media)-1] + } + for p.scan() { + switch p.key { + case "i": + media.Title = p.value + p.next = mtab[1:] + case "c": + conn, err := parseConnInfo(p.value) + if err != nil { + return fmt.Errorf("parse connection info: %w", err) + } + media.Connection = &conn + p.next = mtab[2:] + case "b": + bw, err := parseBandwidth(p.value) + if err != nil { + return fmt.Errorf("parse bandwidth: %w", err) + } + media.Bandwidth = &bw + p.next = mtab[3:] + case "a": + media.Attributes = strings.Fields(p.value) + p.next = mtab[4:] + case "m": + m, err := parseMedia(p.value) + if err != nil { + return fmt.Errorf("parse media description: %w", err) + } + p.session.Media = append(p.session.Media, m) + media = &p.session.Media[len(p.session.Media)-1] + p.next = mtab[:] + default: + return fmt.Errorf("unsupported field char %s", p.key) + } + } + return p.err +} +\ No newline at end of file diff --git a/sdp/sdp.go b/sdp/sdp.go @@ -23,8 +23,8 @@ type Session struct { Phone string Connection *ConnInfo Bandwidth *Bandwidth - Media []Media Attributes []string + Media []Media // TODO(otl): add rest of fields } @@ -36,189 +36,12 @@ type Origin struct { Address string // IPv4, IPv6 literal or a hostname } -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) - if err != nil { - return nil, err - } - - // Time for optional fields. We keep a slice... - // TODO(otl): document in plain language what's going on here. - next := fchars[:] - 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": - session.Info = v - next = fchars[1:] - case "u": - u, err := url.Parse(v) - if err != nil { - return nil, fmt.Errorf("parse uri: %w", err) - } - session.URI = u - next = fchars[2:] - case "e": - addr, err := parseEmail(v) - if err != nil { - return nil, fmt.Errorf("parse email: %w", err) - } - session.Email = addr - next = fchars[3:] - case "p": - session.Phone = cleanPhone(v) - 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 - 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() -} - -func readSession(r io.Reader) (*Session, *bufio.Scanner, error) { - sc := bufio.NewScanner(r) - var session Session - next := "v" -Loop: - for sc.Scan() { - if strings.TrimSpace(sc.Text()) == "" { - return nil, nil, fmt.Errorf("illegal empty line") - } - k, v, found := strings.Cut(sc.Text(), "=") - if !found { - return nil, nil, fmt.Errorf("parse field %q: missing %q", next, "=") - } - if k != next { - return nil, nil, fmt.Errorf("expected field %q, found %q", next, k) - } - switch k { - case "v": - i, err := strconv.Atoi(v) - if err != nil { - return nil, nil, fmt.Errorf("parse version: %w", err) - } - if i != 0 { - return nil, nil, fmt.Errorf("unsupported version %d", i) - } - next = "o" - case "o": - o, err := parseOrigin(v) - if err != nil { - return nil, nil, fmt.Errorf("parse origin: %w", err) - } - session.Origin = o - next = "s" - case "s": - if v == "" { - return nil, nil, fmt.Errorf("empty name") - } - session.Name = v - break Loop - } + parser := &parser{Scanner: bufio.NewScanner(rd)} + if err := parser.parse(); err != nil { + return nil, fmt.Errorf("parse session: %w", err) } - return &session, sc, sc.Err() + return &parser.session, nil } // cleanPhone returns the phone number in s stripped of "-" and space