install go2rtc on bob
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
## StateChange
|
||||
|
||||
1. offer = pc.CreateOffer()
|
||||
2. pc.SetLocalDescription(offer)
|
||||
3. OnICEGatheringStateChange: gathering
|
||||
4. OnSignalingStateChange: have-local-offer
|
||||
*. OnICEGatheringStateChange: complete
|
||||
5. pc.SetRemoteDescription(answer)
|
||||
6. OnSignalingStateChange: stable
|
||||
7. OnICEConnectionStateChange: checking
|
||||
8. OnICEConnectionStateChange: connected
|
||||
@@ -0,0 +1,316 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"slices"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/xnet"
|
||||
"github.com/pion/ice/v4"
|
||||
"github.com/pion/interceptor"
|
||||
"github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
// ReceiveMTU = Ethernet MTU (1500) - IP Header (20) - UDP Header (8)
|
||||
// https://ffmpeg.org/ffmpeg-all.html#Muxer
|
||||
const ReceiveMTU = 1472
|
||||
|
||||
func NewAPI() (*webrtc.API, error) {
|
||||
return NewServerAPI("", "", nil)
|
||||
}
|
||||
|
||||
type Filters struct {
|
||||
Candidates []string `yaml:"candidates"`
|
||||
Loopback bool `yaml:"loopback"`
|
||||
Interfaces []string `yaml:"interfaces"`
|
||||
IPs []string `yaml:"ips"`
|
||||
Networks []string `yaml:"networks"`
|
||||
UDPPorts []uint16 `yaml:"udp_ports"`
|
||||
}
|
||||
|
||||
func (f *Filters) Network(protocol string) string {
|
||||
if f == nil || f.Networks == nil {
|
||||
return protocol
|
||||
}
|
||||
v4 := slices.Contains(f.Networks, protocol+"4")
|
||||
v6 := slices.Contains(f.Networks, protocol+"6")
|
||||
if v4 && v6 {
|
||||
return protocol
|
||||
} else if v4 {
|
||||
return protocol + "4"
|
||||
} else if v6 {
|
||||
return protocol + "6"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (f *Filters) NetIPs() (ips []net.IP) {
|
||||
itfs, _ := net.Interfaces()
|
||||
for _, itf := range itfs {
|
||||
if itf.Flags&net.FlagUp == 0 {
|
||||
continue
|
||||
}
|
||||
if !f.IncludeLoopback() && itf.Flags&net.FlagLoopback != 0 {
|
||||
continue
|
||||
}
|
||||
if !f.InterfaceFilter(itf.Name) {
|
||||
continue
|
||||
}
|
||||
|
||||
addrs, _ := itf.Addrs()
|
||||
for _, addr := range addrs {
|
||||
ip := parseNetAddr(addr)
|
||||
if ip == nil || !f.IPFilter(ip) {
|
||||
continue
|
||||
}
|
||||
ips = append(ips, ip)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func parseNetAddr(addr net.Addr) net.IP {
|
||||
switch addr := addr.(type) {
|
||||
case *net.IPNet:
|
||||
return addr.IP
|
||||
case *net.IPAddr:
|
||||
return addr.IP
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Filters) IncludeLoopback() bool {
|
||||
return f != nil && f.Loopback
|
||||
}
|
||||
|
||||
func (f *Filters) InterfaceFilter(name string) bool {
|
||||
return f == nil || f.Interfaces == nil || slices.Contains(f.Interfaces, name)
|
||||
}
|
||||
|
||||
func (f *Filters) IPFilter(ip net.IP) bool {
|
||||
return f == nil || f.IPs == nil || core.Contains(f.IPs, ip.String())
|
||||
}
|
||||
|
||||
func NewServerAPI(network, address string, filters *Filters) (*webrtc.API, error) {
|
||||
// for debug logs add to env: `PION_LOG_DEBUG=all`
|
||||
m := &webrtc.MediaEngine{}
|
||||
//if err := m.RegisterDefaultCodecs(); err != nil {
|
||||
// return nil, err
|
||||
//}
|
||||
if err := RegisterDefaultCodecs(m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
i := &interceptor.Registry{}
|
||||
if err := webrtc.RegisterDefaultInterceptors(m, i); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s := webrtc.SettingEngine{}
|
||||
|
||||
// fix https://github.com/pion/webrtc/pull/2407
|
||||
s.SetDTLSInsecureSkipHelloVerify(true)
|
||||
|
||||
if filters != nil && filters.Loopback {
|
||||
s.SetIncludeLoopbackCandidate(true)
|
||||
}
|
||||
|
||||
var interfaceFilter func(name string) bool
|
||||
if filters != nil && filters.Interfaces != nil {
|
||||
interfaceFilter = func(name string) bool {
|
||||
return core.Contains(filters.Interfaces, name)
|
||||
}
|
||||
} else {
|
||||
// default interfaces - all, except loopback
|
||||
}
|
||||
s.SetInterfaceFilter(interfaceFilter)
|
||||
|
||||
var ipFilter func(ip net.IP) bool
|
||||
if filters != nil && filters.IPs != nil {
|
||||
ipFilter = func(ip net.IP) bool {
|
||||
return core.Contains(filters.IPs, ip.String())
|
||||
}
|
||||
} else {
|
||||
// try filter all Docker-like interfaces
|
||||
ipFilter = func(ip net.IP) bool {
|
||||
return !xnet.Docker.Contains(ip)
|
||||
}
|
||||
// if there are no such interfaces - disable the filter
|
||||
// the user will need to enable port forwarding
|
||||
if nets, _ := xnet.IPNets(ipFilter); len(nets) == 0 {
|
||||
ipFilter = nil
|
||||
}
|
||||
}
|
||||
s.SetIPFilter(ipFilter)
|
||||
|
||||
var networkTypes []webrtc.NetworkType
|
||||
if filters != nil && filters.Networks != nil {
|
||||
for _, s := range filters.Networks {
|
||||
if networkType, err := webrtc.NewNetworkType(s); err == nil {
|
||||
networkTypes = append(networkTypes, networkType)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// default network types - all
|
||||
networkTypes = []webrtc.NetworkType{
|
||||
webrtc.NetworkTypeUDP4, webrtc.NetworkTypeUDP6,
|
||||
webrtc.NetworkTypeTCP4, webrtc.NetworkTypeTCP6,
|
||||
}
|
||||
}
|
||||
s.SetNetworkTypes(networkTypes)
|
||||
|
||||
if filters != nil && len(filters.UDPPorts) == 2 {
|
||||
_ = s.SetEphemeralUDPPortRange(filters.UDPPorts[0], filters.UDPPorts[1])
|
||||
}
|
||||
|
||||
// If you don't specify an address, this won't cause an error.
|
||||
// Connections can still be established using random UDP addresses.
|
||||
if address != "" {
|
||||
// Both newMux functions respect filters and do not raise an error
|
||||
// if the port cannot be listened on.
|
||||
if network == "" || network == "tcp" {
|
||||
tcpMux := newTCPMux(address, filters)
|
||||
s.SetICETCPMux(tcpMux)
|
||||
}
|
||||
if network == "" || network == "udp" {
|
||||
udpMux := newUDPMux(address, filters)
|
||||
s.SetICEUDPMux(udpMux)
|
||||
}
|
||||
}
|
||||
|
||||
return webrtc.NewAPI(
|
||||
webrtc.WithMediaEngine(m),
|
||||
webrtc.WithInterceptorRegistry(i),
|
||||
webrtc.WithSettingEngine(s),
|
||||
), nil
|
||||
}
|
||||
|
||||
// OnNewListener temporary ugly solution for log
|
||||
var OnNewListener = func(ln any) {}
|
||||
|
||||
func newTCPMux(address string, filters *Filters) ice.TCPMux {
|
||||
networkTCP := filters.Network("tcp") // tcp or tcp4 or tcp6
|
||||
if ln, _ := net.Listen(networkTCP, address); ln != nil {
|
||||
OnNewListener(ln)
|
||||
return webrtc.NewICETCPMux(nil, ln, 8)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func newUDPMux(address string, filters *Filters) ice.UDPMux {
|
||||
host, port, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// UDPMux should not listening on unspecified address.
|
||||
// So we will create a listener on all available interfaces.
|
||||
// We can't use ice.NewMultiUDPMuxFromPort, because it sometimes crashes with an error:
|
||||
// listen udp [***]:8555: bind: cannot assign requested address
|
||||
var addrs []string
|
||||
if host == "" {
|
||||
for _, ip := range filters.NetIPs() {
|
||||
addrs = append(addrs, fmt.Sprintf("%s:%s", ip, port))
|
||||
}
|
||||
} else {
|
||||
addrs = []string{address}
|
||||
}
|
||||
|
||||
networkUDP := filters.Network("udp") // udp or udp4 or udp6
|
||||
|
||||
var muxes []ice.UDPMux
|
||||
for _, addr := range addrs {
|
||||
if ln, _ := net.ListenPacket(networkUDP, addr); ln != nil {
|
||||
OnNewListener(ln)
|
||||
mux := ice.NewUDPMuxDefault(ice.UDPMuxParams{UDPConn: ln})
|
||||
muxes = append(muxes, mux)
|
||||
}
|
||||
}
|
||||
|
||||
switch len(muxes) {
|
||||
case 0:
|
||||
return nil
|
||||
case 1:
|
||||
return muxes[0]
|
||||
}
|
||||
return ice.NewMultiUDPMuxDefault(muxes...)
|
||||
}
|
||||
|
||||
func RegisterDefaultCodecs(m *webrtc.MediaEngine) error {
|
||||
for _, codec := range []webrtc.RTPCodecParameters{
|
||||
{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: webrtc.MimeTypeOpus, ClockRate: 48000, Channels: 2, SDPFmtpLine: "minptime=10;useinbandfec=1",
|
||||
},
|
||||
PayloadType: 101, //111,
|
||||
},
|
||||
{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: webrtc.MimeTypePCMU, ClockRate: 8000,
|
||||
},
|
||||
PayloadType: 0,
|
||||
},
|
||||
{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: webrtc.MimeTypePCMA, ClockRate: 8000,
|
||||
},
|
||||
PayloadType: 8,
|
||||
},
|
||||
} {
|
||||
if err := m.RegisterCodec(codec, webrtc.RTPCodecTypeAudio); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
videoRTCPFeedback := []webrtc.RTCPFeedback{
|
||||
{"goog-remb", ""},
|
||||
{"ccm", "fir"},
|
||||
{"nack", ""},
|
||||
{"nack", "pli"},
|
||||
}
|
||||
for _, codec := range []webrtc.RTPCodecParameters{
|
||||
{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: webrtc.MimeTypeH264,
|
||||
ClockRate: 90000,
|
||||
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f",
|
||||
RTCPFeedback: videoRTCPFeedback,
|
||||
},
|
||||
PayloadType: 96, // Chrome v110 - PayloadType: 102
|
||||
},
|
||||
{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: webrtc.MimeTypeH264,
|
||||
ClockRate: 90000,
|
||||
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f",
|
||||
RTCPFeedback: videoRTCPFeedback,
|
||||
},
|
||||
PayloadType: 97, // Chrome v110 - PayloadType: 106
|
||||
},
|
||||
{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: webrtc.MimeTypeH264,
|
||||
ClockRate: 90000,
|
||||
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640032",
|
||||
RTCPFeedback: videoRTCPFeedback,
|
||||
},
|
||||
PayloadType: 98, // Chrome v110 - PayloadType: 112
|
||||
},
|
||||
// macOS Safari 15.1
|
||||
{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: webrtc.MimeTypeH265,
|
||||
ClockRate: 90000,
|
||||
RTCPFeedback: videoRTCPFeedback,
|
||||
},
|
||||
PayloadType: 100,
|
||||
},
|
||||
} {
|
||||
if err := m.RegisterCodec(codec, webrtc.RTPCodecTypeVideo); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/pion/sdp/v3"
|
||||
"github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
func (c *Conn) CreateOffer(medias []*core.Media) (string, error) {
|
||||
// 1. Create transeivers with proper kind and direction
|
||||
for _, media := range medias {
|
||||
var err error
|
||||
switch media.Direction {
|
||||
case core.DirectionRecvonly:
|
||||
_, err = c.pc.AddTransceiverFromKind(
|
||||
webrtc.NewRTPCodecType(media.Kind),
|
||||
webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionRecvonly},
|
||||
)
|
||||
case core.DirectionSendonly:
|
||||
_, err = c.pc.AddTransceiverFromTrack(
|
||||
NewTrack(media.Kind),
|
||||
webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionSendonly},
|
||||
)
|
||||
case core.DirectionSendRecv:
|
||||
// default transceiver is sendrecv
|
||||
_, err = c.pc.AddTransceiverFromTrack(NewTrack(media.Kind))
|
||||
default:
|
||||
// Nest cameras require data channel
|
||||
_, err = c.pc.CreateDataChannel(media.Kind, nil)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Create local offer
|
||||
desc, err := c.pc.CreateOffer(nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 3. Start gathering phase
|
||||
if err = c.pc.SetLocalDescription(desc); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return c.pc.LocalDescription().SDP, nil
|
||||
}
|
||||
|
||||
func (c *Conn) CreateCompleteOffer(medias []*core.Media) (string, error) {
|
||||
if _, err := c.CreateOffer(medias); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
<-webrtc.GatheringCompletePromise(c.pc)
|
||||
return c.pc.LocalDescription().SDP, nil
|
||||
}
|
||||
|
||||
func (c *Conn) SetAnswer(answer string) (err error) {
|
||||
desc := webrtc.SessionDescription{
|
||||
Type: webrtc.SDPTypeAnswer,
|
||||
SDP: fakeFormatsInAnswer(c.pc.LocalDescription().SDP, answer),
|
||||
}
|
||||
if err = c.pc.SetRemoteDescription(desc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sd := &sdp.SessionDescription{}
|
||||
if err = sd.Unmarshal([]byte(answer)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Medias = UnmarshalMedias(sd.MediaDescriptions)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// fakeFormatsInAnswer - fix pion bug with remote SDP parsing:
|
||||
// pion will process formats only from first media of each kind
|
||||
// so we add all formats from first offer media to the first answer media
|
||||
func fakeFormatsInAnswer(offer, answer string) string {
|
||||
sd2 := &sdp.SessionDescription{}
|
||||
if err := sd2.Unmarshal([]byte(answer)); err != nil {
|
||||
return answer
|
||||
}
|
||||
|
||||
// check if answer has recvonly audio
|
||||
var ok bool
|
||||
for _, md2 := range sd2.MediaDescriptions {
|
||||
if md2.MediaName.Media == "audio" {
|
||||
if _, ok = md2.Attribute("recvonly"); ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
return answer
|
||||
}
|
||||
|
||||
sd1 := &sdp.SessionDescription{}
|
||||
if err := sd1.Unmarshal([]byte(offer)); err != nil {
|
||||
return answer
|
||||
}
|
||||
|
||||
var formats []string
|
||||
var attrs []sdp.Attribute
|
||||
|
||||
for _, md1 := range sd1.MediaDescriptions {
|
||||
if md1.MediaName.Media == "audio" {
|
||||
for _, attr := range md1.Attributes {
|
||||
switch attr.Key {
|
||||
case "rtpmap", "fmtp", "rtcp-fb", "extmap":
|
||||
attrs = append(attrs, attr)
|
||||
}
|
||||
}
|
||||
|
||||
formats = md1.MediaName.Formats
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for _, md2 := range sd2.MediaDescriptions {
|
||||
if md2.MediaName.Media == "audio" {
|
||||
for _, attr := range md2.Attributes {
|
||||
switch attr.Key {
|
||||
case "rtpmap", "fmtp", "rtcp-fb", "extmap":
|
||||
default:
|
||||
attrs = append(attrs, attr)
|
||||
}
|
||||
}
|
||||
|
||||
md2.MediaName.Formats = formats
|
||||
md2.Attributes = attrs
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
b, err := sd2.Marshal()
|
||||
if err != nil {
|
||||
return answer
|
||||
}
|
||||
|
||||
return string(b)
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/pion/webrtc/v4"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestClient(t *testing.T) {
|
||||
api, err := NewAPI()
|
||||
require.Nil(t, err)
|
||||
|
||||
pc, err := api.NewPeerConnection(webrtc.Configuration{})
|
||||
require.Nil(t, err)
|
||||
|
||||
prod := NewConn(pc)
|
||||
|
||||
medias := []*core.Media{
|
||||
{Kind: core.KindVideo, Direction: core.DirectionRecvonly},
|
||||
{Kind: core.KindAudio, Direction: core.DirectionRecvonly},
|
||||
{Kind: core.KindAudio, Direction: core.DirectionSendonly},
|
||||
}
|
||||
|
||||
offer, err := prod.CreateOffer(medias)
|
||||
require.Nil(t, err)
|
||||
assert.NotEmpty(t, offer)
|
||||
|
||||
require.Len(t, prod.pc.GetReceivers(), 2)
|
||||
require.Len(t, prod.pc.GetSenders(), 1)
|
||||
|
||||
answer := `v=0
|
||||
o=- 1934370540648269799 1678277622 IN IP4 0.0.0.0
|
||||
s=-
|
||||
t=0 0
|
||||
a=fingerprint:sha-256 77:8C:9A:62:51:81:69:EA:4E:BE:93:6B:4E:DF:51:D2:2F:E3:DF:E7:F4:8A:18:1A:C0:74:FA:AE:B8:98:29:9B
|
||||
a=extmap-allow-mixed
|
||||
a=group:BUNDLE 0 1 2
|
||||
m=video 9 UDP/TLS/RTP/SAVPF 97
|
||||
c=IN IP4 0.0.0.0
|
||||
a=setup:active
|
||||
a=mid:0
|
||||
a=ice-ufrag:xxx
|
||||
a=ice-pwd:xxx
|
||||
a=rtcp-mux
|
||||
a=rtcp-rsize
|
||||
a=rtpmap:97 H264/90000
|
||||
a=fmtp:97 packetization-mode=1;profile-level-id=42e01f
|
||||
a=extmap:1 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
|
||||
a=ssrc:2815449682 cname:go2rtc
|
||||
a=ssrc:2815449682 msid:go2rtc video
|
||||
a=ssrc:2815449682 mslabel:go2rtc
|
||||
a=ssrc:2815449682 label:video
|
||||
a=msid:go2rtc video
|
||||
a=sendonly
|
||||
m=audio 9 UDP/TLS/RTP/SAVPF 8
|
||||
c=IN IP4 0.0.0.0
|
||||
a=setup:active
|
||||
a=mid:1
|
||||
a=ice-ufrag:xxx
|
||||
a=ice-pwd:xxx
|
||||
a=rtcp-mux
|
||||
a=rtcp-rsize
|
||||
a=rtpmap:8 PCMA/8000
|
||||
a=extmap:1 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
|
||||
a=ssrc:1392166302 cname:go2rtc
|
||||
a=ssrc:1392166302 msid:go2rtc audio
|
||||
a=ssrc:1392166302 mslabel:go2rtc
|
||||
a=ssrc:1392166302 label:audio
|
||||
a=msid:go2rtc audio
|
||||
a=sendonly
|
||||
m=audio 9 UDP/TLS/RTP/SAVPF 0
|
||||
c=IN IP4 0.0.0.0
|
||||
a=setup:active
|
||||
a=mid:2
|
||||
a=ice-ufrag:xxx
|
||||
a=ice-pwd:xxx
|
||||
a=rtcp-mux
|
||||
a=rtcp-rsize
|
||||
a=rtpmap:0 PCMU/8000
|
||||
a=extmap:1 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
|
||||
a=recvonly
|
||||
`
|
||||
|
||||
err = prod.SetAnswer(answer)
|
||||
require.Nil(t, err)
|
||||
|
||||
sender := prod.pc.GetSenders()[0]
|
||||
|
||||
caps := webrtc.RTPCodecCapability{
|
||||
MimeType: webrtc.MimeTypePCMU,
|
||||
ClockRate: 8000,
|
||||
Channels: 0,
|
||||
}
|
||||
track := sender.Track()
|
||||
track, err = webrtc.NewTrackLocalStaticRTP(caps, track.ID(), track.StreamID())
|
||||
require.Nil(t, err)
|
||||
|
||||
err = sender.ReplaceTrack(track)
|
||||
require.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestUnmarshalICEServers(t *testing.T) {
|
||||
s := `[{"credential":"xxx","urls":"xxx","username":"xxx"},{"credential":null,"urls":"xxx","username":null}]`
|
||||
servers, err := UnmarshalICEServers([]byte(s))
|
||||
require.Nil(t, err)
|
||||
require.Len(t, servers, 2)
|
||||
require.Equal(t, []string{"xxx"}, servers[0].URLs)
|
||||
|
||||
s = `[{"urls":"xxx"},{"urls":["yyy","zzz"]}]`
|
||||
servers, err = UnmarshalICEServers([]byte(s))
|
||||
require.Nil(t, err)
|
||||
require.Len(t, servers, 2)
|
||||
require.Equal(t, []string{"xxx"}, servers[0].URLs)
|
||||
require.Equal(t, []string{"yyy", "zzz"}, servers[1].URLs)
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/pion/rtcp"
|
||||
"github.com/pion/rtp"
|
||||
"github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
type Conn struct {
|
||||
core.Connection
|
||||
core.Listener
|
||||
|
||||
Mode core.Mode `json:"mode"`
|
||||
|
||||
pc *webrtc.PeerConnection
|
||||
|
||||
offer string
|
||||
closed core.Waiter
|
||||
}
|
||||
|
||||
func NewConn(pc *webrtc.PeerConnection) *Conn {
|
||||
c := &Conn{
|
||||
Connection: core.Connection{
|
||||
ID: core.NewID(),
|
||||
FormatName: "webrtc",
|
||||
Transport: pc,
|
||||
},
|
||||
pc: pc,
|
||||
}
|
||||
|
||||
pc.OnICECandidate(func(candidate *webrtc.ICECandidate) {
|
||||
// last candidate will be empty
|
||||
if candidate != nil {
|
||||
c.Fire(candidate)
|
||||
}
|
||||
})
|
||||
|
||||
pc.OnDataChannel(func(channel *webrtc.DataChannel) {
|
||||
c.Fire(channel)
|
||||
})
|
||||
|
||||
pc.OnICEConnectionStateChange(func(state webrtc.ICEConnectionState) {
|
||||
if state != webrtc.ICEConnectionStateChecking {
|
||||
return
|
||||
}
|
||||
pc.SCTP().Transport().ICETransport().OnSelectedCandidatePairChange(
|
||||
func(pair *webrtc.ICECandidatePair) {
|
||||
// fix situation when candidate pair changes multiple times
|
||||
if i := strings.IndexByte(c.Protocol, '+'); i > 0 {
|
||||
c.Protocol = c.Protocol[:i]
|
||||
}
|
||||
c.Protocol += "+" + pair.Remote.Protocol.String()
|
||||
c.RemoteAddr = fmt.Sprintf(
|
||||
"%s:%d %s", sanitizeIP6(pair.Remote.Address), pair.Remote.Port, pair.Remote.Typ,
|
||||
)
|
||||
if pair.Remote.RelatedAddress != "" {
|
||||
c.RemoteAddr += fmt.Sprintf(
|
||||
" %s:%d", sanitizeIP6(pair.Remote.RelatedAddress), pair.Remote.RelatedPort,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
pc.OnTrack(func(remote *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {
|
||||
media, codec := c.getMediaCodec(remote)
|
||||
if media == nil {
|
||||
return
|
||||
}
|
||||
|
||||
track, err := c.GetTrack(media, codec)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch c.Mode {
|
||||
case core.ModePassiveProducer, core.ModeActiveProducer:
|
||||
// replace the theoretical list of codecs with the actual list of codecs
|
||||
if len(media.Codecs) > 1 {
|
||||
media.Codecs = []*core.Codec{codec}
|
||||
}
|
||||
}
|
||||
|
||||
if c.Mode == core.ModePassiveProducer && remote.Kind() == webrtc.RTPCodecTypeVideo {
|
||||
go func() {
|
||||
pkts := []rtcp.Packet{&rtcp.PictureLossIndication{MediaSSRC: uint32(remote.SSRC())}}
|
||||
for range time.NewTicker(time.Second * 2).C {
|
||||
if err := pc.WriteRTCP(pkts); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
for {
|
||||
b := make([]byte, ReceiveMTU)
|
||||
n, _, err := remote.Read(b)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.Recv += n
|
||||
|
||||
packet := &rtp.Packet{}
|
||||
if err := packet.Unmarshal(b[:n]); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(packet.Payload) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
track.WriteRTP(packet)
|
||||
}
|
||||
})
|
||||
|
||||
// OK connection:
|
||||
// 15:01:46 ICE connection state changed: checking
|
||||
// 15:01:46 peer connection state changed: connected
|
||||
// 15:01:54 peer connection state changed: disconnected
|
||||
// 15:02:20 peer connection state changed: failed
|
||||
//
|
||||
// Fail connection:
|
||||
// 14:53:08 ICE connection state changed: checking
|
||||
// 14:53:39 peer connection state changed: failed
|
||||
pc.OnConnectionStateChange(func(state webrtc.PeerConnectionState) {
|
||||
c.Fire(state)
|
||||
|
||||
switch state {
|
||||
case webrtc.PeerConnectionStateConnected:
|
||||
for _, sender := range c.Senders {
|
||||
sender.Start()
|
||||
}
|
||||
case webrtc.PeerConnectionStateDisconnected, webrtc.PeerConnectionStateFailed, webrtc.PeerConnectionStateClosed:
|
||||
// disconnect event comes earlier, than failed
|
||||
// but it comes only for success connections
|
||||
_ = c.Close()
|
||||
}
|
||||
})
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Conn) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(c.Connection)
|
||||
}
|
||||
|
||||
func (c *Conn) Close() error {
|
||||
c.closed.Done(nil)
|
||||
return c.pc.Close()
|
||||
}
|
||||
|
||||
func (c *Conn) AddCandidate(candidate string) error {
|
||||
// pion uses only candidate value from json/object candidate struct
|
||||
return c.pc.AddICECandidate(webrtc.ICECandidateInit{Candidate: candidate})
|
||||
}
|
||||
|
||||
func (c *Conn) GetSenderTrack(mid string) *Track {
|
||||
if tr := c.getTranseiver(mid); tr != nil {
|
||||
if s := tr.Sender(); s != nil {
|
||||
if t := s.Track().(*Track); t != nil {
|
||||
return t
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) getTranseiver(mid string) *webrtc.RTPTransceiver {
|
||||
for _, tr := range c.pc.GetTransceivers() {
|
||||
if tr.Mid() == mid {
|
||||
return tr
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) getMediaCodec(remote *webrtc.TrackRemote) (*core.Media, *core.Codec) {
|
||||
for _, tr := range c.pc.GetTransceivers() {
|
||||
// search Transeiver for this TrackRemote
|
||||
if tr.Receiver() == nil || tr.Receiver().Track() != remote {
|
||||
continue
|
||||
}
|
||||
|
||||
// search Media for this MID
|
||||
for _, media := range c.Medias {
|
||||
if media.ID != tr.Mid() || media.Direction != core.DirectionRecvonly {
|
||||
continue
|
||||
}
|
||||
|
||||
// search codec for this PayloadType
|
||||
for _, codec := range media.Codecs {
|
||||
if codec.PayloadType != uint8(remote.PayloadType()) {
|
||||
continue
|
||||
}
|
||||
return media, codec
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fix moment when core.ModePassiveProducer or core.ModeActiveProducer
|
||||
// sends new codec with new payload type to same media
|
||||
// check GetTrack
|
||||
panic(core.Caller())
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func sanitizeIP6(host string) string {
|
||||
if strings.IndexByte(host, ':') > 0 {
|
||||
return "[" + host + "]"
|
||||
}
|
||||
return host
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h265"
|
||||
"github.com/AlexxIT/go2rtc/pkg/pcm"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
func (c *Conn) GetMedias() []*core.Media {
|
||||
return WithResampling(c.Medias)
|
||||
}
|
||||
|
||||
func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
|
||||
core.Assert(media.Direction == core.DirectionSendonly)
|
||||
|
||||
for _, sender := range c.Senders {
|
||||
if sender.Codec == codec {
|
||||
sender.Bind(track)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
switch c.Mode {
|
||||
case core.ModePassiveConsumer: // video/audio for browser
|
||||
case core.ModeActiveProducer: // go2rtc as WebRTC client (backchannel)
|
||||
case core.ModePassiveProducer: // WebRTC/WHIP
|
||||
default:
|
||||
panic(core.Caller())
|
||||
}
|
||||
|
||||
localTrack := c.GetSenderTrack(media.ID)
|
||||
if localTrack == nil {
|
||||
return errors.New("webrtc: can't get track")
|
||||
}
|
||||
|
||||
payloadType := codec.PayloadType
|
||||
|
||||
sender := core.NewSender(media, codec)
|
||||
sender.Handler = func(packet *rtp.Packet) {
|
||||
c.Send += packet.MarshalSize()
|
||||
//important to send with remote PayloadType
|
||||
_ = localTrack.WriteRTP(payloadType, packet)
|
||||
}
|
||||
|
||||
switch track.Codec.Name {
|
||||
case core.CodecH264:
|
||||
sender.Handler = h264.RTPPay(1200, sender.Handler)
|
||||
if track.Codec.IsRTP() {
|
||||
sender.Handler = h264.RTPDepay(track.Codec, sender.Handler)
|
||||
} else {
|
||||
sender.Handler = h264.RepairAVCC(track.Codec, sender.Handler)
|
||||
}
|
||||
|
||||
case core.CodecH265:
|
||||
sender.Handler = h265.RTPPay(1200, sender.Handler)
|
||||
if track.Codec.IsRTP() {
|
||||
sender.Handler = h265.RTPDepay(track.Codec, sender.Handler)
|
||||
} else {
|
||||
sender.Handler = h265.RepairAVCC(track.Codec, sender.Handler)
|
||||
}
|
||||
|
||||
case core.CodecPCMA, core.CodecPCMU, core.CodecPCM, core.CodecPCML:
|
||||
// Fix audio quality https://github.com/AlexxIT/WebRTC/issues/500
|
||||
// should be before ResampleToG711, because it will be called last
|
||||
sender.Handler = pcm.RepackG711(false, sender.Handler)
|
||||
|
||||
if codec.ClockRate == 0 {
|
||||
if codec.Name == core.CodecPCM || codec.Name == core.CodecPCML {
|
||||
codec.Name = core.CodecPCMA
|
||||
}
|
||||
codec.ClockRate = 8000
|
||||
sender.Handler = pcm.TranscodeHandler(codec, track.Codec, sender.Handler)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: rewrite this dirty logic
|
||||
// maybe not best solution, but ActiveProducer connected before AddTrack
|
||||
if c.Mode != core.ModeActiveProducer {
|
||||
sender.Bind(track)
|
||||
} else {
|
||||
sender.HandleRTP(track)
|
||||
}
|
||||
|
||||
c.Senders = append(c.Senders, sender)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/pion/ice/v4"
|
||||
"github.com/pion/sdp/v3"
|
||||
"github.com/pion/stun/v3"
|
||||
"github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
func UnmarshalMedias(descriptions []*sdp.MediaDescription) (medias []*core.Media) {
|
||||
// 1. Sort medias, so video will always be before audio
|
||||
// 2. Ignore application media from Hass default lovelace card
|
||||
// 3. Ignore media without direction (inactive media)
|
||||
// 4. Inverse media direction (because it is remote peer medias list)
|
||||
for _, kind := range []string{core.KindVideo, core.KindAudio} {
|
||||
for _, md := range descriptions {
|
||||
if md.MediaName.Media != kind {
|
||||
continue
|
||||
}
|
||||
|
||||
media := core.UnmarshalMedia(md)
|
||||
switch media.Direction {
|
||||
case core.DirectionSendRecv:
|
||||
media.Direction = core.DirectionRecvonly
|
||||
medias = append(medias, media)
|
||||
|
||||
media = media.Clone()
|
||||
media.Direction = core.DirectionSendonly
|
||||
|
||||
case core.DirectionRecvonly:
|
||||
media.Direction = core.DirectionSendonly
|
||||
|
||||
case core.DirectionSendonly:
|
||||
media.Direction = core.DirectionRecvonly
|
||||
|
||||
case "":
|
||||
continue
|
||||
}
|
||||
|
||||
// skip non-media codecs to avoid confusing users in info and logs
|
||||
media.Codecs = SkipNonMediaCodecs(media.Codecs)
|
||||
|
||||
medias = append(medias, media)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func SkipNonMediaCodecs(input []*core.Codec) (output []*core.Codec) {
|
||||
for _, codec := range input {
|
||||
switch codec.Name {
|
||||
case "RTX", "RED", "ULPFEC", "FLEXFEC-03":
|
||||
continue
|
||||
case "CN", "TELEPHONE-EVENT":
|
||||
continue // https://datatracker.ietf.org/doc/html/rfc7874
|
||||
}
|
||||
// VP8, VP9, H264, H265, AV1
|
||||
// OPUS, G722, PCMU, PCMA
|
||||
output = append(output, codec)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// WithResampling - will add for consumer: PCMA/0, PCMU/0, PCM/0, PCML/0
|
||||
// so it can add resampling for PCMA/PCMU and repack for PCM/PCML
|
||||
func WithResampling(medias []*core.Media) []*core.Media {
|
||||
for _, media := range medias {
|
||||
if media.Kind != core.KindAudio || media.Direction != core.DirectionSendonly {
|
||||
continue
|
||||
}
|
||||
|
||||
var pcma, pcmu, pcm, pcml *core.Codec
|
||||
|
||||
for _, codec := range media.Codecs {
|
||||
switch codec.Name {
|
||||
case core.CodecPCMA:
|
||||
if codec.ClockRate != 0 {
|
||||
pcma = codec
|
||||
} else {
|
||||
pcma = nil
|
||||
}
|
||||
case core.CodecPCMU:
|
||||
if codec.ClockRate != 0 {
|
||||
pcmu = codec
|
||||
} else {
|
||||
pcmu = nil
|
||||
}
|
||||
case core.CodecPCM:
|
||||
pcm = codec
|
||||
case core.CodecPCML:
|
||||
pcml = codec
|
||||
}
|
||||
}
|
||||
|
||||
if pcma != nil {
|
||||
pcma = pcma.Clone()
|
||||
pcma.ClockRate = 0 // reset clock rate so will match any
|
||||
media.Codecs = append(media.Codecs, pcma)
|
||||
}
|
||||
if pcmu != nil {
|
||||
pcmu = pcmu.Clone()
|
||||
pcmu.ClockRate = 0
|
||||
media.Codecs = append(media.Codecs, pcmu)
|
||||
}
|
||||
if pcma != nil && pcm == nil {
|
||||
pcm = pcma.Clone()
|
||||
pcm.Name = core.CodecPCM
|
||||
media.Codecs = append(media.Codecs, pcm)
|
||||
}
|
||||
if pcma != nil && pcml == nil {
|
||||
pcml = pcma.Clone()
|
||||
pcml.Name = core.CodecPCML
|
||||
media.Codecs = append(media.Codecs, pcml)
|
||||
}
|
||||
}
|
||||
|
||||
return medias
|
||||
}
|
||||
|
||||
func NewCandidate(network, address string) (string, error) {
|
||||
i := strings.LastIndexByte(address, ':')
|
||||
if i < 0 {
|
||||
return "", errors.New("wrong candidate: " + address)
|
||||
}
|
||||
host, port := address[:i], address[i+1:]
|
||||
|
||||
i, err := strconv.Atoi(port)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
config := &ice.CandidateHostConfig{
|
||||
Network: network,
|
||||
Address: host,
|
||||
Port: i,
|
||||
Component: ice.ComponentRTP,
|
||||
}
|
||||
|
||||
if network == "tcp" {
|
||||
config.TCPType = ice.TCPTypePassive
|
||||
}
|
||||
|
||||
cand, err := ice.NewCandidateHost(config)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return "candidate:" + cand.Marshal(), nil
|
||||
}
|
||||
|
||||
func LookupIP(address string) (string, error) {
|
||||
if strings.HasPrefix(address, "stun:") {
|
||||
ip, err := GetCachedPublicIP()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return ip.String() + address[4:], nil
|
||||
}
|
||||
|
||||
if IsIP(address) {
|
||||
return address, nil
|
||||
}
|
||||
|
||||
i := strings.IndexByte(address, ':')
|
||||
ips, err := net.LookupIP(address[:i])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(ips) == 0 {
|
||||
return "", fmt.Errorf("can't resolve: %s", address)
|
||||
}
|
||||
|
||||
return ips[0].String() + address[i:], nil
|
||||
}
|
||||
|
||||
// GetPublicIP example from https://github.com/pion/stun
|
||||
func GetPublicIP(address string) (net.IP, error) {
|
||||
conn, err := net.Dial("udp", address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c, err := stun.NewClient(conn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = conn.SetDeadline(time.Now().Add(time.Second * 3)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var res stun.Event
|
||||
|
||||
message := stun.MustBuild(stun.TransactionID, stun.BindingRequest)
|
||||
if err = c.Do(message, func(e stun.Event) { res = e }); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = c.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if res.Error != nil {
|
||||
return nil, res.Error
|
||||
}
|
||||
|
||||
var xorAddr stun.XORMappedAddress
|
||||
if err = xorAddr.GetFrom(res.Message); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return xorAddr.IP, nil
|
||||
}
|
||||
|
||||
var cachedIP net.IP
|
||||
var cachedTS time.Time
|
||||
|
||||
func GetCachedPublicIP(stuns ...string) (net.IP, error) {
|
||||
if now := time.Now(); now.After(cachedTS) {
|
||||
for _, addr := range stuns {
|
||||
if ip, _ := GetPublicIP(addr); ip != nil {
|
||||
cachedIP = ip
|
||||
cachedTS = now.Add(time.Minute * 5)
|
||||
return ip, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if cachedIP == nil {
|
||||
return nil, errors.New("webrtc: can't get public IP")
|
||||
}
|
||||
return cachedIP, nil
|
||||
}
|
||||
|
||||
func IsIP(host string) bool {
|
||||
for _, i := range host {
|
||||
if i >= 'A' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func MimeType(codec *core.Codec) string {
|
||||
switch codec.Name {
|
||||
case core.CodecH264:
|
||||
return webrtc.MimeTypeH264
|
||||
case core.CodecH265:
|
||||
return webrtc.MimeTypeH265
|
||||
case core.CodecVP8:
|
||||
return webrtc.MimeTypeVP8
|
||||
case core.CodecVP9:
|
||||
return webrtc.MimeTypeVP9
|
||||
case core.CodecAV1:
|
||||
return webrtc.MimeTypeAV1
|
||||
case core.CodecPCMU:
|
||||
return webrtc.MimeTypePCMU
|
||||
case core.CodecPCMA:
|
||||
return webrtc.MimeTypePCMA
|
||||
case core.CodecOpus:
|
||||
return webrtc.MimeTypeOpus
|
||||
case core.CodecG722:
|
||||
return webrtc.MimeTypeG722
|
||||
}
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func CandidateICE(network, host, port string, priority uint32) string {
|
||||
// 1. Foundation
|
||||
// 2. Component, always 1 because RTP
|
||||
// 3. "udp" or "tcp"
|
||||
// 4. Priority
|
||||
// 5. Host - IP4 or IP6 or domain name
|
||||
// 6. Port
|
||||
// 7. "typ host"
|
||||
foundation := crc32.ChecksumIEEE([]byte("host" + host + network + "4"))
|
||||
s := fmt.Sprintf("candidate:%d 1 %s %d %s %s typ host", foundation, network, priority, host, port)
|
||||
if network == "tcp" {
|
||||
return s + " tcptype passive"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Priority = type << 24 + local << 8 + component
|
||||
// https://www.rfc-editor.org/rfc/rfc8445#section-5.1.2.1
|
||||
|
||||
const PriorityHostUDP uint32 = 0x001F_FFFF |
|
||||
126<<24 | // udp host
|
||||
7<<21 // udp
|
||||
const PriorityHostTCPPassive uint32 = 0x001F_FFFF |
|
||||
99<<24 | // tcp host
|
||||
4<<21 // tcp passive
|
||||
|
||||
// CandidateHostPriority (lower indexes has a higher priority)
|
||||
func CandidateHostPriority(network string, index int) uint32 {
|
||||
switch network {
|
||||
case "udp":
|
||||
return PriorityHostUDP - uint32(index)
|
||||
case "tcp":
|
||||
return PriorityHostTCPPassive - uint32(index)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func UnmarshalICEServers(b []byte) ([]webrtc.ICEServer, error) {
|
||||
type ICEServer struct {
|
||||
URLs any `json:"urls"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Credential string `json:"credential,omitempty"`
|
||||
}
|
||||
|
||||
var src []ICEServer
|
||||
if err := json.Unmarshal(b, &src); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var dst []webrtc.ICEServer
|
||||
for i := range src {
|
||||
srv := webrtc.ICEServer{
|
||||
Username: src[i].Username,
|
||||
Credential: src[i].Credential,
|
||||
}
|
||||
|
||||
switch v := src[i].URLs.(type) {
|
||||
case []any:
|
||||
for _, u := range v {
|
||||
if s, ok := u.(string); ok {
|
||||
srv.URLs = append(srv.URLs, s)
|
||||
}
|
||||
}
|
||||
case string:
|
||||
srv.URLs = []string{v}
|
||||
}
|
||||
|
||||
dst = append(dst, srv)
|
||||
}
|
||||
|
||||
return dst, nil
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
func (c *Conn) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
|
||||
core.Assert(media.Direction == core.DirectionRecvonly)
|
||||
|
||||
for _, track := range c.Receivers {
|
||||
if track.Codec == codec {
|
||||
return track, nil
|
||||
}
|
||||
}
|
||||
|
||||
switch c.Mode {
|
||||
case core.ModePassiveConsumer: // backchannel from browser
|
||||
// set codec for consumer recv track so remote peer should send media with this codec
|
||||
params := webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: MimeType(codec),
|
||||
ClockRate: codec.ClockRate,
|
||||
Channels: uint16(codec.Channels),
|
||||
},
|
||||
PayloadType: 0, // don't know if this necessary
|
||||
}
|
||||
|
||||
tr := c.getTranseiver(media.ID)
|
||||
|
||||
_ = tr.SetCodecPreferences([]webrtc.RTPCodecParameters{params})
|
||||
|
||||
case core.ModePassiveProducer, core.ModeActiveProducer:
|
||||
// Passive producers: OBS Studio via WHIP or Browser
|
||||
// Active producers: go2rtc as WebRTC client or WebTorrent
|
||||
|
||||
default:
|
||||
panic(core.Caller())
|
||||
}
|
||||
|
||||
track := core.NewReceiver(media, codec)
|
||||
c.Receivers = append(c.Receivers, track)
|
||||
return track, nil
|
||||
}
|
||||
|
||||
func (c *Conn) Start() error {
|
||||
c.closed.Wait()
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/pion/sdp/v3"
|
||||
"github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
func (c *Conn) SetOffer(offer string) (err error) {
|
||||
c.offer = offer
|
||||
|
||||
sd := &sdp.SessionDescription{}
|
||||
if err = sd.Unmarshal([]byte(offer)); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// create transceivers with opposite direction
|
||||
for _, md := range sd.MediaDescriptions {
|
||||
var mid string
|
||||
var tr *webrtc.RTPTransceiver
|
||||
for _, attr := range md.Attributes {
|
||||
switch attr.Key {
|
||||
case core.DirectionSendRecv:
|
||||
tr, _ = c.pc.AddTransceiverFromTrack(NewTrack(md.MediaName.Media))
|
||||
case core.DirectionSendonly:
|
||||
tr, _ = c.pc.AddTransceiverFromKind(
|
||||
webrtc.NewRTPCodecType(md.MediaName.Media),
|
||||
webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionRecvonly},
|
||||
)
|
||||
case core.DirectionRecvonly:
|
||||
tr, _ = c.pc.AddTransceiverFromTrack(
|
||||
NewTrack(md.MediaName.Media),
|
||||
webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionSendonly},
|
||||
)
|
||||
case "mid":
|
||||
mid = attr.Value
|
||||
}
|
||||
}
|
||||
|
||||
if mid != "" && tr != nil {
|
||||
_ = tr.SetMid(mid)
|
||||
}
|
||||
}
|
||||
|
||||
c.Medias = UnmarshalMedias(sd.MediaDescriptions)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Conn) GetAnswer() (answer string, err error) {
|
||||
// we need to process remote offer after we create transeivers
|
||||
desc := webrtc.SessionDescription{Type: webrtc.SDPTypeOffer, SDP: c.offer}
|
||||
if err = c.pc.SetRemoteDescription(desc); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// disable transceivers if we don't have track, make direction=inactive
|
||||
transeivers:
|
||||
for _, tr := range c.pc.GetTransceivers() {
|
||||
for _, sender := range c.Senders {
|
||||
if sender.Media.ID == tr.Mid() {
|
||||
continue transeivers
|
||||
}
|
||||
}
|
||||
|
||||
switch tr.Direction() {
|
||||
case webrtc.RTPTransceiverDirectionSendrecv:
|
||||
_ = tr.Sender().Stop() // don't know if necessary
|
||||
_ = tr.SetSender(tr.Sender(), nil) // set direction to recvonly
|
||||
case webrtc.RTPTransceiverDirectionSendonly:
|
||||
_ = tr.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
if desc, err = c.pc.CreateAnswer(nil); err != nil {
|
||||
return
|
||||
}
|
||||
if err = c.pc.SetLocalDescription(desc); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return c.pc.LocalDescription().SDP, nil
|
||||
}
|
||||
|
||||
// GetCompleteAnswer - get SDP answer with candidates inside
|
||||
func (c *Conn) GetCompleteAnswer(candidates []string, filter func(*webrtc.ICECandidate) bool) (string, error) {
|
||||
var done = make(chan struct{})
|
||||
|
||||
c.pc.OnICECandidate(func(candidate *webrtc.ICECandidate) {
|
||||
if candidate != nil {
|
||||
if filter == nil || filter(candidate) {
|
||||
candidates = append(candidates, candidate.ToJSON().Candidate)
|
||||
}
|
||||
} else {
|
||||
done <- struct{}{}
|
||||
}
|
||||
})
|
||||
|
||||
answer, err := c.GetAnswer()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
<-done
|
||||
|
||||
sd := &sdp.SessionDescription{}
|
||||
if err = sd.Unmarshal([]byte(answer)); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
md := sd.MediaDescriptions[0]
|
||||
|
||||
for _, candidate := range candidates {
|
||||
md.WithPropertyAttribute(candidate)
|
||||
}
|
||||
|
||||
b, err := sd.Marshal()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(b), nil
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/pion/rtp"
|
||||
"github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
type Track struct {
|
||||
kind string
|
||||
id string
|
||||
streamID string
|
||||
sequence uint16
|
||||
ssrc uint32
|
||||
writer webrtc.TrackLocalWriter
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func NewTrack(kind string) *Track {
|
||||
return &Track{
|
||||
kind: kind,
|
||||
id: "go2rtc-" + kind,
|
||||
streamID: "go2rtc",
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Track) Bind(context webrtc.TrackLocalContext) (webrtc.RTPCodecParameters, error) {
|
||||
t.mu.Lock()
|
||||
t.ssrc = uint32(context.SSRC())
|
||||
t.writer = context.WriteStream()
|
||||
t.mu.Unlock()
|
||||
|
||||
for _, parameters := range context.CodecParameters() {
|
||||
// return first parameters
|
||||
return parameters, nil
|
||||
}
|
||||
|
||||
return webrtc.RTPCodecParameters{}, nil
|
||||
}
|
||||
|
||||
func (t *Track) Unbind(context webrtc.TrackLocalContext) error {
|
||||
t.mu.Lock()
|
||||
t.writer = nil
|
||||
t.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Track) ID() string {
|
||||
return t.id
|
||||
}
|
||||
|
||||
func (t *Track) RID() string {
|
||||
return "" // don't know what it is
|
||||
}
|
||||
|
||||
func (t *Track) StreamID() string {
|
||||
return t.streamID
|
||||
}
|
||||
|
||||
func (t *Track) Kind() webrtc.RTPCodecType {
|
||||
return webrtc.NewRTPCodecType(t.kind)
|
||||
}
|
||||
|
||||
func (t *Track) WriteRTP(payloadType uint8, packet *rtp.Packet) (err error) {
|
||||
// using mutex because Unbind https://github.com/AlexxIT/go2rtc/issues/994
|
||||
t.mu.Lock()
|
||||
|
||||
// in case when we start WriteRTP before Track.Bind
|
||||
if t.writer != nil {
|
||||
// important to have internal counter if input packets from different sources
|
||||
t.sequence++
|
||||
|
||||
header := packet.Header
|
||||
header.SSRC = t.ssrc
|
||||
header.PayloadType = payloadType
|
||||
header.SequenceNumber = t.sequence
|
||||
_, err = t.writer.WriteRTP(&header, packet.Payload)
|
||||
}
|
||||
|
||||
t.mu.Unlock()
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/pion/webrtc/v4"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAlexa(t *testing.T) {
|
||||
// from https://github.com/AlexxIT/go2rtc/issues/825
|
||||
offer := `v=0
|
||||
o=- 3911343731 3911343731 IN IP4 0.0.0.0
|
||||
s=a 2 z
|
||||
c=IN IP4 0.0.0.0
|
||||
t=0 0
|
||||
a=group:BUNDLE audio0 video0
|
||||
m=audio 1 UDP/TLS/RTP/SAVPF 96 0 8
|
||||
a=candidate:1 1 UDP 2013266431 52.90.193.210 60128 typ host
|
||||
a=candidate:2 1 TCP 1015021823 52.90.193.210 9 typ host tcptype active
|
||||
a=candidate:3 1 TCP 1010827519 52.90.193.210 45962 typ host tcptype passive
|
||||
a=candidate:1 2 UDP 2013266430 52.90.193.210 46109 typ host
|
||||
a=candidate:2 2 TCP 1015021822 52.90.193.210 9 typ host tcptype active
|
||||
a=candidate:3 2 TCP 1010827518 52.90.193.210 53795 typ host tcptype passive
|
||||
a=setup:actpass
|
||||
a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
|
||||
a=rtpmap:96 opus/48000/2
|
||||
a=rtpmap:0 PCMU/8000
|
||||
a=rtpmap:8 PCMA/8000
|
||||
a=rtcp:9 IN IP4 0.0.0.0
|
||||
a=rtcp-mux
|
||||
a=sendrecv
|
||||
a=mid:audio0
|
||||
a=ssrc:3573704076 cname:user3856789923@host-9dd1dd33
|
||||
a=ice-ufrag:gxfV
|
||||
a=ice-pwd:KepKrlQ1+LD+RGTAFaqVck
|
||||
a=fingerprint:sha-256 A2:93:53:50:E4:2F:C5:4E:DF:7C:70:99:5A:A7:39:50:1A:63:E5:B2:CA:73:70:7A:C5:F4:01:BF:BD:99:57:FC
|
||||
m=video 1 UDP/TLS/RTP/SAVPF 99
|
||||
a=candidate:1 1 UDP 2013266431 52.90.193.210 60128 typ host
|
||||
a=candidate:1 2 UDP 2013266430 52.90.193.210 46109 typ host
|
||||
a=candidate:2 1 TCP 1015021823 52.90.193.210 9 typ host tcptype active
|
||||
a=candidate:3 1 TCP 1010827519 52.90.193.210 45962 typ host tcptype passive
|
||||
a=candidate:3 2 TCP 1010827518 52.90.193.210 53795 typ host tcptype passive
|
||||
a=candidate:2 2 TCP 1015021822 52.90.193.210 9 typ host tcptype active
|
||||
b=AS:2500
|
||||
a=setup:actpass
|
||||
a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
|
||||
a=rtpmap:99 H264/90000
|
||||
a=rtcp:9 IN IP4 0.0.0.0
|
||||
a=rtcp-mux
|
||||
a=sendrecv
|
||||
a=mid:video0
|
||||
a=rtcp-fb:99 nack
|
||||
a=rtcp-fb:99 nack pli
|
||||
a=rtcp-fb:99 ccm fir
|
||||
a=ssrc:3778078295 cname:user3856789923@host-9dd1dd33
|
||||
a=ice-ufrag:gxfV
|
||||
a=ice-pwd:KepKrlQ1+LD+RGTAFaqVck
|
||||
a=fingerprint:sha-256 A2:93:53:50:E4:2F:C5:4E:DF:7C:70:99:5A:A7:39:50:1A:63:E5:B2:CA:73:70:7A:C5:F4:01:BF:BD:99:57:FC
|
||||
`
|
||||
|
||||
pc, err := webrtc.NewPeerConnection(webrtc.Configuration{})
|
||||
require.Nil(t, err)
|
||||
|
||||
conn := NewConn(pc)
|
||||
err = conn.SetOffer(offer)
|
||||
require.Nil(t, err)
|
||||
|
||||
_, err = conn.GetAnswer()
|
||||
require.Nil(t, err)
|
||||
}
|
||||
Reference in New Issue
Block a user