Skip to content

Commit

Permalink
feat: Correct calculation of TargetDuration depending on version.
Browse files Browse the repository at this point in the history
New method SetTargetDuration locks the TargetDuration value.
TargetDuration is now an uint instead for float64.
  • Loading branch information
tobbee committed Jan 5, 2025
1 parent 7e766f1 commit 80530a6
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 33 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Removed SUBTITLES from EXT-X-MEDIA since not in [rfc8216bis-16][rfc8216-bis]
- Changed tests to use matryer.is for conciseness
- Improved documentation
- TargetDuration is now an uint

### Added

Expand All @@ -21,6 +22,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Improved playlist type detection
- Support for SCTE-35 signaling using EXT-X-DATERANGE (following [rfc8216-bis][rfc8216-bis])
- Support for full EXT-X-DATERANGE parsing and writing
- TARGETDURATION calculation depends on HLS version
- New function CalculateTargetDuration
- New method MediaPlaylist.SetTargetDuration that sets and locks the value

### Fixed

Expand Down
2 changes: 1 addition & 1 deletion m3u8/reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -694,7 +694,7 @@ func decodeLineOfMediaPlaylist(p *MediaPlaylist, state *decodingState, line stri
}
case strings.HasPrefix(line, "#EXT-X-TARGETDURATION:"):
state.listType = MEDIA
if _, err = fmt.Sscanf(line, "#EXT-X-TARGETDURATION:%f", &p.TargetDuration); strict && err != nil {
if _, err = fmt.Sscanf(line, "#EXT-X-TARGETDURATION:%d", &p.TargetDuration); strict && err != nil {
return err
}
case strings.HasPrefix(line, "#EXT-X-MEDIA-SEQUENCE:"):
Expand Down
18 changes: 9 additions & 9 deletions m3u8/reader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -285,9 +285,9 @@ func TestDecodeMediaPlaylist(t *testing.T) {
is.NoErr(err) // must decode playlist

// check parsed values
is.Equal(p.ver, uint8(3)) // version must be 3
is.Equal(p.TargetDuration, 12.0) // target duration must be 12.0
is.True(p.Closed) // closed (VOD) playlist but Close field = false")
is.Equal(p.ver, uint8(3)) // version must be 3
is.Equal(p.TargetDuration, uint(12)) // target duration must be 12
is.True(p.Closed) // closed (VOD) playlist but Close field = false")
titles := []string{"Title 1", "Title 2", ""}
for i, s := range p.Segments {
if i > len(titles)-1 {
Expand Down Expand Up @@ -377,9 +377,9 @@ func TestDecodeMediaPlaylistWithAutodetection(t *testing.T) {
CheckType(t, pp)
is.Equal(listType, MEDIA) // must be media playlist
// check parsed values
is.Equal(pp.TargetDuration, 12.0) // target duration must be 12.0
is.True(pp.Closed) // closed (VOD) playlist but Close field = false")
is.Equal(pp.winsize, uint(0)) // window size must be 0
is.Equal(pp.TargetDuration, uint(12)) // target duration must be 12
is.True(pp.Closed) // closed (VOD) playlist but Close field = false")
is.Equal(pp.winsize, uint(0)) // window size must be 0
// TODO check other values…
// fmt.Println(pp.Encode().String())
}
Expand Down Expand Up @@ -829,9 +829,9 @@ func TestDecodeMediaPlaylistWithProgramDateTime(t *testing.T) {
CheckType(t, pp)
is.Equal(listType, MEDIA) // must be media playlist
// check parsed values
is.Equal(pp.TargetDuration, 15.0) // target duration must be 15.0
is.True(pp.Closed) // closed (VOD) playlist but Close field = false")
is.Equal(pp.SeqNo, uint64(0)) // sequence number must be 0
is.Equal(pp.TargetDuration, uint(15)) // target duration must be 15
is.True(pp.Closed) // closed (VOD) playlist but Close field = false")
is.Equal(pp.SeqNo, uint64(0)) // sequence number must be 0

segNames := []string{"20181231/0555e0c371ea801726b92512c331399d_00000000.ts",
"20181231/0555e0c371ea801726b92512c331399d_00000001.ts",
Expand Down
3 changes: 2 additions & 1 deletion m3u8/structure.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ const (
// It is used for both VOD, EVENT and sliding window live media playlists with window size.
// URI lines in the Playlist point to media segments.
type MediaPlaylist struct {
TargetDuration float64 // TargetDuration is the maximum media segment duration in seconds (an integer)
TargetDuration uint // TargetDuration is max media segment duration. Rounding depends on version.
SeqNo uint64 // EXT-X-MEDIA-SEQUENCE
Segments []*MediaSegment // List of segments in the playlist. Output may be limited by winsize.
Args string // optional query placed after URIs (URI?Args)
Expand All @@ -123,6 +123,7 @@ type MediaPlaylist struct {
count uint // number of segments added to the playlist
buf bytes.Buffer // buffer used for encoding and caching playlist output
ver uint8 // protocol version of the playlist, 3 or higher
targetDurLocked bool // target duration is locked and cannot be changed

}

Expand Down
60 changes: 54 additions & 6 deletions m3u8/writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -457,8 +457,8 @@ func (p *MediaPlaylist) AppendSegment(seg *MediaSegment) error {
p.Segments[p.tail] = seg
p.tail = (p.tail + 1) % p.capacity
p.count++
if p.TargetDuration < seg.Duration {
p.TargetDuration = math.Ceil(seg.Duration)
if !p.targetDurLocked {
p.TargetDuration = calcNewTargetDuration(seg.Duration, p.ver, p.TargetDuration)
}
p.buf.Reset()
return nil
Expand Down Expand Up @@ -559,10 +559,7 @@ func (p *MediaPlaylist) Encode() *bytes.Buffer {
p.buf.WriteString(strconv.FormatUint(p.SeqNo, 10))
p.buf.WriteRune('\n')
p.buf.WriteString("#EXT-X-TARGETDURATION:")
// EXT-X-TARGETDURATION must be integer, but is calculated differently
// from version 6 of the standard.
//
p.buf.WriteString(strconv.FormatInt(int64(math.Ceil(p.TargetDuration)), 10))
p.buf.WriteString(strconv.FormatInt(int64(p.TargetDuration), 10))
p.buf.WriteRune('\n')
if p.StartTime > 0.0 {
p.buf.WriteString("#EXT-X-START:TIME-OFFSET=")
Expand Down Expand Up @@ -771,6 +768,57 @@ func (p *MediaPlaylist) Close() {
p.Closed = true
}

// CalculateTargetDuration calculates the target duration for the playlist.
// For HLS v5 and earlier, it is the maximum segment duration as rounded up.
// For HLS v6 and later, it is the maximum segment duration as rounded to the nearest integer.
// It is not allowed to change when the playlist is updated.
func (p *MediaPlaylist) CalculateTargetDuration(hlsVer uint8) uint {
if p.count == 0 {
return 0
}
var max float64
if p.tail >= p.head {
for i := p.head; i < p.tail; i++ {
if p.Segments[i].Duration > max {
max = p.Segments[i].Duration
}
}
} else {
for i := p.head; i < p.capacity; i++ {
if p.Segments[i].Duration > max {
max = p.Segments[i].Duration
}
}
for i := uint(0); i < p.tail; i++ {
if p.Segments[i].Duration > max {
max = p.Segments[i].Duration
}
}
}
return calcNewTargetDuration(max, hlsVer, 0)
}

// calcNewTargetDuration calculates a new target duration based on a segment duration.
func calcNewTargetDuration(segDur float64, hlsVer uint8, oldTargetDuration uint) uint {
var new uint
if hlsVer < 6 {
new = uint(math.Ceil(segDur))
} else {
new = uint(math.Round(segDur))
}
if new > oldTargetDuration {
return new
}
return oldTargetDuration
}

// SetTargetDuration sets the target duration for the playlist and stops automatic calculation.
// Since the target duration is not allowed to change, it is locked after the first call.
func (p *MediaPlaylist) SetTargetDuration(duration uint) {
p.TargetDuration = duration
p.targetDurLocked = true
}

// SetDefaultKey sets encryption key to appear before segments in the media playlist.
func (p *MediaPlaylist) SetDefaultKey(method, uri, iv, keyformat, keyformatversions string) error {
if keyformat != "" || keyformatversions != "" {
Expand Down
60 changes: 44 additions & 16 deletions m3u8/writer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ func TestAppendSegmentToMediaPlaylist(t *testing.T) {
is := is.New(t)
p, _ := NewMediaPlaylist(2, 2)
e := p.AppendSegment(&MediaSegment{Duration: 10})
is.NoErr(e) // Add 1st segment to a media playlist should be successful
is.Equal(p.TargetDuration, 10.0) // target duration should be set to 10
is.NoErr(e) // Add 1st segment to a media playlist should be successful
is.Equal(p.TargetDuration, uint(10)) // target duration should be set to 10
e = p.AppendSegment(&MediaSegment{Duration: 10})
is.NoErr(e) // Add 2nd segment to a media playlist should be successful
e = p.AppendSegment(&MediaSegment{Duration: 10})
Expand Down Expand Up @@ -118,20 +118,6 @@ func TestProgramDateTimeForMediaPlaylist(t *testing.T) {
// fmt.Println(p.Encode().String())
}

// Create new media playlist
// Add two segments to media playlist with duration 9.0 and 9.1.
// Target duration must be set to nearest greater integer (= 10).
func TestTargetDurationForMediaPlaylist(t *testing.T) {
is := is.New(t)
p, e := NewMediaPlaylist(1, 2)
is.NoErr(e) // Create media playlist should be successful
e = p.Append("test01.ts", 9.0, "")
is.NoErr(e) // Add 1st segment to a media playlist should be successful
e = p.Append("test02.ts", 9.1, "")
is.NoErr(e) // Add 2nd segment to a media playlist should be successful
is.Equal(p.TargetDuration, 10.0) // target duration should be set to 10, nearest greater integer 9.1)
}

// Create new media playlist with capacity 10 elements
// Try to add 11 segments to media playlist (oversize error)
func TestOverAddSegmentsToMediaPlaylist(t *testing.T) {
Expand Down Expand Up @@ -785,6 +771,48 @@ func decodeEncode(t *testing.T, fileName string) string {
return pp.Encode().String()
}

// TestCalculateTargetDuration tests the calculation of the target duration.
// It should be rounded up to an integer if the version is 5 or lower.
// If should be rounded to nearest integer if the version is 6 or higher.
// With nrSlides, we check that it works when the circular buffer has wrapped around.
// With lockedTargetDur, we check that it works when the target duration is locked.
func TestCalculateTargetDuration(t *testing.T) {
is := is.New(t)
cases := []struct {
desc string
hlsVersion uint8
segDur float64
nrSlides uint
lockedTargetDur uint
wantedTargetDur uint
}{
{desc: "HLSv5Locked", hlsVersion: 5, segDur: 5.1, nrSlides: 1, lockedTargetDur: 4, wantedTargetDur: 4},
{desc: "HLSv5", hlsVersion: 5, segDur: 5.1, nrSlides: 2, wantedTargetDur: 6},
{desc: "HLSv6", hlsVersion: 6, segDur: 5.1, nrSlides: 2, wantedTargetDur: 5},
{desc: "HLSv5Wrap", hlsVersion: 5, segDur: 5.1, nrSlides: 6, wantedTargetDur: 6},
{desc: "HLSv6Wrap", hlsVersion: 6, segDur: 5.1, nrSlides: 6, wantedTargetDur: 6},
}
for _, c := range cases {
t.Run(c.desc, func(t *testing.T) {
p, err := NewMediaPlaylist(3, 5)
if c.lockedTargetDur != 0 {
p.SetTargetDuration(c.lockedTargetDur)
}
is.NoErr(err) // Create media playlist should be successful
p.ver = c.hlsVersion
for i := 0; i < int(c.nrSlides); i++ {
segDur := c.segDur + 0.1*float64(i)
p.Slide(fmt.Sprintf("test%d.ts", i), segDur, "")
}
is.Equal(p.TargetDuration, c.wantedTargetDur) // Target duration does not match expected
if c.lockedTargetDur == 0 {
calcTargetDur := p.CalculateTargetDuration(c.hlsVersion)
is.Equal(calcTargetDur, c.wantedTargetDur) // Calculate target duration does not match expected
}
})
}
}

/******************************
* Code generation examples *
******************************/
Expand Down

0 comments on commit 80530a6

Please sign in to comment.