Browse Source

api: add /v1/hlsmuxers/list endpoint

pull/707/head
aler9 4 years ago
parent
commit
79e73c5cb4
  1. 31
      apidocs/openapi.yaml
  2. 76
      internal/core/api.go
  3. 88
      internal/core/api_test.go
  4. 4
      internal/core/core.go
  5. 2
      internal/core/core_test.go
  6. 26
      internal/core/hls_muxer.go
  7. 43
      internal/core/hls_server.go
  8. 4
      internal/core/metrics.go
  9. 10
      internal/core/path.go
  10. 24
      internal/core/path_manager.go

31
apidocs/openapi.yaml

@ -307,6 +307,12 @@ components:
type: string type: string
enum: [idle, read, publish] enum: [idle, read, publish]
HLSMuxer:
type: object
properties:
lastRequest:
type: string
PathsList: PathsList:
type: object type: object
properties: properties:
@ -339,6 +345,14 @@ components:
additionalProperties: additionalProperties:
$ref: '#/components/schemas/RTMPConn' $ref: '#/components/schemas/RTMPConn'
HLSMuxersList:
type: object
properties:
items:
type: object
additionalProperties:
$ref: '#/components/schemas/HLSMuxer'
paths: paths:
/v1/config/get: /v1/config/get:
get: get:
@ -573,3 +587,20 @@ paths:
description: invalid request. description: invalid request.
'500': '500':
description: internal server error. description: internal server error.
/v1/hlsmuxers/list:
get:
operationId: hlsMuxersList
summary: returns all active 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.

76
internal/core/api.go

