streaming

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

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 }