streaming

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

sdp.go (9809B)


      1 // Package sdp implements encoding and decoding of
      2 // Session Description Protocol formatted data as specified in
      3 // RFC 8866.
      4 package sdp
      5 
      6 import (
      7 	"bufio"
      8 	"fmt"
      9 	"io"
     10 	"net/mail"
     11 	"net/netip"
     12 	"net/url"
     13 	"strconv"
     14 	"strings"
     15 	"time"
     16 )
     17 
     18 type Session struct {
     19 	Origin Origin
     20 	Name   string
     21 
     22 	Info       string
     23 	URI        *url.URL
     24 	Email      *mail.Address
     25 	Phone      string
     26 	Connection *ConnInfo
     27 	Bandwidth  *Bandwidth
     28 	// Time holds the start time and stop time of the Session, at
     29 	// the first and second index respectively.
     30 	Time [2]time.Time
     31 	// Repeat points to a repetition cycle describing for how long
     32 	// and when the session may reoccur.
     33 	Repeat *Repeat
     34 	// Adjustments holds any time adjustments that may occur, for
     35 	// example daylight savings, throughout the period a repetition
     36 	// cycle is active.
     37 	Adjustments []TimeAdjustment
     38 	Attributes  []string
     39 	Media       []Media
     40 }
     41 
     42 const NoUsername string = "-"
     43 
     44 // Origin represents the originator of the session as described in RFC 8866 section 5.2.
     45 type Origin struct {
     46 	// Username is a named identity on the originating host. If unset,
     47 	// the encoded value will be NoUsername.
     48 	Username string
     49 
     50 	// ID is a globally unique identifier for the session.
     51 	// The recommended value is a timestamp from Now().
     52 	ID int
     53 
     54 	// Version is a version number of the session. It should be
     55 	// incremented each time the session is modified. The recommended
     56 	// value is a timestamp from Now().
     57 	Version int
     58 
     59 	// Address is the originating address of the session.
     60 	Address netip.Addr
     61 }
     62 
     63 func (o Origin) String() string {
     64 	ipv := "IP6"
     65 	if o.Address.Is4() {
     66 		ipv = "IP4"
     67 	}
     68 	return fmt.Sprintf("o=%s %d %d IN %s %s", o.Username, o.ID, o.Version, ipv, o.Address)
     69 }
     70 
     71 func ReadSession(rd io.Reader) (*Session, error) {
     72 	parser := &parser{Scanner: bufio.NewScanner(rd)}
     73 	if err := parser.parse(); err != nil {
     74 		return nil, fmt.Errorf("parse session: %w", err)
     75 	}
     76 	return &parser.session, nil
     77 }
     78 
     79 // cleanPhone returns the phone number in s stripped of "-" and space
     80 // characters. Since "+1 617 555-6011" is semantically equal to
     81 // "+16175556011", storing the number in the latter form lets us test for
     82 // equality more easily.
     83 func cleanPhone(s string) string {
     84 	s = strings.ReplaceAll(s, " ", "")
     85 	return strings.ReplaceAll(s, "-", "")
     86 }
     87 
     88 func parseOrigin(line string) (Origin, error) {
     89 	fields := strings.Fields(line)
     90 	if len(fields) != 6 {
     91 		return Origin{}, fmt.Errorf("need %d fields but only have %d", 6, len(fields))
     92 	}
     93 	o := Origin{Username: fields[0]}
     94 	var err error
     95 	o.ID, err = strconv.Atoi(fields[1])
     96 	if err != nil {
     97 		return o, fmt.Errorf("parse session id: %w", err)
     98 	}
     99 	o.Version, err = strconv.Atoi(fields[2])
    100 	if err != nil {
    101 		return o, fmt.Errorf("parse version: %w", err)
    102 	}
    103 	if fields[3] != "IN" {
    104 		return o, fmt.Errorf("unknown network class %q", fields[3])
    105 	}
    106 
    107 	// skip IP4/IP6 in fields[4]; netip handles the IP version for us.
    108 	addr, err := netip.ParseAddr(fields[5])
    109 	if err != nil {
    110 		return o, fmt.Errorf("parse address: %w", err)
    111 	}
    112 	o.Address = addr
    113 	return o, nil
    114 }
    115 
    116 // parseEmail returns the parsed email address from s.
    117 // Addresses should be in RFC 5322 form, for instance
    118 // "Oliver Lowe <o@olowe.co>" or just "o@olowe.co".
    119 // They can also be in the form detailed in the SDP specification, for instance
    120 // "Oliver Lowe (o@olowe.co)".
    121 func parseEmail(s string) (*mail.Address, error) {
    122 	// Oliver Lowe (o@olowe.co) to RFC 5322 format
    123 	// Oliver Lowe <o@olowe.co>
    124 	s = strings.ReplaceAll(s, "(", "<")
    125 	s = strings.ReplaceAll(s, ")", ">")
    126 	return mail.ParseAddress(s)
    127 }
    128 
    129 const (
    130 	BandwidthConferenceTotal = "CT"
    131 	BandwidthAppSpecific     = "AS"
    132 )
    133 
    134 type Bandwidth struct {
    135 	// Type describes the value of Bitrate, usually one of
    136 	// BandwidthConferenceTotal or BandwidthAppSpecific.
    137 	Type string
    138 	// Bitrate is the measure of bits per second.
    139 	Bitrate int
    140 }
    141 
    142 func (b Bandwidth) String() string {
    143 	// need kilobits per second as per section 5.8.
    144 	return fmt.Sprintf("b=%s:%d", b.Type, b.Bitrate/1e3)
    145 }
    146 
    147 func parseBandwidth(s string) (Bandwidth, error) {
    148 	t, b, ok := strings.Cut(s, ":")
    149 	if !ok {
    150 		return Bandwidth{}, fmt.Errorf("missing %s separator", ":")
    151 	}
    152 	if t == "" {
    153 		return Bandwidth{}, fmt.Errorf("missing bandwidth type")
    154 	}
    155 	kbps, err := strconv.Atoi(b)
    156 	if err != nil {
    157 		return Bandwidth{}, fmt.Errorf("parse bitrate: %w", err)
    158 	}
    159 	// convert to bits per second
    160 	return Bandwidth{t, kbps * 1e3}, nil
    161 }
    162 
    163 // Media represents a media description.
    164 type Media struct {
    165 	Type      MediaType
    166 	Port      int // IP port
    167 	PortCount int // count of subsequent ports from Port
    168 	Transport TransportProto
    169 	// Format describes the media format. Interpretation of the
    170 	// entries depends on the value of Transport. For example, if
    171 	// Transport is ProtoRTP, Format contains RTP payload type
    172 	// numbers. For more, see the <fmt> description in section 5.14
    173 	// of RFC 8866.
    174 	Format []string
    175 
    176 	// Optional fields
    177 	Title      string
    178 	Connection *ConnInfo
    179 	Bandwidth  *Bandwidth
    180 	Attributes []string
    181 }
    182 
    183 func (m Media) String() string {
    184 	buf := &strings.Builder{}
    185 	if m.PortCount == 0 {
    186 		fmt.Fprintf(buf, "m=%s %d %s %s\n", m.Type, m.Port, m.Transport, strings.Join(m.Format, " "))
    187 	} else {
    188 		fmt.Fprintf(buf, "m=%s %d/%d %s %s\n", m.Type, m.Port, m.PortCount, m.Transport, strings.Join(m.Format, " "))
    189 	}
    190 
    191 	if m.Title != "" {
    192 		fmt.Fprintf(buf, "i=%s\n", m.Title)
    193 	}
    194 	if m.Connection != nil {
    195 		fmt.Fprintln(buf, m.Connection)
    196 	}
    197 	if m.Bandwidth != nil {
    198 		fmt.Fprintln(buf, m.Bandwidth)
    199 	}
    200 	if m.Attributes != nil {
    201 		fmt.Fprintf(buf, "a=%s", strings.Join(m.Attributes, " "))
    202 	}
    203 	return strings.TrimSpace(buf.String())
    204 }
    205 
    206 type MediaType uint8
    207 
    208 const (
    209 	MediaTypeAudio MediaType = iota
    210 	MediaTypeVideo
    211 	MediaTypeText
    212 	MediaTypeApplication
    213 	MediaTypeMessage
    214 	MediaTypeImage
    215 )
    216 
    217 func (t MediaType) String() string {
    218 	switch t {
    219 	case MediaTypeAudio:
    220 		return "audio"
    221 	case MediaTypeVideo:
    222 		return "video"
    223 	case MediaTypeText:
    224 		return "text"
    225 	case MediaTypeApplication:
    226 		return "application"
    227 	case MediaTypeMessage:
    228 		return "message"
    229 	case MediaTypeImage:
    230 		return "image"
    231 	}
    232 	return "unknown"
    233 }
    234 
    235 func parseMediaType(s string) (MediaType, error) {
    236 	switch s {
    237 	case MediaTypeAudio.String():
    238 		return MediaTypeAudio, nil
    239 	case MediaTypeVideo.String():
    240 		return MediaTypeVideo, nil
    241 	case MediaTypeText.String():
    242 		return MediaTypeText, nil
    243 	case MediaTypeApplication.String():
    244 		return MediaTypeApplication, nil
    245 	case MediaTypeMessage.String():
    246 		return MediaTypeMessage, nil
    247 	case MediaTypeImage.String():
    248 		return MediaTypeImage, nil
    249 	}
    250 	return 0, fmt.Errorf("unknown media type %s", s)
    251 }
    252 
    253 type TransportProto uint8
    254 
    255 const (
    256 	ProtoUDP TransportProto = iota
    257 	ProtoRTP
    258 	ProtoRTPSecure
    259 	ProtoRTPSecureFeedback
    260 )
    261 
    262 func (tp TransportProto) String() string {
    263 	switch tp {
    264 	case ProtoUDP:
    265 		return "udp"
    266 	case ProtoRTP:
    267 		return "RTP/AVP"
    268 	case ProtoRTPSecure:
    269 		return "RTP/SAVP"
    270 	case ProtoRTPSecureFeedback:
    271 		return "RTP/SAVPF"
    272 	}
    273 	return "unknown"
    274 }
    275 
    276 func parseMedia(s string) (Media, error) {
    277 	fields := strings.Fields(s)
    278 	if len(fields) < 4 {
    279 		return Media{}, fmt.Errorf("found %d fields, need at least %d", len(fields), 4)
    280 	}
    281 
    282 	mtyp, err := parseMediaType(fields[0])
    283 	if err != nil {
    284 		return Media{}, fmt.Errorf("media type: %w", err)
    285 	}
    286 	m := Media{Type: mtyp}
    287 
    288 	p, n, found := strings.Cut(fields[1], "/")
    289 	m.Port, err = strconv.Atoi(p)
    290 	if err != nil {
    291 		return Media{}, fmt.Errorf("parse port: %w", err)
    292 	}
    293 	if found {
    294 		m.PortCount, err = strconv.Atoi(n)
    295 		if err != nil {
    296 			return Media{}, fmt.Errorf("parse port count: %w", err)
    297 		}
    298 	}
    299 
    300 	switch fields[2] {
    301 	case ProtoUDP.String():
    302 		m.Transport = ProtoUDP
    303 	case ProtoRTP.String():
    304 		m.Transport = ProtoRTP
    305 	case ProtoRTPSecure.String():
    306 		m.Transport = ProtoRTPSecure
    307 	case ProtoRTPSecureFeedback.String():
    308 		m.Transport = ProtoRTPSecureFeedback
    309 	default:
    310 		return Media{}, fmt.Errorf("unknown protocol %s", fields[2])
    311 	}
    312 
    313 	m.Format = fields[3:]
    314 	return m, nil
    315 }
    316 
    317 // ConnInfo represents connection information.
    318 type ConnInfo struct {
    319 	Address netip.Addr
    320 	// TTL is the time-to-live of IPv4 multicast packets.
    321 	TTL uint8
    322 	// Count is the number of subsequent IP addresses after
    323 	// Address used in the session.
    324 	Count int
    325 }
    326 
    327 func (c *ConnInfo) String() string {
    328 	ipv := "IP6"
    329 	if c.Address.Is4() {
    330 		ipv = "IP4"
    331 	}
    332 	s := fmt.Sprintf("c=%s %s %s", "IN", ipv, c.Address)
    333 	if c.Address.Is4() && c.TTL > 0 {
    334 		s += fmt.Sprintf("/%d", c.TTL)
    335 	}
    336 	if c.Count > 0 {
    337 		s += fmt.Sprintf("/%d", c.Count)
    338 	}
    339 	return s
    340 }
    341 
    342 func parseConnInfo(s string) (ConnInfo, error) {
    343 	fields := strings.Fields(s)
    344 	if len(fields) != 3 {
    345 		return ConnInfo{}, fmt.Errorf("expected %d fields, got %d", 3, len(fields))
    346 	}
    347 	if fields[0] != "IN" {
    348 		return ConnInfo{}, fmt.Errorf("unsupported class %q, expected IN", fields[0])
    349 	}
    350 
    351 	var conn ConnInfo
    352 	if fields[1] != "IP4" && fields[1] != "IP6" {
    353 		return conn, fmt.Errorf("unsupported network type %s", fields[2])
    354 	}
    355 	addr := strings.Split(fields[2], "/")
    356 	var err error
    357 	conn.Address, err = netip.ParseAddr(addr[0])
    358 	if err != nil {
    359 		return conn, fmt.Errorf("parse address: %w", err)
    360 	}
    361 	if len(addr) == 1 {
    362 		return conn, nil
    363 	}
    364 
    365 	subfields := make([]int, len(addr[1:]))
    366 	for i := range subfields {
    367 		var err error
    368 		subfields[i], err = strconv.Atoi(addr[i+1])
    369 		if err != nil {
    370 			return conn, fmt.Errorf("parse address subfield %d: %w", i, err)
    371 		}
    372 	}
    373 
    374 	if conn.Address.Is4() {
    375 		if subfields[0] < 0 || subfields[0] > 255 {
    376 			return conn, fmt.Errorf("ttl: %d is outside uint8 range", subfields[0])
    377 		}
    378 		conn.TTL = uint8(subfields[0])
    379 		if len(subfields) == 2 {
    380 			conn.Count = subfields[1]
    381 		}
    382 	}
    383 
    384 	if conn.Address.Is6() && len(subfields) > 1 {
    385 		return conn, fmt.Errorf("parse address: only 1 subfield allowed, read %d", len(subfields))
    386 	} else if conn.Address.Is6() && len(subfields) == 1 {
    387 		conn.Count = subfields[0]
    388 	}
    389 	return conn, nil
    390 }