mirror of
https://github.com/bluenviron/mediamtx.git
synced 2025-12-20 02:00:05 -08:00
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.
510 lines
12 KiB
Go
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,
|
|
}
|
|
}
|