Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit ad87291

Browse filesBrowse files
authored
Bugfix: Reuse original parsing (#110)
* Revert "Parse durations of less than a second (#109)" This reverts commit f656b0e. * bugfix: re-use src/time/time.go based parsing * chore: re-update tests, and add new test cases
1 parent f656b0e commit ad87291
Copy full SHA for ad87291

File tree

Expand file treeCollapse file tree

6 files changed

+174
-54
lines changed
Filter options
Expand file treeCollapse file tree

6 files changed

+174
-54
lines changed

‎THIRD_PARTY_NOTICES

Copy file name to clipboard
+2-17Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,9 @@
1-
## Third-Party Notices
1+
### Third-Party Libraries
22

3-
### scte35-go
3+
#### Library: scte35-go
44
- License: Apache License 2.0
55
- URL: https://github.com/Comcast/scte35-go/tree/main
66
- Copyright: None explicitly stated.
77
- Notes: This library does not include a copyright notice, but it is licensed under the Apache License 2.0.
88

99
This library is used in compliance with the Apache License, Version 2.0.
10-
11-
### chrono
12-
- Copyright (c) 2020 Joe Mann
13-
- Licensed under the MIT License
14-
- Source: https://github.com/go-chrono/chrono/tree/master
15-
16-
Permission is hereby granted, free of charge, to any person obtaining a copy
17-
of this software and associated documentation files (the "Software"), to deal
18-
in the Software without restriction, including without limitation the rights
19-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
20-
copies of the Software, and to permit persons to whom the Software is
21-
furnished to do so, subject to the following conditions:
22-
23-
The above copyright notice and this permission notice shall be included in all
24-
copies or substantial portions of the Software.

‎go.mod

Copy file name to clipboardExpand all lines: go.mod
-1Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,5 @@ require github.com/Comcast/scte35-go v1.4.6
88

99
require (
1010
github.com/bamiaux/iobit v0.0.0-20170418073505-498159a04883 // indirect
11-
github.com/go-chrono/chrono v0.0.0-20250124203826-0422557264a6 // indirect
1211
golang.org/x/text v0.16.0 // indirect
1312
)

‎go.sum

Copy file name to clipboardExpand all lines: go.sum
-2Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ github.com/bamiaux/iobit v0.0.0-20170418073505-498159a04883 h1:XNtOMwxmV2PI/vuTH
44
github.com/bamiaux/iobit v0.0.0-20170418073505-498159a04883/go.mod h1:9IjZnSQGh45J46HHS45pxuMJ6WFTtSXbaX0FoHDvxh8=
55
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
66
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
7-
github.com/go-chrono/chrono v0.0.0-20250124203826-0422557264a6 h1:bZajBUDqyayXRqKAD/wX8AVPOeuFvwLAwTZFCvWnohs=
8-
github.com/go-chrono/chrono v0.0.0-20250124203826-0422557264a6/go.mod h1:uTWQdzrjtft2vWY+f+KQ9e3DXHsP0SzhE5SLIicFo08=
97
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
108
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
119
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=

‎mpd/duration.go

Copy file name to clipboard
+161-25Lines changed: 161 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,199 @@
1-
// based on code from golang src/time/time.go
2-
31
package mpd
42

53
import (
64
"encoding/xml"
75
"errors"
6+
"fmt"
7+
"regexp"
8+
"strconv"
9+
"strings"
810
"time"
9-
10-
"github.com/go-chrono/chrono"
1111
)
1212

13+
// Duration is an extension of the original time.Duration. This type is used to
14+
// re-format the String() output to support the ISO 8601 duration standard. And
15+
// add the MarshalXMLAttr and UnmarshalXMLAttr functions.
1316
type Duration time.Duration
1417

15-
var unsupportedFormatErr = errors.New("duration must be in the format: P[nD][T[nH][nM][nS]]")
18+
var (
19+
rStart = "^P" // Must start with a 'P'
20+
rDays = "(\\d+D)?" // We only allow Days for durations, not Months or Years
21+
rTime = "(?:T" // If there's any 'time' units then they must be preceded by a 'T'
22+
rHours = "(\\d+H)?" // Hours
23+
rMinutes = "(\\d+M)?" // Minutes
24+
rSeconds = "([\\d.]+S)?" // Seconds (Potentially decimal)
25+
rEnd = ")?$" // end of regex must close "T" capture group
26+
)
27+
28+
var xmlDurationRegex = regexp.MustCompile(rStart + rDays + rTime + rHours + rMinutes + rSeconds + rEnd)
1629

1730
func (d *Duration) MarshalXMLAttr(name xml.Name) (xml.Attr, error) {
1831
return xml.Attr{Name: name, Value: d.String()}, nil
1932
}
2033

2134
func (d *Duration) UnmarshalXMLAttr(attr xml.Attr) error {
22-
duration, err := ParseDuration(attr.Value)
35+
dur, err := ParseDuration(attr.Value)
2336
if err != nil {
2437
return err
2538
}
26-
*d = Duration(duration)
39+
*d = Duration(dur)
2740
return nil
2841
}
2942

30-
// String parses the duration into a string with the use of the chrono library.
43+
// String returns a string representing the duration in the form "PT72H3M0.5S".
44+
// Leading zero units are omitted. The zero duration formats as PT0S.
45+
// Based on src/time/time.go's time.Duration.String function.
3146
func (d *Duration) String() string {
47+
// This is inlinable to take advantage of "function outlining".
48+
// Thus, the caller can decide whether a string must be heap allocated.
49+
var arr [32]byte
50+
3251
if d == nil {
3352
return "PT0S"
3453
}
3554

36-
return chrono.DurationOf(chrono.Extent(*d)).String()
55+
n := d.format(&arr)
56+
return "PT" + string(arr[n:])
3757
}
3858

39-
// ParseDuration converts the given string into a time.Duration with the use of
40-
// the chrono library. The function doesn't allow the use of negative durations,
41-
// decimal valued periods, or the use of the year, month, or week units as they
42-
// don't make sense.
43-
func ParseDuration(str string) (time.Duration, error) {
44-
period, duration, err := chrono.ParseDuration(str)
45-
if err != nil {
46-
return 0, unsupportedFormatErr
59+
// format formats the representation of d into the end of buf and returns the
60+
// offset of the first character. This function is modified to use the iso 1801
61+
// duration standard. This standard only uses the "H", "M", "S" characters.
62+
// // Based on src/time/time.go's time.Duration.Format function.
63+
func (d *Duration) format(buf *[32]byte) int {
64+
// Largest time is 2540400h10m10.000000000s
65+
w := len(buf)
66+
67+
u := uint64(*d)
68+
neg := *d < 0
69+
if neg {
70+
u = -u
71+
}
72+
73+
w--
74+
buf[w] = 'S'
75+
76+
w, u = fmtFrac(buf[:w], u, 9)
77+
78+
// u is now integer seconds
79+
w = fmtInt(buf[:w], u%60)
80+
u /= 60
81+
82+
// u is now integer minutes
83+
if u > 0 {
84+
w--
85+
buf[w] = 'M'
86+
w = fmtInt(buf[:w], u%60)
87+
u /= 60
88+
89+
// u is now integer hours
90+
// Stop at hours because days can be different lengths.
91+
if u > 0 {
92+
w--
93+
buf[w] = 'H'
94+
w = fmtInt(buf[:w], u)
95+
}
96+
}
97+
98+
if neg {
99+
w--
100+
buf[w] = '-'
101+
}
102+
103+
return w
104+
}
105+
106+
// fmtFrac formats the fraction of v/10**prec (e.g., ".12345") into the
107+
// tail of buf, omitting trailing zeros. it omits the decimal
108+
// point too when the fraction is 0. It returns the index where the
109+
// output bytes begin and the value v/10**prec.
110+
// Copied from src/time/time.go.
111+
func fmtFrac(buf []byte, v uint64, prec int) (nw int, nv uint64) {
112+
// Omit trailing zeros up to and including decimal point.
113+
w := len(buf)
114+
print := false
115+
for i := 0; i < prec; i++ {
116+
digit := v % 10
117+
print = print || digit != 0
118+
if print {
119+
w--
120+
buf[w] = byte(digit) + '0'
121+
}
122+
v /= 10
123+
}
124+
if print {
125+
w--
126+
buf[w] = '.'
47127
}
128+
return w, v
129+
}
48130

49-
hasDecimalDays := period.Days != float32(int64(period.Days))
50-
hasUnsupportedUnits := period.Years+period.Months+period.Years > 0
51-
if hasDecimalDays || hasUnsupportedUnits {
52-
return 0, unsupportedFormatErr
131+
// fmtInt formats v into the tail of buf.
132+
// It returns the index where the output begins.
133+
// Copied from src/time/time.go.
134+
func fmtInt(buf []byte, v uint64) int {
135+
w := len(buf)
136+
if v == 0 {
137+
w--
138+
buf[w] = '0'
139+
} else {
140+
for v > 0 {
141+
w--
142+
buf[w] = byte(v%10) + '0'
143+
v /= 10
144+
}
53145
}
146+
return w
147+
}
54148

55-
durationDays := chrono.Extent(period.Days) * 24 * chrono.Hour
56-
totalDur := duration.Add(chrono.DurationOf(durationDays))
149+
func ParseDuration(str string) (time.Duration, error) {
150+
if len(str) < 3 {
151+
return 0, errors.New("at least one number and designator are required")
152+
}
57153

58-
if totalDur.Compare(chrono.Duration{}) == -1 {
154+
if strings.Contains(str, "-") {
59155
return 0, errors.New("duration cannot be negative")
60156
}
61157

62-
return time.Duration(totalDur.Nanoseconds()), nil
158+
// Check that only the parts we expect exist and that everything's in the correct order
159+
if !xmlDurationRegex.Match([]byte(str)) {
160+
return 0, errors.New("duration must be in the format: P[nD][T[nH][nM][nS]]")
161+
}
162+
163+
var parts = xmlDurationRegex.FindStringSubmatch(str)
164+
var total time.Duration
165+
166+
if parts[1] != "" {
167+
days, err := strconv.Atoi(strings.TrimRight(parts[1], "D"))
168+
if err != nil {
169+
return 0, fmt.Errorf("error parsing Days: %s", err)
170+
}
171+
total += time.Duration(days) * time.Hour * 24
172+
}
173+
174+
if parts[2] != "" {
175+
hours, err := strconv.Atoi(strings.TrimRight(parts[2], "H"))
176+
if err != nil {
177+
return 0, fmt.Errorf("error parsing Hours: %s", err)
178+
}
179+
total += time.Duration(hours) * time.Hour
180+
}
181+
182+
if parts[3] != "" {
183+
mins, err := strconv.Atoi(strings.TrimRight(parts[3], "M"))
184+
if err != nil {
185+
return 0, fmt.Errorf("error parsing Minutes: %s", err)
186+
}
187+
total += time.Duration(mins) * time.Minute
188+
}
189+
190+
if parts[4] != "" {
191+
secs, err := strconv.ParseFloat(strings.TrimRight(parts[4], "S"), 64)
192+
if err != nil {
193+
return 0, fmt.Errorf("error parsing Seconds: %s", err)
194+
}
195+
total += time.Duration(secs * float64(time.Second))
196+
}
197+
198+
return total, nil
63199
}

‎mpd/duration_test.go

Copy file name to clipboardExpand all lines: mpd/duration_test.go
+10-8Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ import (
1010

1111
func TestDuration(t *testing.T) {
1212
in := map[string]string{
13-
"0.5ms": "PT0.0005S",
14-
"7ms": "PT0.007S",
15-
"0s": "PT0S",
16-
"6m16s": "PT6M16S",
17-
"1.97s": "PT1.97S",
13+
"0.5ms": "PT0.0005S",
14+
"7ms": "PT0.007S",
15+
"0s": "PT0S",
16+
"6m16s": "PT6M16S",
17+
"1.97s": "PT1.97S",
18+
"4988000000ns": "PT4.988S",
19+
"2h4m30s7ms": "PT2H4M30.007S",
1820
}
1921
for ins, ex := range in {
2022
t.Run(ins, func(t *testing.T) {
@@ -38,7 +40,6 @@ func TestParseDuration(t *testing.T) {
3840
"PT20M": (20 * time.Minute).Seconds(),
3941
"PT1M30.5S": (time.Minute + 30*time.Second + 500*time.Millisecond).Seconds(),
4042
"PT1004199059S": (1004199059 * time.Second).Seconds(),
41-
"PT2M1H": (time.Minute*2 + time.Hour).Seconds(),
4243
}
4344
for ins, ex := range in {
4445
t.Run(ins, func(t *testing.T) {
@@ -56,8 +57,9 @@ func TestParseBadDurations(t *testing.T) {
5657
"P15.5D": `duration must be in the format: P[nD][T[nH][nM][nS]]`, // Only seconds can be expressed as a decimal
5758
"P2H": `duration must be in the format: P[nD][T[nH][nM][nS]]`, // "T" must be present to separate days and hours
5859
"2DT1H": `duration must be in the format: P[nD][T[nH][nM][nS]]`, // "P" must always be present
59-
"P": `duration must be in the format: P[nD][T[nH][nM][nS]]`, // At least one number and designator are required
60-
"-PT20H": `duration cannot be negative`, // Negative duration doesn't make sense
60+
"PT2M1H": `duration must be in the format: P[nD][T[nH][nM][nS]]`, // Hours must appear before Minutes
61+
"P": `at least one number and designator are required`, // At least one number and designator are required
62+
"-P20H": `duration cannot be negative`, // Negative duration doesn't make sense
6163
}
6264
for ins, msg := range in {
6365
t.Run(ins, func(t *testing.T) {

‎mpd/fixtures/newperiod.mpd

Copy file name to clipboardExpand all lines: mpd/fixtures/newperiod.mpd
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<SegmentTemplate duration="1968" initialization="$RepresentationID$/audio-1.mp4" media="$RepresentationID$/audio-1/seg-$Number$.m4f" startNumber="0" timescale="1000"></SegmentTemplate>
99
</AdaptationSet>
1010
</Period>
11-
<Period duration="PT3M">
11+
<Period duration="PT3M0S">
1212
<AdaptationSet mimeType="video/mp4" startWithSAP="1" scanType="progressive" id="2" segmentAlignment="true">
1313
<SegmentTemplate duration="1968" initialization="$RepresentationID$/video-2.mp4" media="$RepresentationID$/video-2/seg-$Number$.m4f" startNumber="0" timescale="1000"></SegmentTemplate>
1414
</AdaptationSet>

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.