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

Sign up
Appearance settings

Commit 27734b3

Browse files
committed
Revert "Parse durations of less than a second (#109)"
This reverts commit f656b0e.
1 parent f656b0e commit 27734b3

File tree

6 files changed

+189
-72
lines changed

6 files changed

+189
-72
lines changed

‎THIRD_PARTY_NOTICES‎

Lines 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‎

Lines 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‎

Lines 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‎

Lines changed: 169 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,59 +5,202 @@ package mpd
55
import (
66
"encoding/xml"
77
"errors"
8+
"fmt"
9+
"regexp"
10+
"strconv"
11+
"strings"
812
"time"
9-
10-
"github.com/go-chrono/chrono"
1113
)
1214

1315
type Duration time.Duration
1416

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

17-
func (d *Duration) MarshalXMLAttr(name xml.Name) (xml.Attr, error) {
29+
func (d Duration) MarshalXMLAttr(name xml.Name) (xml.Attr, error) {
1830
return xml.Attr{Name: name, Value: d.String()}, nil
1931
}
2032

2133
func (d *Duration) UnmarshalXMLAttr(attr xml.Attr) error {
22-
duration, err := ParseDuration(attr.Value)
34+
dur, err := ParseDuration(attr.Value)
2335
if err != nil {
2436
return err
2537
}
26-
*d = Duration(duration)
38+
*d = Duration(dur)
2739
return nil
2840
}
2941

30-
// String parses the duration into a string with the use of the chrono library.
42+
// String renders a Duration in XML Duration Data Type format
3143
func (d *Duration) String() string {
32-
if d == nil {
33-
return "PT0S"
44+
// Largest time is 2540400h10m10.000000000s
45+
var buf [32]byte
46+
w := len(buf)
47+
48+
u := uint64(*d)
49+
neg := *d < 0
50+
if neg {
51+
u = -u
52+
}
53+
54+
if u < uint64(time.Second) {
55+
// Special case: if duration is smaller than a second,
56+
// use smaller units, like 1.2ms
57+
var prec int
58+
w--
59+
buf[w] = 'S'
60+
w--
61+
if u == 0 {
62+
return "PT0S"
63+
}
64+
/*
65+
switch {
66+
case u < uint64(Millisecond):
67+
// print microseconds
68+
prec = 3
69+
// U+00B5 'μ' micro sign == 0xC2 0xB5
70+
w-- // Need room for two bytes.
71+
copy(buf[w:], "μ")
72+
default:
73+
// print milliseconds
74+
prec = 6
75+
buf[w] = 'm'
76+
}
77+
*/
78+
w, u = fmtFrac(buf[:w], u, prec)
79+
w = fmtInt(buf[:w], u)
80+
} else {
81+
w--
82+
buf[w] = 'S'
83+
84+
w, u = fmtFrac(buf[:w], u, 9)
85+
86+
// u is now integer seconds
87+
w = fmtInt(buf[:w], u%60)
88+
u /= 60
89+
90+
// u is now integer minutes
91+
if u > 0 {
92+
w--
93+
buf[w] = 'M'
94+
w = fmtInt(buf[:w], u%60)
95+
u /= 60
96+
97+
// u is now integer hours
98+
// Stop at hours because days can be different lengths.
99+
if u > 0 {
100+
w--
101+
buf[w] = 'H'
102+
w = fmtInt(buf[:w], u)
103+
}
104+
}
105+
}
106+
107+
if neg {
108+
w--
109+
buf[w] = '-'
110+
}
111+
112+
return "PT" + string(buf[w:])
113+
}
114+
115+
// fmtFrac formats the fraction of v/10**prec (e.g., ".12345") into the
116+
// tail of buf, omitting trailing zeros. it omits the decimal
117+
// point too when the fraction is 0. It returns the index where the
118+
// output bytes begin and the value v/10**prec.
119+
func fmtFrac(buf []byte, v uint64, prec int) (nw int, nv uint64) {
120+
// Omit trailing zeros up to and including decimal point.
121+
w := len(buf)
122+
print := false
123+
for i := 0; i < prec; i++ {
124+
digit := v % 10
125+
print = print || digit != 0
126+
if print {
127+
w--
128+
buf[w] = byte(digit) + '0'
129+
}
130+
v /= 10
34131
}
132+
if print {
133+
w--
134+
buf[w] = '.'
135+
}
136+
return w, v
137+
}
35138

36-
return chrono.DurationOf(chrono.Extent(*d)).String()
139+
// fmtInt formats v into the tail of buf.
140+
// It returns the index where the output begins.
141+
func fmtInt(buf []byte, v uint64) int {
142+
w := len(buf)
143+
if v == 0 {
144+
w--
145+
buf[w] = '0'
146+
} else {
147+
for v > 0 {
148+
w--
149+
buf[w] = byte(v%10) + '0'
150+
v /= 10
151+
}
152+
}
153+
return w
37154
}
38155

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.
43156
func ParseDuration(str string) (time.Duration, error) {
44-
period, duration, err := chrono.ParseDuration(str)
45-
if err != nil {
46-
return 0, unsupportedFormatErr
157+
if len(str) < 3 {
158+
return 0, errors.New("At least one number and designator are required")
159+
}
160+
161+
if strings.Contains(str, "-") {
162+
return 0, errors.New("Duration cannot be negative")
163+
}
164+
165+
// Check that only the parts we expect exist and that everything's in the correct order
166+
if !xmlDurationRegex.Match([]byte(str)) {
167+
return 0, errors.New("Duration must be in the format: P[nD][T[nH][nM][nS]]")
47168
}
48169

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
170+
var parts = xmlDurationRegex.FindStringSubmatch(str)
171+
var total time.Duration
172+
173+
if parts[1] != "" {
174+
days, err := strconv.Atoi(strings.TrimRight(parts[1], "D"))
175+
if err != nil {
176+
return 0, fmt.Errorf("Error parsing Days: %s", err)
177+
}
178+
total += time.Duration(days) * time.Hour * 24
53179
}
54180

55-
durationDays := chrono.Extent(period.Days) * 24 * chrono.Hour
56-
totalDur := duration.Add(chrono.DurationOf(durationDays))
181+
if parts[2] != "" {
182+
hours, err := strconv.Atoi(strings.TrimRight(parts[2], "H"))
183+
if err != nil {
184+
return 0, fmt.Errorf("Error parsing Hours: %s", err)
185+
}
186+
total += time.Duration(hours) * time.Hour
187+
}
188+
189+
if parts[3] != "" {
190+
mins, err := strconv.Atoi(strings.TrimRight(parts[3], "M"))
191+
if err != nil {
192+
return 0, fmt.Errorf("Error parsing Minutes: %s", err)
193+
}
194+
total += time.Duration(mins) * time.Minute
195+
}
57196

58-
if totalDur.Compare(chrono.Duration{}) == -1 {
59-
return 0, errors.New("duration cannot be negative")
197+
if parts[4] != "" {
198+
secs, err := strconv.ParseFloat(strings.TrimRight(parts[4], "S"), 64)
199+
if err != nil {
200+
return 0, fmt.Errorf("Error parsing Seconds: %s", err)
201+
}
202+
total += time.Duration(secs * float64(time.Second))
60203
}
61204

62-
return time.Duration(totalDur.Nanoseconds()), nil
205+
return total, nil
63206
}

‎mpd/duration_test.go‎

Lines changed: 17 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,15 @@ import (
1010

1111
func TestDuration(t *testing.T) {
1212
in := map[string]string{
13-
"0.5ms": "PT0.0005S",
14-
"7ms": "PT0.007S",
1513
"0s": "PT0S",
1614
"6m16s": "PT6M16S",
1715
"1.97s": "PT1.97S",
1816
}
1917
for ins, ex := range in {
20-
t.Run(ins, func(t *testing.T) {
21-
timeDur, err := time.ParseDuration(ins)
22-
require.NoError(t, err)
23-
dur := Duration(timeDur)
24-
require.EqualString(t, ex, dur.String())
25-
})
18+
timeDur, err := time.ParseDuration(ins)
19+
require.NoError(t, err)
20+
dur := Duration(timeDur)
21+
require.EqualString(t, ex, dur.String())
2622
}
2723
}
2824

@@ -38,31 +34,27 @@ func TestParseDuration(t *testing.T) {
3834
"PT20M": (20 * time.Minute).Seconds(),
3935
"PT1M30.5S": (time.Minute + 30*time.Second + 500*time.Millisecond).Seconds(),
4036
"PT1004199059S": (1004199059 * time.Second).Seconds(),
41-
"PT2M1H": (time.Minute*2 + time.Hour).Seconds(),
4237
}
4338
for ins, ex := range in {
44-
t.Run(ins, func(t *testing.T) {
45-
act, err := ParseDuration(ins)
46-
require.NoError(t, err, ins)
47-
require.EqualFloat64(t, ex, act.Seconds(), ins)
48-
})
39+
act, err := ParseDuration(ins)
40+
require.NoError(t, err, ins)
41+
require.EqualFloat64(t, ex, act.Seconds(), ins)
4942
}
5043
}
5144

5245
func TestParseBadDurations(t *testing.T) {
5346
in := map[string]string{
54-
"P20M": `duration must be in the format: P[nD][T[nH][nM][nS]]`, // We don't allow Months (doesn't make sense when converting to duration)
55-
"P20Y": `duration must be in the format: P[nD][T[nH][nM][nS]]`, // We don't allow Years (doesn't make sense when converting to duration)
56-
"P15.5D": `duration must be in the format: P[nD][T[nH][nM][nS]]`, // Only seconds can be expressed as a decimal
57-
"P2H": `duration must be in the format: P[nD][T[nH][nM][nS]]`, // "T" must be present to separate days and hours
58-
"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
47+
"P20M": `Duration must be in the format: P[nD][T[nH][nM][nS]]`, // We don't allow Months (doesn't make sense when converting to duration)
48+
"P20Y": `Duration must be in the format: P[nD][T[nH][nM][nS]]`, // We don't allow Years (doesn't make sense when converting to duration)
49+
"P15.5D": `Duration must be in the format: P[nD][T[nH][nM][nS]]`, // Only seconds can be expressed as a decimal
50+
"P2H": `Duration must be in the format: P[nD][T[nH][nM][nS]]`, // "T" must be present to separate days and hours
51+
"2DT1H": `Duration must be in the format: P[nD][T[nH][nM][nS]]`, // "P" must always be present
52+
"PT2M1H": `Duration must be in the format: P[nD][T[nH][nM][nS]]`, // Hours must appear before Minutes
53+
"P": `At least one number and designator are required`, // At least one number and designator are required
54+
"-P20H": `Duration cannot be negative`, // Negative duration doesn't make sense
6155
}
6256
for ins, msg := range in {
63-
t.Run(ins, func(t *testing.T) {
64-
_, err := ParseDuration(ins)
65-
require.EqualError(t, err, msg, fmt.Sprintf("Expected an error for: %s", ins))
66-
})
57+
_, err := ParseDuration(ins)
58+
require.EqualError(t, err, msg, fmt.Sprintf("Expected an error for: %s", ins))
6759
}
6860
}

‎mpd/fixtures/newperiod.mpd‎

Lines 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)

AltStyle によって変換されたページ (->オリジナル) /