streaming

Media streaming and broadcast systems in Go
Log | Files | Refs | README | LICENSE

commit 6f91fd7e149c0466f57b4288c77d2b2c0c35de78
parent c0a87bbed2f0947fe30ae80a5466bdae4de7621b
Author: blackfalcon <blackfalcon.ru@gmail.com>
Date:   Thu, 30 May 2024 14:03:17 -0300

m3u8: support writing SessionData

Resolves #8

Diffstat:
Mm3u8/m3u8.go | 21++++++++++++++++++---
Am3u8/m3u8_test.go | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mm3u8/parse.go | 1+
Mm3u8/write.go | 16++++++++++++++++
4 files changed, 99 insertions(+), 3 deletions(-)

diff --git a/m3u8/m3u8.go b/m3u8/m3u8.go @@ -283,8 +283,23 @@ type IFrameInfo Variant // SessionData represents the EXT-X-SESSION-DATA tag. type SessionData struct { - ID string - Value string + ID string // This attribute is REQUIRED + Value string // MUST contain either a VALUE or URI attribute, but not both URI string - Language string + Language string // This attribute is OPTIONAL. +} + +func (sd SessionData) String() string { + var attrs []string + attrs = append(attrs, fmt.Sprintf("DATA-ID=%q", sd.ID)) + if sd.Value != "" { + attrs = append(attrs, fmt.Sprintf("VALUE=%q", sd.Value)) + } + if sd.URI != "" { + attrs = append(attrs, fmt.Sprintf("URI=%q", sd.URI)) + } + if sd.Language != "" { + attrs = append(attrs, fmt.Sprintf("LANGUAGE=%q", sd.Language)) + } + return strings.Join(attrs, ",") } diff --git a/m3u8/m3u8_test.go b/m3u8/m3u8_test.go @@ -0,0 +1,64 @@ +package m3u8 + +import ( + "bytes" + "strings" + "testing" +) + +func TestWriteSessionData(t *testing.T) { + var cases = []struct { + name string + sd SessionData + want string + valid bool + }{ + { + "all attributes set", + SessionData{ID: "1234", Value: "5678", Language: "indonesian"}, + `#EXT-X-SESSION-DATA:DATA-ID="1234",VALUE="5678",LANGUAGE="indonesian"`, + true, + }, + { + "language is optional", + SessionData{ID: "1234", URI: "hello/hi.json"}, + `#EXT-X-SESSION-DATA:DATA-ID="1234",URI="hello/hi.json"`, + true, + }, + { + "required attributes set(id, value or uri)", + SessionData{ID: "1234", Value: "5678"}, + `#EXT-X-SESSION-DATA:DATA-ID="1234",VALUE="5678"`, + true, + }, + { + "empty id", + SessionData{URI: "hello/hi.json"}, + "", + false, + }, + { + "both URI and Value set", + SessionData{ID: "1234", URI: "hello/hi.json", Value: "5678"}, + "", + false, + }, + } + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + buf := &bytes.Buffer{} + _, err := writeSessionData(buf, tt.sd) + if err == nil && !tt.valid { + t.Fatalf("nil error on invalid session data %v", tt.sd) + } else if err != nil && tt.valid { + t.Fatalf("non-nil error %v on valid session data", err) + } + got := strings.TrimSpace(buf.String()) // strip newlines + if got != tt.want { + t.Errorf("unexpected session data text") + t.Log("got:", got) + t.Log("want:", tt.want) + } + }) + } +} diff --git a/m3u8/parse.go b/m3u8/parse.go @@ -19,6 +19,7 @@ const ( tagMediaSequence = "#EXT-X-MEDIA-SEQUENCE" // RFC 8216, 4.3.3.2 tagEndList = "#EXT-X-ENDLIST" // RFC 8216, 4.4.3.4 tagIndependentSegments = "#EXT-X-INDEPENDENT-SEGMENTS" // RFC 8216, 4.3.5.1 + tagSessionData = "#EXT-X-SESSION-DATA" // RFC 8216, 4.3.4.4 ) func ParsePlaylist(rd io.Reader) (*Playlist, error) { diff --git a/m3u8/write.go b/m3u8/write.go @@ -120,6 +120,12 @@ func Encode(w io.Writer, p *Playlist) error { fmt.Fprintln(w, v.URI) } + for i, sd := range p.SessionData { + if _, err := writeSessionData(w, sd); err != nil { + return fmt.Errorf("write session data %d: %w", i, err) + } + } + if p.End { fmt.Fprintln(w, tagEndList) } @@ -173,3 +179,13 @@ func writeMap(w io.Writer, m Map) (n int, err error) { } return fmt.Fprintf(w, "%s:URI=%q\n", tagMap, m.URI) } + +func writeSessionData(w io.Writer, sd SessionData) (n int, err error) { + if sd.ID == "" { + return 0, fmt.Errorf("ID not set") + } + if sd.URI != "" && sd.Value != "" { + return 0, fmt.Errorf("only one of Value or URI may be set") + } + return fmt.Fprintf(w, "%s:%s\n", tagSessionData, sd) +}