From a1acd20c691c3d055b5caf61fa7dec0a70bccabf Mon Sep 17 00:00:00 2001 From: Koichi Nakanishi Date: Mon, 20 Nov 2017 10:50:57 +0900 Subject: [PATCH 1/5] MediaSegment implement fmt.Stringer --- writer.go | 133 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) diff --git a/writer.go b/writer.go index 36f0c8ec..b3c34ca6 100644 --- a/writer.go +++ b/writer.go @@ -766,3 +766,136 @@ func (p *MediaPlaylist) SetWinSize(winsize uint) error { p.winsize = winsize return nil } + +func (seg MediaSegment) String() string { + durationCache := make(map[float64]string) + var buf bytes.Buffer + if seg.SCTE != nil { + buf.WriteString(seg.SCTE.String()) + } + if seg.Key != nil { + buf.WriteString(seg.Key.String()) + } + if seg.Discontinuity { + buf.WriteString("#EXT-X-DISCONTINUITY\n") + } + if seg.Map != nil { + buf.WriteString(seg.Map.String()) + } + if !seg.ProgramDateTime.IsZero() { + buf.WriteString("#EXT-X-PROGRAM-DATE-TIME:") + buf.WriteString(seg.ProgramDateTime.Format(DATETIME)) + buf.WriteRune('\n') + } + if seg.Limit > 0 { + buf.WriteString("#EXT-X-BYTERANGE:") + buf.WriteString(strconv.FormatInt(seg.Limit, 10)) + buf.WriteRune('@') + buf.WriteString(strconv.FormatInt(seg.Offset, 10)) + buf.WriteRune('\n') + } + buf.WriteString("#EXTINF:") + if str, ok := durationCache[seg.Duration]; ok { + buf.WriteString(str) + } else { + durationCache[seg.Duration] = strconv.FormatFloat(seg.Duration, 'f', 3, 32) + buf.WriteString(durationCache[seg.Duration]) + } + buf.WriteRune(',') + if seg.Title != "" { + buf.WriteString(seg.Title) + } + buf.WriteRune('\n') + if seg.URI != "" { + buf.WriteString(seg.URI) + } + return buf.String() +} + +func (s SCTE) String() string { + var buf bytes.Buffer + switch s.Syntax { + case SCTE35_67_2014: + buf.WriteString("#EXT-SCTE35:") + buf.WriteString("CUE=\"") + buf.WriteString(s.Cue) + buf.WriteRune('"') + if s.ID != "" { + buf.WriteString(",ID=\"") + buf.WriteString(s.ID) + buf.WriteRune('"') + } + if s.Time != 0 { + buf.WriteString(",TIME=") + buf.WriteString(strconv.FormatFloat(s.Time, 'f', -1, 64)) + } + buf.WriteRune('\n') + case SCTE35_OATCLS: + switch s.CueType { + case SCTE35Cue_Start: + buf.WriteString("#EXT-OATCLS-SCTE35:") + buf.WriteString(s.Cue) + buf.WriteRune('\n') + buf.WriteString("#EXT-X-CUE-OUT:") + buf.WriteString(strconv.FormatFloat(s.Time, 'f', -1, 64)) + buf.WriteRune('\n') + case SCTE35Cue_Mid: + buf.WriteString("#EXT-X-CUE-OUT-CONT:") + buf.WriteString("ElapsedTime=") + buf.WriteString(strconv.FormatFloat(s.Elapsed, 'f', -1, 64)) + buf.WriteString(",Duration=") + buf.WriteString(strconv.FormatFloat(s.Time, 'f', -1, 64)) + buf.WriteString(",SCTE35=") + buf.WriteString(s.Cue) + buf.WriteRune('\n') + case SCTE35Cue_End: + buf.WriteString("#EXT-X-CUE-IN") + buf.WriteRune('\n') + } + } + return buf.String() +} + +func (k Key) String() string { + var buf bytes.Buffer + buf.WriteString("#EXT-X-KEY:") + buf.WriteString("METHOD=") + buf.WriteString(k.Method) + if k.Method != "NONE" { + buf.WriteString(",URI=\"") + buf.WriteString(k.URI) + buf.WriteRune('"') + if k.IV != "" { + buf.WriteString(",IV=") + buf.WriteString(k.IV) + } + if k.Keyformat != "" { + buf.WriteString(",KEYFORMAT=\"") + buf.WriteString(k.Keyformat) + buf.WriteRune('"') + } + if k.Keyformatversions != "" { + buf.WriteString(",KEYFORMATVERSIONS=\"") + buf.WriteString(k.Keyformatversions) + buf.WriteRune('"') + } + } + buf.WriteRune('\n') + return buf.String() +} + +func (m Map) String() string { + var buf bytes.Buffer + 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') + return buf.String() +} From 178325b3c3cfb74ad192bc583db21a09792757f0 Mon Sep 17 00:00:00 2001 From: Koichi Nakanishi Date: Mon, 20 Nov 2017 11:26:13 +0900 Subject: [PATCH 2/5] Add Test for (seg MediaSegment)String() --- writer_test.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/writer_test.go b/writer_test.go index c737e690..074e90ab 100644 --- a/writer_test.go +++ b/writer_test.go @@ -774,6 +774,21 @@ func TestMasterSetVersion(t *testing.T) { } } +func TestMediaSegment_String(t *testing.T) { + test := struct { + seg MediaSegment + expected string + }{ + seg: MediaSegment{ + Duration: 10.000, + URI: "test01.ts"}, + expected: "#EXTINF:10.000,\ntest01.ts", + } + if got := test.seg.String(); test.expected != got { + t.Fatalf("Expected string: %s, got: %s", test.expected, got) + } +} + /****************************** * Code generation examples * ******************************/ From 9177d906dd8eefa5c7852b4d3725e4091a93af94 Mon Sep 17 00:00:00 2001 From: Koichi Nakanishi Date: Mon, 20 Nov 2017 13:15:38 +0900 Subject: [PATCH 3/5] Use Provided bytes.Buffer --- writer.go | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/writer.go b/writer.go index b3c34ca6..2c85dcbd 100644 --- a/writer.go +++ b/writer.go @@ -769,18 +769,18 @@ func (p *MediaPlaylist) SetWinSize(winsize uint) error { func (seg MediaSegment) String() string { durationCache := make(map[float64]string) - var buf bytes.Buffer + buf := new(bytes.Buffer) if seg.SCTE != nil { - buf.WriteString(seg.SCTE.String()) + writeSCTE(buf, seg.SCTE) } if seg.Key != nil { - buf.WriteString(seg.Key.String()) + writeKey(buf, seg.Key) } if seg.Discontinuity { buf.WriteString("#EXT-X-DISCONTINUITY\n") } if seg.Map != nil { - buf.WriteString(seg.Map.String()) + writeMap(buf, seg.Map) } if !seg.ProgramDateTime.IsZero() { buf.WriteString("#EXT-X-PROGRAM-DATE-TIME:") @@ -812,8 +812,7 @@ func (seg MediaSegment) String() string { return buf.String() } -func (s SCTE) String() string { - var buf bytes.Buffer +func writeSCTE(buf *bytes.Buffer, s *SCTE) { switch s.Syntax { case SCTE35_67_2014: buf.WriteString("#EXT-SCTE35:") @@ -853,11 +852,9 @@ func (s SCTE) String() string { buf.WriteRune('\n') } } - return buf.String() } -func (k Key) String() string { - var buf bytes.Buffer +func writeKey(buf *bytes.Buffer, k *Key) { buf.WriteString("#EXT-X-KEY:") buf.WriteString("METHOD=") buf.WriteString(k.Method) @@ -881,11 +878,9 @@ func (k Key) String() string { } } buf.WriteRune('\n') - return buf.String() } -func (m Map) String() string { - var buf bytes.Buffer +func writeMap(buf *bytes.Buffer, m *Map) { buf.WriteString("#EXT-X-MAP:") buf.WriteString("URI=\"") buf.WriteString(m.URI) @@ -897,5 +892,4 @@ func (m Map) String() string { buf.WriteString(strconv.FormatInt(m.Offset, 10)) } buf.WriteRune('\n') - return buf.String() } From dde4c977e438e088aad4f7f4c85d5c0623ef3e24 Mon Sep 17 00:00:00 2001 From: Koichi Nakanishi Date: Mon, 20 Nov 2017 13:32:57 +0900 Subject: [PATCH 4/5] Replace writing buffer to internal function --- writer.go | 76 +++---------------------------------------------------- 1 file changed, 3 insertions(+), 73 deletions(-) diff --git a/writer.go b/writer.go index 2c85dcbd..395caa67 100644 --- a/writer.go +++ b/writer.go @@ -481,88 +481,18 @@ func (p *MediaPlaylist) Encode() *bytes.Buffer { i++ } if seg.SCTE != nil { - switch seg.SCTE.Syntax { - case SCTE35_67_2014: - p.buf.WriteString("#EXT-SCTE35:") - p.buf.WriteString("CUE=\"") - p.buf.WriteString(seg.SCTE.Cue) - p.buf.WriteRune('"') - if seg.SCTE.ID != "" { - p.buf.WriteString(",ID=\"") - p.buf.WriteString(seg.SCTE.ID) - p.buf.WriteRune('"') - } - if seg.SCTE.Time != 0 { - p.buf.WriteString(",TIME=") - p.buf.WriteString(strconv.FormatFloat(seg.SCTE.Time, 'f', -1, 64)) - } - p.buf.WriteRune('\n') - case SCTE35_OATCLS: - switch seg.SCTE.CueType { - case SCTE35Cue_Start: - p.buf.WriteString("#EXT-OATCLS-SCTE35:") - p.buf.WriteString(seg.SCTE.Cue) - p.buf.WriteRune('\n') - p.buf.WriteString("#EXT-X-CUE-OUT:") - p.buf.WriteString(strconv.FormatFloat(seg.SCTE.Time, 'f', -1, 64)) - p.buf.WriteRune('\n') - case SCTE35Cue_Mid: - p.buf.WriteString("#EXT-X-CUE-OUT-CONT:") - p.buf.WriteString("ElapsedTime=") - p.buf.WriteString(strconv.FormatFloat(seg.SCTE.Elapsed, 'f', -1, 64)) - p.buf.WriteString(",Duration=") - p.buf.WriteString(strconv.FormatFloat(seg.SCTE.Time, 'f', -1, 64)) - p.buf.WriteString(",SCTE35=") - p.buf.WriteString(seg.SCTE.Cue) - p.buf.WriteRune('\n') - case SCTE35Cue_End: - p.buf.WriteString("#EXT-X-CUE-IN") - p.buf.WriteRune('\n') - } - } + writeSCTE(&p.buf, seg.SCTE) } // check for key change if seg.Key != nil && p.Key != seg.Key { - p.buf.WriteString("#EXT-X-KEY:") - p.buf.WriteString("METHOD=") - p.buf.WriteString(seg.Key.Method) - if seg.Key.Method != "NONE" { - p.buf.WriteString(",URI=\"") - p.buf.WriteString(seg.Key.URI) - p.buf.WriteRune('"') - if seg.Key.IV != "" { - p.buf.WriteString(",IV=") - p.buf.WriteString(seg.Key.IV) - } - if seg.Key.Keyformat != "" { - p.buf.WriteString(",KEYFORMAT=\"") - p.buf.WriteString(seg.Key.Keyformat) - p.buf.WriteRune('"') - } - if seg.Key.Keyformatversions != "" { - p.buf.WriteString(",KEYFORMATVERSIONS=\"") - p.buf.WriteString(seg.Key.Keyformatversions) - p.buf.WriteRune('"') - } - } - p.buf.WriteRune('\n') + writeKey(&p.buf, seg.Key) } 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') + writeMap(&p.buf, seg.Map) } if !seg.ProgramDateTime.IsZero() { p.buf.WriteString("#EXT-X-PROGRAM-DATE-TIME:") From 625523432ce599f5375de7ca5912c44bd8c72db0 Mon Sep 17 00:00:00 2001 From: Koichi Nakanishi Date: Mon, 11 Dec 2017 14:13:44 +0900 Subject: [PATCH 5/5] remove logical duplication --- writer.go | 98 ++++++++++++++++++++------------------------------ writer_test.go | 2 +- 2 files changed, 39 insertions(+), 61 deletions(-) diff --git a/writer.go b/writer.go index 395caa67..f08ed0cf 100644 --- a/writer.go +++ b/writer.go @@ -480,54 +480,7 @@ func (p *MediaPlaylist) Encode() *bytes.Buffer { if p.winsize > 0 { // skip for VOD playlists, where winsize = 0 i++ } - if seg.SCTE != nil { - writeSCTE(&p.buf, seg.SCTE) - } - // check for key change - if seg.Key != nil && p.Key != seg.Key { - writeKey(&p.buf, seg.Key) - } - 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 { - writeMap(&p.buf, seg.Map) - } - if !seg.ProgramDateTime.IsZero() { - p.buf.WriteString("#EXT-X-PROGRAM-DATE-TIME:") - p.buf.WriteString(seg.ProgramDateTime.Format(DATETIME)) - p.buf.WriteRune('\n') - } - if seg.Limit > 0 { - p.buf.WriteString("#EXT-X-BYTERANGE:") - p.buf.WriteString(strconv.FormatInt(seg.Limit, 10)) - p.buf.WriteRune('@') - p.buf.WriteString(strconv.FormatInt(seg.Offset, 10)) - p.buf.WriteRune('\n') - } - p.buf.WriteString("#EXTINF:") - if str, ok := durationCache[seg.Duration]; ok { - p.buf.WriteString(str) - } else { - if p.durationAsInt { - // Old Android players has problems with non integer Duration. - durationCache[seg.Duration] = strconv.FormatInt(int64(math.Ceil(seg.Duration)), 10) - } else { - // Wowza Mediaserver and some others prefer floats. - durationCache[seg.Duration] = strconv.FormatFloat(seg.Duration, 'f', 3, 32) - } - p.buf.WriteString(durationCache[seg.Duration]) - } - p.buf.WriteRune(',') - p.buf.WriteString(seg.Title) - p.buf.WriteRune('\n') - p.buf.WriteString(seg.URI) - if p.Args != "" { - p.buf.WriteRune('?') - p.buf.WriteString(p.Args) - } - p.buf.WriteRune('\n') + seg.writeSegment(&p.buf, durationCache, p) } if p.Closed { p.buf.WriteString("#EXT-X-ENDLIST\n") @@ -697,20 +650,39 @@ func (p *MediaPlaylist) SetWinSize(winsize uint) error { return nil } +// String implements fmt.Stringer interface. func (seg MediaSegment) String() string { - durationCache := make(map[float64]string) buf := new(bytes.Buffer) + durationCache := make(map[float64]string) + seg.writeSegment(buf, durationCache, nil) + return buf.String() +} + +func (seg MediaSegment) writeSegment(buf *bytes.Buffer, durationCache map[float64]string, p *MediaPlaylist) { if seg.SCTE != nil { writeSCTE(buf, seg.SCTE) } - if seg.Key != nil { - writeKey(buf, seg.Key) + if p != nil { + if seg.Key != nil && p.Key != seg.Key { + writeKey(buf, seg.Key) + } + } else { + if seg.Key != nil { + writeKey(buf, seg.Key) + } } + if seg.Discontinuity { buf.WriteString("#EXT-X-DISCONTINUITY\n") } - if seg.Map != nil { - writeMap(buf, seg.Map) + if p != nil { + if p.Map == nil && seg.Map != nil { + writeMap(buf, seg.Map) + } + } else { + if seg.Map != nil { + writeMap(buf, seg.Map) + } } if !seg.ProgramDateTime.IsZero() { buf.WriteString("#EXT-X-PROGRAM-DATE-TIME:") @@ -728,18 +700,24 @@ func (seg MediaSegment) String() string { if str, ok := durationCache[seg.Duration]; ok { buf.WriteString(str) } else { - durationCache[seg.Duration] = strconv.FormatFloat(seg.Duration, 'f', 3, 32) + if p != nil && p.durationAsInt { + // Old Android players has problems with non integer Duration. + durationCache[seg.Duration] = strconv.FormatInt(int64(math.Ceil(seg.Duration)), 10) + } else { + // Wowza Mediaserver and some others prefer floats. + durationCache[seg.Duration] = strconv.FormatFloat(seg.Duration, 'f', 3, 32) + } buf.WriteString(durationCache[seg.Duration]) } buf.WriteRune(',') - if seg.Title != "" { - buf.WriteString(seg.Title) - } + buf.WriteString(seg.Title) buf.WriteRune('\n') - if seg.URI != "" { - buf.WriteString(seg.URI) + buf.WriteString(seg.URI) + if p != nil && p.Args != "" { + buf.WriteRune('?') + buf.WriteString(p.Args) } - return buf.String() + buf.WriteRune('\n') } func writeSCTE(buf *bytes.Buffer, s *SCTE) { diff --git a/writer_test.go b/writer_test.go index 074e90ab..aaddcdb7 100644 --- a/writer_test.go +++ b/writer_test.go @@ -782,7 +782,7 @@ func TestMediaSegment_String(t *testing.T) { seg: MediaSegment{ Duration: 10.000, URI: "test01.ts"}, - expected: "#EXTINF:10.000,\ntest01.ts", + expected: "#EXTINF:10.000,\ntest01.ts\n", } if got := test.seg.String(); test.expected != got { t.Fatalf("Expected string: %s, got: %s", test.expected, got)