@ -165,7 +165,7 @@ func loadConfPathData(ctx *gin.Context) (interface{}, error) {
return in, err return in, err
} }
type apiPathsItem struct { type apiPathsListItem struct {
ConfName string `json:"confName"` ConfName string `json:"confName"`
Conf *conf.PathConf `json:"conf"` Conf *conf.PathConf `json:"conf"`
Source interface{} `json:"source"` Source interface{} `json:"source"`
@ -174,20 +174,20 @@ type apiPathsItem struct {
} }
type apiPathsListData struct { type apiPathsListData struct {
Items map[string]apiPathsItem `json:"items"` Items map[string]apiPathsListItem `json:"items"`
} }
type apiPathsListRes1 struct { type apiPathsListRes struct {
Data *apiPathsListData Data *apiPathsListData
Paths map[string]*path Paths map[string]*path
Err error Err error
} }
type apiPathsListReq1 struct { type apiPathsListReq struct {
Res chan apiPathsListRes1 Res chan apiPathsListRes
} }
type apiPathsListReq2 struct { type apiPathsListSubReq struct {
Data *apiPathsListData Data *apiPathsListData
Res chan struct{} Res chan struct{}
} }
@ -243,8 +243,31 @@ type apiRTMPConnsKickReq struct {
Res chan apiRTMPConnsKickRes Res chan apiRTMPConnsKickRes
} }
type apiHLSMuxersListItem struct {
LastRequest string `json:"lastRequest"`
}
type apiHLSMuxersListData struct {
Items map[string]apiHLSMuxersListItem `json:"items"`
}
type apiHLSMuxersListRes struct {
Data *apiHLSMuxersListData
Muxers map[string]*hlsMuxer
Err error
}
type apiHLSMuxersListReq struct {
Res chan apiHLSMuxersListRes
}
type apiHLSMuxersListSubReq struct {
Data *apiHLSMuxersListData
Res chan struct{}
}
type apiPathManager interface { type apiPathManager interface {
onAPIPathsList(req apiPathsListReq1) apiPathsListRes1 onAPIPathsList(req apiPathsListReq) apiPathsListRes
} }
type apiRTSPServer interface { type apiRTSPServer interface {
@ -257,6 +280,10 @@ type apiRTMPServer interface {
onAPIRTMPConnsKick(req apiRTMPConnsKickReq) apiRTMPConnsKickRes onAPIRTMPConnsKick(req apiRTMPConnsKickReq) apiRTMPConnsKickRes
} }
type apiHLSServer interface {
onAPIHLSMuxersList(req apiHLSMuxersListReq) apiHLSMuxersListRes
}
type apiParent interface { type apiParent interface {
Log(logger.Level, string, ...interface{}) Log(logger.Level, string, ...interface{})
onAPIConfigSet(conf *conf.Conf) onAPIConfigSet(conf *conf.Conf)
@ -268,6 +295,7 @@ type api struct {
rtspServer apiRTSPServer rtspServer apiRTSPServer
rtspsServer apiRTSPServer rtspsServer apiRTSPServer
rtmpServer apiRTMPServer rtmpServer apiRTMPServer
hlsServer apiHLSServer
parent apiParent parent apiParent
mutex sync.Mutex mutex sync.Mutex
@ -281,6 +309,7 @@ func newAPI(
rtspServer apiRTSPServer, rtspServer apiRTSPServer,
rtspsServer apiRTSPServer, rtspsServer apiRTSPServer,
rtmpServer apiRTMPServer, rtmpServer apiRTMPServer,
hlsServer apiHLSServer,
parent apiParent, parent apiParent,
) (*api, error) { ) (*api, error) {
ln, err := net.Listen("tcp", address) ln, err := net.Listen("tcp", address)
@ -294,6 +323,7 @@ func newAPI(
rtspServer: rtspServer, rtspServer: rtspServer,
rtspsServer: rtspsServer, rtspsServer: rtspsServer,
rtmpServer: rtmpServer, rtmpServer: rtmpServer,
hlsServer: hlsServer,
parent: parent, parent: parent,
} }
@ -312,6 +342,7 @@ func newAPI(
group.POST("/v1/rtspssessions/kick/:id", a.onRTSPSSessionsKick) group.POST("/v1/rtspssessions/kick/:id", a.onRTSPSSessionsKick)
group.GET("/v1/rtmpconns/list", a.onRTMPConnsList) group.GET("/v1/rtmpconns/list", a.onRTMPConnsList)
group.POST("/v1/rtmpconns/kick/:id", a.onRTMPConnsKick) group.POST("/v1/rtmpconns/kick/:id", a.onRTMPConnsKick)
group.GET("/v1/hlsmuxers/list", a.onHLSMuxersList)
a.s = &http.Server{Handler: router} a.s = &http.Server{Handler: router}
@ -510,7 +541,7 @@ func (a *api) onConfigPathsDelete(ctx *gin.Context) {
} }
func (a *api) onPathsList(ctx *gin.Context) { func (a *api) onPathsList(ctx *gin.Context) {
res := a.pathManager.onAPIPathsList(apiPathsListReq1{}) res := a.pathManager.onAPIPathsList(apiPathsListReq{})
if res.Err != nil { if res.Err != nil {
ctx.AbortWithStatus(http.StatusInternalServerError) ctx.AbortWithStatus(http.StatusInternalServerError)
return return
@ -598,13 +629,6 @@ func (a *api) onRTMPConnsList(ctx *gin.Context) {
ctx.JSON(http.StatusOK, res.Data) ctx.JSON(http.StatusOK, res.Data)
} }
// onConfReload is called by core.
func (a *api) onConfReload(conf *conf.Conf) {
a.mutex.Lock()
defer a.mutex.Unlock()
a.conf = conf
}
func (a *api) onRTMPConnsKick(ctx *gin.Context) { func (a *api) onRTMPConnsKick(ctx *gin.Context) {
if interfaceIsEmpty(a.rtmpServer) { if interfaceIsEmpty(a.rtmpServer) {
ctx.AbortWithStatus(http.StatusNotFound) ctx.AbortWithStatus(http.StatusNotFound)
@ -621,3 +645,25 @@ func (a *api) onRTMPConnsKick(ctx *gin.Context) {
ctx.Status(http.StatusOK) ctx.Status(http.StatusOK)
} }
func (a *api) onHLSMuxersList(ctx *gin.Context) {
if interfaceIsEmpty(a.hlsServer) {
ctx.AbortWithStatus(http.StatusNotFound)
return
}
res := a.hlsServer.onAPIHLSMuxersList(apiHLSMuxersListReq{})
if res.Err != nil {
ctx.AbortWithStatus(http.StatusInternalServerError)
return
}
ctx.JSON(http.StatusOK, res.Data)
}
// onConfReload is called by core.
func (a *api) onConfReload(conf *conf.Conf) {
a.mutex.Lock()
defer a.mutex.Unlock()
a.conf = conf
}

88
internal/core/api_test.go

@ -224,12 +224,20 @@ func TestAPIList(t *testing.T) {
"rtsp", "rtsp",
"rtsps", "rtsps",
"rtmp", "rtmp",
"hls",
} { } {
t.Run(ca, func(t *testing.T) { t.Run(ca, func(t *testing.T) {
p, ok := newInstance("api: yes\n" + conf := "api: yes\n"
"encryption: optional\n" +
"serverCert: " + serverCertFpath + "\n" + if ca == "rtsps" {
"serverKey: " + serverKeyFpath + "\n") conf += "protocols: [tcp]\n"
conf += "encryption: strict\n"
}
conf += "serverCert: " + serverCertFpath + "\n"
conf += "serverKey: " + serverKeyFpath + "\n"
p, ok := newInstance(conf)
require.Equal(t, true, ok) require.Equal(t, true, ok)
defer p.close() defer p.close()
@ -261,34 +269,66 @@ func TestAPIList(t *testing.T) {
}) })
require.NoError(t, err) require.NoError(t, err)
defer cnt1.close() defer cnt1.close()
case "hls":
source, err := gortsplib.DialPublish("rtsp://localhost:8554/mypath",
gortsplib.Tracks{track})
require.NoError(t, err)
defer source.Close()
func() {
res, err := http.Get("http://localhost:8888/mypath/index.m3u8")
require.NoError(t, err)
defer res.Body.Close()
require.Equal(t, 200, res.StatusCode)
}()
} }
var pa string
switch ca { switch ca {
case "rtsp": case "rtsp", "rtsps", "rtmp":
pa = "rtspsessions" var pa string
switch ca {
case "rtsp":
pa = "rtspsessions"
case "rtsps":
pa = "rtspssessions"
case "rtmp":
pa = "rtmpconns"
}
var out struct {
Items map[string]struct {
State string `json:"state"`
} `json:"items"`
}
err = httpRequest(http.MethodGet, "http://localhost:9997/v1/"+pa+"/list", nil, &out)
require.NoError(t, err)
case "rtsps": var firstID string
pa = "rtspssessions" for k := range out.Items {
firstID = k
}
case "rtmp": require.Equal(t, "publish", out.Items[firstID].State)
pa = "rtmpconns"
}
var out struct { case "hls":
Items map[string]struct { var out struct {
State string `json:"state"` Items map[string]struct {
} `json:"items"` LastRequest string `json:"lastRequest"`
} } `json:"items"`
err = httpRequest(http.MethodGet, "http://localhost:9997/v1/"+pa+"/list", nil, &out) }
require.NoError(t, err) err = httpRequest(http.MethodGet, "http://localhost:9997/v1/hlsmuxers/list", nil, &out)
require.NoError(t, err)
var firstID string var firstID string
for k := range out.Items { for k := range out.Items {
firstID = k firstID = k
} }
require.Equal(t, "publish", out.Items[firstID].State) require.NotEqual(t, "", out.Items[firstID].LastRequest)
}
}) })
} }
} }

4
internal/core/core.go

@ -344,6 +344,7 @@ func (p *Core) createResources(initial bool) error {
p.rtspServer, p.rtspServer,
p.rtspsServer, p.rtspsServer,
p.rtmpServer, p.rtmpServer,
p.hlsServer,
p) p)
if err != nil { if err != nil {
return err return err
@ -468,7 +469,8 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
closePathManager || closePathManager ||
closeRTSPServer || closeRTSPServer ||
closeRTSPSServer || closeRTSPSServer ||
closeRTMPServer { closeRTMPServer ||
closeHLSServer {
closeAPI = true closeAPI = true
} }

2
internal/core/core_test.go

@ -188,7 +188,7 @@ func TestCorePathAutoDeletion(t *testing.T) {
} }
}() }()
res := p.pathManager.onAPIPathsList(apiPathsListReq1{}) res := p.pathManager.onAPIPathsList(apiPathsListReq{})
require.NoError(t, res.Err) require.NoError(t, res.Err)
require.Equal(t, 0, len(res.Data.Items)) require.Equal(t, 0, len(res.Data.Items))

