Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add playback server (#2452) #2906

Merged
merged 3 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.8.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.8.0 h1:z9ZxCkuivs1IN1NrD6lB7d9twqcRNaCTvlG39NL9Sa4=
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 @@
// show error in logs
a.Log(logger.Error, err.Error())

// send error in response
// add error to response

Check warning on line 277 in internal/api/api.go

View check run for this annotation

Codecov / codecov/patch

internal/api/api.go#L277

Added line #L277 was not covered by tests
ctx.JSON(status, &defs.APIError{
Error: err.Error(),
})
Expand Down Expand Up @@ -303,7 +303,7 @@

newConf.PatchGlobal(&c)

err = newConf.Check()
err = newConf.Validate()

Check warning on line 306 in internal/api/api.go

View check run for this annotation

Codecov / codecov/patch

internal/api/api.go#L306

Added line #L306 was not covered by tests
if err != nil {
a.writeError(ctx, http.StatusBadRequest, err)
return
Expand Down Expand Up @@ -341,7 +341,7 @@

newConf.PatchPathDefaults(&p)

err = newConf.Check()
err = newConf.Validate()

Check warning on line 344 in internal/api/api.go

View check run for this annotation

Codecov / codecov/patch

internal/api/api.go#L344

Added line #L344 was not covered by tests
if err != nil {
a.writeError(ctx, http.StatusBadRequest, err)
return
Expand Down Expand Up @@ -422,7 +422,7 @@
return
}

err = newConf.Check()
err = newConf.Validate()

Check warning on line 425 in internal/api/api.go

View check run for this annotation

Codecov / codecov/patch

internal/api/api.go#L425

Added line #L425 was not covered by tests
if err != nil {
a.writeError(ctx, http.StatusBadRequest, err)
return
Expand Down Expand Up @@ -463,7 +463,7 @@
return
}

err = newConf.Check()
err = newConf.Validate()

Check warning on line 466 in internal/api/api.go

View check run for this annotation

Codecov / codecov/patch

internal/api/api.go#L466

Added line #L466 was not covered by tests
if err != nil {
a.writeError(ctx, http.StatusBadRequest, err)
return
Expand Down Expand Up @@ -504,7 +504,7 @@
return
}

err = newConf.Check()
err = newConf.Validate()

Check warning on line 507 in internal/api/api.go

View check run for this annotation

Codecov / codecov/patch

internal/api/api.go#L507

Added line #L507 was not covered by tests
if err != nil {
a.writeError(ctx, http.StatusBadRequest, err)
return
Expand Down Expand Up @@ -538,7 +538,7 @@
return
}

err = newConf.Check()
err = newConf.Validate()

Check warning on line 541 in internal/api/api.go

View check run for this annotation

Codecov / codecov/patch

internal/api/api.go#L541

Added line #L541 was not covered by tests
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
Loading