Skip to content

Commit

Permalink
add playback server (#2452) (#2906)
Browse files Browse the repository at this point in the history
* add playback server

* add playback switch

* update readme
  • Loading branch information
aler9 authored Jan 23, 2024
1 parent fd2466a commit 57c2d5a
Show file tree
Hide file tree
Showing 30 changed files with 1,411 additions and 345 deletions.
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ And can be recorded with:
* Streams are automatically converted from a protocol to another
* Serve multiple streams at once in separate paths
* Record streams to disk
* Playback recordings
* Authenticate users; use internal or external authentication
* Redirect readers to other RTSP servers (load balancing)
* Query and control the server through the API
Expand Down Expand Up @@ -115,6 +116,7 @@ _rtsp-simple-server_ has been rebranded as _MediaMTX_. The reason is pretty obvi
* [Encrypt the configuration](#encrypt-the-configuration)
* [Remuxing, re-encoding, compression](#remuxing-re-encoding-compression)
* [Record streams to disk](#record-streams-to-disk)
* [Playback recordings](#playback-recordings)
* [Forward streams to other servers](#forward-streams-to-other-servers)
* [Proxy requests to other servers](#proxy-requests-to-other-servers)
* [On-demand publishing](#on-demand-publishing)
Expand Down Expand Up @@ -1183,6 +1185,44 @@ To upload recordings to a remote location, you can use _MediaMTX_ together with
If you want to delete local segments after they are uploaded, replace `rclone sync` with `rclone move`.
### Playback recordings
Recordings can be served to users through a dedicated HTTP server, that can be enabled inside the configuration:
```yml
playback: yes
playbackAddress: :9996
```
The server can be queried for recordings by using the URL:
```
http://localhost:9996/get?path=[mypath]&start=[start_date]&duration=[duration]&format=[format]
```
Where:
* [mypath] is the path name
* [start_date] is the start date in RFC3339 format
* [duration] is the maximum duration of the recording in Golang format (example: 20s, 20h)
* [format] must be fmp4
All parameters must be [url-encoded](https://www.urlencoder.org/).
For instance:
```
http://localhost:9996/get?path=stream2&start=2024-01-14T16%3A33%3A17%2B00%3A00&duration=200s&format=fmp4
```
The resulting stream is natively compatible with any browser, therefore its URL can be directly inserted into a \<video> tag:
```html
<video controls>
<source src="http://localhost:9996/get?path=stream2&start=2024-01-14T16%3A33%3A17%2B00%3A00&duration=200s&format=fmp4" type="video/mp4" />
</video>
```
### Forward streams to other servers
To forward incoming streams to another server, use _FFmpeg_ inside the `runOnReady` parameter:
Expand Down
20 changes: 15 additions & 5 deletions apidocs/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,6 @@ components:
type: integer
externalAuthenticationURL:
type: string
api:
type: boolean
apiAddress:
type: string
metrics:
type: boolean
metricsAddress:
Expand All @@ -62,6 +58,18 @@ components:
runOnDisconnect:
type: string

# API
api:
type: boolean
apiAddress:
type: string

# Playback server
playback:
type: boolean
playbackAddress:
type: string

# RTSP server
rtsp:
type: boolean
Expand Down Expand Up @@ -213,9 +221,11 @@ components:
fallback:
type: string

# Record
# Record and playback
record:
type: boolean
playback:
type: boolean
recordPath:
type: string
recordFormat:
Expand Down
3 changes: 1 addition & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ require (
code.cloudfoundry.org/bytefmt v0.0.0
github.com/abema/go-mp4 v1.2.0
github.com/alecthomas/kong v0.8.1
github.com/aler9/writerseeker v1.1.0
github.com/bluenviron/gohlslib v1.2.0
github.com/bluenviron/gohlslib v1.2.1-0.20240114214154-83fc88edbaad
github.com/bluenviron/gortsplib/v4 v4.7.1
github.com/bluenviron/mediacommon v1.9.0
github.com/datarhei/gosrt v0.5.7
Expand Down
6 changes: 2 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,14 @@ github.com/aler9/sdp/v3 v3.0.0-20231022165400-33437e07f326 h1:HA7u47vkcxFiHtiOjm
github.com/aler9/sdp/v3 v3.0.0-20231022165400-33437e07f326/go.mod h1:I40uD/ZSmK2peI6AdJga5fd55d4bFK0oWOgLS9Q8sVc=
github.com/aler9/webrtc/v3 v3.0.0-20231112223655-e402ed2689c6 h1:wMd3D1mLghoYYh31STig8Kwm2qi8QyQKUy09qUUZrVw=
github.com/aler9/webrtc/v3 v3.0.0-20231112223655-e402ed2689c6/go.mod h1:1CaT2fcZzZ6VZA+O1i9yK2DU4EOcXVvSbWG9pr5jefs=
github.com/aler9/writerseeker v1.1.0 h1:t+Sm3tjp8scNlqyoa8obpeqwciMNOvdvsxjxEb3Sx3g=
github.com/aler9/writerseeker v1.1.0/go.mod h1:QNCcjSKnLsYoTfMmXkEEfgbz6nNXWxKSaBY+hGJGWDA=
github.com/asticode/go-astikit v0.30.0 h1:DkBkRQRIxYcknlaU7W7ksNfn4gMFsB0tqMJflxkRsZA=
github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
github.com/asticode/go-astits v1.13.0 h1:XOgkaadfZODnyZRR5Y0/DWkA9vrkLLPLeeOvDwfKZ1c=
github.com/asticode/go-astits v1.13.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI=
github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c h1:8XZeJrs4+ZYhJeJ2aZxADI2tGADS15AzIF8MQ8XAhT4=
github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c/go.mod h1:x1vxHcL/9AVzuk5HOloOEPrtJY0MaalYr78afXZ+pWI=
github.com/bluenviron/gohlslib v1.2.0 h1:Hrx2/n/AcmKKIV+MjZLKs5kmW+O7xCdUSPJQoS39JKw=
github.com/bluenviron/gohlslib v1.2.0/go.mod h1:kG/Sjebsxnf5asMGaGcQ0aSvtFGNChJPgctds2wDHOI=
github.com/bluenviron/gohlslib v1.2.1-0.20240114214154-83fc88edbaad h1:R9Lqf0A2/3TTB4casoU1LC+HRLmsVxNYUTmnbbD8WAE=
github.com/bluenviron/gohlslib v1.2.1-0.20240114214154-83fc88edbaad/go.mod h1:k94WhiVkgJl45Q1WkLw8/GG2AJ1+VU9c/3i4f41xMq8=
github.com/bluenviron/gortsplib/v4 v4.7.1 h1:ZiPHjnIsdPDfPGZgfBr2n2xCFZlvmc/5zEqdoJUa1vU=
github.com/bluenviron/gortsplib/v4 v4.7.1/go.mod h1:3+IYh85PgIPLHr4D5z7GnRvpu/ogSHMDhsYW/CjrD8E=
github.com/bluenviron/mediacommon v1.9.0 h1:0I7PuwaDD6uOeQlV3WOlC/7FFESDa4dllYylj1YcnI4=
Expand Down
14 changes: 7 additions & 7 deletions internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ func (a *API) writeError(ctx *gin.Context, status int, err error) {
// show error in logs
a.Log(logger.Error, err.Error())

// send error in response
// add error to response
ctx.JSON(status, &defs.APIError{
Error: err.Error(),
})
Expand Down Expand Up @@ -303,7 +303,7 @@ func (a *API) onConfigGlobalPatch(ctx *gin.Context) {

newConf.PatchGlobal(&c)

err = newConf.Check()
err = newConf.Validate()
if err != nil {
a.writeError(ctx, http.StatusBadRequest, err)
return
Expand Down Expand Up @@ -341,7 +341,7 @@ func (a *API) onConfigPathDefaultsPatch(ctx *gin.Context) {

newConf.PatchPathDefaults(&p)

err = newConf.Check()
err = newConf.Validate()
if err != nil {
a.writeError(ctx, http.StatusBadRequest, err)
return
Expand Down Expand Up @@ -422,7 +422,7 @@ func (a *API) onConfigPathsAdd(ctx *gin.Context) { //nolint:dupl
return
}

err = newConf.Check()
err = newConf.Validate()
if err != nil {
a.writeError(ctx, http.StatusBadRequest, err)
return
Expand Down Expand Up @@ -463,7 +463,7 @@ func (a *API) onConfigPathsPatch(ctx *gin.Context) { //nolint:dupl
return
}

err = newConf.Check()
err = newConf.Validate()
if err != nil {
a.writeError(ctx, http.StatusBadRequest, err)
return
Expand Down Expand Up @@ -504,7 +504,7 @@ func (a *API) onConfigPathsReplace(ctx *gin.Context) { //nolint:dupl
return
}

err = newConf.Check()
err = newConf.Validate()
if err != nil {
a.writeError(ctx, http.StatusBadRequest, err)
return
Expand Down Expand Up @@ -538,7 +538,7 @@ func (a *API) onConfigPathsDelete(ctx *gin.Context) {
return
}

err = newConf.Check()
err = newConf.Validate()
if err != nil {
a.writeError(ctx, http.StatusBadRequest, err)
return
Expand Down
35 changes: 23 additions & 12 deletions internal/conf/conf.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,6 @@ type Conf struct {
WriteQueueSize int `json:"writeQueueSize"`
UDPMaxPayloadSize int `json:"udpMaxPayloadSize"`
ExternalAuthenticationURL string `json:"externalAuthenticationURL"`
API bool `json:"api"`
APIAddress string `json:"apiAddress"`
Metrics bool `json:"metrics"`
MetricsAddress string `json:"metricsAddress"`
PPROF bool `json:"pprof"`
Expand All @@ -104,6 +102,14 @@ type Conf struct {
RunOnConnectRestart bool `json:"runOnConnectRestart"`
RunOnDisconnect string `json:"runOnDisconnect"`

// API
API bool `json:"api"`
APIAddress string `json:"apiAddress"`

// Playback
Playback bool `json:"playback"`
PlaybackAddress string `json:"playbackAddress"`

// RTSP server
RTSP bool `json:"rtsp"`
RTSPDisable *bool `json:"rtspDisable,omitempty"` // deprecated
Expand Down Expand Up @@ -195,11 +201,16 @@ func (conf *Conf) setDefaults() {
conf.WriteTimeout = 10 * StringDuration(time.Second)
conf.WriteQueueSize = 512
conf.UDPMaxPayloadSize = 1472
conf.APIAddress = "127.0.0.1:9997"
conf.MetricsAddress = "127.0.0.1:9998"
conf.PPROFAddress = "127.0.0.1:9999"

// RTSP
// API
conf.APIAddress = "127.0.0.1:9997"

// Playback server
conf.PlaybackAddress = ":9996"

// RTSP server
conf.RTSP = true
conf.Protocols = Protocols{
Protocol(gortsplib.TransportUDP): {},
Expand All @@ -217,7 +228,7 @@ func (conf *Conf) setDefaults() {
conf.ServerCert = "server.crt"
conf.AuthMethods = AuthMethods{headers.AuthBasic}

// RTMP
// RTMP server
conf.RTMP = true
conf.RTMPAddress = ":1935"
conf.RTMPSAddress = ":1936"
Expand All @@ -236,7 +247,7 @@ func (conf *Conf) setDefaults() {
conf.HLSSegmentMaxSize = 50 * 1024 * 1024
conf.HLSAllowOrigin = "*"

// WebRTC
// WebRTC server
conf.WebRTC = true
conf.WebRTCAddress = ":8889"
conf.WebRTCServerKey = "server.key"
Expand All @@ -248,7 +259,7 @@ func (conf *Conf) setDefaults() {
conf.WebRTCAdditionalHosts = []string{}
conf.WebRTCICEServers2 = []WebRTCICEServer{}

// SRT
// SRT server
conf.SRT = true
conf.SRTAddress = ":8890"

Expand All @@ -274,7 +285,7 @@ func Load(fpath string, defaultConfPaths []string) (*Conf, string, error) {
return nil, "", err
}

err = conf.Check()
err = conf.Validate()
if err != nil {
return nil, "", err
}
Expand Down Expand Up @@ -337,8 +348,8 @@ func (conf Conf) Clone() *Conf {
return &dest
}

// Check checks the configuration for errors.
func (conf *Conf) Check() error {
// Validate checks the configuration for errors.
func (conf *Conf) Validate() error {
// General

if conf.ReadBufferCount != nil {
Expand Down Expand Up @@ -436,7 +447,7 @@ func (conf *Conf) Check() error {
}
}

// Record
// Record (deprecated)
if conf.Record != nil {
conf.PathDefaults.Record = *conf.Record
}
Expand Down Expand Up @@ -479,7 +490,7 @@ func (conf *Conf) Check() error {
pconf := newPath(&conf.PathDefaults, optional)
conf.Paths[name] = pconf

err := pconf.check(conf, name)
err := pconf.validate(conf, name)
if err != nil {
return err
}
Expand Down
1 change: 1 addition & 0 deletions internal/conf/conf_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ func TestConfFromFile(t *testing.T) {
Source: "publisher",
SourceOnDemandStartTimeout: 10 * StringDuration(time.Second),
SourceOnDemandCloseAfter: 10 * StringDuration(time.Second),
Playback: true,
RecordPath: "./recordings/%path/%Y-%m-%d_%H-%M-%S-%f",
RecordFormat: RecordFormatFMP4,
RecordPartDuration: 100000000,
Expand Down
40 changes: 19 additions & 21 deletions internal/conf/credential.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func (d *Credential) UnmarshalJSON(b []byte) error {
value: in,
}

return d.validateConfig()
return d.validate()
}

// UnmarshalEnv implements env.Unmarshaler.
Expand Down Expand Up @@ -97,26 +97,24 @@ func (d *Credential) Check(guess string) bool {
return d.value == guess
}

func (d *Credential) validateConfig() error {
if d.IsEmpty() {
return nil
}

switch {
case d.IsSha256():
if !reBase64.MatchString(d.value) {
return fmt.Errorf("credential contains unsupported characters, sha256 hash must be base64 encoded")
}
case d.IsArgon2():
// TODO: remove matthewhartstonge/argon2 when this PR gets merged into mainline Go:
// https://go-review.googlesource.com/c/crypto/+/502515
_, err := argon2.Decode([]byte(d.value[len("argon2:"):]))
if err != nil {
return fmt.Errorf("invalid argon2 hash: %w", err)
}
default:
if !rePlainCredential.MatchString(d.value) {
return fmt.Errorf("credential contains unsupported characters. Supported are: %s", plainCredentialSupportedChars)
func (d *Credential) validate() error {
if !d.IsEmpty() {
switch {
case d.IsSha256():
if !reBase64.MatchString(d.value) {
return fmt.Errorf("credential contains unsupported characters, sha256 hash must be base64 encoded")
}
case d.IsArgon2():
// TODO: remove matthewhartstonge/argon2 when this PR gets merged into mainline Go:
// https://go-review.googlesource.com/c/crypto/+/502515
_, err := argon2.Decode([]byte(d.value[len("argon2:"):]))
if err != nil {
return fmt.Errorf("invalid argon2 hash: %w", err)
}
default:
if !rePlainCredential.MatchString(d.value) {
return fmt.Errorf("credential contains unsupported characters. Supported are: %s", plainCredentialSupportedChars)
}
}
}
return nil
Expand Down
4 changes: 2 additions & 2 deletions internal/conf/credential_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ func TestCredential(t *testing.T) {
assert.False(t, cred.Check("notestuser"))
})

t.Run("validateConfig", func(t *testing.T) {
t.Run("validate", func(t *testing.T) {
tests := []struct {
name string
cred *Credential
Expand Down Expand Up @@ -155,7 +155,7 @@ func TestCredential(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.cred.validateConfig()
err := tt.cred.validate()
if tt.wantErr {
assert.Error(t, err)
} else {
Expand Down
Loading

0 comments on commit 57c2d5a

Please sign in to comment.