Skip to content
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
27 changes: 27 additions & 0 deletions api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ components:
error:
type: string

Info:
type: object
properties:
version:
type: string
started:
type: string

AuthInternalUser:
type: object
properties:
Expand Down Expand Up @@ -1134,6 +1142,25 @@ components:

paths:

/v3/info:
get:
operationId: info
tags: [General]
summary: returns informations about the instance.
responses:
'200':
description: the request was successful.
content:
application/json:
schema:
$ref: '#/components/schemas/Info'
'500':
description: server error.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'

/v3/auth/jwks/refresh:
post:
operationId: authJwksRefresh
Expand Down
2 changes: 1 addition & 1 deletion docs/2-usage/18-control-api.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Control API

The server can be queried and controlled with an API, that can be enabled by setting the `api` parameter in the configuration:
The server can be queried and controlled with an API, that can be enabled by toggling the `api` parameter in the configuration:

```yml
api: yes
Expand Down
11 changes: 11 additions & 0 deletions internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ type apiParent interface {

// API is an API server.
type API struct {
Version string
Started time.Time
Address string
Encryption bool
ServerKey string
Expand Down Expand Up @@ -121,6 +123,8 @@ func (a *API) Initialize() error {

group := router.Group("/v3")

group.GET("/info", a.onInfo)

group.POST("/auth/jwks/refresh", a.onAuthJwksRefresh)

group.GET("/config/global/get", a.onConfigGlobalGet)
Expand Down Expand Up @@ -538,6 +542,13 @@ func (a *API) onConfigPathsDelete(ctx *gin.Context) {
ctx.Status(http.StatusOK)
}

func (a *API) onInfo(ctx *gin.Context) {
ctx.JSON(http.StatusOK, &defs.APIInfo{
Version: a.Version,
Started: a.Started,
})
}

func (a *API) onAuthJwksRefresh(ctx *gin.Context) {
a.AuthManager.RefreshJWTJWKS()
ctx.Status(http.StatusOK)
Expand Down
40 changes: 34 additions & 6 deletions internal/api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,34 @@ func TestPreflightRequest(t *testing.T) {
require.Equal(t, byts, []byte{})
}

func TestInfo(t *testing.T) {
cnf := tempConf(t, "api: yes\n")

api := API{
Version: "v1.2.3",
Started: time.Date(2008, 11, 7, 11, 22, 0, 0, time.Local),
Address: "localhost:9997",
ReadTimeout: conf.Duration(10 * time.Second),
Conf: cnf,
AuthManager: test.NilAuthManager,
Parent: &testParent{},
}
err := api.Initialize()
require.NoError(t, err)
defer api.Close()

tr := &http.Transport{}
defer tr.CloseIdleConnections()
hc := &http.Client{Transport: tr}

var out map[string]interface{}
httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/info", nil, &out)
require.Equal(t, map[string]interface{}{
"started": time.Date(2008, 11, 7, 11, 22, 0, 0, time.Local).Format(time.RFC3339),
"version": "v1.2.3",
}, out)
}

func TestConfigGlobalGet(t *testing.T) {
cnf := tempConf(t, "api: yes\n")
checked := false
Expand Down Expand Up @@ -621,18 +649,18 @@ func TestRecordingsList(t *testing.T) {
"name": "mypath1",
"segments": []interface{}{
map[string]interface{}{
"start": time.Date(2008, 11, 0o7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano),
"start": time.Date(2008, 11, 7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano),
},
map[string]interface{}{
"start": time.Date(2009, 11, 0o7, 11, 22, 0, 900000000, time.Local).Format(time.RFC3339Nano),
"start": time.Date(2009, 11, 7, 11, 22, 0, 900000000, time.Local).Format(time.RFC3339Nano),
},
},
},
map[string]interface{}{
"name": "mypath2",
"segments": []interface{}{
map[string]interface{}{
"start": time.Date(2009, 11, 0o7, 11, 22, 0, 900000000, time.Local).Format(time.RFC3339Nano),
"start": time.Date(2009, 11, 7, 11, 22, 0, 900000000, time.Local).Format(time.RFC3339Nano),
},
},
},
Expand Down Expand Up @@ -680,10 +708,10 @@ func TestRecordingsGet(t *testing.T) {
"name": "mypath1",
"segments": []interface{}{
map[string]interface{}{
"start": time.Date(2008, 11, 0o7, 11, 22, 0, 0, time.Local).Format(time.RFC3339Nano),
"start": time.Date(2008, 11, 7, 11, 22, 0, 0, time.Local).Format(time.RFC3339Nano),
},
map[string]interface{}{
"start": time.Date(2009, 11, 0o7, 11, 22, 0, 900000000, time.Local).Format(time.RFC3339Nano),
"start": time.Date(2009, 11, 7, 11, 22, 0, 900000000, time.Local).Format(time.RFC3339Nano),
},
},
}, out)
Expand Down Expand Up @@ -725,7 +753,7 @@ func TestRecordingsDeleteSegment(t *testing.T) {

v := url.Values{}
v.Set("path", "mypath1")
v.Set("start", time.Date(2008, 11, 0o7, 11, 22, 0, 900000000, time.Local).Format(time.RFC3339Nano))
v.Set("start", time.Date(2008, 11, 7, 11, 22, 0, 900000000, time.Local).Format(time.RFC3339Nano))
u.RawQuery = v.Encode()

req, err := http.NewRequest(http.MethodDelete, u.String(), nil)
Expand Down
4 changes: 4 additions & 0 deletions internal/core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ import (
//go:embed VERSION
var version []byte

var started = time.Now()

var defaultConfPaths = []string{
"rtsp-simple-server.yml",
"mediamtx.yml",
Expand Down Expand Up @@ -615,6 +617,8 @@ func (p *Core) createResources(initial bool) error {
if p.conf.API &&
p.api == nil {
i := &api.API{
Version: string(version),
Started: started,
Address: p.conf.APIAddress,
Encryption: p.conf.APIEncryption,
ServerKey: p.conf.APIServerKey,
Expand Down
6 changes: 6 additions & 0 deletions internal/defs/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ type APIError struct {
Error string `json:"error"`
}

// APIInfo is a info response.
type APIInfo struct {
Version string `json:"version"`
Started time.Time `json:"started"`
}

// APIPathConfList is a list of path configurations.
type APIPathConfList struct {
ItemCount int `json:"itemCount"`
Expand Down
6 changes: 3 additions & 3 deletions internal/playback/on_get_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ func TestOnGet(t *testing.T) {

v := url.Values{}
v.Set("path", "mypath")
v.Set("start", time.Date(2008, 11, 0o7, 11, 23, 1, 500000000, time.Local).Format(time.RFC3339Nano))
v.Set("start", time.Date(2008, 11, 7, 11, 23, 1, 500000000, time.Local).Format(time.RFC3339Nano))
v.Set("duration", "3")
v.Set("format", format)
u.RawQuery = v.Encode()
Expand Down Expand Up @@ -488,7 +488,7 @@ func TestOnGetDifferentInit(t *testing.T) {

v := url.Values{}
v.Set("path", "mypath")
v.Set("start", time.Date(2008, 11, 0o7, 11, 23, 1, 500000000, time.Local).Format(time.RFC3339Nano))
v.Set("start", time.Date(2008, 11, 7, 11, 23, 1, 500000000, time.Local).Format(time.RFC3339Nano))
v.Set("duration", "2")
v.Set("format", "fmp4")
u.RawQuery = v.Encode()
Expand Down Expand Up @@ -566,7 +566,7 @@ func TestOnGetNTPCompensation(t *testing.T) {

v := url.Values{}
v.Set("path", "mypath")
v.Set("start", time.Date(2008, 11, 0o7, 11, 23, 1, 500000000, time.Local).Format(time.RFC3339Nano))
v.Set("start", time.Date(2008, 11, 7, 11, 23, 1, 500000000, time.Local).Format(time.RFC3339Nano))
v.Set("duration", "3")
v.Set("format", "fmp4")
u.RawQuery = v.Encode()
Expand Down
44 changes: 22 additions & 22 deletions internal/playback/on_list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,18 +91,18 @@ func TestOnList(t *testing.T) {

switch ca {
case "filtered":
v.Set("start", time.Date(2008, 11, 0o7, 11, 22, 1, 500000000, time.Local).Format(time.RFC3339Nano))
v.Set("end", time.Date(2009, 11, 0o7, 11, 23, 4, 500000000, time.Local).Format(time.RFC3339Nano))
v.Set("start", time.Date(2008, 11, 7, 11, 22, 1, 500000000, time.Local).Format(time.RFC3339Nano))
v.Set("end", time.Date(2009, 11, 7, 11, 23, 4, 500000000, time.Local).Format(time.RFC3339Nano))

case "filtered and gap":
v.Set("start", time.Date(2008, 11, 0o7, 11, 23, 20, 500000000, time.Local).Format(time.RFC3339Nano))
v.Set("end", time.Date(2009, 11, 0o7, 11, 23, 4, 500000000, time.Local).Format(time.RFC3339Nano))
v.Set("start", time.Date(2008, 11, 7, 11, 23, 20, 500000000, time.Local).Format(time.RFC3339Nano))
v.Set("end", time.Date(2009, 11, 7, 11, 23, 4, 500000000, time.Local).Format(time.RFC3339Nano))

case "start after duration":
v.Set("start", time.Date(2010, 11, 0o7, 11, 23, 20, 500000000, time.Local).Format(time.RFC3339Nano))
v.Set("start", time.Date(2010, 11, 7, 11, 23, 20, 500000000, time.Local).Format(time.RFC3339Nano))

case "start before first":
v.Set("start", time.Date(2007, 11, 0o7, 11, 23, 20, 500000000, time.Local).Format(time.RFC3339Nano))
v.Set("start", time.Date(2007, 11, 7, 11, 23, 20, 500000000, time.Local).Format(time.RFC3339Nano))
}

u.RawQuery = v.Encode()
Expand Down Expand Up @@ -130,57 +130,57 @@ func TestOnList(t *testing.T) {
require.Equal(t, []interface{}{
map[string]interface{}{
"duration": float64(66),
"start": time.Date(2008, 11, 0o7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano),
"start": time.Date(2008, 11, 7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano),
"url": "http://localhost:9996/get?duration=66&path=mypath&start=" +
url.QueryEscape(time.Date(2008, 11, 0o7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano)),
url.QueryEscape(time.Date(2008, 11, 7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano)),
},
map[string]interface{}{
"duration": float64(4),
"start": time.Date(2009, 11, 0o7, 11, 23, 2, 500000000, time.Local).Format(time.RFC3339Nano),
"start": time.Date(2009, 11, 7, 11, 23, 2, 500000000, time.Local).Format(time.RFC3339Nano),
"url": "http://localhost:9996/get?duration=4&path=mypath&start=" +
url.QueryEscape(time.Date(2009, 11, 0o7, 11, 23, 2, 500000000, time.Local).Format(time.RFC3339Nano)),
url.QueryEscape(time.Date(2009, 11, 7, 11, 23, 2, 500000000, time.Local).Format(time.RFC3339Nano)),
},
}, out)

case "filtered":
require.Equal(t, []interface{}{
map[string]interface{}{
"duration": float64(65),
"start": time.Date(2008, 11, 0o7, 11, 22, 1, 500000000, time.Local).Format(time.RFC3339Nano),
"start": time.Date(2008, 11, 7, 11, 22, 1, 500000000, time.Local).Format(time.RFC3339Nano),
"url": "http://localhost:9996/get?duration=65&path=mypath&start=" +
url.QueryEscape(time.Date(2008, 11, 0o7, 11, 22, 1, 500000000, time.Local).Format(time.RFC3339Nano)),
url.QueryEscape(time.Date(2008, 11, 7, 11, 22, 1, 500000000, time.Local).Format(time.RFC3339Nano)),
},
map[string]interface{}{
"duration": float64(2),
"start": time.Date(2009, 11, 0o7, 11, 23, 2, 500000000, time.Local).Format(time.RFC3339Nano),
"start": time.Date(2009, 11, 7, 11, 23, 2, 500000000, time.Local).Format(time.RFC3339Nano),
"url": "http://localhost:9996/get?duration=2&path=mypath&start=" +
url.QueryEscape(time.Date(2009, 11, 0o7, 11, 23, 2, 500000000, time.Local).Format(time.RFC3339Nano)),
url.QueryEscape(time.Date(2009, 11, 7, 11, 23, 2, 500000000, time.Local).Format(time.RFC3339Nano)),
},
}, out)

case "filtered and gap":
require.Equal(t, []interface{}{
map[string]interface{}{
"duration": float64(4),
"start": time.Date(2008, 11, 0o7, 11, 24, 2, 500000000, time.Local).Format(time.RFC3339Nano),
"start": time.Date(2008, 11, 7, 11, 24, 2, 500000000, time.Local).Format(time.RFC3339Nano),
"url": "http://localhost:9996/get?duration=4&path=mypath&start=" +
url.QueryEscape(time.Date(2008, 11, 0o7, 11, 24, 2, 500000000, time.Local).Format(time.RFC3339Nano)),
url.QueryEscape(time.Date(2008, 11, 7, 11, 24, 2, 500000000, time.Local).Format(time.RFC3339Nano)),
},
}, out)

case "different init":
require.Equal(t, []interface{}{
map[string]interface{}{
"duration": float64(62),
"start": time.Date(2008, 11, 0o7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano),
"start": time.Date(2008, 11, 7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano),
"url": "http://localhost:9996/get?duration=62&path=mypath&start=" +
url.QueryEscape(time.Date(2008, 11, 0o7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano)),
url.QueryEscape(time.Date(2008, 11, 7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano)),
},
map[string]interface{}{
"duration": float64(1),
"start": time.Date(2008, 11, 0o7, 11, 23, 2, 500000000, time.Local).Format(time.RFC3339Nano),
"start": time.Date(2008, 11, 7, 11, 23, 2, 500000000, time.Local).Format(time.RFC3339Nano),
"url": "http://localhost:9996/get?duration=1&path=mypath&start=" +
url.QueryEscape(time.Date(2008, 11, 0o7, 11, 23, 2, 500000000, time.Local).Format(time.RFC3339Nano)),
url.QueryEscape(time.Date(2008, 11, 7, 11, 23, 2, 500000000, time.Local).Format(time.RFC3339Nano)),
},
}, out)
}
Expand Down Expand Up @@ -327,9 +327,9 @@ func TestOnListCachedDuration(t *testing.T) {
require.Equal(t, []interface{}{
map[string]interface{}{
"duration": float64(50),
"start": time.Date(2008, 11, 0o7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano),
"start": time.Date(2008, 11, 7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano),
"url": "http://localhost:9996/get?duration=50&path=mypath&start=" +
url.QueryEscape(time.Date(2008, 11, 0o7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano)),
url.QueryEscape(time.Date(2008, 11, 7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano)),
},
}, out)
}
2 changes: 1 addition & 1 deletion internal/recordstore/path_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ var pathCases = []struct {
"standard",
"%path/%Y-%m-%d_%H-%M-%S-%f.mp4",
Path{
Start: time.Date(2008, 11, 0o7, 11, 22, 4, 123456000, time.Local),
Start: time.Date(2008, 11, 7, 11, 22, 4, 123456000, time.Local),
Path: "mypath",
},
"mypath/2008-11-07_11-22-04-123456.mp4",
Expand Down
4 changes: 4 additions & 0 deletions internal/testapidocs/apidocs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ func TestAPIDocs(t *testing.T) {
openAPIKey string
goStruct any
}{
{
"Info",
defs.APIInfo{},
},
{
"AuthInternalUser",
conf.AuthInternalUser{},
Expand Down