Skip to content

Commit 066a903

Browse files
committed
api: add /info endpoint (#945)
1 parent cd80814 commit 066a903

File tree

10 files changed

+113
-33
lines changed

10 files changed

+113
-33
lines changed

api/openapi.yaml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ components:
2121
error:
2222
type: string
2323

24+
Info:
25+
type: object
26+
properties:
27+
version:
28+
type: string
29+
started:
30+
type: string
31+
2432
AuthInternalUser:
2533
type: object
2634
properties:
@@ -1134,6 +1142,25 @@ components:
11341142

11351143
paths:
11361144

1145+
/v3/info:
1146+
get:
1147+
operationId: info
1148+
tags: [General]
1149+
summary: returns informations about the instance.
1150+
responses:
1151+
'200':
1152+
description: the request was successful.
1153+
content:
1154+
application/json:
1155+
schema:
1156+
$ref: '#/components/schemas/Info'
1157+
'500':
1158+
description: server error.
1159+
content:
1160+
application/json:
1161+
schema:
1162+
$ref: '#/components/schemas/Error'
1163+
11371164
/v3/auth/jwks/refresh:
11381165
post:
11391166
operationId: authJwksRefresh

docs/2-usage/18-control-api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Control API
22

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

55
```yml
66
api: yes

internal/api/api.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ type apiParent interface {
8888

8989
// API is an API server.
9090
type API struct {
91+
Version string
92+
Started time.Time
9193
Address string
9294
Encryption bool
9395
ServerKey string
@@ -121,6 +123,8 @@ func (a *API) Initialize() error {
121123

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

126+
group.GET("/info", a.onInfo)
127+
124128
group.POST("/auth/jwks/refresh", a.onAuthJwksRefresh)
125129

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

545+
func (a *API) onInfo(ctx *gin.Context) {
546+
ctx.JSON(http.StatusOK, &defs.APIInfo{
547+
Version: a.Version,
548+
Started: a.Started,
549+
})
550+
}
551+
541552
func (a *API) onAuthJwksRefresh(ctx *gin.Context) {
542553
a.AuthManager.RefreshJWTJWKS()
543554
ctx.Status(http.StatusOK)

internal/api/api_test.go

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,34 @@ func TestPreflightRequest(t *testing.T) {
117117
require.Equal(t, byts, []byte{})
118118
}
119119

120+
func TestInfo(t *testing.T) {
121+
cnf := tempConf(t, "api: yes\n")
122+
123+
api := API{
124+
Version: "v1.2.3",
125+
Started: time.Date(2008, 11, 7, 11, 22, 0, 0, time.Local),
126+
Address: "localhost:9997",
127+
ReadTimeout: conf.Duration(10 * time.Second),
128+
Conf: cnf,
129+
AuthManager: test.NilAuthManager,
130+
Parent: &testParent{},
131+
}
132+
err := api.Initialize()
133+
require.NoError(t, err)
134+
defer api.Close()
135+
136+
tr := &http.Transport{}
137+
defer tr.CloseIdleConnections()
138+
hc := &http.Client{Transport: tr}
139+
140+
var out map[string]interface{}
141+
httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/info", nil, &out)
142+
require.Equal(t, map[string]interface{}{
143+
"started": "2008-11-07T11:22:00+01:00",
144+
"version": "v1.2.3",
145+
}, out)
146+
}
147+
120148
func TestConfigGlobalGet(t *testing.T) {
121149
cnf := tempConf(t, "api: yes\n")
122150
checked := false
@@ -621,18 +649,18 @@ func TestRecordingsList(t *testing.T) {
621649
"name": "mypath1",
622650
"segments": []interface{}{
623651
map[string]interface{}{
624-
"start": time.Date(2008, 11, 0o7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano),
652+
"start": time.Date(2008, 11, 7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano),
625653
},
626654
map[string]interface{}{
627-
"start": time.Date(2009, 11, 0o7, 11, 22, 0, 900000000, time.Local).Format(time.RFC3339Nano),
655+
"start": time.Date(2009, 11, 7, 11, 22, 0, 900000000, time.Local).Format(time.RFC3339Nano),
628656
},
629657
},
630658
},
631659
map[string]interface{}{
632660
"name": "mypath2",
633661
"segments": []interface{}{
634662
map[string]interface{}{
635-
"start": time.Date(2009, 11, 0o7, 11, 22, 0, 900000000, time.Local).Format(time.RFC3339Nano),
663+
"start": time.Date(2009, 11, 7, 11, 22, 0, 900000000, time.Local).Format(time.RFC3339Nano),
636664
},
637665
},
638666
},
@@ -680,10 +708,10 @@ func TestRecordingsGet(t *testing.T) {
680708
"name": "mypath1",
681709
"segments": []interface{}{
682710
map[string]interface{}{
683-
"start": time.Date(2008, 11, 0o7, 11, 22, 0, 0, time.Local).Format(time.RFC3339Nano),
711+
"start": time.Date(2008, 11, 7, 11, 22, 0, 0, time.Local).Format(time.RFC3339Nano),
684712
},
685713
map[string]interface{}{
686-
"start": time.Date(2009, 11, 0o7, 11, 22, 0, 900000000, time.Local).Format(time.RFC3339Nano),
714+
"start": time.Date(2009, 11, 7, 11, 22, 0, 900000000, time.Local).Format(time.RFC3339Nano),
687715
},
688716
},
689717
}, out)
@@ -725,7 +753,7 @@ func TestRecordingsDeleteSegment(t *testing.T) {
725753

726754
v := url.Values{}
727755
v.Set("path", "mypath1")
728-
v.Set("start", time.Date(2008, 11, 0o7, 11, 22, 0, 900000000, time.Local).Format(time.RFC3339Nano))
756+
v.Set("start", time.Date(2008, 11, 7, 11, 22, 0, 900000000, time.Local).Format(time.RFC3339Nano))
729757
u.RawQuery = v.Encode()
730758

731759
req, err := http.NewRequest(http.MethodDelete, u.String(), nil)

internal/core/core.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ import (
4141
//go:embed VERSION
4242
var version []byte
4343

44+
var started = time.Now()
45+
4446
var defaultConfPaths = []string{
4547
"rtsp-simple-server.yml",
4648
"mediamtx.yml",
@@ -615,6 +617,8 @@ func (p *Core) createResources(initial bool) error {
615617
if p.conf.API &&
616618
p.api == nil {
617619
i := &api.API{
620+
Version: string(version),
621+
Started: started,
618622
Address: p.conf.APIAddress,
619623
Encryption: p.conf.APIEncryption,
620624
ServerKey: p.conf.APIServerKey,

internal/defs/api.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ type APIError struct {
5555
Error string `json:"error"`
5656
}
5757

58+
// APIInfo is a info response.
59+
type APIInfo struct {
60+
Version string `json:"version"`
61+
Started time.Time `json:"started"`
62+
}
63+
5864
// APIPathConfList is a list of path configurations.
5965
type APIPathConfList struct {
6066
ItemCount int `json:"itemCount"`

internal/playback/on_get_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ func TestOnGet(t *testing.T) {
264264

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

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

567567
v := url.Values{}
568568
v.Set("path", "mypath")
569-
v.Set("start", time.Date(2008, 11, 0o7, 11, 23, 1, 500000000, time.Local).Format(time.RFC3339Nano))
569+
v.Set("start", time.Date(2008, 11, 7, 11, 23, 1, 500000000, time.Local).Format(time.RFC3339Nano))
570570
v.Set("duration", "3")
571571
v.Set("format", "fmp4")
572572
u.RawQuery = v.Encode()

internal/playback/on_list_test.go

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -91,18 +91,18 @@ func TestOnList(t *testing.T) {
9191

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

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

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

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

108108
u.RawQuery = v.Encode()
@@ -130,57 +130,57 @@ func TestOnList(t *testing.T) {
130130
require.Equal(t, []interface{}{
131131
map[string]interface{}{
132132
"duration": float64(66),
133-
"start": time.Date(2008, 11, 0o7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano),
133+
"start": time.Date(2008, 11, 7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano),
134134
"url": "http://localhost:9996/get?duration=66&path=mypath&start=" +
135-
url.QueryEscape(time.Date(2008, 11, 0o7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano)),
135+
url.QueryEscape(time.Date(2008, 11, 7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano)),
136136
},
137137
map[string]interface{}{
138138
"duration": float64(4),
139-
"start": time.Date(2009, 11, 0o7, 11, 23, 2, 500000000, time.Local).Format(time.RFC3339Nano),
139+
"start": time.Date(2009, 11, 7, 11, 23, 2, 500000000, time.Local).Format(time.RFC3339Nano),
140140
"url": "http://localhost:9996/get?duration=4&path=mypath&start=" +
141-
url.QueryEscape(time.Date(2009, 11, 0o7, 11, 23, 2, 500000000, time.Local).Format(time.RFC3339Nano)),
141+
url.QueryEscape(time.Date(2009, 11, 7, 11, 23, 2, 500000000, time.Local).Format(time.RFC3339Nano)),
142142
},
143143
}, out)
144144

145145
case "filtered":
146146
require.Equal(t, []interface{}{
147147
map[string]interface{}{
148148
"duration": float64(65),
149-
"start": time.Date(2008, 11, 0o7, 11, 22, 1, 500000000, time.Local).Format(time.RFC3339Nano),
149+
"start": time.Date(2008, 11, 7, 11, 22, 1, 500000000, time.Local).Format(time.RFC3339Nano),
150150
"url": "http://localhost:9996/get?duration=65&path=mypath&start=" +
151-
url.QueryEscape(time.Date(2008, 11, 0o7, 11, 22, 1, 500000000, time.Local).Format(time.RFC3339Nano)),
151+
url.QueryEscape(time.Date(2008, 11, 7, 11, 22, 1, 500000000, time.Local).Format(time.RFC3339Nano)),
152152
},
153153
map[string]interface{}{
154154
"duration": float64(2),
155-
"start": time.Date(2009, 11, 0o7, 11, 23, 2, 500000000, time.Local).Format(time.RFC3339Nano),
155+
"start": time.Date(2009, 11, 7, 11, 23, 2, 500000000, time.Local).Format(time.RFC3339Nano),
156156
"url": "http://localhost:9996/get?duration=2&path=mypath&start=" +
157-
url.QueryEscape(time.Date(2009, 11, 0o7, 11, 23, 2, 500000000, time.Local).Format(time.RFC3339Nano)),
157+
url.QueryEscape(time.Date(2009, 11, 7, 11, 23, 2, 500000000, time.Local).Format(time.RFC3339Nano)),
158158
},
159159
}, out)
160160

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

171171
case "different init":
172172
require.Equal(t, []interface{}{
173173
map[string]interface{}{
174174
"duration": float64(62),
175-
"start": time.Date(2008, 11, 0o7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano),
175+
"start": time.Date(2008, 11, 7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano),
176176
"url": "http://localhost:9996/get?duration=62&path=mypath&start=" +
177-
url.QueryEscape(time.Date(2008, 11, 0o7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano)),
177+
url.QueryEscape(time.Date(2008, 11, 7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano)),
178178
},
179179
map[string]interface{}{
180180
"duration": float64(1),
181-
"start": time.Date(2008, 11, 0o7, 11, 23, 2, 500000000, time.Local).Format(time.RFC3339Nano),
181+
"start": time.Date(2008, 11, 7, 11, 23, 2, 500000000, time.Local).Format(time.RFC3339Nano),
182182
"url": "http://localhost:9996/get?duration=1&path=mypath&start=" +
183-
url.QueryEscape(time.Date(2008, 11, 0o7, 11, 23, 2, 500000000, time.Local).Format(time.RFC3339Nano)),
183+
url.QueryEscape(time.Date(2008, 11, 7, 11, 23, 2, 500000000, time.Local).Format(time.RFC3339Nano)),
184184
},
185185
}, out)
186186
}
@@ -327,9 +327,9 @@ func TestOnListCachedDuration(t *testing.T) {
327327
require.Equal(t, []interface{}{
328328
map[string]interface{}{
329329
"duration": float64(50),
330-
"start": time.Date(2008, 11, 0o7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano),
330+
"start": time.Date(2008, 11, 7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano),
331331
"url": "http://localhost:9996/get?duration=50&path=mypath&start=" +
332-
url.QueryEscape(time.Date(2008, 11, 0o7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano)),
332+
url.QueryEscape(time.Date(2008, 11, 7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano)),
333333
},
334334
}, out)
335335
}

internal/recordstore/path_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ var pathCases = []struct {
1717
"standard",
1818
"%path/%Y-%m-%d_%H-%M-%S-%f.mp4",
1919
Path{
20-
Start: time.Date(2008, 11, 0o7, 11, 22, 4, 123456000, time.Local),
20+
Start: time.Date(2008, 11, 7, 11, 22, 4, 123456000, time.Local),
2121
Path: "mypath",
2222
},
2323
"mypath/2008-11-07_11-22-04-123456.mp4",

internal/testapidocs/apidocs_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ func TestAPIDocs(t *testing.T) {
4444
openAPIKey string
4545
goStruct any
4646
}{
47+
{
48+
"Info",
49+
defs.APIInfo{},
50+
},
4751
{
4852
"AuthInternalUser",
4953
conf.AuthInternalUser{},

0 commit comments

Comments
 (0)