2023-05-16 13:59:37 +00:00
|
|
|
package core
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"encoding/hex"
|
|
|
|
"fmt"
|
2023-07-23 17:31:34 +00:00
|
|
|
"net"
|
2023-05-16 16:01:05 +00:00
|
|
|
"net/http"
|
|
|
|
"strings"
|
2023-05-16 13:59:37 +00:00
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/bluenviron/gortsplib/v3/pkg/media"
|
|
|
|
"github.com/bluenviron/gortsplib/v3/pkg/ringbuffer"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/pion/ice/v2"
|
2023-07-24 18:32:28 +00:00
|
|
|
"github.com/pion/sdp/v3"
|
2023-05-16 13:59:37 +00:00
|
|
|
"github.com/pion/webrtc/v3"
|
|
|
|
|
2023-05-16 14:14:20 +00:00
|
|
|
"github.com/bluenviron/mediamtx/internal/logger"
|
2023-05-16 13:59:37 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type trackRecvPair struct {
|
|
|
|
track *webrtc.TrackRemote
|
|
|
|
receiver *webrtc.RTPReceiver
|
|
|
|
}
|
|
|
|
|
|
|
|
func mediasOfOutgoingTracks(tracks []*webRTCOutgoingTrack) media.Medias {
|
|
|
|
ret := make(media.Medias, len(tracks))
|
|
|
|
for i, track := range tracks {
|
|
|
|
ret[i] = track.media
|
|
|
|
}
|
|
|
|
return ret
|
|
|
|
}
|
|
|
|
|
|
|
|
func mediasOfIncomingTracks(tracks []*webRTCIncomingTrack) media.Medias {
|
|
|
|
ret := make(media.Medias, len(tracks))
|
|
|
|
for i, track := range tracks {
|
|
|
|
ret[i] = track.media
|
|
|
|
}
|
|
|
|
return ret
|
|
|
|
}
|
|
|
|
|
2023-07-24 18:32:28 +00:00
|
|
|
func waitUntilConnected(
|
|
|
|
ctx context.Context,
|
|
|
|
pc *peerConnection,
|
|
|
|
) error {
|
|
|
|
t := time.NewTimer(webrtcHandshakeTimeout)
|
|
|
|
defer t.Stop()
|
|
|
|
|
|
|
|
outer:
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-t.C:
|
|
|
|
return fmt.Errorf("deadline exceeded while waiting connection")
|
|
|
|
|
|
|
|
case <-pc.connected:
|
|
|
|
break outer
|
|
|
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
return fmt.Errorf("terminated")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-05-16 13:59:37 +00:00
|
|
|
func gatherOutgoingTracks(medias media.Medias) ([]*webRTCOutgoingTrack, error) {
|
|
|
|
var tracks []*webRTCOutgoingTrack
|
|
|
|
|
|
|
|
videoTrack, err := newWebRTCOutgoingTrackVideo(medias)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if videoTrack != nil {
|
|
|
|
tracks = append(tracks, videoTrack)
|
|
|
|
}
|
|
|
|
|
|
|
|
audioTrack, err := newWebRTCOutgoingTrackAudio(medias)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if audioTrack != nil {
|
|
|
|
tracks = append(tracks, audioTrack)
|
|
|
|
}
|
|
|
|
|
|
|
|
if tracks == nil {
|
|
|
|
return nil, fmt.Errorf(
|
2023-07-18 22:14:50 +00:00
|
|
|
"the stream doesn't contain any supported codec, which are currently AV1, VP9, VP8, H264, Opus, G722, G711")
|
2023-05-16 13:59:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return tracks, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func gatherIncomingTracks(
|
|
|
|
ctx context.Context,
|
|
|
|
pc *peerConnection,
|
|
|
|
trackRecv chan trackRecvPair,
|
2023-07-24 18:32:28 +00:00
|
|
|
trackCount int,
|
2023-05-16 13:59:37 +00:00
|
|
|
) ([]*webRTCIncomingTrack, error) {
|
|
|
|
var tracks []*webRTCIncomingTrack
|
|
|
|
|
|
|
|
t := time.NewTimer(webrtcTrackGatherTimeout)
|
|
|
|
defer t.Stop()
|
|
|
|
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-t.C:
|
2023-07-24 18:32:28 +00:00
|
|
|
return nil, fmt.Errorf("deadline exceeded while waiting tracks")
|
2023-05-16 13:59:37 +00:00
|
|
|
|
|
|
|
case pair := <-trackRecv:
|
|
|
|
track, err := newWebRTCIncomingTrack(pair.track, pair.receiver, pc.WriteRTCP)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
tracks = append(tracks, track)
|
|
|
|
|
2023-07-24 18:32:28 +00:00
|
|
|
if len(tracks) == trackCount {
|
2023-05-16 13:59:37 +00:00
|
|
|
return tracks, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
case <-pc.disconnected:
|
|
|
|
return nil, fmt.Errorf("peer connection closed")
|
|
|
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
return nil, fmt.Errorf("terminated")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
type webRTCSessionPathManager interface {
|
2023-07-30 21:53:39 +00:00
|
|
|
addPublisher(req pathAddPublisherReq) pathAddPublisherRes
|
|
|
|
addReader(req pathAddReaderReq) pathAddReaderRes
|
2023-05-16 13:59:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type webRTCSession struct {
|
|
|
|
readBufferCount int
|
2023-07-30 21:53:39 +00:00
|
|
|
req webRTCNewSessionReq
|
2023-05-16 13:59:37 +00:00
|
|
|
wg *sync.WaitGroup
|
|
|
|
iceHostNAT1To1IPs []string
|
|
|
|
iceUDPMux ice.UDPMux
|
|
|
|
iceTCPMux ice.TCPMux
|
|
|
|
pathManager webRTCSessionPathManager
|
|
|
|
parent *webRTCManager
|
|
|
|
|
2023-07-30 21:53:39 +00:00
|
|
|
ctx context.Context
|
|
|
|
ctxCancel func()
|
|
|
|
created time.Time
|
|
|
|
uuid uuid.UUID
|
|
|
|
secret uuid.UUID
|
|
|
|
mutex sync.RWMutex
|
|
|
|
pc *peerConnection
|
|
|
|
|
|
|
|
chNew chan webRTCNewSessionReq
|
|
|
|
chAddCandidates chan webRTCAddSessionCandidatesReq
|
2023-05-16 13:59:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func newWebRTCSession(
|
|
|
|
parentCtx context.Context,
|
|
|
|
readBufferCount int,
|
2023-07-30 21:53:39 +00:00
|
|
|
req webRTCNewSessionReq,
|
2023-05-16 13:59:37 +00:00
|
|
|
wg *sync.WaitGroup,
|
|
|
|
iceHostNAT1To1IPs []string,
|
|
|
|
iceUDPMux ice.UDPMux,
|
|
|
|
iceTCPMux ice.TCPMux,
|
|
|
|
pathManager webRTCSessionPathManager,
|
|
|
|
parent *webRTCManager,
|
|
|
|
) *webRTCSession {
|
|
|
|
ctx, ctxCancel := context.WithCancel(parentCtx)
|
|
|
|
|
|
|
|
s := &webRTCSession{
|
2023-07-18 21:41:52 +00:00
|
|
|
readBufferCount: readBufferCount,
|
2023-07-23 16:40:06 +00:00
|
|
|
req: req,
|
2023-07-18 21:41:52 +00:00
|
|
|
wg: wg,
|
|
|
|
iceHostNAT1To1IPs: iceHostNAT1To1IPs,
|
|
|
|
iceUDPMux: iceUDPMux,
|
|
|
|
iceTCPMux: iceTCPMux,
|
|
|
|
parent: parent,
|
|
|
|
pathManager: pathManager,
|
|
|
|
ctx: ctx,
|
|
|
|
ctxCancel: ctxCancel,
|
|
|
|
created: time.Now(),
|
|
|
|
uuid: uuid.New(),
|
|
|
|
secret: uuid.New(),
|
2023-07-30 21:53:39 +00:00
|
|
|
chNew: make(chan webRTCNewSessionReq),
|
|
|
|
chAddCandidates: make(chan webRTCAddSessionCandidatesReq),
|
2023-07-18 21:41:52 +00:00
|
|
|
}
|
|
|
|
|
2023-07-23 16:40:06 +00:00
|
|
|
s.Log(logger.Info, "created by %s", req.remoteAddr)
|
2023-05-16 13:59:37 +00:00
|
|
|
|
|
|
|
wg.Add(1)
|
|
|
|
go s.run()
|
|
|
|
|
|
|
|
return s
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *webRTCSession) Log(level logger.Level, format string, args ...interface{}) {
|
|
|
|
id := hex.EncodeToString(s.uuid[:4])
|
|
|
|
s.parent.Log(level, "[session %v] "+format, append([]interface{}{id}, args...)...)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *webRTCSession) close() {
|
|
|
|
s.ctxCancel()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *webRTCSession) run() {
|
|
|
|
defer s.wg.Done()
|
|
|
|
|
2023-07-18 21:41:52 +00:00
|
|
|
err := s.runInner()
|
|
|
|
|
|
|
|
s.ctxCancel()
|
|
|
|
|
2023-07-30 21:53:39 +00:00
|
|
|
s.parent.closeSession(s)
|
2023-07-18 21:41:52 +00:00
|
|
|
|
|
|
|
s.Log(logger.Info, "closed (%v)", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *webRTCSession) runInner() error {
|
|
|
|
select {
|
2023-07-23 16:40:06 +00:00
|
|
|
case <-s.chNew:
|
2023-07-18 21:41:52 +00:00
|
|
|
case <-s.ctx.Done():
|
|
|
|
return fmt.Errorf("terminated")
|
|
|
|
}
|
|
|
|
|
|
|
|
errStatusCode, err := s.runInner2()
|
2023-05-16 13:59:37 +00:00
|
|
|
|
2023-07-30 21:53:39 +00:00
|
|
|
if errStatusCode != 0 {
|
|
|
|
s.req.res <- webRTCNewSessionRes{
|
2023-05-16 16:01:05 +00:00
|
|
|
err: err,
|
|
|
|
errStatusCode: errStatusCode,
|
2023-05-16 13:59:37 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-18 21:41:52 +00:00
|
|
|
return err
|
2023-05-16 13:59:37 +00:00
|
|
|
}
|
|
|
|
|
2023-07-18 21:41:52 +00:00
|
|
|
func (s *webRTCSession) runInner2() (int, error) {
|
2023-05-16 13:59:37 +00:00
|
|
|
if s.req.publish {
|
|
|
|
return s.runPublish()
|
|
|
|
}
|
|
|
|
return s.runRead()
|
|
|
|
}
|
|
|
|
|
2023-05-16 16:01:05 +00:00
|
|
|
func (s *webRTCSession) runPublish() (int, error) {
|
2023-07-23 17:31:34 +00:00
|
|
|
ip, _, _ := net.SplitHostPort(s.req.remoteAddr)
|
|
|
|
|
2023-07-30 21:53:39 +00:00
|
|
|
res := s.pathManager.addPublisher(pathAddPublisherReq{
|
2023-05-16 13:59:37 +00:00
|
|
|
author: s,
|
|
|
|
pathName: s.req.pathName,
|
2023-07-23 17:31:34 +00:00
|
|
|
credentials: authCredentials{
|
|
|
|
query: s.req.query,
|
|
|
|
ip: net.ParseIP(ip),
|
|
|
|
user: s.req.user,
|
|
|
|
pass: s.req.pass,
|
|
|
|
proto: authProtocolWebRTC,
|
|
|
|
id: &s.uuid,
|
|
|
|
},
|
2023-05-16 13:59:37 +00:00
|
|
|
})
|
|
|
|
if res.err != nil {
|
2023-07-23 18:06:16 +00:00
|
|
|
if _, ok := res.err.(*errAuthentication); ok {
|
2023-07-23 18:18:58 +00:00
|
|
|
// wait some seconds to stop brute force attacks
|
|
|
|
<-time.After(webrtcPauseAfterAuthError)
|
|
|
|
|
2023-07-23 17:31:34 +00:00
|
|
|
return http.StatusUnauthorized, res.err
|
|
|
|
}
|
2023-07-23 18:18:58 +00:00
|
|
|
|
2023-07-23 17:31:34 +00:00
|
|
|
return http.StatusBadRequest, res.err
|
2023-05-16 13:59:37 +00:00
|
|
|
}
|
|
|
|
|
2023-07-30 21:53:39 +00:00
|
|
|
defer res.path.removePublisher(pathRemovePublisherReq{author: s})
|
2023-05-16 13:59:37 +00:00
|
|
|
|
2023-07-30 20:30:41 +00:00
|
|
|
servers, err := s.parent.generateICEServers()
|
|
|
|
if err != nil {
|
|
|
|
return http.StatusInternalServerError, err
|
|
|
|
}
|
|
|
|
|
2023-05-16 13:59:37 +00:00
|
|
|
pc, err := newPeerConnection(
|
2023-07-30 20:30:41 +00:00
|
|
|
servers,
|
2023-05-16 13:59:37 +00:00
|
|
|
s.iceHostNAT1To1IPs,
|
|
|
|
s.iceUDPMux,
|
|
|
|
s.iceTCPMux,
|
|
|
|
s)
|
|
|
|
if err != nil {
|
2023-05-16 16:01:05 +00:00
|
|
|
return http.StatusBadRequest, err
|
2023-05-16 13:59:37 +00:00
|
|
|
}
|
|
|
|
defer pc.close()
|
|
|
|
|
2023-07-24 18:32:28 +00:00
|
|
|
offer := s.offer()
|
|
|
|
|
|
|
|
var sdp sdp.SessionDescription
|
|
|
|
err = sdp.Unmarshal([]byte(offer.SDP))
|
|
|
|
if err != nil {
|
|
|
|
return http.StatusBadRequest, err
|
|
|
|
}
|
|
|
|
|
|
|
|
videoTrack := false
|
|
|
|
audioTrack := false
|
|
|
|
trackCount := 0
|
|
|
|
|
|
|
|
for _, media := range sdp.MediaDescriptions {
|
|
|
|
switch media.MediaName.Media {
|
|
|
|
case "video":
|
|
|
|
if videoTrack {
|
|
|
|
return http.StatusBadRequest, fmt.Errorf("only a single video and a single audio track are supported")
|
|
|
|
}
|
|
|
|
videoTrack = true
|
|
|
|
|
|
|
|
case "audio":
|
|
|
|
if audioTrack {
|
|
|
|
return http.StatusBadRequest, fmt.Errorf("only a single video and a single audio track are supported")
|
|
|
|
}
|
|
|
|
audioTrack = true
|
|
|
|
|
|
|
|
default:
|
|
|
|
return http.StatusBadRequest, fmt.Errorf("unsupported media '%s'", media.MediaName.Media)
|
|
|
|
}
|
|
|
|
|
|
|
|
trackCount++
|
|
|
|
}
|
|
|
|
|
2023-05-16 13:59:37 +00:00
|
|
|
_, err = pc.AddTransceiverFromKind(webrtc.RTPCodecTypeVideo, webrtc.RtpTransceiverInit{
|
|
|
|
Direction: webrtc.RTPTransceiverDirectionRecvonly,
|
|
|
|
})
|
|
|
|
if err != nil {
|
2023-05-16 16:01:05 +00:00
|
|
|
return http.StatusBadRequest, err
|
2023-05-16 13:59:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
_, err = pc.AddTransceiverFromKind(webrtc.RTPCodecTypeAudio, webrtc.RtpTransceiverInit{
|
|
|
|
Direction: webrtc.RTPTransceiverDirectionRecvonly,
|
|
|
|
})
|
|
|
|
if err != nil {
|
2023-05-16 16:01:05 +00:00
|
|
|
return http.StatusBadRequest, err
|
2023-05-16 13:59:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
trackRecv := make(chan trackRecvPair)
|
|
|
|
|
|
|
|
pc.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {
|
|
|
|
select {
|
|
|
|
case trackRecv <- trackRecvPair{track, receiver}:
|
|
|
|
case <-pc.closed:
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
err = pc.SetRemoteDescription(*offer)
|
|
|
|
if err != nil {
|
2023-05-16 16:01:05 +00:00
|
|
|
return http.StatusBadRequest, err
|
2023-05-16 13:59:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
answer, err := pc.CreateAnswer(nil)
|
|
|
|
if err != nil {
|
2023-05-16 16:01:05 +00:00
|
|
|
return http.StatusBadRequest, err
|
2023-05-16 13:59:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
err = pc.SetLocalDescription(answer)
|
|
|
|
if err != nil {
|
2023-05-16 16:01:05 +00:00
|
|
|
return http.StatusBadRequest, err
|
2023-05-16 13:59:37 +00:00
|
|
|
}
|
|
|
|
|
2023-06-24 16:35:19 +00:00
|
|
|
err = s.waitGatheringDone(pc)
|
|
|
|
if err != nil {
|
|
|
|
return http.StatusBadRequest, err
|
|
|
|
}
|
|
|
|
|
|
|
|
tmp := pc.LocalDescription()
|
|
|
|
answer = *tmp
|
|
|
|
|
2023-07-19 10:31:50 +00:00
|
|
|
s.writeAnswer(&answer)
|
2023-05-16 13:59:37 +00:00
|
|
|
|
|
|
|
go s.readRemoteCandidates(pc)
|
|
|
|
|
2023-07-24 18:32:28 +00:00
|
|
|
err = waitUntilConnected(s.ctx, pc)
|
2023-05-16 13:59:37 +00:00
|
|
|
if err != nil {
|
2023-05-16 16:01:05 +00:00
|
|
|
return 0, err
|
2023-05-16 13:59:37 +00:00
|
|
|
}
|
|
|
|
|
2023-07-24 18:32:28 +00:00
|
|
|
s.mutex.Lock()
|
|
|
|
s.pc = pc
|
|
|
|
s.mutex.Unlock()
|
|
|
|
|
|
|
|
tracks, err := gatherIncomingTracks(s.ctx, pc, trackRecv, trackCount)
|
2023-05-16 13:59:37 +00:00
|
|
|
if err != nil {
|
2023-05-16 16:01:05 +00:00
|
|
|
return 0, err
|
2023-05-16 13:59:37 +00:00
|
|
|
}
|
|
|
|
medias := mediasOfIncomingTracks(tracks)
|
|
|
|
|
2023-07-30 21:53:39 +00:00
|
|
|
rres := res.path.startPublisher(pathStartPublisherReq{
|
2023-05-16 13:59:37 +00:00
|
|
|
author: s,
|
|
|
|
medias: medias,
|
|
|
|
generateRTPPackets: false,
|
|
|
|
})
|
|
|
|
if rres.err != nil {
|
2023-05-16 16:01:05 +00:00
|
|
|
return 0, rres.err
|
2023-05-16 13:59:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
s.Log(logger.Info, "is publishing to path '%s', %s",
|
|
|
|
res.path.name,
|
|
|
|
sourceMediaInfo(medias))
|
|
|
|
|
|
|
|
for _, track := range tracks {
|
|
|
|
track.start(rres.stream)
|
|
|
|
}
|
|
|
|
|
|
|
|
select {
|
|
|
|
case <-pc.disconnected:
|
2023-05-16 16:01:05 +00:00
|
|
|
return 0, fmt.Errorf("peer connection closed")
|
2023-05-16 13:59:37 +00:00
|
|
|
|
|
|
|
case <-s.ctx.Done():
|
2023-05-16 16:01:05 +00:00
|
|
|
return 0, fmt.Errorf("terminated")
|
2023-05-16 13:59:37 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-16 16:01:05 +00:00
|
|
|
func (s *webRTCSession) runRead() (int, error) {
|
2023-07-23 17:31:34 +00:00
|
|
|
ip, _, _ := net.SplitHostPort(s.req.remoteAddr)
|
|
|
|
|
2023-07-30 21:53:39 +00:00
|
|
|
res := s.pathManager.addReader(pathAddReaderReq{
|
2023-05-16 13:59:37 +00:00
|
|
|
author: s,
|
|
|
|
pathName: s.req.pathName,
|
2023-07-23 17:31:34 +00:00
|
|
|
credentials: authCredentials{
|
|
|
|
query: s.req.query,
|
|
|
|
ip: net.ParseIP(ip),
|
|
|
|
user: s.req.user,
|
|
|
|
pass: s.req.pass,
|
|
|
|
proto: authProtocolWebRTC,
|
|
|
|
id: &s.uuid,
|
|
|
|
},
|
2023-05-16 13:59:37 +00:00
|
|
|
})
|
|
|
|
if res.err != nil {
|
2023-07-23 18:06:16 +00:00
|
|
|
if _, ok := res.err.(*errAuthentication); ok {
|
2023-07-23 18:18:58 +00:00
|
|
|
// wait some seconds to stop brute force attacks
|
|
|
|
<-time.After(webrtcPauseAfterAuthError)
|
|
|
|
|
2023-07-23 17:31:34 +00:00
|
|
|
return http.StatusUnauthorized, res.err
|
|
|
|
}
|
2023-07-23 18:18:58 +00:00
|
|
|
|
2023-05-16 16:01:05 +00:00
|
|
|
if strings.HasPrefix(res.err.Error(), "no one is publishing") {
|
|
|
|
return http.StatusNotFound, res.err
|
|
|
|
}
|
2023-07-23 18:18:58 +00:00
|
|
|
|
2023-07-23 17:31:34 +00:00
|
|
|
return http.StatusBadRequest, res.err
|
2023-05-16 13:59:37 +00:00
|
|
|
}
|
|
|
|
|
2023-07-30 21:53:39 +00:00
|
|
|
defer res.path.removeReader(pathRemoveReaderReq{author: s})
|
2023-05-16 13:59:37 +00:00
|
|
|
|
2023-07-30 20:34:35 +00:00
|
|
|
tracks, err := gatherOutgoingTracks(res.stream.Medias())
|
2023-05-16 13:59:37 +00:00
|
|
|
if err != nil {
|
2023-05-16 16:01:05 +00:00
|
|
|
return http.StatusBadRequest, err
|
2023-05-16 13:59:37 +00:00
|
|
|
}
|
|
|
|
|
2023-07-30 20:30:41 +00:00
|
|
|
servers, err := s.parent.generateICEServers()
|
|
|
|
if err != nil {
|
|
|
|
return http.StatusInternalServerError, err
|
|
|
|
}
|
|
|
|
|
2023-05-16 13:59:37 +00:00
|
|
|
pc, err := newPeerConnection(
|
2023-07-30 20:30:41 +00:00
|
|
|
servers,
|
2023-05-16 13:59:37 +00:00
|
|
|
s.iceHostNAT1To1IPs,
|
|
|
|
s.iceUDPMux,
|
|
|
|
s.iceTCPMux,
|
|
|
|
s)
|
|
|
|
if err != nil {
|
2023-05-16 16:01:05 +00:00
|
|
|
return http.StatusBadRequest, err
|
2023-05-16 13:59:37 +00:00
|
|
|
}
|
|
|
|
defer pc.close()
|
|
|
|
|
|
|
|
for _, track := range tracks {
|
|
|
|
var err error
|
|
|
|
track.sender, err = pc.AddTrack(track.track)
|
|
|
|
if err != nil {
|
2023-05-16 16:01:05 +00:00
|
|
|
return http.StatusBadRequest, err
|
2023-05-16 13:59:37 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-24 18:32:28 +00:00
|
|
|
offer := s.offer()
|
|
|
|
|
2023-05-16 13:59:37 +00:00
|
|
|
err = pc.SetRemoteDescription(*offer)
|
|
|
|
if err != nil {
|
2023-05-16 16:01:05 +00:00
|
|
|
return http.StatusBadRequest, err
|
2023-05-16 13:59:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
answer, err := pc.CreateAnswer(nil)
|
|
|
|
if err != nil {
|
2023-05-16 16:01:05 +00:00
|
|
|
return http.StatusBadRequest, err
|
2023-05-16 13:59:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
err = pc.SetLocalDescription(answer)
|
|
|
|
if err != nil {
|
2023-05-16 16:01:05 +00:00
|
|
|
return http.StatusBadRequest, err
|
2023-05-16 13:59:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
err = s.waitGatheringDone(pc)
|
|
|
|
if err != nil {
|
2023-05-16 16:01:05 +00:00
|
|
|
return http.StatusBadRequest, err
|
2023-05-16 13:59:37 +00:00
|
|
|
}
|
|
|
|
|
2023-06-24 16:35:19 +00:00
|
|
|
tmp := pc.LocalDescription()
|
|
|
|
answer = *tmp
|
|
|
|
|
2023-07-19 10:31:50 +00:00
|
|
|
s.writeAnswer(&answer)
|
2023-05-16 13:59:37 +00:00
|
|
|
|
|
|
|
go s.readRemoteCandidates(pc)
|
|
|
|
|
2023-07-24 18:32:28 +00:00
|
|
|
err = waitUntilConnected(s.ctx, pc)
|
2023-05-16 13:59:37 +00:00
|
|
|
if err != nil {
|
2023-05-16 16:01:05 +00:00
|
|
|
return 0, err
|
2023-05-16 13:59:37 +00:00
|
|
|
}
|
|
|
|
|
2023-07-24 18:32:28 +00:00
|
|
|
s.mutex.Lock()
|
|
|
|
s.pc = pc
|
|
|
|
s.mutex.Unlock()
|
|
|
|
|
2023-05-16 13:59:37 +00:00
|
|
|
ringBuffer, _ := ringbuffer.New(uint64(s.readBufferCount))
|
|
|
|
defer ringBuffer.Close()
|
|
|
|
|
|
|
|
writeError := make(chan error)
|
|
|
|
|
|
|
|
for _, track := range tracks {
|
|
|
|
track.start(s.ctx, s, res.stream, ringBuffer, writeError)
|
|
|
|
}
|
|
|
|
|
2023-07-30 20:34:35 +00:00
|
|
|
defer res.stream.RemoveReader(s)
|
2023-05-16 13:59:37 +00:00
|
|
|
|
|
|
|
s.Log(logger.Info, "is reading from path '%s', %s",
|
|
|
|
res.path.name, sourceMediaInfo(mediasOfOutgoingTracks(tracks)))
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
for {
|
|
|
|
item, ok := ringBuffer.Pull()
|
|
|
|
if !ok {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
item.(func())()
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
select {
|
|
|
|
case <-pc.disconnected:
|
2023-05-16 16:01:05 +00:00
|
|
|
return 0, fmt.Errorf("peer connection closed")
|
2023-05-16 13:59:37 +00:00
|
|
|
|
|
|
|
case err := <-writeError:
|
2023-05-16 16:01:05 +00:00
|
|
|
return 0, err
|
2023-05-16 13:59:37 +00:00
|
|
|
|
|
|
|
case <-s.ctx.Done():
|
2023-05-16 16:01:05 +00:00
|
|
|
return 0, fmt.Errorf("terminated")
|
2023-05-16 13:59:37 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-24 18:32:28 +00:00
|
|
|
func (s *webRTCSession) offer() *webrtc.SessionDescription {
|
2023-05-24 15:06:06 +00:00
|
|
|
return &webrtc.SessionDescription{
|
|
|
|
Type: webrtc.SDPTypeOffer,
|
|
|
|
SDP: string(s.req.offer),
|
2023-05-16 13:59:37 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *webRTCSession) waitGatheringDone(pc *peerConnection) error {
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-pc.localCandidateRecv:
|
|
|
|
case <-pc.gatheringDone:
|
|
|
|
return nil
|
|
|
|
case <-s.ctx.Done():
|
|
|
|
return fmt.Errorf("terminated")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-19 10:31:50 +00:00
|
|
|
func (s *webRTCSession) writeAnswer(answer *webrtc.SessionDescription) {
|
2023-07-30 21:53:39 +00:00
|
|
|
s.req.res <- webRTCNewSessionRes{
|
2023-05-16 13:59:37 +00:00
|
|
|
sx: s,
|
2023-05-24 15:06:06 +00:00
|
|
|
answer: []byte(answer.SDP),
|
2023-05-16 13:59:37 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *webRTCSession) readRemoteCandidates(pc *peerConnection) {
|
|
|
|
for {
|
|
|
|
select {
|
2023-07-18 21:41:52 +00:00
|
|
|
case req := <-s.chAddCandidates:
|
2023-05-16 13:59:37 +00:00
|
|
|
for _, candidate := range req.candidates {
|
|
|
|
err := pc.AddICECandidate(*candidate)
|
|
|
|
if err != nil {
|
2023-07-30 21:53:39 +00:00
|
|
|
req.res <- webRTCAddSessionCandidatesRes{err: err}
|
2023-05-16 13:59:37 +00:00
|
|
|
}
|
|
|
|
}
|
2023-07-30 21:53:39 +00:00
|
|
|
req.res <- webRTCAddSessionCandidatesRes{}
|
2023-05-16 13:59:37 +00:00
|
|
|
|
|
|
|
case <-s.ctx.Done():
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-18 21:41:52 +00:00
|
|
|
// new is called by webRTCHTTPServer through webRTCManager.
|
2023-07-30 21:53:39 +00:00
|
|
|
func (s *webRTCSession) new(req webRTCNewSessionReq) webRTCNewSessionRes {
|
2023-07-18 21:41:52 +00:00
|
|
|
select {
|
|
|
|
case s.chNew <- req:
|
|
|
|
return <-req.res
|
|
|
|
|
|
|
|
case <-s.ctx.Done():
|
2023-07-30 21:53:39 +00:00
|
|
|
return webRTCNewSessionRes{err: fmt.Errorf("terminated"), errStatusCode: http.StatusInternalServerError}
|
2023-07-18 21:41:52 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// addCandidates is called by webRTCHTTPServer through webRTCManager.
|
|
|
|
func (s *webRTCSession) addCandidates(
|
2023-07-30 21:53:39 +00:00
|
|
|
req webRTCAddSessionCandidatesReq,
|
|
|
|
) webRTCAddSessionCandidatesRes {
|
2023-05-16 13:59:37 +00:00
|
|
|
select {
|
2023-07-18 21:41:52 +00:00
|
|
|
case s.chAddCandidates <- req:
|
2023-05-16 13:59:37 +00:00
|
|
|
return <-req.res
|
|
|
|
|
|
|
|
case <-s.ctx.Done():
|
2023-07-30 21:53:39 +00:00
|
|
|
return webRTCAddSessionCandidatesRes{err: fmt.Errorf("terminated")}
|
2023-05-16 13:59:37 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// apiSourceDescribe implements sourceStaticImpl.
|
|
|
|
func (s *webRTCSession) apiSourceDescribe() pathAPISourceOrReader {
|
|
|
|
return pathAPISourceOrReader{
|
|
|
|
Type: "webRTCSession",
|
|
|
|
ID: s.uuid.String(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// apiReaderDescribe implements reader.
|
|
|
|
func (s *webRTCSession) apiReaderDescribe() pathAPISourceOrReader {
|
|
|
|
return s.apiSourceDescribe()
|
|
|
|
}
|
2023-05-18 13:07:47 +00:00
|
|
|
|
|
|
|
func (s *webRTCSession) apiItem() *apiWebRTCSession {
|
2023-07-05 19:20:26 +00:00
|
|
|
s.mutex.RLock()
|
|
|
|
defer s.mutex.RUnlock()
|
|
|
|
|
2023-05-18 13:07:47 +00:00
|
|
|
peerConnectionEstablished := false
|
|
|
|
localCandidate := ""
|
|
|
|
remoteCandidate := ""
|
|
|
|
bytesReceived := uint64(0)
|
|
|
|
bytesSent := uint64(0)
|
|
|
|
|
2023-07-05 19:20:26 +00:00
|
|
|
if s.pc != nil {
|
2023-05-18 13:07:47 +00:00
|
|
|
peerConnectionEstablished = true
|
2023-07-05 19:20:26 +00:00
|
|
|
localCandidate = s.pc.localCandidate()
|
|
|
|
remoteCandidate = s.pc.remoteCandidate()
|
|
|
|
bytesReceived = s.pc.bytesReceived()
|
|
|
|
bytesSent = s.pc.bytesSent()
|
2023-05-18 13:07:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return &apiWebRTCSession{
|
|
|
|
ID: s.uuid,
|
|
|
|
Created: s.created,
|
|
|
|
RemoteAddr: s.req.remoteAddr,
|
|
|
|
PeerConnectionEstablished: peerConnectionEstablished,
|
|
|
|
LocalCandidate: localCandidate,
|
|
|
|
RemoteCandidate: remoteCandidate,
|
|
|
|
State: func() string {
|
|
|
|
if s.req.publish {
|
|
|
|
return "publish"
|
|
|
|
}
|
|
|
|
return "read"
|
|
|
|
}(),
|
2023-07-05 19:20:26 +00:00
|
|
|
Path: s.req.pathName,
|
2023-05-18 13:07:47 +00:00
|
|
|
BytesReceived: bytesReceived,
|
|
|
|
BytesSent: bytesSent,
|
|
|
|
}
|
|
|
|
}
|