golanggohlsrtmpwebrtcmedia-serverobs-studiortcprtmp-proxyrtmp-serverrtprtsprtsp-proxyrtsp-relayrtsp-serversrtstreamingwebrtc-proxy
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
378 lines
8.6 KiB
378 lines
8.6 KiB
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 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{} |
|
done chan struct{} |
|
gatheringDone chan struct{} |
|
incomingTrack chan trackRecvPair |
|
|
|
ctx context.Context |
|
ctxCancel context.CancelFunc |
|
} |
|
|
|
// Start starts the peer connection. |
|
func (co *PeerConnection) Start() error { |
|
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.done = make(chan struct{}) |
|
co.gatheringDone = make(chan struct{}) |
|
co.incomingTrack = make(chan trackRecvPair) |
|
|
|
co.ctx, co.ctxCancel = context.WithCancel(context.Background()) |
|
|
|
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.ctx.Done(): |
|
} |
|
}) |
|
} |
|
|
|
co.wr.OnConnectionStateChange(func(state webrtc.PeerConnectionState) { |
|
co.stateChangeMutex.Lock() |
|
defer co.stateChangeMutex.Unlock() |
|
|
|
select { |
|
case <-co.done: |
|
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.done) |
|
} |
|
}) |
|
|
|
co.wr.OnICECandidate(func(i *webrtc.ICECandidate) { |
|
if i != nil { |
|
v := i.ToJSON() |
|
select { |
|
case co.newLocalCandidate <- &v: |
|
case <-co.connected: |
|
case <-co.ctx.Done(): |
|
} |
|
} else { |
|
close(co.gatheringDone) |
|
} |
|
}) |
|
|
|
return nil |
|
} |
|
|
|
// Close closes the connection. |
|
func (co *PeerConnection) Close() { |
|
co.ctxCancel() |
|
co.wr.Close() //nolint:errcheck |
|
<-co.done |
|
} |
|
|
|
// 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, |
|
maxCount int, |
|
) ([]*IncomingTrack, error) { |
|
var tracks []*IncomingTrack |
|
|
|
t := time.NewTimer(webrtcTrackGatherTimeout) |
|
defer t.Stop() |
|
|
|
for { |
|
select { |
|
case <-t.C: |
|
if maxCount == 0 && len(tracks) != 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) == maxCount || 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 |
|
}
|
|
|