streaming

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

playlist.go (3921B)


      1 package cair
      2 
      3 import (
      4 	"bufio"
      5 	"bytes"
      6 	"encoding/xml"
      7 	"fmt"
      8 	"html"
      9 	"io"
     10 	"os"
     11 	"strconv"
     12 	"time"
     13 )
     14 
     15 type Playlist struct {
     16 	XMLName struct{} `xml:"List"`
     17 	Items   []Item   `xml:"Item"`
     18 }
     19 
     20 type Item struct {
     21 	ID            string        `xml:"Id,attr"`
     22 	Name          string        `xml:",attr"`
     23 	Description   string        `xml:",attr"`
     24 	ThirdPartyId  string        `xml:",attr"`
     25 	SubtitleId    string        `xml:",attr"`
     26 	EPGID         string        `xml:"EpgId,attr"`
     27 	ProxyProgress string        `xml:",attr"`
     28 	ScheduledAt   time.Time     `xml:",attr"`
     29 	Duration      time.Duration `xml:",attr"`
     30 	OutOfNetwork  string        `xml:",attr"`
     31 }
     32 
     33 // EndTime calculates the time i should end by inspecting its start time and duration.
     34 func (i *Item) EndTime() (time.Time, error) {
     35 	return i.ScheduledAt.Add(i.Duration), nil
     36 }
     37 
     38 func (i *Item) UnmarshalXML(dec *xml.Decoder, start xml.StartElement) error {
     39 	type alias Item
     40 	aux := &struct {
     41 		ScheduledAt string `xml:",attr"`
     42 		Duration    string `xml:",attr"`
     43 		*alias
     44 	}{
     45 		alias: (*alias)(i),
     46 	}
     47 	if err := dec.DecodeElement(aux, &start); err != nil {
     48 		return err
     49 	}
     50 
     51 	t, err := time.Parse(rfc3339Milli, aux.ScheduledAt)
     52 	if err != nil {
     53 		return fmt.Errorf("parse scheduled at: %w", err)
     54 	}
     55 	i.ScheduledAt = t
     56 	dur, err := parseDuration(aux.Duration)
     57 	if err != nil {
     58 		return fmt.Errorf("parse duration: %w", err)
     59 	}
     60 	i.Duration = dur
     61 	return nil
     62 }
     63 
     64 const rfc3339Milli = "2006-01-02T15:04:05.000Z07:00"
     65 
     66 // parseDuration parses a time.Duration from timecode string.
     67 // A timecode represents a duration of time.
     68 // A duration of 1 second is represented by the timecode "00:00:01.000".
     69 // 12 hours, 34 minutes, 56 seconds and 789 milliseconds is represented by the timecode
     70 // "12:34:56.789".
     71 func parseDuration(timecode string) (time.Duration, error) {
     72 	if len(timecode) != 12 {
     73 		return 0, fmt.Errorf("timecode does not have 12 characters")
     74 	}
     75 
     76 	var duration time.Duration
     77 	hours, err := strconv.ParseInt(string(timecode[:2]), 10, 0)
     78 	if err != nil {
     79 		return 0, fmt.Errorf("parse hours: %v", err)
     80 	}
     81 	duration += time.Duration(hours) * time.Hour
     82 
     83 	if string(timecode[2]) != ":" {
     84 		return 0, fmt.Errorf("parse minutes: expected %q, got %q", ":", timecode[3])
     85 	}
     86 	minutes, err := strconv.ParseInt(string(timecode[3:5]), 10, 0)
     87 	if err != nil {
     88 		return 0, fmt.Errorf("parse minutes: %v", err)
     89 	}
     90 	duration += time.Duration(minutes) * time.Minute
     91 
     92 	if string(timecode[5]) != ":" {
     93 		return 0, fmt.Errorf("parse seconds: expected %q, got %q", ":", timecode[5])
     94 	}
     95 	seconds, err := strconv.ParseInt(string(timecode[6:8]), 10, 0)
     96 	if err != nil {
     97 		return 0, fmt.Errorf("parse seconds: %v", err)
     98 	}
     99 	duration += time.Duration(seconds) * time.Second
    100 
    101 	if string(timecode[8]) != "." {
    102 		return 0, fmt.Errorf("parse milliseconds: expected %q, got %q", ".", timecode[6])
    103 	}
    104 	milliseconds, err := strconv.ParseInt(string(timecode[9:]), 10, 0)
    105 	if err != nil {
    106 		return 0, fmt.Errorf("parse milliseconds: %v", err)
    107 	}
    108 	duration += time.Duration(milliseconds) * time.Millisecond
    109 
    110 	return duration, nil
    111 }
    112 
    113 func playlistFromFile(name string) (*Playlist, error) {
    114 	f, err := os.Open(name)
    115 	if err != nil {
    116 		return nil, err
    117 	}
    118 	defer f.Close()
    119 	return ParsePlaylist(f)
    120 }
    121 
    122 // ParsePlaylist parses a XML-encoded Playlist from r.
    123 func ParsePlaylist(r io.Reader) (*Playlist, error) {
    124 	var p Playlist
    125 	escaped := escapeAmpersand(r)
    126 	if err := xml.NewDecoder(escaped).Decode(&p); err != nil {
    127 		return nil, err
    128 	}
    129 	return &p, nil
    130 }
    131 
    132 // escapeAmpersand returns a new reader which reads from r, escaping all ampersand
    133 // characters according to the XML spec.
    134 // This is a hack to make Playlists encoded as invalid XML from third party sources valid.
    135 func escapeAmpersand(r io.Reader) io.Reader {
    136 	buf := &bytes.Buffer{}
    137 	scanner := bufio.NewScanner(r)
    138 	for scanner.Scan() {
    139 		buf.Write(bytes.ReplaceAll(scanner.Bytes(), []byte("&"), []byte(html.EscapeString("&"))))
    140 	}
    141 	return buf
    142 }