mirror of
https://github.com/bluenviron/mediamtx.git
synced 2025-12-20 02:00:05 -08:00
Some checks are pending
code_lint / go (push) Waiting to run
code_lint / go_mod (push) Waiting to run
code_lint / docs (push) Waiting to run
code_lint / api_docs (push) Waiting to run
code_test / test_64 (push) Waiting to run
code_test / test_32 (push) Waiting to run
code_test / test_e2e (push) Waiting to run
in API (/paths/list, /paths/get) and metrics (paths_bytes_sent), the amount of sent bytes was increased even in case of writes to the recorder and HLS muxer, which are not generating network traffic. This fixes the issue.
511 lines
12 KiB
Go
511 lines
12 KiB
Go
package webrtc
|
|
|
|
import (
|
|
"context"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/bluenviron/gortsplib/v5/pkg/description"
|
|
"github.com/google/uuid"
|
|
"github.com/pion/ice/v4"
|
|
"github.com/pion/sdp/v3"
|
|
pwebrtc "github.com/pion/webrtc/v4"
|
|
|
|
"github.com/bluenviron/mediamtx/internal/auth"
|
|
"github.com/bluenviron/mediamtx/internal/conf"
|
|
"github.com/bluenviron/mediamtx/internal/defs"
|
|
"github.com/bluenviron/mediamtx/internal/externalcmd"
|
|
"github.com/bluenviron/mediamtx/internal/hooks"
|
|
"github.com/bluenviron/mediamtx/internal/logger"
|
|
"github.com/bluenviron/mediamtx/internal/protocols/httpp"
|
|
"github.com/bluenviron/mediamtx/internal/protocols/webrtc"
|
|
"github.com/bluenviron/mediamtx/internal/stream"
|
|
)
|
|
|
|
func whipOffer(body []byte) *pwebrtc.SessionDescription {
|
|
return &pwebrtc.SessionDescription{
|
|
Type: pwebrtc.SDPTypeOffer,
|
|
SDP: string(body),
|
|
}
|
|
}
|
|
|
|
type sessionParent interface {
|
|
closeSession(sx *session)
|
|
generateICEServers(clientConfig bool) ([]pwebrtc.ICEServer, error)
|
|
logger.Writer
|
|
}
|
|
|
|
type session struct {
|
|
parentCtx context.Context
|
|
ipsFromInterfaces bool
|
|
ipsFromInterfacesList []string
|
|
additionalHosts []string
|
|
iceUDPMux ice.UDPMux
|
|
iceTCPMux *webrtc.TCPMuxWrapper
|
|
handshakeTimeout conf.Duration
|
|
trackGatherTimeout conf.Duration
|
|
stunGatherTimeout conf.Duration
|
|
req webRTCNewSessionReq
|
|
wg *sync.WaitGroup
|
|
externalCmdPool *externalcmd.Pool
|
|
pathManager serverPathManager
|
|
parent sessionParent
|
|
|
|
ctx context.Context
|
|
ctxCancel func()
|
|
created time.Time
|
|
uuid uuid.UUID
|
|
secret uuid.UUID
|
|
mutex sync.RWMutex
|
|
pc *webrtc.PeerConnection
|
|
|
|
chNew chan webRTCNewSessionReq
|
|
chAddCandidates chan webRTCAddSessionCandidatesReq
|
|
}
|
|
|
|
func (s *session) initialize() {
|
|
ctx, ctxCancel := context.WithCancel(s.parentCtx)
|
|
|
|
s.ctx = ctx
|
|
s.ctxCancel = ctxCancel
|
|
s.created = time.Now()
|
|
s.uuid = uuid.New()
|
|
s.secret = uuid.New()
|
|
s.chNew = make(chan webRTCNewSessionReq)
|
|
s.chAddCandidates = make(chan webRTCAddSessionCandidatesReq)
|
|
|
|
s.Log(logger.Info, "created by %s", s.req.remoteAddr)
|
|
|
|
s.wg.Add(1)
|
|
|
|
go s.run()
|
|
}
|
|
|
|
// Log implements logger.Writer.
|
|
func (s *session) 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 *session) Close() {
|
|
s.ctxCancel()
|
|
}
|
|
|
|
func (s *session) run() {
|
|
defer s.wg.Done()
|
|
|
|
err := s.runInner()
|
|
|
|
s.ctxCancel()
|
|
|
|
s.parent.closeSession(s)
|
|
|
|
s.Log(logger.Info, "closed: %v", err)
|
|
}
|
|
|
|
func (s *session) runInner() error {
|
|
select {
|
|
case <-s.chNew:
|
|
case <-s.ctx.Done():
|
|
return fmt.Errorf("terminated")
|
|
}
|
|
|
|
errStatusCode, err := s.runInner2()
|
|
|
|
if errStatusCode != 0 {
|
|
s.req.res <- webRTCNewSessionRes{
|
|
errStatusCode: errStatusCode,
|
|
err: err,
|
|
}
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
func (s *session) runInner2() (int, error) {
|
|
if s.req.publish {
|
|
return s.runPublish()
|
|
}
|
|
return s.runRead()
|
|
}
|
|
|
|
func (s *session) runPublish() (int, error) {
|
|
ip, _, _ := net.SplitHostPort(s.req.remoteAddr)
|
|
|
|
pathConf, err := s.pathManager.FindPathConf(defs.PathFindPathConfReq{
|
|
AccessRequest: defs.PathAccessRequest{
|
|
Name: s.req.pathName,
|
|
Query: s.req.httpRequest.URL.RawQuery,
|
|
Publish: true,
|
|
Proto: auth.ProtocolWebRTC,
|
|
ID: &s.uuid,
|
|
Credentials: httpp.Credentials(s.req.httpRequest),
|
|
IP: net.ParseIP(ip),
|
|
},
|
|
})
|
|
if err != nil {
|
|
return http.StatusBadRequest, err
|
|
}
|
|
|
|
iceServers, err := s.parent.generateICEServers(false)
|
|
if err != nil {
|
|
return http.StatusInternalServerError, err
|
|
}
|
|
|
|
pc := &webrtc.PeerConnection{
|
|
ICEUDPMux: s.iceUDPMux,
|
|
ICETCPMux: s.iceTCPMux,
|
|
ICEServers: iceServers,
|
|
IPsFromInterfaces: s.ipsFromInterfaces,
|
|
IPsFromInterfacesList: s.ipsFromInterfacesList,
|
|
AdditionalHosts: s.additionalHosts,
|
|
HandshakeTimeout: s.handshakeTimeout,
|
|
TrackGatherTimeout: s.trackGatherTimeout,
|
|
STUNGatherTimeout: s.stunGatherTimeout,
|
|
Publish: false,
|
|
UseAbsoluteTimestamp: pathConf.UseAbsoluteTimestamp,
|
|
Log: s,
|
|
}
|
|
err = pc.Start()
|
|
if err != nil {
|
|
return http.StatusBadRequest, err
|
|
}
|
|
|
|
terminatorDone := make(chan struct{})
|
|
defer func() { <-terminatorDone }()
|
|
|
|
terminatorRun := make(chan struct{})
|
|
defer close(terminatorRun)
|
|
|
|
go func() {
|
|
defer close(terminatorDone)
|
|
select {
|
|
case <-s.ctx.Done():
|
|
case <-terminatorRun:
|
|
}
|
|
pc.Close()
|
|
}()
|
|
|
|
offer := whipOffer(s.req.offer)
|
|
|
|
var sdp sdp.SessionDescription
|
|
err = sdp.Unmarshal([]byte(offer.SDP))
|
|
if err != nil {
|
|
return http.StatusBadRequest, err
|
|
}
|
|
|
|
err = webrtc.TracksAreValid(sdp.MediaDescriptions)
|
|
if err != nil {
|
|
// RFC draft-ietf-wish-whip
|
|
// if the number of audio and or video
|
|
// tracks or number streams is not supported by the WHIP Endpoint, it
|
|
// MUST reject the HTTP POST request with a "406 Not Acceptable" error
|
|
// response.
|
|
return http.StatusNotAcceptable, err
|
|
}
|
|
|
|
answer, err := pc.CreateFullAnswer(offer)
|
|
if err != nil {
|
|
return http.StatusBadRequest, err
|
|
}
|
|
|
|
s.writeAnswer(answer)
|
|
|
|
go s.readRemoteCandidates(pc)
|
|
|
|
err = pc.WaitUntilConnected()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
s.mutex.Lock()
|
|
s.pc = pc
|
|
s.mutex.Unlock()
|
|
|
|
err = pc.GatherIncomingTracks()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
var stream *stream.Stream
|
|
|
|
medias, err := webrtc.ToStream(pc, &stream)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
var path defs.Path
|
|
path, stream, err = s.pathManager.AddPublisher(defs.PathAddPublisherReq{
|
|
Author: s,
|
|
Desc: &description.Session{Medias: medias},
|
|
GenerateRTPPackets: false,
|
|
ConfToCompare: pathConf,
|
|
AccessRequest: defs.PathAccessRequest{
|
|
Name: s.req.pathName,
|
|
Query: s.req.httpRequest.URL.RawQuery,
|
|
Publish: true,
|
|
SkipAuth: true,
|
|
},
|
|
})
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
defer path.RemovePublisher(defs.PathRemovePublisherReq{Author: s})
|
|
|
|
pc.StartReading()
|
|
|
|
select {
|
|
case <-pc.Failed():
|
|
return 0, fmt.Errorf("peer connection closed")
|
|
|
|
case <-s.ctx.Done():
|
|
return 0, fmt.Errorf("terminated")
|
|
}
|
|
}
|
|
|
|
func (s *session) runRead() (int, error) {
|
|
ip, _, _ := net.SplitHostPort(s.req.remoteAddr)
|
|
|
|
req := defs.PathAccessRequest{
|
|
Name: s.req.pathName,
|
|
Query: s.req.httpRequest.URL.RawQuery,
|
|
Proto: auth.ProtocolWebRTC,
|
|
ID: &s.uuid,
|
|
Credentials: httpp.Credentials(s.req.httpRequest),
|
|
IP: net.ParseIP(ip),
|
|
}
|
|
|
|
path, strm, err := s.pathManager.AddReader(defs.PathAddReaderReq{
|
|
Author: s,
|
|
AccessRequest: req,
|
|
})
|
|
if err != nil {
|
|
var terr2 defs.PathNoStreamAvailableError
|
|
if errors.As(err, &terr2) {
|
|
return http.StatusNotFound, err
|
|
}
|
|
|
|
return http.StatusBadRequest, err
|
|
}
|
|
|
|
defer path.RemoveReader(defs.PathRemoveReaderReq{Author: s})
|
|
|
|
iceServers, err := s.parent.generateICEServers(false)
|
|
if err != nil {
|
|
return http.StatusInternalServerError, err
|
|
}
|
|
|
|
pc := &webrtc.PeerConnection{
|
|
ICEUDPMux: s.iceUDPMux,
|
|
ICETCPMux: s.iceTCPMux,
|
|
ICEServers: iceServers,
|
|
IPsFromInterfaces: s.ipsFromInterfaces,
|
|
IPsFromInterfacesList: s.ipsFromInterfacesList,
|
|
AdditionalHosts: s.additionalHosts,
|
|
HandshakeTimeout: s.handshakeTimeout,
|
|
TrackGatherTimeout: s.trackGatherTimeout,
|
|
STUNGatherTimeout: s.stunGatherTimeout,
|
|
Publish: true,
|
|
UseAbsoluteTimestamp: path.SafeConf().UseAbsoluteTimestamp,
|
|
Log: s,
|
|
}
|
|
|
|
r := &stream.Reader{Parent: s}
|
|
|
|
err = webrtc.FromStream(strm.Desc, r, pc)
|
|
if err != nil {
|
|
return http.StatusBadRequest, err
|
|
}
|
|
|
|
err = pc.Start()
|
|
if err != nil {
|
|
return http.StatusBadRequest, err
|
|
}
|
|
|
|
terminatorDone := make(chan struct{})
|
|
defer func() { <-terminatorDone }()
|
|
|
|
terminatorRun := make(chan struct{})
|
|
defer close(terminatorRun)
|
|
|
|
go func() {
|
|
defer close(terminatorDone)
|
|
select {
|
|
case <-s.ctx.Done():
|
|
case <-terminatorRun:
|
|
}
|
|
pc.Close()
|
|
}()
|
|
|
|
offer := whipOffer(s.req.offer)
|
|
|
|
answer, err := pc.CreateFullAnswer(offer)
|
|
if err != nil {
|
|
return http.StatusBadRequest, err
|
|
}
|
|
|
|
s.writeAnswer(answer)
|
|
|
|
go s.readRemoteCandidates(pc)
|
|
|
|
err = pc.WaitUntilConnected()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
s.mutex.Lock()
|
|
s.pc = pc
|
|
s.mutex.Unlock()
|
|
|
|
s.Log(logger.Info, "is reading from path '%s', %s",
|
|
path.Name(), defs.FormatsInfo(r.Formats()))
|
|
|
|
onUnreadHook := hooks.OnRead(hooks.OnReadParams{
|
|
Logger: s,
|
|
ExternalCmdPool: s.externalCmdPool,
|
|
Conf: path.SafeConf(),
|
|
ExternalCmdEnv: path.ExternalCmdEnv(),
|
|
Reader: s.APIReaderDescribe(),
|
|
Query: s.req.httpRequest.URL.RawQuery,
|
|
})
|
|
defer onUnreadHook()
|
|
|
|
strm.AddReader(r)
|
|
defer strm.RemoveReader(r)
|
|
|
|
select {
|
|
case <-pc.Failed():
|
|
return 0, fmt.Errorf("peer connection closed")
|
|
|
|
case err = <-r.Error():
|
|
return 0, err
|
|
|
|
case <-s.ctx.Done():
|
|
return 0, fmt.Errorf("terminated")
|
|
}
|
|
}
|
|
|
|
func (s *session) writeAnswer(answer *pwebrtc.SessionDescription) {
|
|
s.req.res <- webRTCNewSessionRes{
|
|
sx: s,
|
|
answer: []byte(answer.SDP),
|
|
}
|
|
}
|
|
|
|
func (s *session) readRemoteCandidates(pc *webrtc.PeerConnection) {
|
|
for {
|
|
select {
|
|
case req := <-s.chAddCandidates:
|
|
for _, candidate := range req.candidates {
|
|
err := pc.AddRemoteCandidate(candidate)
|
|
if err != nil {
|
|
req.res <- webRTCAddSessionCandidatesRes{err: err}
|
|
}
|
|
}
|
|
req.res <- webRTCAddSessionCandidatesRes{}
|
|
|
|
case <-s.ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// new is called by webRTCHTTPServer through Server.
|
|
func (s *session) new(req webRTCNewSessionReq) webRTCNewSessionRes {
|
|
select {
|
|
case s.chNew <- req:
|
|
return <-req.res
|
|
|
|
case <-s.ctx.Done():
|
|
return webRTCNewSessionRes{err: fmt.Errorf("terminated"), errStatusCode: http.StatusInternalServerError}
|
|
}
|
|
}
|
|
|
|
// addCandidates is called by webRTCHTTPServer through Server.
|
|
func (s *session) addCandidates(
|
|
req webRTCAddSessionCandidatesReq,
|
|
) webRTCAddSessionCandidatesRes {
|
|
select {
|
|
case s.chAddCandidates <- req:
|
|
return <-req.res
|
|
|
|
case <-s.ctx.Done():
|
|
return webRTCAddSessionCandidatesRes{err: fmt.Errorf("terminated")}
|
|
}
|
|
}
|
|
|
|
// APIReaderDescribe implements reader.
|
|
func (s *session) APIReaderDescribe() defs.APIPathSourceOrReader {
|
|
return defs.APIPathSourceOrReader{
|
|
Type: "webRTCSession",
|
|
ID: s.uuid.String(),
|
|
}
|
|
}
|
|
|
|
// APISourceDescribe implements source.
|
|
func (s *session) APISourceDescribe() defs.APIPathSourceOrReader {
|
|
return s.APIReaderDescribe()
|
|
}
|
|
|
|
func (s *session) apiItem() *defs.APIWebRTCSession {
|
|
s.mutex.RLock()
|
|
defer s.mutex.RUnlock()
|
|
|
|
peerConnectionEstablished := false
|
|
localCandidate := ""
|
|
remoteCandidate := ""
|
|
bytesReceived := uint64(0)
|
|
bytesSent := uint64(0)
|
|
rtpPacketsReceived := uint64(0)
|
|
rtpPacketsSent := uint64(0)
|
|
rtpPacketsLost := uint64(0)
|
|
rtpPacketsJitter := float64(0)
|
|
rtcpPacketsReceived := uint64(0)
|
|
rtcpPacketsSent := uint64(0)
|
|
|
|
if s.pc != nil {
|
|
peerConnectionEstablished = true
|
|
localCandidate = s.pc.LocalCandidate()
|
|
remoteCandidate = s.pc.RemoteCandidate()
|
|
stats := s.pc.Stats()
|
|
bytesReceived = stats.BytesReceived
|
|
bytesSent = stats.BytesSent
|
|
rtpPacketsReceived = stats.RTPPacketsReceived
|
|
rtpPacketsSent = stats.RTPPacketsSent
|
|
rtpPacketsLost = stats.RTPPacketsLost
|
|
rtpPacketsJitter = stats.RTPPacketsJitter
|
|
rtcpPacketsReceived = stats.RTCPPacketsReceived
|
|
rtcpPacketsSent = stats.RTCPPacketsSent
|
|
}
|
|
|
|
return &defs.APIWebRTCSession{
|
|
ID: s.uuid,
|
|
Created: s.created,
|
|
RemoteAddr: s.req.remoteAddr,
|
|
PeerConnectionEstablished: peerConnectionEstablished,
|
|
LocalCandidate: localCandidate,
|
|
RemoteCandidate: remoteCandidate,
|
|
State: func() defs.APIWebRTCSessionState {
|
|
if s.req.publish {
|
|
return defs.APIWebRTCSessionStatePublish
|
|
}
|
|
return defs.APIWebRTCSessionStateRead
|
|
}(),
|
|
Path: s.req.pathName,
|
|
Query: s.req.httpRequest.URL.RawQuery,
|
|
BytesReceived: bytesReceived,
|
|
BytesSent: bytesSent,
|
|
RTPPacketsReceived: rtpPacketsReceived,
|
|
RTPPacketsSent: rtpPacketsSent,
|
|
RTPPacketsLost: rtpPacketsLost,
|
|
RTPPacketsJitter: rtpPacketsJitter,
|
|
RTCPPacketsReceived: rtcpPacketsReceived,
|
|
RTCPPacketsSent: rtcpPacketsSent,
|
|
}
|
|
}
|