streaming

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

parse.go (4830B)


      1 package cmcd
      2 
      3 import (
      4 	"fmt"
      5 	"net/url"
      6 	"strconv"
      7 	"strings"
      8 	"time"
      9 )
     10 
     11 func parseInfo(tokens map[string]string) (Info, error) {
     12 	var info Info
     13 	var err error
     14 	info.Request, err = parseRequest(tokens)
     15 	if err != nil {
     16 		return info, fmt.Errorf("request: %w", err)
     17 	}
     18 	info.Object, err = parseObject(tokens)
     19 	if err != nil {
     20 		return info, fmt.Errorf("object: %w", err)
     21 	}
     22 	info.Status, err = parseStatus(tokens)
     23 	if err != nil {
     24 		return info, fmt.Errorf("status: %w", err)
     25 	}
     26 	info.Session, err = parseSession(tokens)
     27 	if err != nil {
     28 		return info, fmt.Errorf("session: %w", err)
     29 	}
     30 	if custom := parseCustom(tokens); custom != nil {
     31 		info.Custom = custom
     32 	}
     33 	return info, nil
     34 }
     35 
     36 func parseRequest(attrs map[string]string) (Request, error) {
     37 	var req Request
     38 	for k, v := range attrs {
     39 		switch k {
     40 		case "bl":
     41 			i, err := strconv.Atoi(v)
     42 			if err != nil {
     43 				return req, fmt.Errorf("parse buffer length: %w", err)
     44 			}
     45 			req.BufLength = time.Duration(i) * time.Millisecond
     46 		case "dl":
     47 			i, err := strconv.Atoi(v)
     48 			if err != nil {
     49 				return req, fmt.Errorf("parse deadline: %w", err)
     50 			}
     51 			req.Deadline = time.Duration(i) * time.Millisecond
     52 		case "mtp":
     53 			i, err := strconv.Atoi(v)
     54 			if err != nil {
     55 				return req, fmt.Errorf("parse throughput: %w", err)
     56 			}
     57 			req.Throughput = i
     58 		case "nor":
     59 			dec, err := url.QueryUnescape(strings.Trim(v, `"`))
     60 			if err != nil {
     61 				return req, fmt.Errorf("decode next object request: %w", err)
     62 			}
     63 			req.Next = dec
     64 		case "nrr":
     65 			rg, err := parseRange(strings.Trim(v, `"`))
     66 			if err != nil {
     67 				return req, fmt.Errorf("parse next range: %w", err)
     68 			}
     69 			req.NextRange = rg
     70 		case "su":
     71 			req.Startup = true
     72 		}
     73 	}
     74 	return req, nil
     75 }
     76 
     77 func parseObject(attrs map[string]string) (Object, error) {
     78 	var obj Object
     79 	for k, v := range attrs {
     80 		if v == "" {
     81 			continue // stray comma, perhaps at end of line. ignore
     82 		}
     83 		switch k {
     84 		case "br":
     85 			i, err := strconv.Atoi(v)
     86 			if err != nil {
     87 				return obj, fmt.Errorf("parse bitrate: %w", err)
     88 			}
     89 			obj.Bitrate = i
     90 		case "d":
     91 			i, err := strconv.Atoi(v)
     92 			if err != nil {
     93 				return obj, fmt.Errorf("parse duration: %w", err)
     94 			}
     95 			obj.Duration = time.Duration(i) * time.Millisecond
     96 		case "ot":
     97 			// TODO(otl): validate value
     98 			obj.Type = ObjectType(v)
     99 		case "tb":
    100 			i, err := strconv.Atoi(v)
    101 			if err != nil {
    102 				return obj, fmt.Errorf("parse top bitrate: %w", err)
    103 			}
    104 			obj.TopBitrate = i
    105 		}
    106 	}
    107 	return obj, nil
    108 }
    109 
    110 func parseStatus(attrs map[string]string) (Status, error) {
    111 	var stat Status
    112 	for k, v := range attrs {
    113 		switch k {
    114 		case "bs":
    115 			stat.Starved = true
    116 		case "rtp":
    117 			i, err := strconv.Atoi(v)
    118 			if err != nil {
    119 				return stat, fmt.Errorf("parse max throughput: %w", err)
    120 			}
    121 			stat.MaxThroughput = i
    122 		}
    123 	}
    124 	return stat, nil
    125 }
    126 
    127 func parseSession(attrs map[string]string) (Session, error) {
    128 	var ses Session
    129 	for k, v := range attrs {
    130 		switch k {
    131 		case "sid":
    132 			ses.ID = strings.Trim(v, `"`)
    133 		case "st":
    134 			// TODO(otl): what if it's not "l"?
    135 			ses.StreamType = StreamType(v)
    136 		case "cid":
    137 			ses.ContentID = strings.Trim(v, `"`)
    138 		case "pr":
    139 			i, err := strconv.ParseFloat(v, 32)
    140 			if err != nil {
    141 				return ses, fmt.Errorf("play rate: %w", err)
    142 			}
    143 			// TODO(otl): what is this max value? why?
    144 			// Include in the error message.
    145 			ses.PlayRate = PlayRate(i)
    146 		case "sf":
    147 			if len(v) != 1 {
    148 				return ses, fmt.Errorf("stream format: %s is not a single character", v)
    149 			}
    150 			c := StreamFormat([]byte(v)[0])
    151 			switch c {
    152 			case FormatDASH, FormatHLS, FormatSmooth, FormatOther:
    153 				ses.Format = c
    154 			default:
    155 				return ses, fmt.Errorf("stream format: unknown format %c", c)
    156 			}
    157 		}
    158 	}
    159 	// If we didn't see playrate, we must set it to realtime as
    160 	// only values other than realtime should be transmitted. See
    161 	// CTA=5004 page 10.
    162 	if _, ok := attrs["pr"]; !ok {
    163 		ses.PlayRate = RealTime
    164 	}
    165 	return ses, nil
    166 }
    167 
    168 func parseCustom(attrs map[string]string) map[string]any {
    169 	m := make(map[string]any)
    170 	for k, v := range attrs {
    171 		switch k {
    172 		case "bl", "dl", "mtp", "nor", "nrr", "su":
    173 			continue // Request keys
    174 		case "br", "d", "ot", "tb":
    175 			continue // Object keys
    176 		case "bs", "rtp":
    177 			continue // Status keys
    178 		case "sid", "st", "cid", "pr", "sf":
    179 			continue // Session keys
    180 		}
    181 		if v == "" {
    182 			m[k] = true
    183 		} else if i, err := strconv.Atoi(v); err == nil {
    184 			m[k] = i
    185 		} else {
    186 			m[k] = strings.Trim(v, `"`)
    187 		}
    188 	}
    189 	if len(m) == 0 {
    190 		return nil
    191 	}
    192 	return m
    193 }
    194 
    195 func lex(s string) map[string]string {
    196 	m := make(map[string]string)
    197 	s = clean(s)
    198 	for _, attr := range strings.Split(s, ",") {
    199 		name, val, _ := strings.Cut(attr, "=")
    200 		m[name] = val
    201 	}
    202 	return m
    203 }
    204 
    205 // clean removes stray commas. Trailing commas are technically valid
    206 // but we remove them to simplify parsing.
    207 func clean(s string) string { return strings.Trim(s, ",") }