mirror of
https://github.com/aler9/rtsp-simple-server
synced 2025-10-12 19:10:19 +08:00
Support reading with WebRTC (#1242)
This commit is contained in:
38
README.md
38
README.md
@@ -3,20 +3,33 @@
|
||||
<img src="logo.png" alt="rtsp-simple-server">
|
||||
</p>
|
||||
|
||||
_rtsp-simple-server_ is a ready-to-use and zero-dependency server and proxy that allows users to publish, read and proxy live video and audio streams through various protocols:
|
||||
_rtsp-simple-server_ is a ready-to-use and zero-dependency server and proxy that allows users to publish, read and proxy live video and audio streams.
|
||||
|
||||
|protocol|description|variants|publish|read|proxy|
|
||||
|--------|-----------|--------|-------|----|-----|
|
||||
|RTSP|fastest way to publish and read streams|RTSP, RTSPS|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
|
||||
|RTMP|allows to interact with legacy software|RTMP, RTMPS|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
|
||||
|HLS|allows to embed streams into a web page|Low-Latency HLS, standard HLS|:x:|:heavy_check_mark:|:heavy_check_mark:|
|
||||
Live streams can be published to the server with:
|
||||
|
||||
|protocol|variants|codecs|
|
||||
|--------|--------|------|
|
||||
|RTSP clients (FFmpeg, GStreamer, etc)|UDP, TCP, RTSPS|H264, H265, VP8, VP9, AV1, MPEG2, JPEG, MP3, MPEG4 Audio (AAC), Opus, G711, G722, LPCM and any RTP-compatible codec|
|
||||
|RTSP servers and cameras|UDP, UDP-Multicast, TCP, RTSPS|H264, H265, VP8, VP9, AV1, MPEG2, JPEG, MP3, MPEG4 Audio (AAC), Opus, G711, G722, LPCM and any RTP-compatible codec|
|
||||
|RTMP clients (OBS Studio)|RTMP, RTMPS|H264, MPEG4 Audio (AAC)|
|
||||
|RTMP servers and cameras|RTMP, RTMPS|H264, MPEG4 Audio (AAC)|
|
||||
|HLS servers and cameras|Low-Latency HLS, MP4-based HLS, legacy HLS|H264, MPEG4 Audio (AAC)|
|
||||
|Raspberry Pi Cameras||H264|
|
||||
|
||||
And can be read from the server with:
|
||||
|
||||
|protocol|variants|codecs|
|
||||
|--------|--------|------|
|
||||
|RTSP|UDP, UDP-Multicast, TCP, RTSPS|H264, H265, VP8, VP9, AV1, MPEG2, JPEG, MP3, MPEG4 Audio (AAC), Opus, G711, G722, LPCM and any RTP-compatible codec|
|
||||
|RTMP|RTMP, RTMPS|H264, MPEG4 Audio (AAC)|
|
||||
|HLS|Low-Latency HLS, MP4-based HLS, legacy HLS|H264, MPEG4 Audio (AAC)|
|
||||
|WebRTC||H264, VP8, VP9, Opus, G711, G722|
|
||||
|
||||
Features:
|
||||
|
||||
* Publish live streams to the server
|
||||
* Read live streams from the server
|
||||
* Proxy streams from other servers or cameras, always or on-demand
|
||||
* Each stream can have multiple video and audio tracks, encoded with any RTP-compatible codec, including H264, H265, VP8, VP9, MPEG2, MP3, AAC, Opus, PCM, JPEG
|
||||
* Streams are automatically converted from a protocol to another. For instance, it's possible to publish a stream with RTSP and read it with HLS
|
||||
* Serve multiple streams at once in separate paths
|
||||
* Authenticate users; use internal or external authentication
|
||||
@@ -446,6 +459,10 @@ Obtaining:
|
||||
paths{name="[path_name]",state="[state]"} 1
|
||||
paths_bytes_received{name="[path_name]",state="[state]"} 1234
|
||||
|
||||
# metrics of every HLS muxer
|
||||
hls_muxers{name="[name]"} 1
|
||||
hls_muxers_bytes_sent{name="[name]"} 187
|
||||
|
||||
# metrics of every RTSP connection
|
||||
rtsp_conns{id="[id]"} 1
|
||||
rtsp_conns_bytes_received{id="[id]"} 1234
|
||||
@@ -471,9 +488,10 @@ rtmp_conns{id="[id]",state="[state]"} 1
|
||||
rtmp_conns_bytes_received{id="[id]",state="[state]"} 1234
|
||||
rtmp_conns_bytes_sent{id="[id]",state="[state]"} 187
|
||||
|
||||
# metrics of every HLS muxer
|
||||
hls_muxers{name="[name]"} 1
|
||||
hls_muxers_bytes_sent{name="[name]"} 187
|
||||
# metrics of every WebRTC connection
|
||||
webrtc_conns{id="[id]"} 1
|
||||
webrtc_conns_bytes_received{id="[id]",state="[state]"} 1234
|
||||
webrtc_conns_bytes_sent{id="[id]",state="[state]"} 187
|
||||
```
|
||||
|
||||
### pprof
|
||||
|
@@ -429,6 +429,14 @@ components:
|
||||
bytesSent:
|
||||
type: number
|
||||
|
||||
HLSMuxersList:
|
||||
type: object
|
||||
properties:
|
||||
items:
|
||||
type: object
|
||||
additionalProperties:
|
||||
$ref: '#/components/schemas/HLSMuxer'
|
||||
|
||||
PathsList:
|
||||
type: object
|
||||
properties:
|
||||
@@ -437,6 +445,14 @@ components:
|
||||
additionalProperties:
|
||||
$ref: '#/components/schemas/Path'
|
||||
|
||||
RTMPConnsList:
|
||||
type: object
|
||||
properties:
|
||||
items:
|
||||
type: object
|
||||
additionalProperties:
|
||||
$ref: '#/components/schemas/RTMPConn'
|
||||
|
||||
RTSPConnsList:
|
||||
type: object
|
||||
properties:
|
||||
@@ -461,21 +477,25 @@ components:
|
||||
additionalProperties:
|
||||
$ref: '#/components/schemas/RTSPSession'
|
||||
|
||||
RTMPConnsList:
|
||||
WebRTCConn:
|
||||
type: object
|
||||
properties:
|
||||
items:
|
||||
type: object
|
||||
additionalProperties:
|
||||
$ref: '#/components/schemas/RTMPConn'
|
||||
created:
|
||||
type: string
|
||||
remoteAddr:
|
||||
type: string
|
||||
bytesReceived:
|
||||
type: number
|
||||
bytesSent:
|
||||
type: number
|
||||
|
||||
HLSMuxersList:
|
||||
WebRTCConnsList:
|
||||
type: object
|
||||
properties:
|
||||
items:
|
||||
type: object
|
||||
additionalProperties:
|
||||
$ref: '#/components/schemas/HLSMuxer'
|
||||
$ref: '#/components/schemas/WebRTCConn'
|
||||
|
||||
paths:
|
||||
/v1/config/get:
|
||||
@@ -586,6 +606,23 @@ paths:
|
||||
'500':
|
||||
description: internal server error.
|
||||
|
||||
/v1/hlsmuxers/list:
|
||||
get:
|
||||
operationId: hlsMuxersList
|
||||
summary: returns all HLS muxers.
|
||||
description: ''
|
||||
responses:
|
||||
'200':
|
||||
description: the request was successful.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HLSMuxersList'
|
||||
'400':
|
||||
description: invalid request.
|
||||
'500':
|
||||
description: internal server error.
|
||||
|
||||
/v1/paths/list:
|
||||
get:
|
||||
operationId: pathsList
|
||||
@@ -637,23 +674,6 @@ paths:
|
||||
'500':
|
||||
description: internal server error.
|
||||
|
||||
/v1/rtspsconns/list:
|
||||
get:
|
||||
operationId: rtspsConnsList
|
||||
summary: returns all RTSPS connections.
|
||||
description: ''
|
||||
responses:
|
||||
'200':
|
||||
description: the request was successful.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RTSPConnsList'
|
||||
'400':
|
||||
description: invalid request.
|
||||
'500':
|
||||
description: internal server error.
|
||||
|
||||
/v1/rtspsessions/kick/{id}:
|
||||
post:
|
||||
operationId: rtspSessionsKick
|
||||
@@ -674,6 +694,23 @@ paths:
|
||||
'500':
|
||||
description: internal server error.
|
||||
|
||||
/v1/rtspsconns/list:
|
||||
get:
|
||||
operationId: rtspsConnsList
|
||||
summary: returns all RTSPS connections.
|
||||
description: ''
|
||||
responses:
|
||||
'200':
|
||||
description: the request was successful.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RTSPConnsList'
|
||||
'400':
|
||||
description: invalid request.
|
||||
'500':
|
||||
description: internal server error.
|
||||
|
||||
/v1/rtspssessions/list:
|
||||
get:
|
||||
operationId: rtspsSessionsList
|
||||
@@ -785,10 +822,10 @@ paths:
|
||||
'500':
|
||||
description: internal server error.
|
||||
|
||||
/v1/hlsmuxers/list:
|
||||
/v1/webrtcconns/list:
|
||||
get:
|
||||
operationId: hlsMuxersList
|
||||
summary: returns all HLS muxers.
|
||||
operationId: webrtcConnsList
|
||||
summary: returns all WebRTC connections.
|
||||
description: ''
|
||||
responses:
|
||||
'200':
|
||||
@@ -796,7 +833,27 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HLSMuxersList'
|
||||
$ref: '#/components/schemas/WebRTCConnsList'
|
||||
'400':
|
||||
description: invalid request.
|
||||
'500':
|
||||
description: internal server error.
|
||||
|
||||
/v1/webrtcconns/kick/{id}:
|
||||
post:
|
||||
operationId: webrtcConnsKick
|
||||
summary: kicks out a WebRTC connection from the server.
|
||||
description: ''
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
description: the ID of the session.
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: the request was successful.
|
||||
'400':
|
||||
description: invalid request.
|
||||
'500':
|
||||
|
28
go.mod
28
go.mod
@@ -5,19 +5,21 @@ go 1.18
|
||||
require (
|
||||
code.cloudfoundry.org/bytefmt v0.0.0-20211005130812-5bb3c17173e5
|
||||
github.com/abema/go-mp4 v0.8.0
|
||||
github.com/aler9/gortsplib/v2 v2.0.0-20221214165733-d43cb0455e33
|
||||
github.com/aler9/gortsplib/v2 v2.0.0-20221214210611-e1c07a1c8d71
|
||||
github.com/asticode/go-astits v1.10.1-0.20220319093903-4abe66a9b757
|
||||
github.com/fsnotify/fsnotify v1.4.9
|
||||
github.com/gin-gonic/gin v1.8.1
|
||||
github.com/google/uuid v1.1.2
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/gookit/color v1.4.2
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/grafov/m3u8 v0.11.1
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||
github.com/notedit/rtmp v0.0.2
|
||||
github.com/orcaman/writerseeker v0.0.0
|
||||
github.com/pion/rtp v1.7.13
|
||||
github.com/pion/webrtc/v3 v3.1.47
|
||||
github.com/stretchr/testify v1.7.1
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97
|
||||
golang.org/x/crypto v0.0.0-20221010152910-d6f0a8c073c2
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
@@ -38,14 +40,26 @@ require (
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.1 // indirect
|
||||
github.com/pion/datachannel v1.5.2 // indirect
|
||||
github.com/pion/dtls/v2 v2.1.5 // indirect
|
||||
github.com/pion/ice/v2 v2.2.11 // indirect
|
||||
github.com/pion/interceptor v0.1.11 // indirect
|
||||
github.com/pion/logging v0.2.2 // indirect
|
||||
github.com/pion/mdns v0.0.5 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/rtcp v1.2.9 // indirect
|
||||
github.com/pion/sdp/v3 v3.0.5 // indirect
|
||||
github.com/pion/rtcp v1.2.10 // indirect
|
||||
github.com/pion/sctp v1.8.2 // indirect
|
||||
github.com/pion/sdp/v3 v3.0.6 // indirect
|
||||
github.com/pion/srtp/v2 v2.0.10 // indirect
|
||||
github.com/pion/stun v0.3.5 // indirect
|
||||
github.com/pion/transport v0.13.1 // indirect
|
||||
github.com/pion/turn/v2 v2.0.8 // indirect
|
||||
github.com/pion/udp v0.1.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/ugorji/go/codec v1.2.7 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
|
||||
golang.org/x/net v0.0.0-20220526153639-5463443f8c37 // indirect
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
|
||||
golang.org/x/net v0.0.0-20221004154528-8021a29435af // indirect
|
||||
golang.org/x/sys v0.0.0-20221010170243-090e33056c14 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
google.golang.org/protobuf v1.28.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||
|
76
go.sum
76
go.sum
@@ -6,8 +6,8 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafo
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/aler9/gortsplib/v2 v2.0.0-20221214165733-d43cb0455e33 h1:7r2VpQoRSYOCU9qSXit9A4RKI7ufdI5UAxDHHjZ1Occ=
|
||||
github.com/aler9/gortsplib/v2 v2.0.0-20221214165733-d43cb0455e33/go.mod h1:zJ+fWtakOMN6cKV169EMNVBLPTITArrJKu/fyM+dov8=
|
||||
github.com/aler9/gortsplib/v2 v2.0.0-20221214210611-e1c07a1c8d71 h1:dgKa+8HxFRliWSRFHyYg1Fz2F6OlDapT81oDGS6kits=
|
||||
github.com/aler9/gortsplib/v2 v2.0.0-20221214210611-e1c07a1c8d71/go.mod h1:zJ+fWtakOMN6cKV169EMNVBLPTITArrJKu/fyM+dov8=
|
||||
github.com/aler9/writerseeker v0.0.0-20220601075008-6f0e685b9c82 h1:9WgSzBLo3a9ToSVV7sRTBYZ1GGOZUpq4+5H3SN0UZq4=
|
||||
github.com/aler9/writerseeker v0.0.0-20220601075008-6f0e685b9c82/go.mod h1:qsMrZCbeBf/mCLOeF16KDkPu4gktn/pOWyaq1aYQE7U=
|
||||
github.com/asticode/go-astikit v0.20.0 h1:+7N+J4E4lWx2QOkRdOf6DafWJMv6O4RRfgClwQokrH8=
|
||||
@@ -52,10 +52,13 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gookit/color v1.4.2 h1:tXy44JFSFkKnELV6WaMo/lLfu/meqITX3iAV52do7lk=
|
||||
github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grafov/m3u8 v0.11.1 h1:igZ7EBIB2IAsPPazKwRKdbhxcoBKO3lO1UY57PZDeNA=
|
||||
github.com/grafov/m3u8 v0.11.1/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
@@ -87,22 +90,55 @@ github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
|
||||
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.16.0 h1:6gjqkI8iiRHMvdccRJM8rVKjCWk6ZIm6FTm3ddIe4/c=
|
||||
github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||
github.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE=
|
||||
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||
github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU=
|
||||
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
|
||||
github.com/pion/datachannel v1.5.2 h1:piB93s8LGmbECrpO84DnkIVWasRMk3IimbcXkTQLE6E=
|
||||
github.com/pion/datachannel v1.5.2/go.mod h1:FTGQWaHrdCwIJ1rw6xBIfZVkslikjShim5yr05XFuCQ=
|
||||
github.com/pion/dtls/v2 v2.1.5 h1:jlh2vtIyUBShchoTDqpCCqiYCyRFJ/lvf/gQ8TALs+c=
|
||||
github.com/pion/dtls/v2 v2.1.5/go.mod h1:BqCE7xPZbPSubGasRoDFJeTsyJtdD1FanJYL0JGheqY=
|
||||
github.com/pion/ice/v2 v2.2.11 h1:wiAy7TSrVZ4KdyjC0CcNTkwltz9ywetbe4wbHLKUbIg=
|
||||
github.com/pion/ice/v2 v2.2.11/go.mod h1:NqUDUao6SjSs1+4jrqpexDmFlptlVhGxQjcymXLaVvE=
|
||||
github.com/pion/interceptor v0.1.11 h1:00U6OlqxA3FFB50HSg25J/8cWi7P6FbSzw4eFn24Bvs=
|
||||
github.com/pion/interceptor v0.1.11/go.mod h1:tbtKjZY14awXd7Bq0mmWvgtHB5MDaRN7HV3OZ/uy7s8=
|
||||
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||
github.com/pion/mdns v0.0.5 h1:Q2oj/JB3NqfzY9xGZ1fPzZzK7sDSD8rZPOvcIQ10BCw=
|
||||
github.com/pion/mdns v0.0.5/go.mod h1:UgssrvdD3mxpi8tMxAXbsppL3vJ4Jipw1mTCW+al01g=
|
||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/rtcp v1.2.9 h1:1ujStwg++IOLIEoOiIQ2s+qBuJ1VN81KW+9pMPsif+U=
|
||||
github.com/pion/rtcp v1.2.9/go.mod h1:qVPhiCzAm4D/rxb6XzKeyZiQK69yJpbUDJSF7TgrqNo=
|
||||
github.com/pion/rtcp v1.2.10 h1:nkr3uj+8Sp97zyItdN60tE/S6vk4al5CPRR6Gejsdjc=
|
||||
github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I=
|
||||
github.com/pion/rtp v1.7.13 h1:qcHwlmtiI50t1XivvoawdCGTP4Uiypzfrsap+bijcoA=
|
||||
github.com/pion/rtp v1.7.13/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
|
||||
github.com/pion/sdp/v3 v3.0.5 h1:ouvI7IgGl+V4CrqskVtr3AaTrPvPisEOxwgpdktctkU=
|
||||
github.com/pion/sdp/v3 v3.0.5/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
|
||||
github.com/pion/sctp v1.8.0/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
|
||||
github.com/pion/sctp v1.8.2 h1:yBBCIrUMJ4yFICL3RIvR4eh/H2BTTvlligmSTy+3kiA=
|
||||
github.com/pion/sctp v1.8.2/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
|
||||
github.com/pion/sdp/v3 v3.0.6 h1:WuDLhtuFUUVpTfus9ILC4HRyHsW6TdugjEX/QY9OiUw=
|
||||
github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
|
||||
github.com/pion/srtp/v2 v2.0.10 h1:b8ZvEuI+mrL8hbr/f1YiJFB34UMrOac3R3N1yq2UN0w=
|
||||
github.com/pion/srtp/v2 v2.0.10/go.mod h1:XEeSWaK9PfuMs7zxXyiN252AHPbH12NX5q/CFDWtUuA=
|
||||
github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg=
|
||||
github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA=
|
||||
github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q=
|
||||
github.com/pion/transport v0.12.3/go.mod h1:OViWW9SP2peE/HbwBvARicmAVnesphkNkCVZIWJ6q9A=
|
||||
github.com/pion/transport v0.13.0/go.mod h1:yxm9uXpK9bpBBWkITk13cLo1y5/ur5VQpG22ny6EP7g=
|
||||
github.com/pion/transport v0.13.1 h1:/UH5yLeQtwm2VZIPjxwnNFxjS4DFhyLfS4GlfuKUzfA=
|
||||
github.com/pion/transport v0.13.1/go.mod h1:EBxbqzyv+ZrmDb82XswEE0BjfQFtuw1Nu6sjnjWCsGg=
|
||||
github.com/pion/turn/v2 v2.0.8 h1:KEstL92OUN3k5k8qxsXHpr7WWfrdp7iJZHx99ud8muw=
|
||||
github.com/pion/turn/v2 v2.0.8/go.mod h1:+y7xl719J8bAEVpSXBXvTxStjJv3hbz9YFflvkpcGPw=
|
||||
github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o=
|
||||
github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M=
|
||||
github.com/pion/webrtc/v3 v3.1.47 h1:2dFEKRI1rzFvehXDq43hK9OGGyTGJSusUi3j6QKHC5s=
|
||||
github.com/pion/webrtc/v3 v3.1.47/go.mod h1:8U39MYZCLVV4sIBn01htASVNkWQN2zDa/rx5xisEXWs=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
@@ -110,6 +146,7 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
@@ -128,18 +165,27 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20221010152910-d6f0a8c073c2 h1:x8vtB3zMecnlqZIwJNUUpwYKYSqCz5jXbiyv0ZJJZeI=
|
||||
golang.org/x/crypto v0.0.0-20221010152910-d6f0a8c073c2/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20220526153639-5463443f8c37 h1:lUkvobShwKsOesNfWWlCS5q7fnbG1MEliIzwu886fn8=
|
||||
golang.org/x/net v0.0.0-20220526153639-5463443f8c37/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220531201128-c960675eff93/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
golang.org/x/net v0.0.0-20221004154528-8021a29435af h1:wv66FM3rLZGPdxpYL+ApnDe2HzHcTFta3z5nsc13wI4=
|
||||
golang.org/x/net v0.0.0-20221004154528-8021a29435af/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -159,8 +205,13 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20221010170243-090e33056c14 h1:k5II8e6QD8mITdi+okbbmR/cIyEbeXLBhy5Ha4nevyc=
|
||||
golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -190,6 +241,7 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQ
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
|
@@ -54,6 +54,7 @@ func (d *AuthMethods) UnmarshalJSON(b []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// unmarshalEnv implements envUnmarshaler.
|
||||
func (d *AuthMethods) unmarshalEnv(s string) error {
|
||||
byts, _ := json.Marshal(strings.Split(s, ","))
|
||||
return d.UnmarshalJSON(byts)
|
||||
|
@@ -211,6 +211,9 @@ type Conf struct {
|
||||
// HLS
|
||||
HLSDisable bool `json:"hlsDisable"`
|
||||
HLSAddress string `json:"hlsAddress"`
|
||||
HLSEncryption bool `json:"hlsEncryption"`
|
||||
HLSServerKey string `json:"hlsServerKey"`
|
||||
HLSServerCert string `json:"hlsServerCert"`
|
||||
HLSAlwaysRemux bool `json:"hlsAlwaysRemux"`
|
||||
HLSVariant HLSVariant `json:"hlsVariant"`
|
||||
HLSSegmentCount int `json:"hlsSegmentCount"`
|
||||
@@ -218,11 +221,17 @@ type Conf struct {
|
||||
HLSPartDuration StringDuration `json:"hlsPartDuration"`
|
||||
HLSSegmentMaxSize StringSize `json:"hlsSegmentMaxSize"`
|
||||
HLSAllowOrigin string `json:"hlsAllowOrigin"`
|
||||
HLSEncryption bool `json:"hlsEncryption"`
|
||||
HLSServerKey string `json:"hlsServerKey"`
|
||||
HLSServerCert string `json:"hlsServerCert"`
|
||||
HLSTrustedProxies IPsOrCIDRs `json:"hlsTrustedProxies"`
|
||||
|
||||
// WebRTC
|
||||
WebRTC bool `json:"webrtc"`
|
||||
WebRTCAddress string `json:"webrtcAddress"`
|
||||
WebRTCServerKey string `json:"webrtcServerKey"`
|
||||
WebRTCServerCert string `json:"webrtcServerCert"`
|
||||
WebRTCAllowOrigin string `json:"webrtcAllowOrigin"`
|
||||
WebRTCTrustedProxies IPsOrCIDRs `json:"webrtcTrustedProxies"`
|
||||
WebRTCICEServers []string `json:"webrtcICEServers"`
|
||||
|
||||
// paths
|
||||
Paths map[string]*PathConf `json:"paths"`
|
||||
}
|
||||
@@ -251,52 +260,45 @@ func Load(fpath string) (*Conf, bool, error) {
|
||||
|
||||
// CheckAndFillMissing checks the configuration for errors and fills missing parameters.
|
||||
func (conf *Conf) CheckAndFillMissing() error {
|
||||
// general
|
||||
if conf.LogLevel == 0 {
|
||||
conf.LogLevel = LogLevel(logger.Info)
|
||||
}
|
||||
|
||||
if len(conf.LogDestinations) == 0 {
|
||||
conf.LogDestinations = LogDestinations{logger.DestinationStdout: {}}
|
||||
}
|
||||
|
||||
if conf.LogFile == "" {
|
||||
conf.LogFile = "rtsp-simple-server.log"
|
||||
}
|
||||
|
||||
if conf.ReadTimeout == 0 {
|
||||
conf.ReadTimeout = 10 * StringDuration(time.Second)
|
||||
}
|
||||
|
||||
if conf.WriteTimeout == 0 {
|
||||
conf.WriteTimeout = 10 * StringDuration(time.Second)
|
||||
}
|
||||
|
||||
if conf.ReadBufferCount == 0 {
|
||||
conf.ReadBufferCount = 512
|
||||
}
|
||||
if (conf.ReadBufferCount & (conf.ReadBufferCount - 1)) != 0 {
|
||||
return fmt.Errorf("'ReadBufferCount' must be a power of two")
|
||||
}
|
||||
|
||||
if conf.ExternalAuthenticationURL != "" {
|
||||
if !strings.HasPrefix(conf.ExternalAuthenticationURL, "http://") &&
|
||||
!strings.HasPrefix(conf.ExternalAuthenticationURL, "https://") {
|
||||
return fmt.Errorf("'externalAuthenticationURL' must be a HTTP URL")
|
||||
}
|
||||
}
|
||||
|
||||
if conf.APIAddress == "" {
|
||||
conf.APIAddress = "127.0.0.1:9997"
|
||||
}
|
||||
|
||||
if conf.MetricsAddress == "" {
|
||||
conf.MetricsAddress = "127.0.0.1:9998"
|
||||
}
|
||||
|
||||
if conf.PPROFAddress == "" {
|
||||
conf.PPROFAddress = "127.0.0.1:9999"
|
||||
}
|
||||
|
||||
// RTSP
|
||||
if len(conf.Protocols) == 0 {
|
||||
conf.Protocols = Protocols{
|
||||
Protocol(gortsplib.TransportUDP): {},
|
||||
@@ -304,7 +306,6 @@ func (conf *Conf) CheckAndFillMissing() error {
|
||||
Protocol(gortsplib.TransportTCP): {},
|
||||
}
|
||||
}
|
||||
|
||||
if conf.Encryption == EncryptionStrict {
|
||||
if _, ok := conf.Protocols[Protocol(gortsplib.TransportUDP)]; ok {
|
||||
return fmt.Errorf("strict encryption can't be used with the UDP transport protocol")
|
||||
@@ -314,87 +315,70 @@ func (conf *Conf) CheckAndFillMissing() error {
|
||||
return fmt.Errorf("strict encryption can't be used with the UDP-multicast transport protocol")
|
||||
}
|
||||
}
|
||||
|
||||
if conf.RTSPAddress == "" {
|
||||
conf.RTSPAddress = ":8554"
|
||||
}
|
||||
|
||||
if conf.RTSPSAddress == "" {
|
||||
conf.RTSPSAddress = ":8322"
|
||||
}
|
||||
|
||||
if conf.RTPAddress == "" {
|
||||
conf.RTPAddress = ":8000"
|
||||
}
|
||||
|
||||
if conf.RTCPAddress == "" {
|
||||
conf.RTCPAddress = ":8001"
|
||||
}
|
||||
|
||||
if conf.MulticastIPRange == "" {
|
||||
conf.MulticastIPRange = "224.1.0.0/16"
|
||||
}
|
||||
|
||||
if conf.MulticastRTPPort == 0 {
|
||||
conf.MulticastRTPPort = 8002
|
||||
}
|
||||
|
||||
if conf.MulticastRTCPPort == 0 {
|
||||
conf.MulticastRTCPPort = 8003
|
||||
}
|
||||
|
||||
if conf.ServerKey == "" {
|
||||
conf.ServerKey = "server.key"
|
||||
}
|
||||
|
||||
if conf.ServerCert == "" {
|
||||
conf.ServerCert = "server.crt"
|
||||
}
|
||||
|
||||
if len(conf.AuthMethods) == 0 {
|
||||
conf.AuthMethods = AuthMethods{headers.AuthBasic, headers.AuthDigest}
|
||||
}
|
||||
|
||||
// RTMP
|
||||
if conf.RTMPAddress == "" {
|
||||
conf.RTMPAddress = ":1935"
|
||||
}
|
||||
|
||||
if conf.RTMPSAddress == "" {
|
||||
conf.RTMPSAddress = ":1936"
|
||||
}
|
||||
|
||||
// HLS
|
||||
if conf.HLSAddress == "" {
|
||||
conf.HLSAddress = ":8888"
|
||||
}
|
||||
|
||||
if conf.HLSSegmentCount == 0 {
|
||||
conf.HLSSegmentCount = 7
|
||||
}
|
||||
|
||||
if conf.HLSSegmentDuration == 0 {
|
||||
conf.HLSSegmentDuration = 1 * StringDuration(time.Second)
|
||||
}
|
||||
|
||||
if conf.HLSPartDuration == 0 {
|
||||
conf.HLSPartDuration = 200 * StringDuration(time.Millisecond)
|
||||
}
|
||||
|
||||
if conf.HLSSegmentMaxSize == 0 {
|
||||
conf.HLSSegmentMaxSize = 50 * 1024 * 1024
|
||||
}
|
||||
|
||||
if conf.HLSAllowOrigin == "" {
|
||||
conf.HLSAllowOrigin = "*"
|
||||
}
|
||||
|
||||
if conf.HLSServerKey == "" {
|
||||
conf.HLSServerKey = "server.key"
|
||||
}
|
||||
|
||||
if conf.HLSServerCert == "" {
|
||||
conf.HLSServerCert = "server.crt"
|
||||
}
|
||||
|
||||
if conf.HLSSegmentCount == 0 {
|
||||
conf.HLSSegmentCount = 7
|
||||
}
|
||||
if conf.HLSSegmentDuration == 0 {
|
||||
conf.HLSSegmentDuration = 1 * StringDuration(time.Second)
|
||||
}
|
||||
if conf.HLSPartDuration == 0 {
|
||||
conf.HLSPartDuration = 200 * StringDuration(time.Millisecond)
|
||||
}
|
||||
if conf.HLSSegmentMaxSize == 0 {
|
||||
conf.HLSSegmentMaxSize = 50 * 1024 * 1024
|
||||
}
|
||||
if conf.HLSAllowOrigin == "" {
|
||||
conf.HLSAllowOrigin = "*"
|
||||
}
|
||||
switch conf.HLSVariant {
|
||||
case HLSVariantLowLatency:
|
||||
if conf.HLSSegmentCount < 7 {
|
||||
@@ -411,6 +395,23 @@ func (conf *Conf) CheckAndFillMissing() error {
|
||||
}
|
||||
}
|
||||
|
||||
// WebRTC
|
||||
if conf.WebRTCAddress == "" {
|
||||
conf.WebRTCAddress = ":8889"
|
||||
}
|
||||
if conf.WebRTCServerKey == "" {
|
||||
conf.WebRTCServerKey = "server.key"
|
||||
}
|
||||
if conf.WebRTCServerCert == "" {
|
||||
conf.WebRTCServerCert = "server.crt"
|
||||
}
|
||||
if conf.WebRTCAllowOrigin == "" {
|
||||
conf.WebRTCAllowOrigin = "*"
|
||||
}
|
||||
if conf.WebRTCICEServers == nil {
|
||||
conf.WebRTCICEServers = []string{"stun:stun.l.google.com:19302"}
|
||||
}
|
||||
|
||||
// do not add automatically "all", since user may want to
|
||||
// initialize all paths through API or hot reloading.
|
||||
if conf.Paths == nil {
|
||||
|
@@ -36,6 +36,7 @@ func (d *Credential) UnmarshalJSON(b []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// unmarshalEnv implements envUnmarshaler.
|
||||
func (d *Credential) unmarshalEnv(s string) error {
|
||||
return d.UnmarshalJSON([]byte(`"` + s + `"`))
|
||||
}
|
||||
|
@@ -57,6 +57,7 @@ func (d *Encryption) UnmarshalJSON(b []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// unmarshalEnv implements envUnmarshaler.
|
||||
func (d *Encryption) unmarshalEnv(s string) error {
|
||||
return d.UnmarshalJSON([]byte(`"` + s + `"`))
|
||||
}
|
||||
|
@@ -131,6 +131,14 @@ func loadEnvInternal(env map[string]string, prefix string, rv reflect.Value) err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
case reflect.Slice:
|
||||
if rt.Elem() == reflect.TypeOf("") {
|
||||
if ev, ok := env[prefix]; ok {
|
||||
rv.Set(reflect.ValueOf(strings.Split(ev, ",")))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("unsupported type: %v", rt)
|
||||
|
@@ -24,6 +24,7 @@ type testStruct struct {
|
||||
MyBool bool
|
||||
MyDuration StringDuration
|
||||
MyMap map[string]*mapEntry
|
||||
MySlice []string
|
||||
}
|
||||
|
||||
func TestEnvironment(t *testing.T) {
|
||||
@@ -51,6 +52,9 @@ func TestEnvironment(t *testing.T) {
|
||||
os.Setenv("MYPREFIX_MYMAP_MYKEY2_MYSTRUCT_MYPARAM", "456")
|
||||
defer os.Unsetenv("MYPREFIX_MYMAP_MYKEY2_MYSTRUCT_MYPARAM")
|
||||
|
||||
os.Setenv("MYPREFIX_MYSLICE", "val1,val2")
|
||||
defer os.Unsetenv("MYPREFIX_MYSLICE")
|
||||
|
||||
var s testStruct
|
||||
err := loadFromEnvironment("MYPREFIX", &s)
|
||||
require.NoError(t, err)
|
||||
@@ -68,4 +72,6 @@ func TestEnvironment(t *testing.T) {
|
||||
require.Equal(t, true, ok)
|
||||
require.Equal(t, "asd", v.MyValue)
|
||||
require.Equal(t, 456, v.MyStruct.MyParam)
|
||||
|
||||
require.Equal(t, []string{"val1", "val2"}, s.MySlice)
|
||||
}
|
||||
|
@@ -59,6 +59,7 @@ func (d *HLSVariant) UnmarshalJSON(b []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// unmarshalEnv implements envUnmarshaler.
|
||||
func (d *HLSVariant) unmarshalEnv(s string) error {
|
||||
return d.UnmarshalJSON([]byte(`"` + s + `"`))
|
||||
}
|
||||
|
@@ -48,6 +48,7 @@ func (d *IPsOrCIDRs) UnmarshalJSON(b []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// unmarshalEnv implements envUnmarshaler.
|
||||
func (d *IPsOrCIDRs) unmarshalEnv(s string) error {
|
||||
byts, _ := json.Marshal(strings.Split(s, ","))
|
||||
return d.UnmarshalJSON(byts)
|
||||
|
@@ -68,6 +68,7 @@ func (d *LogDestinations) UnmarshalJSON(b []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// unmarshalEnv implements envUnmarshaler.
|
||||
func (d *LogDestinations) unmarshalEnv(s string) error {
|
||||
byts, _ := json.Marshal(strings.Split(s, ","))
|
||||
return d.UnmarshalJSON(byts)
|
||||
|
@@ -58,6 +58,7 @@ func (d *LogLevel) UnmarshalJSON(b []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// unmarshalEnv implements envUnmarshaler.
|
||||
func (d *LogLevel) unmarshalEnv(s string) error {
|
||||
return d.UnmarshalJSON([]byte(`"` + s + `"`))
|
||||
}
|
||||
|
@@ -71,6 +71,7 @@ func (d *Protocols) UnmarshalJSON(b []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// unmarshalEnv implements envUnmarshaler.
|
||||
func (d *Protocols) unmarshalEnv(s string) error {
|
||||
byts, _ := json.Marshal(strings.Split(s, ","))
|
||||
return d.UnmarshalJSON(byts)
|
||||
|
@@ -63,6 +63,7 @@ func (d *SourceProtocol) UnmarshalJSON(b []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// unmarshalEnv implements envUnmarshaler.
|
||||
func (d *SourceProtocol) unmarshalEnv(s string) error {
|
||||
return d.UnmarshalJSON([]byte(`"` + s + `"`))
|
||||
}
|
||||
|
@@ -30,6 +30,7 @@ func (d *StringDuration) UnmarshalJSON(b []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// unmarshalEnv implements envUnmarshaler.
|
||||
func (d *StringDuration) unmarshalEnv(s string) error {
|
||||
return d.UnmarshalJSON([]byte(`"` + s + `"`))
|
||||
}
|
||||
|
@@ -30,6 +30,7 @@ func (s *StringSize) UnmarshalJSON(b []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// unmarshalEnv implements envUnmarshaler.
|
||||
func (s *StringSize) unmarshalEnv(v string) error {
|
||||
return s.UnmarshalJSON([]byte(`"` + v + `"`))
|
||||
}
|
||||
|
@@ -5,7 +5,6 @@ import (
|
||||
"encoding/json"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"reflect"
|
||||
"sync"
|
||||
|
||||
@@ -86,6 +85,10 @@ type apiPathManager interface {
|
||||
apiPathsList() pathAPIPathsListRes
|
||||
}
|
||||
|
||||
type apiHLSServer interface {
|
||||
apiMuxersList() hlsServerAPIMuxersListRes
|
||||
}
|
||||
|
||||
type apiRTSPServer interface {
|
||||
apiConnsList() rtspServerAPIConnsListRes
|
||||
apiSessionsList() rtspServerAPISessionsListRes
|
||||
@@ -97,15 +100,16 @@ type apiRTMPServer interface {
|
||||
apiConnsKick(id string) rtmpServerAPIConnsKickRes
|
||||
}
|
||||
|
||||
type apiHLSServer interface {
|
||||
apiHLSMuxersList() hlsServerAPIMuxersListRes
|
||||
}
|
||||
|
||||
type apiParent interface {
|
||||
Log(logger.Level, string, ...interface{})
|
||||
apiConfigSet(conf *conf.Conf)
|
||||
}
|
||||
|
||||
type apiWebRTCServer interface {
|
||||
apiConnsList() webRTCServerAPIConnsListRes
|
||||
apiConnsKick(id string) webRTCServerAPIConnsKickRes
|
||||
}
|
||||
|
||||
type api struct {
|
||||
conf *conf.Conf
|
||||
pathManager apiPathManager
|
||||
@@ -114,6 +118,7 @@ type api struct {
|
||||
rtmpServer apiRTMPServer
|
||||
rtmpsServer apiRTMPServer
|
||||
hlsServer apiHLSServer
|
||||
webRTCServer apiWebRTCServer
|
||||
parent apiParent
|
||||
|
||||
ln net.Listener
|
||||
@@ -130,6 +135,7 @@ func newAPI(
|
||||
rtmpServer apiRTMPServer,
|
||||
rtmpsServer apiRTMPServer,
|
||||
hlsServer apiHLSServer,
|
||||
webRTCServer apiWebRTCServer,
|
||||
parent apiParent,
|
||||
) (*api, error) {
|
||||
ln, err := net.Listen("tcp", address)
|
||||
@@ -145,14 +151,16 @@ func newAPI(
|
||||
rtmpServer: rtmpServer,
|
||||
rtmpsServer: rtmpsServer,
|
||||
hlsServer: hlsServer,
|
||||
webRTCServer: webRTCServer,
|
||||
parent: parent,
|
||||
ln: ln,
|
||||
}
|
||||
|
||||
router := gin.New()
|
||||
router.SetTrustedProxies(nil)
|
||||
router.NoRoute(a.mwLog)
|
||||
group := router.Group("/", a.mwLog)
|
||||
mwLog := httpLoggerMiddleware(a)
|
||||
router.NoRoute(mwLog)
|
||||
group := router.Group("/", mwLog)
|
||||
|
||||
group.GET("/v1/config/get", a.onConfigGet)
|
||||
group.POST("/v1/config/set", a.onConfigSet)
|
||||
@@ -160,6 +168,10 @@ func newAPI(
|
||||
group.POST("/v1/config/paths/edit/*name", a.onConfigPathsEdit)
|
||||
group.POST("/v1/config/paths/remove/*name", a.onConfigPathsDelete)
|
||||
|
||||
if !interfaceIsEmpty(a.hlsServer) {
|
||||
group.GET("/v1/hlsmuxers/list", a.onHLSMuxersList)
|
||||
}
|
||||
|
||||
group.GET("/v1/paths/list", a.onPathsList)
|
||||
|
||||
if !interfaceIsEmpty(a.rtspServer) {
|
||||
@@ -184,8 +196,9 @@ func newAPI(
|
||||
group.POST("/v1/rtmpsconns/kick/:id", a.onRTMPSConnsKick)
|
||||
}
|
||||
|
||||
if !interfaceIsEmpty(a.hlsServer) {
|
||||
group.GET("/v1/hlsmuxers/list", a.onHLSMuxersList)
|
||||
if !interfaceIsEmpty(a.webRTCServer) {
|
||||
group.GET("/v1/webrtcconns/list", a.onWebRTCConnsList)
|
||||
group.POST("/v1/webrtcconns/kick/:id", a.onWebRTCConnsKick)
|
||||
}
|
||||
|
||||
a.s = &http.Server{Handler: router}
|
||||
@@ -207,22 +220,6 @@ func (a *api) log(level logger.Level, format string, args ...interface{}) {
|
||||
a.parent.Log(level, "[API] "+format, args...)
|
||||
}
|
||||
|
||||
func (a *api) mwLog(ctx *gin.Context) {
|
||||
a.log(logger.Info, "[conn %v] %s %s", ctx.Request.RemoteAddr, ctx.Request.Method, ctx.Request.URL.Path)
|
||||
|
||||
byts, _ := httputil.DumpRequest(ctx.Request, true)
|
||||
a.log(logger.Debug, "[conn %v] [c->s] %s", ctx.Request.RemoteAddr, string(byts))
|
||||
|
||||
logw := &httpLogWriter{ResponseWriter: ctx.Writer}
|
||||
ctx.Writer = logw
|
||||
|
||||
ctx.Writer.Header().Set("Server", "rtsp-simple-server")
|
||||
|
||||
ctx.Next()
|
||||
|
||||
a.log(logger.Debug, "[conn %v] [s->c] %s", ctx.Request.RemoteAddr, logw.dump())
|
||||
}
|
||||
|
||||
func (a *api) onConfigGet(ctx *gin.Context) {
|
||||
a.mutex.Lock()
|
||||
c := a.conf
|
||||
@@ -419,7 +416,6 @@ func (a *api) onRTSPSessionsKick(ctx *gin.Context) {
|
||||
|
||||
res := a.rtspServer.apiSessionsKick(id)
|
||||
if res.err != nil {
|
||||
ctx.AbortWithStatus(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -451,7 +447,6 @@ func (a *api) onRTSPSSessionsKick(ctx *gin.Context) {
|
||||
|
||||
res := a.rtspsServer.apiSessionsKick(id)
|
||||
if res.err != nil {
|
||||
ctx.AbortWithStatus(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -473,7 +468,6 @@ func (a *api) onRTMPConnsKick(ctx *gin.Context) {
|
||||
|
||||
res := a.rtmpServer.apiConnsKick(id)
|
||||
if res.err != nil {
|
||||
ctx.AbortWithStatus(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -495,7 +489,6 @@ func (a *api) onRTMPSConnsKick(ctx *gin.Context) {
|
||||
|
||||
res := a.rtmpsServer.apiConnsKick(id)
|
||||
if res.err != nil {
|
||||
ctx.AbortWithStatus(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -503,7 +496,7 @@ func (a *api) onRTMPSConnsKick(ctx *gin.Context) {
|
||||
}
|
||||
|
||||
func (a *api) onHLSMuxersList(ctx *gin.Context) {
|
||||
res := a.hlsServer.apiHLSMuxersList()
|
||||
res := a.hlsServer.apiMuxersList()
|
||||
if res.err != nil {
|
||||
ctx.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
@@ -512,6 +505,27 @@ func (a *api) onHLSMuxersList(ctx *gin.Context) {
|
||||
ctx.JSON(http.StatusOK, res.data)
|
||||
}
|
||||
|
||||
func (a *api) onWebRTCConnsList(ctx *gin.Context) {
|
||||
res := a.webRTCServer.apiConnsList()
|
||||
if res.err != nil {
|
||||
ctx.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, res.data)
|
||||
}
|
||||
|
||||
func (a *api) onWebRTCConnsKick(ctx *gin.Context) {
|
||||
id := ctx.Param("id")
|
||||
|
||||
res := a.webRTCServer.apiConnsKick(id)
|
||||
if res.err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
// confReload is called by core.
|
||||
func (a *api) confReload(conf *conf.Conf) {
|
||||
a.mutex.Lock()
|
||||
|
@@ -38,6 +38,7 @@ type Core struct {
|
||||
rtmpServer *rtmpServer
|
||||
rtmpsServer *rtmpServer
|
||||
hlsServer *hlsServer
|
||||
webRTCServer *webRTCServer
|
||||
api *api
|
||||
confWatcher *confwatcher.ConfWatcher
|
||||
|
||||
@@ -180,7 +181,8 @@ func (p *Core) createResources(initial bool) error {
|
||||
p.logger, err = logger.New(
|
||||
logger.Level(p.conf.LogLevel),
|
||||
p.conf.LogDestinations,
|
||||
p.conf.LogFile)
|
||||
p.conf.LogFile,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -201,7 +203,8 @@ func (p *Core) createResources(initial bool) error {
|
||||
if p.metrics == nil {
|
||||
p.metrics, err = newMetrics(
|
||||
p.conf.MetricsAddress,
|
||||
p)
|
||||
p,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -212,7 +215,8 @@ func (p *Core) createResources(initial bool) error {
|
||||
if p.pprof == nil {
|
||||
p.pprof, err = newPPROF(
|
||||
p.conf.PPROFAddress,
|
||||
p)
|
||||
p,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -229,7 +233,8 @@ func (p *Core) createResources(initial bool) error {
|
||||
p.conf.Paths,
|
||||
p.externalCmdPool,
|
||||
p.metrics,
|
||||
p)
|
||||
p,
|
||||
)
|
||||
}
|
||||
|
||||
if !p.conf.RTSPDisable &&
|
||||
@@ -263,7 +268,8 @@ func (p *Core) createResources(initial bool) error {
|
||||
p.externalCmdPool,
|
||||
p.metrics,
|
||||
p.pathManager,
|
||||
p)
|
||||
p,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -299,7 +305,8 @@ func (p *Core) createResources(initial bool) error {
|
||||
p.externalCmdPool,
|
||||
p.metrics,
|
||||
p.pathManager,
|
||||
p)
|
||||
p,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -326,7 +333,8 @@ func (p *Core) createResources(initial bool) error {
|
||||
p.externalCmdPool,
|
||||
p.metrics,
|
||||
p.pathManager,
|
||||
p)
|
||||
p,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -353,7 +361,8 @@ func (p *Core) createResources(initial bool) error {
|
||||
p.externalCmdPool,
|
||||
p.metrics,
|
||||
p.pathManager,
|
||||
p)
|
||||
p,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -365,6 +374,9 @@ func (p *Core) createResources(initial bool) error {
|
||||
p.hlsServer, err = newHLSServer(
|
||||
p.ctx,
|
||||
p.conf.HLSAddress,
|
||||
p.conf.HLSEncryption,
|
||||
p.conf.HLSServerKey,
|
||||
p.conf.HLSServerCert,
|
||||
p.conf.ExternalAuthenticationURL,
|
||||
p.conf.HLSAlwaysRemux,
|
||||
p.conf.HLSVariant,
|
||||
@@ -373,14 +385,34 @@ func (p *Core) createResources(initial bool) error {
|
||||
p.conf.HLSPartDuration,
|
||||
p.conf.HLSSegmentMaxSize,
|
||||
p.conf.HLSAllowOrigin,
|
||||
p.conf.HLSEncryption,
|
||||
p.conf.HLSServerKey,
|
||||
p.conf.HLSServerCert,
|
||||
p.conf.HLSTrustedProxies,
|
||||
p.conf.ReadBufferCount,
|
||||
p.pathManager,
|
||||
p.metrics,
|
||||
p)
|
||||
p,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if p.conf.WebRTC {
|
||||
if p.webRTCServer == nil {
|
||||
p.webRTCServer, err = newWebRTCServer(
|
||||
p.ctx,
|
||||
p.conf.ExternalAuthenticationURL,
|
||||
p.conf.WebRTCAddress,
|
||||
p.conf.WebRTCServerKey,
|
||||
p.conf.WebRTCServerCert,
|
||||
p.conf.WebRTCAllowOrigin,
|
||||
p.conf.WebRTCTrustedProxies,
|
||||
p.conf.WebRTCICEServers,
|
||||
p.conf.ReadBufferCount,
|
||||
p.pathManager,
|
||||
p.metrics,
|
||||
p,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -398,7 +430,9 @@ func (p *Core) createResources(initial bool) error {
|
||||
p.rtmpServer,
|
||||
p.rtmpsServer,
|
||||
p.hlsServer,
|
||||
p)
|
||||
p.webRTCServer,
|
||||
p,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -416,41 +450,29 @@ func (p *Core) createResources(initial bool) error {
|
||||
}
|
||||
|
||||
func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
|
||||
closeLogger := false
|
||||
if newConf == nil ||
|
||||
closeLogger := newConf == nil ||
|
||||
!reflect.DeepEqual(newConf.LogDestinations, p.conf.LogDestinations) ||
|
||||
newConf.LogFile != p.conf.LogFile {
|
||||
closeLogger = true
|
||||
}
|
||||
newConf.LogFile != p.conf.LogFile
|
||||
|
||||
closeMetrics := false
|
||||
if newConf == nil ||
|
||||
closeMetrics := newConf == nil ||
|
||||
newConf.Metrics != p.conf.Metrics ||
|
||||
newConf.MetricsAddress != p.conf.MetricsAddress {
|
||||
closeMetrics = true
|
||||
}
|
||||
newConf.MetricsAddress != p.conf.MetricsAddress
|
||||
|
||||
closePPROF := false
|
||||
if newConf == nil ||
|
||||
closePPROF := newConf == nil ||
|
||||
newConf.PPROF != p.conf.PPROF ||
|
||||
newConf.PPROFAddress != p.conf.PPROFAddress {
|
||||
closePPROF = true
|
||||
}
|
||||
newConf.PPROFAddress != p.conf.PPROFAddress
|
||||
|
||||
closePathManager := false
|
||||
if newConf == nil ||
|
||||
closePathManager := newConf == nil ||
|
||||
newConf.RTSPAddress != p.conf.RTSPAddress ||
|
||||
newConf.ReadTimeout != p.conf.ReadTimeout ||
|
||||
newConf.WriteTimeout != p.conf.WriteTimeout ||
|
||||
newConf.ReadBufferCount != p.conf.ReadBufferCount ||
|
||||
closeMetrics {
|
||||
closePathManager = true
|
||||
} else if !reflect.DeepEqual(newConf.Paths, p.conf.Paths) {
|
||||
closeMetrics
|
||||
if !closePathManager && !reflect.DeepEqual(newConf.Paths, p.conf.Paths) {
|
||||
p.pathManager.confReload(newConf.Paths)
|
||||
}
|
||||
|
||||
closeRTSPServer := false
|
||||
if newConf == nil ||
|
||||
closeRTSPServer := newConf == nil ||
|
||||
newConf.RTSPDisable != p.conf.RTSPDisable ||
|
||||
newConf.Encryption != p.conf.Encryption ||
|
||||
newConf.ExternalAuthenticationURL != p.conf.ExternalAuthenticationURL ||
|
||||
@@ -470,12 +492,9 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
|
||||
newConf.RunOnConnect != p.conf.RunOnConnect ||
|
||||
newConf.RunOnConnectRestart != p.conf.RunOnConnectRestart ||
|
||||
closeMetrics ||
|
||||
closePathManager {
|
||||
closeRTSPServer = true
|
||||
}
|
||||
closePathManager
|
||||
|
||||
closeRTSPSServer := false
|
||||
if newConf == nil ||
|
||||
closeRTSPSServer := newConf == nil ||
|
||||
newConf.RTSPDisable != p.conf.RTSPDisable ||
|
||||
newConf.Encryption != p.conf.Encryption ||
|
||||
newConf.ExternalAuthenticationURL != p.conf.ExternalAuthenticationURL ||
|
||||
@@ -491,12 +510,9 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
|
||||
newConf.RunOnConnect != p.conf.RunOnConnect ||
|
||||
newConf.RunOnConnectRestart != p.conf.RunOnConnectRestart ||
|
||||
closeMetrics ||
|
||||
closePathManager {
|
||||
closeRTSPSServer = true
|
||||
}
|
||||
closePathManager
|
||||
|
||||
closeRTMPServer := false
|
||||
if newConf == nil ||
|
||||
closeRTMPServer := newConf == nil ||
|
||||
newConf.RTMPDisable != p.conf.RTMPDisable ||
|
||||
newConf.RTMPEncryption != p.conf.RTMPEncryption ||
|
||||
newConf.RTMPAddress != p.conf.RTMPAddress ||
|
||||
@@ -508,12 +524,9 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
|
||||
newConf.RunOnConnect != p.conf.RunOnConnect ||
|
||||
newConf.RunOnConnectRestart != p.conf.RunOnConnectRestart ||
|
||||
closeMetrics ||
|
||||
closePathManager {
|
||||
closeRTMPServer = true
|
||||
}
|
||||
closePathManager
|
||||
|
||||
closeRTMPSServer := false
|
||||
if newConf == nil ||
|
||||
closeRTMPSServer := newConf == nil ||
|
||||
newConf.RTMPDisable != p.conf.RTMPDisable ||
|
||||
newConf.RTMPEncryption != p.conf.RTMPEncryption ||
|
||||
newConf.RTMPSAddress != p.conf.RTMPSAddress ||
|
||||
@@ -527,14 +540,14 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
|
||||
newConf.RunOnConnect != p.conf.RunOnConnect ||
|
||||
newConf.RunOnConnectRestart != p.conf.RunOnConnectRestart ||
|
||||
closeMetrics ||
|
||||
closePathManager {
|
||||
closeRTMPSServer = true
|
||||
}
|
||||
closePathManager
|
||||
|
||||
closeHLSServer := false
|
||||
if newConf == nil ||
|
||||
closeHLSServer := newConf == nil ||
|
||||
newConf.HLSDisable != p.conf.HLSDisable ||
|
||||
newConf.HLSAddress != p.conf.HLSAddress ||
|
||||
newConf.HLSEncryption != p.conf.HLSEncryption ||
|
||||
newConf.HLSServerKey != p.conf.HLSServerKey ||
|
||||
newConf.HLSServerCert != p.conf.HLSServerCert ||
|
||||
newConf.ExternalAuthenticationURL != p.conf.ExternalAuthenticationURL ||
|
||||
newConf.HLSAlwaysRemux != p.conf.HLSAlwaysRemux ||
|
||||
newConf.HLSVariant != p.conf.HLSVariant ||
|
||||
@@ -543,27 +556,33 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
|
||||
newConf.HLSPartDuration != p.conf.HLSPartDuration ||
|
||||
newConf.HLSSegmentMaxSize != p.conf.HLSSegmentMaxSize ||
|
||||
newConf.HLSAllowOrigin != p.conf.HLSAllowOrigin ||
|
||||
newConf.HLSEncryption != p.conf.HLSEncryption ||
|
||||
newConf.HLSServerKey != p.conf.HLSServerKey ||
|
||||
newConf.HLSServerCert != p.conf.HLSServerCert ||
|
||||
!reflect.DeepEqual(newConf.HLSTrustedProxies, p.conf.HLSTrustedProxies) ||
|
||||
newConf.ReadBufferCount != p.conf.ReadBufferCount ||
|
||||
closePathManager ||
|
||||
closeMetrics {
|
||||
closeHLSServer = true
|
||||
}
|
||||
closeMetrics
|
||||
|
||||
closeAPI := false
|
||||
if newConf == nil ||
|
||||
closeWebrtcServer := newConf == nil ||
|
||||
newConf.WebRTC != p.conf.WebRTC ||
|
||||
newConf.ExternalAuthenticationURL != p.conf.ExternalAuthenticationURL ||
|
||||
newConf.WebRTCAddress != p.conf.WebRTCAddress ||
|
||||
newConf.WebRTCServerKey != p.conf.WebRTCServerKey ||
|
||||
newConf.WebRTCServerCert != p.conf.WebRTCServerCert ||
|
||||
newConf.WebRTCAllowOrigin != p.conf.WebRTCAllowOrigin ||
|
||||
!reflect.DeepEqual(newConf.WebRTCTrustedProxies, p.conf.WebRTCTrustedProxies) ||
|
||||
!reflect.DeepEqual(newConf.WebRTCICEServers, p.conf.WebRTCICEServers) ||
|
||||
newConf.ReadBufferCount != p.conf.ReadBufferCount ||
|
||||
closeMetrics ||
|
||||
closePathManager
|
||||
|
||||
closeAPI := newConf == nil ||
|
||||
newConf.API != p.conf.API ||
|
||||
newConf.APIAddress != p.conf.APIAddress ||
|
||||
closePathManager ||
|
||||
closeRTSPServer ||
|
||||
closeRTSPSServer ||
|
||||
closeRTMPServer ||
|
||||
closeHLSServer {
|
||||
closeAPI = true
|
||||
}
|
||||
closeHLSServer ||
|
||||
closeWebrtcServer
|
||||
|
||||
if newConf == nil && p.confWatcher != nil {
|
||||
p.confWatcher.Close()
|
||||
@@ -594,6 +613,11 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
|
||||
p.pathManager = nil
|
||||
}
|
||||
|
||||
if closeWebrtcServer && p.webRTCServer != nil {
|
||||
p.webRTCServer.close()
|
||||
p.webRTCServer = nil
|
||||
}
|
||||
|
||||
if closeHLSServer && p.hlsServer != nil {
|
||||
p.hlsServer.close()
|
||||
p.hlsServer = nil
|
||||
@@ -632,7 +656,6 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
|
||||
|
||||
func (p *Core) reloadConf(newConf *conf.Conf, calledByAPI bool) error {
|
||||
p.closeResources(newConf, calledByAPI)
|
||||
|
||||
p.conf = newConf
|
||||
return p.createResources(false)
|
||||
}
|
||||
|
@@ -16,12 +16,15 @@ func newFormatProcessor(forma format.Format, generateRTPPackets bool) (formatPro
|
||||
case *format.H265:
|
||||
return newFormatProcessorH265(forma, generateRTPPackets)
|
||||
|
||||
case *format.VP8:
|
||||
return newFormatProcessorVP8(forma, generateRTPPackets)
|
||||
|
||||
case *format.VP9:
|
||||
return newFormatProcessorVP9(forma, generateRTPPackets)
|
||||
|
||||
case *format.MPEG4Audio:
|
||||
return newFormatProcessorMPEG4Audio(forma, generateRTPPackets)
|
||||
|
||||
case *format.Opus:
|
||||
return newFormatProcessorOpus(forma, generateRTPPackets)
|
||||
|
||||
default:
|
||||
return newFormatProcessorGeneric(forma, generateRTPPackets)
|
||||
}
|
||||
|
@@ -45,7 +45,7 @@ func newFormatProcessorMPEG4Audio(
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (t *formatProcessorMPEG4Audio) process(dat data, hasNonRTSPReaders bool) error {
|
||||
func (t *formatProcessorMPEG4Audio) process(dat data, hasNonRTSPReaders bool) error { //nolint:dupl
|
||||
tdata := dat.(*dataMPEG4Audio)
|
||||
|
||||
if tdata.rtpPackets != nil {
|
||||
|
@@ -5,36 +5,36 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/aler9/gortsplib/v2/pkg/format"
|
||||
"github.com/aler9/gortsplib/v2/pkg/formatdecenc/rtpsimpleaudio"
|
||||
"github.com/aler9/gortsplib/v2/pkg/formatdecenc/rtpvp8"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
type dataOpus struct {
|
||||
type dataVP8 struct {
|
||||
rtpPackets []*rtp.Packet
|
||||
ntp time.Time
|
||||
pts time.Duration
|
||||
au []byte
|
||||
frame []byte
|
||||
}
|
||||
|
||||
func (d *dataOpus) getRTPPackets() []*rtp.Packet {
|
||||
func (d *dataVP8) getRTPPackets() []*rtp.Packet {
|
||||
return d.rtpPackets
|
||||
}
|
||||
|
||||
func (d *dataOpus) getNTP() time.Time {
|
||||
func (d *dataVP8) getNTP() time.Time {
|
||||
return d.ntp
|
||||
}
|
||||
|
||||
type formatProcessorOpus struct {
|
||||
format *format.Opus
|
||||
encoder *rtpsimpleaudio.Encoder
|
||||
decoder *rtpsimpleaudio.Decoder
|
||||
type formatProcessorVP8 struct {
|
||||
format *format.VP8
|
||||
encoder *rtpvp8.Encoder
|
||||
decoder *rtpvp8.Decoder
|
||||
}
|
||||
|
||||
func newFormatProcessorOpus(
|
||||
forma *format.Opus,
|
||||
func newFormatProcessorVP8(
|
||||
forma *format.VP8,
|
||||
allocateEncoder bool,
|
||||
) (*formatProcessorOpus, error) {
|
||||
t := &formatProcessorOpus{
|
||||
) (*formatProcessorVP8, error) {
|
||||
t := &formatProcessorVP8{
|
||||
format: forma,
|
||||
}
|
||||
|
||||
@@ -45,18 +45,8 @@ func newFormatProcessorOpus(
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (t *formatProcessorOpus) generateRTPPackets(tdata *dataOpus) error {
|
||||
pkt, err := t.encoder.Encode(tdata.au, tdata.pts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tdata.rtpPackets = []*rtp.Packet{pkt}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *formatProcessorOpus) process(dat data, hasNonRTSPReaders bool) error {
|
||||
tdata := dat.(*dataOpus)
|
||||
func (t *formatProcessorVP8) process(dat data, hasNonRTSPReaders bool) error { //nolint:dupl
|
||||
tdata := dat.(*dataVP8)
|
||||
|
||||
if tdata.rtpPackets != nil {
|
||||
pkt := tdata.rtpPackets[0]
|
||||
@@ -76,12 +66,15 @@ func (t *formatProcessorOpus) process(dat data, hasNonRTSPReaders bool) error {
|
||||
t.decoder = t.format.CreateDecoder()
|
||||
}
|
||||
|
||||
au, pts, err := t.decoder.Decode(pkt)
|
||||
frame, pts, err := t.decoder.Decode(pkt)
|
||||
if err != nil {
|
||||
if err == rtpvp8.ErrMorePacketsNeeded {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
tdata.au = au
|
||||
tdata.frame = frame
|
||||
tdata.pts = pts
|
||||
}
|
||||
|
||||
@@ -89,5 +82,11 @@ func (t *formatProcessorOpus) process(dat data, hasNonRTSPReaders bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
return t.generateRTPPackets(tdata)
|
||||
pkts, err := t.encoder.Encode(tdata.frame, tdata.pts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tdata.rtpPackets = pkts
|
||||
return nil
|
||||
}
|
92
internal/core/formatprocessor_vp9.go
Normal file
92
internal/core/formatprocessor_vp9.go
Normal file
@@ -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
|
||||
}
|
67
internal/core/hls_index.html
Normal file
67
internal/core/hls_index.html
Normal file
@@ -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>
|
@@ -3,6 +3,7 @@ package core
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
_ "embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
@@ -27,73 +28,8 @@ const (
|
||||
closeAfterInactivity = 60 * time.Second
|
||||
)
|
||||
|
||||
const index = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
}
|
||||
#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>
|
||||
`
|
||||
//go:embed hls_index.html
|
||||
var hlsIndex []byte
|
||||
|
||||
type hlsMuxerResponse struct {
|
||||
muxer *hlsMuxer
|
||||
@@ -296,7 +232,6 @@ func (m *hlsMuxer) runInner(innerCtx context.Context, innerReady chan struct{})
|
||||
res := m.pathManager.readerAdd(pathReaderAddReq{
|
||||
author: m,
|
||||
pathName: m.pathName,
|
||||
authenticate: nil,
|
||||
})
|
||||
if res.err != nil {
|
||||
return res.err
|
||||
@@ -487,7 +422,7 @@ func (m *hlsMuxer) handleRequest(req *hlsMuxerRequest) func() *hls.MuxerFileResp
|
||||
Header: map[string]string{
|
||||
"Content-Type": `text/html`,
|
||||
},
|
||||
Body: bytes.NewReader([]byte(index)),
|
||||
Body: bytes.NewReader(hlsIndex),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -574,8 +509,8 @@ func (m *hlsMuxer) request(req *hlsMuxerRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
// apiHLSMuxersList is called by api.
|
||||
func (m *hlsMuxer) apiHLSMuxersList(req hlsServerAPIMuxersListSubReq) {
|
||||
// apiMuxersList is called by api.
|
||||
func (m *hlsMuxer) apiMuxersList(req hlsServerAPIMuxersListSubReq) {
|
||||
req.res = make(chan struct{})
|
||||
select {
|
||||
case m.chAPIHLSMuxersList <- req:
|
||||
|
@@ -8,7 +8,6 @@ import (
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
gopath "path"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -57,14 +56,14 @@ type hlsServerParent interface {
|
||||
|
||||
type hlsServer struct {
|
||||
externalAuthenticationURL string
|
||||
hlsAlwaysRemux bool
|
||||
hlsVariant conf.HLSVariant
|
||||
hlsSegmentCount int
|
||||
hlsSegmentDuration conf.StringDuration
|
||||
hlsPartDuration conf.StringDuration
|
||||
hlsSegmentMaxSize conf.StringSize
|
||||
hlsAllowOrigin string
|
||||
hlsTrustedProxies conf.IPsOrCIDRs
|
||||
alwaysRemux bool
|
||||
variant conf.HLSVariant
|
||||
segmentCount int
|
||||
segmentDuration conf.StringDuration
|
||||
partDuration conf.StringDuration
|
||||
segmentMaxSize conf.StringSize
|
||||
allowOrigin string
|
||||
trustedProxies conf.IPsOrCIDRs
|
||||
readBufferCount int
|
||||
pathManager *pathManager
|
||||
metrics *metrics
|
||||
@@ -88,18 +87,18 @@ type hlsServer struct {
|
||||
func newHLSServer(
|
||||
parentCtx context.Context,
|
||||
address string,
|
||||
encryption bool,
|
||||
serverKey string,
|
||||
serverCert string,
|
||||
externalAuthenticationURL string,
|
||||
hlsAlwaysRemux bool,
|
||||
hlsVariant conf.HLSVariant,
|
||||
hlsSegmentCount int,
|
||||
hlsSegmentDuration conf.StringDuration,
|
||||
hlsPartDuration conf.StringDuration,
|
||||
hlsSegmentMaxSize conf.StringSize,
|
||||
hlsAllowOrigin string,
|
||||
hlsEncryption bool,
|
||||
hlsServerKey string,
|
||||
hlsServerCert string,
|
||||
hlsTrustedProxies conf.IPsOrCIDRs,
|
||||
alwaysRemux bool,
|
||||
variant conf.HLSVariant,
|
||||
segmentCount int,
|
||||
segmentDuration conf.StringDuration,
|
||||
partDuration conf.StringDuration,
|
||||
segmentMaxSize conf.StringSize,
|
||||
allowOrigin string,
|
||||
trustedProxies conf.IPsOrCIDRs,
|
||||
readBufferCount int,
|
||||
pathManager *pathManager,
|
||||
metrics *metrics,
|
||||
@@ -111,8 +110,8 @@ func newHLSServer(
|
||||
}
|
||||
|
||||
var tlsConfig *tls.Config
|
||||
if hlsEncryption {
|
||||
crt, err := tls.LoadX509KeyPair(hlsServerCert, hlsServerKey)
|
||||
if encryption {
|
||||
crt, err := tls.LoadX509KeyPair(serverCert, serverKey)
|
||||
if err != nil {
|
||||
ln.Close()
|
||||
return nil, err
|
||||
@@ -127,14 +126,14 @@ func newHLSServer(
|
||||
|
||||
s := &hlsServer{
|
||||
externalAuthenticationURL: externalAuthenticationURL,
|
||||
hlsAlwaysRemux: hlsAlwaysRemux,
|
||||
hlsVariant: hlsVariant,
|
||||
hlsSegmentCount: hlsSegmentCount,
|
||||
hlsSegmentDuration: hlsSegmentDuration,
|
||||
hlsPartDuration: hlsPartDuration,
|
||||
hlsSegmentMaxSize: hlsSegmentMaxSize,
|
||||
hlsAllowOrigin: hlsAllowOrigin,
|
||||
hlsTrustedProxies: hlsTrustedProxies,
|
||||
alwaysRemux: alwaysRemux,
|
||||
variant: variant,
|
||||
segmentCount: segmentCount,
|
||||
segmentDuration: segmentDuration,
|
||||
partDuration: partDuration,
|
||||
segmentMaxSize: segmentMaxSize,
|
||||
allowOrigin: allowOrigin,
|
||||
trustedProxies: trustedProxies,
|
||||
readBufferCount: readBufferCount,
|
||||
pathManager: pathManager,
|
||||
parent: parent,
|
||||
@@ -180,10 +179,10 @@ func (s *hlsServer) run() {
|
||||
defer s.wg.Done()
|
||||
|
||||
router := gin.New()
|
||||
router.NoRoute(s.onRequest)
|
||||
router.NoRoute(httpLoggerMiddleware(s), s.onRequest)
|
||||
|
||||
tmp := make([]string, len(s.hlsTrustedProxies))
|
||||
for i, entry := range s.hlsTrustedProxies {
|
||||
tmp := make([]string, len(s.trustedProxies))
|
||||
for i, entry := range s.trustedProxies {
|
||||
tmp[i] = entry.String()
|
||||
}
|
||||
router.SetTrustedProxies(tmp)
|
||||
@@ -204,12 +203,12 @@ outer:
|
||||
for {
|
||||
select {
|
||||
case pa := <-s.chPathSourceReady:
|
||||
if s.hlsAlwaysRemux {
|
||||
if s.alwaysRemux {
|
||||
s.findOrCreateMuxer(pa.Name(), "", nil)
|
||||
}
|
||||
|
||||
case pa := <-s.chPathSourceNotReady:
|
||||
if s.hlsAlwaysRemux {
|
||||
if s.alwaysRemux {
|
||||
c, ok := s.muxers[pa.Name()]
|
||||
if ok {
|
||||
c.close()
|
||||
@@ -226,7 +225,7 @@ outer:
|
||||
}
|
||||
delete(s.muxers, c.PathName())
|
||||
|
||||
if s.hlsAlwaysRemux && c.remoteAddr == "" {
|
||||
if s.alwaysRemux && c.remoteAddr == "" {
|
||||
s.findOrCreateMuxer(c.PathName(), "", nil)
|
||||
}
|
||||
|
||||
@@ -259,16 +258,7 @@ outer:
|
||||
}
|
||||
|
||||
func (s *hlsServer) onRequest(ctx *gin.Context) {
|
||||
s.log(logger.Debug, "[conn %v] %s %s", ctx.ClientIP(), ctx.Request.Method, ctx.Request.URL.Path)
|
||||
|
||||
byts, _ := httputil.DumpRequest(ctx.Request, true)
|
||||
s.log(logger.Debug, "[conn %v] [c->s] %s", ctx.ClientIP(), string(byts))
|
||||
|
||||
logw := &httpLogWriter{ResponseWriter: ctx.Writer}
|
||||
ctx.Writer = logw
|
||||
|
||||
ctx.Writer.Header().Set("Server", "rtsp-simple-server")
|
||||
ctx.Writer.Header().Set("Access-Control-Allow-Origin", s.hlsAllowOrigin)
|
||||
ctx.Writer.Header().Set("Access-Control-Allow-Origin", s.allowOrigin)
|
||||
ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
|
||||
switch ctx.Request.Method {
|
||||
@@ -281,7 +271,6 @@ func (s *hlsServer) onRequest(ctx *gin.Context) {
|
||||
return
|
||||
|
||||
default:
|
||||
ctx.Writer.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -290,7 +279,6 @@ func (s *hlsServer) onRequest(ctx *gin.Context) {
|
||||
|
||||
switch pa {
|
||||
case "", "favicon.ico":
|
||||
ctx.Writer.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -336,8 +324,6 @@ func (s *hlsServer) onRequest(ctx *gin.Context) {
|
||||
|
||||
case <-s.ctx.Done():
|
||||
}
|
||||
|
||||
s.log(logger.Debug, "[conn %v] [s->c] %s", ctx.ClientIP(), logw.dump())
|
||||
}
|
||||
|
||||
func (s *hlsServer) findOrCreateMuxer(pathName string, remoteAddr string, req *hlsMuxerRequest) *hlsMuxer {
|
||||
@@ -348,11 +334,11 @@ func (s *hlsServer) findOrCreateMuxer(pathName string, remoteAddr string, req *h
|
||||
pathName,
|
||||
remoteAddr,
|
||||
s.externalAuthenticationURL,
|
||||
s.hlsVariant,
|
||||
s.hlsSegmentCount,
|
||||
s.hlsSegmentDuration,
|
||||
s.hlsPartDuration,
|
||||
s.hlsSegmentMaxSize,
|
||||
s.variant,
|
||||
s.segmentCount,
|
||||
s.segmentDuration,
|
||||
s.partDuration,
|
||||
s.segmentMaxSize,
|
||||
s.readBufferCount,
|
||||
req,
|
||||
&s.wg,
|
||||
@@ -390,8 +376,8 @@ func (s *hlsServer) pathSourceNotReady(pa *path) {
|
||||
}
|
||||
}
|
||||
|
||||
// apiHLSMuxersList is called by api.
|
||||
func (s *hlsServer) apiHLSMuxersList() hlsServerAPIMuxersListRes {
|
||||
// apiMuxersList is called by api.
|
||||
func (s *hlsServer) apiMuxersList() hlsServerAPIMuxersListRes {
|
||||
req := hlsServerAPIMuxersListReq{
|
||||
res: make(chan hlsServerAPIMuxersListRes),
|
||||
}
|
||||
@@ -405,7 +391,7 @@ func (s *hlsServer) apiHLSMuxersList() hlsServerAPIMuxersListRes {
|
||||
}
|
||||
|
||||
for _, pa := range res.muxers {
|
||||
pa.apiHLSMuxersList(hlsServerAPIMuxersListSubReq{data: res.data})
|
||||
pa.apiMuxersList(hlsServerAPIMuxersListSubReq{data: res.data})
|
||||
}
|
||||
|
||||
return res
|
||||
|
60
internal/core/http_logger.go
Normal file
60
internal/core/http_logger.go
Normal file
@@ -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()
|
||||
}
|
@@ -17,23 +17,6 @@ func metric(key string, value int64) string {
|
||||
return key + " " + strconv.FormatInt(value, 10) + "\n"
|
||||
}
|
||||
|
||||
type metricsPathManager interface {
|
||||
apiPathsList() pathAPIPathsListRes
|
||||
}
|
||||
|
||||
type metricsRTSPServer interface {
|
||||
apiConnsList() rtspServerAPIConnsListRes
|
||||
apiSessionsList() rtspServerAPISessionsListRes
|
||||
}
|
||||
|
||||
type metricsRTMPServer interface {
|
||||
apiConnsList() rtmpServerAPIConnsListRes
|
||||
}
|
||||
|
||||
type metricsHLSServer interface {
|
||||
apiHLSMuxersList() hlsServerAPIMuxersListRes
|
||||
}
|
||||
|
||||
type metricsParent interface {
|
||||
Log(logger.Level, string, ...interface{})
|
||||
}
|
||||
@@ -44,11 +27,12 @@ type metrics struct {
|
||||
ln net.Listener
|
||||
server *http.Server
|
||||
mutex sync.Mutex
|
||||
pathManager metricsPathManager
|
||||
rtspServer metricsRTSPServer
|
||||
rtspsServer metricsRTSPServer
|
||||
rtmpServer metricsRTMPServer
|
||||
hlsServer metricsHLSServer
|
||||
pathManager apiPathManager
|
||||
rtspServer apiRTSPServer
|
||||
rtspsServer apiRTSPServer
|
||||
rtmpServer apiRTMPServer
|
||||
hlsServer apiHLSServer
|
||||
webRTCServer apiWebRTCServer
|
||||
}
|
||||
|
||||
func newMetrics(
|
||||
@@ -107,6 +91,17 @@ func (m *metrics) onMetrics(ctx *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
if !interfaceIsEmpty(m.hlsServer) {
|
||||
res := m.hlsServer.apiMuxersList()
|
||||
if res.err == nil {
|
||||
for name, i := range res.data.Items {
|
||||
tags := "{name=\"" + name + "\"}"
|
||||
out += metric("hls_muxers"+tags, 1)
|
||||
out += metric("hls_muxers_bytes_sent"+tags, int64(i.BytesSent))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !interfaceIsEmpty(m.rtspServer) { //nolint:dupl
|
||||
func() {
|
||||
res := m.rtspServer.apiConnsList()
|
||||
@@ -171,13 +166,14 @@ func (m *metrics) onMetrics(ctx *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
if !interfaceIsEmpty(m.hlsServer) {
|
||||
res := m.hlsServer.apiHLSMuxersList()
|
||||
if !interfaceIsEmpty(m.webRTCServer) {
|
||||
res := m.webRTCServer.apiConnsList()
|
||||
if res.err == nil {
|
||||
for name, i := range res.data.Items {
|
||||
tags := "{name=\"" + name + "\"}"
|
||||
out += metric("hls_muxers"+tags, 1)
|
||||
out += metric("hls_muxers_bytes_sent"+tags, int64(i.BytesSent))
|
||||
for id, i := range res.data.Items {
|
||||
tags := "{id=\"" + id + "\"}"
|
||||
out += metric("webrtc_conns"+tags, 1)
|
||||
out += metric("webrtc_conns_bytes_received"+tags, int64(i.BytesReceived))
|
||||
out += metric("webrtc_conns_bytes_sent"+tags, int64(i.BytesSent))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -187,36 +183,43 @@ func (m *metrics) onMetrics(ctx *gin.Context) {
|
||||
}
|
||||
|
||||
// pathManagerSet is called by pathManager.
|
||||
func (m *metrics) pathManagerSet(s metricsPathManager) {
|
||||
func (m *metrics) pathManagerSet(s apiPathManager) {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
m.pathManager = s
|
||||
}
|
||||
|
||||
// hlsServerSet is called by hlsServer.
|
||||
func (m *metrics) hlsServerSet(s apiHLSServer) {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
m.hlsServer = s
|
||||
}
|
||||
|
||||
// rtspServerSet is called by rtspServer (plain).
|
||||
func (m *metrics) rtspServerSet(s metricsRTSPServer) {
|
||||
func (m *metrics) rtspServerSet(s apiRTSPServer) {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
m.rtspServer = s
|
||||
}
|
||||
|
||||
// rtspsServerSet is called by rtspServer (tls).
|
||||
func (m *metrics) rtspsServerSet(s metricsRTSPServer) {
|
||||
func (m *metrics) rtspsServerSet(s apiRTSPServer) {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
m.rtspsServer = s
|
||||
}
|
||||
|
||||
// rtmpServerSet is called by rtmpServer.
|
||||
func (m *metrics) rtmpServerSet(s metricsRTMPServer) {
|
||||
func (m *metrics) rtmpServerSet(s apiRTMPServer) {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
m.rtmpServer = s
|
||||
}
|
||||
|
||||
// hlsServerSet is called by hlsServer.
|
||||
func (m *metrics) hlsServerSet(s metricsHLSServer) {
|
||||
// webRTCServerSet is called by webRTCServer.
|
||||
func (m *metrics) webRTCServerSet(s apiWebRTCServer) {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
m.hlsServer = s
|
||||
m.webRTCServer = s
|
||||
}
|
||||
|
@@ -27,6 +27,9 @@ func TestMetrics(t *testing.T) {
|
||||
defer os.Remove(serverKeyFpath)
|
||||
|
||||
p, ok := newInstance("metrics: yes\n" +
|
||||
"webrtc: yes\n" +
|
||||
"webrtcServerCert: " + serverCertFpath + "\n" +
|
||||
"webrtcServerKey: " + serverKeyFpath + "\n" +
|
||||
"encryption: optional\n" +
|
||||
"serverCert: " + serverCertFpath + "\n" +
|
||||
"serverKey: " + serverKeyFpath + "\n" +
|
||||
@@ -99,6 +102,8 @@ func TestMetrics(t *testing.T) {
|
||||
`paths_bytes_received\{name=".*?",state="ready"\} 0`+"\n"+
|
||||
`paths\{name=".*?",state="ready"\} 1`+"\n"+
|
||||
`paths_bytes_received\{name=".*?",state="ready"\} 0`+"\n"+
|
||||
`hls_muxers\{name="rtsp_path"\} 1`+"\n"+
|
||||
`hls_muxers_bytes_sent\{name="rtsp_path"\} [0-9]+`+"\n"+
|
||||
`rtsp_conns\{id=".*?"\} 1`+"\n"+
|
||||
`rtsp_conns_bytes_received\{id=".*?"\} [0-9]+`+"\n"+
|
||||
`rtsp_conns_bytes_sent\{id=".*?"\} [0-9]+`+"\n"+
|
||||
@@ -114,7 +119,6 @@ func TestMetrics(t *testing.T) {
|
||||
`rtmp_conns\{id=".*?",state="publish"\} 1`+"\n"+
|
||||
`rtmp_conns_bytes_received\{id=".*?",state="publish"\} [0-9]+`+"\n"+
|
||||
`rtmp_conns_bytes_sent\{id=".*?",state="publish"\} [0-9]+`+"\n"+
|
||||
`hls_muxers\{name="rtsp_path"\} 1`+"\n"+
|
||||
`hls_muxers_bytes_sent\{name="rtsp_path"\} [0-9]+`+"\n"+"$",
|
||||
"$",
|
||||
string(bo))
|
||||
}
|
||||
|
@@ -180,6 +180,7 @@ outer:
|
||||
continue
|
||||
}
|
||||
|
||||
if req.authenticate != nil {
|
||||
err = req.authenticate(
|
||||
pathConf.ReadIPs,
|
||||
pathConf.ReadUser,
|
||||
@@ -188,6 +189,7 @@ outer:
|
||||
req.res <- pathDescribeRes{err: err}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// create path if it doesn't exist
|
||||
if _, ok := pm.paths[req.pathName]; !ok {
|
||||
@@ -352,12 +354,18 @@ func (pm *pathManager) describe(req pathDescribeReq) pathDescribeRes {
|
||||
req.res = make(chan pathDescribeRes)
|
||||
select {
|
||||
case pm.chDescribe <- req:
|
||||
res := <-req.res
|
||||
if res.err != nil {
|
||||
return res
|
||||
res1 := <-req.res
|
||||
if res1.err != nil {
|
||||
return res1
|
||||
}
|
||||
|
||||
return res.path.describe(req)
|
||||
res2 := res1.path.describe(req)
|
||||
if res2.err != nil {
|
||||
return res2
|
||||
}
|
||||
|
||||
res2.path = res1.path
|
||||
return res2
|
||||
|
||||
case <-pm.ctx.Done():
|
||||
return pathDescribeRes{err: fmt.Errorf("terminated")}
|
||||
|
@@ -76,7 +76,7 @@ type rtmpConn struct {
|
||||
ctxCancel func()
|
||||
uuid uuid.UUID
|
||||
created time.Time
|
||||
path *path
|
||||
// path *path
|
||||
state rtmpConnState
|
||||
stateMutex sync.Mutex
|
||||
}
|
||||
@@ -153,7 +153,6 @@ func (c *rtmpConn) safeState() rtmpConnState {
|
||||
func (c *rtmpConn) run() {
|
||||
defer c.wg.Done()
|
||||
|
||||
err := func() error {
|
||||
if c.runOnConnect != "" {
|
||||
c.log(logger.Info, "runOnConnect command started")
|
||||
_, port, _ := net.SplitHostPort(c.rtspAddress)
|
||||
@@ -181,17 +180,16 @@ func (c *rtmpConn) run() {
|
||||
runErr <- c.runInner(ctx)
|
||||
}()
|
||||
|
||||
var err error
|
||||
select {
|
||||
case err := <-runErr:
|
||||
case err = <-runErr:
|
||||
cancel()
|
||||
return err
|
||||
|
||||
case <-c.ctx.Done():
|
||||
cancel()
|
||||
<-runErr
|
||||
return errors.New("terminated")
|
||||
err = errors.New("terminated")
|
||||
}
|
||||
}()
|
||||
|
||||
c.ctxCancel()
|
||||
|
||||
@@ -243,10 +241,10 @@ func (c *rtmpConn) runRead(ctx context.Context, u *url.URL) error {
|
||||
return res.err
|
||||
}
|
||||
|
||||
c.path = res.path
|
||||
path := res.path
|
||||
|
||||
defer func() {
|
||||
c.path.readerRemove(pathReaderRemoveReq{author: c})
|
||||
path.readerRemove(pathReaderRemoveReq{author: c})
|
||||
}()
|
||||
|
||||
c.stateMutex.Lock()
|
||||
@@ -288,15 +286,15 @@ func (c *rtmpConn) runRead(ctx context.Context, u *url.URL) error {
|
||||
defer res.stream.readerRemove(c)
|
||||
|
||||
c.log(logger.Info, "is reading from path '%s', %s",
|
||||
c.path.Name(), sourceMediaInfo(medias))
|
||||
path.Name(), sourceMediaInfo(medias))
|
||||
|
||||
if c.path.Conf().RunOnRead != "" {
|
||||
if path.Conf().RunOnRead != "" {
|
||||
c.log(logger.Info, "runOnRead command started")
|
||||
onReadCmd := externalcmd.NewCmd(
|
||||
c.externalCmdPool,
|
||||
c.path.Conf().RunOnRead,
|
||||
c.path.Conf().RunOnReadRestart,
|
||||
c.path.externalCmdEnv(),
|
||||
path.Conf().RunOnRead,
|
||||
path.Conf().RunOnReadRestart,
|
||||
path.externalCmdEnv(),
|
||||
func(co int) {
|
||||
c.log(logger.Info, "runOnRead command exited with code %d", co)
|
||||
})
|
||||
@@ -477,10 +475,10 @@ func (c *rtmpConn) runPublish(ctx context.Context, u *url.URL) error {
|
||||
return res.err
|
||||
}
|
||||
|
||||
c.path = res.path
|
||||
path := res.path
|
||||
|
||||
defer func() {
|
||||
c.path.publisherRemove(pathPublisherRemoveReq{author: c})
|
||||
path.publisherRemove(pathPublisherRemoveReq{author: c})
|
||||
}()
|
||||
|
||||
c.stateMutex.Lock()
|
||||
@@ -512,7 +510,7 @@ func (c *rtmpConn) runPublish(ctx context.Context, u *url.URL) error {
|
||||
medias = append(medias, audioMedia)
|
||||
}
|
||||
|
||||
rres := c.path.publisherStart(pathPublisherStartReq{
|
||||
rres := path.publisherStart(pathPublisherStartReq{
|
||||
author: c,
|
||||
medias: medias,
|
||||
generateRTPPackets: true,
|
||||
@@ -522,7 +520,7 @@ func (c *rtmpConn) runPublish(ctx context.Context, u *url.URL) error {
|
||||
}
|
||||
|
||||
c.log(logger.Info, "is publishing to path '%s', %s",
|
||||
c.path.Name(),
|
||||
path.Name(),
|
||||
sourceMediaInfo(medias))
|
||||
|
||||
// disable write deadline to allow outgoing acknowledges
|
||||
|
@@ -214,9 +214,6 @@ outer:
|
||||
s.conns[c] = struct{}{}
|
||||
|
||||
case c := <-s.chConnClose:
|
||||
if _, ok := s.conns[c]; !ok {
|
||||
continue
|
||||
}
|
||||
delete(s.conns, c)
|
||||
|
||||
case req := <-s.chAPIConnsList:
|
||||
|
@@ -180,11 +180,11 @@ func newRTSPServer(
|
||||
|
||||
s.log(logger.Info, "listener opened on %s", printAddresses(s.srv))
|
||||
|
||||
if s.metrics != nil {
|
||||
if metrics != nil {
|
||||
if !isTLS {
|
||||
s.metrics.rtspServerSet(s)
|
||||
metrics.rtspServerSet(s)
|
||||
} else {
|
||||
s.metrics.rtspsServerSet(s)
|
||||
metrics.rtspsServerSet(s)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -330,9 +330,9 @@ func (s *rtspSession) onRecord(ctx *gortsplib.ServerHandlerOnRecordCtx) (*base.R
|
||||
}
|
||||
})
|
||||
|
||||
case *format.MPEG4Audio:
|
||||
case *format.VP8:
|
||||
ctx.Session.OnPacketRTP(medi, forma, func(pkt *rtp.Packet) {
|
||||
err := s.stream.writeData(cmedia, cformat, &dataMPEG4Audio{
|
||||
err := s.stream.writeData(cmedia, cformat, &dataVP8{
|
||||
rtpPackets: []*rtp.Packet{pkt},
|
||||
ntp: time.Now(),
|
||||
})
|
||||
@@ -341,9 +341,20 @@ func (s *rtspSession) onRecord(ctx *gortsplib.ServerHandlerOnRecordCtx) (*base.R
|
||||
}
|
||||
})
|
||||
|
||||
case *format.Opus:
|
||||
case *format.VP9:
|
||||
ctx.Session.OnPacketRTP(medi, forma, func(pkt *rtp.Packet) {
|
||||
err := s.stream.writeData(cmedia, cformat, &dataOpus{
|
||||
err := s.stream.writeData(cmedia, cformat, &dataVP9{
|
||||
rtpPackets: []*rtp.Packet{pkt},
|
||||
ntp: time.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
s.log(logger.Warn, "%v", err)
|
||||
}
|
||||
})
|
||||
|
||||
case *format.MPEG4Audio:
|
||||
ctx.Session.OnPacketRTP(medi, forma, func(pkt *rtp.Packet) {
|
||||
err := s.stream.writeData(cmedia, cformat, &dataMPEG4Audio{
|
||||
rtpPackets: []*rtp.Packet{pkt},
|
||||
ntp: time.Now(),
|
||||
})
|
||||
|
@@ -170,9 +170,9 @@ func (s *rtspSource) run(ctx context.Context) error {
|
||||
}
|
||||
})
|
||||
|
||||
case *format.MPEG4Audio:
|
||||
case *format.VP8:
|
||||
c.OnPacketRTP(medi, forma, func(pkt *rtp.Packet) {
|
||||
err := res.stream.writeData(cmedia, cformat, &dataMPEG4Audio{
|
||||
err := res.stream.writeData(cmedia, cformat, &dataVP8{
|
||||
rtpPackets: []*rtp.Packet{pkt},
|
||||
ntp: time.Now(),
|
||||
})
|
||||
@@ -181,9 +181,20 @@ func (s *rtspSource) run(ctx context.Context) error {
|
||||
}
|
||||
})
|
||||
|
||||
case *format.Opus:
|
||||
case *format.VP9:
|
||||
c.OnPacketRTP(medi, forma, func(pkt *rtp.Packet) {
|
||||
err := res.stream.writeData(cmedia, cformat, &dataOpus{
|
||||
err := res.stream.writeData(cmedia, cformat, &dataVP9{
|
||||
rtpPackets: []*rtp.Packet{pkt},
|
||||
ntp: time.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
s.Log(logger.Warn, "%v", err)
|
||||
}
|
||||
})
|
||||
|
||||
case *format.MPEG4Audio:
|
||||
c.OnPacketRTP(medi, forma, func(pkt *rtp.Packet) {
|
||||
err := res.stream.writeData(cmedia, cformat, &dataMPEG4Audio{
|
||||
rtpPackets: []*rtp.Packet{pkt},
|
||||
ntp: time.Now(),
|
||||
})
|
||||
|
740
internal/core/webrtc_conn.go
Normal file
740
internal/core/webrtc_conn.go
Normal file
@@ -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"}
|
||||
}
|
175
internal/core/webrtc_index.html
Normal file
175
internal/core/webrtc_index.html
Normal file
@@ -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>
|
431
internal/core/webrtc_server.go
Normal file
431
internal/core/webrtc_server.go
Normal file
@@ -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")}
|
||||
}
|
||||
}
|
@@ -121,6 +121,16 @@ rtmpServerCert: server.crt
|
||||
hlsDisable: no
|
||||
# Address of the HLS listener.
|
||||
hlsAddress: :8888
|
||||
# Enable TLS/HTTPS on the HLS server.
|
||||
# This is required for Low-Latency HLS.
|
||||
hlsEncryption: no
|
||||
# Path to the server key. This is needed only when encryption is yes.
|
||||
# This can be generated with:
|
||||
# openssl genrsa -out server.key 2048
|
||||
# openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650
|
||||
hlsServerKey: server.key
|
||||
# Path to the server certificate.
|
||||
hlsServerCert: server.crt
|
||||
# By default, HLS is generated only when requested by a user.
|
||||
# This option allows to generate it always, avoiding the delay between request and generation.
|
||||
hlsAlwaysRemux: no
|
||||
@@ -151,21 +161,40 @@ hlsSegmentMaxSize: 50M
|
||||
# Value of the Access-Control-Allow-Origin header provided in every HTTP response.
|
||||
# This allows to play the HLS stream from an external website.
|
||||
hlsAllowOrigin: '*'
|
||||
# Enable TLS/HTTPS on the HLS server.
|
||||
# This is required for Low-Latency HLS.
|
||||
hlsEncryption: no
|
||||
# Path to the server key. This is needed only when encryption is yes.
|
||||
# This can be generated with:
|
||||
# openssl genrsa -out server.key 2048
|
||||
# openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650
|
||||
hlsServerKey: server.key
|
||||
# Path to the server certificate.
|
||||
hlsServerCert: server.crt
|
||||
# List of IPs or CIDRs of proxies behind the HLS server.
|
||||
# List of IPs or CIDRs of proxies placed before the HLS server.
|
||||
# If the server receives a request from one of these entries, IP in logs
|
||||
# will be taken from the X-Forwarded-For header.
|
||||
hlsTrustedProxies: []
|
||||
|
||||
###############################################
|
||||
# WebRTC parameters
|
||||
|
||||
# Enable support for the WebRTC protocol.
|
||||
webrtc: no
|
||||
# Address of the WebRTC listener.
|
||||
webrtcAddress: :8889
|
||||
# Path to the server key. This is mandatory since HTTPS is mandatory in order to use WebRTC.
|
||||
# This can be generated with:
|
||||
# openssl genrsa -out server.key 2048
|
||||
# openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650
|
||||
webrtcServerKey: server.key
|
||||
# Path to the server certificate.
|
||||
webrtcServerCert: server.crt
|
||||
# Value of the Access-Control-Allow-Origin header provided in every HTTP response.
|
||||
# This allows to play the WebRTC stream from an external website.
|
||||
webrtcAllowOrigin: '*'
|
||||
# List of IPs or CIDRs of proxies placed before the WebRTC server.
|
||||
# If the server receives a request from one of these entries, IP in logs
|
||||
# will be taken from the X-Forwarded-For header.
|
||||
webrtcTrustedProxies: []
|
||||
# List of ICE servers, in format type:user:pass:host:port or type:host:port.
|
||||
# type can be "stun", "turn" or "turns".
|
||||
# STUN servers are used to get the public IP of both server and clients.
|
||||
# TURN/TURNS servers are used as relay when a direct connection between server and clients is not possible.
|
||||
# if user is "AUTH_SECRET", then authentication is secret based.
|
||||
# the secret must be inserted into the pass field.
|
||||
webrtcICEServers: [stun:stun.l.google.com:19302]
|
||||
|
||||
###############################################
|
||||
# Path parameters
|
||||
|
||||
|
Reference in New Issue
Block a user