30 changed files with 2932 additions and 2333 deletions
@ -0,0 +1,148 @@
@@ -0,0 +1,148 @@
|
||||
package core |
||||
|
||||
import ( |
||||
"context" |
||||
"crypto/tls" |
||||
"log" |
||||
"net" |
||||
"net/http" |
||||
gopath "path" |
||||
"strings" |
||||
"time" |
||||
|
||||
"github.com/gin-gonic/gin" |
||||
|
||||
"github.com/aler9/mediamtx/internal/conf" |
||||
"github.com/aler9/mediamtx/internal/logger" |
||||
) |
||||
|
||||
type hlsHTTPServerParent interface { |
||||
logger.Writer |
||||
handleRequest(req hlsMuxerHandleRequestReq) |
||||
} |
||||
|
||||
type hlsHTTPServer struct { |
||||
allowOrigin string |
||||
parent hlsHTTPServerParent |
||||
|
||||
ln net.Listener |
||||
inner *http.Server |
||||
} |
||||
|
||||
func newHLSHTTPServer( |
||||
address string, |
||||
encryption bool, |
||||
serverKey string, |
||||
serverCert string, |
||||
allowOrigin string, |
||||
trustedProxies conf.IPsOrCIDRs, |
||||
readTimeout conf.StringDuration, |
||||
parent hlsHTTPServerParent, |
||||
) (*hlsHTTPServer, error) { |
||||
ln, err := net.Listen(restrictNetwork("tcp", address)) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
var tlsConfig *tls.Config |
||||
if encryption { |
||||
crt, err := tls.LoadX509KeyPair(serverCert, serverKey) |
||||
if err != nil { |
||||
ln.Close() |
||||
return nil, err |
||||
} |
||||
|
||||
tlsConfig = &tls.Config{ |
||||
Certificates: []tls.Certificate{crt}, |
||||
} |
||||
} |
||||
|
||||
s := &hlsHTTPServer{ |
||||
allowOrigin: allowOrigin, |
||||
parent: parent, |
||||
ln: ln, |
||||
} |
||||
|
||||
router := gin.New() |
||||
httpSetTrustedProxies(router, trustedProxies) |
||||
|
||||
router.NoRoute(httpLoggerMiddleware(s), httpServerHeaderMiddleware, s.onRequest) |
||||
|
||||
s.inner = &http.Server{ |
||||
Handler: router, |
||||
TLSConfig: tlsConfig, |
||||
ReadHeaderTimeout: time.Duration(readTimeout), |
||||
ErrorLog: log.New(&nilWriter{}, "", 0), |
||||
} |
||||
|
||||
if tlsConfig != nil { |
||||
go s.inner.ServeTLS(s.ln, "", "") |
||||
} else { |
||||
go s.inner.Serve(s.ln) |
||||
} |
||||
|
||||
return s, nil |
||||
} |
||||
|
||||
func (s *hlsHTTPServer) Log(level logger.Level, format string, args ...interface{}) { |
||||
s.parent.Log(level, format, args...) |
||||
} |
||||
|
||||
func (s *hlsHTTPServer) close() { |
||||
s.inner.Shutdown(context.Background()) |
||||
s.ln.Close() // in case Shutdown() is called before Serve()
|
||||
} |
||||
|
||||
func (s *hlsHTTPServer) onRequest(ctx *gin.Context) { |
||||
ctx.Writer.Header().Set("Access-Control-Allow-Origin", s.allowOrigin) |
||||
ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true") |
||||
|
||||
switch ctx.Request.Method { |
||||
case http.MethodGet: |
||||
|
||||
case http.MethodOptions: |
||||
ctx.Writer.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS") |
||||
ctx.Writer.Header().Set("Access-Control-Allow-Headers", ctx.Request.Header.Get("Access-Control-Request-Headers")) |
||||
ctx.Writer.WriteHeader(http.StatusOK) |
||||
return |
||||
|
||||
default: |
||||
return |
||||
} |
||||
|
||||
// remove leading prefix
|
||||
pa := ctx.Request.URL.Path[1:] |
||||
|
||||
switch pa { |
||||
case "", "favicon.ico": |
||||
return |
||||
} |
||||
|
||||
dir, fname := func() (string, string) { |
||||
if strings.HasSuffix(pa, ".m3u8") || |
||||
strings.HasSuffix(pa, ".ts") || |
||||
strings.HasSuffix(pa, ".mp4") || |
||||
strings.HasSuffix(pa, ".mp") { |
||||
return gopath.Dir(pa), gopath.Base(pa) |
||||
} |
||||
return pa, "" |
||||
}() |
||||
|
||||
if fname == "" && !strings.HasSuffix(dir, "/") { |
||||
ctx.Writer.Header().Set("Location", "/"+dir+"/") |
||||
ctx.Writer.WriteHeader(http.StatusMovedPermanently) |
||||
return |
||||
} |
||||
|
||||
if strings.HasSuffix(fname, ".mp") { |
||||
fname += "4" |
||||
} |
||||
|
||||
dir = strings.TrimSuffix(dir, "/") |
||||
|
||||
s.parent.handleRequest(hlsMuxerHandleRequestReq{ |
||||
path: dir, |
||||
file: fname, |
||||
ctx: ctx, |
||||
}) |
||||
} |
@ -0,0 +1,314 @@
@@ -0,0 +1,314 @@
|
||||
package core |
||||
|
||||
import ( |
||||
"context" |
||||
"fmt" |
||||
"sync" |
||||
"time" |
||||
|
||||
"github.com/aler9/mediamtx/internal/conf" |
||||
"github.com/aler9/mediamtx/internal/logger" |
||||
) |
||||
|
||||
type nilWriter struct{} |
||||
|
||||
func (nilWriter) Write(p []byte) (int, error) { |
||||
return len(p), nil |
||||
} |
||||
|
||||
type hlsManagerAPIMuxersListItem struct { |
||||
Created time.Time `json:"created"` |
||||
LastRequest time.Time `json:"lastRequest"` |
||||
BytesSent uint64 `json:"bytesSent"` |
||||
} |
||||
|
||||
type hlsManagerAPIMuxersListData struct { |
||||
Items map[string]hlsManagerAPIMuxersListItem `json:"items"` |
||||
} |
||||
|
||||
type hlsManagerAPIMuxersListRes struct { |
||||
data *hlsManagerAPIMuxersListData |
||||
muxers map[string]*hlsMuxer |
||||
err error |
||||
} |
||||
|
||||
type hlsManagerAPIMuxersListReq struct { |
||||
res chan hlsManagerAPIMuxersListRes |
||||
} |
||||
|
||||
type hlsManagerAPIMuxersListSubReq struct { |
||||
data *hlsManagerAPIMuxersListData |
||||
res chan struct{} |
||||
} |
||||
|
||||
type hlsManagerParent interface { |
||||
logger.Writer |
||||
} |
||||
|
||||
type hlsManager struct { |
||||
externalAuthenticationURL string |
||||
alwaysRemux bool |
||||
variant conf.HLSVariant |
||||
segmentCount int |
||||
segmentDuration conf.StringDuration |
||||
partDuration conf.StringDuration |
||||
segmentMaxSize conf.StringSize |
||||
directory string |
||||
readBufferCount int |
||||
pathManager *pathManager |
||||
metrics *metrics |
||||
parent hlsManagerParent |
||||
|
||||
ctx context.Context |
||||
ctxCancel func() |
||||
wg sync.WaitGroup |
||||
httpServer *hlsHTTPServer |
||||
muxers map[string]*hlsMuxer |
||||
|
||||
// in
|
||||
chPathSourceReady chan *path |
||||
chPathSourceNotReady chan *path |
||||
chHandleRequest chan hlsMuxerHandleRequestReq |
||||
chMuxerClose chan *hlsMuxer |
||||
chAPIMuxerList chan hlsManagerAPIMuxersListReq |
||||
} |
||||
|
||||
func newHLSManager( |
||||
parentCtx context.Context, |
||||
address string, |
||||
encryption bool, |
||||
serverKey string, |
||||
serverCert string, |
||||
externalAuthenticationURL string, |
||||
alwaysRemux bool, |
||||
variant conf.HLSVariant, |
||||
segmentCount int, |
||||
segmentDuration conf.StringDuration, |
||||
partDuration conf.StringDuration, |
||||
segmentMaxSize conf.StringSize, |
||||
allowOrigin string, |
||||
trustedProxies conf.IPsOrCIDRs, |
||||
directory string, |
||||
readTimeout conf.StringDuration, |
||||
readBufferCount int, |
||||
pathManager *pathManager, |
||||
metrics *metrics, |
||||
parent hlsManagerParent, |
||||
) (*hlsManager, error) { |
||||
ctx, ctxCancel := context.WithCancel(parentCtx) |
||||
|
||||
m := &hlsManager{ |
||||
externalAuthenticationURL: externalAuthenticationURL, |
||||
alwaysRemux: alwaysRemux, |
||||
variant: variant, |
||||
segmentCount: segmentCount, |
||||
segmentDuration: segmentDuration, |
||||
partDuration: partDuration, |
||||
segmentMaxSize: segmentMaxSize, |
||||
directory: directory, |
||||
readBufferCount: readBufferCount, |
||||
pathManager: pathManager, |
||||
parent: parent, |
||||
metrics: metrics, |
||||
ctx: ctx, |
||||
ctxCancel: ctxCancel, |
||||
muxers: make(map[string]*hlsMuxer), |
||||
chPathSourceReady: make(chan *path), |
||||
chPathSourceNotReady: make(chan *path), |
||||
chHandleRequest: make(chan hlsMuxerHandleRequestReq), |
||||
chMuxerClose: make(chan *hlsMuxer), |
||||
chAPIMuxerList: make(chan hlsManagerAPIMuxersListReq), |
||||
} |
||||
|
||||
var err error |
||||
m.httpServer, err = newHLSHTTPServer( |
||||
address, |
||||
encryption, |
||||
serverKey, |
||||
serverCert, |
||||
allowOrigin, |
||||
trustedProxies, |
||||
readTimeout, |
||||
m, |
||||
) |
||||
if err != nil { |
||||
ctxCancel() |
||||
return nil, err |
||||
} |
||||
|
||||
m.Log(logger.Info, "listener opened on "+address) |
||||
|
||||
m.pathManager.hlsManagerSet(m) |
||||
|
||||
if m.metrics != nil { |
||||
m.metrics.hlsManagerSet(m) |
||||
} |
||||
|
||||
m.wg.Add(1) |
||||
go m.run() |
||||
|
||||
return m, nil |
||||
} |
||||
|
||||
// Log is the main logging function.
|
||||
func (m *hlsManager) Log(level logger.Level, format string, args ...interface{}) { |
||||
m.parent.Log(level, "[HLS] "+format, append([]interface{}{}, args...)...) |
||||
} |
||||
|
||||
func (m *hlsManager) close() { |
||||
m.Log(logger.Info, "listener is closing") |
||||
m.ctxCancel() |
||||
m.wg.Wait() |
||||
} |
||||
|
||||
func (m *hlsManager) run() { |
||||
defer m.wg.Done() |
||||
|
||||
outer: |
||||
for { |
||||
select { |
||||
case pa := <-m.chPathSourceReady: |
||||
if m.alwaysRemux { |
||||
m.createMuxer(pa.name, "") |
||||
} |
||||
|
||||
case pa := <-m.chPathSourceNotReady: |
||||
if m.alwaysRemux { |
||||
c, ok := m.muxers[pa.name] |
||||
if ok { |
||||
c.close() |
||||
delete(m.muxers, pa.name) |
||||
} |
||||
} |
||||
|
||||
case req := <-m.chHandleRequest: |
||||
r, ok := m.muxers[req.path] |
||||
switch { |
||||
case ok: |
||||
r.processRequest(&req) |
||||
|
||||
case m.alwaysRemux: |
||||
req.res <- nil |
||||
|
||||
default: |
||||
r := m.createMuxer(req.path, req.ctx.ClientIP()) |
||||
r.processRequest(&req) |
||||
} |
||||
|
||||
case c := <-m.chMuxerClose: |
||||
if c2, ok := m.muxers[c.PathName()]; !ok || c2 != c { |
||||
continue |
||||
} |
||||
delete(m.muxers, c.PathName()) |
||||
|
||||
case req := <-m.chAPIMuxerList: |
||||
muxers := make(map[string]*hlsMuxer) |
||||
|
||||
for name, m := range m.muxers { |
||||
muxers[name] = m |
||||
} |
||||
|
||||
req.res <- hlsManagerAPIMuxersListRes{ |
||||
muxers: muxers, |
||||
} |
||||
|
||||
case <-m.ctx.Done(): |
||||
break outer |
||||
} |
||||
} |
||||
|
||||
m.ctxCancel() |
||||
|
||||
m.httpServer.close() |
||||
|
||||
m.pathManager.hlsManagerSet(nil) |
||||
|
||||
if m.metrics != nil { |
||||
m.metrics.hlsManagerSet(nil) |
||||
} |
||||
} |
||||
|
||||
func (m *hlsManager) createMuxer(pathName string, remoteAddr string) *hlsMuxer { |
||||
r := newHLSMuxer( |
||||
m.ctx, |
||||
remoteAddr, |
||||
m.externalAuthenticationURL, |
||||
m.alwaysRemux, |
||||
m.variant, |
||||
m.segmentCount, |
||||
m.segmentDuration, |
||||
m.partDuration, |
||||
m.segmentMaxSize, |
||||
m.directory, |
||||
m.readBufferCount, |
||||
&m.wg, |
||||
pathName, |
||||
m.pathManager, |
||||
m) |
||||
m.muxers[pathName] = r |
||||
return r |
||||
} |
||||
|
||||
// muxerClose is called by hlsMuxer.
|
||||
func (m *hlsManager) muxerClose(c *hlsMuxer) { |
||||
select { |
||||
case m.chMuxerClose <- c: |
||||
case <-m.ctx.Done(): |
||||
} |
||||
} |
||||
|
||||
// pathSourceReady is called by pathManager.
|
||||
func (m *hlsManager) pathSourceReady(pa *path) { |
||||
select { |
||||
case m.chPathSourceReady <- pa: |
||||
case <-m.ctx.Done(): |
||||
} |
||||
} |
||||
|
||||
// pathSourceNotReady is called by pathManager.
|
||||
func (m *hlsManager) pathSourceNotReady(pa *path) { |
||||
select { |
||||
case m.chPathSourceNotReady <- pa: |
||||
case <-m.ctx.Done(): |
||||
} |
||||
} |
||||
|
||||
// apiMuxersList is called by api.
|
||||
func (m *hlsManager) apiMuxersList() hlsManagerAPIMuxersListRes { |
||||
req := hlsManagerAPIMuxersListReq{ |
||||
res: make(chan hlsManagerAPIMuxersListRes), |
||||
} |
||||
|
||||
select { |
||||
case m.chAPIMuxerList <- req: |
||||
res := <-req.res |
||||
|
||||
res.data = &hlsManagerAPIMuxersListData{ |
||||
Items: make(map[string]hlsManagerAPIMuxersListItem), |
||||
} |
||||
|
||||
for _, pa := range res.muxers { |
||||
pa.apiMuxersList(hlsManagerAPIMuxersListSubReq{data: res.data}) |
||||
} |
||||
|
||||
return res |
||||
|
||||
case <-m.ctx.Done(): |
||||
return hlsManagerAPIMuxersListRes{err: fmt.Errorf("terminated")} |
||||
} |
||||
} |
||||
|
||||
func (m *hlsManager) handleRequest(req hlsMuxerHandleRequestReq) { |
||||
req.res = make(chan *hlsMuxer) |
||||
|
||||
select { |
||||
case m.chHandleRequest <- req: |
||||
muxer := <-req.res |
||||
if muxer != nil { |
||||
req.ctx.Request.URL.Path = req.file |
||||
muxer.handleRequest(req.ctx) |
||||
} |
||||
|
||||
case <-m.ctx.Done(): |
||||
} |
||||
} |
@ -1,398 +0,0 @@
@@ -1,398 +0,0 @@
|
||||
package core |
||||
|
||||
import ( |
||||
"context" |
||||
"crypto/tls" |
||||
"fmt" |
||||
"log" |
||||
"net" |
||||
"net/http" |
||||
gopath "path" |
||||
"strings" |
||||
"sync" |
||||
"time" |
||||
|
||||
"github.com/gin-gonic/gin" |
||||
|
||||
"github.com/aler9/mediamtx/internal/conf" |
||||
"github.com/aler9/mediamtx/internal/logger" |
||||
) |
||||
|
||||
type nilWriter struct{} |
||||
|
||||
func (nilWriter) Write(p []byte) (int, error) { |
||||
return len(p), nil |
||||
} |
||||
|
||||
type hlsServerAPIMuxersListItem struct { |
||||
Created time.Time `json:"created"` |
||||
LastRequest time.Time `json:"lastRequest"` |
||||
BytesSent uint64 `json:"bytesSent"` |
||||
} |
||||
|
||||
type hlsServerAPIMuxersListData struct { |
||||
Items map[string]hlsServerAPIMuxersListItem `json:"items"` |
||||
} |
||||
|
||||
type hlsServerAPIMuxersListRes struct { |
||||
data *hlsServerAPIMuxersListData |
||||
muxers map[string]*hlsMuxer |
||||
err error |
||||
} |
||||
|
||||
type hlsServerAPIMuxersListReq struct { |
||||
res chan hlsServerAPIMuxersListRes |
||||
} |
||||
|
||||
type hlsServerAPIMuxersListSubReq struct { |
||||
data *hlsServerAPIMuxersListData |
||||
res chan struct{} |
||||
} |
||||
|
||||
type hlsServerParent interface { |
||||
logger.Writer |
||||
} |
||||
|
||||
type hlsServer struct { |
||||
externalAuthenticationURL string |
||||
alwaysRemux bool |
||||
variant conf.HLSVariant |
||||
segmentCount int |
||||
segmentDuration conf.StringDuration |
||||
partDuration conf.StringDuration |
||||
segmentMaxSize conf.StringSize |
||||
allowOrigin string |
||||
directory string |
||||
readBufferCount int |
||||
pathManager *pathManager |
||||
metrics *metrics |
||||
parent hlsServerParent |
||||
|
||||
ctx context.Context |
||||
ctxCancel func() |
||||
wg sync.WaitGroup |
||||
ln net.Listener |
||||
httpServer *http.Server |
||||
muxers map[string]*hlsMuxer |
||||
|
||||
// in
|
||||
chPathSourceReady chan *path |
||||
chPathSourceNotReady chan *path |
||||
request chan *hlsMuxerRequest |
||||
chMuxerClose chan *hlsMuxer |
||||
chAPIMuxerList chan hlsServerAPIMuxersListReq |
||||
} |
||||
|
||||
func newHLSServer( |
||||
parentCtx context.Context, |
||||
address string, |
||||
encryption bool, |
||||
serverKey string, |
||||
serverCert string, |
||||
externalAuthenticationURL string, |
||||
alwaysRemux bool, |
||||
variant conf.HLSVariant, |
||||
segmentCount int, |
||||
segmentDuration conf.StringDuration, |
||||
partDuration conf.StringDuration, |
||||
segmentMaxSize conf.StringSize, |
||||
allowOrigin string, |
||||
trustedProxies conf.IPsOrCIDRs, |
||||
directory string, |
||||
readTimeout conf.StringDuration, |
||||
readBufferCount int, |
||||
pathManager *pathManager, |
||||
metrics *metrics, |
||||
parent hlsServerParent, |
||||
) (*hlsServer, error) { |
||||
ln, err := net.Listen(restrictNetwork("tcp", address)) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
var tlsConfig *tls.Config |
||||
if encryption { |
||||
crt, err := tls.LoadX509KeyPair(serverCert, serverKey) |
||||
if err != nil { |
||||
ln.Close() |
||||
return nil, err |
||||
} |
||||
|
||||
tlsConfig = &tls.Config{ |
||||
Certificates: []tls.Certificate{crt}, |
||||
} |
||||
} |
||||
|
||||
ctx, ctxCancel := context.WithCancel(parentCtx) |
||||
|
||||
s := &hlsServer{ |
||||
externalAuthenticationURL: externalAuthenticationURL, |
||||
alwaysRemux: alwaysRemux, |
||||
variant: variant, |
||||
segmentCount: segmentCount, |
||||
segmentDuration: segmentDuration, |
||||
partDuration: partDuration, |
||||
segmentMaxSize: segmentMaxSize, |
||||
allowOrigin: allowOrigin, |
||||
directory: directory, |
||||
readBufferCount: readBufferCount, |
||||
pathManager: pathManager, |
||||
parent: parent, |
||||
metrics: metrics, |
||||
ctx: ctx, |
||||
ctxCancel: ctxCancel, |
||||
ln: ln, |
||||
muxers: make(map[string]*hlsMuxer), |
||||
chPathSourceReady: make(chan *path), |
||||
chPathSourceNotReady: make(chan *path), |
||||
request: make(chan *hlsMuxerRequest), |
||||
chMuxerClose: make(chan *hlsMuxer), |
||||
chAPIMuxerList: make(chan hlsServerAPIMuxersListReq), |
||||
} |
||||
|
||||
router := gin.New() |
||||
httpSetTrustedProxies(router, trustedProxies) |
||||
|
||||
router.NoRoute(httpLoggerMiddleware(s), httpServerHeaderMiddleware, s.onRequest) |
||||
|
||||
s.httpServer = &http.Server{ |
||||
Handler: router, |
||||
TLSConfig: tlsConfig, |
||||
ReadHeaderTimeout: time.Duration(readTimeout), |
||||
ErrorLog: log.New(&nilWriter{}, "", 0), |
||||
} |
||||
|
||||
s.Log(logger.Info, "listener opened on "+address) |
||||
|
||||
s.pathManager.hlsServerSet(s) |
||||
|
||||
if s.metrics != nil { |
||||
s.metrics.hlsServerSet(s) |
||||
} |
||||
|
||||
s.wg.Add(1) |
||||
go s.run() |
||||
|
||||
return s, nil |
||||
} |
||||
|
||||
// Log is the main logging function.
|
||||
func (s *hlsServer) Log(level logger.Level, format string, args ...interface{}) { |
||||
s.parent.Log(level, "[HLS] "+format, append([]interface{}{}, args...)...) |
||||
} |
||||
|
||||
func (s *hlsServer) close() { |
||||
s.Log(logger.Info, "listener is closing") |
||||
s.ctxCancel() |
||||
s.wg.Wait() |
||||
} |
||||
|
||||
func (s *hlsServer) run() { |
||||
defer s.wg.Done() |
||||
|
||||
if s.httpServer.TLSConfig != nil { |
||||
go s.httpServer.ServeTLS(s.ln, "", "") |
||||
} else { |
||||
go s.httpServer.Serve(s.ln) |
||||
} |
||||
|
||||
outer: |
||||
for { |
||||
select { |
||||
case pa := <-s.chPathSourceReady: |
||||
if s.alwaysRemux { |
||||
s.createMuxer(pa.name, "") |
||||
} |
||||
|
||||
case pa := <-s.chPathSourceNotReady: |
||||
if s.alwaysRemux { |
||||
c, ok := s.muxers[pa.name] |
||||
if ok { |
||||
c.close() |
||||
delete(s.muxers, pa.name) |
||||
} |
||||
} |
||||
|
||||
case req := <-s.request: |
||||
r, ok := s.muxers[req.path] |
||||
switch { |
||||
case ok: |
||||
r.processRequest(req) |
||||
|
||||
case s.alwaysRemux: |
||||
req.res <- nil |
||||
|
||||
default: |
||||
r := s.createMuxer(req.path, req.clientIP) |
||||
r.processRequest(req) |
||||
} |
||||
|
||||
case c := <-s.chMuxerClose: |
||||
if c2, ok := s.muxers[c.PathName()]; !ok || c2 != c { |
||||
continue |
||||
} |
||||
delete(s.muxers, c.PathName()) |
||||
|
||||
case req := <-s.chAPIMuxerList: |
||||
muxers := make(map[string]*hlsMuxer) |
||||
|
||||
for name, m := range s.muxers { |
||||
muxers[name] = m |
||||
} |
||||
|
||||
req.res <- hlsServerAPIMuxersListRes{ |
||||
muxers: muxers, |
||||
} |
||||
|
||||
case <-s.ctx.Done(): |
||||
break outer |
||||
} |
||||
} |
||||
|
||||
s.ctxCancel() |
||||
|
||||
s.httpServer.Shutdown(context.Background()) |
||||
s.ln.Close() // in case Shutdown() is called before Serve()
|
||||
|
||||
s.pathManager.hlsServerSet(nil) |
||||
|
||||
if s.metrics != nil { |
||||
s.metrics.hlsServerSet(nil) |
||||
} |
||||
} |
||||
|
||||
func (s *hlsServer) onRequest(ctx *gin.Context) { |
||||
ctx.Writer.Header().Set("Access-Control-Allow-Origin", s.allowOrigin) |
||||
ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true") |
||||
|
||||
switch ctx.Request.Method { |
||||
case http.MethodGet: |
||||
|
||||
case http.MethodOptions: |
||||
ctx.Writer.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS") |
||||
ctx.Writer.Header().Set("Access-Control-Allow-Headers", ctx.Request.Header.Get("Access-Control-Request-Headers")) |
||||
ctx.Writer.WriteHeader(http.StatusOK) |
||||
return |
||||
|
||||
default: |
||||
return |
||||
} |
||||
|
||||
// remove leading prefix
|
||||
pa := ctx.Request.URL.Path[1:] |
||||
|
||||
switch pa { |
||||
case "", "favicon.ico": |
||||
return |
||||
} |
||||
|
||||
dir, fname := func() (string, string) { |
||||
if strings.HasSuffix(pa, ".m3u8") || |
||||
strings.HasSuffix(pa, ".ts") || |
||||
strings.HasSuffix(pa, ".mp4") || |
||||
strings.HasSuffix(pa, ".mp") { |
||||
return gopath.Dir(pa), gopath.Base(pa) |
||||
} |
||||
return pa, "" |
||||
}() |
||||
|
||||
if fname == "" && !strings.HasSuffix(dir, "/") { |
||||
ctx.Writer.Header().Set("Location", "/"+dir+"/") |
||||
ctx.Writer.WriteHeader(http.StatusMovedPermanently) |
||||
return |
||||
} |
||||
|
||||
if strings.HasSuffix(fname, ".mp") { |
||||
fname += "4" |
||||
} |
||||
|
||||
dir = strings.TrimSuffix(dir, "/") |
||||
|
||||
hreq := &hlsMuxerRequest{ |
||||
path: dir, |
||||
file: fname, |
||||
clientIP: ctx.ClientIP(), |
||||
res: make(chan *hlsMuxer), |
||||
} |
||||
|
||||
select { |
||||
case s.request <- hreq: |
||||
muxer := <-hreq.res |
||||
if muxer != nil { |
||||
ctx.Request.URL.Path = fname |
||||
muxer.handleRequest(ctx) |
||||
} |
||||
|
||||
case <-s.ctx.Done(): |
||||
} |
||||
} |
||||
|
||||
func (s *hlsServer) createMuxer(pathName string, remoteAddr string) *hlsMuxer { |
||||
r := newHLSMuxer( |
||||
s.ctx, |
||||
remoteAddr, |
||||
s.externalAuthenticationURL, |
||||
s.alwaysRemux, |
||||
s.variant, |
||||
s.segmentCount, |
||||
s.segmentDuration, |
||||
s.partDuration, |
||||
s.segmentMaxSize, |
||||
s.directory, |
||||
s.readBufferCount, |
||||
&s.wg, |
||||
pathName, |
||||
s.pathManager, |
||||
s) |
||||
s.muxers[pathName] = r |
||||
return r |
||||
} |
||||
|
||||
// muxerClose is called by hlsMuxer.
|
||||
func (s *hlsServer) muxerClose(c *hlsMuxer) { |
||||
select { |
||||
case s.chMuxerClose <- c: |
||||
case <-s.ctx.Done(): |
||||
} |
||||
} |
||||
|
||||
// pathSourceReady is called by pathManager.
|
||||
func (s *hlsServer) pathSourceReady(pa *path) { |
||||
select { |
||||
case s.chPathSourceReady <- pa: |
||||
case <-s.ctx.Done(): |
||||
} |
||||
} |
||||
|
||||
// pathSourceNotReady is called by pathManager.
|
||||
func (s *hlsServer) pathSourceNotReady(pa *path) { |
||||
select { |
||||
case s.chPathSourceNotReady <- pa: |
||||
case <-s.ctx.Done(): |
||||
} |
||||
} |
||||
|
||||
// apiMuxersList is called by api.
|
||||
func (s *hlsServer) apiMuxersList() hlsServerAPIMuxersListRes { |
||||
req := hlsServerAPIMuxersListReq{ |
||||
res: make(chan hlsServerAPIMuxersListRes), |
||||
} |
||||
|
||||
select { |
||||
case s.chAPIMuxerList <- req: |
||||
res := <-req.res |
||||
|
||||
res.data = &hlsServerAPIMuxersListData{ |
||||
Items: make(map[string]hlsServerAPIMuxersListItem), |
||||
} |
||||
|
||||
for _, pa := range res.muxers { |
||||
pa.apiMuxersList(hlsServerAPIMuxersListSubReq{data: res.data}) |
||||
} |
||||
|
||||
return res |
||||
|
||||
case <-s.ctx.Done(): |
||||
return hlsServerAPIMuxersListRes{err: fmt.Errorf("terminated")} |
||||
} |
||||
} |
@ -1,25 +0,0 @@
@@ -1,25 +0,0 @@
|
||||
package core |
||||
|
||||
import ( |
||||
"sync" |
||||
|
||||
"github.com/gin-gonic/gin" |
||||
) |
||||
|
||||
type httpRequestPool struct { |
||||
wg sync.WaitGroup |
||||
} |
||||
|
||||
func newHTTPRequestPool() *httpRequestPool { |
||||
return &httpRequestPool{} |
||||
} |
||||
|
||||
func (rp *httpRequestPool) mw(ctx *gin.Context) { |
||||
rp.wg.Add(1) |
||||
ctx.Next() |
||||
rp.wg.Done() |
||||
} |
||||
|
||||
func (rp *httpRequestPool) close() { |
||||
rp.wg.Wait() |
||||
} |
@ -1,73 +0,0 @@
@@ -1,73 +0,0 @@
|
||||
package core |
||||
|
||||
import ( |
||||
"context" |
||||
|
||||
"github.com/pion/webrtc/v3" |
||||
|
||||
"github.com/aler9/mediamtx/internal/websocket" |
||||
) |
||||
|
||||
type webRTCCandidateReader struct { |
||||
ws *websocket.ServerConn |
||||
|
||||
ctx context.Context |
||||
ctxCancel func() |
||||
|
||||
stopGathering chan struct{} |
||||
readError chan error |
||||
remoteCandidate chan *webrtc.ICECandidateInit |
||||
} |
||||
|
||||
func newWebRTCCandidateReader(ws *websocket.ServerConn) *webRTCCandidateReader { |
||||
ctx, ctxCancel := context.WithCancel(context.Background()) |
||||
|
||||
r := &webRTCCandidateReader{ |
||||
ws: ws, |
||||
ctx: ctx, |
||||
ctxCancel: ctxCancel, |
||||
stopGathering: make(chan struct{}), |
||||
readError: make(chan error), |
||||
remoteCandidate: make(chan *webrtc.ICECandidateInit), |
||||
} |
||||
|
||||
go r.run() |
||||
|
||||
return r |
||||
} |
||||
|
||||
func (r *webRTCCandidateReader) close() { |
||||
r.ctxCancel() |
||||
// do not wait for ReadJSON() to return
|
||||
// it is terminated by ws.Close() later
|
||||
} |
||||
|
||||
func (r *webRTCCandidateReader) run() { |
||||
for { |
||||
candidate, err := r.readCandidate() |
||||
if err != nil { |
||||
select { |
||||
case r.readError <- err: |
||||
case <-r.ctx.Done(): |
||||
} |
||||
return |
||||
} |
||||
|
||||
select { |
||||
case r.remoteCandidate <- candidate: |
||||
case <-r.stopGathering: |
||||
case <-r.ctx.Done(): |
||||
return |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (r *webRTCCandidateReader) readCandidate() (*webrtc.ICECandidateInit, error) { |
||||
var candidate webrtc.ICECandidateInit |
||||
err := r.ws.ReadJSON(&candidate) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return &candidate, err |
||||
} |
@ -1,694 +0,0 @@
@@ -1,694 +0,0 @@
|
||||
package core |
||||
|
||||
import ( |
||||
"context" |
||||
"crypto/hmac" |
||||
"crypto/sha1" |
||||
"encoding/base64" |
||||
"errors" |
||||
"fmt" |
||||
"math/rand" |
||||
"net" |
||||
"strconv" |
||||
"strings" |
||||
"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" |
||||
"github.com/pion/sdp/v3" |
||||
"github.com/pion/webrtc/v3" |
||||
|
||||
"github.com/aler9/mediamtx/internal/formatprocessor" |
||||
"github.com/aler9/mediamtx/internal/logger" |
||||
"github.com/aler9/mediamtx/internal/websocket" |
||||
) |
||||
|
||||
const ( |
||||
webrtcHandshakeTimeout = 10 * time.Second |
||||
webrtcTrackGatherTimeout = 2 * time.Second |
||||
webrtcPayloadMaxSize = 1188 // 1200 - 12 (RTP header)
|
||||
) |
||||
|
||||
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 |
||||
} |
||||
|
||||
func insertTias(offer *webrtc.SessionDescription, value uint64) { |
||||
var sd sdp.SessionDescription |
||||
err := sd.Unmarshal([]byte(offer.SDP)) |
||||
if err != nil { |
||||
return |
||||
} |
||||
|
||||
for _, media := range sd.MediaDescriptions { |
||||
if media.MediaName.Media == "video" { |
||||
media.Bandwidth = append(media.Bandwidth, sdp.Bandwidth{ |
||||
Type: "TIAS", |
||||
Bandwidth: value, |
||||
}) |
||||
} |
||||
} |
||||
|
||||
enc, err := sd.Marshal() |
||||
if err != nil { |
||||
return |
||||
} |
||||
|
||||
offer.SDP = string(enc) |
||||
} |
||||
|
||||
type webRTCConnPathManager interface { |
||||
publisherAdd(req pathPublisherAddReq) pathPublisherAnnounceRes |
||||
readerAdd(req pathReaderAddReq) pathReaderSetupPlayRes |
||||
} |
||||
|
||||
type webRTCConnParent interface { |
||||
logger.Writer |
||||
connClose(*webRTCConn) |
||||
} |
||||
|
||||
type webRTCConn struct { |
||||
readBufferCount int |
||||
pathName string |
||||
publish bool |
||||
ws *websocket.ServerConn |
||||
videoCodec string |
||||
audioCodec string |
||||
videoBitrate string |
||||
iceServers []string |
||||
wg *sync.WaitGroup |
||||
pathManager webRTCConnPathManager |
||||
parent webRTCConnParent |
||||
iceUDPMux ice.UDPMux |
||||
iceTCPMux ice.TCPMux |
||||
iceHostNAT1To1IPs []string |
||||
|
||||
ctx context.Context |
||||
ctxCancel func() |
||||
uuid uuid.UUID |
||||
created time.Time |
||||
pc *peerConnection |
||||
mutex sync.RWMutex |
||||
|
||||
closed chan struct{} |
||||
} |
||||
|
||||
func newWebRTCConn( |
||||
parentCtx context.Context, |
||||
readBufferCount int, |
||||
pathName string, |
||||
publish bool, |
||||
ws *websocket.ServerConn, |
||||
videoCodec string, |
||||
audioCodec string, |
||||
videoBitrate string, |
||||
iceServers []string, |
||||
wg *sync.WaitGroup, |
||||
pathManager webRTCConnPathManager, |
||||
parent webRTCConnParent, |
||||
iceHostNAT1To1IPs []string, |
||||
iceUDPMux ice.UDPMux, |
||||
iceTCPMux ice.TCPMux, |
||||
) *webRTCConn { |
||||
ctx, ctxCancel := context.WithCancel(parentCtx) |
||||
|
||||
c := &webRTCConn{ |
||||
readBufferCount: readBufferCount, |
||||
pathName: pathName, |
||||
publish: publish, |
||||
ws: ws, |
||||
iceServers: iceServers, |
||||
wg: wg, |
||||
videoCodec: videoCodec, |
||||
audioCodec: audioCodec, |
||||
videoBitrate: videoBitrate, |
||||
pathManager: pathManager, |
||||
parent: parent, |
||||
ctx: ctx, |
||||
ctxCancel: ctxCancel, |
||||
uuid: uuid.New(), |
||||
created: time.Now(), |
||||
iceUDPMux: iceUDPMux, |
||||
iceTCPMux: iceTCPMux, |
||||
iceHostNAT1To1IPs: iceHostNAT1To1IPs, |
||||
closed: make(chan struct{}), |
||||
} |
||||
|
||||
c.Log(logger.Info, "opened") |
||||
|
||||
wg.Add(1) |
||||
go c.run() |
||||
|
||||
return c |
||||
} |
||||
|
||||
func (c *webRTCConn) close() { |
||||
c.ctxCancel() |
||||
} |
||||
|
||||
func (c *webRTCConn) wait() { |
||||
<-c.closed |
||||
} |
||||
|
||||
func (c *webRTCConn) remoteAddr() net.Addr { |
||||
return c.ws.RemoteAddr() |
||||
} |
||||
|
||||
func (c *webRTCConn) safePC() *peerConnection { |
||||
c.mutex.RLock() |
||||
defer c.mutex.RUnlock() |
||||
return c.pc |
||||
} |
||||
|
||||
func (c *webRTCConn) Log(level logger.Level, format string, args ...interface{}) { |
||||
c.parent.Log(level, "[conn %v] "+format, append([]interface{}{c.ws.RemoteAddr()}, args...)...) |
||||
} |
||||
|
||||
func (c *webRTCConn) run() { |
||||
defer close(c.closed) |
||||
defer c.wg.Done() |
||||
|
||||
innerCtx, innerCtxCancel := context.WithCancel(c.ctx) |
||||
runErr := make(chan error) |
||||
go func() { |
||||
runErr <- c.runInner(innerCtx) |
||||
}() |
||||
|
||||
var err error |
||||
select { |
||||
case err = <-runErr: |
||||
innerCtxCancel() |
||||
|
||||
case <-c.ctx.Done(): |
||||
innerCtxCancel() |
||||
<-runErr |
||||
err = errors.New("terminated") |
||||
} |
||||
|
||||
c.ctxCancel() |
||||
|
||||
c.parent.connClose(c) |
||||
|
||||
c.Log(logger.Info, "closed (%v)", err) |
||||
} |
||||
|
||||
func (c *webRTCConn) runInner(ctx context.Context) error { |
||||
if c.publish { |
||||
return c.runPublish(ctx) |
||||
} |
||||
return c.runRead(ctx) |
||||
} |
||||
|
||||
func (c *webRTCConn) runPublish(ctx context.Context) error { |
||||
res := c.pathManager.publisherAdd(pathPublisherAddReq{ |
||||
author: c, |
||||
pathName: c.pathName, |
||||
skipAuth: true, |
||||
}) |
||||
if res.err != nil { |
||||
return res.err |
||||
} |
||||
|
||||
defer res.path.publisherRemove(pathPublisherRemoveReq{author: c}) |
||||
|
||||
err := c.writeICEServers() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
pc, err := newPeerConnection( |
||||
c.videoCodec, |
||||
c.audioCodec, |
||||
c.genICEServers(), |
||||
c.iceHostNAT1To1IPs, |
||||
c.iceUDPMux, |
||||
c.iceTCPMux, |
||||
c) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
defer pc.close() |
||||
|
||||
_, err = pc.AddTransceiverFromKind(webrtc.RTPCodecTypeVideo, webrtc.RtpTransceiverInit{ |
||||
Direction: webrtc.RTPTransceiverDirectionRecvonly, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = pc.AddTransceiverFromKind(webrtc.RTPCodecTypeAudio, webrtc.RtpTransceiverInit{ |
||||
Direction: webrtc.RTPTransceiverDirectionRecvonly, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
trackRecv := make(chan trackRecvPair) |
||||
|
||||
pc.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) { |
||||
select { |
||||
case trackRecv <- trackRecvPair{track, receiver}: |
||||
case <-pc.closed: |
||||
} |
||||
}) |
||||
|
||||
offer, err := pc.CreateOffer(nil) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = pc.SetLocalDescription(offer) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
tmp, err := strconv.ParseUint(c.videoBitrate, 10, 31) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
insertTias(&offer, tmp*1024) |
||||
|
||||
err = c.writeOffer(&offer) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
answer, err := c.readAnswer() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = pc.SetRemoteDescription(*answer) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
cr := newWebRTCCandidateReader(c.ws) |
||||
defer cr.close() |
||||
|
||||
err = c.establishConnection(ctx, pc, cr) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
close(cr.stopGathering) |
||||
|
||||
tracks, err := c.gatherIncomingTracks(ctx, pc, cr, trackRecv) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
medias := mediasOfIncomingTracks(tracks) |
||||
|
||||
rres := res.path.publisherStart(pathPublisherStartReq{ |
||||
author: c, |
||||
medias: medias, |
||||
generateRTPPackets: false, |
||||
}) |
||||
if rres.err != nil { |
||||
return rres.err |
||||
} |
||||
|
||||
c.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: |
||||
return fmt.Errorf("peer connection closed") |
||||
|
||||
case err := <-cr.readError: |
||||
return fmt.Errorf("websocket error: %v", err) |
||||
|
||||
case <-ctx.Done(): |
||||
return fmt.Errorf("terminated") |
||||
} |
||||
} |
||||
|
||||
func (c *webRTCConn) runRead(ctx context.Context) error { |
||||
res := c.pathManager.readerAdd(pathReaderAddReq{ |
||||
author: c, |
||||
pathName: c.pathName, |
||||
skipAuth: true, |
||||
}) |
||||
if res.err != nil { |
||||
return res.err |
||||
} |
||||
|
||||
defer res.path.readerRemove(pathReaderRemoveReq{author: c}) |
||||
|
||||
tracks, err := c.gatherOutgoingTracks(res.stream.medias()) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = c.writeICEServers() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
offer, err := c.readOffer() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
pc, err := newPeerConnection( |
||||
"", |
||||
"", |
||||
c.genICEServers(), |
||||
c.iceHostNAT1To1IPs, |
||||
c.iceUDPMux, |
||||
c.iceTCPMux, |
||||
c) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
defer pc.close() |
||||
|
||||
for _, track := range tracks { |
||||
var err error |
||||
track.sender, err = pc.AddTrack(track.track) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
|
||||
err = pc.SetRemoteDescription(*offer) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
answer, err := pc.CreateAnswer(nil) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = pc.SetLocalDescription(answer) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = c.writeAnswer(&answer) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
cr := newWebRTCCandidateReader(c.ws) |
||||
defer cr.close() |
||||
|
||||
err = c.establishConnection(ctx, pc, cr) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
close(cr.stopGathering) |
||||
|
||||
for _, track := range tracks { |
||||
track.start() |
||||
} |
||||
|
||||
ringBuffer, _ := ringbuffer.New(uint64(c.readBufferCount)) |
||||
defer ringBuffer.Close() |
||||
|
||||
writeError := make(chan error) |
||||
|
||||
for _, track := range tracks { |
||||
ctrack := track |
||||
res.stream.readerAdd(c, track.media, track.format, func(unit formatprocessor.Unit) { |
||||
ringBuffer.Push(func() { |
||||
ctrack.cb(unit, ctx, writeError) |
||||
}) |
||||
}) |
||||
} |
||||
defer res.stream.readerRemove(c) |
||||
|
||||
c.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: |
||||
return fmt.Errorf("peer connection closed") |
||||
|
||||
case err := <-cr.readError: |
||||
return fmt.Errorf("websocket error: %v", err) |
||||
|
||||
case err := <-writeError: |
||||
return err |
||||
|
||||
case <-ctx.Done(): |
||||
return fmt.Errorf("terminated") |
||||
} |
||||
} |
||||
|
||||
func (c *webRTCConn) 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( |
||||
"the stream doesn't contain any supported codec, which are currently H264, VP8, VP9, G711, G722, Opus") |
||||
} |
||||
|
||||
return tracks, nil |
||||
} |
||||
|
||||
func (c *webRTCConn) gatherIncomingTracks( |
||||
ctx context.Context, |
||||
pc *peerConnection, |
||||
cr *webRTCCandidateReader, |
||||
trackRecv chan trackRecvPair, |
||||
) ([]*webRTCIncomingTrack, error) { |
||||
var tracks []*webRTCIncomingTrack |
||||
|
||||
t := time.NewTimer(webrtcTrackGatherTimeout) |
||||
defer t.Stop() |
||||
|
||||
for { |
||||
select { |
||||
case <-t.C: |
||||
return tracks, nil |
||||
|
||||
case pair := <-trackRecv: |
||||
track, err := newWebRTCIncomingTrack(pair.track, pair.receiver, pc.WriteRTCP) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
tracks = append(tracks, track) |
||||
|
||||
if len(tracks) == 2 { |
||||
return tracks, nil |
||||
} |
||||
|
||||
case <-pc.disconnected: |
||||
return nil, fmt.Errorf("peer connection closed") |
||||
|
||||
case err := <-cr.readError: |
||||
return nil, fmt.Errorf("websocket error: %v", err) |
||||
|
||||
case <-ctx.Done(): |
||||
return nil, fmt.Errorf("terminated") |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (c *webRTCConn) genICEServers() []webrtc.ICEServer { |
||||
ret := make([]webrtc.ICEServer, len(c.iceServers)) |
||||
for i, s := range c.iceServers { |
||||
parts := strings.Split(s, ":") |
||||
if len(parts) == 5 { |
||||
if parts[1] == "AUTH_SECRET" { |
||||
s := webrtc.ICEServer{ |
||||
URLs: []string{parts[0] + ":" + parts[3] + ":" + parts[4]}, |
||||
} |
||||
|
||||
randomUser := func() string { |
||||
const charset = "abcdefghijklmnopqrstuvwxyz1234567890" |
||||
b := make([]byte, 20) |
||||
for i := range b { |
||||
b[i] = charset[rand.Intn(len(charset))] |
||||
} |
||||
return string(b) |
||||
}() |
||||
|
||||
expireDate := time.Now().Add(24 * 3600 * time.Second).Unix() |
||||
s.Username = strconv.FormatInt(expireDate, 10) + ":" + randomUser |
||||
|
||||
h := hmac.New(sha1.New, []byte(parts[2])) |
||||
h.Write([]byte(s.Username)) |
||||
s.Credential = base64.StdEncoding.EncodeToString(h.Sum(nil)) |
||||
|
||||
ret[i] = s |
||||
} else { |
||||
ret[i] = webrtc.ICEServer{ |
||||
URLs: []string{parts[0] + ":" + parts[3] + ":" + parts[4]}, |
||||
Username: parts[1], |
||||
Credential: parts[2], |
||||
} |
||||
} |
||||
} else { |
||||
ret[i] = webrtc.ICEServer{ |
||||
URLs: []string{s}, |
||||
} |
||||
} |
||||
} |
||||
return ret |
||||
} |
||||
|
||||
func (c *webRTCConn) establishConnection( |
||||
ctx context.Context, |
||||
pc *peerConnection, |
||||
cr *webRTCCandidateReader, |
||||
) error { |
||||
t := time.NewTimer(webrtcHandshakeTimeout) |
||||
defer t.Stop() |
||||
|
||||
outer: |
||||
for { |
||||
select { |
||||
case candidate := <-pc.localCandidateRecv: |
||||
c.Log(logger.Debug, "local candidate: %+v", candidate.Candidate) |
||||
err := c.ws.WriteJSON(candidate) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
case candidate := <-cr.remoteCandidate: |
||||
c.Log(logger.Debug, "remote candidate: %+v", candidate.Candidate) |
||||
err := pc.AddICECandidate(*candidate) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
case err := <-cr.readError: |
||||
return err |
||||
|
||||
case <-t.C: |
||||
return fmt.Errorf("deadline exceeded") |
||||
|
||||
case <-pc.connected: |
||||
break outer |
||||
|
||||
case <-ctx.Done(): |
||||
return fmt.Errorf("terminated") |
||||
} |
||||
} |
||||
|
||||
// Keep WebSocket connection open and use it to notify shutdowns.
|
||||
// This is because pion/webrtc doesn't write yet a WebRTC shutdown
|
||||
// message to clients (like a DTLS close alert or a RTCP BYE),
|
||||
// therefore browsers do not properly detect shutdowns and do not
|
||||
// attempt to restart the connection immediately.
|
||||
|
||||
c.mutex.Lock() |
||||
c.pc = pc |
||||
c.mutex.Unlock() |
||||
|
||||
c.Log(logger.Info, "peer connection established, local candidate: %v, remote candidate: %v", |
||||
pc.localCandidate(), pc.remoteCandidate()) |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func (c *webRTCConn) writeICEServers() error { |
||||
return c.ws.WriteJSON(c.genICEServers()) |
||||
} |
||||
|
||||
func (c *webRTCConn) readOffer() (*webrtc.SessionDescription, error) { |
||||
var offer webrtc.SessionDescription |
||||
err := c.ws.ReadJSON(&offer) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
if offer.Type != webrtc.SDPTypeOffer { |
||||
return nil, fmt.Errorf("received SDP is not an offer") |
||||
} |
||||
|
||||
return &offer, nil |
||||
} |
||||
|
||||
func (c *webRTCConn) writeOffer(offer *webrtc.SessionDescription) error { |
||||
return c.ws.WriteJSON(offer) |
||||
} |
||||
|
||||
func (c *webRTCConn) readAnswer() (*webrtc.SessionDescription, error) { |
||||
var answer webrtc.SessionDescription |
||||
err := c.ws.ReadJSON(&answer) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
if answer.Type != webrtc.SDPTypeAnswer { |
||||
return nil, fmt.Errorf("received SDP is not an offer") |
||||
} |
||||
|
||||
return &answer, nil |
||||
} |
||||
|
||||
func (c *webRTCConn) writeAnswer(answer *webrtc.SessionDescription) error { |
||||
return c.ws.WriteJSON(answer) |
||||
} |
||||
|
||||
// apiSourceDescribe implements sourceStaticImpl.
|
||||
func (c *webRTCConn) apiSourceDescribe() pathAPISourceOrReader { |
||||
return pathAPISourceOrReader{ |
||||
Type: "webRTCConn", |
||||
ID: c.uuid.String(), |
||||
} |
||||
} |
||||
|
||||
// apiReaderDescribe implements reader.
|
||||
func (c *webRTCConn) apiReaderDescribe() pathAPISourceOrReader { |
||||
return c.apiSourceDescribe() |
||||
} |
@ -0,0 +1,364 @@
@@ -0,0 +1,364 @@
|
||||
package core |
||||
|
||||
import ( |
||||
"context" |
||||
"crypto/tls" |
||||
_ "embed" |
||||
"fmt" |
||||
"io" |
||||
"log" |
||||
"net" |
||||
"net/http" |
||||
"strconv" |
||||
"strings" |
||||
"time" |
||||
|
||||
"github.com/gin-gonic/gin" |
||||
"github.com/google/uuid" |
||||
"github.com/pion/sdp/v3" |
||||
"github.com/pion/webrtc/v3" |
||||
|
||||
"github.com/aler9/mediamtx/internal/conf" |
||||
"github.com/aler9/mediamtx/internal/logger" |
||||
) |
||||
|
||||
//go:embed webrtc_publish_index.html
|
||||
var webrtcPublishIndex []byte |
||||
|
||||
//go:embed webrtc_read_index.html
|
||||
var webrtcReadIndex []byte |
||||
|
||||
func unmarshalICEFragment(buf []byte) ([]*webrtc.ICECandidateInit, error) { |
||||
buf = append([]byte("v=0\r\no=- 0 0 IN IP4 0.0.0.0\r\ns=-\r\nt=0 0\r\n"), buf...) |
||||
|
||||
var sdp sdp.SessionDescription |
||||
err := sdp.Unmarshal(buf) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
usernameFragment, ok := sdp.Attribute("ice-ufrag") |
||||
if !ok { |
||||
return nil, fmt.Errorf("ice-ufrag attribute is missing") |
||||
} |
||||
|
||||
var ret []*webrtc.ICECandidateInit |
||||
|
||||
for _, media := range sdp.MediaDescriptions { |
||||
mid, ok := media.Attribute("mid") |
||||
if !ok { |
||||
return nil, fmt.Errorf("mid attribute is missing") |
||||
} |
||||
|
||||
tmp, err := strconv.ParseUint(mid, 10, 16) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("invalid mid attribute") |
||||
} |
||||
midNum := uint16(tmp) |
||||
|
||||
for _, attr := range media.Attributes { |
||||
if attr.Key == "candidate" { |
||||
ret = append(ret, &webrtc.ICECandidateInit{ |
||||
Candidate: attr.Value, |
||||
SDPMid: &mid, |
||||
SDPMLineIndex: &midNum, |
||||
UsernameFragment: &usernameFragment, |
||||
}) |
||||
} |
||||
} |
||||
} |
||||
|
||||
return ret, nil |
||||
} |
||||
|
||||
func marshalICEFragment(offer *webrtc.SessionDescription, candidates []*webrtc.ICECandidateInit) ([]byte, error) { |
||||
var sdp sdp.SessionDescription |
||||
err := sdp.Unmarshal([]byte(offer.SDP)) |
||||
if err != nil || len(sdp.MediaDescriptions) == 0 { |
||||
return nil, err |
||||
} |
||||
|
||||
firstMedia := sdp.MediaDescriptions[0] |
||||
iceUfrag, _ := firstMedia.Attribute("ice-ufrag") |
||||
icePwd, _ := firstMedia.Attribute("ice-pwd") |
||||
|
||||
candidatesByMedia := make(map[uint16][]*webrtc.ICECandidateInit) |
||||
for _, candidate := range candidates { |
||||
mid := *candidate.SDPMLineIndex |
||||
candidatesByMedia[mid] = append(candidatesByMedia[mid], candidate) |
||||
} |
||||
|
||||
frag := "a=ice-ufrag:" + iceUfrag + "\r\n" + |
||||
"a=ice-pwd:" + icePwd + "\r\n" |
||||
|
||||
for mid, media := range sdp.MediaDescriptions { |
||||
cbm, ok := candidatesByMedia[uint16(mid)] |
||||
if ok { |
||||
frag += "m=" + media.MediaName.String() + "\r\n" + |
||||
"a=mid:" + strconv.FormatUint(uint64(mid), 10) + "\r\n" |
||||
|
||||
for _, candidate := range cbm { |
||||
frag += "a=" + candidate.Candidate + "\r\n" |
||||
} |
||||
} |
||||
} |
||||
|
||||
return []byte(frag), nil |
||||
} |
||||
|
||||
type webRTCHTTPServerParent interface { |
||||
logger.Writer |
||||
genICEServers() []webrtc.ICEServer |
||||
sessionNew(req webRTCSessionNewReq) webRTCNewSessionRes |
||||
sessionAddCandidates(req webRTCSessionAddCandidatesReq) webRTCSessionAddCandidatesRes |
||||
} |
||||
|
||||
type webRTCHTTPServer struct { |
||||
allowOrigin string |
||||
pathManager *pathManager |
||||
parent webRTCHTTPServerParent |
||||
|
||||
ln net.Listener |
||||
inner *http.Server |
||||
} |
||||
|
||||
func newWebRTCHTTPServer( |
||||
address string, |
||||
encryption bool, |
||||
serverKey string, |
||||
serverCert string, |
||||
allowOrigin string, |
||||
trustedProxies conf.IPsOrCIDRs, |
||||
readTimeout conf.StringDuration, |
||||
pathManager *pathManager, |
||||
parent webRTCHTTPServerParent, |
||||
) (*webRTCHTTPServer, error) { |
||||
ln, err := net.Listen(restrictNetwork("tcp", address)) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
var tlsConfig *tls.Config |
||||
if encryption { |
||||
crt, err := tls.LoadX509KeyPair(serverCert, serverKey) |
||||
if err != nil { |
||||
ln.Close() |
||||
return nil, err |
||||
} |
||||
|
||||
tlsConfig = &tls.Config{ |
||||
Certificates: []tls.Certificate{crt}, |
||||
} |
||||
} |
||||
|
||||
s := &webRTCHTTPServer{ |
||||
allowOrigin: allowOrigin, |
||||
pathManager: pathManager, |
||||
parent: parent, |
||||
ln: ln, |
||||
} |
||||
|
||||
router := gin.New() |
||||
httpSetTrustedProxies(router, trustedProxies) |
||||
router.NoRoute(httpLoggerMiddleware(s), httpServerHeaderMiddleware, s.onRequest) |
||||
|
||||
s.inner = &http.Server{ |
||||
Handler: router, |
||||
TLSConfig: tlsConfig, |
||||
ReadHeaderTimeout: time.Duration(readTimeout), |
||||
ErrorLog: log.New(&nilWriter{}, "", 0), |
||||
} |
||||
|
||||
if tlsConfig != nil { |
||||
go s.inner.ServeTLS(s.ln, "", "") |
||||
} else { |
||||
go s.inner.Serve(s.ln) |
||||
} |
||||
|
||||
return s, nil |
||||
} |
||||
|
||||
func (s *webRTCHTTPServer) Log(level logger.Level, format string, args ...interface{}) { |
||||
s.parent.Log(level, format, args...) |
||||
} |
||||
|
||||
func (s *webRTCHTTPServer) close() { |
||||
s.inner.Shutdown(context.Background()) |
||||
s.ln.Close() // in case Shutdown() is called before Serve()
|
||||
} |
||||
|
||||
func (s *webRTCHTTPServer) onRequest(ctx *gin.Context) { |
||||
ctx.Writer.Header().Set("Access-Control-Allow-Origin", s.allowOrigin) |
||||
ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true") |
||||
|
||||
// remove leading prefix
|
||||
pa := ctx.Request.URL.Path[1:] |
||||
|
||||
if !strings.HasSuffix(pa, "/whip") && !strings.HasSuffix(pa, "/whep") { |
||||
switch ctx.Request.Method { |
||||
case http.MethodGet: |
||||
|
||||
case http.MethodOptions: |
||||
ctx.Writer.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS") |
||||
ctx.Writer.Header().Set("Access-Control-Allow-Headers", ctx.Request.Header.Get("Access-Control-Request-Headers")) |
||||
ctx.Writer.WriteHeader(http.StatusOK) |
||||
return |
||||
|
||||
default: |
||||
return |
||||
} |
||||
} |
||||
|
||||
var dir string |
||||
var fname string |
||||
var publish bool |
||||
|
||||
switch { |
||||
case pa == "favicon.ico": |
||||
return |
||||
|
||||
case strings.HasSuffix(pa, "/publish"): |
||||
dir, fname = pa[:len(pa)-len("/publish")], "publish" |
||||
publish = true |
||||
|
||||
case strings.HasSuffix(pa, "/whip"): |
||||
dir, fname = pa[:len(pa)-len("/whip")], "whip" |
||||
publish = true |
||||
|
||||
case strings.HasSuffix(pa, "/whep"): |
||||
dir, fname = pa[:len(pa)-len("/whep")], "whep" |
||||
publish = false |
||||
|
||||
default: |
||||
dir, fname = pa, "" |
||||
publish = false |
||||
|
||||
if !strings.HasSuffix(dir, "/") { |
||||
ctx.Writer.Header().Set("Location", "/"+dir+"/") |
||||
ctx.Writer.WriteHeader(http.StatusMovedPermanently) |
||||
return |
||||
} |
||||
} |
||||
|
||||
dir = strings.TrimSuffix(dir, "/") |
||||
if dir == "" { |
||||
return |
||||
} |
||||
|
||||
user, pass, hasCredentials := ctx.Request.BasicAuth() |
||||
|
||||
res := s.pathManager.getPathConf(pathGetPathConfReq{ |
||||
name: dir, |
||||
publish: publish, |
||||
credentials: authCredentials{ |
||||
query: ctx.Request.URL.RawQuery, |
||||
ip: net.ParseIP(ctx.ClientIP()), |
||||
user: user, |
||||
pass: pass, |
||||
proto: authProtocolWebRTC, |
||||
}, |
||||
}) |
||||
if res.err != nil { |
||||
if terr, ok := res.err.(pathErrAuth); ok { |
||||
if !hasCredentials { |
||||
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`) |
||||
ctx.Writer.WriteHeader(http.StatusUnauthorized) |
||||
return |
||||
} |
||||
|
||||
s.Log(logger.Info, "authentication error: %v", terr.wrapped) |
||||
ctx.Writer.WriteHeader(http.StatusUnauthorized) |
||||
return |
||||
} |
||||
|
||||
ctx.Writer.WriteHeader(http.StatusNotFound) |
||||
return |
||||
} |
||||
|
||||
switch fname { |
||||
case "": |
||||
ctx.Writer.Header().Set("Content-Type", "text/html") |
||||
ctx.Writer.WriteHeader(http.StatusOK) |
||||
ctx.Writer.Write(webrtcReadIndex) |
||||
|
||||
case "publish": |
||||
ctx.Writer.Header().Set("Content-Type", "text/html") |
||||
ctx.Writer.WriteHeader(http.StatusOK) |
||||
ctx.Writer.Write(webrtcPublishIndex) |
||||
|
||||
case "whip", "whep": |
||||
switch ctx.Request.Method { |
||||
case http.MethodOptions: |
||||
ctx.Writer.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS") |
||||
ctx.Writer.Header().Set("Access-Control-Allow-Headers", ctx.Request.Header.Get("Access-Control-Request-Headers")) |
||||
ctx.Writer.Header()["Link"] = iceServersToLinkHeader(s.parent.genICEServers()) |
||||
ctx.Writer.WriteHeader(http.StatusOK) |
||||
|
||||
case http.MethodPost: |
||||
if ctx.Request.Header.Get("Content-Type") != "application/sdp" { |
||||
ctx.Writer.WriteHeader(http.StatusBadRequest) |
||||
return |
||||
} |
||||
|
||||
offer, err := io.ReadAll(ctx.Request.Body) |
||||
if err != nil { |
||||
return |
||||
} |
||||
|
||||
res := s.parent.sessionNew(webRTCSessionNewReq{ |
||||
pathName: dir, |
||||
remoteAddr: ctx.ClientIP(), |
||||
offer: offer, |
||||
publish: (fname == "whip"), |
||||
videoCodec: ctx.Query("video_codec"), |
||||
audioCodec: ctx.Query("audio_codec"), |
||||
videoBitrate: ctx.Query("video_bitrate"), |
||||
}) |
||||
if res.err != nil { |
||||
ctx.Writer.WriteHeader(http.StatusInternalServerError) |
||||
return |
||||
} |
||||
|
||||
ctx.Writer.Header().Set("Content-Type", "application/sdp") |
||||
ctx.Writer.Header().Set("E-Tag", res.sx.secret.String()) |
||||
ctx.Writer.Header().Set("Accept-Patch", "application/trickle-ice-sdpfrag") |
||||
ctx.Writer.Header()["Link"] = iceServersToLinkHeader(s.parent.genICEServers()) |
||||
ctx.Writer.WriteHeader(http.StatusCreated) |
||||
ctx.Writer.Write(res.answer) |
||||
|
||||
case http.MethodPatch: |
||||
secret, err := uuid.Parse(ctx.Request.Header.Get("If-Match")) |
||||
if err != nil { |
||||
ctx.Writer.WriteHeader(http.StatusBadRequest) |
||||
return |
||||
} |
||||
|
||||
if ctx.Request.Header.Get("Content-Type") != "application/trickle-ice-sdpfrag" { |
||||
ctx.Writer.WriteHeader(http.StatusBadRequest) |
||||
return |
||||
} |
||||
|
||||
byts, err := io.ReadAll(ctx.Request.Body) |
||||
if err != nil { |
||||
return |
||||
} |
||||
|
||||
candidates, err := unmarshalICEFragment(byts) |
||||
if err != nil { |
||||
ctx.Writer.WriteHeader(http.StatusBadRequest) |
||||
return |
||||
} |
||||
|
||||
res := s.parent.sessionAddCandidates(webRTCSessionAddCandidatesReq{ |
||||
secret: secret, |
||||
candidates: candidates, |
||||
}) |
||||
if res.err != nil { |
||||
ctx.Writer.WriteHeader(http.StatusBadRequest) |
||||
return |
||||
} |
||||
|
||||
ctx.Writer.WriteHeader(http.StatusNoContent) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,508 @@
@@ -0,0 +1,508 @@
|
||||
package core |
||||
|
||||
import ( |
||||
"context" |
||||
"crypto/hmac" |
||||
"crypto/sha1" |
||||
"encoding/base64" |
||||
"fmt" |
||||
"math/rand" |
||||
"net" |
||||
"regexp" |
||||
"strconv" |
||||
"strings" |
||||
"sync" |
||||
"time" |
||||
|
||||
"github.com/google/uuid" |
||||
"github.com/pion/ice/v2" |
||||
"github.com/pion/webrtc/v3" |
||||
|
||||
"github.com/aler9/mediamtx/internal/conf" |
||||
"github.com/aler9/mediamtx/internal/logger" |
||||
) |
||||
|
||||
func iceServersToLinkHeader(iceServers []webrtc.ICEServer) []string { |
||||
ret := make([]string, len(iceServers)) |
||||
|
||||
for i, server := range iceServers { |
||||
link := "<" + server.URLs[0] + ">; rel=\"ice-server\"" |
||||
if server.Username != "" { |
||||
link += "; username=\"" + server.Username + "\"" + |
||||
"; credential=\"" + server.Credential.(string) + "\"; credential-type=\"password\"" |
||||
} |
||||
ret[i] = link |
||||
} |
||||
|
||||
return ret |
||||
} |
||||
|
||||
var reLink = regexp.MustCompile(`^<(.+?)>; rel="ice-server"(; username="(.+?)"` + |
||||
`; credential="(.+?)"; credential-type="password")?`) |
||||
|
||||
func linkHeaderToIceServers(link []string) []webrtc.ICEServer { |
||||
var ret []webrtc.ICEServer |
||||
|
||||
for _, li := range link { |
||||
m := reLink.FindStringSubmatch(li) |
||||
if m != nil { |
||||
s := webrtc.ICEServer{ |
||||
URLs: []string{m[1]}, |
||||
} |
||||
|
||||
if m[3] != "" { |
||||
s.Username = m[3] |
||||
s.Credential = m[4] |
||||
s.CredentialType = webrtc.ICECredentialTypePassword |
||||
} |
||||
|
||||
ret = append(ret, s) |
||||
} |
||||
} |
||||
|
||||
return ret |
||||
} |
||||
|
||||
type webRTCManagerAPISessionsListItem struct { |
||||
Created time.Time `json:"created"` |
||||
RemoteAddr string `json:"remoteAddr"` |
||||
PeerConnectionEstablished bool `json:"peerConnectionEstablished"` |
||||
LocalCandidate string `json:"localCandidate"` |
||||
RemoteCandidate string `json:"remoteCandidate"` |
||||
State string `json:"state"` |
||||
BytesReceived uint64 `json:"bytesReceived"` |
||||
BytesSent uint64 `json:"bytesSent"` |
||||
} |
||||
|
||||
type webRTCManagerAPISessionsListData struct { |
||||
Items map[string]webRTCManagerAPISessionsListItem `json:"items"` |
||||
} |
||||
|
||||
type webRTCManagerAPISessionsListRes struct { |
||||
data *webRTCManagerAPISessionsListData |
||||
err error |
||||
} |
||||
|
||||
type webRTCManagerAPISessionsListReq struct { |
||||
res chan webRTCManagerAPISessionsListRes |
||||
} |
||||
|
||||
type webRTCManagerAPISessionsKickRes struct { |
||||
err error |
||||
} |
||||
|
||||
type webRTCManagerAPISessionsKickReq struct { |
||||
uuid uuid.UUID |
||||
res chan webRTCManagerAPISessionsKickRes |
||||
} |
||||
|
||||
type webRTCNewSessionRes struct { |
||||
sx *webRTCSession |
||||
answer []byte |
||||
err error |
||||
} |
||||
|
||||
type webRTCSessionNewReq struct { |
||||
pathName string |
||||
remoteAddr string |
||||
offer []byte |
||||
publish bool |
||||
videoCodec string |
||||
audioCodec string |
||||
videoBitrate string |
||||
res chan webRTCNewSessionRes |
||||
} |
||||
|
||||
type webRTCSessionAddCandidatesRes struct { |
||||
sx *webRTCSession |
||||
err error |
||||
} |
||||
|
||||
type webRTCSessionAddCandidatesReq struct { |
||||
secret uuid.UUID |
||||
candidates []*webrtc.ICECandidateInit |
||||
res chan webRTCSessionAddCandidatesRes |
||||
} |
||||
|
||||
type webRTCManagerParent interface { |
||||
logger.Writer |
||||
} |
||||
|
||||
type webRTCManager struct { |
||||
allowOrigin string |
||||
trustedProxies conf.IPsOrCIDRs |
||||
iceServers []string |
||||
readBufferCount int |
||||
pathManager *pathManager |
||||
metrics *metrics |
||||
parent webRTCManagerParent |
||||
|
||||
ctx context.Context |
||||
ctxCancel func() |
||||
httpServer *webRTCHTTPServer |
||||
udpMuxLn net.PacketConn |
||||
tcpMuxLn net.Listener |
||||
sessions map[*webRTCSession]struct{} |
||||
sessionsBySecret map[uuid.UUID]*webRTCSession |
||||
iceHostNAT1To1IPs []string |
||||
iceUDPMux ice.UDPMux |
||||
iceTCPMux ice.TCPMux |
||||
|
||||
// in
|
||||
chSessionNew chan webRTCSessionNewReq |
||||
chSessionClose chan *webRTCSession |
||||
chSessionAddCandidates chan webRTCSessionAddCandidatesReq |
||||
chAPISessionsList chan webRTCManagerAPISessionsListReq |
||||
chAPIConnsKick chan webRTCManagerAPISessionsKickReq |
||||
|
||||
// out
|
||||
done chan struct{} |
||||
} |
||||
|
||||
func newWebRTCManager( |
||||
parentCtx context.Context, |
||||
address string, |
||||
encryption bool, |
||||
serverKey string, |
||||
serverCert string, |
||||
allowOrigin string, |
||||
trustedProxies conf.IPsOrCIDRs, |
||||
iceServers []string, |
||||
readTimeout conf.StringDuration, |
||||
readBufferCount int, |
||||
pathManager *pathManager, |
||||
metrics *metrics, |
||||
parent webRTCManagerParent, |
||||
iceHostNAT1To1IPs []string, |
||||
iceUDPMuxAddress string, |
||||
iceTCPMuxAddress string, |
||||
) (*webRTCManager, error) { |
||||
ctx, ctxCancel := context.WithCancel(parentCtx) |
||||
|
||||
m := &webRTCManager{ |
||||
allowOrigin: allowOrigin, |
||||
trustedProxies: trustedProxies, |
||||
iceServers: iceServers, |
||||
readBufferCount: readBufferCount, |
||||
pathManager: pathManager, |
||||
metrics: metrics, |
||||
parent: parent, |
||||
ctx: ctx, |
||||
ctxCancel: ctxCancel, |
||||
iceHostNAT1To1IPs: iceHostNAT1To1IPs, |
||||
sessions: make(map[*webRTCSession]struct{}), |
||||
sessionsBySecret: make(map[uuid.UUID]*webRTCSession), |
||||
chSessionNew: make(chan webRTCSessionNewReq), |
||||
chSessionClose: make(chan *webRTCSession), |
||||
chSessionAddCandidates: make(chan webRTCSessionAddCandidatesReq), |
||||
chAPISessionsList: make(chan webRTCManagerAPISessionsListReq), |
||||
chAPIConnsKick: make(chan webRTCManagerAPISessionsKickReq), |
||||
done: make(chan struct{}), |
||||
} |
||||
|
||||
var err error |
||||
m.httpServer, err = newWebRTCHTTPServer( |
||||
address, |
||||
encryption, |
||||
serverKey, |
||||
serverCert, |
||||
allowOrigin, |
||||
trustedProxies, |
||||
readTimeout, |
||||
pathManager, |
||||
m, |
||||
) |
||||
if err != nil { |
||||
ctxCancel() |
||||
return nil, err |
||||
} |
||||
|
||||
if iceUDPMuxAddress != "" { |
||||
m.udpMuxLn, err = net.ListenPacket(restrictNetwork("udp", iceUDPMuxAddress)) |
||||
if err != nil { |
||||
m.httpServer.close() |
||||
ctxCancel() |
||||
return nil, err |
||||
} |
||||
m.iceUDPMux = webrtc.NewICEUDPMux(nil, m.udpMuxLn) |
||||
} |
||||
|
||||
if iceTCPMuxAddress != "" { |
||||
m.tcpMuxLn, err = net.Listen(restrictNetwork("tcp", iceTCPMuxAddress)) |
||||
if err != nil { |
||||
m.udpMuxLn.Close() |
||||
m.httpServer.close() |
||||
ctxCancel() |
||||
return nil, err |
||||
} |
||||
m.iceTCPMux = webrtc.NewICETCPMux(nil, m.tcpMuxLn, 8) |
||||
} |
||||
|
||||
str := "listener opened on " + address + " (HTTP)" |
||||
if m.udpMuxLn != nil { |
||||
str += ", " + iceUDPMuxAddress + " (ICE/UDP)" |
||||
} |
||||
if m.tcpMuxLn != nil { |
||||
str += ", " + iceTCPMuxAddress + " (ICE/TCP)" |
||||
} |
||||
m.Log(logger.Info, str) |
||||
|
||||
if m.metrics != nil { |
||||
m.metrics.webRTCManagerSet(m) |
||||
} |
||||
|
||||
go m.run() |
||||
|
||||
return m, nil |
||||
} |
||||
|
||||
// Log is the main logging function.
|
||||
func (m *webRTCManager) Log(level logger.Level, format string, args ...interface{}) { |
||||
m.parent.Log(level, "[WebRTC] "+format, append([]interface{}{}, args...)...) |
||||
} |
||||
|
||||
func (m *webRTCManager) close() { |
||||
m.Log(logger.Info, "listener is closing") |
||||
m.ctxCancel() |
||||
<-m.done |
||||
} |
||||
|
||||
func (m *webRTCManager) run() { |
||||
defer close(m.done) |
||||
|
||||
var wg sync.WaitGroup |
||||
|
||||
outer: |
||||
for { |
||||
select { |
||||
case req := <-m.chSessionNew: |
||||
sx := newWebRTCSession( |
||||
m.ctx, |
||||
m.readBufferCount, |
||||
req, |
||||
&wg, |
||||
m.iceHostNAT1To1IPs, |
||||
m.iceUDPMux, |
||||
m.iceTCPMux, |
||||
m.pathManager, |
||||
m, |
||||
) |
||||
m.sessions[sx] = struct{}{} |
||||
m.sessionsBySecret[sx.secret] = sx |
||||
req.res <- webRTCNewSessionRes{sx: sx} |
||||
|
||||
case sx := <-m.chSessionClose: |
||||
delete(m.sessions, sx) |
||||
delete(m.sessionsBySecret, sx.secret) |
||||
|
||||
case req := <-m.chSessionAddCandidates: |
||||
sx, ok := m.sessionsBySecret[req.secret] |
||||
if !ok { |
||||
req.res <- webRTCSessionAddCandidatesRes{err: fmt.Errorf("session not found")} |
||||
continue |
||||
} |
||||
|
||||
req.res <- webRTCSessionAddCandidatesRes{sx: sx} |
||||
|
||||
case req := <-m.chAPISessionsList: |
||||
data := &webRTCManagerAPISessionsListData{ |
||||
Items: make(map[string]webRTCManagerAPISessionsListItem), |
||||
} |
||||
|
||||
for sx := range m.sessions { |
||||
peerConnectionEstablished := false |
||||
localCandidate := "" |
||||
remoteCandidate := "" |
||||
bytesReceived := uint64(0) |
||||
bytesSent := uint64(0) |
||||
|
||||
pc := sx.safePC() |
||||
if pc != nil { |
||||
peerConnectionEstablished = true |
||||
localCandidate = pc.localCandidate() |
||||
remoteCandidate = pc.remoteCandidate() |
||||
bytesReceived = pc.bytesReceived() |
||||
bytesSent = pc.bytesSent() |
||||
} |
||||
|
||||
data.Items[sx.uuid.String()] = webRTCManagerAPISessionsListItem{ |
||||
Created: sx.created, |
||||
RemoteAddr: sx.req.remoteAddr, |
||||
PeerConnectionEstablished: peerConnectionEstablished, |
||||
LocalCandidate: localCandidate, |
||||
RemoteCandidate: remoteCandidate, |
||||
State: func() string { |
||||
if sx.req.publish { |
||||
return "publish" |
||||
} |
||||
return "read" |
||||
}(), |
||||
BytesReceived: bytesReceived, |
||||
BytesSent: bytesSent, |
||||
} |
||||
} |
||||
|
||||
req.res <- webRTCManagerAPISessionsListRes{data: data} |
||||
|
||||
case req := <-m.chAPIConnsKick: |
||||
sx := m.findSessionByUUID(req.uuid) |
||||
if sx == nil { |
||||
req.res <- webRTCManagerAPISessionsKickRes{fmt.Errorf("not found")} |
||||
continue |
||||
} |
||||
|
||||
delete(m.sessions, sx) |
||||
delete(m.sessionsBySecret, sx.secret) |
||||
sx.close() |
||||
req.res <- webRTCManagerAPISessionsKickRes{} |
||||
|
||||
case <-m.ctx.Done(): |
||||
break outer |
||||
} |
||||
} |
||||
|
||||
m.ctxCancel() |
||||
|
||||
wg.Wait() |
||||
|
||||
m.httpServer.close() |
||||
|
||||
if m.udpMuxLn != nil { |
||||
m.udpMuxLn.Close() |
||||
} |
||||
|
||||
if m.tcpMuxLn != nil { |
||||
m.tcpMuxLn.Close() |
||||
} |
||||
} |
||||
|
||||
func (m *webRTCManager) findSessionByUUID(uuid uuid.UUID) *webRTCSession { |
||||
for sx := range m.sessions { |
||||
if sx.uuid == uuid { |
||||
return sx |
||||
} |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func (m *webRTCManager) genICEServers() []webrtc.ICEServer { |
||||
ret := make([]webrtc.ICEServer, len(m.iceServers)) |
||||
for i, s := range m.iceServers { |
||||
parts := strings.Split(s, ":") |
||||
if len(parts) == 5 { |
||||
if parts[1] == "AUTH_SECRET" { |
||||
s := webrtc.ICEServer{ |
||||
URLs: []string{parts[0] + ":" + parts[3] + ":" + parts[4]}, |
||||
} |
||||
|
||||
randomUser := func() string { |
||||
const charset = "abcdefghijklmnopqrstuvwxyz1234567890" |
||||
b := make([]byte, 20) |
||||
for i := range b { |
||||
b[i] = charset[rand.Intn(len(charset))] |
||||
} |
||||
return string(b) |
||||
}() |
||||
|
||||
expireDate := time.Now().Add(24 * 3600 * time.Second).Unix() |
||||
s.Username = strconv.FormatInt(expireDate, 10) + ":" + randomUser |
||||
|
||||
h := hmac.New(sha1.New, []byte(parts[2])) |
||||
h.Write([]byte(s.Username)) |
||||
s.Credential = base64.StdEncoding.EncodeToString(h.Sum(nil)) |
||||
|
||||
ret[i] = s |
||||
} else { |
||||
ret[i] = webrtc.ICEServer{ |
||||
URLs: []string{parts[0] + ":" + parts[3] + ":" + parts[4]}, |
||||
Username: parts[1], |
||||
Credential: parts[2], |
||||
} |
||||
} |
||||
} else { |
||||
ret[i] = webrtc.ICEServer{ |
||||
URLs: []string{s}, |
||||
} |
||||
} |
||||
} |
||||
return ret |
||||
} |
||||
|
||||
// sessionNew is called by webRTCHTTPServer.
|
||||
func (m *webRTCManager) sessionNew(req webRTCSessionNewReq) webRTCNewSessionRes { |
||||
req.res = make(chan webRTCNewSessionRes) |
||||
|
||||
select { |
||||
case m.chSessionNew <- req: |
||||
res1 := <-req.res |
||||
|
||||
select { |
||||
case res2 := <-req.res: |
||||
return res2 |
||||
|
||||
case <-res1.sx.ctx.Done(): |
||||
return webRTCNewSessionRes{err: fmt.Errorf("terminated")} |
||||
} |
||||
|
||||
case <-m.ctx.Done(): |
||||
return webRTCNewSessionRes{err: fmt.Errorf("terminated")} |
||||
} |
||||
} |
||||
|
||||
// sessionClose is called by webRTCSession.
|
||||
func (m *webRTCManager) sessionClose(sx *webRTCSession) { |
||||
select { |
||||
case m.chSessionClose <- sx: |
||||
case <-m.ctx.Done(): |
||||
} |
||||
} |
||||
|
||||
// sessionAddCandidates is called by webRTCHTTPServer.
|
||||
func (m *webRTCManager) sessionAddCandidates( |
||||
req webRTCSessionAddCandidatesReq, |
||||
) webRTCSessionAddCandidatesRes { |
||||
req.res = make(chan webRTCSessionAddCandidatesRes) |
||||
select { |
||||
case m.chSessionAddCandidates <- req: |
||||
res1 := <-req.res |
||||
if res1.err != nil { |
||||
return res1 |
||||
} |
||||
|
||||
return res1.sx.addRemoteCandidates(req) |
||||
|
||||
case <-m.ctx.Done(): |
||||
return webRTCSessionAddCandidatesRes{err: fmt.Errorf("terminated")} |
||||
} |
||||
} |
||||
|
||||
// apiSessionsList is called by api.
|
||||
func (m *webRTCManager) apiSessionsList() webRTCManagerAPISessionsListRes { |
||||
req := webRTCManagerAPISessionsListReq{ |
||||
res: make(chan webRTCManagerAPISessionsListRes), |
||||
} |
||||
|
||||
select { |
||||
case m.chAPISessionsList <- req: |
||||
return <-req.res |
||||
|
||||
case <-m.ctx.Done(): |
||||
return webRTCManagerAPISessionsListRes{err: fmt.Errorf("terminated")} |
||||
} |
||||
} |
||||
|
||||
// apiSessionsKick is called by api.
|
||||
func (m *webRTCManager) apiSessionsKick(uuid uuid.UUID) webRTCManagerAPISessionsKickRes { |
||||
req := webRTCManagerAPISessionsKickReq{ |
||||
uuid: uuid, |
||||
res: make(chan webRTCManagerAPISessionsKickRes), |
||||
} |
||||
|
||||
select { |
||||
case m.chAPIConnsKick <- req: |
||||
return <-req.res |
||||
|
||||
case <-m.ctx.Done(): |
||||
return webRTCManagerAPISessionsKickRes{err: fmt.Errorf("terminated")} |
||||
} |
||||
} |
@ -0,0 +1,353 @@
@@ -0,0 +1,353 @@
|
||||
package core |
||||
|
||||
import ( |
||||
"bytes" |
||||
"encoding/json" |
||||
"net/http" |
||||
"sync" |
||||
"testing" |
||||
"time" |
||||
|
||||
"github.com/bluenviron/gortsplib/v3" |
||||
"github.com/bluenviron/gortsplib/v3/pkg/formats" |
||||
"github.com/bluenviron/gortsplib/v3/pkg/media" |
||||
"github.com/bluenviron/gortsplib/v3/pkg/url" |
||||
"github.com/pion/rtp" |
||||
"github.com/pion/webrtc/v3" |
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
func whipGetICEServers(t *testing.T, ur string) []webrtc.ICEServer { |
||||
req, err := http.NewRequest("OPTIONS", ur, nil) |
||||
require.NoError(t, err) |
||||
|
||||
res, err := http.DefaultClient.Do(req) |
||||
require.NoError(t, err) |
||||
defer res.Body.Close() |
||||
|
||||
require.Equal(t, http.StatusOK, res.StatusCode) |
||||
|
||||
link, ok := res.Header["Link"] |
||||
require.Equal(t, true, ok) |
||||
servers := linkHeaderToIceServers(link) |
||||
require.NotEqual(t, 0, len(servers)) |
||||
|
||||
return servers |
||||
} |
||||
|
||||
func whipPostOffer(t *testing.T, ur string, offer *webrtc.SessionDescription) (*webrtc.SessionDescription, string) { |
||||
enc, err := json.Marshal(offer) |
||||
require.NoError(t, err) |
||||
|
||||
req, err := http.NewRequest("POST", ur, bytes.NewReader(enc)) |
||||
require.NoError(t, err) |
||||
|
||||
req.Header.Set("Content-Type", "application/sdp") |
||||
|
||||
res, err := http.DefaultClient.Do(req) |
||||
require.NoError(t, err) |
||||
defer res.Body.Close() |
||||
|
||||
require.Equal(t, http.StatusCreated, res.StatusCode) |
||||
|
||||
link, ok := res.Header["Link"] |
||||
require.Equal(t, true, ok) |
||||
servers := linkHeaderToIceServers(link) |
||||
require.NotEqual(t, 0, len(servers)) |
||||
|
||||
require.Equal(t, "application/sdp", res.Header.Get("Content-Type")) |
||||
etag := res.Header.Get("E-Tag") |
||||
require.NotEqual(t, 0, len(etag)) |
||||
require.Equal(t, "application/trickle-ice-sdpfrag", res.Header.Get("Accept-Patch")) |
||||
|
||||
var answer webrtc.SessionDescription |
||||
err = json.NewDecoder(res.Body).Decode(&answer) |
||||
require.NoError(t, err) |
||||
|
||||
return &answer, etag |
||||
} |
||||
|
||||
func whipPostCandidate(t *testing.T, ur string, offer *webrtc.SessionDescription, |
||||
etag string, candidate *webrtc.ICECandidateInit, |
||||
) { |
||||
frag, err := marshalICEFragment(offer, []*webrtc.ICECandidateInit{candidate}) |
||||
require.NoError(t, err) |
||||
|
||||
req, err := http.NewRequest("PATCH", ur, bytes.NewReader(frag)) |
||||
require.NoError(t, err) |
||||
|
||||
req.Header.Set("Content-Type", "application/trickle-ice-sdpfrag") |
||||
req.Header.Set("If-Match", etag) |
||||
|
||||
res, err := http.DefaultClient.Do(req) |
||||
require.NoError(t, err) |
||||
defer res.Body.Close() |
||||
|
||||
require.Equal(t, http.StatusNoContent, res.StatusCode) |
||||
} |
||||
|
||||
type webRTCTestClient struct { |
||||
pc *webrtc.PeerConnection |
||||
outgoingTrack1 *webrtc.TrackLocalStaticRTP |
||||
outgoingTrack2 *webrtc.TrackLocalStaticRTP |
||||
incomingTrack chan *webrtc.TrackRemote |
||||
closed chan struct{} |
||||
} |
||||
|
||||
func newWebRTCTestClient(t *testing.T, ur string, publish bool) *webRTCTestClient { |
||||
iceServers := whipGetICEServers(t, ur) |
||||
|
||||
pc, err := webrtc.NewPeerConnection(webrtc.Configuration{ |
||||
ICEServers: iceServers, |
||||
}) |
||||
require.NoError(t, err) |
||||
|
||||
connected := make(chan struct{}) |
||||
closed := make(chan struct{}) |
||||
var stateChangeMutex sync.Mutex |
||||
|
||||
pc.OnConnectionStateChange(func(state webrtc.PeerConnectionState) { |
||||
stateChangeMutex.Lock() |
||||
defer stateChangeMutex.Unlock() |
||||
|
||||
select { |
||||
case <-closed: |
||||
return |
||||
default: |
||||
} |
||||
|
||||
switch state { |
||||
case webrtc.PeerConnectionStateConnected: |
||||
close(connected) |
||||
|
||||
case webrtc.PeerConnectionStateClosed: |
||||
close(closed) |
||||
} |
||||
}) |
||||
|
||||
var outgoingTrack1 *webrtc.TrackLocalStaticRTP |
||||
var outgoingTrack2 *webrtc.TrackLocalStaticRTP |
||||
var incomingTrack chan *webrtc.TrackRemote |
||||
|
||||
if publish { |
||||
var err error |
||||
outgoingTrack1, err = webrtc.NewTrackLocalStaticRTP( |
||||
webrtc.RTPCodecCapability{ |
||||
MimeType: webrtc.MimeTypeVP8, |
||||
ClockRate: 90000, |
||||
}, |
||||
"vp8", |
||||
webrtcStreamID, |
||||
) |
||||
require.NoError(t, err) |
||||
|
||||
_, err = pc.AddTrack(outgoingTrack1) |
||||
require.NoError(t, err) |
||||
|
||||
outgoingTrack2, err = webrtc.NewTrackLocalStaticRTP( |
||||
webrtc.RTPCodecCapability{ |
||||
MimeType: webrtc.MimeTypeOpus, |
||||
ClockRate: 48000, |
||||
Channels: 2, |
||||
}, |
||||
"opus", |
||||
webrtcStreamID, |
||||
) |
||||
require.NoError(t, err) |
||||
|
||||
_, err = pc.AddTrack(outgoingTrack2) |
||||
require.NoError(t, err) |
||||
} else { |
||||
incomingTrack = make(chan *webrtc.TrackRemote, 1) |
||||
pc.OnTrack(func(trak *webrtc.TrackRemote, recv *webrtc.RTPReceiver) { |
||||
incomingTrack <- trak |
||||
}) |
||||
|
||||
_, err = pc.AddTransceiverFromKind(webrtc.RTPCodecTypeVideo) |
||||
require.NoError(t, err) |
||||
} |
||||
|
||||
offer, err := pc.CreateOffer(nil) |
||||
require.NoError(t, err) |
||||
|
||||
answer, etag := whipPostOffer(t, ur, &offer) |
||||
|
||||
// test adding additional candidates, even if it is not mandatory here
|
||||
gatheringDone := make(chan struct{}) |
||||
pc.OnICECandidate(func(i *webrtc.ICECandidate) { |
||||
if i != nil { |
||||
c := i.ToJSON() |
||||
whipPostCandidate(t, ur, &offer, etag, &c) |
||||
} else { |
||||
close(gatheringDone) |
||||
} |
||||
}) |
||||
|
||||
err = pc.SetLocalDescription(offer) |
||||
require.NoError(t, err) |
||||
|
||||
err = pc.SetRemoteDescription(*answer) |
||||
require.NoError(t, err) |
||||
|
||||
<-gatheringDone |
||||
<-connected |
||||
|
||||
if publish { |
||||
time.Sleep(200 * time.Millisecond) |
||||
|
||||
err := outgoingTrack1.WriteRTP(&rtp.Packet{ |
||||
Header: rtp.Header{ |
||||
Version: 2, |
||||
Marker: true, |
||||
PayloadType: 96, |
||||
SequenceNumber: 123, |
||||
Timestamp: 45343, |
||||
SSRC: 563423, |
||||
}, |
||||
Payload: []byte{0x01, 0x02, 0x03, 0x04}, |
||||
}) |
||||
require.NoError(t, err) |
||||
|
||||
err = outgoingTrack2.WriteRTP(&rtp.Packet{ |
||||
Header: rtp.Header{ |
||||
Version: 2, |
||||
Marker: true, |
||||
PayloadType: 96, |
||||
SequenceNumber: 1123, |
||||
Timestamp: 45343, |
||||
SSRC: 563423, |
||||
}, |
||||
Payload: []byte{0x01, 0x02, 0x03, 0x04}, |
||||
}) |
||||
require.NoError(t, err) |
||||
|
||||
time.Sleep(200 * time.Millisecond) |
||||
} |
||||
|
||||
return &webRTCTestClient{ |
||||
pc: pc, |
||||
outgoingTrack1: outgoingTrack1, |
||||
outgoingTrack2: outgoingTrack2, |
||||
incomingTrack: incomingTrack, |
||||
closed: closed, |
||||
} |
||||
} |
||||
|
||||
func (c *webRTCTestClient) close() { |
||||
c.pc.Close() |
||||
<-c.closed |
||||
} |
||||
|
||||
func TestWebRTCRead(t *testing.T) { |
||||
p, ok := newInstance("paths:\n" + |
||||
" all:\n") |
||||
require.Equal(t, true, ok) |
||||
defer p.Close() |
||||
|
||||
medi := &media.Media{ |
||||
Type: media.TypeVideo, |
||||
Formats: []formats.Format{&formats.H264{ |
||||
PayloadTyp: 96, |
||||
PacketizationMode: 1, |
||||
}}, |
||||
} |
||||
|
||||
v := gortsplib.TransportTCP |
||||
source := gortsplib.Client{ |
||||
Transport: &v, |
||||
} |
||||
err := source.StartRecording("rtsp://localhost:8554/stream", media.Medias{medi}) |
||||
require.NoError(t, err) |
||||
defer source.Close() |
||||
|
||||
c := newWebRTCTestClient(t, "http://localhost:8889/stream/whep", false) |
||||
defer c.close() |
||||
|
||||
time.Sleep(500 * time.Millisecond) |
||||
|
||||
source.WritePacketRTP(medi, &rtp.Packet{ |
||||
Header: rtp.Header{ |
||||
Version: 2, |
||||
Marker: true, |
||||
PayloadType: 96, |
||||
SequenceNumber: 123, |
||||
Timestamp: 45343, |
||||
SSRC: 563423, |
||||
}, |
||||
Payload: []byte{0x01, 0x02, 0x03, 0x04}, |
||||
}) |
||||
|
||||
trak := <-c.incomingTrack |
||||
|
||||
pkt, _, err := trak.ReadRTP() |
||||
require.NoError(t, err) |
||||
require.Equal(t, &rtp.Packet{ |
||||
Header: rtp.Header{ |
||||
Version: 2, |
||||
Marker: true, |
||||
PayloadType: 102, |
||||
SequenceNumber: pkt.SequenceNumber, |
||||
Timestamp: pkt.Timestamp, |
||||
SSRC: pkt.SSRC, |
||||
CSRC: []uint32{}, |
||||
}, |
||||
Payload: []byte{0x01, 0x02, 0x03, 0x04}, |
||||
}, pkt) |
||||
} |
||||
|
||||
func TestWebRTCPublish(t *testing.T) { |
||||
p, ok := newInstance("paths:\n" + |
||||
" all:\n") |
||||
require.Equal(t, true, ok) |
||||
defer p.Close() |
||||
|
||||
s := newWebRTCTestClient(t, "http://localhost:8889/stream/whip", true) |
||||
defer s.close() |
||||
|
||||
c := gortsplib.Client{ |
||||
OnDecodeError: func(err error) { |
||||
panic(err) |
||||
}, |
||||
} |
||||
|
||||
u, err := url.Parse("rtsp://127.0.0.1:8554/stream") |
||||
require.NoError(t, err) |
||||
|
||||
err = c.Start(u.Scheme, u.Host) |
||||
require.NoError(t, err) |
||||
defer c.Close() |
||||
|
||||
medias, baseURL, _, err := c.Describe(u) |
||||
require.NoError(t, err) |
||||
|
||||
var forma *formats.VP8 |
||||
medi := medias.FindFormat(&forma) |
||||
|
||||
_, err = c.Setup(medi, baseURL, 0, 0) |
||||
require.NoError(t, err) |
||||
|
||||
received := make(chan struct{}) |
||||
|
||||
c.OnPacketRTP(medi, forma, func(pkt *rtp.Packet) { |
||||
require.Equal(t, []byte{0x05, 0x06, 0x07, 0x08}, pkt.Payload) |
||||
close(received) |
||||
}) |
||||
|
||||
_, err = c.Play(nil) |
||||
require.NoError(t, err) |
||||
|
||||
err = s.outgoingTrack1.WriteRTP(&rtp.Packet{ |
||||
Header: rtp.Header{ |
||||
Version: 2, |
||||
Marker: true, |
||||
PayloadType: 96, |
||||
SequenceNumber: 124, |
||||
Timestamp: 45343, |
||||
SSRC: 563423, |
||||
}, |
||||
Payload: []byte{0x05, 0x06, 0x07, 0x08}, |
||||
}) |
||||
require.NoError(t, err) |
||||
|
||||
<-received |
||||
} |
@ -1,522 +0,0 @@
@@ -1,522 +0,0 @@
|
||||
package core |
||||
|
||||
import ( |
||||
"context" |
||||
"crypto/tls" |
||||
_ "embed" |
||||
"fmt" |
||||
"log" |
||||
"net" |
||||
"net/http" |
||||
"strings" |
||||
"sync" |
||||
"time" |
||||
|
||||
"github.com/gin-gonic/gin" |
||||
"github.com/pion/ice/v2" |
||||
"github.com/pion/webrtc/v3" |
||||
|
||||
"github.com/aler9/mediamtx/internal/conf" |
||||
"github.com/aler9/mediamtx/internal/logger" |
||||
"github.com/aler9/mediamtx/internal/websocket" |
||||
) |
||||
|
||||
//go:embed webrtc_publish_index.html
|
||||
var webrtcPublishIndex []byte |
||||
|
||||
//go:embed webrtc_read_index.html
|
||||
var webrtcReadIndex []byte |
||||
|
||||
type webRTCServerAPIConnsListItem struct { |
||||
Created time.Time `json:"created"` |
||||
RemoteAddr string `json:"remoteAddr"` |
||||
PeerConnectionEstablished bool `json:"peerConnectionEstablished"` |
||||
LocalCandidate string `json:"localCandidate"` |
||||
RemoteCandidate string `json:"remoteCandidate"` |
||||
State string `json:"state"` |
||||
BytesReceived uint64 `json:"bytesReceived"` |
||||
BytesSent uint64 `json:"bytesSent"` |
||||
} |
||||
|
||||
type webRTCServerAPIConnsListData struct { |
||||
Items map[string]webRTCServerAPIConnsListItem `json:"items"` |
||||
} |
||||
|
||||
type webRTCServerAPIConnsListRes struct { |
||||
data *webRTCServerAPIConnsListData |
||||
err error |
||||
} |
||||
|
||||
type webRTCServerAPIConnsListReq struct { |
||||
res chan webRTCServerAPIConnsListRes |
||||
} |
||||
|
||||
type webRTCServerAPIConnsKickRes struct { |
||||
err error |
||||
} |
||||
|
||||
type webRTCServerAPIConnsKickReq struct { |
||||
id string |
||||
res chan webRTCServerAPIConnsKickRes |
||||
} |
||||
|
||||
type webRTCConnNewReq struct { |
||||
pathName string |
||||
publish bool |
||||
wsconn *websocket.ServerConn |
||||
res chan *webRTCConn |
||||
videoCodec string |
||||
audioCodec string |
||||
videoBitrate string |
||||
} |
||||
|
||||
type webRTCServerParent interface { |
||||
logger.Writer |
||||
} |
||||
|
||||
type webRTCServer struct { |
||||
allowOrigin string |
||||
trustedProxies conf.IPsOrCIDRs |
||||
iceServers []string |
||||
readBufferCount int |
||||
pathManager *pathManager |
||||
metrics *metrics |
||||
parent webRTCServerParent |
||||
|
||||
ctx context.Context |
||||
ctxCancel func() |
||||
ln net.Listener |
||||
requestPool *httpRequestPool |
||||
httpServer *http.Server |
||||
udpMuxLn net.PacketConn |
||||
tcpMuxLn net.Listener |
||||
conns map[*webRTCConn]struct{} |
||||
iceHostNAT1To1IPs []string |
||||
iceUDPMux ice.UDPMux |
||||
iceTCPMux ice.TCPMux |
||||
|
||||
// in
|
||||
connNew chan webRTCConnNewReq |
||||
chConnClose chan *webRTCConn |
||||
chAPIConnsList chan webRTCServerAPIConnsListReq |
||||
chAPIConnsKick chan webRTCServerAPIConnsKickReq |
||||
|
||||
// out
|
||||
done chan struct{} |
||||
} |
||||
|
||||
func newWebRTCServer( |
||||
parentCtx context.Context, |
||||
address string, |
||||
encryption bool, |
||||
serverKey string, |
||||
serverCert string, |
||||
allowOrigin string, |
||||
trustedProxies conf.IPsOrCIDRs, |
||||
iceServers []string, |
||||
readTimeout conf.StringDuration, |
||||
readBufferCount int, |
||||
pathManager *pathManager, |
||||
metrics *metrics, |
||||
parent webRTCServerParent, |
||||
iceHostNAT1To1IPs []string, |
||||
iceUDPMuxAddress string, |
||||
iceTCPMuxAddress string, |
||||
) (*webRTCServer, error) { |
||||
ln, err := net.Listen(restrictNetwork("tcp", address)) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
var tlsConfig *tls.Config |
||||
if encryption { |
||||
crt, err := tls.LoadX509KeyPair(serverCert, serverKey) |
||||
if err != nil { |
||||
ln.Close() |
||||
return nil, err |
||||
} |
||||
|
||||
tlsConfig = &tls.Config{ |
||||
Certificates: []tls.Certificate{crt}, |
||||
} |
||||
} |
||||
|
||||
var iceUDPMux ice.UDPMux |
||||
var udpMuxLn net.PacketConn |
||||
if iceUDPMuxAddress != "" { |
||||
udpMuxLn, err = net.ListenPacket(restrictNetwork("udp", iceUDPMuxAddress)) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
iceUDPMux = webrtc.NewICEUDPMux(nil, udpMuxLn) |
||||
} |
||||
|
||||
var iceTCPMux ice.TCPMux |
||||
var tcpMuxLn net.Listener |
||||
if iceTCPMuxAddress != "" { |
||||
tcpMuxLn, err = net.Listen(restrictNetwork("tcp", iceTCPMuxAddress)) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
iceTCPMux = webrtc.NewICETCPMux(nil, tcpMuxLn, 8) |
||||
} |
||||
|
||||
ctx, ctxCancel := context.WithCancel(parentCtx) |
||||
|
||||
s := &webRTCServer{ |
||||
allowOrigin: allowOrigin, |
||||
trustedProxies: trustedProxies, |
||||
iceServers: iceServers, |
||||
readBufferCount: readBufferCount, |
||||
pathManager: pathManager, |
||||
metrics: metrics, |
||||
parent: parent, |
||||
ctx: ctx, |
||||
ctxCancel: ctxCancel, |
||||
ln: ln, |
||||
udpMuxLn: udpMuxLn, |
||||
tcpMuxLn: tcpMuxLn, |
||||
iceUDPMux: iceUDPMux, |
||||
iceTCPMux: iceTCPMux, |
||||
iceHostNAT1To1IPs: iceHostNAT1To1IPs, |
||||
conns: make(map[*webRTCConn]struct{}), |
||||
connNew: make(chan webRTCConnNewReq), |
||||
chConnClose: make(chan *webRTCConn), |
||||
chAPIConnsList: make(chan webRTCServerAPIConnsListReq), |
||||
chAPIConnsKick: make(chan webRTCServerAPIConnsKickReq), |
||||
done: make(chan struct{}), |
||||
} |
||||
|
||||
s.requestPool = newHTTPRequestPool() |
||||
|
||||
router := gin.New() |
||||
httpSetTrustedProxies(router, trustedProxies) |
||||
|
||||
router.NoRoute(s.requestPool.mw, httpLoggerMiddleware(s), httpServerHeaderMiddleware, s.onRequest) |
||||
|
||||
s.httpServer = &http.Server{ |
||||
Handler: router, |
||||
TLSConfig: tlsConfig, |
||||
ReadHeaderTimeout: time.Duration(readTimeout), |
||||
ErrorLog: log.New(&nilWriter{}, "", 0), |
||||
} |
||||
|
||||
str := "listener opened on " + address + " (HTTP)" |
||||
if udpMuxLn != nil { |
||||
str += ", " + iceUDPMuxAddress + " (ICE/UDP)" |
||||
} |
||||
if tcpMuxLn != nil { |
||||
str += ", " + iceTCPMuxAddress + " (ICE/TCP)" |
||||
} |
||||
s.Log(logger.Info, str) |
||||
|
||||
if s.metrics != nil { |
||||
s.metrics.webRTCServerSet(s) |
||||
} |
||||
|
||||
go s.run() |
||||
|
||||
return s, nil |
||||
} |
||||
|
||||
// Log is the main logging function.
|
||||
func (s *webRTCServer) Log(level logger.Level, format string, args ...interface{}) { |
||||
s.parent.Log(level, "[WebRTC] "+format, append([]interface{}{}, args...)...) |
||||
} |
||||
|
||||
func (s *webRTCServer) close() { |
||||
s.Log(logger.Info, "listener is closing") |
||||
s.ctxCancel() |
||||
<-s.done |
||||
} |
||||
|
||||
func (s *webRTCServer) run() { |
||||
defer close(s.done) |
||||
|
||||
if s.httpServer.TLSConfig != nil { |
||||
go s.httpServer.ServeTLS(s.ln, "", "") |
||||
} else { |
||||
go s.httpServer.Serve(s.ln) |
||||
} |
||||
|
||||
var wg sync.WaitGroup |
||||
|
||||
outer: |
||||
for { |
||||
select { |
||||
case req := <-s.connNew: |
||||
c := newWebRTCConn( |
||||
s.ctx, |
||||
s.readBufferCount, |
||||
req.pathName, |
||||
req.publish, |
||||
req.wsconn, |
||||
req.videoCodec, |
||||
req.audioCodec, |
||||
req.videoBitrate, |
||||
s.iceServers, |
||||
&wg, |
||||
s.pathManager, |
||||
s, |
||||
s.iceHostNAT1To1IPs, |
||||
s.iceUDPMux, |
||||
s.iceTCPMux, |
||||
) |
||||
s.conns[c] = struct{}{} |
||||
req.res <- c |
||||
|
||||
case conn := <-s.chConnClose: |
||||
delete(s.conns, conn) |
||||
|
||||
case req := <-s.chAPIConnsList: |
||||
data := &webRTCServerAPIConnsListData{ |
||||
Items: make(map[string]webRTCServerAPIConnsListItem), |
||||
} |
||||
|
||||
for c := range s.conns { |
||||
peerConnectionEstablished := false |
||||
localCandidate := "" |
||||
remoteCandidate := "" |
||||
bytesReceived := uint64(0) |
||||
bytesSent := uint64(0) |
||||
|
||||
pc := c.safePC() |
||||
if pc != nil { |
||||
peerConnectionEstablished = true |
||||
localCandidate = pc.localCandidate() |
||||
remoteCandidate = pc.remoteCandidate() |
||||
bytesReceived = pc.bytesReceived() |
||||
bytesSent = pc.bytesSent() |
||||
} |
||||
|
||||
data.Items[c.uuid.String()] = webRTCServerAPIConnsListItem{ |
||||
Created: c.created, |
||||
RemoteAddr: c.remoteAddr().String(), |
||||
PeerConnectionEstablished: peerConnectionEstablished, |
||||
LocalCandidate: localCandidate, |
||||
RemoteCandidate: remoteCandidate, |
||||
State: func() string { |
||||
if c.publish { |
||||
return "publish" |
||||
} |
||||
return "read" |
||||
}(), |
||||
BytesReceived: bytesReceived, |
||||
BytesSent: bytesSent, |
||||
} |
||||
} |
||||
|
||||
req.res <- webRTCServerAPIConnsListRes{data: data} |
||||
|
||||
case req := <-s.chAPIConnsKick: |
||||
res := func() bool { |
||||
for c := range s.conns { |
||||
if c.uuid.String() == req.id { |
||||
delete(s.conns, c) |
||||
c.close() |
||||
return true |
||||
} |
||||
} |
||||
return false |
||||
}() |
||||
if res { |
||||
req.res <- webRTCServerAPIConnsKickRes{} |
||||
} else { |
||||
req.res <- webRTCServerAPIConnsKickRes{fmt.Errorf("not found")} |
||||
} |
||||
|
||||
case <-s.ctx.Done(): |
||||
break outer |
||||
} |
||||
} |
||||
|
||||
s.ctxCancel() |
||||
|
||||
s.httpServer.Shutdown(context.Background()) |
||||
s.ln.Close() // in case Shutdown() is called before Serve()
|
||||
|
||||
s.requestPool.close() |
||||
wg.Wait() |
||||
|
||||
if s.udpMuxLn != nil { |
||||
s.udpMuxLn.Close() |
||||
} |
||||
|
||||
if s.tcpMuxLn != nil { |
||||
s.tcpMuxLn.Close() |
||||
} |
||||
} |
||||
|
||||
func (s *webRTCServer) onRequest(ctx *gin.Context) { |
||||
ctx.Writer.Header().Set("Access-Control-Allow-Origin", s.allowOrigin) |
||||
ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true") |
||||
|
||||
switch ctx.Request.Method { |
||||
case http.MethodGet: |
||||
|
||||
case http.MethodOptions: |
||||
ctx.Writer.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS") |
||||
ctx.Writer.Header().Set("Access-Control-Allow-Headers", ctx.Request.Header.Get("Access-Control-Request-Headers")) |
||||
ctx.Writer.WriteHeader(http.StatusOK) |
||||
return |
||||
|
||||
default: |
||||
return |
||||
} |
||||
|
||||
// remove leading prefix
|
||||
pa := ctx.Request.URL.Path[1:] |
||||
|
||||
var dir string |
||||
var fname string |
||||
var publish bool |
||||
|
||||
switch { |
||||
case strings.HasSuffix(pa, "/publish/ws"): |
||||
dir = pa[:len(pa)-len("/publish/ws")] |
||||
fname = "publish/ws" |
||||
publish = true |
||||
|
||||
case strings.HasSuffix(pa, "/publish"): |
||||
dir = pa[:len(pa)-len("/publish")] |
||||
fname = "publish" |
||||
publish = true |
||||
|
||||
case strings.HasSuffix(pa, "/ws"): |
||||
dir = pa[:len(pa)-len("/ws")] |
||||
fname = "ws" |
||||
publish = false |
||||
|
||||
case pa == "favicon.ico": |
||||
return |
||||
|
||||
default: |
||||
dir = pa |
||||
fname = "" |
||||
publish = false |
||||
|
||||
if !strings.HasSuffix(dir, "/") { |
||||
ctx.Writer.Header().Set("Location", "/"+dir+"/") |
||||
ctx.Writer.WriteHeader(http.StatusMovedPermanently) |
||||
return |
||||
} |
||||
} |
||||
|
||||
dir = strings.TrimSuffix(dir, "/") |
||||
if dir == "" { |
||||
return |
||||
} |
||||
|
||||
user, pass, hasCredentials := ctx.Request.BasicAuth() |
||||
|
||||
res := s.pathManager.getPathConf(pathGetPathConfReq{ |
||||
name: dir, |
||||
publish: publish, |
||||
credentials: authCredentials{ |
||||
query: ctx.Request.URL.RawQuery, |
||||
ip: net.ParseIP(ctx.ClientIP()), |
||||
user: user, |
||||
pass: pass, |
||||
proto: authProtocolWebRTC, |
||||
}, |
||||
}) |
||||
if res.err != nil { |
||||
if terr, ok := res.err.(pathErrAuth); ok { |
||||
if !hasCredentials { |
||||
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`) |
||||
ctx.Writer.WriteHeader(http.StatusUnauthorized) |
||||
return |
||||
} |
||||
|
||||
s.Log(logger.Info, "authentication error: %v", terr.wrapped) |
||||
ctx.Writer.WriteHeader(http.StatusUnauthorized) |
||||
return |
||||
} |
||||
|
||||
ctx.Writer.WriteHeader(http.StatusNotFound) |
||||
return |
||||
} |
||||
|
||||
switch fname { |
||||
case "": |
||||
ctx.Writer.Header().Set("Content-Type", "text/html") |
||||
ctx.Writer.WriteHeader(http.StatusOK) |
||||
ctx.Writer.Write(webrtcReadIndex) |
||||
|
||||
case "publish": |
||||
ctx.Writer.Header().Set("Content-Type", "text/html") |
||||
ctx.Writer.WriteHeader(http.StatusOK) |
||||
ctx.Writer.Write(webrtcPublishIndex) |
||||
|
||||
case "ws", "publish/ws": |
||||
wsconn, err := websocket.NewServerConn(ctx.Writer, ctx.Request) |
||||
if err != nil { |
||||
return |
||||
} |
||||
defer wsconn.Close() |
||||
|
||||
c := s.newConn(webRTCConnNewReq{ |
||||
pathName: dir, |
||||
publish: (fname == "publish/ws"), |
||||
wsconn: wsconn, |
||||
videoCodec: ctx.Query("video_codec"), |
||||
audioCodec: ctx.Query("audio_codec"), |
||||
videoBitrate: ctx.Query("video_bitrate"), |
||||
}) |
||||
if c == nil { |
||||
return |
||||
} |
||||
|
||||
c.wait() |
||||
} |
||||
} |
||||
|
||||
func (s *webRTCServer) newConn(req webRTCConnNewReq) *webRTCConn { |
||||
req.res = make(chan *webRTCConn) |
||||
|
||||
select { |
||||
case s.connNew <- req: |
||||
return <-req.res |
||||
case <-s.ctx.Done(): |
||||
return nil |
||||
} |
||||
} |
||||
|
||||
// connClose is called by webRTCConn.
|
||||
func (s *webRTCServer) connClose(c *webRTCConn) { |
||||
select { |
||||
case s.chConnClose <- c: |
||||
case <-s.ctx.Done(): |
||||
} |
||||
} |
||||
|
||||
// apiConnsList is called by api.
|
||||
func (s *webRTCServer) apiConnsList() webRTCServerAPIConnsListRes { |
||||
req := webRTCServerAPIConnsListReq{ |
||||
res: make(chan webRTCServerAPIConnsListRes), |
||||
} |
||||
|
||||
select { |
||||
case s.chAPIConnsList <- req: |
||||
return <-req.res |
||||
|
||||
case <-s.ctx.Done(): |
||||
return webRTCServerAPIConnsListRes{err: fmt.Errorf("terminated")} |
||||
} |
||||
} |
||||
|
||||
// apiConnsKick is called by api.
|
||||
func (s *webRTCServer) apiConnsKick(id string) webRTCServerAPIConnsKickRes { |
||||
req := webRTCServerAPIConnsKickReq{ |
||||
id: id, |
||||
res: make(chan webRTCServerAPIConnsKickRes), |
||||
} |
||||
|
||||
select { |
||||
case s.chAPIConnsKick <- req: |
||||
return <-req.res |
||||
|
||||
case <-s.ctx.Done(): |
||||
return webRTCServerAPIConnsKickRes{err: fmt.Errorf("terminated")} |
||||
} |
||||
} |
@ -1,235 +0,0 @@
@@ -1,235 +0,0 @@
|
||||
package core |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"sync" |
||||
"testing" |
||||
"time" |
||||
|
||||
"github.com/bluenviron/gortsplib/v3" |
||||
"github.com/bluenviron/gortsplib/v3/pkg/formats" |
||||
"github.com/bluenviron/gortsplib/v3/pkg/media" |
||||
"github.com/gorilla/websocket" |
||||
"github.com/pion/rtp" |
||||
"github.com/pion/webrtc/v3" |
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
type webRTCTestClient struct { |
||||
wc *websocket.Conn |
||||
pc *webrtc.PeerConnection |
||||
track chan *webrtc.TrackRemote |
||||
closed chan struct{} |
||||
} |
||||
|
||||
func newWebRTCTestClient(addr string) (*webRTCTestClient, error) { |
||||
wc, res, err := websocket.DefaultDialer.Dial(addr, nil) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
defer res.Body.Close() |
||||
|
||||
_, msg, err := wc.ReadMessage() |
||||
if err != nil { |
||||
wc.Close() |
||||
return nil, err |
||||
} |
||||
|
||||
var iceServers []webrtc.ICEServer |
||||
err = json.Unmarshal(msg, &iceServers) |
||||
if err != nil { |
||||
wc.Close() |
||||
return nil, err |
||||
} |
||||
|
||||
pc, err := webrtc.NewPeerConnection(webrtc.Configuration{ |
||||
ICEServers: iceServers, |
||||
}) |
||||
if err != nil { |
||||
wc.Close() |
||||
return nil, err |
||||
} |
||||
|
||||
pc.OnICECandidate(func(i *webrtc.ICECandidate) { |
||||
if i != nil { |
||||
enc, _ := json.Marshal(i.ToJSON()) |
||||
wc.WriteMessage(websocket.TextMessage, enc) |
||||
} |
||||
}) |
||||
|
||||
connected := make(chan struct{}) |
||||
closed := make(chan struct{}) |
||||
var stateChangeMutex sync.Mutex |
||||
|
||||
pc.OnConnectionStateChange(func(state webrtc.PeerConnectionState) { |
||||
stateChangeMutex.Lock() |
||||
defer stateChangeMutex.Unlock() |
||||
|
||||
select { |
||||
case <-closed: |
||||
return |
||||
default: |
||||
} |
||||
|
||||
switch state { |
||||
case webrtc.PeerConnectionStateConnected: |
||||
close(connected) |
||||
|
||||
case webrtc.PeerConnectionStateClosed: |
||||
close(closed) |
||||
} |
||||
}) |
||||
|
||||
track := make(chan *webrtc.TrackRemote, 1) |
||||
|
||||
pc.OnTrack(func(trak *webrtc.TrackRemote, recv *webrtc.RTPReceiver) { |
||||
track <- trak |
||||
}) |
||||
|
||||
_, err = pc.AddTransceiverFromKind(webrtc.RTPCodecTypeVideo) |
||||
if err != nil { |
||||
wc.Close() |
||||
pc.Close() |
||||
return nil, err |
||||
} |
||||
|
||||
localOffer, err := pc.CreateOffer(nil) |
||||
if err != nil { |
||||
wc.Close() |
||||
pc.Close() |
||||
return nil, err |
||||
} |
||||
|
||||
enc, err := json.Marshal(localOffer) |
||||
if err != nil { |
||||
wc.Close() |
||||
pc.Close() |
||||
return nil, err |
||||
} |
||||
|
||||
err = wc.WriteMessage(websocket.TextMessage, enc) |
||||
if err != nil { |
||||
wc.Close() |
||||
pc.Close() |
||||
return nil, err |
||||
} |
||||
|
||||
err = pc.SetLocalDescription(localOffer) |
||||
if err != nil { |
||||
wc.Close() |
||||
pc.Close() |
||||
return nil, err |
||||
} |
||||
|
||||
_, msg, err = wc.ReadMessage() |
||||
if err != nil { |
||||
wc.Close() |
||||
pc.Close() |
||||
return nil, err |
||||
} |
||||
|
||||
var remoteOffer webrtc.SessionDescription |
||||
err = json.Unmarshal(msg, &remoteOffer) |
||||
if err != nil { |
||||
wc.Close() |
||||
pc.Close() |
||||
return nil, err |
||||
} |
||||
|
||||
err = pc.SetRemoteDescription(remoteOffer) |
||||
if err != nil { |
||||
wc.Close() |
||||
pc.Close() |
||||
return nil, err |
||||
} |
||||
|
||||
go func() { |
||||
for { |
||||
_, msg, err := wc.ReadMessage() |
||||
if err != nil { |
||||
return |
||||
} |
||||
|
||||
var candidate webrtc.ICECandidateInit |
||||
err = json.Unmarshal(msg, &candidate) |
||||
if err != nil { |
||||
return |
||||
} |
||||
|
||||
pc.AddICECandidate(candidate) |
||||
} |
||||
}() |
||||
|
||||
<-connected |
||||
|
||||
return &webRTCTestClient{ |
||||
wc: wc, |
||||
pc: pc, |
||||
track: track, |
||||
closed: closed, |
||||
}, nil |
||||
} |
||||
|
||||
func (c *webRTCTestClient) close() { |
||||
c.pc.Close() |
||||
c.wc.Close() |
||||
<-c.closed |
||||
} |
||||
|
||||
func TestWebRTCServer(t *testing.T) { |
||||
p, ok := newInstance("paths:\n" + |
||||
" all:\n") |
||||
require.Equal(t, true, ok) |
||||
defer p.Close() |
||||
|
||||
medi := &media.Media{ |
||||
Type: media.TypeVideo, |
||||
Formats: []formats.Format{&formats.H264{ |
||||
PayloadTyp: 96, |
||||
PacketizationMode: 1, |
||||
}}, |
||||
} |
||||
|
||||
v := gortsplib.TransportTCP |
||||
source := gortsplib.Client{ |
||||
Transport: &v, |
||||
} |
||||
err := source.StartRecording("rtsp://localhost:8554/stream", media.Medias{medi}) |
||||
require.NoError(t, err) |
||||
defer source.Close() |
||||
|
||||
c, err := newWebRTCTestClient("ws://localhost:8889/stream/ws") |
||||
require.NoError(t, err) |
||||
defer c.close() |
||||
|
||||
time.Sleep(500 * time.Millisecond) |
||||
|
||||
source.WritePacketRTP(medi, &rtp.Packet{ |
||||
Header: rtp.Header{ |
||||
Version: 2, |
||||
Marker: true, |
||||
PayloadType: 96, |
||||
SequenceNumber: 123, |
||||
Timestamp: 45343, |
||||
SSRC: 563423, |
||||
}, |
||||
Payload: []byte{0x01, 0x02, 0x03, 0x04}, |
||||
}) |
||||
|
||||
trak := <-c.track |
||||
|
||||
pkt, _, err := trak.ReadRTP() |
||||
require.NoError(t, err) |
||||
require.Equal(t, &rtp.Packet{ |
||||
Header: rtp.Header{ |
||||
Version: 2, |
||||
Marker: true, |
||||
PayloadType: 102, |
||||
SequenceNumber: pkt.SequenceNumber, |
||||
Timestamp: pkt.Timestamp, |
||||
SSRC: pkt.SSRC, |
||||
CSRC: []uint32{}, |
||||
}, |
||||
Payload: []byte{0x01, 0x02, 0x03, 0x04}, |
||||
}, pkt) |
||||
} |
@ -0,0 +1,592 @@
@@ -0,0 +1,592 @@
|
||||
package core |
||||
|
||||
import ( |
||||
"context" |
||||
"encoding/hex" |
||||
"encoding/json" |
||||
"fmt" |
||||
"strconv" |
||||
"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" |
||||
"github.com/pion/sdp/v3" |
||||
"github.com/pion/webrtc/v3" |
||||
|
||||
"github.com/aler9/mediamtx/internal/logger" |
||||
) |
||||
|
||||
const ( |
||||
webrtcHandshakeTimeout = 10 * time.Second |
||||
webrtcTrackGatherTimeout = 2 * time.Second |
||||
webrtcPayloadMaxSize = 1188 // 1200 - 12 (RTP header)
|
||||
webrtcStreamID = "mediamtx" |
||||
) |
||||
|
||||
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 |
||||
} |
||||
|
||||
func insertTias(offer *webrtc.SessionDescription, value uint64) { |
||||
var sd sdp.SessionDescription |
||||
err := sd.Unmarshal([]byte(offer.SDP)) |
||||
if err != nil { |
||||
return |
||||
} |
||||
|
||||
for _, media := range sd.MediaDescriptions { |
||||
if media.MediaName.Media == "video" { |
||||
media.Bandwidth = append(media.Bandwidth, sdp.Bandwidth{ |
||||
Type: "TIAS", |
||||
Bandwidth: value, |
||||
}) |
||||
} |
||||
} |
||||
|
||||
enc, err := sd.Marshal() |
||||
if err != nil { |
||||
return |
||||
} |
||||
|
||||
offer.SDP = string(enc) |
||||
} |
||||
|
||||
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( |
||||
"the stream doesn't contain any supported codec, which are currently H264, VP8, VP9, G711, G722, Opus") |
||||
} |
||||
|
||||
return tracks, nil |
||||
} |
||||
|
||||
func gatherIncomingTracks( |
||||
ctx context.Context, |
||||
pc *peerConnection, |
||||
trackRecv chan trackRecvPair, |
||||
) ([]*webRTCIncomingTrack, error) { |
||||
var tracks []*webRTCIncomingTrack |
||||
|
||||
t := time.NewTimer(webrtcTrackGatherTimeout) |
||||
defer t.Stop() |
||||
|
||||
for { |
||||
select { |
||||
case <-t.C: |
||||
return tracks, nil |
||||
|
||||
case pair := <-trackRecv: |
||||
track, err := newWebRTCIncomingTrack(pair.track, pair.receiver, pc.WriteRTCP) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
tracks = append(tracks, track) |
||||
|
||||
if len(tracks) == 2 { |
||||
return tracks, nil |
||||
} |
||||
|
||||
case <-pc.disconnected: |
||||
return nil, fmt.Errorf("peer connection closed") |
||||
|
||||
case <-ctx.Done(): |
||||
return nil, fmt.Errorf("terminated") |
||||
} |
||||
} |
||||
} |
||||
|
||||
type webRTCSessionPathManager interface { |
||||
publisherAdd(req pathPublisherAddReq) pathPublisherAnnounceRes |
||||
readerAdd(req pathReaderAddReq) pathReaderSetupPlayRes |
||||
} |
||||
|
||||
type webRTCSession struct { |
||||
readBufferCount int |
||||
req webRTCSessionNewReq |
||||
wg *sync.WaitGroup |
||||
iceHostNAT1To1IPs []string |
||||
iceUDPMux ice.UDPMux |
||||
iceTCPMux ice.TCPMux |
||||
pathManager webRTCSessionPathManager |
||||
parent *webRTCManager |
||||
|
||||
ctx context.Context |
||||
ctxCancel func() |
||||
created time.Time |
||||
uuid uuid.UUID |
||||
secret uuid.UUID |
||||
answerSent bool |
||||
pcMutex sync.RWMutex |
||||
pc *peerConnection |
||||
|
||||
chAddRemoteCandidates chan webRTCSessionAddCandidatesReq |
||||
} |
||||
|
||||
func newWebRTCSession( |
||||
parentCtx context.Context, |
||||
readBufferCount int, |
||||
req webRTCSessionNewReq, |
||||
wg *sync.WaitGroup, |
||||
iceHostNAT1To1IPs []string, |
||||
iceUDPMux ice.UDPMux, |
||||
iceTCPMux ice.TCPMux, |
||||
pathManager webRTCSessionPathManager, |
||||
parent *webRTCManager, |
||||
) *webRTCSession { |
||||
ctx, ctxCancel := context.WithCancel(parentCtx) |
||||
|
||||
s := &webRTCSession{ |
||||
readBufferCount: readBufferCount, |
||||
req: req, |
||||
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(), |
||||
chAddRemoteCandidates: make(chan webRTCSessionAddCandidatesReq), |
||||
} |
||||
|
||||
s.Log(logger.Info, "created by %s", req.remoteAddr) |
||||
|
||||
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) safePC() *peerConnection { |
||||
s.pcMutex.RLock() |
||||
defer s.pcMutex.RUnlock() |
||||
return s.pc |
||||
} |
||||
|
||||
func (s *webRTCSession) run() { |
||||
defer s.wg.Done() |
||||
|
||||
err := s.runInner() |
||||
|
||||
if !s.answerSent { |
||||
select { |
||||
case s.req.res <- webRTCNewSessionRes{ |
||||
err: err, |
||||
}: |
||||
case <-s.ctx.Done(): |
||||
} |
||||
} |
||||
|
||||
s.parent.sessionClose(s) |
||||
|
||||
s.Log(logger.Info, "closed (%v)", err) |
||||
} |
||||
|
||||
func (s *webRTCSession) runInner() error { |
||||
if s.req.publish { |
||||
return s.runPublish() |
||||
} |
||||
return s.runRead() |
||||
} |
||||
|
||||
func (s *webRTCSession) runPublish() error { |
||||
res := s.pathManager.publisherAdd(pathPublisherAddReq{ |
||||
author: s, |
||||
pathName: s.req.pathName, |
||||
skipAuth: true, |
||||
}) |
||||
if res.err != nil { |
||||
return res.err |
||||
} |
||||
|
||||
defer res.path.publisherRemove(pathPublisherRemoveReq{author: s}) |
||||
|
||||
offer, err := s.decodeOffer() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
pc, err := newPeerConnection( |
||||
s.req.videoCodec, |
||||
s.req.audioCodec, |
||||
s.parent.genICEServers(), |
||||
s.iceHostNAT1To1IPs, |
||||
s.iceUDPMux, |
||||
s.iceTCPMux, |
||||
s) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
defer pc.close() |
||||
|
||||
_, err = pc.AddTransceiverFromKind(webrtc.RTPCodecTypeVideo, webrtc.RtpTransceiverInit{ |
||||
Direction: webrtc.RTPTransceiverDirectionRecvonly, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = pc.AddTransceiverFromKind(webrtc.RTPCodecTypeAudio, webrtc.RtpTransceiverInit{ |
||||
Direction: webrtc.RTPTransceiverDirectionRecvonly, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
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 { |
||||
return err |
||||
} |
||||
|
||||
answer, err := pc.CreateAnswer(nil) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = pc.SetLocalDescription(answer) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
if s.req.videoBitrate != "" { |
||||
tmp, err := strconv.ParseUint(s.req.videoBitrate, 10, 31) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
insertTias(&answer, tmp*1024) |
||||
} |
||||
|
||||
err = s.waitGatheringDone(pc) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = s.writeAnswer(pc.LocalDescription()) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
go s.readRemoteCandidates(pc) |
||||
|
||||
err = s.waitUntilConnected(pc) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
tracks, err := gatherIncomingTracks(s.ctx, pc, trackRecv) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
medias := mediasOfIncomingTracks(tracks) |
||||
|
||||
rres := res.path.publisherStart(pathPublisherStartReq{ |
||||
author: s, |
||||
medias: medias, |
||||
generateRTPPackets: false, |
||||
}) |
||||
if rres.err != nil { |
||||
return rres.err |
||||
} |
||||
|
||||
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: |
||||
return fmt.Errorf("peer connection closed") |
||||
|
||||
case <-s.ctx.Done(): |
||||
return fmt.Errorf("terminated") |
||||
} |
||||
} |
||||
|
||||
func (s *webRTCSession) runRead() error { |
||||
res := s.pathManager.readerAdd(pathReaderAddReq{ |
||||
author: s, |
||||
pathName: s.req.pathName, |
||||
skipAuth: true, |
||||
}) |
||||
if res.err != nil { |
||||
return res.err |
||||
} |
||||
|
||||
defer res.path.readerRemove(pathReaderRemoveReq{author: s}) |
||||
|
||||
tracks, err := gatherOutgoingTracks(res.stream.medias()) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
offer, err := s.decodeOffer() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
pc, err := newPeerConnection( |
||||
"", |
||||
"", |
||||
s.parent.genICEServers(), |
||||
s.iceHostNAT1To1IPs, |
||||
s.iceUDPMux, |
||||
s.iceTCPMux, |
||||
s) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
defer pc.close() |
||||
|
||||
for _, track := range tracks { |
||||
var err error |
||||
track.sender, err = pc.AddTrack(track.track) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
|
||||
err = pc.SetRemoteDescription(*offer) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
answer, err := pc.CreateAnswer(nil) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = pc.SetLocalDescription(answer) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = s.waitGatheringDone(pc) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = s.writeAnswer(pc.LocalDescription()) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
go s.readRemoteCandidates(pc) |
||||
|
||||
err = s.waitUntilConnected(pc) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
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) |
||||
} |
||||
|
||||
defer res.stream.readerRemove(s) |
||||
|
||||
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: |
||||
return fmt.Errorf("peer connection closed") |
||||
|
||||
case err := <-writeError: |
||||
return err |
||||
|
||||
case <-s.ctx.Done(): |
||||
return fmt.Errorf("terminated") |
||||
} |
||||
} |
||||
|
||||
func (s *webRTCSession) decodeOffer() (*webrtc.SessionDescription, error) { |
||||
var offer webrtc.SessionDescription |
||||
err := json.Unmarshal(s.req.offer, &offer) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
if offer.Type != webrtc.SDPTypeOffer { |
||||
return nil, fmt.Errorf("received SDP is not an offer") |
||||
} |
||||
|
||||
return &offer, nil |
||||
} |
||||
|
||||
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") |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (s *webRTCSession) writeAnswer(answer *webrtc.SessionDescription) error { |
||||
enc, err := json.Marshal(answer) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
select { |
||||
case s.req.res <- webRTCNewSessionRes{ |
||||
sx: s, |
||||
answer: enc, |
||||
}: |
||||
s.answerSent = true |
||||
case <-s.ctx.Done(): |
||||
return fmt.Errorf("terminated") |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func (s *webRTCSession) waitUntilConnected(pc *peerConnection) error { |
||||
t := time.NewTimer(webrtcHandshakeTimeout) |
||||
defer t.Stop() |
||||
|
||||
outer: |
||||
for { |
||||
select { |
||||
case <-t.C: |
||||
return fmt.Errorf("deadline exceeded") |
||||
|
||||
case <-pc.connected: |
||||
break outer |
||||
|
||||
case <-s.ctx.Done(): |
||||
return fmt.Errorf("terminated") |
||||
} |
||||
} |
||||
|
||||
s.pcMutex.Lock() |
||||
s.pc = pc |
||||
s.pcMutex.Unlock() |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func (s *webRTCSession) readRemoteCandidates(pc *peerConnection) { |
||||
for { |
||||
select { |
||||
case req := <-s.chAddRemoteCandidates: |
||||
for _, candidate := range req.candidates { |
||||
err := pc.AddICECandidate(*candidate) |
||||
if err != nil { |
||||
req.res <- webRTCSessionAddCandidatesRes{err: err} |
||||
} |
||||
} |
||||
req.res <- webRTCSessionAddCandidatesRes{} |
||||
|
||||
case <-s.ctx.Done(): |
||||
return |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (s *webRTCSession) addRemoteCandidates( |
||||
req webRTCSessionAddCandidatesReq, |
||||
) webRTCSessionAddCandidatesRes { |
||||
select { |
||||
case s.chAddRemoteCandidates <- req: |
||||
return <-req.res |
||||
|
||||
case <-s.ctx.Done(): |
||||
return webRTCSessionAddCandidatesRes{err: fmt.Errorf("terminated")} |
||||
} |
||||
} |
||||
|
||||
// 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() |
||||
} |
Loading…
Reference in new issue