Skip to content

Commit

Permalink
fix: EXT-X-MAP is written and stored when changed
Browse files Browse the repository at this point in the history
  • Loading branch information
tobbee committed Jan 20, 2025
1 parent b0cfe49 commit 73cdd20
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 64 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed

- EXT-X-START was not written for negative values
- EXT-X-MAP is written when changed

### Chore

- Refactored EXT-X-DEFINE and EXT-X-MAP parsing and writing

## [v0.3.0] 2025-01-14

Expand Down
60 changes: 36 additions & 24 deletions m3u8/reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,21 @@ func parseSessionData(line string) (*SessionData, error) {
return &sd, nil
}

func parseExtXMapParameters(parameters string) (*Map, error) {
m := Map{}
for _, attr := range decodeAttributes(parameters) {
switch attr.Key {
case "URI":
m.URI = attr.Val
case "BYTERANGE":
if _, err := fmt.Sscanf(attr.Val, "%d@%d", &m.Limit, &m.Offset); err != nil {
return nil, fmt.Errorf("byterange sub-range length value parsing error: %w", err)
}
}
}
return &m, nil
}

func parseKeyParams(parameters string) *Key {
key := Key{}
for _, attr := range decodeAttributes(parameters) {
Expand Down Expand Up @@ -768,7 +783,16 @@ func decodeLineOfMediaPlaylist(p *MediaPlaylist, state *decodingState, line stri
}
case !strings.HasPrefix(line, "#"):
if state.tagInf {
err := p.Append(line, state.duration, state.title)
seg := MediaSegment{
URI: line,
Duration: state.duration,
Title: state.title,
}
if state.lastReadMap != nil && !state.lastReadMap.Equal(state.lastStoredMap) {
seg.Map = state.lastReadMap
state.lastStoredMap = state.lastReadMap
}
err := p.AppendSegment(&seg)
if err == ErrPlaylistFull {
// Extend playlist by doubling size, reset internal state, try again.
// If the second Append fails, the if err block will handle it.
Expand All @@ -777,7 +801,7 @@ func decodeLineOfMediaPlaylist(p *MediaPlaylist, state *decodingState, line stri
p.Segments = append(p.Segments, make([]*MediaSegment, p.Count())...)
p.capacity = uint(len(p.Segments))
p.tail = p.count
err = p.Append(line, state.duration, state.title)
err = p.AppendSegment(&seg)
}
// Check err for first or subsequent Append()
if err != nil {
Expand Down Expand Up @@ -829,17 +853,6 @@ func decodeLineOfMediaPlaylist(p *MediaPlaylist, state *decodingState, line stri
}
state.tagKey = false
}
// If EXT-X-MAP appeared before reference to segment (EXTINF) then it linked to this segment
if state.tagMap {
p.Segments[p.last()].Map = &Map{state.xmap.URI, state.xmap.Limit, state.xmap.Offset}
// First EXT-X-MAP may appeared in the header of the playlist and linked to first segment
// but for convenient playlist generation it also linked as default playlist map
if p.Map == nil {
p.Map = state.xmap
}
state.tagMap = false
}

// if segment custom tag appeared before EXTINF then it links to this segment
if state.tagCustom {
p.Segments[p.last()].Custom = state.custom
Expand Down Expand Up @@ -904,18 +917,17 @@ func decodeLineOfMediaPlaylist(p *MediaPlaylist, state *decodingState, line stri
state.tagKey = true
case strings.HasPrefix(line, "#EXT-X-MAP:"):
state.listType = MEDIA
state.xmap = new(Map)
for k, v := range decodeAndTrimAttributes(line[11:]) {
switch k {
case "URI":
state.xmap.URI = v
case "BYTERANGE":
if _, err = fmt.Sscanf(v, "%d@%d", &state.xmap.Limit, &state.xmap.Offset); strict && err != nil {
return fmt.Errorf("byterange sub-range length value parsing error: %w", err)
}
}
xMap, err := parseExtXMapParameters(line[11:])
if err != nil {
return fmt.Errorf("error parsing EXT-X-MAP: %w", err)
}
if state.lastReadMap == nil && p.Count() == 0 {
p.Map = xMap
state.lastStoredMap = xMap
}
if state.lastReadMap == nil || !state.lastReadMap.Equal(xMap) {
state.lastReadMap = xMap
}
state.tagMap = true
case !state.tagProgramDateTime && strings.HasPrefix(line, "#EXT-X-PROGRAM-DATE-TIME:"):
state.tagProgramDateTime = true
state.listType = MEDIA
Expand Down
15 changes: 13 additions & 2 deletions m3u8/structure.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,17 @@ type Map struct {
Offset int64 // [@o] is offset from the start of the file under URI
}

// Equal compares two MediaSegment for equality.
func (m *Map) Equal(other *Map) bool {
if m == nil && other == nil {
return true
}
if m == nil || other == nil {
return false
}
return m.URI == other.URI && m.Limit == other.Limit && m.Offset == other.Offset
}

// Internal structure for decoding a line of input stream with a list type detection
type decodingState struct {
listType ListType
Expand All @@ -279,7 +290,6 @@ type decodingState struct {
tagDiscontinuity bool
tagProgramDateTime bool
tagKey bool
tagMap bool
tagCustom bool
programDateTime time.Time
limit int64
Expand All @@ -289,7 +299,8 @@ type decodingState struct {
variant *Variant
alternatives []*Alternative
xkey *Key
xmap *Map
lastReadMap *Map
lastStoredMap *Map
scte *SCTE
scte35DateRanges []*DateRange
custom CustomMap
Expand Down
51 changes: 24 additions & 27 deletions m3u8/writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,20 @@ func writeExtXStart(buf *bytes.Buffer, startTime float64, precise bool) {
buf.WriteRune('\n')
}

func writeExtXMap(buf *bytes.Buffer, m *Map) {
buf.WriteString("#EXT-X-MAP:")
buf.WriteString("URI=\"")
buf.WriteString(m.URI)
buf.WriteRune('"')
if m.Limit > 0 {
buf.WriteString(",BYTERANGE=")
buf.WriteString(strconv.FormatInt(m.Limit, 10))
buf.WriteRune('@')
buf.WriteString(strconv.FormatInt(m.Offset, 10))
}
buf.WriteRune('\n')
}

func writeKey(tag string, buf *bytes.Buffer, key *Key) {
buf.WriteString(tag)
buf.WriteString("METHOD=")
Expand Down Expand Up @@ -631,6 +645,8 @@ func (p *MediaPlaylist) Encode() *bytes.Buffer {
return &p.buf
}

var lastMap *Map

p.buf.WriteString("#EXTM3U\n#EXT-X-VERSION:")
p.buf.WriteString(strVer(p.ver))
p.buf.WriteRune('\n')
Expand Down Expand Up @@ -664,20 +680,6 @@ func (p *MediaPlaylist) Encode() *bytes.Buffer {
writeKey("#EXT-X-KEY:", &p.buf, p.Key)
}

// default MAP before any segment
if p.Map != nil {
p.buf.WriteString("#EXT-X-MAP:")
p.buf.WriteString("URI=\"")
p.buf.WriteString(p.Map.URI)
p.buf.WriteRune('"')
if p.Map.Limit > 0 {
p.buf.WriteString(",BYTERANGE=")
p.buf.WriteString(strconv.FormatInt(p.Map.Limit, 10))
p.buf.WriteRune('@')
p.buf.WriteString(strconv.FormatInt(p.Map.Offset, 10))
}
p.buf.WriteRune('\n')
}
if p.MediaType > 0 {
p.buf.WriteString("#EXT-X-PLAYLIST-TYPE:")
switch p.MediaType {
Expand All @@ -704,6 +706,10 @@ func (p *MediaPlaylist) Encode() *bytes.Buffer {
if p.Iframe {
p.buf.WriteString("#EXT-X-I-FRAMES-ONLY\n")
}
if p.Map != nil {
writeExtXMap(&p.buf, p.Map)
lastMap = p.Map
}

var (
seg *MediaSegment
Expand Down Expand Up @@ -774,19 +780,10 @@ func (p *MediaPlaylist) Encode() *bytes.Buffer {
if seg.Discontinuity {
p.buf.WriteString("#EXT-X-DISCONTINUITY\n")
}
// ignore segment Map if default playlist Map is present
if p.Map == nil && seg.Map != nil {
p.buf.WriteString("#EXT-X-MAP:")
p.buf.WriteString("URI=\"")
p.buf.WriteString(seg.Map.URI)
p.buf.WriteRune('"')
if seg.Map.Limit > 0 {
p.buf.WriteString(",BYTERANGE=")
p.buf.WriteString(strconv.FormatInt(seg.Map.Limit, 10))
p.buf.WriteRune('@')
p.buf.WriteString(strconv.FormatInt(seg.Map.Offset, 10))
}
p.buf.WriteRune('\n')
// ignore segment Map if already written
if seg.Map != nil && !seg.Map.Equal(lastMap) {
writeExtXMap(&p.buf, seg.Map)
lastMap = seg.Map
}
if !seg.ProgramDateTime.IsZero() {
p.buf.WriteString("#EXT-X-PROGRAM-DATE-TIME:")
Expand Down
41 changes: 30 additions & 11 deletions m3u8/writer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -261,25 +261,44 @@ test01.ts`

// Create new media playlist
// Set default map
// Add segment to media playlist
// Set map on segment (should be ignored when encoding)
// Add segment to media playlist with two different maps.
// Only the second should be included in the playlist.
func TestEncodeMediaPlaylistWithDefaultMap(t *testing.T) {
is := is.New(t)
p, e := NewMediaPlaylist(3, 5)
is.NoErr(e) // Create media playlist should be successful
p, err := NewMediaPlaylist(3, 5)
is.NoErr(err) // Create media playlist should be successful
p.SetDefaultMap("https://example.com", 1000*1024, 1024*1024)

e = p.Append("test01.ts", 5.0, "")
is.NoErr(e) // Add 1st segment to a media playlist should be successful
e = p.SetMap("https://notencoded.com", 1000*1024, 1024*1024)
is.NoErr(e) // Set map to a media playlist should be successful, but not set
err = p.Append("test01.ts", 5.0, "")
is.NoErr(err) // Add 1st segment to a media playlist should be successful
err = p.SetMap("https://example.com", 1000*1024, 1024*1024)
is.NoErr(err) // Set map to a media playlist should be successful, but not set since same as default.

err = p.Append("test02.ts", 5.0, "")
is.NoErr(err) // Add 1st segment to a media playlist should be successful
err = p.SetMap("https://example2.com", 1000*1024, 1024*1024)
is.NoErr(err) // Set map to a media playlist should be successful, but not set since same as already set.

err = p.SetDiscontinuity()
is.NoErr(err) // Set discontinuity tag should be successful

err = p.Append("test03.ts", 5.0, "")
is.NoErr(err) // Add 1st segment to a media playlist should be successful
err = p.SetMap("https://example2.com", 1000*1024, 1024*1024)
is.NoErr(err) // Set map to a media playlist should be successful, but not set since same as already set.

encoded := p.String()
expected := `EXT-X-MAP:URI="https://example.com",BYTERANGE=1024000@1048576`
is.True(strings.Contains(encoded, expected)) // default map is included in the playlist
is.Equal(1, strings.Count(encoded, expected)) // default map is included in the playlist just once

expected = `EXT-X-MAP:URI="https://example2.com",BYTERANGE=1024000@1048576`
is.Equal(1, strings.Count(encoded, expected)) // new map is included in the playlist just once

split := strings.Split(encoded, "#EXT-X-DISCONTINUITY")
is.Equal(2, len(split)) // discontinuity tag is included one time

ignored := `EXT-X-MAP:URI="https://notencoded.com"`
is.True(!strings.Contains(encoded, ignored)) // additional map is not included in the playlist
expected = `EXT-X-MAP:URI="https://example2.com",BYTERANGE=1024000@1048576`
is.Equal(1, strings.Count(split[1], expected)) // new map should be after discontinuity tag
}

// Create new media playlist
Expand Down

0 comments on commit 73cdd20

Please sign in to comment.