41 changed files with 2248 additions and 530 deletions
@ -0,0 +1,92 @@ |
|||||||
|
package core |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/aler9/gortsplib/v2/pkg/format" |
||||||
|
"github.com/aler9/gortsplib/v2/pkg/formatdecenc/rtpvp9" |
||||||
|
"github.com/pion/rtp" |
||||||
|
) |
||||||
|
|
||||||
|
type dataVP9 struct { |
||||||
|
rtpPackets []*rtp.Packet |
||||||
|
ntp time.Time |
||||||
|
pts time.Duration |
||||||
|
frame []byte |
||||||
|
} |
||||||
|
|
||||||
|
func (d *dataVP9) getRTPPackets() []*rtp.Packet { |
||||||
|
return d.rtpPackets |
||||||
|
} |
||||||
|
|
||||||
|
func (d *dataVP9) getNTP() time.Time { |
||||||
|
return d.ntp |
||||||
|
} |
||||||
|
|
||||||
|
type formatProcessorVP9 struct { |
||||||
|
format *format.VP9 |
||||||
|
encoder *rtpvp9.Encoder |
||||||
|
decoder *rtpvp9.Decoder |
||||||
|
} |
||||||
|
|
||||||
|
func newFormatProcessorVP9( |
||||||
|
forma *format.VP9, |
||||||
|
allocateEncoder bool, |
||||||
|
) (*formatProcessorVP9, error) { |
||||||
|
t := &formatProcessorVP9{ |
||||||
|
format: forma, |
||||||
|
} |
||||||
|
|
||||||
|
if allocateEncoder { |
||||||
|
t.encoder = forma.CreateEncoder() |
||||||
|
} |
||||||
|
|
||||||
|
return t, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (t *formatProcessorVP9) process(dat data, hasNonRTSPReaders bool) error { //nolint:dupl
|
||||||
|
tdata := dat.(*dataVP9) |
||||||
|
|
||||||
|
if tdata.rtpPackets != nil { |
||||||
|
pkt := tdata.rtpPackets[0] |
||||||
|
|
||||||
|
// remove padding
|
||||||
|
pkt.Header.Padding = false |
||||||
|
pkt.PaddingSize = 0 |
||||||
|
|
||||||
|
if pkt.MarshalSize() > maxPacketSize { |
||||||
|
return fmt.Errorf("payload size (%d) is greater than maximum allowed (%d)", |
||||||
|
pkt.MarshalSize(), maxPacketSize) |
||||||
|
} |
||||||
|
|
||||||
|
// decode from RTP
|
||||||
|
if hasNonRTSPReaders { |
||||||
|
if t.decoder == nil { |
||||||
|
t.decoder = t.format.CreateDecoder() |
||||||
|
} |
||||||
|
|
||||||
|
frame, pts, err := t.decoder.Decode(pkt) |
||||||
|
if err != nil { |
||||||
|
if err == rtpvp9.ErrMorePacketsNeeded { |
||||||
|
return nil |
||||||
|
} |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
tdata.frame = frame |
||||||
|
tdata.pts = pts |
||||||
|
} |
||||||
|
|
||||||
|
// route packet as is
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
pkts, err := t.encoder.Encode(tdata.frame, tdata.pts) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
tdata.rtpPackets = pkts |
||||||
|
return nil |
||||||
|
} |
||||||
@ -0,0 +1,67 @@ |
|||||||
|
<!DOCTYPE html> |
||||||
|
<html> |
||||||
|
<head> |
||||||
|
<meta charset="utf-8"> |
||||||
|
<style> |
||||||
|
html, body { |
||||||
|
margin: 0; |
||||||
|
padding: 0; |
||||||
|
height: 100%; |
||||||
|
overflow: hidden; |
||||||
|
} |
||||||
|
#video { |
||||||
|
width: 100%; |
||||||
|
height: 100%; |
||||||
|
background: black; |
||||||
|
} |
||||||
|
</style> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
|
||||||
|
<video id="video" muted controls autoplay playsinline></video> |
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/hls.js@1.1.5"></script> |
||||||
|
|
||||||
|
<script> |
||||||
|
|
||||||
|
const create = () => { |
||||||
|
const video = document.getElementById('video'); |
||||||
|
|
||||||
|
// always prefer hls.js over native HLS. |
||||||
|
// this is because some Android versions support native HLS |
||||||
|
// but don't support fMP4s. |
||||||
|
if (Hls.isSupported()) { |
||||||
|
const hls = new Hls({ |
||||||
|
maxLiveSyncPlaybackRate: 1.5, |
||||||
|
}); |
||||||
|
|
||||||
|
hls.on(Hls.Events.ERROR, (evt, data) => { |
||||||
|
if (data.fatal) { |
||||||
|
hls.destroy(); |
||||||
|
|
||||||
|
setTimeout(create, 2000); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
hls.loadSource('index.m3u8'); |
||||||
|
hls.attachMedia(video); |
||||||
|
|
||||||
|
video.play(); |
||||||
|
|
||||||
|
} else if (video.canPlayType('application/vnd.apple.mpegurl')) { |
||||||
|
// since it's not possible to detect timeout errors in iOS, |
||||||
|
// wait for the playlist to be available before starting the stream |
||||||
|
fetch('stream.m3u8') |
||||||
|
.then(() => { |
||||||
|
video.src = 'index.m3u8'; |
||||||
|
video.play(); |
||||||
|
}); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
window.addEventListener('DOMContentLoaded', create); |
||||||
|
|
||||||
|
</script> |
||||||
|
|
||||||
|
</body> |
||||||
|
</html> |
||||||
@ -0,0 +1,60 @@ |
|||||||
|
package core |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"fmt" |
||||||
|
"net/http" |
||||||
|
"net/http/httputil" |
||||||
|
|
||||||
|
"github.com/gin-gonic/gin" |
||||||
|
|
||||||
|
"github.com/aler9/rtsp-simple-server/internal/logger" |
||||||
|
) |
||||||
|
|
||||||
|
type httpLoggerWriter struct { |
||||||
|
gin.ResponseWriter |
||||||
|
buf bytes.Buffer |
||||||
|
} |
||||||
|
|
||||||
|
func (w *httpLoggerWriter) Write(b []byte) (int, error) { |
||||||
|
w.buf.Write(b) |
||||||
|
return w.ResponseWriter.Write(b) |
||||||
|
} |
||||||
|
|
||||||
|
func (w *httpLoggerWriter) WriteString(s string) (int, error) { |
||||||
|
w.buf.WriteString(s) |
||||||
|
return w.ResponseWriter.WriteString(s) |
||||||
|
} |
||||||
|
|
||||||
|
func (w *httpLoggerWriter) dump() string { |
||||||
|
var buf bytes.Buffer |
||||||
|
fmt.Fprintf(&buf, "%s %d %s\n", "HTTP/1.1", w.ResponseWriter.Status(), http.StatusText(w.ResponseWriter.Status())) |
||||||
|
w.ResponseWriter.Header().Write(&buf) |
||||||
|
buf.Write([]byte("\n")) |
||||||
|
if w.buf.Len() > 0 { |
||||||
|
fmt.Fprintf(&buf, "(body of %d bytes)", w.buf.Len()) |
||||||
|
} |
||||||
|
return buf.String() |
||||||
|
} |
||||||
|
|
||||||
|
type httpLoggerParent interface { |
||||||
|
log(logger.Level, string, ...interface{}) |
||||||
|
} |
||||||
|
|
||||||
|
func httpLoggerMiddleware(p httpLoggerParent) func(*gin.Context) { |
||||||
|
return func(ctx *gin.Context) { |
||||||
|
p.log(logger.Debug, "[conn %v] %s %s", ctx.ClientIP(), ctx.Request.Method, ctx.Request.URL.Path) |
||||||
|
|
||||||
|
byts, _ := httputil.DumpRequest(ctx.Request, true) |
||||||
|
p.log(logger.Debug, "[conn %v] [c->s] %s", ctx.ClientIP(), string(byts)) |
||||||
|
|
||||||
|
logw := &httpLoggerWriter{ResponseWriter: ctx.Writer} |
||||||
|
ctx.Writer = logw |
||||||
|
|
||||||
|
ctx.Writer.Header().Set("Server", "rtsp-simple-server") |
||||||
|
|
||||||
|
ctx.Next() |
||||||
|
|
||||||
|
p.log(logger.Debug, "[conn %v] [s->c] %s", ctx.ClientIP(), logw.dump()) |
||||||
|
} |
||||||
|
} |
||||||
@ -1,35 +0,0 @@ |
|||||||
package core |
|
||||||
|
|
||||||
import ( |
|
||||||
"bytes" |
|
||||||
"fmt" |
|
||||||
"net/http" |
|
||||||
|
|
||||||
"github.com/gin-gonic/gin" |
|
||||||
) |
|
||||||
|
|
||||||
type httpLogWriter struct { |
|
||||||
gin.ResponseWriter |
|
||||||
buf bytes.Buffer |
|
||||||
} |
|
||||||
|
|
||||||
func (w *httpLogWriter) Write(b []byte) (int, error) { |
|
||||||
w.buf.Write(b) |
|
||||||
return w.ResponseWriter.Write(b) |
|
||||||
} |
|
||||||
|
|
||||||
func (w *httpLogWriter) WriteString(s string) (int, error) { |
|
||||||
w.buf.WriteString(s) |
|
||||||
return w.ResponseWriter.WriteString(s) |
|
||||||
} |
|
||||||
|
|
||||||
func (w *httpLogWriter) dump() string { |
|
||||||
var buf bytes.Buffer |
|
||||||
fmt.Fprintf(&buf, "%s %d %s\n", "HTTP/1.1", w.ResponseWriter.Status(), http.StatusText(w.ResponseWriter.Status())) |
|
||||||
w.ResponseWriter.Header().Write(&buf) |
|
||||||
buf.Write([]byte("\n")) |
|
||||||
if w.buf.Len() > 0 { |
|
||||||
fmt.Fprintf(&buf, "(body of %d bytes)", w.buf.Len()) |
|
||||||
} |
|
||||||
return buf.String() |
|
||||||
} |
|
||||||
@ -0,0 +1,740 @@ |
|||||||
|
package core |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"crypto/hmac" |
||||||
|
"crypto/sha1" |
||||||
|
"encoding/base64" |
||||||
|
"encoding/json" |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
"math/rand" |
||||||
|
"net" |
||||||
|
"strconv" |
||||||
|
"strings" |
||||||
|
"sync" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/aler9/gortsplib/v2/pkg/format" |
||||||
|
"github.com/aler9/gortsplib/v2/pkg/formatdecenc/rtph264" |
||||||
|
"github.com/aler9/gortsplib/v2/pkg/formatdecenc/rtpvp8" |
||||||
|
"github.com/aler9/gortsplib/v2/pkg/formatdecenc/rtpvp9" |
||||||
|
"github.com/aler9/gortsplib/v2/pkg/h264" |
||||||
|
"github.com/aler9/gortsplib/v2/pkg/media" |
||||||
|
"github.com/aler9/gortsplib/v2/pkg/ringbuffer" |
||||||
|
"github.com/google/uuid" |
||||||
|
"github.com/gorilla/websocket" |
||||||
|
"github.com/pion/webrtc/v3" |
||||||
|
|
||||||
|
"github.com/aler9/rtsp-simple-server/internal/conf" |
||||||
|
"github.com/aler9/rtsp-simple-server/internal/logger" |
||||||
|
) |
||||||
|
|
||||||
|
type webRTCTrack struct { |
||||||
|
media *media.Media |
||||||
|
format format.Format |
||||||
|
webRTCTrack *webrtc.TrackLocalStaticRTP |
||||||
|
cb func(data, context.Context, chan error) |
||||||
|
} |
||||||
|
|
||||||
|
func gatherMedias(tracks []*webRTCTrack) media.Medias { |
||||||
|
var ret media.Medias |
||||||
|
|
||||||
|
for _, track := range tracks { |
||||||
|
ret = append(ret, track.media) |
||||||
|
} |
||||||
|
|
||||||
|
return ret |
||||||
|
} |
||||||
|
|
||||||
|
type webRTCConnPathManager interface { |
||||||
|
readerAdd(req pathReaderAddReq) pathReaderSetupPlayRes |
||||||
|
} |
||||||
|
|
||||||
|
type webRTCConnParent interface { |
||||||
|
log(logger.Level, string, ...interface{}) |
||||||
|
connClose(*webRTCConn) |
||||||
|
} |
||||||
|
|
||||||
|
type webRTCConn struct { |
||||||
|
readBufferCount int |
||||||
|
pathName string |
||||||
|
wsconn *websocket.Conn |
||||||
|
iceServers []string |
||||||
|
wg *sync.WaitGroup |
||||||
|
pathManager webRTCConnPathManager |
||||||
|
parent webRTCConnParent |
||||||
|
|
||||||
|
ctx context.Context |
||||||
|
ctxCancel func() |
||||||
|
uuid uuid.UUID |
||||||
|
created time.Time |
||||||
|
curPC *webrtc.PeerConnection |
||||||
|
mutex sync.RWMutex |
||||||
|
} |
||||||
|
|
||||||
|
func newWebRTCConn( |
||||||
|
parentCtx context.Context, |
||||||
|
readBufferCount int, |
||||||
|
pathName string, |
||||||
|
wsconn *websocket.Conn, |
||||||
|
iceServers []string, |
||||||
|
wg *sync.WaitGroup, |
||||||
|
pathManager webRTCConnPathManager, |
||||||
|
parent webRTCConnParent, |
||||||
|
) *webRTCConn { |
||||||
|
ctx, ctxCancel := context.WithCancel(parentCtx) |
||||||
|
|
||||||
|
c := &webRTCConn{ |
||||||
|
readBufferCount: readBufferCount, |
||||||
|
pathName: pathName, |
||||||
|
wsconn: wsconn, |
||||||
|
iceServers: iceServers, |
||||||
|
wg: wg, |
||||||
|
pathManager: pathManager, |
||||||
|
parent: parent, |
||||||
|
ctx: ctx, |
||||||
|
ctxCancel: ctxCancel, |
||||||
|
uuid: uuid.New(), |
||||||
|
created: time.Now(), |
||||||
|
} |
||||||
|
|
||||||
|
c.log(logger.Info, "opened") |
||||||
|
|
||||||
|
wg.Add(1) |
||||||
|
go c.run() |
||||||
|
|
||||||
|
return c |
||||||
|
} |
||||||
|
|
||||||
|
func (c *webRTCConn) close() { |
||||||
|
c.ctxCancel() |
||||||
|
} |
||||||
|
|
||||||
|
func (c *webRTCConn) remoteAddr() net.Addr { |
||||||
|
return c.wsconn.RemoteAddr() |
||||||
|
} |
||||||
|
|
||||||
|
func (c *webRTCConn) bytesReceived() uint64 { |
||||||
|
c.mutex.RLock() |
||||||
|
defer c.mutex.RUnlock() |
||||||
|
for _, stats := range c.curPC.GetStats() { |
||||||
|
if tstats, ok := stats.(webrtc.TransportStats); ok { |
||||||
|
if tstats.ID == "iceTransport" { |
||||||
|
return tstats.BytesReceived |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return 0 |
||||||
|
} |
||||||
|
|
||||||
|
func (c *webRTCConn) bytesSent() uint64 { |
||||||
|
c.mutex.RLock() |
||||||
|
defer c.mutex.RUnlock() |
||||||
|
for _, stats := range c.curPC.GetStats() { |
||||||
|
if tstats, ok := stats.(webrtc.TransportStats); ok { |
||||||
|
if tstats.ID == "iceTransport" { |
||||||
|
return tstats.BytesSent |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return 0 |
||||||
|
} |
||||||
|
|
||||||
|
func (c *webRTCConn) log(level logger.Level, format string, args ...interface{}) { |
||||||
|
c.parent.log(level, "[conn %v] "+format, append([]interface{}{c.wsconn.RemoteAddr()}, args...)...) |
||||||
|
} |
||||||
|
|
||||||
|
func (c *webRTCConn) run() { |
||||||
|
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 { |
||||||
|
go func() { |
||||||
|
<-ctx.Done() |
||||||
|
c.wsconn.Close() |
||||||
|
}() |
||||||
|
|
||||||
|
res := c.pathManager.readerAdd(pathReaderAddReq{ |
||||||
|
author: c, |
||||||
|
pathName: c.pathName, |
||||||
|
authenticate: func( |
||||||
|
pathIPs []fmt.Stringer, |
||||||
|
pathUser conf.Credential, |
||||||
|
pathPass conf.Credential, |
||||||
|
) error { |
||||||
|
return nil |
||||||
|
}, |
||||||
|
}) |
||||||
|
if res.err != nil { |
||||||
|
return res.err |
||||||
|
} |
||||||
|
|
||||||
|
path := res.path |
||||||
|
|
||||||
|
defer func() { |
||||||
|
path.readerRemove(pathReaderRemoveReq{author: c}) |
||||||
|
}() |
||||||
|
|
||||||
|
tracks, err := c.allocateTracks(res.stream.medias()) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// maximum deadline to complete the handshake
|
||||||
|
c.wsconn.SetReadDeadline(time.Now().Add(10 * time.Second)) |
||||||
|
c.wsconn.SetWriteDeadline(time.Now().Add(10 * time.Second)) |
||||||
|
|
||||||
|
err = c.writeICEServers(c.genICEServers()) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
offer, err := c.readOffer() |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
pc, err := webrtc.NewPeerConnection(webrtc.Configuration{ |
||||||
|
ICEServers: c.genICEServers(), |
||||||
|
}) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
defer pc.Close() |
||||||
|
|
||||||
|
c.mutex.Lock() |
||||||
|
c.curPC = pc |
||||||
|
c.mutex.Unlock() |
||||||
|
|
||||||
|
for _, track := range tracks { |
||||||
|
_, err = pc.AddTrack(track.webRTCTrack) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
outgoingCandidate := make(chan *webrtc.ICECandidate) |
||||||
|
pcConnected := make(chan struct{}) |
||||||
|
pcDisconnected := make(chan struct{}) |
||||||
|
|
||||||
|
pc.OnICECandidate(func(i *webrtc.ICECandidate) { |
||||||
|
if i != nil { |
||||||
|
select { |
||||||
|
case outgoingCandidate <- i: |
||||||
|
case <-pcConnected: |
||||||
|
case <-ctx.Done(): |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
pc.OnConnectionStateChange(func(state webrtc.PeerConnectionState) { |
||||||
|
c.log(logger.Debug, "peer connection state: "+state.String()) |
||||||
|
|
||||||
|
switch state { |
||||||
|
case webrtc.PeerConnectionStateConnected: |
||||||
|
close(pcConnected) |
||||||
|
|
||||||
|
case webrtc.PeerConnectionStateDisconnected: |
||||||
|
close(pcDisconnected) |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
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 |
||||||
|
} |
||||||
|
|
||||||
|
readError := make(chan error) |
||||||
|
incomingCandidate := make(chan *webrtc.ICECandidateInit) |
||||||
|
|
||||||
|
go func() { |
||||||
|
for { |
||||||
|
candidate, err := c.readCandidate() |
||||||
|
if err != nil { |
||||||
|
select { |
||||||
|
case readError <- err: |
||||||
|
case <-pcConnected: |
||||||
|
case <-ctx.Done(): |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
select { |
||||||
|
case incomingCandidate <- candidate: |
||||||
|
case <-pcConnected: |
||||||
|
case <-ctx.Done(): |
||||||
|
} |
||||||
|
} |
||||||
|
}() |
||||||
|
|
||||||
|
outer: |
||||||
|
for { |
||||||
|
select { |
||||||
|
case candidate := <-outgoingCandidate: |
||||||
|
c.writeCandidate(candidate) |
||||||
|
|
||||||
|
case candidate := <-incomingCandidate: |
||||||
|
err = pc.AddICECandidate(*candidate) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
case err := <-readError: |
||||||
|
return err |
||||||
|
|
||||||
|
case <-pcConnected: |
||||||
|
break outer |
||||||
|
|
||||||
|
case <-ctx.Done(): |
||||||
|
return fmt.Errorf("terminated") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
c.log(logger.Info, "peer connection established") |
||||||
|
c.wsconn.Close() |
||||||
|
|
||||||
|
ringBuffer, _ := ringbuffer.New(uint64(c.readBufferCount)) |
||||||
|
defer ringBuffer.Close() |
||||||
|
|
||||||
|
writeError := make(chan error) |
||||||
|
|
||||||
|
for _, track := range tracks { |
||||||
|
res.stream.readerAdd(c, track.media, track.format, func(dat data) { |
||||||
|
ringBuffer.Push(func() { |
||||||
|
track.cb(dat, ctx, writeError) |
||||||
|
}) |
||||||
|
}) |
||||||
|
} |
||||||
|
defer res.stream.readerRemove(c) |
||||||
|
|
||||||
|
c.log(logger.Info, "is reading from path '%s', %s", |
||||||
|
path.Name(), sourceMediaInfo(gatherMedias(tracks))) |
||||||
|
|
||||||
|
go func() { |
||||||
|
for { |
||||||
|
item, ok := ringBuffer.Pull() |
||||||
|
if !ok { |
||||||
|
return |
||||||
|
} |
||||||
|
item.(func())() |
||||||
|
} |
||||||
|
}() |
||||||
|
|
||||||
|
select { |
||||||
|
case <-pcDisconnected: |
||||||
|
return fmt.Errorf("peer connection closed") |
||||||
|
|
||||||
|
case err := <-writeError: |
||||||
|
return err |
||||||
|
|
||||||
|
case <-ctx.Done(): |
||||||
|
return fmt.Errorf("terminated") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (c *webRTCConn) allocateTracks(medias media.Medias) ([]*webRTCTrack, error) { |
||||||
|
var ret []*webRTCTrack |
||||||
|
|
||||||
|
var vp9Format *format.VP9 |
||||||
|
vp9Media := medias.FindFormat(&vp9Format) |
||||||
|
|
||||||
|
if vp9Format != nil { |
||||||
|
webRTCTrak, err := webrtc.NewTrackLocalStaticRTP( |
||||||
|
webrtc.RTPCodecCapability{ |
||||||
|
MimeType: webrtc.MimeTypeVP9, |
||||||
|
ClockRate: uint32(vp9Format.ClockRate()), |
||||||
|
}, |
||||||
|
"vp9", |
||||||
|
"rtspss", |
||||||
|
) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
encoder := &rtpvp9.Encoder{ |
||||||
|
PayloadType: 96, |
||||||
|
PayloadMaxSize: 1200, |
||||||
|
} |
||||||
|
encoder.Init() |
||||||
|
|
||||||
|
ret = append(ret, &webRTCTrack{ |
||||||
|
media: vp9Media, |
||||||
|
format: vp9Format, |
||||||
|
webRTCTrack: webRTCTrak, |
||||||
|
cb: func(dat data, ctx context.Context, writeError chan error) { |
||||||
|
tdata := dat.(*dataVP9) |
||||||
|
|
||||||
|
if tdata.frame == nil { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
packets, err := encoder.Encode(tdata.frame, tdata.pts) |
||||||
|
if err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
for _, pkt := range packets { |
||||||
|
webRTCTrak.WriteRTP(pkt) |
||||||
|
} |
||||||
|
}, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
var vp8Format *format.VP8 |
||||||
|
|
||||||
|
if vp9Format == nil { |
||||||
|
vp8Media := medias.FindFormat(&vp8Format) |
||||||
|
|
||||||
|
if vp8Format != nil { |
||||||
|
webRTCTrak, err := webrtc.NewTrackLocalStaticRTP( |
||||||
|
webrtc.RTPCodecCapability{ |
||||||
|
MimeType: webrtc.MimeTypeVP8, |
||||||
|
ClockRate: uint32(vp8Format.ClockRate()), |
||||||
|
}, |
||||||
|
"vp8", |
||||||
|
"rtspss", |
||||||
|
) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
encoder := &rtpvp8.Encoder{ |
||||||
|
PayloadType: 96, |
||||||
|
PayloadMaxSize: 1200, |
||||||
|
} |
||||||
|
encoder.Init() |
||||||
|
|
||||||
|
ret = append(ret, &webRTCTrack{ |
||||||
|
media: vp8Media, |
||||||
|
format: vp8Format, |
||||||
|
webRTCTrack: webRTCTrak, |
||||||
|
cb: func(dat data, ctx context.Context, writeError chan error) { |
||||||
|
tdata := dat.(*dataVP8) |
||||||
|
|
||||||
|
if tdata.frame == nil { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
packets, err := encoder.Encode(tdata.frame, tdata.pts) |
||||||
|
if err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
for _, pkt := range packets { |
||||||
|
webRTCTrak.WriteRTP(pkt) |
||||||
|
} |
||||||
|
}, |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if vp9Format == nil && vp8Format == nil { |
||||||
|
var h264Format *format.H264 |
||||||
|
h264Media := medias.FindFormat(&h264Format) |
||||||
|
|
||||||
|
if h264Format != nil { |
||||||
|
webRTCTrak, err := webrtc.NewTrackLocalStaticRTP( |
||||||
|
webrtc.RTPCodecCapability{ |
||||||
|
MimeType: webrtc.MimeTypeH264, |
||||||
|
ClockRate: uint32(h264Format.ClockRate()), |
||||||
|
}, |
||||||
|
"h264", |
||||||
|
"rtspss", |
||||||
|
) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
encoder := &rtph264.Encoder{ |
||||||
|
PayloadType: 96, |
||||||
|
PayloadMaxSize: 1200, |
||||||
|
} |
||||||
|
encoder.Init() |
||||||
|
|
||||||
|
var lastPTS time.Duration |
||||||
|
firstNALUReceived := false |
||||||
|
|
||||||
|
ret = append(ret, &webRTCTrack{ |
||||||
|
media: h264Media, |
||||||
|
format: h264Format, |
||||||
|
webRTCTrack: webRTCTrak, |
||||||
|
cb: func(dat data, ctx context.Context, writeError chan error) { |
||||||
|
tdata := dat.(*dataH264) |
||||||
|
|
||||||
|
if tdata.nalus == nil { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
if !firstNALUReceived { |
||||||
|
if !h264.IDRPresent(tdata.nalus) { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
firstNALUReceived = true |
||||||
|
lastPTS = tdata.pts |
||||||
|
} else { |
||||||
|
if tdata.pts < lastPTS { |
||||||
|
select { |
||||||
|
case writeError <- fmt.Errorf("WebRTC doesn't support H264 streams with B-frames"): |
||||||
|
case <-ctx.Done(): |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
lastPTS = tdata.pts |
||||||
|
} |
||||||
|
|
||||||
|
packets, err := encoder.Encode(tdata.nalus, tdata.pts) |
||||||
|
if err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
for _, pkt := range packets { |
||||||
|
webRTCTrak.WriteRTP(pkt) |
||||||
|
} |
||||||
|
}, |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
var opusFormat *format.Opus |
||||||
|
opusMedia := medias.FindFormat(&opusFormat) |
||||||
|
|
||||||
|
if opusFormat != nil { |
||||||
|
webRTCTrak, err := webrtc.NewTrackLocalStaticRTP( |
||||||
|
webrtc.RTPCodecCapability{ |
||||||
|
MimeType: webrtc.MimeTypeOpus, |
||||||
|
ClockRate: uint32(opusFormat.ClockRate()), |
||||||
|
}, |
||||||
|
"opus", |
||||||
|
"rtspss", |
||||||
|
) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
ret = append(ret, &webRTCTrack{ |
||||||
|
media: opusMedia, |
||||||
|
format: opusFormat, |
||||||
|
webRTCTrack: webRTCTrak, |
||||||
|
cb: func(dat data, ctx context.Context, writeError chan error) { |
||||||
|
for _, pkt := range dat.getRTPPackets() { |
||||||
|
webRTCTrak.WriteRTP(pkt) |
||||||
|
} |
||||||
|
}, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
var g722Format *format.G722 |
||||||
|
|
||||||
|
if opusFormat == nil { |
||||||
|
g722Media := medias.FindFormat(&g722Format) |
||||||
|
|
||||||
|
if g722Format != nil { |
||||||
|
webRTCTrak, err := webrtc.NewTrackLocalStaticRTP( |
||||||
|
webrtc.RTPCodecCapability{ |
||||||
|
MimeType: webrtc.MimeTypeG722, |
||||||
|
ClockRate: uint32(g722Format.ClockRate()), |
||||||
|
}, |
||||||
|
"g722", |
||||||
|
"rtspss", |
||||||
|
) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
ret = append(ret, &webRTCTrack{ |
||||||
|
media: g722Media, |
||||||
|
format: g722Format, |
||||||
|
webRTCTrack: webRTCTrak, |
||||||
|
cb: func(dat data, ctx context.Context, writeError chan error) { |
||||||
|
for _, pkt := range dat.getRTPPackets() { |
||||||
|
webRTCTrak.WriteRTP(pkt) |
||||||
|
} |
||||||
|
}, |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
var g711Format *format.G711 |
||||||
|
|
||||||
|
if opusFormat == nil && g722Format == nil { |
||||||
|
g711Media := medias.FindFormat(&g711Format) |
||||||
|
|
||||||
|
if g711Format != nil { |
||||||
|
var mtyp string |
||||||
|
if g711Format.MULaw { |
||||||
|
mtyp = webrtc.MimeTypePCMU |
||||||
|
} else { |
||||||
|
mtyp = webrtc.MimeTypePCMA |
||||||
|
} |
||||||
|
|
||||||
|
webRTCTrak, err := webrtc.NewTrackLocalStaticRTP( |
||||||
|
webrtc.RTPCodecCapability{ |
||||||
|
MimeType: mtyp, |
||||||
|
ClockRate: uint32(g711Format.ClockRate()), |
||||||
|
}, |
||||||
|
"g711", |
||||||
|
"rtspss", |
||||||
|
) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
ret = append(ret, &webRTCTrack{ |
||||||
|
media: g711Media, |
||||||
|
format: g711Format, |
||||||
|
webRTCTrack: webRTCTrak, |
||||||
|
cb: func(dat data, ctx context.Context, writeError chan error) { |
||||||
|
for _, pkt := range dat.getRTPPackets() { |
||||||
|
webRTCTrak.WriteRTP(pkt) |
||||||
|
} |
||||||
|
}, |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if ret == nil { |
||||||
|
return nil, fmt.Errorf( |
||||||
|
"the stream doesn't contain any supported codec (which are currently VP9, VP8, H264, Opus, G722, G711)") |
||||||
|
} |
||||||
|
|
||||||
|
return ret, nil |
||||||
|
} |
||||||
|
|
||||||
|
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) writeICEServers(iceServers []webrtc.ICEServer) error { |
||||||
|
enc, _ := json.Marshal(iceServers) |
||||||
|
return c.wsconn.WriteMessage(websocket.TextMessage, enc) |
||||||
|
} |
||||||
|
|
||||||
|
func (c *webRTCConn) readOffer() (*webrtc.SessionDescription, error) { |
||||||
|
_, enc, err := c.wsconn.ReadMessage() |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
var offer webrtc.SessionDescription |
||||||
|
err = json.Unmarshal(enc, &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) writeAnswer(answer *webrtc.SessionDescription) error { |
||||||
|
enc, _ := json.Marshal(answer) |
||||||
|
return c.wsconn.WriteMessage(websocket.TextMessage, enc) |
||||||
|
} |
||||||
|
|
||||||
|
func (c *webRTCConn) writeCandidate(candidate *webrtc.ICECandidate) error { |
||||||
|
enc, _ := json.Marshal(candidate.ToJSON()) |
||||||
|
return c.wsconn.WriteMessage(websocket.TextMessage, enc) |
||||||
|
} |
||||||
|
|
||||||
|
func (c *webRTCConn) readCandidate() (*webrtc.ICECandidateInit, error) { |
||||||
|
_, enc, err := c.wsconn.ReadMessage() |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
var candidate webrtc.ICECandidateInit |
||||||
|
err = json.Unmarshal(enc, &candidate) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
return &candidate, err |
||||||
|
} |
||||||
|
|
||||||
|
// apiReaderDescribe implements reader.
|
||||||
|
func (c *webRTCConn) apiReaderDescribe() interface{} { |
||||||
|
return struct { |
||||||
|
Type string `json:"type"` |
||||||
|
}{"webRTCConn"} |
||||||
|
} |
||||||
@ -0,0 +1,175 @@ |
|||||||
|
<!DOCTYPE html> |
||||||
|
<html> |
||||||
|
<head> |
||||||
|
<meta charset="utf-8"> |
||||||
|
<style> |
||||||
|
html, body { |
||||||
|
margin: 0; |
||||||
|
padding: 0; |
||||||
|
height: 100%; |
||||||
|
overflow: hidden; |
||||||
|
} |
||||||
|
#video { |
||||||
|
width: 100%; |
||||||
|
height: 100%; |
||||||
|
background: black; |
||||||
|
} |
||||||
|
</style> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
|
||||||
|
<video id="video" muted controls autoplay playsinline></video> |
||||||
|
|
||||||
|
<script> |
||||||
|
|
||||||
|
const restartPause = 2000; |
||||||
|
|
||||||
|
class Receiver { |
||||||
|
constructor() { |
||||||
|
this.terminated = false; |
||||||
|
this.ws = null; |
||||||
|
this.pc = null; |
||||||
|
this.restartTimeout = null; |
||||||
|
this.start(); |
||||||
|
} |
||||||
|
|
||||||
|
start() { |
||||||
|
console.log("connecting"); |
||||||
|
|
||||||
|
this.ws = new WebSocket(window.location.href.replace(/^http/, "ws") + 'ws'); |
||||||
|
|
||||||
|
this.ws.onerror = () => { |
||||||
|
console.log("ws error"); |
||||||
|
if (this.ws === null) { |
||||||
|
return; |
||||||
|
} |
||||||
|
this.ws.close(); |
||||||
|
this.ws = null; |
||||||
|
}; |
||||||
|
|
||||||
|
this.ws.onclose = () => { |
||||||
|
console.log("ws closed"); |
||||||
|
this.ws = null; |
||||||
|
this.scheduleRestart(); |
||||||
|
}; |
||||||
|
|
||||||
|
this.ws.onmessage = (msg) => this.onIceServers(msg); |
||||||
|
} |
||||||
|
|
||||||
|
onIceServers(msg) { |
||||||
|
if (this.ws === null) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
const iceServers = JSON.parse(msg.data); |
||||||
|
|
||||||
|
this.pc = new RTCPeerConnection({ |
||||||
|
iceServers, |
||||||
|
}); |
||||||
|
|
||||||
|
this.ws.onmessage = (msg) => this.onRemoteDescription(msg); |
||||||
|
this.pc.onicecandidate = (evt) => this.onIceCandidate(evt); |
||||||
|
|
||||||
|
this.pc.oniceconnectionstatechange = () => { |
||||||
|
if (this.pc === null) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
console.log("peer connection state:", this.pc.iceConnectionState); |
||||||
|
|
||||||
|
switch (this.pc.iceConnectionState) { |
||||||
|
case "connected": |
||||||
|
this.pc.onicecandidate = undefined; |
||||||
|
this.ws.onmessage = undefined; |
||||||
|
this.ws.onclose = undefined; |
||||||
|
this.ws.close(); |
||||||
|
this.ws = null; |
||||||
|
break; |
||||||
|
|
||||||
|
case "disconnected": |
||||||
|
this.scheduleRestart(); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
this.pc.ontrack = (evt) => { |
||||||
|
console.log("new track " + evt.track.kind); |
||||||
|
document.getElementById("video").srcObject = new MediaStream([evt.track]); |
||||||
|
}; |
||||||
|
|
||||||
|
// use sendrecv for firefox |
||||||
|
// https://github.com/pion/webrtc/issues/717#issuecomment-507990273 |
||||||
|
// https://github.com/pion/example-webrtc-applications/commit/c641b530a001eb057d8b481185c50bf67d1931b4 |
||||||
|
const direction = "sendrecv"; // (isFirefox) ? "sendrecv" : "recvonly"; |
||||||
|
this.pc.addTransceiver("video", { direction }); |
||||||
|
this.pc.addTransceiver("audio", { direction }); |
||||||
|
|
||||||
|
this.pc.createOffer() |
||||||
|
.then((desc) => { |
||||||
|
if (this.pc === null || this.ws === null) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
this.pc.setLocalDescription(desc); |
||||||
|
|
||||||
|
console.log("sending offer"); |
||||||
|
this.ws.send(JSON.stringify(desc)); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
onRemoteDescription(msg) { |
||||||
|
if (this.pc === null || this.ws === null) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
this.pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(msg.data))); |
||||||
|
this.ws.onmessage = (msg) => this.onRemoteCandidate(msg); |
||||||
|
} |
||||||
|
|
||||||
|
onIceCandidate(evt) { |
||||||
|
if (this.ws === null) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (evt.candidate !== null) { |
||||||
|
if (evt.candidate.candidate !== "") { |
||||||
|
this.ws.send(JSON.stringify(evt.candidate)); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
onRemoteCandidate(msg) { |
||||||
|
if (this.pc === null) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
this.pc.addIceCandidate(JSON.parse(msg.data)); |
||||||
|
} |
||||||
|
|
||||||
|
scheduleRestart() { |
||||||
|
if (this.terminated) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (this.ws !== null) { |
||||||
|
this.ws.close(); |
||||||
|
this.ws = null; |
||||||
|
} |
||||||
|
|
||||||
|
if (this.pc !== null) { |
||||||
|
this.pc.close(); |
||||||
|
this.pc = null; |
||||||
|
} |
||||||
|
|
||||||
|
this.restartTimeout = window.setTimeout(() => { |
||||||
|
this.restartTimeout = null; |
||||||
|
this.start(); |
||||||
|
}, restartPause); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
window.addEventListener('DOMContentLoaded', () => new Receiver()); |
||||||
|
|
||||||
|
</script> |
||||||
|
|
||||||
|
</body> |
||||||
|
</html> |
||||||
@ -0,0 +1,431 @@ |
|||||||
|
package core |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"crypto/tls" |
||||||
|
_ "embed" |
||||||
|
"fmt" |
||||||
|
"log" |
||||||
|
"net" |
||||||
|
"net/http" |
||||||
|
gopath "path" |
||||||
|
"strings" |
||||||
|
"sync" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/gin-gonic/gin" |
||||||
|
"github.com/gorilla/websocket" |
||||||
|
|
||||||
|
"github.com/aler9/rtsp-simple-server/internal/conf" |
||||||
|
"github.com/aler9/rtsp-simple-server/internal/logger" |
||||||
|
) |
||||||
|
|
||||||
|
//go:embed webrtc_index.html
|
||||||
|
var webrtcIndex []byte |
||||||
|
|
||||||
|
var upgrader = websocket.Upgrader{ |
||||||
|
CheckOrigin: func(r *http.Request) bool { |
||||||
|
return true |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
type webRTCServerAPIConnsListItem struct { |
||||||
|
Created time.Time `json:"created"` |
||||||
|
RemoteAddr string `json:"remoteAddr"` |
||||||
|
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 |
||||||
|
wsconn *websocket.Conn |
||||||
|
} |
||||||
|
|
||||||
|
type webRTCServerParent interface { |
||||||
|
Log(logger.Level, string, ...interface{}) |
||||||
|
} |
||||||
|
|
||||||
|
type webRTCServer struct { |
||||||
|
externalAuthenticationURL string |
||||||
|
allowOrigin string |
||||||
|
trustedProxies conf.IPsOrCIDRs |
||||||
|
stunServers []string |
||||||
|
readBufferCount int |
||||||
|
pathManager *pathManager |
||||||
|
metrics *metrics |
||||||
|
parent webRTCServerParent |
||||||
|
|
||||||
|
ctx context.Context |
||||||
|
ctxCancel func() |
||||||
|
wg sync.WaitGroup |
||||||
|
ln net.Listener |
||||||
|
tlsConfig *tls.Config |
||||||
|
conns map[*webRTCConn]struct{} |
||||||
|
|
||||||
|
// in
|
||||||
|
connNew chan webRTCConnNewReq |
||||||
|
chConnClose chan *webRTCConn |
||||||
|
chAPIConnsList chan webRTCServerAPIConnsListReq |
||||||
|
chAPIConnsKick chan webRTCServerAPIConnsKickReq |
||||||
|
} |
||||||
|
|
||||||
|
func newWebRTCServer( |
||||||
|
parentCtx context.Context, |
||||||
|
externalAuthenticationURL string, |
||||||
|
address string, |
||||||
|
serverKey string, |
||||||
|
serverCert string, |
||||||
|
allowOrigin string, |
||||||
|
trustedProxies conf.IPsOrCIDRs, |
||||||
|
stunServers []string, |
||||||
|
readBufferCount int, |
||||||
|
pathManager *pathManager, |
||||||
|
metrics *metrics, |
||||||
|
parent webRTCServerParent, |
||||||
|
) (*webRTCServer, error) { |
||||||
|
ln, err := net.Listen("tcp", address) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
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 := &webRTCServer{ |
||||||
|
externalAuthenticationURL: externalAuthenticationURL, |
||||||
|
allowOrigin: allowOrigin, |
||||||
|
trustedProxies: trustedProxies, |
||||||
|
stunServers: stunServers, |
||||||
|
readBufferCount: readBufferCount, |
||||||
|
pathManager: pathManager, |
||||||
|
metrics: metrics, |
||||||
|
parent: parent, |
||||||
|
ctx: ctx, |
||||||
|
ctxCancel: ctxCancel, |
||||||
|
ln: ln, |
||||||
|
tlsConfig: tlsConfig, |
||||||
|
conns: make(map[*webRTCConn]struct{}), |
||||||
|
connNew: make(chan webRTCConnNewReq), |
||||||
|
chConnClose: make(chan *webRTCConn), |
||||||
|
chAPIConnsList: make(chan webRTCServerAPIConnsListReq), |
||||||
|
chAPIConnsKick: make(chan webRTCServerAPIConnsKickReq), |
||||||
|
} |
||||||
|
|
||||||
|
s.log(logger.Info, "listener opened on "+address) |
||||||
|
|
||||||
|
if s.metrics != nil { |
||||||
|
s.metrics.webRTCServerSet(s) |
||||||
|
} |
||||||
|
|
||||||
|
s.wg.Add(1) |
||||||
|
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.wg.Wait() |
||||||
|
} |
||||||
|
|
||||||
|
func (s *webRTCServer) run() { |
||||||
|
defer s.wg.Done() |
||||||
|
|
||||||
|
router := gin.New() |
||||||
|
router.NoRoute(httpLoggerMiddleware(s), s.onRequest) |
||||||
|
|
||||||
|
tmp := make([]string, len(s.trustedProxies)) |
||||||
|
for i, entry := range s.trustedProxies { |
||||||
|
tmp[i] = entry.String() |
||||||
|
} |
||||||
|
router.SetTrustedProxies(tmp) |
||||||
|
|
||||||
|
hs := &http.Server{ |
||||||
|
Handler: router, |
||||||
|
TLSConfig: s.tlsConfig, |
||||||
|
ErrorLog: log.New(&nilWriter{}, "", 0), |
||||||
|
} |
||||||
|
|
||||||
|
if s.tlsConfig != nil { |
||||||
|
go hs.ServeTLS(s.ln, "", "") |
||||||
|
} else { |
||||||
|
go hs.Serve(s.ln) |
||||||
|
} |
||||||
|
|
||||||
|
outer: |
||||||
|
for { |
||||||
|
select { |
||||||
|
case req := <-s.connNew: |
||||||
|
c := newWebRTCConn( |
||||||
|
s.ctx, |
||||||
|
s.readBufferCount, |
||||||
|
req.pathName, |
||||||
|
req.wsconn, |
||||||
|
s.stunServers, |
||||||
|
&s.wg, |
||||||
|
s.pathManager, |
||||||
|
s, |
||||||
|
) |
||||||
|
s.conns[c] = struct{}{} |
||||||
|
|
||||||
|
case conn := <-s.chConnClose: |
||||||
|
delete(s.conns, conn) |
||||||
|
|
||||||
|
case req := <-s.chAPIConnsList: |
||||||
|
data := &webRTCServerAPIConnsListData{ |
||||||
|
Items: make(map[string]webRTCServerAPIConnsListItem), |
||||||
|
} |
||||||
|
|
||||||
|
for c := range s.conns { |
||||||
|
data.Items[c.uuid.String()] = webRTCServerAPIConnsListItem{ |
||||||
|
Created: c.created, |
||||||
|
RemoteAddr: c.remoteAddr().String(), |
||||||
|
BytesReceived: c.bytesReceived(), |
||||||
|
BytesSent: c.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() |
||||||
|
|
||||||
|
hs.Shutdown(context.Background()) |
||||||
|
s.ln.Close() // in case Shutdown() is called before Serve()
|
||||||
|
} |
||||||
|
|
||||||
|
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:] |
||||||
|
|
||||||
|
switch pa { |
||||||
|
case "", "favicon.ico": |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
dir, fname := func() (string, string) { |
||||||
|
if strings.HasSuffix(pa, "/ws") { |
||||||
|
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 |
||||||
|
} |
||||||
|
|
||||||
|
dir = strings.TrimSuffix(dir, "/") |
||||||
|
|
||||||
|
res := s.pathManager.describe(pathDescribeReq{ |
||||||
|
pathName: dir, |
||||||
|
}) |
||||||
|
if res.err != nil { |
||||||
|
ctx.Writer.WriteHeader(http.StatusNotFound) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
err := s.authenticate(res.path, ctx) |
||||||
|
if err != nil { |
||||||
|
if terr, ok := err.(pathErrAuthCritical); ok { |
||||||
|
s.log(logger.Info, "authentication error: %s", terr.message) |
||||||
|
ctx.Writer.Header().Set("WWW-Authenticate", `Basic realm="rtsp-simple-server"`) |
||||||
|
ctx.Writer.WriteHeader(http.StatusUnauthorized) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
ctx.Writer.Header().Set("WWW-Authenticate", `Basic realm="rtsp-simple-server"`) |
||||||
|
ctx.Writer.WriteHeader(http.StatusUnauthorized) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
switch fname { |
||||||
|
case "": |
||||||
|
ctx.Writer.Header().Set("Content-Type", "text/html") |
||||||
|
ctx.Writer.Write(webrtcIndex) |
||||||
|
return |
||||||
|
|
||||||
|
case "ws": |
||||||
|
wsconn, err := upgrader.Upgrade(ctx.Writer, ctx.Request, nil) |
||||||
|
if err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
select { |
||||||
|
case s.connNew <- webRTCConnNewReq{ |
||||||
|
pathName: dir, |
||||||
|
wsconn: wsconn, |
||||||
|
}: |
||||||
|
case <-s.ctx.Done(): |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (s *webRTCServer) authenticate(pa *path, ctx *gin.Context) error { |
||||||
|
pathConf := pa.Conf() |
||||||
|
pathIPs := pathConf.ReadIPs |
||||||
|
pathUser := pathConf.ReadUser |
||||||
|
pathPass := pathConf.ReadPass |
||||||
|
|
||||||
|
if s.externalAuthenticationURL != "" { |
||||||
|
ip := net.ParseIP(ctx.ClientIP()) |
||||||
|
user, pass, ok := ctx.Request.BasicAuth() |
||||||
|
|
||||||
|
err := externalAuth( |
||||||
|
s.externalAuthenticationURL, |
||||||
|
ip.String(), |
||||||
|
user, |
||||||
|
pass, |
||||||
|
pa.name, |
||||||
|
false, |
||||||
|
ctx.Request.URL.RawQuery) |
||||||
|
if err != nil { |
||||||
|
if !ok { |
||||||
|
return pathErrAuthNotCritical{} |
||||||
|
} |
||||||
|
|
||||||
|
return pathErrAuthCritical{ |
||||||
|
message: fmt.Sprintf("external authentication failed: %s", err), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if pathIPs != nil { |
||||||
|
ip := net.ParseIP(ctx.ClientIP()) |
||||||
|
|
||||||
|
if !ipEqualOrInRange(ip, pathIPs) { |
||||||
|
return pathErrAuthCritical{ |
||||||
|
message: fmt.Sprintf("IP '%s' not allowed", ip), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if pathUser != "" { |
||||||
|
user, pass, ok := ctx.Request.BasicAuth() |
||||||
|
if !ok { |
||||||
|
return pathErrAuthNotCritical{} |
||||||
|
} |
||||||
|
|
||||||
|
if user != string(pathUser) || pass != string(pathPass) { |
||||||
|
return pathErrAuthCritical{ |
||||||
|
message: "invalid credentials", |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
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")} |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue