Initial commit
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"github.com/pion/interceptor"
|
||||
"github.com/pion/webrtc/v3"
|
||||
"net"
|
||||
)
|
||||
|
||||
func NewAPI(address string) (*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
|
||||
}
|
||||
|
||||
if address == "" {
|
||||
return webrtc.NewAPI(
|
||||
webrtc.WithMediaEngine(m),
|
||||
webrtc.WithInterceptorRegistry(i),
|
||||
), nil
|
||||
}
|
||||
|
||||
ln, err := net.Listen("tcp", address)
|
||||
if err != nil {
|
||||
return webrtc.NewAPI(
|
||||
webrtc.WithMediaEngine(m),
|
||||
webrtc.WithInterceptorRegistry(i),
|
||||
), err
|
||||
}
|
||||
|
||||
s := webrtc.SettingEngine{
|
||||
//LoggerFactory: customLoggerFactory{},
|
||||
}
|
||||
s.SetNetworkTypes([]webrtc.NetworkType{
|
||||
webrtc.NetworkTypeUDP4, webrtc.NetworkTypeUDP6,
|
||||
webrtc.NetworkTypeTCP4, webrtc.NetworkTypeTCP6,
|
||||
})
|
||||
|
||||
tcpMux := webrtc.NewICETCPMux(nil, ln, 8)
|
||||
s.SetICETCPMux(tcpMux)
|
||||
|
||||
return webrtc.NewAPI(
|
||||
webrtc.WithMediaEngine(m),
|
||||
webrtc.WithInterceptorRegistry(i),
|
||||
webrtc.WithSettingEngine(s),
|
||||
), nil
|
||||
}
|
||||
|
||||
func RegisterDefaultCodecs(m *webrtc.MediaEngine) error {
|
||||
for _, codec := range []webrtc.RTPCodecParameters{
|
||||
{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{webrtc.MimeTypeOpus, 48000, 2, "minptime=10;useinbandfec=1", nil},
|
||||
PayloadType: 101, //111,
|
||||
}, {
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{webrtc.MimeTypePCMU, 8000, 0, "", nil},
|
||||
PayloadType: 0,
|
||||
}, {
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{webrtc.MimeTypePCMA, 8000, 0, "", nil},
|
||||
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{
|
||||
// macOS Google Chrome 103.0.5060.134
|
||||
{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{webrtc.MimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f", videoRTCPFeedback},
|
||||
PayloadType: 96, //102,
|
||||
}, {
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{webrtc.MimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f", videoRTCPFeedback},
|
||||
PayloadType: 97, //125,
|
||||
}, {
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{webrtc.MimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640032", videoRTCPFeedback},
|
||||
PayloadType: 98, //123,
|
||||
},
|
||||
// macOS Safari 15.1
|
||||
{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{webrtc.MimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640c1f", videoRTCPFeedback},
|
||||
PayloadType: 99,
|
||||
},
|
||||
{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{webrtc.MimeTypeH265, 90000, 0, "", videoRTCPFeedback},
|
||||
PayloadType: 100,
|
||||
},
|
||||
} {
|
||||
if err := m.RegisterCodec(codec, webrtc.RTPCodecTypeVideo); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/pion/webrtc/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
MsgTypeOffer = "webrtc/offer"
|
||||
MsgTypeOfferComplete = "webrtc/offer-complete"
|
||||
MsgTypeAnswer = "webrtc/answer"
|
||||
MsgTypeCandidate = "webrtc/candidate"
|
||||
)
|
||||
|
||||
type Conn struct {
|
||||
streamer.Element
|
||||
|
||||
UserAgent string
|
||||
|
||||
Conn *webrtc.PeerConnection
|
||||
|
||||
medias []*streamer.Media
|
||||
tracks []*streamer.Track
|
||||
|
||||
receive int
|
||||
send int
|
||||
}
|
||||
|
||||
func (c *Conn) Init() {
|
||||
c.Conn.OnICECandidate(func(candidate *webrtc.ICECandidate) {
|
||||
if candidate != nil {
|
||||
c.Fire(&streamer.Message{
|
||||
Type: MsgTypeCandidate, Value: candidate.ToJSON().Candidate,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
c.Conn.OnTrack(func(remote *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {
|
||||
for _, track := range c.tracks {
|
||||
if track.Direction != streamer.DirectionRecvonly {
|
||||
continue
|
||||
}
|
||||
if track.Codec.PayloadType != uint8(remote.PayloadType()) {
|
||||
continue
|
||||
}
|
||||
|
||||
for {
|
||||
packet, _, err := remote.ReadRTP()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(packet.Payload) == 0 {
|
||||
continue
|
||||
}
|
||||
c.receive += len(packet.Payload)
|
||||
_ = track.WriteRTP(packet)
|
||||
}
|
||||
}
|
||||
|
||||
panic("something wrong")
|
||||
})
|
||||
|
||||
c.Conn.OnConnectionStateChange(func(state webrtc.PeerConnectionState) {
|
||||
c.Fire(state)
|
||||
|
||||
// TODO: remove
|
||||
switch state {
|
||||
case webrtc.PeerConnectionStateConnected:
|
||||
c.Fire(streamer.StatePlaying)
|
||||
case webrtc.PeerConnectionStateDisconnected:
|
||||
c.Fire(streamer.StateNull)
|
||||
case webrtc.PeerConnectionStateFailed:
|
||||
_ = c.Conn.Close()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Conn) ExchangeSDP(offer string, complete bool) (answer string, err error) {
|
||||
sdOffer := webrtc.SessionDescription{
|
||||
Type: webrtc.SDPTypeOffer, SDP: offer,
|
||||
}
|
||||
if err = c.Conn.SetRemoteDescription(sdOffer); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
//for _, tr := range c.Conn.GetTransceivers() {
|
||||
// switch tr.Direction() {
|
||||
// case webrtc.RTPTransceiverDirectionSendonly:
|
||||
// // disable transceivers if we don't have track
|
||||
// // make direction=inactive
|
||||
// // don't really necessary, but anyway
|
||||
// if tr.Sender() == nil {
|
||||
// if err = tr.Stop(); err != nil {
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
// case webrtc.RTPTransceiverDirectionRecvonly:
|
||||
// // TODO: change codecs list
|
||||
// caps := webrtc.RTPCodecCapability{
|
||||
// MimeType: webrtc.MimeTypePCMU,
|
||||
// ClockRate: 8000,
|
||||
// }
|
||||
// codecs := []webrtc.RTPCodecParameters{
|
||||
// {RTPCodecCapability: caps},
|
||||
// }
|
||||
// if err = tr.SetCodecPreferences(codecs); err != nil {
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
var sdAnswer webrtc.SessionDescription
|
||||
sdAnswer, err = c.Conn.CreateAnswer(nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
//var sd *sdp.SessionDescription
|
||||
//sd, err = sdAnswer.Unmarshal()
|
||||
//for _, media := range sd.MediaDescriptions {
|
||||
// if media.MediaName.Media != "audio" {
|
||||
// continue
|
||||
// }
|
||||
// for i, attr := range media.Attributes {
|
||||
// if attr.Key == "sendonly" {
|
||||
// attr.Key = "inactive"
|
||||
// media.Attributes[i] = attr
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
//var b []byte
|
||||
//b, err = sd.Marshal()
|
||||
//sdAnswer.SDP = string(b)
|
||||
|
||||
if err = c.Conn.SetLocalDescription(sdAnswer); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if complete {
|
||||
<-webrtc.GatheringCompletePromise(c.Conn)
|
||||
return c.Conn.LocalDescription().SDP, nil
|
||||
}
|
||||
|
||||
return sdAnswer.SDP, nil
|
||||
}
|
||||
|
||||
func (c *Conn) SetOffer(offer string) (err error) {
|
||||
sdOffer := webrtc.SessionDescription{
|
||||
Type: webrtc.SDPTypeOffer, SDP: offer,
|
||||
}
|
||||
if err = c.Conn.SetRemoteDescription(sdOffer); err != nil {
|
||||
return
|
||||
}
|
||||
rawSDP := []byte(c.Conn.RemoteDescription().SDP)
|
||||
c.medias, err = streamer.UnmarshalSDP(rawSDP)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Conn) GetAnswer() (answer string, err error) {
|
||||
for _, tr := range c.Conn.GetTransceivers() {
|
||||
if tr.Direction() != webrtc.RTPTransceiverDirectionSendonly {
|
||||
continue
|
||||
}
|
||||
|
||||
// disable transceivers if we don't have track
|
||||
// make direction=inactive
|
||||
// don't really necessary, but anyway
|
||||
if tr.Sender() == nil {
|
||||
if err = tr.Stop(); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var sdAnswer webrtc.SessionDescription
|
||||
sdAnswer, err = c.Conn.CreateAnswer(nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = c.Conn.SetLocalDescription(sdAnswer); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return sdAnswer.SDP, nil
|
||||
}
|
||||
|
||||
func (c *Conn) remote() string {
|
||||
for _, trans := range c.Conn.GetTransceivers() {
|
||||
pair, _ := trans.Receiver().Transport().ICETransport().GetSelectedCandidatePair()
|
||||
return pair.Remote.String()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/pion/ice/v2"
|
||||
"github.com/pion/stun"
|
||||
"github.com/pion/webrtc/v3"
|
||||
"net"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func NewCandidate(address string) (string, error) {
|
||||
host, port, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
i, err := strconv.Atoi(port)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
cand, err := ice.NewCandidateHost(&ice.CandidateHostConfig{
|
||||
Network: "tcp",
|
||||
Address: host,
|
||||
Port: i,
|
||||
Component: ice.ComponentRTP,
|
||||
TCPType: ice.TCPTypePassive,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return "candidate:" + cand.Marshal(), nil
|
||||
}
|
||||
|
||||
// GetPublicIP example from https://github.com/pion/stun
|
||||
func GetPublicIP() (net.IP, error) {
|
||||
c, err := stun.Dial("udp", "stun.l.google.com:19302")
|
||||
if 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 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
|
||||
}
|
||||
|
||||
func MimeType(codec *streamer.Codec) string {
|
||||
switch codec.Name {
|
||||
case streamer.CodecH264:
|
||||
return webrtc.MimeTypeH264
|
||||
case streamer.CodecH265:
|
||||
return webrtc.MimeTypeH265
|
||||
case streamer.CodecVP8:
|
||||
return webrtc.MimeTypeVP8
|
||||
case streamer.CodecVP9:
|
||||
return webrtc.MimeTypeVP9
|
||||
case streamer.CodecAV1:
|
||||
return webrtc.MimeTypeAV1
|
||||
case streamer.CodecPCMU:
|
||||
return webrtc.MimeTypePCMU
|
||||
case streamer.CodecPCMA:
|
||||
return webrtc.MimeTypePCMA
|
||||
case streamer.CodecOpus:
|
||||
return webrtc.MimeTypeOpus
|
||||
case streamer.CodecG722:
|
||||
return webrtc.MimeTypeG722
|
||||
}
|
||||
panic("not implemented")
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/pion/rtp"
|
||||
"github.com/pion/webrtc/v3"
|
||||
)
|
||||
|
||||
// Consumer
|
||||
|
||||
func (c *Conn) GetMedias() []*streamer.Media {
|
||||
return c.medias
|
||||
}
|
||||
|
||||
func (c *Conn) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
|
||||
switch track.Direction {
|
||||
// send our track to WebRTC consumer
|
||||
case streamer.DirectionSendonly:
|
||||
codec := track.Codec
|
||||
|
||||
// webrtc.codecParametersFuzzySearch
|
||||
caps := webrtc.RTPCodecCapability{
|
||||
MimeType: MimeType(codec),
|
||||
Channels: codec.Channels,
|
||||
ClockRate: codec.ClockRate,
|
||||
}
|
||||
|
||||
if codec.Name == streamer.CodecH264 {
|
||||
// don't know if this really neccessary
|
||||
// I have tested multiple browsers and H264 profile has no effect on anything
|
||||
caps.SDPFmtpLine = "packetization-mode=1;profile-level-id=42e01f"
|
||||
}
|
||||
|
||||
// important to use same streamID so JS will automatically
|
||||
// join two tracks as one source/stream
|
||||
trackLocal, err := webrtc.NewTrackLocalStaticRTP(
|
||||
caps, caps.MimeType[:5], "go2rtc",
|
||||
)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err = c.Conn.AddTrack(trackLocal); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
push := func(packet *rtp.Packet) error {
|
||||
c.send += packet.MarshalSize()
|
||||
return trackLocal.WriteRTP(packet)
|
||||
}
|
||||
|
||||
if codec.Name == streamer.CodecH264 {
|
||||
wrapper := h264.RTPPay(1200)
|
||||
push = wrapper(push)
|
||||
|
||||
if codec.PayloadType != 255 {
|
||||
wrapper = h264.RTPDepay(track)
|
||||
push = wrapper(push)
|
||||
}
|
||||
}
|
||||
|
||||
track = track.Bind(push)
|
||||
c.tracks = append(c.tracks, track)
|
||||
return track
|
||||
|
||||
// receive track from WebRTC consumer (microphone, backchannel, two way audio)
|
||||
case streamer.DirectionRecvonly:
|
||||
for _, tr := range c.Conn.GetTransceivers() {
|
||||
if tr.Mid() != media.MID {
|
||||
continue
|
||||
}
|
||||
|
||||
codec := track.Codec
|
||||
caps := webrtc.RTPCodecCapability{
|
||||
MimeType: MimeType(codec),
|
||||
ClockRate: codec.ClockRate,
|
||||
Channels: codec.Channels,
|
||||
}
|
||||
codecs := []webrtc.RTPCodecParameters{
|
||||
{RTPCodecCapability: caps},
|
||||
}
|
||||
if err := tr.SetCodecPreferences(codecs); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
c.tracks = append(c.tracks, track)
|
||||
return track
|
||||
}
|
||||
}
|
||||
|
||||
panic("wrong direction")
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
func (c *Conn) Push(msg interface{}) {
|
||||
if msg := msg.(*streamer.Message); msg != nil {
|
||||
if msg.Type == MsgTypeCandidate {
|
||||
_ = c.Conn.AddICECandidate(webrtc.ICECandidateInit{
|
||||
Candidate: msg.Value.(string),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) MarshalJSON() ([]byte, error) {
|
||||
v := map[string]interface{}{
|
||||
streamer.JSONType: "WebRTC server consumer",
|
||||
streamer.JSONRemoteAddr: c.remote(),
|
||||
}
|
||||
|
||||
if c.receive > 0 {
|
||||
v[streamer.JSONReceive] = c.receive
|
||||
}
|
||||
if c.send > 0 {
|
||||
v[streamer.JSONSend] = c.send
|
||||
}
|
||||
if c.UserAgent != "" {
|
||||
v[streamer.JSONUserAgent] = c.UserAgent
|
||||
}
|
||||
|
||||
return json.Marshal(v)
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"github.com/pion/ice/v2"
|
||||
"github.com/pion/sdp/v3"
|
||||
"github.com/pion/webrtc/v3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestName(t *testing.T) {
|
||||
i, _ := ice.NewCandidateHost(&ice.CandidateHostConfig{
|
||||
Network: "tcp",
|
||||
Address: "192.168.1.123",
|
||||
Port: 8555,
|
||||
Component: ice.ComponentRTP,
|
||||
TCPType: ice.TCPTypePassive,
|
||||
})
|
||||
a := i.Marshal()
|
||||
println(a)
|
||||
}
|
||||
|
||||
func TestPublicIP(t *testing.T) {
|
||||
ip, err := GetPublicIP()
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, ip)
|
||||
t.Logf("your public IP: %s", ip.String())
|
||||
}
|
||||
|
||||
func TestMedia(t *testing.T) {
|
||||
codec := webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: webrtc.MimeTypeH264,
|
||||
ClockRate: 90000,
|
||||
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f",
|
||||
},
|
||||
PayloadType: 96,
|
||||
}
|
||||
|
||||
md := &sdp.MediaDescription{
|
||||
MediaName: sdp.MediaName{
|
||||
Media: "video", Protos: []string{"RTP", "AVP"},
|
||||
},
|
||||
}
|
||||
md.WithCodec(
|
||||
uint8(codec.PayloadType), codec.MimeType[6:], codec.ClockRate,
|
||||
codec.Channels, codec.SDPFmtpLine,
|
||||
)
|
||||
|
||||
sd := &sdp.SessionDescription{
|
||||
MediaDescriptions: []*sdp.MediaDescription{md},
|
||||
}
|
||||
data, err := sd.Marshal()
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, data)
|
||||
}
|
||||
Reference in New Issue
Block a user