mediamtx/internal/servers/webrtc/session.go
Alessandro Ros 0cdae40fe3
estimate absolute timestamp more precisely (#5078)
When the absolute timestamp of incoming frames was not available, it
was filled with the current timestamp, which is influenced by latency
over time.

This mechanism is replaced by an algorithm that detects when latency is
the lowest, stores the current timestamp and uses it as reference
throughout the rest of the stream.
2025-10-12 11:02:14 +02:00

510 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,
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, pathConf, &stream, s)
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,
FillNTP: !pathConf.UseAbsoluteTimestamp,
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,
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,
}
}