streaming

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

commit f2462ddb1dad2764d71341d7cc259938f633d5d6
parent 92f8e13f32219d0d60cd4fa1ff2875298203f2fa
Author: Oliver Lowe <o@olowe.co>
Date:   Tue, 16 Jul 2024 12:59:45 +1000

sdp: parse time description, repeat fields

There are still some tests that should be written - see the TODOs. We
do not support parsing the timezone offset yet; for now we stop
parsing and return an error if we encounter it.

Diffstat:
Msdp/parser.go | 112++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Msdp/sdp.go | 4+++-
Msdp/sdp_test.go | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 193 insertions(+), 5 deletions(-)

diff --git a/sdp/parser.go b/sdp/parser.go @@ -6,6 +6,7 @@ import ( "net/url" "strconv" "strings" + "time" ) type parser struct { @@ -19,7 +20,7 @@ type parser struct { session Session } -var ftab = [...]string{"i", "u", "e", "p", "c", "b", "t", "r", "z", "m", "a"} +var ftab = [...]string{"i", "u", "e", "p", "c", "b", "t", "r", "z", "a", "m", "a"} var mtab = [...]string{"i", "c", "b", "a", "m"} @@ -125,9 +126,25 @@ func (p *parser) parseOptional() error { } p.session.Bandwidth = &bw p.next = ftab[6:] + case "t": + when, err := parseTimes(p.value) + if err != nil { + return fmt.Errorf("parse time description: %w", err) + } + p.session.Time = when + p.next = ftab[7:] + case "r": + repeat, err := parseRepeat(p.value) + if err != nil { + return fmt.Errorf("parse repeat: %w", err) + } + p.session.Repeat = &repeat + p.next = ftab[8:] + case "z": + return fmt.Errorf("parse time desc %s not yet implemented", p.value) case "a": p.session.Attributes = strings.Fields(p.value) - p.next = ftab[7:] + p.next = ftab[9:] case "m": m, err := parseMedia(p.value) if err != nil { @@ -181,4 +198,92 @@ func (p *parser) parseMedia() error { } } return p.err -} -\ No newline at end of file +} + +// number of seconds since the zero time used in SDP 1900-01-01T00:00Z. +const sinceTimeZero = 2208988800 + +func parseTimes(s string) ([2]time.Time, error) { + var times [2]time.Time + fields := strings.Fields(s) + if len(fields) != 2 { + return times, fmt.Errorf("bad number of fields %d: need 2", len(fields)) + } + start, err := strconv.Atoi(fields[0]) + if err != nil { + return times, fmt.Errorf("parse start time: %w", err) + } + if start != 0 { + times[0] = time.Unix(int64(start-sinceTimeZero), 0).UTC() + } + end, err := strconv.Atoi(fields[1]) + if err != nil { + return times, fmt.Errorf("parse end time: %w", err) + } + if end != 0 { + times[1] = time.Unix(int64(end-sinceTimeZero), 0).UTC() + } + return times, nil +} + +type Repeat struct { + Interval time.Duration + Active time.Duration + Offsets []time.Duration +} + +func parseRepeat(s string) (Repeat, error) { + // guard against negative durations, decimals. + // these are valid for time.ParseDuration, but not for our Repeat. + if i := strings.IndexAny(s, "-."); i > 0 { + return Repeat{}, fmt.Errorf("illegal character %c", s[i]) + } + fields := strings.Fields(s) + if len(fields) < 3 { + return Repeat{}, fmt.Errorf("short line: have %d, want at least %d fields", len(fields), 3) + } + + var repeat Repeat + var err error + repeat.Interval, err = parseDuration(fields[0]) + if err != nil { + return Repeat{}, fmt.Errorf("parse interval %s: %w", fields[0], err) + } + repeat.Active, err = parseDuration(fields[1]) + if err != nil { + return Repeat{}, fmt.Errorf("parse active duration %s: %w", fields[1], err) + } + for _, s := range fields[2:] { + offset, err := parseDuration(s) + if err != nil { + return Repeat{}, fmt.Errorf("parse offset %s: %w", s, err) + } + repeat.Offsets = append(repeat.Offsets, offset) + } + return repeat, nil +} + +func parseDuration(s string) (time.Duration, error) { + // a bare int, like 86400 + i, err := strconv.Atoi(s) + if err == nil { + return time.Duration(i) * time.Second, nil + } + + // a duration string like 24h + dur, err := time.ParseDuration(s) + if err == nil { + return dur, nil + } + + // a duration string with days suffix, like 1d + // [0-9]+d + if !strings.HasSuffix(s, "d") { + return 0, fmt.Errorf("bad duration: expected d suffix for days") + } + j, err := strconv.Atoi(s[:len(s)-1]) + if err != nil { + return 0, fmt.Errorf("parse days: %w", err) + } + return time.Duration(j) * 24 * time.Hour, nil +} diff --git a/sdp/sdp.go b/sdp/sdp.go @@ -11,6 +11,7 @@ import ( "net/url" "strconv" "strings" + "time" ) type Session struct { @@ -23,7 +24,8 @@ type Session struct { Phone string Connection *ConnInfo Bandwidth *Bandwidth - // TODO(otl): add time description fields + Time [2]time.Time + Repeat *Repeat Attributes []string Media []Media } diff --git a/sdp/sdp_test.go b/sdp/sdp_test.go @@ -6,6 +6,7 @@ import ( "os" "reflect" "testing" + "time" ) func TestReadSession(t *testing.T) { @@ -147,3 +148,84 @@ func TestConnInfo(t *testing.T) { }) } } + +func TestParseTimes(t *testing.T) { + var cases = []struct { + when string + want [2]time.Time + }{ + {"0 0", [2]time.Time{time.Time{}, time.Time{}}}, + { + "3930082049 0", + [2]time.Time{ + time.Date(2024, time.July, 16, 1, 27, 29, 0, time.UTC), + time.Time{}, + }, + }, + { + "3724394400 3724398000", + [2]time.Time{ + time.Date(2018, time.January, 8, 10, 0, 0, 0, time.UTC), + time.Date(2018, time.January, 8, 11, 0, 0, 0, time.UTC), + }, + }, + { + "3724484400 3724488000", + [2]time.Time{ + time.Date(2018, time.January, 9, 11, 0, 0, 0, time.UTC), + time.Date(2018, time.January, 9, 12, 0, 0, 0, time.UTC), + }, + }, + } + + for _, tt := range cases { + t.Run(tt.when, func(t *testing.T) { + got, err := parseTimes(tt.when) + if err != nil { + t.Fatal(err) + } + if got != tt.want { + t.Errorf("parseTimes(%q) = %v, want %v", tt.when, got, tt.want) + } + }) + } +} + +// TODO(otl): tests for invalid repeat lines, e.g. missing fields, negative values + +func TestParseRepeat(t *testing.T) { + line := "604800 3600 0 90000" + want := Repeat{7 * 24 * time.Hour, time.Hour, []time.Duration{0, 25 * time.Hour}} + got, err := parseRepeat(line) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("parse repeat %s: got %v, want %v", line, got, want) + } +} + +// TODO(otl): tests for invalid duration strings e.g. bad suffix, no numbers + +func TestDuration(t *testing.T) { + var cases = []struct { + s string + want time.Duration + }{ + {"86400", 24 * time.Hour}, + {"24h", 24 * time.Hour}, + {"1d", 24 * time.Hour}, + {"69s", 69 * time.Second}, + } + for _, tt := range cases { + t.Run(tt.s, func(t *testing.T) { + got, err := parseDuration(tt.s) + if err != nil { + t.Fatal(err) + } + if got != tt.want { + t.Errorf("parseDuration(%q) = %s, want %s", tt.s, got, tt.want) + } + }) + } +}