Initial commit

This commit is contained in:
Alexey Khit
2022-08-18 09:19:00 +03:00
commit 3e77835583
65 changed files with 6372 additions and 0 deletions
+104
View File
@@ -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
}
+195
View File
@@ -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 ""
}
+85
View File
@@ -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")
}
+125
View File
@@ -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)
}
+56
View File
@@ -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)
}