write.go (3845B)
1 package m3u8 2 3 import ( 4 "encoding/hex" 5 "fmt" 6 "io" 7 "strings" 8 "time" 9 10 "github.com/untangledco/streaming/scte35" 11 ) 12 13 func Encode(w io.Writer, p *Playlist) error { 14 fmt.Fprintln(w, "#EXTM3U") 15 if p.Version > 0 { 16 fmt.Fprintf(w, "%s:%d\n", tagVersion, p.Version) 17 } 18 if p.Type != PlaylistNone { 19 fmt.Fprintf(w, "%s:%s\n", tagPlaylistType, p.Type) 20 } 21 if p.IndependentSegments { 22 fmt.Fprintln(w, tagIndependentSegments) 23 } 24 if p.TargetDuration > 0 { 25 fmt.Fprintf(w, "%s:%d\n", tagTargetDuration, p.TargetDuration/time.Second) 26 } 27 fmt.Fprintf(w, "%s:%d\n", tagMediaSequence, p.Sequence) 28 29 if _, err := writeSegments(w, p.Segments); err != nil { 30 return fmt.Errorf("write segments: %w", err) 31 } 32 33 for _, r := range p.Media { 34 if _, err := writeRendition(w, r); err != nil { 35 return fmt.Errorf("rendition %s: %w", r.Name, err) 36 } 37 } 38 39 for i, v := range p.Variants { 40 if _, err := writeVariant(w, &v); err != nil { 41 return fmt.Errorf("write variant %d: %w", i, err) 42 } 43 } 44 45 for i, sd := range p.SessionData { 46 if _, err := writeSessionData(w, sd); err != nil { 47 return fmt.Errorf("write session data %d: %w", i, err) 48 } 49 } 50 51 if p.End { 52 fmt.Fprintln(w, tagEndList) 53 } 54 return nil 55 } 56 57 func writeVariant(w io.Writer, v *Variant) (n int, err error) { 58 if v.Bandwidth <= 0 { 59 return 0, fmt.Errorf("invalid bandwidth %d: must be larger than zero", v.Bandwidth) 60 } 61 if v.URI == "" { 62 return 0, fmt.Errorf("empty URI") 63 } 64 return fmt.Fprintln(w, v) 65 } 66 67 func writeDateRange(w io.Writer, dr *DateRange) error { 68 if dr.ID == "" { 69 return fmt.Errorf("empty ID") 70 } else if dr.Start.IsZero() { 71 return fmt.Errorf("zero start time") 72 } 73 74 var attrs []string 75 attrs = append(attrs, fmt.Sprintf("ID=%q", dr.ID)) 76 attrs = append(attrs, fmt.Sprintf("START-DATE=%q", dr.Start.Format(rfc3339Milli))) 77 if !dr.End.IsZero() { 78 attrs = append(attrs, fmt.Sprintf("END-DATE=%q", dr.End.Format(rfc3339Milli))) 79 } 80 if dr.Class != "" { 81 attrs = append(attrs, fmt.Sprintf("CLASS=%q", dr.Class)) 82 } 83 // TODO(otl): dr.Duration, dr.Planned. Differentiate zero value and user-set zero. 84 // TODO(otl): dr.Custom. 85 // TODO(otl): dr.CueCommand, when to write this versuse cuein, cueout. 86 if dr.CueIn != nil { 87 b, err := scte35.Encode(dr.CueIn) 88 if err != nil { 89 return fmt.Errorf("encode cue in: %w", err) 90 } 91 attrs = append(attrs, fmt.Sprintf("SCTE35-IN=0x%s", hex.EncodeToString(b))) 92 } 93 if dr.CueOut != nil { 94 b, err := scte35.Encode(dr.CueOut) 95 if err != nil { 96 return fmt.Errorf("encode cue out: %w", err) 97 } 98 attrs = append(attrs, fmt.Sprintf("SCTE35-OUT=0x%s", hex.EncodeToString(b))) 99 } 100 if dr.EndOnNext { 101 if dr.Class == "" { 102 return fmt.Errorf("empty class with end-on-next set") 103 } else if !dr.End.IsZero() { 104 return fmt.Errorf("non-zero end time with end-on-next set") 105 } else if dr.Duration > 0 { 106 return fmt.Errorf("non-zero duration %s with end-on-next set", dr.Duration) 107 } 108 attrs = append(attrs, "END-ON-NEXT:YES") 109 } 110 tag := tagDateRange + ":" + strings.Join(attrs, ",") 111 _, err := fmt.Fprintln(w, tag) 112 return err 113 } 114 115 func writeRendition(w io.Writer, r Rendition) (n int, err error) { 116 if r.Name == "" { 117 return 0, fmt.Errorf("empty name") 118 } else if r.Group == "" { 119 return 0, fmt.Errorf("empty group") 120 } 121 122 if r.Type > MediaClosedCaptions { 123 return 0, fmt.Errorf("unknown type %s", r.Type) 124 } 125 if r.Type != MediaClosedCaptions && r.InstreamID != nil { 126 return 0, fmt.Errorf("instream-id set but type is %s", r.Type) 127 } else if r.Type == MediaClosedCaptions && r.InstreamID == nil { 128 return 0, fmt.Errorf("nil instream-id") 129 } 130 return fmt.Fprintln(w, r) 131 } 132 133 func writeSessionData(w io.Writer, sd SessionData) (n int, err error) { 134 if sd.ID == "" { 135 return 0, fmt.Errorf("ID not set") 136 } 137 if sd.URI != "" && sd.Value != "" { 138 return 0, fmt.Errorf("only one of Value or URI may be set") 139 } 140 return fmt.Fprintln(w, sd) 141 }