streaming

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

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 }