27 changed files with 1650 additions and 1464 deletions
@ -1,177 +0,0 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -1,4 +1,4 @@
|
||||
package whip |
||||
package webrtc |
||||
|
||||
import ( |
||||
"fmt" |
@ -1,4 +1,4 @@
@@ -1,4 +1,4 @@
|
||||
package whip |
||||
package webrtc |
||||
|
||||
import ( |
||||
"testing" |
@ -0,0 +1,152 @@
@@ -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 @@
@@ -1,4 +1,4 @@
|
||||
package whip |
||||
package webrtc |
||||
|
||||
import ( |
||||
"encoding/json" |
@ -1,4 +1,4 @@
@@ -1,4 +1,4 @@
|
||||
package whip |
||||
package webrtc |
||||
|
||||
import ( |
||||
"testing" |
@ -0,0 +1,154 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -0,0 +1,2 @@
|
||||
// Package webrtc contains WebRTC utilities.
|
||||
package webrtc |
@ -0,0 +1,213 @@
@@ -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 @@
@@ -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