Support reading with WebRTC (#1242)

This commit is contained in:
Alessandro Ros
2022-12-16 00:50:47 +01:00
committed by GitHub
parent 478607a602
commit 5efe97abf1
41 changed files with 2261 additions and 543 deletions

View File

@@ -3,20 +3,33 @@
<img src="logo.png" alt="rtsp-simple-server"> <img src="logo.png" alt="rtsp-simple-server">
</p> </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| Live streams can be published to the server with:
|--------|-----------|--------|-------|----|-----|
|RTSP|fastest way to publish and read streams|RTSP, RTSPS|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:| |protocol|variants|codecs|
|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:| |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: Features:
* Publish live streams to the server * Publish live streams to the server
* Read live streams from the server * Read live streams from the server
* Proxy streams from other servers or cameras, always or on-demand * 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 * 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 * Serve multiple streams at once in separate paths
* Authenticate users; use internal or external authentication * Authenticate users; use internal or external authentication
@@ -446,6 +459,10 @@ Obtaining:
paths{name="[path_name]",state="[state]"} 1 paths{name="[path_name]",state="[state]"} 1
paths_bytes_received{name="[path_name]",state="[state]"} 1234 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 # metrics of every RTSP connection
rtsp_conns{id="[id]"} 1 rtsp_conns{id="[id]"} 1
rtsp_conns_bytes_received{id="[id]"} 1234 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_received{id="[id]",state="[state]"} 1234
rtmp_conns_bytes_sent{id="[id]",state="[state]"} 187 rtmp_conns_bytes_sent{id="[id]",state="[state]"} 187
# metrics of every HLS muxer # metrics of every WebRTC connection
hls_muxers{name="[name]"} 1 webrtc_conns{id="[id]"} 1
hls_muxers_bytes_sent{name="[name]"} 187 webrtc_conns_bytes_received{id="[id]",state="[state]"} 1234
webrtc_conns_bytes_sent{id="[id]",state="[state]"} 187
``` ```
### pprof ### pprof

View File

@@ -429,6 +429,14 @@ components:
bytesSent: bytesSent:
type: number type: number
HLSMuxersList:
type: object
properties:
items:
type: object
additionalProperties:
$ref: '#/components/schemas/HLSMuxer'
PathsList: PathsList:
type: object type: object
properties: properties:
@@ -437,6 +445,14 @@ components:
additionalProperties: additionalProperties:
$ref: '#/components/schemas/Path' $ref: '#/components/schemas/Path'
RTMPConnsList:
type: object
properties:
items:
type: object
additionalProperties:
$ref: '#/components/schemas/RTMPConn'
RTSPConnsList: RTSPConnsList:
type: object type: object
properties: properties:
@@ -461,21 +477,25 @@ components:
additionalProperties: additionalProperties:
$ref: '#/components/schemas/RTSPSession' $ref: '#/components/schemas/RTSPSession'
RTMPConnsList: WebRTCConn:
type: object type: object
properties: properties:
items: created:
type: object type: string
additionalProperties: remoteAddr:
$ref: '#/components/schemas/RTMPConn' type: string
bytesReceived:
type: number
bytesSent:
type: number
HLSMuxersList: WebRTCConnsList:
type: object type: object
properties: properties:
items: items:
type: object type: object
additionalProperties: additionalProperties:
$ref: '#/components/schemas/HLSMuxer' $ref: '#/components/schemas/WebRTCConn'
paths: paths:
/v1/config/get: /v1/config/get:
@@ -586,6 +606,23 @@ paths:
'500': '500':
description: internal server error. 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: /v1/paths/list:
get: get:
operationId: pathsList operationId: pathsList
@@ -637,23 +674,6 @@ paths:
'500': '500':
description: internal server error. 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}: /v1/rtspsessions/kick/{id}:
post: post:
operationId: rtspSessionsKick operationId: rtspSessionsKick
@@ -674,6 +694,23 @@ paths:
'500': '500':
description: internal server error. 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: /v1/rtspssessions/list:
get: get:
operationId: rtspsSessionsList operationId: rtspsSessionsList
@@ -785,10 +822,10 @@ paths:
'500': '500':
description: internal server error. description: internal server error.
/v1/hlsmuxers/list: /v1/webrtcconns/list:
get: get:
operationId: hlsMuxersList operationId: webrtcConnsList
summary: returns all HLS muxers. summary: returns all WebRTC connections.
description: '' description: ''
responses: responses:
'200': '200':
@@ -796,7 +833,27 @@ paths:
content: content:
application/json: application/json:
schema: 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': '400':
description: invalid request. description: invalid request.
'500': '500':

28
go.mod
View File

