276 lines
6.8 KiB
Go
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
|
|
}
|