41 changed files with 2248 additions and 530 deletions
@ -0,0 +1,92 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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