Files
Juliusz Chroboczek e09b135cd2 Run gofmt.
2025-03-21 15:15:41 +01:00

276 lines
6.8 KiB
Go

// Package sdpfrag is an incomplete implementation of RFC 8840 Section 9
package sdpfrag
import (
"bufio"
"bytes"
"errors"
"fmt"
"github.com/pion/sdp/v3"
"github.com/pion/webrtc/v4"
)
// An SDPFrag contains the ICE-related subset of a session description.
type SDPFrag struct {
UsernameFragment, Password string
Candidates []webrtc.ICECandidateInit
MediaDescriptions []MediaDescription
}
type MediaDescription struct {
MLine string
Mid string
UsernameFragment, Password string
Candidates []webrtc.ICECandidateInit
}
func (s *SDPFrag) Unmarshal(value []byte) error {
scanner := bufio.NewScanner(bytes.NewReader(value))
var mediaDescription *MediaDescription
for scanner.Scan() {
l := scanner.Bytes()
if bytes.HasPrefix(l, []byte("a=ice-ufrag:")) {
ufrag := string(l[len("a=ice-ufrag:"):])
if mediaDescription == nil {
s.UsernameFragment = ufrag
} else {
mediaDescription.UsernameFragment = ufrag
}
} else if bytes.HasPrefix(l, []byte("a=ice-pwd:")) {
pwd := string(l[len("a=ice-pwd:"):])
if mediaDescription == nil {
s.Password = pwd
} else {
mediaDescription.Password = pwd
}
} else if bytes.HasPrefix(l, []byte("m=")) {
if mediaDescription != nil {
s.MediaDescriptions = append(
s.MediaDescriptions,
*mediaDescription,
)
}
mediaDescription = &MediaDescription{
MLine: string(l[len("m="):]),
}
} else if bytes.HasPrefix(l, []byte("a=mid:")) {
if mediaDescription == nil {
return errors.New("unexpected mid")
}
mediaDescription.Mid = string(l[len("a=mid:"):])
} else if bytes.HasPrefix(l, []byte("a=candidate:")) {
init := webrtc.ICECandidateInit{
Candidate: string(l[len("a=candidate:"):]),
}
if len(s.UsernameFragment) > 0 {
ufrag := s.UsernameFragment
init.UsernameFragment = &ufrag
}
if mediaDescription != nil {
i := uint16(len(s.MediaDescriptions))
init.SDPMLineIndex = &i
m := mediaDescription.Mid
init.SDPMid = &m
mediaDescription.Candidates = append(
mediaDescription.Candidates,
init,
)
} else {
s.Candidates = append(s.Candidates, init)
}
}
}
if mediaDescription != nil {
s.MediaDescriptions = append(
s.MediaDescriptions,
*mediaDescription,
)
}
return nil
}
func (s *SDPFrag) Marshal() ([]byte, error) {
w := new(bytes.Buffer)
if s.UsernameFragment != "" {
fmt.Fprintf(w, "a=ice-ufrag:%v\r\n", s.UsernameFragment)
}
if s.Password != "" {
fmt.Fprintf(w, "a=ice-pwd:%v\r\n", s.Password)
}
for _, c := range s.Candidates {
fmt.Fprintf(w, "a=%v\r\n", c.Candidate)
}
for _, m := range s.MediaDescriptions {
fmt.Fprintf(w, "m=%v\r\n", m.MLine)
fmt.Fprintf(w, "a=mid:%v\r\n", m.Mid)
if m.UsernameFragment != "" {
fmt.Fprintf(w, "a=ice-ufrag:%v\r\n", m.UsernameFragment)
}
if m.Password != "" {
fmt.Fprintf(w, "a=ice-pwd:%v\r\n", m.Password)
}
for _, c := range m.Candidates {
fmt.Fprintf(w, "a=candidate:%v\r\n", c.Candidate)
}
}
return w.Bytes(), nil
}
func patch(ufrag, pwd string, old []sdp.Attribute, cs []webrtc.ICECandidateInit, override bool) []sdp.Attribute {
var as []sdp.Attribute
for _, a := range old {
if a.Key == "ice-ufrag" {
if ufrag == "" {
continue
}
as = append(as, sdp.Attribute{
Key: "ice-ufrag",
Value: ufrag,
})
} else if a.Key == "ice-pwd" {
if pwd == "" {
continue
}
as = append(as, sdp.Attribute{
Key: "ice-pwd",
Value: pwd,
})
} else if !override || a.Key != "candidate" {
as = append(as, a)
}
}
for _, c := range cs {
as = append(as, sdp.Attribute{
Key: "candidate",
Value: c.Candidate,
})
}
return as
}
// SDPUFragPwd returns the ICE ufrag and password of an SDP.
// In principle, the ufrag/pwd should be at the same place in the
// frag as in the original SDP, RFC 8840 Section 9. However, we
// only do bundle, and some implementations put the data at random
// places, so we walk the whole structure to find the ufrag/pwd pair.
func SDPUFragPwd(s sdp.SessionDescription) (string, string) {
ufrag, _ := s.Attribute("ice-ufrag")
pwd, _ := s.Attribute("ice-pwd")
if ufrag != "" {
return ufrag, pwd
}
for _, m := range s.MediaDescriptions {
ufrag, _ := m.Attribute("ice-ufrag")
pwd, _ := m.Attribute("ice-pwd")
if ufrag != "" {
return ufrag, pwd
}
}
return "", ""
}
// UFragPwd returns the ICE ufrag and password of a fragment.
// See the description of [SDPUFragPwd] for details.
func (f *SDPFrag) UFragPwd() (string, string) {
ufrag := f.UsernameFragment
pwd := f.Password
if ufrag != "" {
return ufrag, pwd
}
for _, m := range f.MediaDescriptions {
ufrag := m.UsernameFragment
pwd := m.Password
if ufrag != "" {
return ufrag, pwd
}
}
return "", ""
}
func (f *SDPFrag) AllCandidates() []webrtc.ICECandidateInit {
var cs []webrtc.ICECandidateInit
cs = append(cs, f.Candidates...)
for _, m := range f.MediaDescriptions {
cs = append(cs, m.Candidates...)
}
return cs
}
// PatchSDP applies an [SDPFrag] to an [sdp.SessionDescription],
// either adding or replacing candidates depending on the ufrag/password
// values.
func PatchSDP(s sdp.SessionDescription, f SDPFrag) (sdp.SessionDescription, bool) {
ufrag, pwd := SDPUFragPwd(s)
ufrag1, pwd1 := f.UFragPwd()
override := (ufrag != ufrag1) || (pwd != pwd1)
s.Attributes = patch(ufrag1, pwd1, s.Attributes, f.Candidates, override)
old := s.MediaDescriptions
s.MediaDescriptions = nil
for i, m := range old {
var mm MediaDescription
if i < len(f.MediaDescriptions) {
mm = f.MediaDescriptions[i]
}
m.Attributes = patch(
ufrag1, pwd1, m.Attributes, mm.Candidates, override,
)
s.MediaDescriptions = append(s.MediaDescriptions, m)
}
return s, override
}
func getCandidates(as []sdp.Attribute, mid *string, index *uint16, ufrag string) []webrtc.ICECandidateInit {
var cs []webrtc.ICECandidateInit
for _, a := range as {
if a.Key != "candidate" {
continue
}
c := webrtc.ICECandidateInit{
Candidate: a.Value,
SDPMid: mid,
SDPMLineIndex: index,
}
if ufrag != "" {
c.UsernameFragment = &ufrag
}
cs = append(cs, c)
}
return cs
}
// FromSDP extracts the ICE information from an [sdp.SessionDescription]
func FromSDP(s sdp.SessionDescription) SDPFrag {
var f SDPFrag
ufrag, _ := s.Attribute("ice-ufrag")
f.UsernameFragment = ufrag
pwd, _ := s.Attribute("ice-pwd")
f.Password = pwd
f.Candidates = getCandidates(s.Attributes, nil, nil, ufrag)
for index, m := range s.MediaDescriptions {
var mm MediaDescription
mm.MLine = m.MediaName.String()
mid, _ := m.Attribute("mid")
mm.Mid = mid
ufrag, _ := m.Attribute("ice-ufrag")
mm.UsernameFragment = ufrag
pwd, _ := m.Attribute("ice-pwd")
mm.Password = pwd
i := uint16(index)
mm.Candidates = getCandidates(
m.Attributes, &mid, &i, ufrag,
)
f.MediaDescriptions = append(f.MediaDescriptions, mm)
}
return f
}