26
internal/core/hls_muxer.go

@ -122,6 +122,7 @@ type hlsMuxerParent interface {
} }
type hlsMuxer struct { type hlsMuxer struct {
name string
hlsAlwaysRemux bool hlsAlwaysRemux bool
hlsSegmentCount int hlsSegmentCount int
hlsSegmentDuration conf.StringDuration hlsSegmentDuration conf.StringDuration
@ -140,11 +141,13 @@ type hlsMuxer struct {
requests []hlsMuxerRequest requests []hlsMuxerRequest
// in // in
request chan hlsMuxerRequest request chan hlsMuxerRequest
apiHLSMuxersList chan apiHLSMuxersListSubReq
} }
func newHLSMuxer( func newHLSMuxer(
parentCtx context.Context, parentCtx context.Context,
name string,
hlsAlwaysRemux bool, hlsAlwaysRemux bool,
hlsSegmentCount int, hlsSegmentCount int,
hlsSegmentDuration conf.StringDuration, hlsSegmentDuration conf.StringDuration,
@ -156,6 +159,7 @@ func newHLSMuxer(
ctx, ctxCancel := context.WithCancel(parentCtx) ctx, ctxCancel := context.WithCancel(parentCtx)
m := &hlsMuxer{ m := &hlsMuxer{
name: name,
hlsAlwaysRemux: hlsAlwaysRemux, hlsAlwaysRemux: hlsAlwaysRemux,
hlsSegmentCount: hlsSegmentCount, hlsSegmentCount: hlsSegmentCount,
hlsSegmentDuration: hlsSegmentDuration, hlsSegmentDuration: hlsSegmentDuration,
@ -170,7 +174,8 @@ func newHLSMuxer(
v := time.Now().Unix() v := time.Now().Unix()
return &v return &v
}(), }(),
request: make(chan hlsMuxerRequest), request: make(chan hlsMuxerRequest),
apiHLSMuxersList: make(chan apiHLSMuxersListSubReq),
} }
m.log(logger.Info, "opened") m.log(logger.Info, "opened")
@ -221,6 +226,12 @@ func (m *hlsMuxer) run() {
m.requests = append(m.requests, req) m.requests = append(m.requests, req)
} }
case req := <-m.apiHLSMuxersList:
req.Data.Items[m.name] = apiHLSMuxersListItem{
LastRequest: time.Unix(atomic.LoadInt64(m.lastRequestTime), 0).String(),
}
close(req.Res)
case <-innerReady: case <-innerReady:
isReady = true isReady = true
for _, req := range m.requests { for _, req := range m.requests {
@ -499,3 +510,14 @@ func (m *hlsMuxer) onReaderAPIDescribe() interface{} {
Type string `json:"type"` Type string `json:"type"`
}{"hlsMuxer"} }{"hlsMuxer"}
} }
// onAPIHLSMuxersList is called by api.
func (m *hlsMuxer) onAPIHLSMuxersList(req apiHLSMuxersListSubReq) {
req.Res = make(chan struct{})
select {
case m.apiHLSMuxersList <- req:
<-req.Res
case <-m.ctx.Done():
}
}

43
internal/core/hls_server.go

@ -2,6 +2,7 @@ package core
import ( import (
"context" "context"
"fmt"
"io" "io"
"net" "net"
"net/http" "net/http"
@ -36,9 +37,10 @@ type hlsServer struct {
muxers map[string]*hlsMuxer muxers map[string]*hlsMuxer
// in // in
pathSourceReady chan *path pathSourceReady chan *path
request chan hlsMuxerRequest request chan hlsMuxerRequest
muxerClose chan *hlsMuxer muxerClose chan *hlsMuxer
apiHLSMuxersList chan apiHLSMuxersListReq
} }
func newHLSServer( func newHLSServer(
@ -74,6 +76,7 @@ func newHLSServer(
pathSourceReady: make(chan *path), pathSourceReady: make(chan *path),
request: make(chan hlsMuxerRequest), request: make(chan hlsMuxerRequest),
muxerClose: make(chan *hlsMuxer), muxerClose: make(chan *hlsMuxer),
apiHLSMuxersList: make(chan apiHLSMuxersListReq),
} }
s.log(logger.Info, "listener opened on "+address) s.log(logger.Info, "listener opened on "+address)
@ -124,6 +127,17 @@ outer:
} }
delete(s.muxers, c.PathName()) delete(s.muxers, c.PathName())
case req := <-s.apiHLSMuxersList:
muxers := make(map[string]*hlsMuxer)
for name, m := range s.muxers {
muxers[name] = m
}
req.Res <- apiHLSMuxersListRes{
Muxers: muxers,
}
case <-s.ctx.Done(): case <-s.ctx.Done():
break outer break outer
} }
@ -219,6 +233,7 @@ func (s *hlsServer) findOrCreateMuxer(pathName string) *hlsMuxer {
if !ok { if !ok {
r = newHLSMuxer( r = newHLSMuxer(
s.ctx, s.ctx,
pathName,
s.hlsAlwaysRemux, s.hlsAlwaysRemux,
s.hlsSegmentCount, s.hlsSegmentCount,
s.hlsSegmentDuration, s.hlsSegmentDuration,
@ -247,3 +262,25 @@ func (s *hlsServer) onPathSourceReady(pa *path) {
case <-s.ctx.Done(): case <-s.ctx.Done():
} }
} }
// onAPIHLSMuxersList is called by api.
func (s *hlsServer) onAPIHLSMuxersList(req apiHLSMuxersListReq) apiHLSMuxersListRes {
req.Res = make(chan apiHLSMuxersListRes)
select {
case s.apiHLSMuxersList <- req:
res := <-req.Res
res.Data = &apiHLSMuxersListData{
Items: make(map[string]apiHLSMuxersListItem),
}
for _, pa := range res.Muxers {
pa.onAPIHLSMuxersList(apiHLSMuxersListSubReq{Data: res.Data})
}
return res
case <-s.ctx.Done():
return apiHLSMuxersListRes{Err: fmt.Errorf("terminated")}
}
}

4
internal/core/metrics.go

@ -18,7 +18,7 @@ func formatMetric(key string, value int64) string {
} }
type metricsPathManager interface { type metricsPathManager interface {
onAPIPathsList(req apiPathsListReq1) apiPathsListRes1 onAPIPathsList(req apiPathsListReq) apiPathsListRes
} }
type metricsRTSPServer interface { type metricsRTSPServer interface {
@ -83,7 +83,7 @@ func (m *metrics) run() {
func (m *metrics) onMetrics(ctx *gin.Context) { func (m *metrics) onMetrics(ctx *gin.Context) {
out := "" out := ""
res := m.pathManager.onAPIPathsList(apiPathsListReq1{}) res := m.pathManager.onAPIPathsList(apiPathsListReq{})
if res.Err == nil { if res.Err == nil {
for name, p := range res.Data.Items { for name, p := range res.Data.Items {
if p.SourceReady { if p.SourceReady {

10
internal/core/path.go

@ -218,7 +218,7 @@ type path struct {
readerSetupPlay chan pathReaderSetupPlayReq readerSetupPlay chan pathReaderSetupPlayReq
readerPlay chan pathReaderPlayReq readerPlay chan pathReaderPlayReq
readerPause chan pathReaderPauseReq readerPause chan pathReaderPauseReq
apiPathsList chan apiPathsListReq2 apiPathsList chan apiPathsListSubReq
} }
func newPath( func newPath(
@ -262,7 +262,7 @@ func newPath(
readerSetupPlay: make(chan pathReaderSetupPlayReq), readerSetupPlay: make(chan pathReaderSetupPlayReq),
readerPlay: make(chan pathReaderPlayReq), readerPlay: make(chan pathReaderPlayReq),
readerPause: make(chan pathReaderPauseReq), readerPause: make(chan pathReaderPauseReq),
apiPathsList: make(chan apiPathsListReq2), apiPathsList: make(chan apiPathsListSubReq),
} }
pa.log(logger.Info, "opened") pa.log(logger.Info, "opened")
@ -832,8 +832,8 @@ func (pa *path) handleReaderPause(req pathReaderPauseReq) {
close(req.Res) close(req.Res)
} }
func (pa *path) handleAPIPathsList(req apiPathsListReq2) { func (pa *path) handleAPIPathsList(req apiPathsListSubReq) {
req.Data.Items[pa.name] = apiPathsItem{ req.Data.Items[pa.name] = apiPathsListItem{
ConfName: pa.confName, ConfName: pa.confName,
Conf: pa.conf, Conf: pa.conf,
Source: func() interface{} { Source: func() interface{} {
@ -967,7 +967,7 @@ func (pa *path) onReaderPause(req pathReaderPauseReq) {
} }
// onAPIPathsList is called by api. // onAPIPathsList is called by api.
func (pa *path) onAPIPathsList(req apiPathsListReq2) { func (pa *path) onAPIPathsList(req apiPathsListSubReq) {
req.Res = make(chan struct{}) req.Res = make(chan struct{})
select { select {
case pa.apiPathsList <- req: case pa.apiPathsList <- req:

24
internal/core/path_manager.go

@ -44,7 +44,7 @@ type pathManager struct {
readerSetupPlay chan pathReaderSetupPlayReq readerSetupPlay chan pathReaderSetupPlayReq
publisherAnnounce chan pathPublisherAnnounceReq publisherAnnounce chan pathPublisherAnnounceReq
hlsServerSet chan pathManagerHLSServer hlsServerSet chan pathManagerHLSServer
apiPathsList chan apiPathsListReq1 apiPathsList chan apiPathsListReq
} }
func newPathManager( func newPathManager(
@ -78,7 +78,7 @@ func newPathManager(
readerSetupPlay: make(chan pathReaderSetupPlayReq), readerSetupPlay: make(chan pathReaderSetupPlayReq),
publisherAnnounce: make(chan pathPublisherAnnounceReq), publisherAnnounce: make(chan pathPublisherAnnounceReq),
hlsServerSet: make(chan pathManagerHLSServer), hlsServerSet: make(chan pathManagerHLSServer),
apiPathsList: make(chan apiPathsListReq1), apiPathsList: make(chan apiPathsListReq),
} }
for pathName, pathConf := range pm.pathConfs { for pathName, pathConf := range pm.pathConfs {
@ -254,7 +254,7 @@ outer:
paths[name] = pa paths[name] = pa
} }
req.Res <- apiPathsListRes1{ req.Res <- apiPathsListRes{
Paths: paths, Paths: paths,
} }
@ -421,23 +421,23 @@ func (pm *pathManager) onHLSServerSet(s pathManagerHLSServer) {
} }
// onAPIPathsList is called by api. // onAPIPathsList is called by api.
func (pm *pathManager) onAPIPathsList(req apiPathsListReq1) apiPathsListRes1 { func (pm *pathManager) onAPIPathsList(req apiPathsListReq) apiPathsListRes {
req.Res = make(chan apiPathsListRes1) req.Res = make(chan apiPathsListRes)
select { select {
case pm.apiPathsList <- req: case pm.apiPathsList <- req:
res1 := <-req.Res res := <-req.Res
res1.Data = &apiPathsListData{ res.Data = &apiPathsListData{
Items: make(map[string]apiPathsItem), Items: make(map[string]apiPathsListItem),
} }
for _, pa := range res1.Paths { for _, pa := range res.Paths {
pa.onAPIPathsList(apiPathsListReq2{Data: res1.Data}) pa.onAPIPathsList(apiPathsListSubReq{Data: res.Data})
} }
return res1 return res
case <-pm.ctx.Done(): case <-pm.ctx.Done():
return apiPathsListRes1{Err: fmt.Errorf("terminated")} return apiPathsListRes{Err: fmt.Errorf("terminated")}
} }
} }

Loading…
Cancel
Save