mirror of
				https://github.com/aler9/rtsp-simple-server
				synced 2025-10-31 11:06:28 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			466 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			466 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package webrtc
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"encoding/hex"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"net"
 | |
| 	"net/http"
 | |
| 	"sync"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/bluenviron/gortsplib/v4/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             ice.TCPMux
 | |
| 	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
 | |
| 	}
 | |
| 	defer 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(s.ctx, offer)
 | |
| 	if err != nil {
 | |
| 		return http.StatusBadRequest, err
 | |
| 	}
 | |
| 
 | |
| 	s.writeAnswer(answer)
 | |
| 
 | |
| 	go s.readRemoteCandidates(pc)
 | |
| 
 | |
| 	err = pc.WaitUntilConnected(s.ctx)
 | |
| 	if err != nil {
 | |
| 		return 0, err
 | |
| 	}
 | |
| 
 | |
| 	s.mutex.Lock()
 | |
| 	s.pc = pc
 | |
| 	s.mutex.Unlock()
 | |
| 
 | |
| 	err = pc.GatherIncomingTracks(s.ctx)
 | |
| 	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, stream, 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,
 | |
| 	}
 | |
| 
 | |
| 	err = webrtc.FromStream(stream, s, pc)
 | |
| 	if err != nil {
 | |
| 		return http.StatusBadRequest, err
 | |
| 	}
 | |
| 
 | |
| 	err = pc.Start()
 | |
| 	if err != nil {
 | |
| 		stream.RemoveReader(s)
 | |
| 		return http.StatusBadRequest, err
 | |
| 	}
 | |
| 	defer pc.Close()
 | |
| 
 | |
| 	offer := whipOffer(s.req.offer)
 | |
| 
 | |
| 	answer, err := pc.CreateFullAnswer(s.ctx, offer)
 | |
| 	if err != nil {
 | |
| 		stream.RemoveReader(s)
 | |
| 		return http.StatusBadRequest, err
 | |
| 	}
 | |
| 
 | |
| 	s.writeAnswer(answer)
 | |
| 
 | |
| 	go s.readRemoteCandidates(pc)
 | |
| 
 | |
| 	err = pc.WaitUntilConnected(s.ctx)
 | |
| 	if err != nil {
 | |
| 		stream.RemoveReader(s)
 | |
| 		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(stream.ReaderFormats(s)))
 | |
| 
 | |
| 	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()
 | |
| 
 | |
| 	stream.StartReader(s)
 | |
| 	defer stream.RemoveReader(s)
 | |
| 
 | |
| 	select {
 | |
| 	case <-pc.Failed():
 | |
| 		return 0, fmt.Errorf("peer connection closed")
 | |
| 
 | |
| 	case err = <-stream.ReaderError(s):
 | |
| 		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)
 | |
| 
 | |
| 	if s.pc != nil {
 | |
| 		peerConnectionEstablished = true
 | |
| 		localCandidate = s.pc.LocalCandidate()
 | |
| 		remoteCandidate = s.pc.RemoteCandidate()
 | |
| 		bytesReceived = s.pc.BytesReceived()
 | |
| 		bytesSent = s.pc.BytesSent()
 | |
| 	}
 | |
| 
 | |
| 	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,
 | |
| 	}
 | |
| }
 | 
