cmcd.go (5316B)
1 /* 2 Package cmcd provides types and functions for exchanging 3 Common Media Client Data (CMCD) as specified in CTA-5004. 4 5 The typical use case for servers is to read client playback 6 information from a HTTP GET request, then relay the information to a 7 database for later analysis. 8 For instance, clients sending CMCD information as a query parameter 9 can be read with ParseInfo. 10 11 func (srv *Server) ServeSegment(w http.ResponseWriter, req *http.Request) { 12 v := req.URL.Query() 13 var info cmcd.Info 14 if v.Has("CMCD") { 15 info, err := cmcd.ParseInfo(v.Get("CMCD")) 16 if err != nil { 17 log.Println("parse cmcd info: %v: ignoring", err) 18 } 19 relayToMetricStore(&info) 20 } 21 // serve response... 22 } 23 */ 24 package cmcd 25 26 import ( 27 "fmt" 28 "net/http" 29 "strconv" 30 "strings" 31 "time" 32 ) 33 34 const ( 35 HeaderRequest = "CMCD-Request" 36 HeaderObject = "CMCD-Object" 37 HeaderStatus = "CMCD-Status" 38 HeaderSession = "CMCD-Session" 39 ) 40 41 type Info struct { 42 Request 43 Object 44 Status 45 Session 46 // Holds custom attributes as either a string, integer or 47 // boolean. 48 Custom map[string]any 49 } 50 51 func (info Info) Encode() string { 52 ss := make([]string, 4) 53 ss[0] = info.Request.Encode() 54 ss[1] = info.Object.Encode() 55 ss[2] = info.Status.Encode() 56 ss[3] = info.Session.Encode() 57 if info.Custom != nil && len(info.Custom) > 0 { 58 for k, v := range info.Custom { 59 switch v.(type) { 60 case string: 61 ss = append(ss, fmt.Sprintf("%s=%q", k, v)) 62 case int: 63 ss = append(ss, fmt.Sprintf("%s=%d", k, v)) 64 case bool: 65 ss = append(ss, k) 66 default: 67 ss = append(ss, fmt.Sprintf("%s=%q", k, v)) 68 } 69 } 70 } 71 72 var noEmpty []string 73 for _, s := range ss { 74 if s == "" { 75 continue 76 } 77 noEmpty = append(noEmpty, s) 78 } 79 return strings.Join(noEmpty, ",") 80 } 81 82 // ParseInfo returns the Info encoded in the string s. 83 // Typical usage is to parse a URL containing the "CMCD" query parameter, 84 // then pass the corresponding value to ParseInfo. 85 // See ExampleParseInfo. 86 func ParseInfo(s string) (Info, error) { 87 return parseInfo(lex(s)) 88 } 89 90 func ExtractInfo(header http.Header) (Info, error) { 91 var fields []string 92 fields = append(fields, header.Get(HeaderRequest)) 93 fields = append(fields, header.Get(HeaderObject)) 94 fields = append(fields, header.Get(HeaderStatus)) 95 fields = append(fields, header.Get(HeaderSession)) 96 tokens := lex(strings.Join(fields, ",")) 97 return parseInfo(tokens) 98 } 99 100 // Request represents data relating to the client's... 101 type Request struct { 102 // Playback duration of the requested content. When encoded, 103 // values are rounded to the nearest 100 milliseconds. 104 BufLength time.Duration 105 // Time limit to receive a response to the request before the 106 // client may experience playback problems. 107 // When encoded, values are rounded to the nearest 100 milliseconds. 108 Deadline time.Duration 109 // Kilobits per second between client and server, as measured by the client. 110 Throughput int 111 // Relative path of the next request. 112 Next string 113 // Byte range of the next request. 114 NextRange Range 115 // If true, a response is needed urgently as playback may be 116 // starting, seeking, or the client has an empty playback buffer. 117 Startup bool 118 } 119 120 type Range [2]int 121 122 func (r Range) String() string { 123 if r[1] < 0 { 124 return fmt.Sprintf("%d-", r[0]) 125 } 126 return fmt.Sprintf("%d-%d", r[0], r[1]) 127 } 128 129 type Object struct { 130 // Encoded bitrate, in kilobits per second. 131 Bitrate int 132 // Playback duration. When encoded, values are rounded to the 133 // nearest millisecond. 134 Duration time.Duration 135 // Media type, such as audio or video. 136 Type ObjectType 137 // The client's highest allowed bitrate, in kilobits per second. 138 TopBitrate int 139 } 140 141 type ObjectType string 142 143 const ( 144 ObjTypeText ObjectType = "m" 145 ObjTypeAudio ObjectType = "a" 146 ObjTypeVideo ObjectType = "v" 147 ObjTypeAV ObjectType = "av" 148 ObjTypeI ObjectType = "i" 149 ObjTypeCaption ObjectType = "c" 150 ObjTypeTimedText ObjectType = "tt" 151 ObjTypeKey ObjectType = "k" 152 ObjTypeOther ObjectType = "o" 153 ) 154 155 func parseRange(s string) (Range, error) { 156 offset, end, found := strings.Cut(s, "-") 157 if !found { 158 return Range{}, fmt.Errorf("parse next range request: missing range separator %q", "-") 159 } 160 off, err := strconv.Atoi(offset) 161 if err != nil { 162 return Range{}, fmt.Errorf("offset: %w", err) 163 } 164 e, err := strconv.Atoi(end) 165 if err != nil { 166 return Range{}, fmt.Errorf("end: %w", err) 167 } 168 return Range{off, e}, nil 169 } 170 171 type Status struct { 172 Starved bool 173 MaxThroughput int 174 } 175 176 type Session struct { 177 // A GUID uniquely identifying the session, no longer than 64 178 // characters. 179 ID string 180 // Type of the stream. 181 StreamType 182 // A unique identifier of the client's requested content, no 183 // longer than 64 characters. 184 ContentID string 185 // The playback rate of the content. 186 PlayRate PlayRate 187 // The format of the stream, such as HLS or MPEG-DASH. 188 Format StreamFormat 189 version int 190 } 191 192 type PlayRate float32 193 194 const ( 195 Stopped PlayRate = iota 196 RealTime 197 DoubleTime 198 ) 199 200 type StreamType string 201 202 const ( 203 StreamTypeLive = "l" 204 StreamTypeVOD = "v" 205 ) 206 207 func (st StreamType) Live() bool { return st == StreamTypeLive } 208 209 type StreamFormat byte 210 211 const ( 212 FormatDASH StreamFormat = 'd' 213 FormatHLS StreamFormat = 'h' 214 FormatSmooth StreamFormat = 's' 215 FormatOther StreamFormat = 'o' 216 ) 217 218 func (c StreamFormat) String() string { 219 return fmt.Sprintf("%c", c) 220 }