27 changed files with 1650 additions and 1464 deletions
@ -1,177 +0,0 @@ |
|||||||
package core |
|
||||||
|
|
||||||
import ( |
|
||||||
"fmt" |
|
||||||
"strings" |
|
||||||
"time" |
|
||||||
|
|
||||||
"github.com/bluenviron/gortsplib/v4/pkg/description" |
|
||||||
"github.com/bluenviron/gortsplib/v4/pkg/format" |
|
||||||
"github.com/bluenviron/gortsplib/v4/pkg/liberrors" |
|
||||||
"github.com/bluenviron/gortsplib/v4/pkg/rtplossdetector" |
|
||||||
"github.com/bluenviron/gortsplib/v4/pkg/rtptime" |
|
||||||
"github.com/pion/rtcp" |
|
||||||
"github.com/pion/rtp" |
|
||||||
"github.com/pion/webrtc/v3" |
|
||||||
|
|
||||||
"github.com/bluenviron/mediamtx/internal/logger" |
|
||||||
"github.com/bluenviron/mediamtx/internal/stream" |
|
||||||
) |
|
||||||
|
|
||||||
const ( |
|
||||||
keyFrameInterval = 2 * time.Second |
|
||||||
) |
|
||||||
|
|
||||||
type webRTCIncomingTrack struct { |
|
||||||
track *webrtc.TrackRemote |
|
||||||
receiver *webrtc.RTPReceiver |
|
||||||
writeRTCP func([]rtcp.Packet) error |
|
||||||
|
|
||||||
mediaType description.MediaType |
|
||||||
format format.Format |
|
||||||
media *description.Media |
|
||||||
} |
|
||||||
|
|
||||||
func newWebRTCIncomingTrack( |
|
||||||
track *webrtc.TrackRemote, |
|
||||||
receiver *webrtc.RTPReceiver, |
|
||||||
writeRTCP func([]rtcp.Packet) error, |
|
||||||
) (*webRTCIncomingTrack, error) { |
|
||||||
t := &webRTCIncomingTrack{ |
|
||||||
track: track, |
|
||||||
receiver: receiver, |
|
||||||
writeRTCP: writeRTCP, |
|
||||||
} |
|
||||||
|
|
||||||
switch strings.ToLower(track.Codec().MimeType) { |
|
||||||
case strings.ToLower(webrtc.MimeTypeAV1): |
|
||||||
t.mediaType = description.MediaTypeVideo |
|
||||||
t.format = &format.AV1{ |
|
||||||
PayloadTyp: uint8(track.PayloadType()), |
|
||||||
} |
|
||||||
|
|
||||||
case strings.ToLower(webrtc.MimeTypeVP9): |
|
||||||
t.mediaType = description.MediaTypeVideo |
|
||||||
t.format = &format.VP9{ |
|
||||||
PayloadTyp: uint8(track.PayloadType()), |
|
||||||
} |
|
||||||
|
|
||||||
case strings.ToLower(webrtc.MimeTypeVP8): |
|
||||||
t.mediaType = description.MediaTypeVideo |
|
||||||
t.format = &format.VP8{ |
|
||||||
PayloadTyp: uint8(track.PayloadType()), |
|
||||||
} |
|
||||||
|
|
||||||
case strings.ToLower(webrtc.MimeTypeH264): |
|
||||||
t.mediaType = description.MediaTypeVideo |
|
||||||
t.format = &format.H264{ |
|
||||||
PayloadTyp: uint8(track.PayloadType()), |
|
||||||
PacketizationMode: 1, |
|
||||||
} |
|
||||||
|
|
||||||
case strings.ToLower(webrtc.MimeTypeOpus): |
|
||||||
t.mediaType = description.MediaTypeAudio |
|
||||||
t.format = &format.Opus{ |
|
||||||
PayloadTyp: uint8(track.PayloadType()), |
|
||||||
} |
|
||||||
|
|
||||||
case strings.ToLower(webrtc.MimeTypeG722): |
|
||||||
t.mediaType = description.MediaTypeAudio |
|
||||||
t.format = &format.G722{} |
|
||||||
|
|
||||||
case strings.ToLower(webrtc.MimeTypePCMU): |
|
||||||
t.mediaType = description.MediaTypeAudio |
|
||||||
t.format = &format.G711{ |
|
||||||
MULaw: true, |
|
||||||
} |
|
||||||
|
|
||||||
case strings.ToLower(webrtc.MimeTypePCMA): |
|
||||||
t.mediaType = description.MediaTypeAudio |
|
||||||
t.format = &format.G711{ |
|
||||||
MULaw: false, |
|
||||||
} |
|
||||||
|
|
||||||
default: |
|
||||||
return nil, fmt.Errorf("unsupported codec: %v", track.Codec()) |
|
||||||
} |
|
||||||
|
|
||||||
t.media = &description.Media{ |
|
||||||
Type: t.mediaType, |
|
||||||
Formats: []format.Format{t.format}, |
|
||||||
} |
|
||||||
|
|
||||||
return t, nil |
|
||||||
} |
|
||||||
|
|
||||||
type webrtcTrackWrapper struct { |
|
||||||
clockRate int |
|
||||||
} |
|
||||||
|
|
||||||
func (w webrtcTrackWrapper) ClockRate() int { |
|
||||||
return w.clockRate |
|
||||||
} |
|
||||||
|
|
||||||
func (webrtcTrackWrapper) PTSEqualsDTS(*rtp.Packet) bool { |
|
||||||
return true |
|
||||||
} |
|
||||||
|
|
||||||
func (t *webRTCIncomingTrack) start(stream *stream.Stream, timeDecoder *rtptime.GlobalDecoder, log logger.Writer) { |
|
||||||
lossDetector := rtplossdetector.New() |
|
||||||
trackWrapper := &webrtcTrackWrapper{clockRate: int(t.track.Codec().ClockRate)} |
|
||||||
|
|
||||||
go func() { |
|
||||||
for { |
|
||||||
pkt, _, err := t.track.ReadRTP() |
|
||||||
if err != nil { |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
lost := lossDetector.Process(pkt) |
|
||||||
if lost != 0 { |
|
||||||
log.Log(logger.Warn, (liberrors.ErrClientRTPPacketsLost{Lost: lost}).Error()) |
|
||||||
// do not return
|
|
||||||
} |
|
||||||
|
|
||||||
// sometimes Chrome sends empty RTP packets. ignore them.
|
|
||||||
if len(pkt.Payload) == 0 { |
|
||||||
continue |
|
||||||
} |
|
||||||
|
|
||||||
pts, ok := timeDecoder.Decode(trackWrapper, pkt) |
|
||||||
if !ok { |
|
||||||
continue |
|
||||||
} |
|
||||||
|
|
||||||
stream.WriteRTPPacket(t.media, t.format, pkt, time.Now(), pts) |
|
||||||
} |
|
||||||
}() |
|
||||||
|
|
||||||
// read incoming RTCP packets to make interceptors work
|
|
||||||
go func() { |
|
||||||
buf := make([]byte, 1500) |
|
||||||
for { |
|
||||||
_, _, err := t.receiver.Read(buf) |
|
||||||
if err != nil { |
|
||||||
return |
|
||||||
} |
|
||||||
} |
|
||||||
}() |
|
||||||
|
|
||||||
if t.mediaType == description.MediaTypeVideo { |
|
||||||
go func() { |
|
||||||
keyframeTicker := time.NewTicker(keyFrameInterval) |
|
||||||
defer keyframeTicker.Stop() |
|
||||||
|
|
||||||
for range keyframeTicker.C { |
|
||||||
err := t.writeRTCP([]rtcp.Packet{ |
|
||||||
&rtcp.PictureLossIndication{ |
|
||||||
MediaSSRC: uint32(t.track.SSRC()), |
|
||||||
}, |
|
||||||
}) |
|
||||||
if err != nil { |
|
||||||
return |
|
||||||
} |
|
||||||
} |
|
||||||
}() |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,364 +0,0 @@ |
|||||||
package core |
|
||||||
|
|
||||||
import ( |
|
||||||
"fmt" |
|
||||||
"time" |
|
||||||
|
|
||||||
"github.com/bluenviron/gortsplib/v4/pkg/description" |
|
||||||
"github.com/bluenviron/gortsplib/v4/pkg/format" |
|
||||||
"github.com/bluenviron/gortsplib/v4/pkg/format/rtpav1" |
|
||||||
"github.com/bluenviron/gortsplib/v4/pkg/format/rtph264" |
|
||||||
"github.com/bluenviron/gortsplib/v4/pkg/format/rtpvp8" |
|
||||||
"github.com/bluenviron/gortsplib/v4/pkg/format/rtpvp9" |
|
||||||
"github.com/pion/webrtc/v3" |
|
||||||
|
|
||||||
"github.com/bluenviron/mediamtx/internal/asyncwriter" |
|
||||||
"github.com/bluenviron/mediamtx/internal/stream" |
|
||||||
"github.com/bluenviron/mediamtx/internal/unit" |
|
||||||
) |
|
||||||
|
|
||||||
type webRTCOutgoingTrack struct { |
|
||||||
sender *webrtc.RTPSender |
|
||||||
media *description.Media |
|
||||||
format format.Format |
|
||||||
track *webrtc.TrackLocalStaticRTP |
|
||||||
cb func(unit.Unit) error |
|
||||||
} |
|
||||||
|
|
||||||
func newWebRTCOutgoingTrackVideo(desc *description.Session) (*webRTCOutgoingTrack, error) { |
|
||||||
var av1Format *format.AV1 |
|
||||||
videoMedia := desc.FindFormat(&av1Format) |
|
||||||
|
|
||||||
if videoMedia != nil { |
|
||||||
webRTCTrak, err := webrtc.NewTrackLocalStaticRTP( |
|
||||||
webrtc.RTPCodecCapability{ |
|
||||||
MimeType: webrtc.MimeTypeAV1, |
|
||||||
ClockRate: 90000, |
|
||||||
}, |
|
||||||
"av1", |
|
||||||
webrtcStreamID, |
|
||||||
) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
encoder := &rtpav1.Encoder{ |
|
||||||
PayloadType: 105, |
|
||||||
PayloadMaxSize: webrtcPayloadMaxSize, |
|
||||||
} |
|
||||||
err = encoder.Init() |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
return &webRTCOutgoingTrack{ |
|
||||||
media: videoMedia, |
|
||||||
format: av1Format, |
|
||||||
track: webRTCTrak, |
|
||||||
cb: func(u unit.Unit) error { |
|
||||||
tunit := u.(*unit.AV1) |
|
||||||
|
|
||||||
if tunit.TU == nil { |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
packets, err := encoder.Encode(tunit.TU) |
|
||||||
if err != nil { |
|
||||||
return nil //nolint:nilerr
|
|
||||||
} |
|
||||||
|
|
||||||
for _, pkt := range packets { |
|
||||||
pkt.Timestamp += tunit.RTPPackets[0].Timestamp |
|
||||||
webRTCTrak.WriteRTP(pkt) //nolint:errcheck
|
|
||||||
} |
|
||||||
|
|
||||||
return nil |
|
||||||
}, |
|
||||||
}, nil |
|
||||||
} |
|
||||||
|
|
||||||
var vp9Format *format.VP9 |
|
||||||
videoMedia = desc.FindFormat(&vp9Format) |
|
||||||
|
|
||||||
if videoMedia != nil { //nolint:dupl
|
|
||||||
webRTCTrak, err := webrtc.NewTrackLocalStaticRTP( |
|
||||||
webrtc.RTPCodecCapability{ |
|
||||||
MimeType: webrtc.MimeTypeVP9, |
|
||||||
ClockRate: uint32(vp9Format.ClockRate()), |
|
||||||
}, |
|
||||||
"vp9", |
|
||||||
webrtcStreamID, |
|
||||||
) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
encoder := &rtpvp9.Encoder{ |
|
||||||
PayloadType: 96, |
|
||||||
PayloadMaxSize: webrtcPayloadMaxSize, |
|
||||||
} |
|
||||||
err = encoder.Init() |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
return &webRTCOutgoingTrack{ |
|
||||||
media: videoMedia, |
|
||||||
format: vp9Format, |
|
||||||
track: webRTCTrak, |
|
||||||
cb: func(u unit.Unit) error { |
|
||||||
tunit := u.(*unit.VP9) |
|
||||||
|
|
||||||
if tunit.Frame == nil { |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
packets, err := encoder.Encode(tunit.Frame) |
|
||||||
if err != nil { |
|
||||||
return nil //nolint:nilerr
|
|
||||||
} |
|
||||||
|
|
||||||
for _, pkt := range packets { |
|
||||||
pkt.Timestamp += tunit.RTPPackets[0].Timestamp |
|
||||||
webRTCTrak.WriteRTP(pkt) //nolint:errcheck
|
|
||||||
} |
|
||||||
|
|
||||||
return nil |
|
||||||
}, |
|
||||||
}, nil |
|
||||||
} |
|
||||||
|
|
||||||
var vp8Format *format.VP8 |
|
||||||
videoMedia = desc.FindFormat(&vp8Format) |
|
||||||
|
|
||||||
if videoMedia != nil { //nolint:dupl
|
|
||||||
webRTCTrak, err := webrtc.NewTrackLocalStaticRTP( |
|
||||||
webrtc.RTPCodecCapability{ |
|
||||||
MimeType: webrtc.MimeTypeVP8, |
|
||||||
ClockRate: uint32(vp8Format.ClockRate()), |
|
||||||
}, |
|
||||||
"vp8", |
|
||||||
webrtcStreamID, |
|
||||||
) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
encoder := &rtpvp8.Encoder{ |
|
||||||
PayloadType: 96, |
|
||||||
PayloadMaxSize: webrtcPayloadMaxSize, |
|
||||||
} |
|
||||||
err = encoder.Init() |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
return &webRTCOutgoingTrack{ |
|
||||||
media: videoMedia, |
|
||||||
format: vp8Format, |
|
||||||
track: webRTCTrak, |
|
||||||
cb: func(u unit.Unit) error { |
|
||||||
tunit := u.(*unit.VP8) |
|
||||||
|
|
||||||
if tunit.Frame == nil { |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
packets, err := encoder.Encode(tunit.Frame) |
|
||||||
if err != nil { |
|
||||||
return nil //nolint:nilerr
|
|
||||||
} |
|
||||||
|
|
||||||
for _, pkt := range packets { |
|
||||||
pkt.Timestamp += tunit.RTPPackets[0].Timestamp |
|
||||||
webRTCTrak.WriteRTP(pkt) //nolint:errcheck
|
|
||||||
} |
|
||||||
|
|
||||||
return nil |
|
||||||
}, |
|
||||||
}, nil |
|
||||||
} |
|
||||||
|
|
||||||
var h264Format *format.H264 |
|
||||||
videoMedia = desc.FindFormat(&h264Format) |
|
||||||
|
|
||||||
if videoMedia != nil { |
|
||||||
webRTCTrak, err := webrtc.NewTrackLocalStaticRTP( |
|
||||||
webrtc.RTPCodecCapability{ |
|
||||||
MimeType: webrtc.MimeTypeH264, |
|
||||||
ClockRate: uint32(h264Format.ClockRate()), |
|
||||||
}, |
|
||||||
"h264", |
|
||||||
webrtcStreamID, |
|
||||||
) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
encoder := &rtph264.Encoder{ |
|
||||||
PayloadType: 96, |
|
||||||
PayloadMaxSize: webrtcPayloadMaxSize, |
|
||||||
} |
|
||||||
err = encoder.Init() |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
firstReceived := false |
|
||||||
var lastPTS time.Duration |
|
||||||
|
|
||||||
return &webRTCOutgoingTrack{ |
|
||||||
media: videoMedia, |
|
||||||
format: h264Format, |
|
||||||
track: webRTCTrak, |
|
||||||
cb: func(u unit.Unit) error { |
|
||||||
tunit := u.(*unit.H264) |
|
||||||
|
|
||||||
if tunit.AU == nil { |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
if !firstReceived { |
|
||||||
firstReceived = true |
|
||||||
} else if tunit.PTS < lastPTS { |
|
||||||
return fmt.Errorf("WebRTC doesn't support H264 streams with B-frames") |
|
||||||
} |
|
||||||
lastPTS = tunit.PTS |
|
||||||
|
|
||||||
packets, err := encoder.Encode(tunit.AU) |
|
||||||
if err != nil { |
|
||||||
return nil //nolint:nilerr
|
|
||||||
} |
|
||||||
|
|
||||||
for _, pkt := range packets { |
|
||||||
pkt.Timestamp += tunit.RTPPackets[0].Timestamp |
|
||||||
webRTCTrak.WriteRTP(pkt) //nolint:errcheck
|
|
||||||
} |
|
||||||
|
|
||||||
return nil |
|
||||||
}, |
|
||||||
}, nil |
|
||||||
} |
|
||||||
|
|
||||||
return nil, nil |
|
||||||
} |
|
||||||
|
|
||||||
func newWebRTCOutgoingTrackAudio(desc *description.Session) (*webRTCOutgoingTrack, error) { |
|
||||||
var opusFormat *format.Opus |
|
||||||
audioMedia := desc.FindFormat(&opusFormat) |
|
||||||
|
|
||||||
if audioMedia != nil { |
|
||||||
webRTCTrak, err := webrtc.NewTrackLocalStaticRTP( |
|
||||||
webrtc.RTPCodecCapability{ |
|
||||||
MimeType: webrtc.MimeTypeOpus, |
|
||||||
ClockRate: uint32(opusFormat.ClockRate()), |
|
||||||
Channels: 2, |
|
||||||
}, |
|
||||||
"opus", |
|
||||||
webrtcStreamID, |
|
||||||
) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
return &webRTCOutgoingTrack{ |
|
||||||
media: audioMedia, |
|
||||||
format: opusFormat, |
|
||||||
track: webRTCTrak, |
|
||||||
cb: func(u unit.Unit) error { |
|
||||||
for _, pkt := range u.GetRTPPackets() { |
|
||||||
webRTCTrak.WriteRTP(pkt) //nolint:errcheck
|
|
||||||
} |
|
||||||
|
|
||||||
return nil |
|
||||||
}, |
|
||||||
}, nil |
|
||||||
} |
|
||||||
|
|
||||||
var g722Format *format.G722 |
|
||||||
audioMedia = desc.FindFormat(&g722Format) |
|
||||||
|
|
||||||
if audioMedia != nil { |
|
||||||
webRTCTrak, err := webrtc.NewTrackLocalStaticRTP( |
|
||||||
webrtc.RTPCodecCapability{ |
|
||||||
MimeType: webrtc.MimeTypeG722, |
|
||||||
ClockRate: uint32(g722Format.ClockRate()), |
|
||||||
}, |
|
||||||
"g722", |
|
||||||
webrtcStreamID, |
|
||||||
) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
return &webRTCOutgoingTrack{ |
|
||||||
media: audioMedia, |
|
||||||
format: g722Format, |
|
||||||
track: webRTCTrak, |
|
||||||
cb: func(u unit.Unit) error { |
|
||||||
for _, pkt := range u.GetRTPPackets() { |
|
||||||
webRTCTrak.WriteRTP(pkt) //nolint:errcheck
|
|
||||||
} |
|
||||||
|
|
||||||
return nil |
|
||||||
}, |
|
||||||
}, nil |
|
||||||
} |
|
||||||
|
|
||||||
var g711Format *format.G711 |
|
||||||
audioMedia = desc.FindFormat(&g711Format) |
|
||||||
|
|
||||||
if audioMedia != nil { |
|
||||||
var mtyp string |
|
||||||
if g711Format.MULaw { |
|
||||||
mtyp = webrtc.MimeTypePCMU |
|
||||||
} else { |
|
||||||
mtyp = webrtc.MimeTypePCMA |
|
||||||
} |
|
||||||
|
|
||||||
webRTCTrak, err := webrtc.NewTrackLocalStaticRTP( |
|
||||||
webrtc.RTPCodecCapability{ |
|
||||||
MimeType: mtyp, |
|
||||||
ClockRate: uint32(g711Format.ClockRate()), |
|
||||||
}, |
|
||||||
"g711", |
|
||||||
webrtcStreamID, |
|
||||||
) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
return &webRTCOutgoingTrack{ |
|
||||||
media: audioMedia, |
|
||||||
format: g711Format, |
|
||||||
track: webRTCTrak, |
|
||||||
cb: func(u unit.Unit) error { |
|
||||||
for _, pkt := range u.GetRTPPackets() { |
|
||||||
webRTCTrak.WriteRTP(pkt) //nolint:errcheck
|
|
||||||
} |
|
||||||
|
|
||||||
return nil |
|
||||||
}, |
|
||||||
}, nil |
|
||||||
} |
|
||||||
|
|
||||||
return nil, nil |
|
||||||
} |
|
||||||
|
|
||||||
func (t *webRTCOutgoingTrack) start( |
|
||||||
stream *stream.Stream, |
|
||||||
writer *asyncwriter.Writer, |
|
||||||
) { |
|
||||||
// read incoming RTCP packets to make interceptors work
|
|
||||||
go func() { |
|
||||||
buf := make([]byte, 1500) |
|
||||||
for { |
|
||||||
_, _, err := t.sender.Read(buf) |
|
||||||
if err != nil { |
|
||||||
return |
|
||||||
} |
|
||||||
} |
|
||||||
}() |
|
||||||
|
|
||||||
stream.AddReader(writer, t.media, t.format, t.cb) |
|
||||||
} |
|
||||||
@ -0,0 +1,156 @@ |
|||||||
|
package webrtc |
||||||
|
|
||||||
|
import ( |
||||||
|
"github.com/pion/ice/v2" |
||||||
|
"github.com/pion/interceptor" |
||||||
|
"github.com/pion/webrtc/v3" |
||||||
|
) |
||||||
|
|
||||||
|
func stringInSlice(a string, list []string) bool { |
||||||
|
for _, b := range list { |
||||||
|
if b == a { |
||||||
|
return true |
||||||
|
} |
||||||
|
} |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
var videoCodecs = []webrtc.RTPCodecParameters{ |
||||||
|
{ |
||||||
|
RTPCodecCapability: webrtc.RTPCodecCapability{ |
||||||
|
MimeType: webrtc.MimeTypeAV1, |
||||||
|
ClockRate: 90000, |
||||||
|
}, |
||||||
|
PayloadType: 96, |
||||||
|
}, |
||||||
|
{ |
||||||
|
RTPCodecCapability: webrtc.RTPCodecCapability{ |
||||||
|
MimeType: webrtc.MimeTypeVP9, |
||||||
|
ClockRate: 90000, |
||||||
|
SDPFmtpLine: "profile-id=0", |
||||||
|
}, |
||||||
|
PayloadType: 97, |
||||||
|
}, |
||||||
|
{ |
||||||
|
RTPCodecCapability: webrtc.RTPCodecCapability{ |
||||||
|
MimeType: webrtc.MimeTypeVP9, |
||||||
|
ClockRate: 90000, |
||||||
|
SDPFmtpLine: "profile-id=1", |
||||||
|
}, |
||||||
|
PayloadType: 98, |
||||||
|
}, |
||||||
|
{ |
||||||
|
RTPCodecCapability: webrtc.RTPCodecCapability{ |
||||||
|
MimeType: webrtc.MimeTypeVP8, |
||||||
|
ClockRate: 90000, |
||||||
|
}, |
||||||
|
PayloadType: 99, |
||||||
|
}, |
||||||
|
{ |
||||||
|
RTPCodecCapability: webrtc.RTPCodecCapability{ |
||||||
|
MimeType: webrtc.MimeTypeH264, |
||||||
|
ClockRate: 90000, |
||||||
|
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f", |
||||||
|
}, |
||||||
|
PayloadType: 100, |
||||||
|
}, |
||||||
|
{ |
||||||
|
RTPCodecCapability: webrtc.RTPCodecCapability{ |
||||||
|
MimeType: webrtc.MimeTypeH264, |
||||||
|
ClockRate: 90000, |
||||||
|
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f", |
||||||
|
}, |
||||||
|
PayloadType: 101, |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
var audioCodecs = []webrtc.RTPCodecParameters{ |
||||||
|
{ |
||||||
|
RTPCodecCapability: webrtc.RTPCodecCapability{ |
||||||
|
MimeType: webrtc.MimeTypeOpus, |
||||||
|
ClockRate: 48000, |
||||||
|
Channels: 2, |
||||||
|
SDPFmtpLine: "minptime=10;useinbandfec=1;stereo=1;sprop-stereo=1", |
||||||
|
}, |
||||||
|
PayloadType: 111, |
||||||
|
}, |
||||||
|
{ |
||||||
|
RTPCodecCapability: webrtc.RTPCodecCapability{ |
||||||
|
MimeType: webrtc.MimeTypeG722, |
||||||
|
ClockRate: 8000, |
||||||
|
}, |
||||||
|
PayloadType: 9, |
||||||
|
}, |
||||||
|
{ |
||||||
|
RTPCodecCapability: webrtc.RTPCodecCapability{ |
||||||
|
MimeType: webrtc.MimeTypePCMU, |
||||||
|
ClockRate: 8000, |
||||||
|
}, |
||||||
|
PayloadType: 0, |
||||||
|
}, |
||||||
|
{ |
||||||
|
RTPCodecCapability: webrtc.RTPCodecCapability{ |
||||||
|
MimeType: webrtc.MimeTypePCMA, |
||||||
|
ClockRate: 8000, |
||||||
|
}, |
||||||
|
PayloadType: 8, |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
// APIConf is the configuration passed to NewAPI().
|
||||||
|
type APIConf struct { |
||||||
|
ICEInterfaces []string |
||||||
|
ICEHostNAT1To1IPs []string |
||||||
|
ICEUDPMux ice.UDPMux |
||||||
|
ICETCPMux ice.TCPMux |
||||||
|
} |
||||||
|
|
||||||
|
// NewAPI allocates a webrtc API.
|
||||||
|
func NewAPI(conf APIConf) (*webrtc.API, error) { |
||||||
|
settingsEngine := webrtc.SettingEngine{} |
||||||
|
|
||||||
|
if len(conf.ICEInterfaces) != 0 { |
||||||
|
settingsEngine.SetInterfaceFilter(func(iface string) bool { |
||||||
|
return stringInSlice(iface, conf.ICEInterfaces) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
if len(conf.ICEHostNAT1To1IPs) != 0 { |
||||||
|
settingsEngine.SetNAT1To1IPs(conf.ICEHostNAT1To1IPs, webrtc.ICECandidateTypeHost) |
||||||
|
} |
||||||
|
|
||||||
|
if conf.ICEUDPMux != nil { |
||||||
|
settingsEngine.SetICEUDPMux(conf.ICEUDPMux) |
||||||
|
} |
||||||
|
|
||||||
|
if conf.ICETCPMux != nil { |
||||||
|
settingsEngine.SetICETCPMux(conf.ICETCPMux) |
||||||
|
settingsEngine.SetNetworkTypes([]webrtc.NetworkType{webrtc.NetworkTypeTCP4}) |
||||||
|
} |
||||||
|
|
||||||
|
mediaEngine := &webrtc.MediaEngine{} |
||||||
|
|
||||||
|
for _, codec := range videoCodecs { |
||||||
|
err := mediaEngine.RegisterCodec(codec, webrtc.RTPCodecTypeVideo) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
for _, codec := range audioCodecs { |
||||||
|
err := mediaEngine.RegisterCodec(codec, webrtc.RTPCodecTypeAudio) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
interceptorRegistry := &interceptor.Registry{} |
||||||
|
if err := webrtc.RegisterDefaultInterceptors(mediaEngine, interceptorRegistry); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
return webrtc.NewAPI( |
||||||
|
webrtc.WithSettingEngine(settingsEngine), |
||||||
|
webrtc.WithMediaEngine(mediaEngine), |
||||||
|
webrtc.WithInterceptorRegistry(interceptorRegistry)), nil |
||||||
|
} |
||||||
@ -1,4 +1,4 @@ |
|||||||
package whip |
package webrtc |
||||||
|
|
||||||
import ( |
import ( |
||||||
"fmt" |
"fmt" |
||||||
@ -1,4 +1,4 @@ |
|||||||
package whip |
package webrtc |
||||||
|
|
||||||
import ( |
import ( |
||||||
"testing" |
"testing" |
||||||
@ -0,0 +1,152 @@ |
|||||||
|
package webrtc |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"strings" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/bluenviron/gortsplib/v4/pkg/format" |
||||||
|
"github.com/bluenviron/gortsplib/v4/pkg/liberrors" |
||||||
|
"github.com/bluenviron/gortsplib/v4/pkg/rtplossdetector" |
||||||
|
"github.com/pion/rtcp" |
||||||
|
"github.com/pion/rtp" |
||||||
|
"github.com/pion/webrtc/v3" |
||||||
|
|
||||||
|
"github.com/bluenviron/mediamtx/internal/logger" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
keyFrameInterval = 2 * time.Second |
||||||
|
) |
||||||
|
|
||||||
|
// IncomingTrack is an incoming track.
|
||||||
|
type IncomingTrack struct { |
||||||
|
track *webrtc.TrackRemote |
||||||
|
log logger.Writer |
||||||
|
|
||||||
|
format format.Format |
||||||
|
lossDetector *rtplossdetector.LossDetector |
||||||
|
} |
||||||
|
|
||||||
|
func newIncomingTrack( |
||||||
|
track *webrtc.TrackRemote, |
||||||
|
receiver *webrtc.RTPReceiver, |
||||||
|
writeRTCP func([]rtcp.Packet) error, |
||||||
|
log logger.Writer, |
||||||
|
) (*IncomingTrack, error) { |
||||||
|
t := &IncomingTrack{ |
||||||
|
track: track, |
||||||
|
log: log, |
||||||
|
lossDetector: rtplossdetector.New(), |
||||||
|
} |
||||||
|
|
||||||
|
isVideo := false |
||||||
|
|
||||||
|
switch strings.ToLower(track.Codec().MimeType) { |
||||||
|
case strings.ToLower(webrtc.MimeTypeAV1): |
||||||
|
isVideo = true |
||||||
|
t.format = &format.AV1{ |
||||||
|
PayloadTyp: uint8(track.PayloadType()), |
||||||
|
} |
||||||
|
|
||||||
|
case strings.ToLower(webrtc.MimeTypeVP9): |
||||||
|
isVideo = true |
||||||
|
t.format = &format.VP9{ |
||||||
|
PayloadTyp: uint8(track.PayloadType()), |
||||||
|
} |
||||||
|
|
||||||
|
case strings.ToLower(webrtc.MimeTypeVP8): |
||||||
|
isVideo = true |
||||||
|
t.format = &format.VP8{ |
||||||
|
PayloadTyp: uint8(track.PayloadType()), |
||||||
|
} |
||||||
|
|
||||||
|
case strings.ToLower(webrtc.MimeTypeH264): |
||||||
|
isVideo = true |
||||||
|
t.format = &format.H264{ |
||||||
|
PayloadTyp: uint8(track.PayloadType()), |
||||||
|
PacketizationMode: 1, |
||||||
|
} |
||||||
|
|
||||||
|
case strings.ToLower(webrtc.MimeTypeOpus): |
||||||
|
t.format = &format.Opus{ |
||||||
|
PayloadTyp: uint8(track.PayloadType()), |
||||||
|
} |
||||||
|
|
||||||
|
case strings.ToLower(webrtc.MimeTypeG722): |
||||||
|
t.format = &format.G722{} |
||||||
|
|
||||||
|
case strings.ToLower(webrtc.MimeTypePCMU): |
||||||
|
t.format = &format.G711{ |
||||||
|
MULaw: true, |
||||||
|
} |
||||||
|
|
||||||
|
case strings.ToLower(webrtc.MimeTypePCMA): |
||||||
|
t.format = &format.G711{ |
||||||
|
MULaw: false, |
||||||
|
} |
||||||
|
|
||||||
|
default: |
||||||
|
return nil, fmt.Errorf("unsupported codec: %v", track.Codec()) |
||||||
|
} |
||||||
|
|
||||||
|
// read incoming RTCP packets to make interceptors work
|
||||||
|
go func() { |
||||||
|
buf := make([]byte, 1500) |
||||||
|
for { |
||||||
|
_, _, err := receiver.Read(buf) |
||||||
|
if err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
}() |
||||||
|
|
||||||
|
// send period key frame requests
|
||||||
|
if isVideo { |
||||||
|
go func() { |
||||||
|
keyframeTicker := time.NewTicker(keyFrameInterval) |
||||||
|
defer keyframeTicker.Stop() |
||||||
|
|
||||||
|
for range keyframeTicker.C { |
||||||
|
err := writeRTCP([]rtcp.Packet{ |
||||||
|
&rtcp.PictureLossIndication{ |
||||||
|
MediaSSRC: uint32(t.track.SSRC()), |
||||||
|
}, |
||||||
|
}) |
||||||
|
if err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
}() |
||||||
|
} |
||||||
|
|
||||||
|
return t, nil |
||||||
|
} |
||||||
|
|
||||||
|
// Format returns the track format.
|
||||||
|
func (t *IncomingTrack) Format() format.Format { |
||||||
|
return t.format |
||||||
|
} |
||||||
|
|
||||||
|
// ReadRTP reads a RTP packet.
|
||||||
|
func (t *IncomingTrack) ReadRTP() (*rtp.Packet, error) { |
||||||
|
for { |
||||||
|
pkt, _, err := t.track.ReadRTP() |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
lost := t.lossDetector.Process(pkt) |
||||||
|
if lost != 0 { |
||||||
|
t.log.Log(logger.Warn, (liberrors.ErrClientRTPPacketsLost{Lost: lost}).Error()) |
||||||
|
// do not return
|
||||||
|
} |
||||||
|
|
||||||
|
// sometimes Chrome sends empty RTP packets. ignore them.
|
||||||
|
if len(pkt.Payload) == 0 { |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
return pkt, nil |
||||||
|
} |
||||||
|
} |
||||||
@ -1,4 +1,4 @@ |
|||||||
package whip |
package webrtc |
||||||
|
|
||||||
import ( |
import ( |
||||||
"encoding/json" |
"encoding/json" |
||||||
@ -1,4 +1,4 @@ |
|||||||
package whip |
package webrtc |
||||||
|
|
||||||
import ( |
import ( |
||||||
"testing" |
"testing" |
||||||
@ -0,0 +1,154 @@ |
|||||||
|
package webrtc |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
|
||||||
|
"github.com/bluenviron/gortsplib/v4/pkg/format" |
||||||
|
"github.com/pion/rtp" |
||||||
|
"github.com/pion/webrtc/v3" |
||||||
|
) |
||||||
|
|
||||||
|
type addTrackFunc func(webrtc.TrackLocal) (*webrtc.RTPSender, error) |
||||||
|
|
||||||
|
// OutgoingTrack is a WebRTC outgoing track
|
||||||
|
type OutgoingTrack struct { |
||||||
|
track *webrtc.TrackLocalStaticRTP |
||||||
|
} |
||||||
|
|
||||||
|
func newOutgoingTrack(forma format.Format, addTrack addTrackFunc) (*OutgoingTrack, error) { |
||||||
|
t := &OutgoingTrack{} |
||||||
|
|
||||||
|
switch forma := forma.(type) { |
||||||
|
case *format.AV1: |
||||||
|
var err error |
||||||
|
t.track, err = webrtc.NewTrackLocalStaticRTP( |
||||||
|
webrtc.RTPCodecCapability{ |
||||||
|
MimeType: webrtc.MimeTypeAV1, |
||||||
|
ClockRate: 90000, |
||||||
|
}, |
||||||
|
"av1", |
||||||
|
webrtcStreamID, |
||||||
|
) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
case *format.VP9: |
||||||
|
var err error |
||||||
|
t.track, err = webrtc.NewTrackLocalStaticRTP( |
||||||
|
webrtc.RTPCodecCapability{ |
||||||
|
MimeType: webrtc.MimeTypeVP9, |
||||||
|
ClockRate: uint32(forma.ClockRate()), |
||||||
|
}, |
||||||
|
"vp9", |
||||||
|
webrtcStreamID, |
||||||
|
) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
case *format.VP8: |
||||||
|
var err error |
||||||
|
t.track, err = webrtc.NewTrackLocalStaticRTP( |
||||||
|
webrtc.RTPCodecCapability{ |
||||||
|
MimeType: webrtc.MimeTypeVP8, |
||||||
|
ClockRate: uint32(forma.ClockRate()), |
||||||
|
}, |
||||||
|
"vp8", |
||||||
|
webrtcStreamID, |
||||||
|
) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
case *format.H264: |
||||||
|
var err error |
||||||
|
t.track, err = webrtc.NewTrackLocalStaticRTP( |
||||||
|
webrtc.RTPCodecCapability{ |
||||||
|
MimeType: webrtc.MimeTypeH264, |
||||||
|
ClockRate: uint32(forma.ClockRate()), |
||||||
|
}, |
||||||
|
"h264", |
||||||
|
webrtcStreamID, |
||||||
|
) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
case *format.Opus: |
||||||
|
var err error |
||||||
|
t.track, err = webrtc.NewTrackLocalStaticRTP( |
||||||
|
webrtc.RTPCodecCapability{ |
||||||
|
MimeType: webrtc.MimeTypeOpus, |
||||||
|
ClockRate: uint32(forma.ClockRate()), |
||||||
|
Channels: 2, |
||||||
|
}, |
||||||
|
"opus", |
||||||
|
webrtcStreamID, |
||||||
|
) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
case *format.G722: |
||||||
|
var err error |
||||||
|
t.track, err = webrtc.NewTrackLocalStaticRTP( |
||||||
|
webrtc.RTPCodecCapability{ |
||||||
|
MimeType: webrtc.MimeTypeG722, |
||||||
|
ClockRate: uint32(forma.ClockRate()), |
||||||
|
}, |
||||||
|
"g722", |
||||||
|
webrtcStreamID, |
||||||
|
) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
case *format.G711: |
||||||
|
var mtyp string |
||||||
|
if forma.MULaw { |
||||||
|
mtyp = webrtc.MimeTypePCMU |
||||||
|
} else { |
||||||
|
mtyp = webrtc.MimeTypePCMA |
||||||
|
} |
||||||
|
|
||||||
|
var err error |
||||||
|
t.track, err = webrtc.NewTrackLocalStaticRTP( |
||||||
|
webrtc.RTPCodecCapability{ |
||||||
|
MimeType: mtyp, |
||||||
|
ClockRate: uint32(forma.ClockRate()), |
||||||
|
}, |
||||||
|
"g711", |
||||||
|
webrtcStreamID, |
||||||
|
) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
default: |
||||||
|
return nil, fmt.Errorf("unsupported track type: %T", forma) |
||||||
|
} |
||||||
|
|
||||||
|
sender, err := addTrack(t.track) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
// read incoming RTCP packets to make interceptors work
|
||||||
|
go func() { |
||||||
|
buf := make([]byte, 1500) |
||||||
|
for { |
||||||
|
_, _, err := sender.Read(buf) |
||||||
|
if err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
}() |
||||||
|
|
||||||
|
return t, nil |
||||||
|
} |
||||||
|
|
||||||
|
// WriteRTP writes a RTP packet.
|
||||||
|
func (t *OutgoingTrack) WriteRTP(pkt *rtp.Packet) error { |
||||||
|
return t.track.WriteRTP(pkt) |
||||||
|
} |
||||||
@ -0,0 +1,381 @@ |
|||||||
|
package webrtc |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"fmt" |
||||||
|
"strconv" |
||||||
|
"sync" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/bluenviron/gortsplib/v4/pkg/format" |
||||||
|
"github.com/pion/webrtc/v3" |
||||||
|
|
||||||
|
"github.com/bluenviron/mediamtx/internal/logger" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
webrtcHandshakeTimeout = 10 * time.Second |
||||||
|
webrtcTrackGatherTimeout = 2 * time.Second |
||||||
|
webrtcStreamID = "mediamtx" |
||||||
|
) |
||||||
|
|
||||||
|
type nilLogger struct{} |
||||||
|
|
||||||
|
func (nilLogger) Log(_ logger.Level, _ string, _ ...interface{}) { |
||||||
|
} |
||||||
|
|
||||||
|
type trackRecvPair struct { |
||||||
|
track *webrtc.TrackRemote |
||||||
|
receiver *webrtc.RTPReceiver |
||||||
|
} |
||||||
|
|
||||||
|
// PeerConnection is a wrapper around webrtc.PeerConnection.
|
||||||
|
type PeerConnection struct { |
||||||
|
ICEServers []webrtc.ICEServer |
||||||
|
API *webrtc.API |
||||||
|
Publish bool |
||||||
|
Log logger.Writer |
||||||
|
|
||||||
|
wr *webrtc.PeerConnection |
||||||
|
stateChangeMutex sync.Mutex |
||||||
|
newLocalCandidate chan *webrtc.ICECandidateInit |
||||||
|
connected chan struct{} |
||||||
|
disconnected chan struct{} |
||||||
|
closed chan struct{} |
||||||
|
gatheringDone chan struct{} |
||||||
|
incomingTrack chan trackRecvPair |
||||||
|
} |
||||||
|
|
||||||
|
// Start starts the peer connection.
|
||||||
|
func (co *PeerConnection) Start() error { |
||||||
|
if co.Log == nil { |
||||||
|
co.Log = &nilLogger{} |
||||||
|
} |
||||||
|
|
||||||
|
configuration := webrtc.Configuration{ |
||||||
|
ICEServers: co.ICEServers, |
||||||
|
} |
||||||
|
|
||||||
|
var err error |
||||||
|
co.wr, err = co.API.NewPeerConnection(configuration) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
co.newLocalCandidate = make(chan *webrtc.ICECandidateInit) |
||||||
|
co.connected = make(chan struct{}) |
||||||
|
co.disconnected = make(chan struct{}) |
||||||
|
co.closed = make(chan struct{}) |
||||||
|
co.gatheringDone = make(chan struct{}) |
||||||
|
co.incomingTrack = make(chan trackRecvPair) |
||||||
|
|
||||||
|
if !co.Publish { |
||||||
|
_, err = co.wr.AddTransceiverFromKind(webrtc.RTPCodecTypeVideo, webrtc.RtpTransceiverInit{ |
||||||
|
Direction: webrtc.RTPTransceiverDirectionRecvonly, |
||||||
|
}) |
||||||
|
if err != nil { |
||||||
|
co.wr.Close() //nolint:errcheck
|
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
_, err = co.wr.AddTransceiverFromKind(webrtc.RTPCodecTypeAudio, webrtc.RtpTransceiverInit{ |
||||||
|
Direction: webrtc.RTPTransceiverDirectionRecvonly, |
||||||
|
}) |
||||||
|
if err != nil { |
||||||
|
co.wr.Close() //nolint:errcheck
|
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
co.wr.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) { |
||||||
|
select { |
||||||
|
case co.incomingTrack <- trackRecvPair{track, receiver}: |
||||||
|
case <-co.closed: |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
co.wr.OnConnectionStateChange(func(state webrtc.PeerConnectionState) { |
||||||
|
co.stateChangeMutex.Lock() |
||||||
|
defer co.stateChangeMutex.Unlock() |
||||||
|
|
||||||
|
select { |
||||||
|
case <-co.closed: |
||||||
|
return |
||||||
|
default: |
||||||
|
} |
||||||
|
|
||||||
|
co.Log.Log(logger.Debug, "peer connection state: "+state.String()) |
||||||
|
|
||||||
|
switch state { |
||||||
|
case webrtc.PeerConnectionStateConnected: |
||||||
|
co.Log.Log(logger.Info, "peer connection established, local candidate: %v, remote candidate: %v", |
||||||
|
co.LocalCandidate(), co.RemoteCandidate()) |
||||||
|
|
||||||
|
close(co.connected) |
||||||
|
|
||||||
|
case webrtc.PeerConnectionStateDisconnected: |
||||||
|
close(co.disconnected) |
||||||
|
|
||||||
|
case webrtc.PeerConnectionStateClosed: |
||||||
|
close(co.closed) |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
co.wr.OnICECandidate(func(i *webrtc.ICECandidate) { |
||||||
|
if i != nil { |
||||||
|
v := i.ToJSON() |
||||||
|
select { |
||||||
|
case co.newLocalCandidate <- &v: |
||||||
|
case <-co.connected: |
||||||
|
case <-co.closed: |
||||||
|
} |
||||||
|
} else { |
||||||
|
close(co.gatheringDone) |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Close closes the connection.
|
||||||
|
func (co *PeerConnection) Close() { |
||||||
|
co.wr.Close() //nolint:errcheck
|
||||||
|
<-co.closed |
||||||
|
} |
||||||
|
|
||||||
|
// CreatePartialOffer creates a partial offer.
|
||||||
|
func (co *PeerConnection) CreatePartialOffer() (*webrtc.SessionDescription, error) { |
||||||
|
offer, err := co.wr.CreateOffer(nil) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
err = co.wr.SetLocalDescription(offer) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
return &offer, nil |
||||||
|
} |
||||||
|
|
||||||
|
// SetAnswer sets the answer.
|
||||||
|
func (co *PeerConnection) SetAnswer(answer *webrtc.SessionDescription) error { |
||||||
|
return co.wr.SetRemoteDescription(*answer) |
||||||
|
} |
||||||
|
|
||||||
|
// AddRemoteCandidate adds a remote candidate.
|
||||||
|
func (co *PeerConnection) AddRemoteCandidate(candidate webrtc.ICECandidateInit) error { |
||||||
|
return co.wr.AddICECandidate(candidate) |
||||||
|
} |
||||||
|
|
||||||
|
// CreateFullAnswer creates a full answer.
|
||||||
|
func (co *PeerConnection) CreateFullAnswer( |
||||||
|
ctx context.Context, |
||||||
|
offer *webrtc.SessionDescription, |
||||||
|
) (*webrtc.SessionDescription, error) { |
||||||
|
err := co.wr.SetRemoteDescription(*offer) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
answer, err := co.wr.CreateAnswer(nil) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
err = co.wr.SetLocalDescription(answer) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
err = co.WaitGatheringDone(ctx) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
return co.wr.LocalDescription(), nil |
||||||
|
} |
||||||
|
|
||||||
|
// WaitGatheringDone waits until candidate gathering is complete.
|
||||||
|
func (co *PeerConnection) WaitGatheringDone(ctx context.Context) error { |
||||||
|
for { |
||||||
|
select { |
||||||
|
case <-co.NewLocalCandidate(): |
||||||
|
case <-co.GatheringDone(): |
||||||
|
return nil |
||||||
|
case <-ctx.Done(): |
||||||
|
return fmt.Errorf("terminated") |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// WaitUntilConnected waits until connection is established.
|
||||||
|
func (co *PeerConnection) WaitUntilConnected( |
||||||
|
ctx context.Context, |
||||||
|
) error { |
||||||
|
t := time.NewTimer(webrtcHandshakeTimeout) |
||||||
|
defer t.Stop() |
||||||
|
|
||||||
|
outer: |
||||||
|
for { |
||||||
|
select { |
||||||
|
case <-t.C: |
||||||
|
return fmt.Errorf("deadline exceeded while waiting connection") |
||||||
|
|
||||||
|
case <-co.connected: |
||||||
|
break outer |
||||||
|
|
||||||
|
case <-ctx.Done(): |
||||||
|
return fmt.Errorf("terminated") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// GatherIncomingTracks gathers incoming tracks.
|
||||||
|
func (co *PeerConnection) GatherIncomingTracks( |
||||||
|
ctx context.Context, |
||||||
|
count int, |
||||||
|
) ([]*IncomingTrack, error) { |
||||||
|
var tracks []*IncomingTrack |
||||||
|
|
||||||
|
t := time.NewTimer(webrtcTrackGatherTimeout) |
||||||
|
defer t.Stop() |
||||||
|
|
||||||
|
for { |
||||||
|
select { |
||||||
|
case <-t.C: |
||||||
|
if count == 0 { |
||||||
|
return tracks, nil |
||||||
|
} |
||||||
|
return nil, fmt.Errorf("deadline exceeded while waiting tracks") |
||||||
|
|
||||||
|
case pair := <-co.incomingTrack: |
||||||
|
track, err := newIncomingTrack(pair.track, pair.receiver, co.wr.WriteRTCP, co.Log) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
tracks = append(tracks, track) |
||||||
|
|
||||||
|
if len(tracks) == count || len(tracks) >= 2 { |
||||||
|
return tracks, nil |
||||||
|
} |
||||||
|
|
||||||
|
case <-co.Disconnected(): |
||||||
|
return nil, fmt.Errorf("peer connection closed") |
||||||
|
|
||||||
|
case <-ctx.Done(): |
||||||
|
return nil, fmt.Errorf("terminated") |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// SetupOutgoingTracks setups outgoing tracks.
|
||||||
|
func (co *PeerConnection) SetupOutgoingTracks( |
||||||
|
videoTrack format.Format, |
||||||
|
audioTrack format.Format, |
||||||
|
) ([]*OutgoingTrack, error) { |
||||||
|
var tracks []*OutgoingTrack |
||||||
|
|
||||||
|
for _, forma := range []format.Format{videoTrack, audioTrack} { |
||||||
|
if forma != nil { |
||||||
|
track, err := newOutgoingTrack(forma, co.wr.AddTrack) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
tracks = append(tracks, track) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return tracks, nil |
||||||
|
} |
||||||
|
|
||||||
|
// Connected returns when connected.
|
||||||
|
func (co *PeerConnection) Connected() <-chan struct{} { |
||||||
|
return co.connected |
||||||
|
} |
||||||
|
|
||||||
|
// Disconnected returns when disconnected.
|
||||||
|
func (co *PeerConnection) Disconnected() <-chan struct{} { |
||||||
|
return co.disconnected |
||||||
|
} |
||||||
|
|
||||||
|
// NewLocalCandidate returns when there's a new local candidate.
|
||||||
|
func (co *PeerConnection) NewLocalCandidate() <-chan *webrtc.ICECandidateInit { |
||||||
|
return co.newLocalCandidate |
||||||
|
} |
||||||
|
|
||||||
|
// GatheringDone returns when candidate gathering is complete.
|
||||||
|
func (co *PeerConnection) GatheringDone() <-chan struct{} { |
||||||
|
return co.gatheringDone |
||||||
|
} |
||||||
|
|
||||||
|
// LocalCandidate returns the local candidate.
|
||||||
|
func (co *PeerConnection) LocalCandidate() string { |
||||||
|
var cid string |
||||||
|
for _, stats := range co.wr.GetStats() { |
||||||
|
if tstats, ok := stats.(webrtc.ICECandidatePairStats); ok && tstats.Nominated { |
||||||
|
cid = tstats.LocalCandidateID |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if cid != "" { |
||||||
|
for _, stats := range co.wr.GetStats() { |
||||||
|
if tstats, ok := stats.(webrtc.ICECandidateStats); ok && tstats.ID == cid { |
||||||
|
return tstats.CandidateType.String() + "/" + tstats.Protocol + "/" + |
||||||
|
tstats.IP + "/" + strconv.FormatInt(int64(tstats.Port), 10) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return "" |
||||||
|
} |
||||||
|
|
||||||
|
// RemoteCandidate returns the remote candidate.
|
||||||
|
func (co *PeerConnection) RemoteCandidate() string { |
||||||
|
var cid string |
||||||
|
for _, stats := range co.wr.GetStats() { |
||||||
|
if tstats, ok := stats.(webrtc.ICECandidatePairStats); ok && tstats.Nominated { |
||||||
|
cid = tstats.RemoteCandidateID |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if cid != "" { |
||||||
|
for _, stats := range co.wr.GetStats() { |
||||||
|
if tstats, ok := stats.(webrtc.ICECandidateStats); ok && tstats.ID == cid { |
||||||
|
return tstats.CandidateType.String() + "/" + tstats.Protocol + "/" + |
||||||
|
tstats.IP + "/" + strconv.FormatInt(int64(tstats.Port), 10) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return "" |
||||||
|
} |
||||||
|
|
||||||
|
// BytesReceived returns received bytes.
|
||||||
|
func (co *PeerConnection) BytesReceived() uint64 { |
||||||
|
for _, stats := range co.wr.GetStats() { |
||||||
|
if tstats, ok := stats.(webrtc.TransportStats); ok { |
||||||
|
if tstats.ID == "iceTransport" { |
||||||
|
return tstats.BytesReceived |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return 0 |
||||||
|
} |
||||||
|
|
||||||
|
// BytesSent returns sent bytes.
|
||||||
|
func (co *PeerConnection) BytesSent() uint64 { |
||||||
|
for _, stats := range co.wr.GetStats() { |
||||||
|
if tstats, ok := stats.(webrtc.TransportStats); ok { |
||||||
|
if tstats.ID == "iceTransport" { |
||||||
|
return tstats.BytesSent |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return 0 |
||||||
|
} |
||||||
@ -0,0 +1,37 @@ |
|||||||
|
package webrtc |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
|
||||||
|
"github.com/pion/sdp/v3" |
||||||
|
) |
||||||
|
|
||||||
|
// TrackCount returns the track count.
|
||||||
|
func TrackCount(medias []*sdp.MediaDescription) (int, error) { |
||||||
|
videoTrack := false |
||||||
|
audioTrack := false |
||||||
|
trackCount := 0 |
||||||
|
|
||||||
|
for _, media := range medias { |
||||||
|
switch media.MediaName.Media { |
||||||
|
case "video": |
||||||
|
if videoTrack { |
||||||
|
return 0, fmt.Errorf("only a single video and a single audio track are supported") |
||||||
|
} |
||||||
|
videoTrack = true |
||||||
|
|
||||||
|
case "audio": |
||||||
|
if audioTrack { |
||||||
|
return 0, fmt.Errorf("only a single video and a single audio track are supported") |
||||||
|
} |
||||||
|
audioTrack = true |
||||||
|
|
||||||
|
default: |
||||||
|
return 0, fmt.Errorf("unsupported media '%s'", media.MediaName.Media) |
||||||
|
} |
||||||
|
|
||||||
|
trackCount++ |
||||||
|
} |
||||||
|
|
||||||
|
return trackCount, nil |
||||||
|
} |
||||||
@ -0,0 +1,2 @@ |
|||||||
|
// Package webrtc contains WebRTC utilities.
|
||||||
|
package webrtc |
||||||
@ -0,0 +1,213 @@ |
|||||||
|
package webrtc |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"fmt" |
||||||
|
"net/http" |
||||||
|
"net/url" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/bluenviron/gortsplib/v4/pkg/format" |
||||||
|
"github.com/pion/sdp/v3" |
||||||
|
|
||||||
|
"github.com/bluenviron/mediamtx/internal/logger" |
||||||
|
) |
||||||
|
|
||||||
|
// WHIPClient is a WHIP client.
|
||||||
|
type WHIPClient struct { |
||||||
|
HTTPClient *http.Client |
||||||
|
URL *url.URL |
||||||
|
Log logger.Writer |
||||||
|
|
||||||
|
pc *PeerConnection |
||||||
|
} |
||||||
|
|
||||||
|
// Publish publishes tracks.
|
||||||
|
func (c *WHIPClient) Publish( |
||||||
|
ctx context.Context, |
||||||
|
videoTrack format.Format, |
||||||
|
audioTrack format.Format, |
||||||
|
) ([]*OutgoingTrack, error) { |
||||||
|
iceServers, err := WHIPOptionsICEServers(ctx, c.HTTPClient, c.URL.String()) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
api, err := NewAPI(APIConf{}) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
c.pc = &PeerConnection{ |
||||||
|
ICEServers: iceServers, |
||||||
|
API: api, |
||||||
|
Publish: true, |
||||||
|
Log: c.Log, |
||||||
|
} |
||||||
|
err = c.pc.Start() |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
tracks, err := c.pc.SetupOutgoingTracks(videoTrack, audioTrack) |
||||||
|
if err != nil { |
||||||
|
c.pc.Close() |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
offer, err := c.pc.CreatePartialOffer() |
||||||
|
if err != nil { |
||||||
|
c.pc.Close() |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
res, err := PostOffer(ctx, c.HTTPClient, c.URL.String(), offer) |
||||||
|
if err != nil { |
||||||
|
c.pc.Close() |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
c.URL, err = c.URL.Parse(res.Location) |
||||||
|
if err != nil { |
||||||
|
c.pc.Close() |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
err = c.pc.SetAnswer(res.Answer) |
||||||
|
if err != nil { |
||||||
|
c.pc.Close() |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
t := time.NewTimer(webrtcHandshakeTimeout) |
||||||
|
defer t.Stop() |
||||||
|
|
||||||
|
outer: |
||||||
|
for { |
||||||
|
select { |
||||||
|
case ca := <-c.pc.NewLocalCandidate(): |
||||||
|
err := WHIPPatchCandidate(context.Background(), c.HTTPClient, c.URL.String(), offer, res.ETag, ca) |
||||||
|
if err != nil { |
||||||
|
c.pc.Close() |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
case <-c.pc.GatheringDone(): |
||||||
|
|
||||||
|
case <-c.pc.Connected(): |
||||||
|
break outer |
||||||
|
|
||||||
|
case <-t.C: |
||||||
|
c.pc.Close() |
||||||
|
return nil, fmt.Errorf("deadline exceeded while waiting connection") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return tracks, nil |
||||||
|
} |
||||||
|
|
||||||
|
// Read reads tracks.
|
||||||
|
func (c *WHIPClient) Read(ctx context.Context) ([]*IncomingTrack, error) { |
||||||
|
iceServers, err := WHIPOptionsICEServers(ctx, c.HTTPClient, c.URL.String()) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
api, err := NewAPI(APIConf{}) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
c.pc = &PeerConnection{ |
||||||
|
ICEServers: iceServers, |
||||||
|
API: api, |
||||||
|
Publish: false, |
||||||
|
Log: c.Log, |
||||||
|
} |
||||||
|
err = c.pc.Start() |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
offer, err := c.pc.CreatePartialOffer() |
||||||
|
if err != nil { |
||||||
|
c.pc.Close() |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
res, err := PostOffer(ctx, c.HTTPClient, c.URL.String(), offer) |
||||||
|
if err != nil { |
||||||
|
c.pc.Close() |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
c.URL, err = c.URL.Parse(res.Location) |
||||||
|
if err != nil { |
||||||
|
c.pc.Close() |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
var sdp sdp.SessionDescription |
||||||
|
err = sdp.Unmarshal([]byte(res.Answer.SDP)) |
||||||
|
if err != nil { |
||||||
|
c.pc.Close() |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
// check that there are at most two tracks
|
||||||
|
_, err = TrackCount(sdp.MediaDescriptions) |
||||||
|
if err != nil { |
||||||
|
c.pc.Close() |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
err = c.pc.SetAnswer(res.Answer) |
||||||
|
if err != nil { |
||||||
|
c.pc.Close() |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
t := time.NewTimer(webrtcHandshakeTimeout) |
||||||
|
defer t.Stop() |
||||||
|
|
||||||
|
outer: |
||||||
|
for { |
||||||
|
select { |
||||||
|
case ca := <-c.pc.NewLocalCandidate(): |
||||||
|
err := WHIPPatchCandidate(context.Background(), c.HTTPClient, c.URL.String(), offer, res.ETag, ca) |
||||||
|
if err != nil { |
||||||
|
c.pc.Close() |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
case <-c.pc.GatheringDone(): |
||||||
|
|
||||||
|
case <-c.pc.Connected(): |
||||||
|
break outer |
||||||
|
|
||||||
|
case <-t.C: |
||||||
|
c.pc.Close() |
||||||
|
return nil, fmt.Errorf("deadline exceeded while waiting connection") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return c.pc.GatherIncomingTracks(ctx, 0) |
||||||
|
} |
||||||
|
|
||||||
|
// Close closes the client.
|
||||||
|
func (c *WHIPClient) Close() error { |
||||||
|
err := WHIPDeleteSession(context.Background(), c.HTTPClient, c.URL.String()) |
||||||
|
c.pc.Close() |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// Wait waits for client errors.
|
||||||
|
func (c *WHIPClient) Wait(ctx context.Context) error { |
||||||
|
select { |
||||||
|
case <-c.pc.Disconnected(): |
||||||
|
return fmt.Errorf("peer connection closed") |
||||||
|
|
||||||
|
case <-ctx.Done(): |
||||||
|
return fmt.Errorf("terminated") |
||||||
|
} |
||||||
|
} |
||||||
@ -1,196 +0,0 @@ |
|||||||
// Package webrtcpc contains a WebRTC peer connection wrapper.
|
|
||||||
package webrtcpc |
|
||||||
|
|
||||||
import ( |
|
||||||
"context" |
|
||||||
"fmt" |
|
||||||
"strconv" |
|
||||||
"sync" |
|
||||||
|
|
||||||
"github.com/pion/webrtc/v3" |
|
||||||
|
|
||||||
"github.com/bluenviron/mediamtx/internal/logger" |
|
||||||
) |
|
||||||
|
|
||||||
// PeerConnection is a wrapper around webrtc.PeerConnection.
|
|
||||||
type PeerConnection struct { |
|
||||||
*webrtc.PeerConnection |
|
||||||
stateChangeMutex sync.Mutex |
|
||||||
newLocalCandidate chan *webrtc.ICECandidateInit |
|
||||||
connected chan struct{} |
|
||||||
disconnected chan struct{} |
|
||||||
closed chan struct{} |
|
||||||
gatheringDone chan struct{} |
|
||||||
} |
|
||||||
|
|
||||||
// New allocates a PeerConnection.
|
|
||||||
func New( |
|
||||||
iceServers []webrtc.ICEServer, |
|
||||||
api *webrtc.API, |
|
||||||
log logger.Writer, |
|
||||||
) (*PeerConnection, error) { |
|
||||||
configuration := webrtc.Configuration{ICEServers: iceServers} |
|
||||||
|
|
||||||
pc, err := api.NewPeerConnection(configuration) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
co := &PeerConnection{ |
|
||||||
PeerConnection: pc, |
|
||||||
newLocalCandidate: make(chan *webrtc.ICECandidateInit), |
|
||||||
connected: make(chan struct{}), |
|
||||||
disconnected: make(chan struct{}), |
|
||||||
closed: make(chan struct{}), |
|
||||||
gatheringDone: make(chan struct{}), |
|
||||||
} |
|
||||||
|
|
||||||
pc.OnConnectionStateChange(func(state webrtc.PeerConnectionState) { |
|
||||||
co.stateChangeMutex.Lock() |
|
||||||
defer co.stateChangeMutex.Unlock() |
|
||||||
|
|
||||||
select { |
|
||||||
case <-co.closed: |
|
||||||
return |
|
||||||
default: |
|
||||||
} |
|
||||||
|
|
||||||
log.Log(logger.Debug, "peer connection state: "+state.String()) |
|
||||||
|
|
||||||
switch state { |
|
||||||
case webrtc.PeerConnectionStateConnected: |
|
||||||
log.Log(logger.Info, "peer connection established, local candidate: %v, remote candidate: %v", |
|
||||||
co.LocalCandidate(), co.RemoteCandidate()) |
|
||||||
|
|
||||||
close(co.connected) |
|
||||||
|
|
||||||
case webrtc.PeerConnectionStateDisconnected: |
|
||||||
close(co.disconnected) |
|
||||||
|
|
||||||
case webrtc.PeerConnectionStateClosed: |
|
||||||
close(co.closed) |
|
||||||
} |
|
||||||
}) |
|
||||||
|
|
||||||
pc.OnICECandidate(func(i *webrtc.ICECandidate) { |
|
||||||
if i != nil { |
|
||||||
v := i.ToJSON() |
|
||||||
select { |
|
||||||
case co.newLocalCandidate <- &v: |
|
||||||
case <-co.connected: |
|
||||||
case <-co.closed: |
|
||||||
} |
|
||||||
} else { |
|
||||||
close(co.gatheringDone) |
|
||||||
} |
|
||||||
}) |
|
||||||
|
|
||||||
return co, nil |
|
||||||
} |
|
||||||
|
|
||||||
// Close closes the connection.
|
|
||||||
func (co *PeerConnection) Close() { |
|
||||||
co.PeerConnection.Close() //nolint:errcheck
|
|
||||||
<-co.closed |
|
||||||
} |
|
||||||
|
|
||||||
// Connected returns when connected.
|
|
||||||
func (co *PeerConnection) Connected() <-chan struct{} { |
|
||||||
return co.connected |
|
||||||
} |
|
||||||
|
|
||||||
// Disconnected returns when disconnected.
|
|
||||||
func (co *PeerConnection) Disconnected() <-chan struct{} { |
|
||||||
return co.disconnected |
|
||||||
} |
|
||||||
|
|
||||||
// NewLocalCandidate returns when there's a new local candidate.
|
|
||||||
func (co *PeerConnection) NewLocalCandidate() <-chan *webrtc.ICECandidateInit { |
|
||||||
return co.newLocalCandidate |
|
||||||
} |
|
||||||
|
|
||||||
// GatheringDone returns when candidate gathering is complete.
|
|
||||||
func (co *PeerConnection) GatheringDone() <-chan struct{} { |
|
||||||
return co.gatheringDone |
|
||||||
} |
|
||||||
|
|
||||||
// WaitGatheringDone waits until candidate gathering is complete.
|
|
||||||
func (co *PeerConnection) WaitGatheringDone(ctx context.Context) error { |
|
||||||
for { |
|
||||||
select { |
|
||||||
case <-co.NewLocalCandidate(): |
|
||||||
case <-co.GatheringDone(): |
|
||||||
return nil |
|
||||||
case <-ctx.Done(): |
|
||||||
return fmt.Errorf("terminated") |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// LocalCandidate returns the local candidate.
|
|
||||||
func (co *PeerConnection) LocalCandidate() string { |
|
||||||
var cid string |
|
||||||
for _, stats := range co.GetStats() { |
|
||||||
if tstats, ok := stats.(webrtc.ICECandidatePairStats); ok && tstats.Nominated { |
|
||||||
cid = tstats.LocalCandidateID |
|
||||||
break |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if cid != "" { |
|
||||||
for _, stats := range co.GetStats() { |
|
||||||
if tstats, ok := stats.(webrtc.ICECandidateStats); ok && tstats.ID == cid { |
|
||||||
return tstats.CandidateType.String() + "/" + tstats.Protocol + "/" + |
|
||||||
tstats.IP + "/" + strconv.FormatInt(int64(tstats.Port), 10) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return "" |
|
||||||
} |
|
||||||
|
|
||||||
// RemoteCandidate returns the remote candidate.
|
|
||||||
func (co *PeerConnection) RemoteCandidate() string { |
|
||||||
var cid string |
|
||||||
for _, stats := range co.GetStats() { |
|
||||||
if tstats, ok := stats.(webrtc.ICECandidatePairStats); ok && tstats.Nominated { |
|
||||||
cid = tstats.RemoteCandidateID |
|
||||||
break |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if cid != "" { |
|
||||||
for _, stats := range co.GetStats() { |
|
||||||
if tstats, ok := stats.(webrtc.ICECandidateStats); ok && tstats.ID == cid { |
|
||||||
return tstats.CandidateType.String() + "/" + tstats.Protocol + "/" + |
|
||||||
tstats.IP + "/" + strconv.FormatInt(int64(tstats.Port), 10) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return "" |
|
||||||
} |
|
||||||
|
|
||||||
// BytesReceived returns received bytes.
|
|
||||||
func (co *PeerConnection) BytesReceived() uint64 { |
|
||||||
for _, stats := range co.GetStats() { |
|
||||||
if tstats, ok := stats.(webrtc.TransportStats); ok { |
|
||||||
if tstats.ID == "iceTransport" { |
|
||||||
return tstats.BytesReceived |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
return 0 |
|
||||||
} |
|
||||||
|
|
||||||
// BytesSent returns sent bytes.
|
|
||||||
func (co *PeerConnection) BytesSent() uint64 { |
|
||||||
for _, stats := range co.GetStats() { |
|
||||||
if tstats, ok := stats.(webrtc.TransportStats); ok { |
|
||||||
if tstats.ID == "iceTransport" { |
|
||||||
return tstats.BytesSent |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
return 0 |
|
||||||
} |
|
||||||
Loading…
Reference in new issue