@@ -5,19 +5,21 @@ go 1.18
require ( require (
code.cloudfoundry.org/bytefmt v0.0.0-20211005130812-5bb3c17173e5 code.cloudfoundry.org/bytefmt v0.0.0-20211005130812-5bb3c17173e5
github.com/abema/go-mp4 v0.8.0 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/asticode/go-astits v1.10.1-0.20220319093903-4abe66a9b757
github.com/fsnotify/fsnotify v1.4.9 github.com/fsnotify/fsnotify v1.4.9
github.com/gin-gonic/gin v1.8.1 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/gookit/color v1.4.2
github.com/gorilla/websocket v1.5.0
github.com/grafov/m3u8 v0.11.1 github.com/grafov/m3u8 v0.11.1
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/notedit/rtmp v0.0.2 github.com/notedit/rtmp v0.0.2
github.com/orcaman/writerseeker v0.0.0 github.com/orcaman/writerseeker v0.0.0
github.com/pion/rtp v1.7.13 github.com/pion/rtp v1.7.13
github.com/pion/webrtc/v3 v3.1.47
github.com/stretchr/testify v1.7.1 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/alecthomas/kingpin.v2 v2.2.6
gopkg.in/yaml.v2 v2.4.0 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/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.1 // 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/randutil v0.1.0 // indirect
github.com/pion/rtcp v1.2.9 // indirect github.com/pion/rtcp v1.2.10 // indirect
github.com/pion/sdp/v3 v3.0.5 // 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/pmezard/go-difflib v1.0.0 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect github.com/ugorji/go/codec v1.2.7 // indirect
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
golang.org/x/net v0.0.0-20220526153639-5463443f8c37 // indirect golang.org/x/net v0.0.0-20221004154528-8021a29435af // indirect
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect golang.org/x/sys v0.0.0-20221010170243-090e33056c14 // indirect
golang.org/x/text v0.3.7 // indirect golang.org/x/text v0.3.7 // indirect
google.golang.org/protobuf v1.28.0 // indirect google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect

76
go.sum
View File

@@ -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/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 h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= 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-20221214210611-e1c07a1c8d71 h1:dgKa+8HxFRliWSRFHyYg1Fz2F6OlDapT81oDGS6kits=
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/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 h1:9WgSzBLo3a9ToSVV7sRTBYZ1GGOZUpq4+5H3SN0UZq4=
github.com/aler9/writerseeker v0.0.0-20220601075008-6f0e685b9c82/go.mod h1:qsMrZCbeBf/mCLOeF16KDkPu4gktn/pOWyaq1aYQE7U= 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= 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 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 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/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.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 h1:tXy44JFSFkKnELV6WaMo/lLfu/meqITX3iAV52do7lk=
github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= 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 h1:igZ7EBIB2IAsPPazKwRKdbhxcoBKO3lO1UY57PZDeNA=
github.com/grafov/m3u8 v0.11.1/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080= 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= 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/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.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.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.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.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.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.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 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU=
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= 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 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= 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.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 h1:qcHwlmtiI50t1XivvoawdCGTP4Uiypzfrsap+bijcoA=
github.com/pion/rtp v1.7.13/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko= 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/sctp v1.8.0/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
github.com/pion/sdp/v3 v3.0.5/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw= 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/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/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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.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 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= 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/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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 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-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-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-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-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/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-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-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-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-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-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-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-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-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220526153639-5463443f8c37/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 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-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-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/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-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-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-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-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-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 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= 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/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 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-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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 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= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=

View File

@@ -54,6 +54,7 @@ func (d *AuthMethods) UnmarshalJSON(b []byte) error {
return nil return nil
} }
// unmarshalEnv implements envUnmarshaler.
func (d *AuthMethods) unmarshalEnv(s string) error { func (d *AuthMethods) unmarshalEnv(s string) error {
byts, _ := json.Marshal(strings.Split(s, ",")) byts, _ := json.Marshal(strings.Split(s, ","))
return d.UnmarshalJSON(byts) return d.UnmarshalJSON(byts)

View File

@@ -211,6 +211,9 @@ type Conf struct {
// HLS // HLS
HLSDisable bool `json:"hlsDisable"` HLSDisable bool `json:"hlsDisable"`
HLSAddress string `json:"hlsAddress"` HLSAddress string `json:"hlsAddress"`
HLSEncryption bool `json:"hlsEncryption"`
HLSServerKey string `json:"hlsServerKey"`
HLSServerCert string `json:"hlsServerCert"`
HLSAlwaysRemux bool `json:"hlsAlwaysRemux"` HLSAlwaysRemux bool `json:"hlsAlwaysRemux"`
HLSVariant HLSVariant `json:"hlsVariant"` HLSVariant HLSVariant `json:"hlsVariant"`
HLSSegmentCount int `json:"hlsSegmentCount"` HLSSegmentCount int `json:"hlsSegmentCount"`
@@ -218,11 +221,17 @@ type Conf struct {
HLSPartDuration StringDuration `json:"hlsPartDuration"` HLSPartDuration StringDuration `json:"hlsPartDuration"`
HLSSegmentMaxSize StringSize `json:"hlsSegmentMaxSize"` HLSSegmentMaxSize StringSize `json:"hlsSegmentMaxSize"`
HLSAllowOrigin string `json:"hlsAllowOrigin"` HLSAllowOrigin string `json:"hlsAllowOrigin"`
HLSEncryption bool `json:"hlsEncryption"`
HLSServerKey string `json:"hlsServerKey"`
HLSServerCert string `json:"hlsServerCert"`
HLSTrustedProxies IPsOrCIDRs `json:"hlsTrustedProxies"` 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
Paths map[string]*PathConf `json:"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. // CheckAndFillMissing checks the configuration for errors and fills missing parameters.
func (conf *Conf) CheckAndFillMissing() error { func (conf *Conf) CheckAndFillMissing() error {
// general
if conf.LogLevel == 0 { if conf.LogLevel == 0 {
conf.LogLevel = LogLevel(logger.Info) conf.LogLevel = LogLevel(logger.Info)
} }
if len(conf.LogDestinations) == 0 { if len(conf.LogDestinations) == 0 {
conf.LogDestinations = LogDestinations{logger.DestinationStdout: {}} conf.LogDestinations = LogDestinations{logger.DestinationStdout: {}}
} }
if conf.LogFile == "" { if conf.LogFile == "" {
conf.LogFile = "rtsp-simple-server.log" conf.LogFile = "rtsp-simple-server.log"
} }
if conf.ReadTimeout == 0 { if conf.ReadTimeout == 0 {
conf.ReadTimeout = 10 * StringDuration(time.Second) conf.ReadTimeout = 10 * StringDuration(time.Second)
} }
if conf.WriteTimeout == 0 { if conf.WriteTimeout == 0 {
conf.WriteTimeout = 10 * StringDuration(time.Second) conf.WriteTimeout = 10 * StringDuration(time.Second)
} }
if conf.ReadBufferCount == 0 { if conf.ReadBufferCount == 0 {
conf.ReadBufferCount = 512 conf.ReadBufferCount = 512
} }
if (conf.ReadBufferCount & (conf.ReadBufferCount - 1)) != 0 { if (conf.ReadBufferCount & (conf.ReadBufferCount - 1)) != 0 {
return fmt.Errorf("'ReadBufferCount' must be a power of two") return fmt.Errorf("'ReadBufferCount' must be a power of two")
} }
if conf.ExternalAuthenticationURL != "" { if conf.ExternalAuthenticationURL != "" {
if !strings.HasPrefix(conf.ExternalAuthenticationURL, "http://") && if !strings.HasPrefix(conf.ExternalAuthenticationURL, "http://") &&
!strings.HasPrefix(conf.ExternalAuthenticationURL, "https://") { !strings.HasPrefix(conf.ExternalAuthenticationURL, "https://") {
return fmt.Errorf("'externalAuthenticationURL' must be a HTTP URL") return fmt.Errorf("'externalAuthenticationURL' must be a HTTP URL")
} }
} }
if conf.APIAddress == "" { if conf.APIAddress == "" {
conf.APIAddress = "127.0.0.1:9997" conf.APIAddress = "127.0.0.1:9997"
} }
if conf.MetricsAddress == "" { if conf.MetricsAddress == "" {
conf.MetricsAddress = "127.0.0.1:9998" conf.MetricsAddress = "127.0.0.1:9998"
} }
if conf.PPROFAddress == "" { if conf.PPROFAddress == "" {
conf.PPROFAddress = "127.0.0.1:9999" conf.PPROFAddress = "127.0.0.1:9999"
} }
// RTSP
if len(conf.Protocols) == 0 { if len(conf.Protocols) == 0 {
conf.Protocols = Protocols{ conf.Protocols = Protocols{
Protocol(gortsplib.TransportUDP): {}, Protocol(gortsplib.TransportUDP): {},
@@ -304,7 +306,6 @@ func (conf *Conf) CheckAndFillMissing() error {
Protocol(gortsplib.TransportTCP): {}, Protocol(gortsplib.TransportTCP): {},
} }
} }
if conf.Encryption == EncryptionStrict { if conf.Encryption == EncryptionStrict {
if _, ok := conf.Protocols[Protocol(gortsplib.TransportUDP)]; ok { if _, ok := conf.Protocols[Protocol(gortsplib.TransportUDP)]; ok {
return fmt.Errorf("strict encryption can't be used with the UDP transport protocol") 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") return fmt.Errorf("strict encryption can't be used with the UDP-multicast transport protocol")
} }
} }
if conf.RTSPAddress == "" { if conf.RTSPAddress == "" {
conf.RTSPAddress = ":8554" conf.RTSPAddress = ":8554"
} }
if conf.RTSPSAddress == "" { if conf.RTSPSAddress == "" {
conf.RTSPSAddress = ":8322" conf.RTSPSAddress = ":8322"
} }
if conf.RTPAddress == "" { if conf.RTPAddress == "" {
conf.RTPAddress = ":8000" conf.RTPAddress = ":8000"
} }
if conf.RTCPAddress == "" { if conf.RTCPAddress == "" {
conf.RTCPAddress = ":8001" conf.RTCPAddress = ":8001"
} }
if conf.MulticastIPRange == "" { if conf.MulticastIPRange == "" {
conf.MulticastIPRange = "224.1.0.0/16" conf.MulticastIPRange = "224.1.0.0/16"
} }
if conf.MulticastRTPPort == 0 { if conf.MulticastRTPPort == 0 {
conf.MulticastRTPPort = 8002 conf.MulticastRTPPort = 8002
} }
if conf.MulticastRTCPPort == 0 { if conf.MulticastRTCPPort == 0 {
conf.MulticastRTCPPort = 8003 conf.MulticastRTCPPort = 8003
} }
if conf.ServerKey == "" { if conf.ServerKey == "" {
conf.ServerKey = "server.key" conf.ServerKey = "server.key"
} }
if conf.ServerCert == "" { if conf.ServerCert == "" {
conf.ServerCert = "server.crt" conf.ServerCert = "server.crt"
} }
if len(conf.AuthMethods) == 0 { if len(conf.AuthMethods) == 0 {
conf.AuthMethods = AuthMethods{headers.AuthBasic, headers.AuthDigest} conf.AuthMethods = AuthMethods{headers.AuthBasic, headers.AuthDigest}
} }
// RTMP
if conf.RTMPAddress == "" { if conf.RTMPAddress == "" {
conf.RTMPAddress = ":1935" conf.RTMPAddress = ":1935"
} }
if conf.RTMPSAddress == "" { if conf.RTMPSAddress == "" {
conf.RTMPSAddress = ":1936" conf.RTMPSAddress = ":1936"
} }
// HLS
if conf.HLSAddress == "" { if conf.HLSAddress == "" {
conf.HLSAddress = ":8888" 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 == "" { if conf.HLSServerKey == "" {
conf.HLSServerKey = "server.key" conf.HLSServerKey = "server.key"
} }
if conf.HLSServerCert == "" { if conf.HLSServerCert == "" {
conf.HLSServerCert = "server.crt" 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 { switch conf.HLSVariant {
case HLSVariantLowLatency: case HLSVariantLowLatency:
if conf.HLSSegmentCount < 7 { 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 // do not add automatically "all", since user may want to
// initialize all paths through API or hot reloading. // initialize all paths through API or hot reloading.
if conf.Paths == nil { if conf.Paths == nil {

View File

@@ -36,6 +36,7 @@ func (d *Credential) UnmarshalJSON(b []byte) error {
return nil return nil
} }
// unmarshalEnv implements envUnmarshaler.
func (d *Credential) unmarshalEnv(s string) error { func (d *Credential) unmarshalEnv(s string) error {
return d.UnmarshalJSON([]byte(`"` + s + `"`)) return d.UnmarshalJSON([]byte(`"` + s + `"`))
} }

View File

@@ -57,6 +57,7 @@ func (d *Encryption) UnmarshalJSON(b []byte) error {
return nil return nil
} }
// unmarshalEnv implements envUnmarshaler.
func (d *Encryption) unmarshalEnv(s string) error { func (d *Encryption) unmarshalEnv(s string) error {
return d.UnmarshalJSON([]byte(`"` + s + `"`)) return d.UnmarshalJSON([]byte(`"` + s + `"`))
} }

View File

@@ -131,6 +131,14 @@ func loadEnvInternal(env map[string]string, prefix string, rv reflect.Value) err
} }
} }
return nil 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) return fmt.Errorf("unsupported type: %v", rt)

View File

@@ -24,6 +24,7 @@ type testStruct struct {
MyBool bool MyBool bool
MyDuration StringDuration MyDuration StringDuration
MyMap map[string]*mapEntry MyMap map[string]*mapEntry
MySlice []string
} }
func TestEnvironment(t *testing.T) { func TestEnvironment(t *testing.T) {
@@ -51,6 +52,9 @@ func TestEnvironment(t *testing.T) {
os.Setenv("MYPREFIX_MYMAP_MYKEY2_MYSTRUCT_MYPARAM", "456") os.Setenv("MYPREFIX_MYMAP_MYKEY2_MYSTRUCT_MYPARAM", "456")
defer os.Unsetenv("MYPREFIX_MYMAP_MYKEY2_MYSTRUCT_MYPARAM") defer os.Unsetenv("MYPREFIX_MYMAP_MYKEY2_MYSTRUCT_MYPARAM")
os.Setenv("MYPREFIX_MYSLICE", "val1,val2")
defer os.Unsetenv("MYPREFIX_MYSLICE")
var s testStruct var s testStruct
err := loadFromEnvironment("MYPREFIX", &s) err := loadFromEnvironment("MYPREFIX", &s)
require.NoError(t, err) require.NoError(t, err)
@@ -68,4 +72,6 @@ func TestEnvironment(t *testing.T) {
require.Equal(t, true, ok) require.Equal(t, true, ok)
require.Equal(t, "asd", v.MyValue) require.Equal(t, "asd", v.MyValue)
require.Equal(t, 456, v.MyStruct.MyParam) require.Equal(t, 456, v.MyStruct.MyParam)
require.Equal(t, []string{"val1", "val2"}, s.MySlice)
} }

View File

@@ -59,6 +59,7 @@ func (d *HLSVariant) UnmarshalJSON(b []byte) error {
return nil return nil
} }
// unmarshalEnv implements envUnmarshaler.
func (d *HLSVariant) unmarshalEnv(s string) error { func (d *HLSVariant) unmarshalEnv(s string) error {
return d.UnmarshalJSON([]byte(`"` + s + `"`)) return d.UnmarshalJSON([]byte(`"` + s + `"`))
} }

View File

@@ -48,6 +48,7 @@ func (d *IPsOrCIDRs) UnmarshalJSON(b []byte) error {
return nil return nil
} }
// unmarshalEnv implements envUnmarshaler.
func (d *IPsOrCIDRs) unmarshalEnv(s string) error { func (d *IPsOrCIDRs) unmarshalEnv(s string) error {
byts, _ := json.Marshal(strings.Split(s, ",")) byts, _ := json.Marshal(strings.Split(s, ","))
return d.UnmarshalJSON(byts) return d.UnmarshalJSON(byts)

View File

@@ -68,6 +68,7 @@ func (d *LogDestinations) UnmarshalJSON(b []byte) error {
return nil return nil
} }
// unmarshalEnv implements envUnmarshaler.
func (d *LogDestinations) unmarshalEnv(s string) error { func (d *LogDestinations) unmarshalEnv(s string) error {
byts, _ := json.Marshal(strings.Split(s, ",")) byts, _ := json.Marshal(strings.Split(s, ","))
return d.UnmarshalJSON(byts) return d.UnmarshalJSON(byts)

View File

@@ -58,6 +58,7 @@ func (d *LogLevel) UnmarshalJSON(b []byte) error {
return nil return nil
} }
// unmarshalEnv implements envUnmarshaler.
func (d *LogLevel) unmarshalEnv(s string) error { func (d *LogLevel) unmarshalEnv(s string) error {
return d.UnmarshalJSON([]byte(`"` + s + `"`)) return d.UnmarshalJSON([]byte(`"` + s + `"`))
} }

View File

@@ -71,6 +71,7 @@ func (d *Protocols) UnmarshalJSON(b []byte) error {
return nil return nil
} }
// unmarshalEnv implements envUnmarshaler.
func (d *Protocols) unmarshalEnv(s string) error { func (d *Protocols) unmarshalEnv(s string) error {
byts, _ := json.Marshal(strings.Split(s, ",")) byts, _ := json.Marshal(strings.Split(s, ","))
return d.UnmarshalJSON(byts) return d.UnmarshalJSON(byts)

View File

@@ -63,6 +63,7 @@ func (d *SourceProtocol) UnmarshalJSON(b []byte) error {
return nil return nil
} }
// unmarshalEnv implements envUnmarshaler.
func (d *SourceProtocol) unmarshalEnv(s string) error { func (d *SourceProtocol) unmarshalEnv(s string) error {
return d.UnmarshalJSON([]byte(`"` + s + `"`)) return d.UnmarshalJSON([]byte(`"` + s + `"`))
} }

View File

@@ -30,6 +30,7 @@ func (d *StringDuration) UnmarshalJSON(b []byte) error {
return nil return nil
} }
// unmarshalEnv implements envUnmarshaler.
func (d *StringDuration) unmarshalEnv(s string) error { func (d *StringDuration) unmarshalEnv(s string) error {
return d.UnmarshalJSON([]byte(`"` + s + `"`)) return d.UnmarshalJSON([]byte(`"` + s + `"`))
} }

View File

@@ -30,6 +30,7 @@ func (s *StringSize) UnmarshalJSON(b []byte) error {
return nil return nil
} }
// unmarshalEnv implements envUnmarshaler.
func (s *StringSize) unmarshalEnv(v string) error { func (s *StringSize) unmarshalEnv(v string) error {
return s.UnmarshalJSON([]byte(`"` + v + `"`)) return s.UnmarshalJSON([]byte(`"` + v + `"`))
} }

View File

@@ -5,7 +5,6 @@ import (
"encoding/json" "encoding/json"
"net" "net"
"net/http" "net/http"
"net/http/httputil"
"reflect" "reflect"
"sync" "sync"
@@ -86,6 +85,10 @@ type apiPathManager interface {
apiPathsList() pathAPIPathsListRes apiPathsList() pathAPIPathsListRes
} }
type apiHLSServer interface {
apiMuxersList() hlsServerAPIMuxersListRes
}
type apiRTSPServer interface { type apiRTSPServer interface {
apiConnsList() rtspServerAPIConnsListRes apiConnsList() rtspServerAPIConnsListRes
apiSessionsList() rtspServerAPISessionsListRes apiSessionsList() rtspServerAPISessionsListRes
@@ -97,15 +100,16 @@ type apiRTMPServer interface {
apiConnsKick(id string) rtmpServerAPIConnsKickRes apiConnsKick(id string) rtmpServerAPIConnsKickRes
} }
type apiHLSServer interface {
apiHLSMuxersList() hlsServerAPIMuxersListRes
}
type apiParent interface { type apiParent interface {
Log(logger.Level, string, ...interface{}) Log(logger.Level, string, ...interface{})
apiConfigSet(conf *conf.Conf) apiConfigSet(conf *conf.Conf)
} }
type apiWebRTCServer interface {
apiConnsList() webRTCServerAPIConnsListRes
apiConnsKick(id string) webRTCServerAPIConnsKickRes
}
type api struct { type api struct {
conf *conf.Conf conf *conf.Conf
pathManager apiPathManager pathManager apiPathManager
@@ -114,6 +118,7 @@ type api struct {
rtmpServer apiRTMPServer rtmpServer apiRTMPServer
rtmpsServer apiRTMPServer rtmpsServer apiRTMPServer
hlsServer apiHLSServer hlsServer apiHLSServer
webRTCServer apiWebRTCServer
parent apiParent parent apiParent
ln net.Listener ln net.Listener
@@ -130,6 +135,7 @@ func newAPI(
rtmpServer apiRTMPServer, rtmpServer apiRTMPServer,
rtmpsServer apiRTMPServer, rtmpsServer apiRTMPServer,
hlsServer apiHLSServer, hlsServer apiHLSServer,
webRTCServer apiWebRTCServer,
parent apiParent, parent apiParent,
) (*api, error) { ) (*api, error) {
ln, err := net.Listen("tcp", address) ln, err := net.Listen("tcp", address)
@@ -145,14 +151,16 @@ func newAPI(
rtmpServer: rtmpServer, rtmpServer: rtmpServer,
rtmpsServer: rtmpsServer, rtmpsServer: rtmpsServer,
hlsServer: hlsServer, hlsServer: hlsServer,
webRTCServer: webRTCServer,
parent: parent, parent: parent,
ln: ln, ln: ln,
} }
router := gin.New() router := gin.New()
router.SetTrustedProxies(nil) router.SetTrustedProxies(nil)
router.NoRoute(a.mwLog) mwLog := httpLoggerMiddleware(a)
group := router.Group("/", a.mwLog) router.NoRoute(mwLog)
group := router.Group("/", mwLog)
group.GET("/v1/config/get", a.onConfigGet) group.GET("/v1/config/get", a.onConfigGet)
group.POST("/v1/config/set", a.onConfigSet) 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/edit/*name", a.onConfigPathsEdit)
group.POST("/v1/config/paths/remove/*name", a.onConfigPathsDelete) 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) group.GET("/v1/paths/list", a.onPathsList)
if !interfaceIsEmpty(a.rtspServer) { if !interfaceIsEmpty(a.rtspServer) {
@@ -184,8 +196,9 @@ func newAPI(
group.POST("/v1/rtmpsconns/kick/:id", a.onRTMPSConnsKick) group.POST("/v1/rtmpsconns/kick/:id", a.onRTMPSConnsKick)
} }
if !interfaceIsEmpty(a.hlsServer) { if !interfaceIsEmpty(a.webRTCServer) {
group.GET("/v1/hlsmuxers/list", a.onHLSMuxersList) group.GET("/v1/webrtcconns/list", a.onWebRTCConnsList)
group.POST("/v1/webrtcconns/kick/:id", a.onWebRTCConnsKick)
} }
a.s = &http.Server{Handler: router} 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...) 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) { func (a *api) onConfigGet(ctx *gin.Context) {
a.mutex.Lock() a.mutex.Lock()
c := a.conf c := a.conf
@@ -419,7 +416,6 @@ func (a *api) onRTSPSessionsKick(ctx *gin.Context) {
res := a.rtspServer.apiSessionsKick(id) res := a.rtspServer.apiSessionsKick(id)
if res.err != nil { if res.err != nil {
ctx.AbortWithStatus(http.StatusNotFound)
return return
} }
@@ -451,7 +447,6 @@ func (a *api) onRTSPSSessionsKick(ctx *gin.Context) {
res := a.rtspsServer.apiSessionsKick(id) res := a.rtspsServer.apiSessionsKick(id)
if res.err != nil { if res.err != nil {
ctx.AbortWithStatus(http.StatusNotFound)
return return
} }
@@ -473,7 +468,6 @@ func (a *api) onRTMPConnsKick(ctx *gin.Context) {
res := a.rtmpServer.apiConnsKick(id) res := a.rtmpServer.apiConnsKick(id)
if res.err != nil { if res.err != nil {
ctx.AbortWithStatus(http.StatusNotFound)
return return
} }
@@ -495,7 +489,6 @@ func (a *api) onRTMPSConnsKick(ctx *gin.Context) {
res := a.rtmpsServer.apiConnsKick(id) res := a.rtmpsServer.apiConnsKick(id)
if res.err != nil { if res.err != nil {
ctx.AbortWithStatus(http.StatusNotFound)
return return
} }
@@ -503,7 +496,7 @@ func (a *api) onRTMPSConnsKick(ctx *gin.Context) {
} }
func (a *api) onHLSMuxersList(ctx *gin.Context) { func (a *api) onHLSMuxersList(ctx *gin.Context) {
res := a.hlsServer.apiHLSMuxersList() res := a.hlsServer.apiMuxersList()
if res.err != nil { if res.err != nil {
ctx.AbortWithStatus(http.StatusInternalServerError) ctx.AbortWithStatus(http.StatusInternalServerError)
return return
@@ -512,6 +505,27 @@ func (a *api) onHLSMuxersList(ctx *gin.Context) {
ctx.JSON(http.StatusOK, res.data) 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. // confReload is called by core.
func (a *api) confReload(conf *conf.Conf) { func (a *api) confReload(conf *conf.Conf) {
a.mutex.Lock() a.mutex.Lock()

View File

@@ -38,6 +38,7 @@ type Core struct {
rtmpServer *rtmpServer rtmpServer *rtmpServer
rtmpsServer *rtmpServer rtmpsServer *rtmpServer
hlsServer *hlsServer hlsServer *hlsServer
webRTCServer *webRTCServer
api *api api *api
confWatcher *confwatcher.ConfWatcher confWatcher *confwatcher.ConfWatcher
@@ -180,7 +181,8 @@ func (p *Core) createResources(initial bool) error {
p.logger, err = logger.New( p.logger, err = logger.New(
logger.Level(p.conf.LogLevel), logger.Level(p.conf.LogLevel),
p.conf.LogDestinations, p.conf.LogDestinations,
p.conf.LogFile) p.conf.LogFile,
)
if err != nil { if err != nil {
return err return err
} }
@@ -201,7 +203,8 @@ func (p *Core) createResources(initial bool) error {
if p.metrics == nil { if p.metrics == nil {
p.metrics, err = newMetrics( p.metrics, err = newMetrics(
p.conf.MetricsAddress, p.conf.MetricsAddress,
p) p,
)
if err != nil { if err != nil {
return err return err
} }
@@ -212,7 +215,8 @@ func (p *Core) createResources(initial bool) error {
if p.pprof == nil { if p.pprof == nil {
p.pprof, err = newPPROF( p.pprof, err = newPPROF(
p.conf.PPROFAddress, p.conf.PPROFAddress,
p) p,
)
if err != nil { if err != nil {
return err return err
} }
@@ -229,7 +233,8 @@ func (p *Core) createResources(initial bool) error {
p.conf.Paths, p.conf.Paths,
p.externalCmdPool, p.externalCmdPool,
p.metrics, p.metrics,
p) p,
)
} }
if !p.conf.RTSPDisable && if !p.conf.RTSPDisable &&
@@ -263,7 +268,8 @@ func (p *Core) createResources(initial bool) error {
p.externalCmdPool, p.externalCmdPool,
p.metrics, p.metrics,
p.pathManager, p.pathManager,
p) p,
)
if err != nil { if err != nil {
return err return err
} }
@@ -299,7 +305,8 @@ func (p *Core) createResources(initial bool) error {
p.externalCmdPool, p.externalCmdPool,
p.metrics, p.metrics,
p.pathManager, p.pathManager,
p) p,
)
if err != nil { if err != nil {
return err return err
} }
@@ -326,7 +333,8 @@ func (p *Core) createResources(initial bool) error {
p.externalCmdPool, p.externalCmdPool,
p.metrics, p.metrics,
p.pathManager, p.pathManager,
p) p,
)
if err != nil { if err != nil {
return err return err
} }
@@ -353,7 +361,8 @@ func (p *Core) createResources(initial bool) error {
p.externalCmdPool, p.externalCmdPool,
p.metrics, p.metrics,
p.pathManager, p.pathManager,
p) p,
)
if err != nil { if err != nil {
return err return err
} }
@@ -365,6 +374,9 @@ func (p *Core) createResources(initial bool) error {
p.hlsServer, err = newHLSServer( p.hlsServer, err = newHLSServer(
p.ctx, p.ctx,
p.conf.HLSAddress, p.conf.HLSAddress,
p.conf.HLSEncryption,
p.conf.HLSServerKey,
p.conf.HLSServerCert,
p.conf.ExternalAuthenticationURL, p.conf.ExternalAuthenticationURL,
p.conf.HLSAlwaysRemux, p.conf.HLSAlwaysRemux,
p.conf.HLSVariant, p.conf.HLSVariant,
@@ -373,14 +385,34 @@ func (p *Core) createResources(initial bool) error {
p.conf.HLSPartDuration, p.conf.HLSPartDuration,
p.conf.HLSSegmentMaxSize, p.conf.HLSSegmentMaxSize,
p.conf.HLSAllowOrigin, p.conf.HLSAllowOrigin,
p.conf.HLSEncryption,
p.conf.HLSServerKey,
p.conf.HLSServerCert,
p.conf.HLSTrustedProxies, p.conf.HLSTrustedProxies,
p.conf.ReadBufferCount, p.conf.ReadBufferCount,
p.pathManager, p.pathManager,
p.metrics, 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 { if err != nil {
return err return err
} }
@@ -398,7 +430,9 @@ func (p *Core) createResources(initial bool) error {
p.rtmpServer, p.rtmpServer,
p.rtmpsServer, p.rtmpsServer,
p.hlsServer, p.hlsServer,
p) p.webRTCServer,
p,
)
if err != nil { if err != nil {
return err return err
} }
@@ -416,41 +450,29 @@ func (p *Core) createResources(initial bool) error {
} }
func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) { func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
closeLogger := false closeLogger := newConf == nil ||
if newConf == nil ||
!reflect.DeepEqual(newConf.LogDestinations, p.conf.LogDestinations) || !reflect.DeepEqual(newConf.LogDestinations, p.conf.LogDestinations) ||
newConf.LogFile != p.conf.LogFile { newConf.LogFile != p.conf.LogFile
closeLogger = true
}
closeMetrics := false closeMetrics := newConf == nil ||
if newConf == nil ||
newConf.Metrics != p.conf.Metrics || newConf.Metrics != p.conf.Metrics ||
newConf.MetricsAddress != p.conf.MetricsAddress { newConf.MetricsAddress != p.conf.MetricsAddress
closeMetrics = true
}
closePPROF := false closePPROF := newConf == nil ||
if newConf == nil ||
newConf.PPROF != p.conf.PPROF || newConf.PPROF != p.conf.PPROF ||
newConf.PPROFAddress != p.conf.PPROFAddress { newConf.PPROFAddress != p.conf.PPROFAddress
closePPROF = true
}
closePathManager := false closePathManager := newConf == nil ||
if newConf == nil ||
newConf.RTSPAddress != p.conf.RTSPAddress || newConf.RTSPAddress != p.conf.RTSPAddress ||
newConf.ReadTimeout != p.conf.ReadTimeout || newConf.ReadTimeout != p.conf.ReadTimeout ||
newConf.WriteTimeout != p.conf.WriteTimeout || newConf.WriteTimeout != p.conf.WriteTimeout ||
newConf.ReadBufferCount != p.conf.ReadBufferCount || newConf.ReadBufferCount != p.conf.ReadBufferCount ||
closeMetrics { closeMetrics
closePathManager = true if !closePathManager && !reflect.DeepEqual(newConf.Paths, p.conf.Paths) {
} else if !reflect.DeepEqual(newConf.Paths, p.conf.Paths) {
p.pathManager.confReload(newConf.Paths) p.pathManager.confReload(newConf.Paths)
} }
closeRTSPServer := false closeRTSPServer := newConf == nil ||
if newConf == nil ||
newConf.RTSPDisable != p.conf.RTSPDisable || newConf.RTSPDisable != p.conf.RTSPDisable ||
newConf.Encryption != p.conf.Encryption || newConf.Encryption != p.conf.Encryption ||
newConf.ExternalAuthenticationURL != p.conf.ExternalAuthenticationURL || newConf.ExternalAuthenticationURL != p.conf.ExternalAuthenticationURL ||
@@ -470,12 +492,9 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
newConf.RunOnConnect != p.conf.RunOnConnect || newConf.RunOnConnect != p.conf.RunOnConnect ||
newConf.RunOnConnectRestart != p.conf.RunOnConnectRestart || newConf.RunOnConnectRestart != p.conf.RunOnConnectRestart ||
closeMetrics || closeMetrics ||
closePathManager { closePathManager
closeRTSPServer = true
}
closeRTSPSServer := false closeRTSPSServer := newConf == nil ||
if newConf == nil ||
newConf.RTSPDisable != p.conf.RTSPDisable || newConf.RTSPDisable != p.conf.RTSPDisable ||
newConf.Encryption != p.conf.Encryption || newConf.Encryption != p.conf.Encryption ||
newConf.ExternalAuthenticationURL != p.conf.ExternalAuthenticationURL || newConf.ExternalAuthenticationURL != p.conf.ExternalAuthenticationURL ||
@@ -491,12 +510,9 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
newConf.RunOnConnect != p.conf.RunOnConnect || newConf.RunOnConnect != p.conf.RunOnConnect ||
newConf.RunOnConnectRestart != p.conf.RunOnConnectRestart || newConf.RunOnConnectRestart != p.conf.RunOnConnectRestart ||
closeMetrics || closeMetrics ||
closePathManager { closePathManager
closeRTSPSServer = true
}
closeRTMPServer := false closeRTMPServer := newConf == nil ||
if newConf == nil ||
newConf.RTMPDisable != p.conf.RTMPDisable || newConf.RTMPDisable != p.conf.RTMPDisable ||
newConf.RTMPEncryption != p.conf.RTMPEncryption || newConf.RTMPEncryption != p.conf.RTMPEncryption ||
newConf.RTMPAddress != p.conf.RTMPAddress || newConf.RTMPAddress != p.conf.RTMPAddress ||
@@ -508,12 +524,9 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
newConf.RunOnConnect != p.conf.RunOnConnect || newConf.RunOnConnect != p.conf.RunOnConnect ||
newConf.RunOnConnectRestart != p.conf.RunOnConnectRestart || newConf.RunOnConnectRestart != p.conf.RunOnConnectRestart ||
closeMetrics || closeMetrics ||
closePathManager { closePathManager
closeRTMPServer = true
}
closeRTMPSServer := false closeRTMPSServer := newConf == nil ||
if newConf == nil ||
newConf.RTMPDisable != p.conf.RTMPDisable || newConf.RTMPDisable != p.conf.RTMPDisable ||
newConf.RTMPEncryption != p.conf.RTMPEncryption || newConf.RTMPEncryption != p.conf.RTMPEncryption ||
newConf.RTMPSAddress != p.conf.RTMPSAddress || newConf.RTMPSAddress != p.conf.RTMPSAddress ||
@@ -527,14 +540,14 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
newConf.RunOnConnect != p.conf.RunOnConnect || newConf.RunOnConnect != p.conf.RunOnConnect ||
newConf.RunOnConnectRestart != p.conf.RunOnConnectRestart || newConf.RunOnConnectRestart != p.conf.RunOnConnectRestart ||
closeMetrics || closeMetrics ||
closePathManager { closePathManager
closeRTMPSServer = true
}
closeHLSServer := false closeHLSServer := newConf == nil ||
if newConf == nil ||
newConf.HLSDisable != p.conf.HLSDisable || newConf.HLSDisable != p.conf.HLSDisable ||
newConf.HLSAddress != p.conf.HLSAddress || 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.ExternalAuthenticationURL != p.conf.ExternalAuthenticationURL ||
newConf.HLSAlwaysRemux != p.conf.HLSAlwaysRemux || newConf.HLSAlwaysRemux != p.conf.HLSAlwaysRemux ||
newConf.HLSVariant != p.conf.HLSVariant || newConf.HLSVariant != p.conf.HLSVariant ||
@@ -543,27 +556,33 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
newConf.HLSPartDuration != p.conf.HLSPartDuration || newConf.HLSPartDuration != p.conf.HLSPartDuration ||
newConf.HLSSegmentMaxSize != p.conf.HLSSegmentMaxSize || newConf.HLSSegmentMaxSize != p.conf.HLSSegmentMaxSize ||
newConf.HLSAllowOrigin != p.conf.HLSAllowOrigin || 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) || !reflect.DeepEqual(newConf.HLSTrustedProxies, p.conf.HLSTrustedProxies) ||
newConf.ReadBufferCount != p.conf.ReadBufferCount || newConf.ReadBufferCount != p.conf.ReadBufferCount ||
closePathManager || closePathManager ||
closeMetrics { closeMetrics
closeHLSServer = true
}
closeAPI := false closeWebrtcServer := newConf == nil ||
if 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.API != p.conf.API ||
newConf.APIAddress != p.conf.APIAddress || newConf.APIAddress != p.conf.APIAddress ||
closePathManager || closePathManager ||
closeRTSPServer || closeRTSPServer ||
closeRTSPSServer || closeRTSPSServer ||
closeRTMPServer || closeRTMPServer ||
closeHLSServer { closeHLSServer ||
closeAPI = true closeWebrtcServer
}
if newConf == nil && p.confWatcher != nil { if newConf == nil && p.confWatcher != nil {
p.confWatcher.Close() p.confWatcher.Close()
@@ -594,6 +613,11 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
p.pathManager = nil p.pathManager = nil
} }
if closeWebrtcServer && p.webRTCServer != nil {
p.webRTCServer.close()
p.webRTCServer = nil
}
if closeHLSServer && p.hlsServer != nil { if closeHLSServer && p.hlsServer != nil {
p.hlsServer.close() p.hlsServer.close()
p.hlsServer = nil 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 { func (p *Core) reloadConf(newConf *conf.Conf, calledByAPI bool) error {
p.closeResources(newConf, calledByAPI) p.closeResources(newConf, calledByAPI)
p.conf = newConf p.conf = newConf
return p.createResources(false) return p.createResources(false)
} }

View File

@@ -16,12 +16,15 @@ func newFormatProcessor(forma format.Format, generateRTPPackets bool) (formatPro
case *format.H265: case *format.H265:
return newFormatProcessorH265(forma, generateRTPPackets) return newFormatProcessorH265(forma, generateRTPPackets)
case *format.VP8:
return newFormatProcessorVP8(forma, generateRTPPackets)
case *format.VP9:
return newFormatProcessorVP9(forma, generateRTPPackets)
case *format.MPEG4Audio: case *format.MPEG4Audio:
return newFormatProcessorMPEG4Audio(forma, generateRTPPackets) return newFormatProcessorMPEG4Audio(forma, generateRTPPackets)
case *format.Opus:
return newFormatProcessorOpus(forma, generateRTPPackets)
default: default:
return newFormatProcessorGeneric(forma, generateRTPPackets) return newFormatProcessorGeneric(forma, generateRTPPackets)
} }

View File

@@ -45,7 +45,7 @@ func newFormatProcessorMPEG4Audio(
return t, nil 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) tdata := dat.(*dataMPEG4Audio)
if tdata.rtpPackets != nil { if tdata.rtpPackets != nil {

View File

@@ -5,36 +5,36 @@ import (
"time" "time"
"github.com/aler9/gortsplib/v2/pkg/format" "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" "github.com/pion/rtp"
) )
type dataOpus struct { type dataVP8 struct {
rtpPackets []*rtp.Packet rtpPackets []*rtp.Packet
ntp time.Time ntp time.Time
pts time.Duration pts time.Duration
au []byte frame []byte
} }
func (d *dataOpus) getRTPPackets() []*rtp.Packet { func (d *dataVP8) getRTPPackets() []*rtp.Packet {
return d.rtpPackets return d.rtpPackets
} }
func (d *dataOpus) getNTP() time.Time { func (d *dataVP8) getNTP() time.Time {
return d.ntp return d.ntp
} }
type formatProcessorOpus struct { type formatProcessorVP8 struct {
format *format.Opus format *format.VP8
encoder *rtpsimpleaudio.Encoder encoder *rtpvp8.Encoder
decoder *rtpsimpleaudio.Decoder decoder *rtpvp8.Decoder
} }
func newFormatProcessorOpus( func newFormatProcessorVP8(
forma *format.Opus, forma *format.VP8,
allocateEncoder bool, allocateEncoder bool,
) (*formatProcessorOpus, error) { ) (*formatProcessorVP8, error) {
t := &formatProcessorOpus{ t := &formatProcessorVP8{
format: forma, format: forma,
} }
@@ -45,18 +45,8 @@ func newFormatProcessorOpus(
return t, nil return t, nil
} }
func (t *formatProcessorOpus) generateRTPPackets(tdata *dataOpus) error { func (t *formatProcessorVP8) process(dat data, hasNonRTSPReaders bool) error { //nolint:dupl
pkt, err := t.encoder.Encode(tdata.au, tdata.pts) tdata := dat.(*dataVP8)
if err != nil {
return err
}
tdata.rtpPackets = []*rtp.Packet{pkt}
return nil
}
func (t *formatProcessorOpus) process(dat data, hasNonRTSPReaders bool) error {
tdata := dat.(*dataOpus)
if tdata.rtpPackets != nil { if tdata.rtpPackets != nil {
pkt := tdata.rtpPackets[0] pkt := tdata.rtpPackets[0]
@@ -76,12 +66,15 @@ func (t *formatProcessorOpus) process(dat data, hasNonRTSPReaders bool) error {
t.decoder = t.format.CreateDecoder() t.decoder = t.format.CreateDecoder()
} }
au, pts, err := t.decoder.Decode(pkt) frame, pts, err := t.decoder.Decode(pkt)
if err != nil { if err != nil {
if err == rtpvp8.ErrMorePacketsNeeded {
return nil
}
return err return err
} }
tdata.au = au tdata.frame = frame
tdata.pts = pts tdata.pts = pts
} }
@@ -89,5 +82,11 @@ func (t *formatProcessorOpus) process(dat data, hasNonRTSPReaders bool) error {
return nil 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
} }

View 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
}

View 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>

View File

@@ -3,6 +3,7 @@ package core
import ( import (
"bytes" "bytes"
"context" "context"
_ "embed"
"errors" "errors"
"fmt" "fmt"
"net" "net"
@@ -27,73 +28,8 @@ const (
closeAfterInactivity = 60 * time.Second closeAfterInactivity = 60 * time.Second
) )
const index = `<!DOCTYPE html> //go:embed hls_index.html
<html> var hlsIndex []byte
<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>
`
type hlsMuxerResponse struct { type hlsMuxerResponse struct {
muxer *hlsMuxer muxer *hlsMuxer
@@ -296,7 +232,6 @@ func (m *hlsMuxer) runInner(innerCtx context.Context, innerReady chan struct{})
res := m.pathManager.readerAdd(pathReaderAddReq{ res := m.pathManager.readerAdd(pathReaderAddReq{
author: m, author: m,
pathName: m.pathName, pathName: m.pathName,
authenticate: nil,
}) })
if res.err != nil { if res.err != nil {
return res.err return res.err
@@ -487,7 +422,7 @@ func (m *hlsMuxer) handleRequest(req *hlsMuxerRequest) func() *hls.MuxerFileResp
Header: map[string]string{ Header: map[string]string{
"Content-Type": `text/html`, "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. // apiMuxersList is called by api.
func (m *hlsMuxer) apiHLSMuxersList(req hlsServerAPIMuxersListSubReq) { func (m *hlsMuxer) apiMuxersList(req hlsServerAPIMuxersListSubReq) {
req.res = make(chan struct{}) req.res = make(chan struct{})
select { select {
case m.chAPIHLSMuxersList <- req: case m.chAPIHLSMuxersList <- req:

View File

@@ -8,7 +8,6 @@ import (
"log" "log"
"net" "net"
"net/http" "net/http"
"net/http/httputil"
gopath "path" gopath "path"
"strings" "strings"
"sync" "sync"
@@ -57,14 +56,14 @@ type hlsServerParent interface {
type hlsServer struct { type hlsServer struct {
externalAuthenticationURL string externalAuthenticationURL string
hlsAlwaysRemux bool alwaysRemux bool
hlsVariant conf.HLSVariant variant conf.HLSVariant
hlsSegmentCount int segmentCount int
hlsSegmentDuration conf.StringDuration segmentDuration conf.StringDuration
hlsPartDuration conf.StringDuration partDuration conf.StringDuration
hlsSegmentMaxSize conf.StringSize segmentMaxSize conf.StringSize
hlsAllowOrigin string allowOrigin string
hlsTrustedProxies conf.IPsOrCIDRs trustedProxies conf.IPsOrCIDRs
readBufferCount int readBufferCount int
pathManager *pathManager pathManager *pathManager
metrics *metrics metrics *metrics
@@ -88,18 +87,18 @@ type hlsServer struct {
func newHLSServer( func newHLSServer(
parentCtx context.Context, parentCtx context.Context,
address string, address string,
encryption bool,
serverKey string,
serverCert string,
externalAuthenticationURL string, externalAuthenticationURL string,
hlsAlwaysRemux bool, alwaysRemux bool,
hlsVariant conf.HLSVariant, variant conf.HLSVariant,
hlsSegmentCount int, segmentCount int,
hlsSegmentDuration conf.StringDuration, segmentDuration conf.StringDuration,
hlsPartDuration conf.StringDuration, partDuration conf.StringDuration,
hlsSegmentMaxSize conf.StringSize, segmentMaxSize conf.StringSize,
hlsAllowOrigin string, allowOrigin string,
hlsEncryption bool, trustedProxies conf.IPsOrCIDRs,
hlsServerKey string,
hlsServerCert string,
hlsTrustedProxies conf.IPsOrCIDRs,
readBufferCount int, readBufferCount int,
pathManager *pathManager, pathManager *pathManager,
metrics *metrics, metrics *metrics,
@@ -111,8 +110,8 @@ func newHLSServer(
} }
var tlsConfig *tls.Config var tlsConfig *tls.Config
if hlsEncryption { if encryption {
crt, err := tls.LoadX509KeyPair(hlsServerCert, hlsServerKey) crt, err := tls.LoadX509KeyPair(serverCert, serverKey)
if err != nil { if err != nil {
ln.Close() ln.Close()
return nil, err return nil, err
@@ -127,14 +126,14 @@ func newHLSServer(
s := &hlsServer{ s := &hlsServer{
externalAuthenticationURL: externalAuthenticationURL, externalAuthenticationURL: externalAuthenticationURL,
hlsAlwaysRemux: hlsAlwaysRemux, alwaysRemux: alwaysRemux,
hlsVariant: hlsVariant, variant: variant,
hlsSegmentCount: hlsSegmentCount, segmentCount: segmentCount,
hlsSegmentDuration: hlsSegmentDuration, segmentDuration: segmentDuration,
hlsPartDuration: hlsPartDuration, partDuration: partDuration,
hlsSegmentMaxSize: hlsSegmentMaxSize, segmentMaxSize: segmentMaxSize,
hlsAllowOrigin: hlsAllowOrigin, allowOrigin: allowOrigin,
hlsTrustedProxies: hlsTrustedProxies, trustedProxies: trustedProxies,
readBufferCount: readBufferCount, readBufferCount: readBufferCount,
pathManager: pathManager, pathManager: pathManager,
parent: parent, parent: parent,
@@ -180,10 +179,10 @@ func (s *hlsServer) run() {
defer s.wg.Done() defer s.wg.Done()
router := gin.New() router := gin.New()
router.NoRoute(s.onRequest) router.NoRoute(httpLoggerMiddleware(s), s.onRequest)
tmp := make([]string, len(s.hlsTrustedProxies)) tmp := make([]string, len(s.trustedProxies))
for i, entry := range s.hlsTrustedProxies { for i, entry := range s.trustedProxies {
tmp[i] = entry.String() tmp[i] = entry.String()
} }
router.SetTrustedProxies(tmp) router.SetTrustedProxies(tmp)
@@ -204,12 +203,12 @@ outer:
for { for {
select { select {
case pa := <-s.chPathSourceReady: case pa := <-s.chPathSourceReady:
if s.hlsAlwaysRemux { if s.alwaysRemux {
s.findOrCreateMuxer(pa.Name(), "", nil) s.findOrCreateMuxer(pa.Name(), "", nil)
} }
case pa := <-s.chPathSourceNotReady: case pa := <-s.chPathSourceNotReady:
if s.hlsAlwaysRemux { if s.alwaysRemux {
c, ok := s.muxers[pa.Name()] c, ok := s.muxers[pa.Name()]
if ok { if ok {
c.close() c.close()
@@ -226,7 +225,7 @@ outer:
} }
delete(s.muxers, c.PathName()) delete(s.muxers, c.PathName())
if s.hlsAlwaysRemux && c.remoteAddr == "" { if s.alwaysRemux && c.remoteAddr == "" {
s.findOrCreateMuxer(c.PathName(), "", nil) s.findOrCreateMuxer(c.PathName(), "", nil)
} }
@@ -259,16 +258,7 @@ outer:
} }
func (s *hlsServer) onRequest(ctx *gin.Context) { func (s *hlsServer) onRequest(ctx *gin.Context) {
s.log(logger.Debug, "[conn %v] %s %s", ctx.ClientIP(), ctx.Request.Method, ctx.Request.URL.Path) ctx.Writer.Header().Set("Access-Control-Allow-Origin", s.allowOrigin)
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-Credentials", "true") ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
switch ctx.Request.Method { switch ctx.Request.Method {
@@ -281,7 +271,6 @@ func (s *hlsServer) onRequest(ctx *gin.Context) {
return return
default: default:
ctx.Writer.WriteHeader(http.StatusNotFound)
return return
} }
@@ -290,7 +279,6 @@ func (s *hlsServer) onRequest(ctx *gin.Context) {
switch pa { switch pa {
case "", "favicon.ico": case "", "favicon.ico":
ctx.Writer.WriteHeader(http.StatusNotFound)
return return
} }
@@ -336,8 +324,6 @@ func (s *hlsServer) onRequest(ctx *gin.Context) {
case <-s.ctx.Done(): 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 { 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, pathName,
remoteAddr, remoteAddr,
s.externalAuthenticationURL, s.externalAuthenticationURL,
s.hlsVariant, s.variant,
s.hlsSegmentCount, s.segmentCount,
s.hlsSegmentDuration, s.segmentDuration,
s.hlsPartDuration, s.partDuration,
s.hlsSegmentMaxSize, s.segmentMaxSize,
s.readBufferCount, s.readBufferCount,
req, req,
&s.wg, &s.wg,
@@ -390,8 +376,8 @@ func (s *hlsServer) pathSourceNotReady(pa *path) {
} }
} }
// apiHLSMuxersList is called by api. // apiMuxersList is called by api.
func (s *hlsServer) apiHLSMuxersList() hlsServerAPIMuxersListRes { func (s *hlsServer) apiMuxersList() hlsServerAPIMuxersListRes {
req := hlsServerAPIMuxersListReq{ req := hlsServerAPIMuxersListReq{
res: make(chan hlsServerAPIMuxersListRes), res: make(chan hlsServerAPIMuxersListRes),
} }
@@ -405,7 +391,7 @@ func (s *hlsServer) apiHLSMuxersList() hlsServerAPIMuxersListRes {
} }
for _, pa := range res.muxers { for _, pa := range res.muxers {
pa.apiHLSMuxersList(hlsServerAPIMuxersListSubReq{data: res.data}) pa.apiMuxersList(hlsServerAPIMuxersListSubReq{data: res.data})
} }
return res return res

View 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())
}
}

View File

@@ -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()
}

View File

@@ -17,23 +17,6 @@ func metric(key string, value int64) string {
return key + " " + strconv.FormatInt(value, 10) + "\n" 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 { type metricsParent interface {
Log(logger.Level, string, ...interface{}) Log(logger.Level, string, ...interface{})
} }
@@ -44,11 +27,12 @@ type metrics struct {
ln net.Listener ln net.Listener
server *http.Server server *http.Server
mutex sync.Mutex mutex sync.Mutex
pathManager metricsPathManager pathManager apiPathManager
rtspServer metricsRTSPServer rtspServer apiRTSPServer
rtspsServer metricsRTSPServer rtspsServer apiRTSPServer
rtmpServer metricsRTMPServer rtmpServer apiRTMPServer
hlsServer metricsHLSServer hlsServer apiHLSServer
webRTCServer apiWebRTCServer
} }
func newMetrics( 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 if !interfaceIsEmpty(m.rtspServer) { //nolint:dupl
func() { func() {
res := m.rtspServer.apiConnsList() res := m.rtspServer.apiConnsList()
@@ -171,13 +166,14 @@ func (m *metrics) onMetrics(ctx *gin.Context) {
} }
} }
if !interfaceIsEmpty(m.hlsServer) { if !interfaceIsEmpty(m.webRTCServer) {
res := m.hlsServer.apiHLSMuxersList() res := m.webRTCServer.apiConnsList()
if res.err == nil { if res.err == nil {
for name, i := range res.data.Items { for id, i := range res.data.Items {
tags := "{name=\"" + name + "\"}" tags := "{id=\"" + id + "\"}"
out += metric("hls_muxers"+tags, 1) out += metric("webrtc_conns"+tags, 1)
out += metric("hls_muxers_bytes_sent"+tags, int64(i.BytesSent)) 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. // pathManagerSet is called by pathManager.
func (m *metrics) pathManagerSet(s metricsPathManager) { func (m *metrics) pathManagerSet(s apiPathManager) {
m.mutex.Lock() m.mutex.Lock()
defer m.mutex.Unlock() defer m.mutex.Unlock()
m.pathManager = s 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). // rtspServerSet is called by rtspServer (plain).
func (m *metrics) rtspServerSet(s metricsRTSPServer) { func (m *metrics) rtspServerSet(s apiRTSPServer) {
m.mutex.Lock() m.mutex.Lock()
defer m.mutex.Unlock() defer m.mutex.Unlock()
m.rtspServer = s m.rtspServer = s
} }
// rtspsServerSet is called by rtspServer (tls). // rtspsServerSet is called by rtspServer (tls).
func (m *metrics) rtspsServerSet(s metricsRTSPServer) { func (m *metrics) rtspsServerSet(s apiRTSPServer) {
m.mutex.Lock() m.mutex.Lock()
defer m.mutex.Unlock() defer m.mutex.Unlock()
m.rtspsServer = s m.rtspsServer = s
} }
// rtmpServerSet is called by rtmpServer. // rtmpServerSet is called by rtmpServer.
func (m *metrics) rtmpServerSet(s metricsRTMPServer) { func (m *metrics) rtmpServerSet(s apiRTMPServer) {
m.mutex.Lock() m.mutex.Lock()
defer m.mutex.Unlock() defer m.mutex.Unlock()
m.rtmpServer = s m.rtmpServer = s
} }
// hlsServerSet is called by hlsServer. // webRTCServerSet is called by webRTCServer.
func (m *metrics) hlsServerSet(s metricsHLSServer) { func (m *metrics) webRTCServerSet(s apiWebRTCServer) {
m.mutex.Lock() m.mutex.Lock()
defer m.mutex.Unlock() defer m.mutex.Unlock()
m.hlsServer = s m.webRTCServer = s
} }

View File

@@ -27,6 +27,9 @@ func TestMetrics(t *testing.T) {
defer os.Remove(serverKeyFpath) defer os.Remove(serverKeyFpath)
p, ok := newInstance("metrics: yes\n" + p, ok := newInstance("metrics: yes\n" +
"webrtc: yes\n" +
"webrtcServerCert: " + serverCertFpath + "\n" +
"webrtcServerKey: " + serverKeyFpath + "\n" +
"encryption: optional\n" + "encryption: optional\n" +
"serverCert: " + serverCertFpath + "\n" + "serverCert: " + serverCertFpath + "\n" +
"serverKey: " + serverKeyFpath + "\n" + "serverKey: " + serverKeyFpath + "\n" +
@@ -99,6 +102,8 @@ func TestMetrics(t *testing.T) {
`paths_bytes_received\{name=".*?",state="ready"\} 0`+"\n"+ `paths_bytes_received\{name=".*?",state="ready"\} 0`+"\n"+
`paths\{name=".*?",state="ready"\} 1`+"\n"+ `paths\{name=".*?",state="ready"\} 1`+"\n"+
`paths_bytes_received\{name=".*?",state="ready"\} 0`+"\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\{id=".*?"\} 1`+"\n"+
`rtsp_conns_bytes_received\{id=".*?"\} [0-9]+`+"\n"+ `rtsp_conns_bytes_received\{id=".*?"\} [0-9]+`+"\n"+
`rtsp_conns_bytes_sent\{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\{id=".*?",state="publish"\} 1`+"\n"+
`rtmp_conns_bytes_received\{id=".*?",state="publish"\} [0-9]+`+"\n"+ `rtmp_conns_bytes_received\{id=".*?",state="publish"\} [0-9]+`+"\n"+
`rtmp_conns_bytes_sent\{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)) string(bo))
} }

View File

@@ -180,6 +180,7 @@ outer:
continue continue
} }
if req.authenticate != nil {
err = req.authenticate( err = req.authenticate(
pathConf.ReadIPs, pathConf.ReadIPs,
pathConf.ReadUser, pathConf.ReadUser,
@@ -188,6 +189,7 @@ outer:
req.res <- pathDescribeRes{err: err} req.res <- pathDescribeRes{err: err}
continue continue
} }
}
// create path if it doesn't exist // create path if it doesn't exist
if _, ok := pm.paths[req.pathName]; !ok { if _, ok := pm.paths[req.pathName]; !ok {
@@ -352,12 +354,18 @@ func (pm *pathManager) describe(req pathDescribeReq) pathDescribeRes {
req.res = make(chan pathDescribeRes) req.res = make(chan pathDescribeRes)
select { select {
case pm.chDescribe <- req: case pm.chDescribe <- req:
res := <-req.res res1 := <-req.res
if res.err != nil { if res1.err != nil {
return res 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(): case <-pm.ctx.Done():
return pathDescribeRes{err: fmt.Errorf("terminated")} return pathDescribeRes{err: fmt.Errorf("terminated")}

View File

@@ -76,7 +76,7 @@ type rtmpConn struct {
ctxCancel func() ctxCancel func()
uuid uuid.UUID uuid uuid.UUID
created time.Time created time.Time
path *path // path *path
state rtmpConnState state rtmpConnState
stateMutex sync.Mutex stateMutex sync.Mutex
} }
@@ -153,7 +153,6 @@ func (c *rtmpConn) safeState() rtmpConnState {
func (c *rtmpConn) run() { func (c *rtmpConn) run() {
defer c.wg.Done() defer c.wg.Done()
err := func() error {
if c.runOnConnect != "" { if c.runOnConnect != "" {
c.log(logger.Info, "runOnConnect command started") c.log(logger.Info, "runOnConnect command started")
_, port, _ := net.SplitHostPort(c.rtspAddress) _, port, _ := net.SplitHostPort(c.rtspAddress)
@@ -181,17 +180,16 @@ func (c *rtmpConn) run() {
runErr <- c.runInner(ctx) runErr <- c.runInner(ctx)
}() }()
var err error
select { select {
case err := <-runErr: case err = <-runErr:
cancel() cancel()
return err
case <-c.ctx.Done(): case <-c.ctx.Done():
cancel() cancel()
<-runErr <-runErr
return errors.New("terminated") err = errors.New("terminated")
} }
}()
c.ctxCancel() c.ctxCancel()
@@ -243,10 +241,10 @@ func (c *rtmpConn) runRead(ctx context.Context, u *url.URL) error {
return res.err return res.err
} }
c.path = res.path path := res.path
defer func() { defer func() {
c.path.readerRemove(pathReaderRemoveReq{author: c}) path.readerRemove(pathReaderRemoveReq{author: c})
}() }()
c.stateMutex.Lock() c.stateMutex.Lock()
@@ -288,15 +286,15 @@ func (c *rtmpConn) runRead(ctx context.Context, u *url.URL) error {
defer res.stream.readerRemove(c) defer res.stream.readerRemove(c)
c.log(logger.Info, "is reading from path '%s', %s", 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") c.log(logger.Info, "runOnRead command started")
onReadCmd := externalcmd.NewCmd( onReadCmd := externalcmd.NewCmd(
c.externalCmdPool, c.externalCmdPool,
c.path.Conf().RunOnRead, path.Conf().RunOnRead,
c.path.Conf().RunOnReadRestart, path.Conf().RunOnReadRestart,
c.path.externalCmdEnv(), path.externalCmdEnv(),
func(co int) { func(co int) {
c.log(logger.Info, "runOnRead command exited with code %d", co) 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 return res.err
} }
c.path = res.path path := res.path
defer func() { defer func() {
c.path.publisherRemove(pathPublisherRemoveReq{author: c}) path.publisherRemove(pathPublisherRemoveReq{author: c})
}() }()
c.stateMutex.Lock() c.stateMutex.Lock()
@@ -512,7 +510,7 @@ func (c *rtmpConn) runPublish(ctx context.Context, u *url.URL) error {
medias = append(medias, audioMedia) medias = append(medias, audioMedia)
} }
rres := c.path.publisherStart(pathPublisherStartReq{ rres := path.publisherStart(pathPublisherStartReq{
author: c, author: c,
medias: medias, medias: medias,
generateRTPPackets: true, 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.log(logger.Info, "is publishing to path '%s', %s",
c.path.Name(), path.Name(),
sourceMediaInfo(medias)) sourceMediaInfo(medias))
// disable write deadline to allow outgoing acknowledges // disable write deadline to allow outgoing acknowledges

View File

@@ -214,9 +214,6 @@ outer:
s.conns[c] = struct{}{} s.conns[c] = struct{}{}
case c := <-s.chConnClose: case c := <-s.chConnClose:
if _, ok := s.conns[c]; !ok {
continue
}
delete(s.conns, c) delete(s.conns, c)
case req := <-s.chAPIConnsList: case req := <-s.chAPIConnsList:

View File

@@ -180,11 +180,11 @@ func newRTSPServer(
s.log(logger.Info, "listener opened on %s", printAddresses(s.srv)) s.log(logger.Info, "listener opened on %s", printAddresses(s.srv))
if s.metrics != nil { if metrics != nil {
if !isTLS { if !isTLS {
s.metrics.rtspServerSet(s) metrics.rtspServerSet(s)
} else { } else {
s.metrics.rtspsServerSet(s) metrics.rtspsServerSet(s)
} }
} }

View File

@@ -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) { 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}, rtpPackets: []*rtp.Packet{pkt},
ntp: time.Now(), 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) { 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}, rtpPackets: []*rtp.Packet{pkt},
ntp: time.Now(), ntp: time.Now(),
}) })

View File

@@ -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) { 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}, rtpPackets: []*rtp.Packet{pkt},
ntp: time.Now(), 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) { 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}, rtpPackets: []*rtp.Packet{pkt},
ntp: time.Now(), ntp: time.Now(),
}) })

View 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"}
}

View 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>

View 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")}
}
}

View File

@@ -121,6 +121,16 @@ rtmpServerCert: server.crt
hlsDisable: no hlsDisable: no
# Address of the HLS listener. # Address of the HLS listener.
hlsAddress: :8888 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. # 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. # This option allows to generate it always, avoiding the delay between request and generation.
hlsAlwaysRemux: no hlsAlwaysRemux: no
@@ -151,21 +161,40 @@ hlsSegmentMaxSize: 50M
# Value of the Access-Control-Allow-Origin header provided in every HTTP response. # Value of the Access-Control-Allow-Origin header provided in every HTTP response.
# This allows to play the HLS stream from an external website. # This allows to play the HLS stream from an external website.
hlsAllowOrigin: '*' hlsAllowOrigin: '*'
# Enable TLS/HTTPS on the HLS server. # List of IPs or CIDRs of proxies placed before 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.
# If the server receives a request from one of these entries, IP in logs # If the server receives a request from one of these entries, IP in logs
# will be taken from the X-Forwarded-For header. # will be taken from the X-Forwarded-For header.
hlsTrustedProxies: [] 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 # Path parameters