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, ",") }