sessions are now tracked through cookies or query parameters. This provides the ability to inspect sessions through logs, metrics and API, allows more precise tracking of outbound bytes, decreases load on external HTTP authentication URLs since they are now called once per session and not once per request.
This commit is contained in:
+152
-2
@@ -45,8 +45,7 @@ components:
|
|||||||
PathReaderType:
|
PathReaderType:
|
||||||
type: string
|
type: string
|
||||||
enum:
|
enum:
|
||||||
- hlsMuxer
|
- hlsSession
|
||||||
- rpiCameraSecondary
|
|
||||||
- rtmpConn
|
- rtmpConn
|
||||||
- rtmpsConn
|
- rtmpsConn
|
||||||
- rtspConn
|
- rtspConn
|
||||||
@@ -1196,6 +1195,40 @@ components:
|
|||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/HLSMuxer'
|
$ref: '#/components/schemas/HLSMuxer'
|
||||||
|
|
||||||
|
HLSSession:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
created:
|
||||||
|
type: string
|
||||||
|
remoteAddr:
|
||||||
|
type: string
|
||||||
|
path:
|
||||||
|
type: string
|
||||||
|
query:
|
||||||
|
type: string
|
||||||
|
user:
|
||||||
|
type: string
|
||||||
|
outboundBytes:
|
||||||
|
type: integer
|
||||||
|
format: uint64
|
||||||
|
|
||||||
|
HLSSessionList:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
pageCount:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
itemCount:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
items:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/HLSSession'
|
||||||
|
|
||||||
Recording:
|
Recording:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -2291,6 +2324,123 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/Error'
|
$ref: '#/components/schemas/Error'
|
||||||
|
|
||||||
|
/v3/hlssessions/list:
|
||||||
|
get:
|
||||||
|
operationId: hlssessionsList
|
||||||
|
tags: [HLS]
|
||||||
|
summary: returns all HLS sessions.
|
||||||
|
description: ''
|
||||||
|
parameters:
|
||||||
|
- name: page
|
||||||
|
in: query
|
||||||
|
description: page number.
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
default: 0
|
||||||
|
- name: itemsPerPage
|
||||||
|
in: query
|
||||||
|
description: items per page.
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
default: 100
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: the request was successful.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/HLSSessionList'
|
||||||
|
'400':
|
||||||
|
description: invalid request.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Error'
|
||||||
|
'500':
|
||||||
|
description: server error.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Error'
|
||||||
|
|
||||||
|
/v3/hlssessions/get/{id}:
|
||||||
|
get:
|
||||||
|
operationId: hlssessionsGet
|
||||||
|
tags: [HLS]
|
||||||
|
summary: returns a HLS session.
|
||||||
|
description: ''
|
||||||
|
parameters:
|
||||||
|
- name: id
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
description: ID of the session.
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: the request was successful.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/HLSSession'
|
||||||
|
'400':
|
||||||
|
description: invalid request.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Error'
|
||||||
|
'404':
|
||||||
|
description: session not found.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Error'
|
||||||
|
'500':
|
||||||
|
description: server error.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Error'
|
||||||
|
|
||||||
|
/v3/hlssessions/kick/{id}:
|
||||||
|
post:
|
||||||
|
operationId: hlssessionsKick
|
||||||
|
tags: [HLS]
|
||||||
|
summary: kicks out a HLS session from the server.
|
||||||
|
description: ''
|
||||||
|
parameters:
|
||||||
|
- name: id
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
description: ID of the session.
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: the request was successful.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/OK'
|
||||||
|
'400':
|
||||||
|
description: invalid request.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Error'
|
||||||
|
'404':
|
||||||
|
description: session not found.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Error'
|
||||||
|
'500':
|
||||||
|
description: server error.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Error'
|
||||||
|
|
||||||
/v3/paths/list:
|
/v3/paths/list:
|
||||||
get:
|
get:
|
||||||
operationId: pathsList
|
operationId: pathsList
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ paths_inbound_bytes{name="[path_name]",state="[state]"} 1234
|
|||||||
paths_outbound_bytes{name="[path_name]",state="[state]"} 1234
|
paths_outbound_bytes{name="[path_name]",state="[state]"} 1234
|
||||||
paths_inbound_frames_in_error{name="[path_name]",state="[state]"} 1234
|
paths_inbound_frames_in_error{name="[path_name]",state="[state]"} 1234
|
||||||
|
|
||||||
|
# HLS sessions
|
||||||
|
hls_sessions{id="[id]",path="[path]",remoteAddr="[remoteAddr]"} 1
|
||||||
|
hls_sessions_outbound_bytes{id="[id]",path="[path]",remoteAddr="[remoteAddr]"} 187
|
||||||
|
|
||||||
# HLS muxers
|
# HLS muxers
|
||||||
hls_muxers{name="[name]"} 1
|
hls_muxers{name="[name]"} 1
|
||||||
hls_muxers_outbound_bytes{name="[name]"} 187
|
hls_muxers_outbound_bytes{name="[name]"} 187
|
||||||
@@ -151,9 +155,10 @@ Bitrates are not provided directly as metrics because they can be computed from
|
|||||||
|
|
||||||
Metrics can be filtered by using HTTP query parameters:
|
Metrics can be filtered by using HTTP query parameters:
|
||||||
|
|
||||||
- `type=[TYPE]`: show metrics of a certain type only. TYPE can be `paths`, `hls_muxers`, `rtsp_conns`, `rtsp_sessions`, `rtsps_conns`, `rtsps_sessions`, `rtmp_conns`, `rtmps_conns`, `srt_conns`, `webrtc_sessions`.
|
- `type=[TYPE]`: show metrics of a certain type only. TYPE can be `paths`, `hls_sessions`, `hls_muxers`, `rtsp_conns`, `rtsp_sessions`, `rtsps_conns`, `rtsps_sessions`, `rtmp_conns`, `rtmps_conns`, `srt_conns`, `webrtc_sessions`.
|
||||||
- `path=[PATH]`: show metrics belonging to a specific path only
|
- `path=[PATH]`: show metrics belonging to a specific path only
|
||||||
- `hls_muxer=[PATH]`: show metrics belonging to a specific HLS muxer only
|
- `hls_muxer=[PATH]`: show metrics belonging to a specific HLS muxer only
|
||||||
|
- `hls_session=[ID]`: show metrics belonging to a specific HLS session only
|
||||||
- `rtsp_conn=[ID]` show metrics belonging to a specific RTSP connection only
|
- `rtsp_conn=[ID]` show metrics belonging to a specific RTSP connection only
|
||||||
- `rtsp_session=[SESSION]`: show metrics belonging to a specific RTSP session only
|
- `rtsp_session=[SESSION]`: show metrics belonging to a specific RTSP session only
|
||||||
- `rtsps_conn=[ID]` show metrics belonging to a specific RTSPS connection only
|
- `rtsps_conn=[ID]` show metrics belonging to a specific RTSPS connection only
|
||||||
|
|||||||
@@ -164,6 +164,8 @@ After the video tag, add a script that initializes the stream when the page is f
|
|||||||
if (Hls.isSupported()) {
|
if (Hls.isSupported()) {
|
||||||
const hls = new Hls({
|
const hls = new Hls({
|
||||||
xhrSetup: function (xhr, url) {
|
xhrSetup: function (xhr, url) {
|
||||||
|
xhr.withCredentials = true;
|
||||||
|
|
||||||
let user = ""; // fill if needed
|
let user = ""; // fill if needed
|
||||||
let pass = ""; // fill if needed
|
let pass = ""; // fill if needed
|
||||||
let token = ""; // fill if needed
|
let token = ""; // fill if needed
|
||||||
|
|||||||
@@ -120,6 +120,9 @@ func (a *API) Initialize() error {
|
|||||||
if !interfaceIsEmpty(a.HLSServer) {
|
if !interfaceIsEmpty(a.HLSServer) {
|
||||||
group.GET("/hlsmuxers/list", a.onHLSMuxersList)
|
group.GET("/hlsmuxers/list", a.onHLSMuxersList)
|
||||||
group.GET("/hlsmuxers/get/*name", a.onHLSMuxersGet)
|
group.GET("/hlsmuxers/get/*name", a.onHLSMuxersGet)
|
||||||
|
group.GET("/hlssessions/list", a.onHLSSessionsList)
|
||||||
|
group.GET("/hlssessions/get/:id", a.onHLSSessionsGet)
|
||||||
|
group.POST("/hlssessions/kick/:id", a.onHLSSessionsKick)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !interfaceIsEmpty(a.RTSPServer) {
|
if !interfaceIsEmpty(a.RTSPServer) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/bluenviron/mediamtx/internal/servers/hls"
|
"github.com/bluenviron/mediamtx/internal/servers/hls"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a *API) onHLSMuxersList(ctx *gin.Context) {
|
func (a *API) onHLSMuxersList(ctx *gin.Context) {
|
||||||
@@ -47,3 +48,63 @@ func (a *API) onHLSMuxersGet(ctx *gin.Context) {
|
|||||||
|
|
||||||
ctx.JSON(http.StatusOK, data)
|
ctx.JSON(http.StatusOK, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *API) onHLSSessionsList(ctx *gin.Context) {
|
||||||
|
data, err := a.HLSServer.APISessionsList()
|
||||||
|
if err != nil {
|
||||||
|
a.writeError(ctx, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.ItemCount = len(data.Items)
|
||||||
|
pageCount, err := paginate(&data.Items, ctx.Query("itemsPerPage"), ctx.Query("page"))
|
||||||
|
if err != nil {
|
||||||
|
a.writeError(ctx, http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data.PageCount = pageCount
|
||||||
|
|
||||||
|
ctx.JSON(http.StatusOK, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *API) onHLSSessionsGet(ctx *gin.Context) {
|
||||||
|
id := ctx.Param("id")
|
||||||
|
|
||||||
|
uuid, err := uuid.Parse(id)
|
||||||
|
if err != nil {
|
||||||
|
a.writeError(ctx, http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := a.HLSServer.APISessionsGet(uuid)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, hls.ErrSessionNotFound) {
|
||||||
|
a.writeError(ctx, http.StatusNotFound, err)
|
||||||
|
} else {
|
||||||
|
a.writeError(ctx, http.StatusInternalServerError, err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(http.StatusOK, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *API) onHLSSessionsKick(ctx *gin.Context) {
|
||||||
|
uuid, err := uuid.Parse(ctx.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
a.writeError(ctx, http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = a.HLSServer.APISessionsKick(uuid)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, hls.ErrSessionNotFound) {
|
||||||
|
a.writeError(ctx, http.StatusNotFound, err)
|
||||||
|
} else {
|
||||||
|
a.writeError(ctx, http.StatusInternalServerError, err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
a.writeOK(ctx)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package api //nolint:revive
|
package api //nolint:revive
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sort"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -9,13 +11,17 @@ import (
|
|||||||
"github.com/bluenviron/mediamtx/internal/defs"
|
"github.com/bluenviron/mediamtx/internal/defs"
|
||||||
"github.com/bluenviron/mediamtx/internal/servers/hls"
|
"github.com/bluenviron/mediamtx/internal/servers/hls"
|
||||||
"github.com/bluenviron/mediamtx/internal/test"
|
"github.com/bluenviron/mediamtx/internal/test"
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
type testHLSServer struct {
|
type testHLSServer struct {
|
||||||
muxers map[string]*defs.APIHLSMuxer
|
muxers map[string]*defs.APIHLSMuxer
|
||||||
|
sessions map[string]*defs.APIHLSSession
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var testTime = time.Date(2024, 1, 2, 3, 4, 5, 0, time.UTC)
|
||||||
|
|
||||||
func (s *testHLSServer) APIMuxersList() (*defs.APIHLSMuxerList, error) {
|
func (s *testHLSServer) APIMuxersList() (*defs.APIHLSMuxerList, error) {
|
||||||
items := make([]defs.APIHLSMuxer, 0, len(s.muxers))
|
items := make([]defs.APIHLSMuxer, 0, len(s.muxers))
|
||||||
for _, muxer := range s.muxers {
|
for _, muxer := range s.muxers {
|
||||||
@@ -32,8 +38,36 @@ func (s *testHLSServer) APIMuxersGet(name string) (*defs.APIHLSMuxer, error) {
|
|||||||
return muxer, nil
|
return muxer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *testHLSServer) APISessionsList() (*defs.APIHLSSessionList, error) {
|
||||||
|
items := make([]defs.APIHLSSession, 0, len(s.sessions))
|
||||||
|
for _, session := range s.sessions {
|
||||||
|
items = append(items, *session)
|
||||||
|
}
|
||||||
|
sort.Slice(items, func(i, j int) bool {
|
||||||
|
return items[i].Created.Before(items[j].Created)
|
||||||
|
})
|
||||||
|
return &defs.APIHLSSessionList{Items: items}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *testHLSServer) APISessionsGet(id uuid.UUID) (*defs.APIHLSSession, error) {
|
||||||
|
session, ok := s.sessions[id.String()]
|
||||||
|
if !ok {
|
||||||
|
return nil, hls.ErrSessionNotFound
|
||||||
|
}
|
||||||
|
return session, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *testHLSServer) APISessionsKick(id uuid.UUID) error {
|
||||||
|
_, ok := s.sessions[id.String()]
|
||||||
|
if !ok {
|
||||||
|
return hls.ErrSessionNotFound
|
||||||
|
}
|
||||||
|
delete(s.sessions, id.String())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func TestHLSMuxersList(t *testing.T) {
|
func TestHLSMuxersList(t *testing.T) {
|
||||||
now := time.Now()
|
now := testTime
|
||||||
hlsServer := &testHLSServer{
|
hlsServer := &testHLSServer{
|
||||||
muxers: map[string]*defs.APIHLSMuxer{
|
muxers: map[string]*defs.APIHLSMuxer{
|
||||||
"test1": {
|
"test1": {
|
||||||
@@ -80,7 +114,7 @@ func TestHLSMuxersList(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestHLSMuxersGet(t *testing.T) {
|
func TestHLSMuxersGet(t *testing.T) {
|
||||||
now := time.Now()
|
now := testTime
|
||||||
hlsServer := &testHLSServer{
|
hlsServer := &testHLSServer{
|
||||||
muxers: map[string]*defs.APIHLSMuxer{
|
muxers: map[string]*defs.APIHLSMuxer{
|
||||||
"mypath": {
|
"mypath": {
|
||||||
@@ -118,3 +152,185 @@ func TestHLSMuxersGet(t *testing.T) {
|
|||||||
require.Equal(t, uint64(12), out.OutboundFramesDiscarded)
|
require.Equal(t, uint64(12), out.OutboundFramesDiscarded)
|
||||||
require.Equal(t, uint64(9999), out.BytesSent)
|
require.Equal(t, uint64(9999), out.BytesSent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHLSSessionsList(t *testing.T) {
|
||||||
|
now := testTime
|
||||||
|
hlsServer := &testHLSServer{
|
||||||
|
sessions: map[string]*defs.APIHLSSession{
|
||||||
|
"session1": {
|
||||||
|
ID: uuid.MustParse("18294761-f9d1-4ea9-9a35-fe265b62eb41"),
|
||||||
|
Created: now,
|
||||||
|
RemoteAddr: "192.168.1.1:5000",
|
||||||
|
Path: "stream1",
|
||||||
|
Query: "key=val1",
|
||||||
|
User: "user1",
|
||||||
|
OutboundBytes: 111,
|
||||||
|
},
|
||||||
|
"session2": {
|
||||||
|
ID: uuid.MustParse("18294761-f9d1-4ea9-9a35-fe265b62eb42"),
|
||||||
|
Created: now.Add(time.Minute),
|
||||||
|
RemoteAddr: "192.168.1.2:5001",
|
||||||
|
Path: "stream2",
|
||||||
|
Query: "key=val2",
|
||||||
|
User: "user2",
|
||||||
|
OutboundBytes: 222,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
api := API{
|
||||||
|
Address: "localhost:9997",
|
||||||
|
ReadTimeout: conf.Duration(10 * time.Second),
|
||||||
|
WriteTimeout: conf.Duration(10 * time.Second),
|
||||||
|
AuthManager: test.NilAuthManager,
|
||||||
|
HLSServer: hlsServer,
|
||||||
|
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 defs.APIHLSSessionList
|
||||||
|
httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/hlssessions/list", nil, &out)
|
||||||
|
|
||||||
|
require.Equal(t, 2, out.ItemCount)
|
||||||
|
require.Equal(t, 1, out.PageCount)
|
||||||
|
require.Len(t, out.Items, 2)
|
||||||
|
require.Equal(t, []defs.APIHLSSession{
|
||||||
|
{
|
||||||
|
ID: uuid.MustParse("18294761-f9d1-4ea9-9a35-fe265b62eb41"),
|
||||||
|
Created: now,
|
||||||
|
RemoteAddr: "192.168.1.1:5000",
|
||||||
|
Path: "stream1",
|
||||||
|
Query: "key=val1",
|
||||||
|
User: "user1",
|
||||||
|
OutboundBytes: 111,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: uuid.MustParse("18294761-f9d1-4ea9-9a35-fe265b62eb42"),
|
||||||
|
Created: now.Add(time.Minute),
|
||||||
|
RemoteAddr: "192.168.1.2:5001",
|
||||||
|
Path: "stream2",
|
||||||
|
Query: "key=val2",
|
||||||
|
User: "user2",
|
||||||
|
OutboundBytes: 222,
|
||||||
|
},
|
||||||
|
}, out.Items)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHLSSessionsGet(t *testing.T) {
|
||||||
|
now := testTime
|
||||||
|
hlsServer := &testHLSServer{
|
||||||
|
sessions: map[string]*defs.APIHLSSession{
|
||||||
|
"18294761-f9d1-4ea9-9a35-fe265b62eb41": {
|
||||||
|
ID: uuid.MustParse("18294761-f9d1-4ea9-9a35-fe265b62eb41"),
|
||||||
|
Created: now,
|
||||||
|
RemoteAddr: "192.168.1.100:5000",
|
||||||
|
Path: "mystream",
|
||||||
|
Query: "key=val",
|
||||||
|
User: "myuser",
|
||||||
|
OutboundBytes: 345,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
api := API{
|
||||||
|
Address: "localhost:9997",
|
||||||
|
ReadTimeout: conf.Duration(10 * time.Second),
|
||||||
|
WriteTimeout: conf.Duration(10 * time.Second),
|
||||||
|
AuthManager: test.NilAuthManager,
|
||||||
|
HLSServer: hlsServer,
|
||||||
|
Parent: &testParent{},
|
||||||
|
}
|
||||||
|
err := api.Initialize()
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer api.Close()
|
||||||
|
|
||||||
|
tr := &http.Transport{}
|
||||||
|
defer tr.CloseIdleConnections()
|
||||||
|
hc := &http.Client{Transport: tr}
|
||||||
|
|
||||||
|
sessionID := "18294761-f9d1-4ea9-9a35-fe265b62eb41"
|
||||||
|
|
||||||
|
var out defs.APIHLSSession
|
||||||
|
httpRequest(t, hc, http.MethodGet, fmt.Sprintf("http://localhost:9997/v3/hlssessions/get/%s", sessionID), nil, &out)
|
||||||
|
|
||||||
|
require.Equal(t, uuid.MustParse(sessionID), out.ID)
|
||||||
|
require.Equal(t, "192.168.1.100:5000", out.RemoteAddr)
|
||||||
|
require.Equal(t, "mystream", out.Path)
|
||||||
|
require.Equal(t, "key=val", out.Query)
|
||||||
|
require.Equal(t, "myuser", out.User)
|
||||||
|
require.Equal(t, uint64(345), out.OutboundBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHLSSessionsKick(t *testing.T) {
|
||||||
|
now := testTime
|
||||||
|
sessionID := uuid.MustParse("18294761-f9d1-4ea9-9a35-fe265b62eb41")
|
||||||
|
hlsServer := &testHLSServer{
|
||||||
|
sessions: map[string]*defs.APIHLSSession{
|
||||||
|
sessionID.String(): {
|
||||||
|
ID: sessionID,
|
||||||
|
Created: now,
|
||||||
|
RemoteAddr: "192.168.1.100:5000",
|
||||||
|
Path: "mystream",
|
||||||
|
OutboundBytes: 345,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
api := API{
|
||||||
|
Address: "localhost:9997",
|
||||||
|
ReadTimeout: conf.Duration(10 * time.Second),
|
||||||
|
WriteTimeout: conf.Duration(10 * time.Second),
|
||||||
|
AuthManager: test.NilAuthManager,
|
||||||
|
HLSServer: hlsServer,
|
||||||
|
Parent: &testParent{},
|
||||||
|
}
|
||||||
|
err := api.Initialize()
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer api.Close()
|
||||||
|
|
||||||
|
tr := &http.Transport{}
|
||||||
|
defer tr.CloseIdleConnections()
|
||||||
|
hc := &http.Client{Transport: tr}
|
||||||
|
|
||||||
|
httpRequest(t, hc, http.MethodPost, fmt.Sprintf("http://localhost:9997/v3/hlssessions/kick/%s", sessionID), nil, nil)
|
||||||
|
|
||||||
|
_, ok := hlsServer.sessions[sessionID.String()]
|
||||||
|
require.False(t, ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHLSSessionsKickNotFound(t *testing.T) {
|
||||||
|
hlsServer := &testHLSServer{}
|
||||||
|
|
||||||
|
api := API{
|
||||||
|
Address: "localhost:9997",
|
||||||
|
ReadTimeout: conf.Duration(10 * time.Second),
|
||||||
|
WriteTimeout: conf.Duration(10 * time.Second),
|
||||||
|
AuthManager: test.NilAuthManager,
|
||||||
|
HLSServer: hlsServer,
|
||||||
|
Parent: &testParent{},
|
||||||
|
}
|
||||||
|
err := api.Initialize()
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer api.Close()
|
||||||
|
|
||||||
|
tr := &http.Transport{}
|
||||||
|
defer tr.CloseIdleConnections()
|
||||||
|
hc := &http.Client{Transport: tr}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodPost,
|
||||||
|
fmt.Sprintf("http://localhost:9997/v3/hlssessions/kick/%s", uuid.New()), nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
res, err := hc.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusNotFound, res.StatusCode)
|
||||||
|
checkError(t, res.Body, "session not found")
|
||||||
|
}
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ func TestPathsGet(t *testing.T) {
|
|||||||
BytesReceived: 123456,
|
BytesReceived: 123456,
|
||||||
BytesSent: 789012,
|
BytesSent: 789012,
|
||||||
Readers: []defs.APIPathReader{
|
Readers: []defs.APIPathReader{
|
||||||
{Type: defs.APIPathReaderTypeHLSMuxer, ID: "muxer1"},
|
{Type: defs.APIPathReaderTypeHLSSession, ID: "session6123"},
|
||||||
{Type: defs.APIPathReaderTypeWebRTCSession, ID: "session456"},
|
{Type: defs.APIPathReaderTypeWebRTCSession, ID: "session456"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,6 +7,6 @@ type Error struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Error implements the error interface.
|
// Error implements the error interface.
|
||||||
func (e Error) Error() string {
|
func (e *Error) Error() string {
|
||||||
return "authentication failed: " + e.Wrapped.Error()
|
return "authentication failed: " + e.Wrapped.Error()
|
||||||
}
|
}
|
||||||
|
|||||||
+87
-16
@@ -14,10 +14,13 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/bluenviron/gohlslib/v2"
|
||||||
"github.com/bluenviron/gortmplib"
|
"github.com/bluenviron/gortmplib"
|
||||||
rtmpcodecs "github.com/bluenviron/gortmplib/pkg/codecs"
|
rtmpcodecs "github.com/bluenviron/gortmplib/pkg/codecs"
|
||||||
"github.com/bluenviron/gortsplib/v5"
|
"github.com/bluenviron/gortsplib/v5"
|
||||||
"github.com/bluenviron/gortsplib/v5/pkg/description"
|
"github.com/bluenviron/gortsplib/v5/pkg/description"
|
||||||
|
"github.com/bluenviron/gortsplib/v5/pkg/format"
|
||||||
|
"github.com/bluenviron/gortsplib/v5/pkg/format/rtph264"
|
||||||
"github.com/bluenviron/mediacommon/v2/pkg/formats/mpegts"
|
"github.com/bluenviron/mediacommon/v2/pkg/formats/mpegts"
|
||||||
tscodecs "github.com/bluenviron/mediacommon/v2/pkg/formats/mpegts/codecs"
|
tscodecs "github.com/bluenviron/mediacommon/v2/pkg/formats/mpegts/codecs"
|
||||||
srt "github.com/datarhei/gosrt"
|
srt "github.com/datarhei/gosrt"
|
||||||
@@ -369,7 +372,8 @@ func TestAPIProtocolListGet(t *testing.T) {
|
|||||||
"rtsps sessions",
|
"rtsps sessions",
|
||||||
"rtmp",
|
"rtmp",
|
||||||
"rtmps",
|
"rtmps",
|
||||||
"hls",
|
"hls sessions",
|
||||||
|
"hls muxers",
|
||||||
"webrtc",
|
"webrtc",
|
||||||
"srt",
|
"srt",
|
||||||
} {
|
} {
|
||||||
@@ -470,7 +474,7 @@ func TestAPIProtocolListGet(t *testing.T) {
|
|||||||
|
|
||||||
time.Sleep(500 * time.Millisecond)
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
|
||||||
case "hls":
|
case "hls sessions", "hls muxers":
|
||||||
source := gortsplib.Client{}
|
source := gortsplib.Client{}
|
||||||
err = source.StartRecording("rtsp://localhost:8554/mypath",
|
err = source.StartRecording("rtsp://localhost:8554/mypath",
|
||||||
&description.Session{Medias: []*description.Media{medi}})
|
&description.Session{Medias: []*description.Media{medi}})
|
||||||
@@ -612,7 +616,10 @@ func TestAPIProtocolListGet(t *testing.T) {
|
|||||||
case "rtmps":
|
case "rtmps":
|
||||||
pa = "rtmpsconns"
|
pa = "rtmpsconns"
|
||||||
|
|
||||||
case "hls":
|
case "hls sessions":
|
||||||
|
pa = "hlssessions"
|
||||||
|
|
||||||
|
case "hls muxers":
|
||||||
pa = "hlsmuxers"
|
pa = "hlsmuxers"
|
||||||
|
|
||||||
case "webrtc":
|
case "webrtc":
|
||||||
@@ -792,7 +799,24 @@ func TestAPIProtocolListGet(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}, out1)
|
}, out1)
|
||||||
|
|
||||||
case "hls":
|
case "hls sessions":
|
||||||
|
require.Equal(t, map[string]any{
|
||||||
|
"itemCount": float64(1),
|
||||||
|
"pageCount": float64(1),
|
||||||
|
"items": []any{
|
||||||
|
map[string]any{
|
||||||
|
"id": out1.(map[string]any)["items"].([]any)[0].(map[string]any)["id"],
|
||||||
|
"created": out1.(map[string]any)["items"].([]any)[0].(map[string]any)["created"],
|
||||||
|
"remoteAddr": out1.(map[string]any)["items"].([]any)[0].(map[string]any)["remoteAddr"],
|
||||||
|
"path": "mypath",
|
||||||
|
"query": "",
|
||||||
|
"user": "",
|
||||||
|
"outboundBytes": out1.(map[string]any)["items"].([]any)[0].(map[string]any)["outboundBytes"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, out1)
|
||||||
|
|
||||||
|
case "hls muxers":
|
||||||
require.Equal(t, map[string]any{
|
require.Equal(t, map[string]any{
|
||||||
"itemCount": float64(1),
|
"itemCount": float64(1),
|
||||||
"pageCount": float64(1),
|
"pageCount": float64(1),
|
||||||
@@ -919,7 +943,7 @@ func TestAPIProtocolListGet(t *testing.T) {
|
|||||||
|
|
||||||
var out2 any
|
var out2 any
|
||||||
|
|
||||||
if ca == "hls" {
|
if ca == "hls muxers" {
|
||||||
httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/"+pa+"/get/"+
|
httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/"+pa+"/get/"+
|
||||||
out1.(map[string]any)["items"].([]any)[0].(map[string]any)["path"].(string),
|
out1.(map[string]any)["items"].([]any)[0].(map[string]any)["path"].(string),
|
||||||
nil, &out2)
|
nil, &out2)
|
||||||
@@ -979,7 +1003,7 @@ func TestAPIProtocolListGet(t *testing.T) {
|
|||||||
out2.(map[string]any)["rtcpPacketsReceived"] = out1.(map[string]any)["items"].([]any)[0].(map[string]any)["rtcpPacketsReceived"]
|
out2.(map[string]any)["rtcpPacketsReceived"] = out1.(map[string]any)["items"].([]any)[0].(map[string]any)["rtcpPacketsReceived"]
|
||||||
out2.(map[string]any)["rtcpPacketsSent"] = out1.(map[string]any)["items"].([]any)[0].(map[string]any)["rtcpPacketsSent"]
|
out2.(map[string]any)["rtcpPacketsSent"] = out1.(map[string]any)["items"].([]any)[0].(map[string]any)["rtcpPacketsSent"]
|
||||||
|
|
||||||
case "hls":
|
case "hls muxers":
|
||||||
out2.(map[string]any)["lastRequest"] = out1.(map[string]any)["items"].([]any)[0].(map[string]any)["lastRequest"]
|
out2.(map[string]any)["lastRequest"] = out1.(map[string]any)["items"].([]any)[0].(map[string]any)["lastRequest"]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1004,7 +1028,8 @@ func TestAPIProtocolGetNotFound(t *testing.T) {
|
|||||||
"rtsps sessions",
|
"rtsps sessions",
|
||||||
"rtmp",
|
"rtmp",
|
||||||
"rtmps",
|
"rtmps",
|
||||||
"hls",
|
"hls sessions",
|
||||||
|
"hls muxers",
|
||||||
"webrtc",
|
"webrtc",
|
||||||
"srt",
|
"srt",
|
||||||
} {
|
} {
|
||||||
@@ -1055,7 +1080,10 @@ func TestAPIProtocolGetNotFound(t *testing.T) {
|
|||||||
case "rtmps":
|
case "rtmps":
|
||||||
pa = "rtmpsconns"
|
pa = "rtmpsconns"
|
||||||
|
|
||||||
case "hls":
|
case "hls sessions":
|
||||||
|
pa = "hlssessions"
|
||||||
|
|
||||||
|
case "hls muxers":
|
||||||
pa = "hlsmuxers"
|
pa = "hlsmuxers"
|
||||||
|
|
||||||
case "webrtc":
|
case "webrtc":
|
||||||
@@ -1081,10 +1109,10 @@ func TestAPIProtocolGetNotFound(t *testing.T) {
|
|||||||
case "rtsp conns", "rtsps conns", "rtmp", "rtmps", "srt":
|
case "rtsp conns", "rtsps conns", "rtmp", "rtmps", "srt":
|
||||||
checkError(t, "connection not found", res.Body)
|
checkError(t, "connection not found", res.Body)
|
||||||
|
|
||||||
case "rtsp sessions", "rtsps sessions", "webrtc":
|
case "rtsp sessions", "rtsps sessions", "hls sessions", "webrtc":
|
||||||
checkError(t, "session not found", res.Body)
|
checkError(t, "session not found", res.Body)
|
||||||
|
|
||||||
case "hls":
|
case "hls muxers":
|
||||||
checkError(t, "muxer not found", res.Body)
|
checkError(t, "muxer not found", res.Body)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -1105,6 +1133,7 @@ func TestAPIProtocolKick(t *testing.T) {
|
|||||||
"rtsp",
|
"rtsp",
|
||||||
"rtsps",
|
"rtsps",
|
||||||
"rtmp",
|
"rtmp",
|
||||||
|
"hls",
|
||||||
"webrtc",
|
"webrtc",
|
||||||
"srt",
|
"srt",
|
||||||
} {
|
} {
|
||||||
@@ -1134,7 +1163,6 @@ func TestAPIProtocolKick(t *testing.T) {
|
|||||||
switch ca {
|
switch ca {
|
||||||
case "rtsp":
|
case "rtsp":
|
||||||
source := gortsplib.Client{}
|
source := gortsplib.Client{}
|
||||||
|
|
||||||
err = source.StartRecording("rtsp://localhost:8554/mypath",
|
err = source.StartRecording("rtsp://localhost:8554/mypath",
|
||||||
&description.Session{Medias: []*description.Media{medi}})
|
&description.Session{Medias: []*description.Media{medi}})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -1144,7 +1172,6 @@ func TestAPIProtocolKick(t *testing.T) {
|
|||||||
source := gortsplib.Client{
|
source := gortsplib.Client{
|
||||||
TLSConfig: &tls.Config{InsecureSkipVerify: true},
|
TLSConfig: &tls.Config{InsecureSkipVerify: true},
|
||||||
}
|
}
|
||||||
|
|
||||||
err = source.StartRecording("rtsps://localhost:8322/mypath",
|
err = source.StartRecording("rtsps://localhost:8322/mypath",
|
||||||
&description.Session{Medias: []*description.Media{medi}})
|
&description.Session{Medias: []*description.Media{medi}})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -1180,6 +1207,45 @@ func TestAPIProtocolKick(t *testing.T) {
|
|||||||
err = w.WriteH264(track, 2*time.Second, 2*time.Second, [][]byte{{5, 2, 3, 4}})
|
err = w.WriteH264(track, 2*time.Second, 2*time.Second, [][]byte{{5, 2, 3, 4}})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
case "hls":
|
||||||
|
source := gortsplib.Client{}
|
||||||
|
err = source.StartRecording("rtsp://localhost:8554/mypath",
|
||||||
|
&description.Session{Medias: []*description.Media{medi}})
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer source.Close()
|
||||||
|
|
||||||
|
var enc *rtph264.Encoder
|
||||||
|
enc, err = medi.Formats[0].(*format.H264).CreateEncoder()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tracksReceived := make(chan struct{})
|
||||||
|
|
||||||
|
client := &gohlslib.Client{
|
||||||
|
URI: "http://localhost:8888/mypath/index.m3u8",
|
||||||
|
OnTracks: func(_ []*gohlslib.Track) error {
|
||||||
|
close(tracksReceived)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err = client.Start()
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
|
||||||
|
for i := range 2 {
|
||||||
|
var pkts []*rtp.Packet
|
||||||
|
pkts, err = enc.Encode([][]byte{{5, 2, 3, 4}})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
pkts[0].Timestamp = uint32(i * 90000)
|
||||||
|
|
||||||
|
err = source.WritePacketRTP(medi, pkts[0])
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
<-tracksReceived
|
||||||
|
|
||||||
case "webrtc":
|
case "webrtc":
|
||||||
var u *url.URL
|
var u *url.URL
|
||||||
u, err = url.Parse("http://localhost:8889/mypath/whip")
|
u, err = url.Parse("http://localhost:8889/mypath/whip")
|
||||||
@@ -1243,6 +1309,9 @@ func TestAPIProtocolKick(t *testing.T) {
|
|||||||
case "rtmp":
|
case "rtmp":
|
||||||
pa = "rtmpconns"
|
pa = "rtmpconns"
|
||||||
|
|
||||||
|
case "hls":
|
||||||
|
pa = "hlssessions"
|
||||||
|
|
||||||
case "webrtc":
|
case "webrtc":
|
||||||
pa = "webrtcsessions"
|
pa = "webrtcsessions"
|
||||||
|
|
||||||
@@ -1256,6 +1325,7 @@ func TestAPIProtocolKick(t *testing.T) {
|
|||||||
} `json:"items"`
|
} `json:"items"`
|
||||||
}
|
}
|
||||||
httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/"+pa+"/list", nil, &out1)
|
httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/"+pa+"/list", nil, &out1)
|
||||||
|
require.NotEmpty(t, out1.Items)
|
||||||
|
|
||||||
httpRequest(t, hc, http.MethodPost, "http://localhost:9997/v3/"+pa+"/kick/"+out1.Items[0].ID, nil, nil)
|
httpRequest(t, hc, http.MethodPost, "http://localhost:9997/v3/"+pa+"/kick/"+out1.Items[0].ID, nil, nil)
|
||||||
|
|
||||||
@@ -1283,6 +1353,7 @@ func TestAPIProtocolKickNotFound(t *testing.T) {
|
|||||||
"rtsp",
|
"rtsp",
|
||||||
"rtsps",
|
"rtsps",
|
||||||
"rtmp",
|
"rtmp",
|
||||||
|
"hls",
|
||||||
"webrtc",
|
"webrtc",
|
||||||
"srt",
|
"srt",
|
||||||
} {
|
} {
|
||||||
@@ -1318,6 +1389,9 @@ func TestAPIProtocolKickNotFound(t *testing.T) {
|
|||||||
case "rtmp":
|
case "rtmp":
|
||||||
pa = "rtmpconns"
|
pa = "rtmpconns"
|
||||||
|
|
||||||
|
case "hls":
|
||||||
|
pa = "hlssessions"
|
||||||
|
|
||||||
case "webrtc":
|
case "webrtc":
|
||||||
pa = "webrtcsessions"
|
pa = "webrtcsessions"
|
||||||
|
|
||||||
@@ -1341,11 +1415,8 @@ func TestAPIProtocolKickNotFound(t *testing.T) {
|
|||||||
case "rtsp conns", "rtsps conns", "rtmp", "rtmps", "srt":
|
case "rtsp conns", "rtsps conns", "rtmp", "rtmps", "srt":
|
||||||
checkError(t, "connection not found", res.Body)
|
checkError(t, "connection not found", res.Body)
|
||||||
|
|
||||||
case "rtsp sessions", "rtsps sessions", "webrtc":
|
case "rtsp sessions", "rtsps sessions", "hls", "webrtc":
|
||||||
checkError(t, "session not found", res.Body)
|
checkError(t, "session not found", res.Body)
|
||||||
|
|
||||||
case "hls":
|
|
||||||
checkError(t, "muxer not found", res.Body)
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -609,6 +609,7 @@ func (p *Core) createResources(initial bool) error {
|
|||||||
ReadTimeout: p.conf.ReadTimeout,
|
ReadTimeout: p.conf.ReadTimeout,
|
||||||
WriteTimeout: p.conf.WriteTimeout,
|
WriteTimeout: p.conf.WriteTimeout,
|
||||||
MuxerCloseAfter: p.conf.HLSMuxerCloseAfter,
|
MuxerCloseAfter: p.conf.HLSMuxerCloseAfter,
|
||||||
|
ExternalCmdPool: p.externalCmdPool,
|
||||||
Metrics: p.metrics,
|
Metrics: p.metrics,
|
||||||
PathManager: p.pathManager,
|
PathManager: p.pathManager,
|
||||||
Parent: p,
|
Parent: p,
|
||||||
|
|||||||
@@ -86,6 +86,10 @@ paths_bytes_received 0
|
|||||||
paths_bytes_sent 0
|
paths_bytes_sent 0
|
||||||
paths_readers 0
|
paths_readers 0
|
||||||
|
|
||||||
|
# HLS sessions
|
||||||
|
hls_sessions 0
|
||||||
|
hls_sessions_outbound_bytes 0
|
||||||
|
|
||||||
# HLS muxers
|
# HLS muxers
|
||||||
hls_muxers 0
|
hls_muxers 0
|
||||||
hls_muxers_outbound_bytes 0
|
hls_muxers_outbound_bytes 0
|
||||||
|
|||||||
@@ -506,7 +506,7 @@ func (pa *path) doDescribe(req defs.PathDescribeReq) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
req.Res <- defs.PathDescribeRes{Err: defs.PathNoStreamAvailableError{PathName: pa.name}}
|
req.Res <- defs.PathDescribeRes{Err: &defs.PathNoStreamAvailableError{PathName: pa.name}}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pa *path) doRemovePublisher(req defs.PathRemovePublisherReq) {
|
func (pa *path) doRemovePublisher(req defs.PathRemovePublisherReq) {
|
||||||
@@ -598,7 +598,7 @@ func (pa *path) doAddReader(req defs.PathAddReaderReq) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
req.Res <- defs.PathAddReaderRes{Err: defs.PathNoStreamAvailableError{PathName: pa.name}}
|
req.Res <- defs.PathAddReaderRes{Err: &defs.PathNoStreamAvailableError{PathName: pa.name}}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pa *path) doRemoveReader(req defs.PathRemoveReaderReq) {
|
func (pa *path) doRemoveReader(req defs.PathRemoveReaderReq) {
|
||||||
@@ -699,11 +699,13 @@ func (pa *path) doAPIPathsGet(req pathAPIPathsGetReq) {
|
|||||||
return pa.stream.OutboundBytes()
|
return pa.stream.OutboundBytes()
|
||||||
}(),
|
}(),
|
||||||
Readers: func() []defs.APIPathReader {
|
Readers: func() []defs.APIPathReader {
|
||||||
ret := make([]defs.APIPathReader, len(pa.readers))
|
ret := make([]defs.APIPathReader, 0, len(pa.readers))
|
||||||
i := 0
|
|
||||||
for r := range pa.readers {
|
for r := range pa.readers {
|
||||||
ret[i] = *r.APIReaderDescribe()
|
desc := *r.APIReaderDescribe()
|
||||||
i++
|
if desc.Type != defs.APIPathReaderTypeHidden {
|
||||||
|
ret = append(ret, desc)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Slice(ret, func(i, j int) bool {
|
sort.Slice(ret, func(i, j int) bool {
|
||||||
|
|||||||
@@ -1,13 +1,45 @@
|
|||||||
package defs
|
package defs
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
// APIHLSServer contains methods used by the API and Metrics server.
|
// APIHLSServer contains methods used by the API and Metrics server.
|
||||||
type APIHLSServer interface {
|
type APIHLSServer interface {
|
||||||
|
APISessionsList() (*APIHLSSessionList, error)
|
||||||
|
APISessionsGet(uuid.UUID) (*APIHLSSession, error)
|
||||||
|
APISessionsKick(uuid.UUID) error
|
||||||
APIMuxersList() (*APIHLSMuxerList, error)
|
APIMuxersList() (*APIHLSMuxerList, error)
|
||||||
APIMuxersGet(string) (*APIHLSMuxer, error)
|
APIMuxersGet(string) (*APIHLSMuxer, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// APIHLSSessionList is a list of HLS sessions.
|
||||||
|
type APIHLSSessionList struct {
|
||||||
|
ItemCount int `json:"itemCount"`
|
||||||
|
PageCount int `json:"pageCount"`
|
||||||
|
Items []APIHLSSession `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// APIHLSSession is an HLS session.
|
||||||
|
type APIHLSSession struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
Created time.Time `json:"created"`
|
||||||
|
RemoteAddr string `json:"remoteAddr"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
Query string `json:"query"`
|
||||||
|
User string `json:"user"`
|
||||||
|
OutboundBytes uint64 `json:"outboundBytes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// APIHLSMuxerList is a list of HLS muxers.
|
||||||
|
type APIHLSMuxerList struct {
|
||||||
|
ItemCount int `json:"itemCount"`
|
||||||
|
PageCount int `json:"pageCount"`
|
||||||
|
Items []APIHLSMuxer `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
// APIHLSMuxer is an HLS muxer.
|
// APIHLSMuxer is an HLS muxer.
|
||||||
type APIHLSMuxer struct {
|
type APIHLSMuxer struct {
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
@@ -18,10 +50,3 @@ type APIHLSMuxer struct {
|
|||||||
// deprecated
|
// deprecated
|
||||||
BytesSent uint64 `json:"bytesSent" deprecated:"true"`
|
BytesSent uint64 `json:"bytesSent" deprecated:"true"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// APIHLSMuxerList is a list of HLS muxers.
|
|
||||||
type APIHLSMuxerList struct {
|
|
||||||
ItemCount int `json:"itemCount"`
|
|
||||||
PageCount int `json:"pageCount"`
|
|
||||||
Items []APIHLSMuxer `json:"items"`
|
|
||||||
}
|
|
||||||
|
|||||||
+10
-10
@@ -43,16 +43,16 @@ type APIPathReaderType string
|
|||||||
|
|
||||||
// reader types.
|
// reader types.
|
||||||
const (
|
const (
|
||||||
APIPathReaderTypeHLSMuxer APIPathReaderType = "hlsMuxer"
|
APIPathReaderTypeHLSSession APIPathReaderType = "hlsSession"
|
||||||
APIPathReaderTypeRTMPConn APIPathReaderType = "rtmpConn"
|
APIPathReaderTypeRTMPConn APIPathReaderType = "rtmpConn"
|
||||||
APIPathReaderTypeRTMPSConn APIPathReaderType = "rtmpsConn"
|
APIPathReaderTypeRTMPSConn APIPathReaderType = "rtmpsConn"
|
||||||
APIPathReaderTypeRTSPConn APIPathReaderType = "rtspConn"
|
APIPathReaderTypeRTSPConn APIPathReaderType = "rtspConn"
|
||||||
APIPathReaderTypeRPICameraSecondary APIPathReaderType = "rpiCameraSecondary"
|
APIPathReaderTypeRTSPSession APIPathReaderType = "rtspSession"
|
||||||
APIPathReaderTypeRTSPSession APIPathReaderType = "rtspSession"
|
APIPathReaderTypeRTSPSConn APIPathReaderType = "rtspsConn"
|
||||||
APIPathReaderTypeRTSPSConn APIPathReaderType = "rtspsConn"
|
APIPathReaderTypeRTSPSSession APIPathReaderType = "rtspsSession"
|
||||||
APIPathReaderTypeRTSPSSession APIPathReaderType = "rtspsSession"
|
APIPathReaderTypeSRTConn APIPathReaderType = "srtConn"
|
||||||
APIPathReaderTypeSRTConn APIPathReaderType = "srtConn"
|
APIPathReaderTypeWebRTCSession APIPathReaderType = "webRTCSession"
|
||||||
APIPathReaderTypeWebRTCSession APIPathReaderType = "webRTCSession"
|
APIPathReaderTypeHidden APIPathReaderType = "hidden"
|
||||||
)
|
)
|
||||||
|
|
||||||
// APIPathReader is a reader.
|
// APIPathReader is a reader.
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ type PathNoStreamAvailableError struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Error implements the error interface.
|
// Error implements the error interface.
|
||||||
func (e PathNoStreamAvailableError) Error() string {
|
func (e *PathNoStreamAvailableError) Error() string {
|
||||||
return fmt.Sprintf("no stream is available on path '%s'", e.PathName)
|
return fmt.Sprintf("no stream is available on path '%s'", e.PathName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -175,8 +175,7 @@ func goEnumToApi(rt reflect.Type) (openAPISchema, bool) {
|
|||||||
|
|
||||||
case reflect.TypeOf(defs.APIPathReaderType("")):
|
case reflect.TypeOf(defs.APIPathReaderType("")):
|
||||||
return openAPISchema{Type: "string", Enum: []string{
|
return openAPISchema{Type: "string", Enum: []string{
|
||||||
"hlsMuxer",
|
"hlsSession",
|
||||||
"rpiCameraSecondary",
|
|
||||||
"rtmpConn",
|
"rtmpConn",
|
||||||
"rtmpsConn",
|
"rtmpsConn",
|
||||||
"rtspConn",
|
"rtspConn",
|
||||||
@@ -352,6 +351,14 @@ func TestGo2API(t *testing.T) {
|
|||||||
"HLSMuxerList",
|
"HLSMuxerList",
|
||||||
defs.APIHLSMuxerList{},
|
defs.APIHLSMuxerList{},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"HLSSession",
|
||||||
|
defs.APIHLSSession{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"HLSSessionList",
|
||||||
|
defs.APIHLSSessionList{},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"Info",
|
"Info",
|
||||||
defs.APIInfo{},
|
defs.APIInfo{},
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ type metricsType string
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
metricsTypePaths metricsType = "paths"
|
metricsTypePaths metricsType = "paths"
|
||||||
|
metricsTypeHLSSessions metricsType = "hls_sessions"
|
||||||
metricsTypeHLSMuxers metricsType = "hls_muxers"
|
metricsTypeHLSMuxers metricsType = "hls_muxers"
|
||||||
metricsTypeRTSPConns metricsType = "rtsp_conns"
|
metricsTypeRTSPConns metricsType = "rtsp_conns"
|
||||||
metricsTypeRTSPSessions metricsType = "rtsp_sessions"
|
metricsTypeRTSPSessions metricsType = "rtsp_sessions"
|
||||||
@@ -227,6 +228,7 @@ func (m *Metrics) onMetrics(ctx *gin.Context) {
|
|||||||
typ := metricsType(ctx.Query("type"))
|
typ := metricsType(ctx.Query("type"))
|
||||||
pathFilter := ctx.Query("path")
|
pathFilter := ctx.Query("path")
|
||||||
hlsMuxerFilter := ctx.Query("hls_muxer")
|
hlsMuxerFilter := ctx.Query("hls_muxer")
|
||||||
|
hlsSessionFilter := ctx.Query("hls_session")
|
||||||
rtspConnFilter := ctx.Query("rtsp_conn")
|
rtspConnFilter := ctx.Query("rtsp_conn")
|
||||||
rtspSessionFilter := ctx.Query("rtsp_session")
|
rtspSessionFilter := ctx.Query("rtsp_session")
|
||||||
rtspsConnFilter := ctx.Query("rtsps_conn")
|
rtspsConnFilter := ctx.Query("rtsps_conn")
|
||||||
@@ -238,6 +240,7 @@ func (m *Metrics) onMetrics(ctx *gin.Context) {
|
|||||||
|
|
||||||
anyFilterActive := pathFilter != "" ||
|
anyFilterActive := pathFilter != "" ||
|
||||||
hlsMuxerFilter != "" ||
|
hlsMuxerFilter != "" ||
|
||||||
|
hlsSessionFilter != "" ||
|
||||||
rtspConnFilter != "" ||
|
rtspConnFilter != "" ||
|
||||||
rtspSessionFilter != "" ||
|
rtspSessionFilter != "" ||
|
||||||
rtspsConnFilter != "" ||
|
rtspsConnFilter != "" ||
|
||||||
@@ -336,6 +339,32 @@ func (m *Metrics) onMetrics(ctx *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !interfaceIsEmpty(hlsServer) {
|
if !interfaceIsEmpty(hlsServer) {
|
||||||
|
if (typ == "" || typ == metricsTypeHLSSessions) && (!anyFilterActive || hlsSessionFilter != "") {
|
||||||
|
var data *defs.APIHLSSessionList
|
||||||
|
data, err := hlsServer.APISessionsList()
|
||||||
|
if err == nil && len(data.Items) != 0 {
|
||||||
|
out.WriteString("# HLS sessions\n")
|
||||||
|
for _, i := range data.Items {
|
||||||
|
if hlsSessionFilter == "" || hlsSessionFilter == i.ID.String() {
|
||||||
|
ta := tags(map[string]string{
|
||||||
|
"id": i.ID.String(),
|
||||||
|
"path": i.Path,
|
||||||
|
"remoteAddr": i.RemoteAddr,
|
||||||
|
})
|
||||||
|
|
||||||
|
metric(&out, "hls_sessions", ta, 1)
|
||||||
|
metric(&out, "hls_sessions_outbound_bytes", ta, int64(i.OutboundBytes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.WriteString("\n")
|
||||||
|
} else if hlsSessionFilter == "" {
|
||||||
|
out.WriteString("# HLS sessions\n")
|
||||||
|
metric(&out, "hls_sessions", "", 0)
|
||||||
|
metric(&out, "hls_sessions_outbound_bytes", "", 0)
|
||||||
|
out.WriteString("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (typ == "" || typ == metricsTypeHLSMuxers) && (!anyFilterActive || hlsMuxerFilter != "") {
|
if (typ == "" || typ == metricsTypeHLSMuxers) && (!anyFilterActive || hlsMuxerFilter != "") {
|
||||||
var data *defs.APIHLSMuxerList
|
var data *defs.APIHLSMuxerList
|
||||||
data, err := hlsServer.APIMuxersList()
|
data, err := hlsServer.APIMuxersList()
|
||||||
|
|||||||
@@ -89,6 +89,28 @@ func (dummyHLSServer) APIMuxersGet(string) (*defs.APIHLSMuxer, error) {
|
|||||||
panic("unused")
|
panic("unused")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (dummyHLSServer) APISessionsList() (*defs.APIHLSSessionList, error) {
|
||||||
|
return &defs.APIHLSSessionList{
|
||||||
|
ItemCount: 1,
|
||||||
|
PageCount: 1,
|
||||||
|
Items: []defs.APIHLSSession{{
|
||||||
|
ID: uuid.MustParse("18294761-f9d1-4ea9-9a35-fe265b62eb41"),
|
||||||
|
Created: time.Date(2003, 11, 4, 23, 15, 7, 0, time.UTC),
|
||||||
|
RemoteAddr: "124.5.5.5:34542",
|
||||||
|
Path: "mypath",
|
||||||
|
OutboundBytes: 187,
|
||||||
|
}},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dummyHLSServer) APISessionsGet(uuid.UUID) (*defs.APIHLSSession, error) {
|
||||||
|
panic("unused")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dummyHLSServer) APISessionsKick(uuid.UUID) error {
|
||||||
|
panic("unused")
|
||||||
|
}
|
||||||
|
|
||||||
type dummyRTSPServer struct{}
|
type dummyRTSPServer struct{}
|
||||||
|
|
||||||
func (dummyRTSPServer) APIConnsList() (*defs.APIRTSPConnsList, error) {
|
func (dummyRTSPServer) APIConnsList() (*defs.APIRTSPConnsList, error) {
|
||||||
@@ -332,6 +354,18 @@ func (emptyHLSServer) APIMuxersGet(string) (*defs.APIHLSMuxer, error) {
|
|||||||
panic("unused")
|
panic("unused")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (emptyHLSServer) APISessionsList() (*defs.APIHLSSessionList, error) {
|
||||||
|
return &defs.APIHLSSessionList{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (emptyHLSServer) APISessionsGet(uuid.UUID) (*defs.APIHLSSession, error) {
|
||||||
|
panic("unused")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (emptyHLSServer) APISessionsKick(uuid.UUID) error {
|
||||||
|
panic("unused")
|
||||||
|
}
|
||||||
|
|
||||||
type emptyRTSPServer struct{}
|
type emptyRTSPServer struct{}
|
||||||
|
|
||||||
func (emptyRTSPServer) APIConnsList() (*defs.APIRTSPConnsList, error) {
|
func (emptyRTSPServer) APIConnsList() (*defs.APIRTSPConnsList, error) {
|
||||||
@@ -478,6 +512,12 @@ func TestMetrics(t *testing.T) {
|
|||||||
"paths_bytes_received{name=\"mypath\",state=\"ready\"} 123\n"+
|
"paths_bytes_received{name=\"mypath\",state=\"ready\"} 123\n"+
|
||||||
"paths_bytes_sent{name=\"mypath\",state=\"ready\"} 456\n"+
|
"paths_bytes_sent{name=\"mypath\",state=\"ready\"} 456\n"+
|
||||||
"\n"+
|
"\n"+
|
||||||
|
"# HLS sessions\n"+
|
||||||
|
"hls_sessions{id=\"18294761-f9d1-4ea9-9a35-fe265b62eb41\",path=\"mypath\","+
|
||||||
|
"remoteAddr=\"124.5.5.5:34542\"} 1\n"+
|
||||||
|
"hls_sessions_outbound_bytes{id=\"18294761-f9d1-4ea9-9a35-fe265b62eb41\",path=\"mypath\","+
|
||||||
|
"remoteAddr=\"124.5.5.5:34542\"} 187\n"+
|
||||||
|
"\n"+
|
||||||
"# HLS muxers\n"+
|
"# HLS muxers\n"+
|
||||||
"hls_muxers{name=\"mypath\"} 1\n"+
|
"hls_muxers{name=\"mypath\"} 1\n"+
|
||||||
"hls_muxers_outbound_bytes{name=\"mypath\"} 789\n"+
|
"hls_muxers_outbound_bytes{name=\"mypath\"} 789\n"+
|
||||||
@@ -835,6 +875,10 @@ func TestZeroMetricsFallback(t *testing.T) {
|
|||||||
"paths_bytes_sent 0\n"+
|
"paths_bytes_sent 0\n"+
|
||||||
"paths_readers 0\n"+
|
"paths_readers 0\n"+
|
||||||
"\n"+
|
"\n"+
|
||||||
|
"# HLS sessions\n"+
|
||||||
|
"hls_sessions 0\n"+
|
||||||
|
`hls_sessions_outbound_bytes 0`+"\n"+
|
||||||
|
"\n"+
|
||||||
"# HLS muxers\n"+
|
"# HLS muxers\n"+
|
||||||
"hls_muxers 0\n"+
|
"hls_muxers 0\n"+
|
||||||
"hls_muxers_outbound_bytes 0\n"+
|
"hls_muxers_outbound_bytes 0\n"+
|
||||||
@@ -965,6 +1009,7 @@ func TestFilter(t *testing.T) {
|
|||||||
for _, ca := range []string{
|
for _, ca := range []string{
|
||||||
"path",
|
"path",
|
||||||
"hls_muxer",
|
"hls_muxer",
|
||||||
|
"hls_session",
|
||||||
"rtsp_conn",
|
"rtsp_conn",
|
||||||
"rtsp_session",
|
"rtsp_session",
|
||||||
"rtsps_conn",
|
"rtsps_conn",
|
||||||
@@ -1007,6 +1052,8 @@ func TestFilter(t *testing.T) {
|
|||||||
u += "?path=mypath"
|
u += "?path=mypath"
|
||||||
case "hls_muxer":
|
case "hls_muxer":
|
||||||
u += "?hls_muxer=mypath"
|
u += "?hls_muxer=mypath"
|
||||||
|
case "hls_session":
|
||||||
|
u += "?hls_session=18294761-f9d1-4ea9-9a35-fe265b62eb41"
|
||||||
case "rtsp_conn":
|
case "rtsp_conn":
|
||||||
u += "?rtsp_conn=18294761-f9d1-4ea9-9a35-fe265b62eb41"
|
u += "?rtsp_conn=18294761-f9d1-4ea9-9a35-fe265b62eb41"
|
||||||
case "rtsp_session":
|
case "rtsp_session":
|
||||||
@@ -1059,6 +1106,16 @@ func TestFilter(t *testing.T) {
|
|||||||
`hls_muxers_bytes_sent{name="mypath"} 789`+"\n\n",
|
`hls_muxers_bytes_sent{name="mypath"} 789`+"\n\n",
|
||||||
string(byts))
|
string(byts))
|
||||||
|
|
||||||
|
case "hls_session":
|
||||||
|
require.Equal(t,
|
||||||
|
"# HLS sessions\n"+
|
||||||
|
`hls_sessions{id="18294761-f9d1-4ea9-9a35-fe265b62eb41",path="mypath",`+
|
||||||
|
`remoteAddr="124.5.5.5:34542"} 1`+"\n"+
|
||||||
|
`hls_sessions_outbound_bytes{id="18294761-f9d1-4ea9-9a35-fe265b62eb41",path="mypath",`+
|
||||||
|
`remoteAddr="124.5.5.5:34542"} 187`+"\n"+
|
||||||
|
"\n",
|
||||||
|
string(byts))
|
||||||
|
|
||||||
case "rtsp_conn":
|
case "rtsp_conn":
|
||||||
require.Equal(t,
|
require.Equal(t,
|
||||||
"# RTSP connections\n"+
|
"# RTSP connections\n"+
|
||||||
|
|||||||
@@ -15,7 +15,10 @@ func isOriginAllowed(origin string, allowOrigins []string) (string, bool) {
|
|||||||
|
|
||||||
for _, o := range allowOrigins {
|
for _, o := range allowOrigins {
|
||||||
if o == "*" {
|
if o == "*" {
|
||||||
return o, true
|
if origin != "" {
|
||||||
|
return origin, true
|
||||||
|
}
|
||||||
|
return "*", true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ func TestHandlerOrigin(t *testing.T) {
|
|||||||
"everything allowed, with origin",
|
"everything allowed, with origin",
|
||||||
"https://example.com",
|
"https://example.com",
|
||||||
[]string{"*"},
|
[]string{"*"},
|
||||||
"*",
|
"https://example.com",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allowed",
|
"allowed",
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ func (ri *recorderInstance) initialize() {
|
|||||||
ri.format,
|
ri.format,
|
||||||
)
|
)
|
||||||
ri.reader = &stream.Reader{
|
ri.reader = &stream.Reader{
|
||||||
SkipBytesSent: true,
|
SkipOutboundBytes: true,
|
||||||
Parent: ri,
|
Parent: ri,
|
||||||
}
|
}
|
||||||
|
|
||||||
ri.terminate = make(chan struct{})
|
ri.terminate = make(chan struct{})
|
||||||
|
|||||||
@@ -35,6 +35,12 @@ func mergePathAndQuery(path string, rawQuery string) string {
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isIOS(userAgent string) bool {
|
||||||
|
return strings.Contains(userAgent, "iPad") ||
|
||||||
|
strings.Contains(userAgent, "iPhone") ||
|
||||||
|
strings.Contains(userAgent, "iPod")
|
||||||
|
}
|
||||||
|
|
||||||
type httpServer struct {
|
type httpServer struct {
|
||||||
address string
|
address string
|
||||||
dumpPackets bool
|
dumpPackets bool
|
||||||
@@ -124,6 +130,17 @@ func (s *httpServer) onRequest(ctx *gin.Context) {
|
|||||||
var dir string
|
var dir string
|
||||||
var fname string
|
var fname string
|
||||||
|
|
||||||
|
type contentType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
index contentType = iota
|
||||||
|
multivariantPlaylist
|
||||||
|
mediaPlaylist
|
||||||
|
segment
|
||||||
|
)
|
||||||
|
|
||||||
|
var contentTyp contentType
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case strings.HasSuffix(pa, "/hls.min.js"):
|
case strings.HasSuffix(pa, "/hls.min.js"):
|
||||||
ctx.Header("Cache-Control", "max-age=3600")
|
ctx.Header("Cache-Control", "max-age=3600")
|
||||||
@@ -135,8 +152,16 @@ func (s *httpServer) onRequest(ctx *gin.Context) {
|
|||||||
case pa == "", pa == "favicon.ico", strings.HasSuffix(pa, "/hls.min.js.map"):
|
case pa == "", pa == "favicon.ico", strings.HasSuffix(pa, "/hls.min.js.map"):
|
||||||
return
|
return
|
||||||
|
|
||||||
case strings.HasSuffix(pa, ".m3u8") ||
|
case strings.HasSuffix(pa, ".m3u8"):
|
||||||
strings.HasSuffix(pa, ".ts") ||
|
dir, fname = gopath.Dir(pa), gopath.Base(pa)
|
||||||
|
|
||||||
|
if fname == "index.m3u8" {
|
||||||
|
contentTyp = multivariantPlaylist
|
||||||
|
} else {
|
||||||
|
contentTyp = mediaPlaylist
|
||||||
|
}
|
||||||
|
|
||||||
|
case strings.HasSuffix(pa, ".ts") ||
|
||||||
strings.HasSuffix(pa, ".mp4") ||
|
strings.HasSuffix(pa, ".mp4") ||
|
||||||
strings.HasSuffix(pa, ".mp"):
|
strings.HasSuffix(pa, ".mp"):
|
||||||
dir, fname = gopath.Dir(pa), gopath.Base(pa)
|
dir, fname = gopath.Dir(pa), gopath.Base(pa)
|
||||||
@@ -145,42 +170,169 @@ func (s *httpServer) onRequest(ctx *gin.Context) {
|
|||||||
fname += "4"
|
fname += "4"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
contentTyp = segment
|
||||||
|
|
||||||
default:
|
default:
|
||||||
dir, fname = pa, ""
|
dir = pa
|
||||||
|
|
||||||
if !strings.HasSuffix(dir, "/") {
|
if !strings.HasSuffix(dir, "/") {
|
||||||
ctx.Header("Location", mergePathAndQuery(ctx.Request.URL.Path+"/", ctx.Request.URL.RawQuery))
|
ctx.Header("Location", mergePathAndQuery(ctx.Request.URL.Path+"/", ctx.Request.URL.RawQuery))
|
||||||
ctx.Writer.WriteHeader(http.StatusMovedPermanently)
|
ctx.Writer.WriteHeader(http.StatusMovedPermanently)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dir = dir[:len(dir)-1]
|
||||||
|
contentTyp = index
|
||||||
}
|
}
|
||||||
|
|
||||||
dir = strings.TrimSuffix(dir, "/")
|
switch contentTyp {
|
||||||
if dir == "" {
|
case index:
|
||||||
return
|
_, err := s.pathManager.FindPathConf(defs.PathFindPathConfReq{
|
||||||
}
|
AccessRequest: defs.PathAccessRequest{
|
||||||
|
Name: dir,
|
||||||
|
Query: ctx.Request.URL.RawQuery,
|
||||||
|
Publish: false,
|
||||||
|
Proto: auth.ProtocolHLS,
|
||||||
|
Credentials: httpp.Credentials(ctx.Request),
|
||||||
|
IP: net.ParseIP(ctx.ClientIP()),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
var terr *auth.Error
|
||||||
|
if errors.As(err, &terr) {
|
||||||
|
if terr.AskCredentials {
|
||||||
|
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
|
||||||
|
s.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Log(logger.Info, "connection %v failed to authenticate: %v", httpp.RemoteAddr(ctx), terr.Wrapped)
|
||||||
|
|
||||||
|
// wait some seconds to delay brute force attacks
|
||||||
|
<-time.After(auth.PauseAfterError)
|
||||||
|
|
||||||
res, err := s.pathManager.FindPathConf(defs.PathFindPathConfReq{
|
|
||||||
AccessRequest: defs.PathAccessRequest{
|
|
||||||
Name: dir,
|
|
||||||
Query: ctx.Request.URL.RawQuery,
|
|
||||||
Publish: false,
|
|
||||||
Proto: auth.ProtocolHLS,
|
|
||||||
Credentials: httpp.Credentials(ctx.Request),
|
|
||||||
IP: net.ParseIP(ctx.ClientIP()),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
var terr *auth.Error
|
|
||||||
if errors.As(err, &terr) {
|
|
||||||
if terr.AskCredentials {
|
|
||||||
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
|
|
||||||
s.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error"))
|
s.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Log(logger.Info, "connection %v failed to authenticate: %v", httpp.RemoteAddr(ctx), terr.Wrapped)
|
s.writeErrorNoLog(ctx, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Header("Cache-Control", "max-age=3600")
|
||||||
|
ctx.Header("Content-Type", "text/html")
|
||||||
|
ctx.Writer.WriteHeader(http.StatusOK)
|
||||||
|
ctx.Writer.Write(hlsIndex)
|
||||||
|
|
||||||
|
case multivariantPlaylist:
|
||||||
|
if ctx.Request.URL.Query().Get("cookieCheck") != "1" {
|
||||||
|
http.SetCookie(ctx.Writer, &http.Cookie{
|
||||||
|
Name: "cookieCheck",
|
||||||
|
Value: "1",
|
||||||
|
})
|
||||||
|
|
||||||
|
http.SetCookie(ctx.Writer, &http.Cookie{
|
||||||
|
Name: "cookieCheck",
|
||||||
|
Value: "1",
|
||||||
|
SameSite: http.SameSiteNoneMode,
|
||||||
|
Secure: true,
|
||||||
|
Partitioned: true,
|
||||||
|
HttpOnly: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
q := ctx.Request.URL.Query()
|
||||||
|
q.Set("cookieCheck", "1")
|
||||||
|
ctx.Request.URL.RawQuery = q.Encode()
|
||||||
|
ctx.Writer.Header().Set("Location", mergePathAndQuery(ctx.Request.URL.Path, ctx.Request.URL.RawQuery))
|
||||||
|
|
||||||
|
ctx.Writer.WriteHeader(http.StatusFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := ctx.Request.Cookie("cookieCheck"); err != nil && isIOS(ctx.Request.UserAgent()) {
|
||||||
|
s.writeErrorNoLog(ctx, http.StatusBadRequest, fmt.Errorf("HLS on iOS requires the server to set and read cookies"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
q := ctx.Request.URL.Query()
|
||||||
|
q.Del("cookieCheck")
|
||||||
|
ctx.Request.URL.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
sx := &session{
|
||||||
|
remoteAddr: httpp.RemoteAddr(ctx),
|
||||||
|
pathName: dir,
|
||||||
|
externalCmdPool: s.parent.ExternalCmdPool,
|
||||||
|
pathManager: s.pathManager,
|
||||||
|
server: s.parent,
|
||||||
|
}
|
||||||
|
err := sx.initialize(ctx)
|
||||||
|
if err != nil {
|
||||||
|
var terr *auth.Error
|
||||||
|
if errors.As(err, &terr) {
|
||||||
|
if terr.AskCredentials {
|
||||||
|
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
|
||||||
|
s.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Log(logger.Info, "connection %v failed to authenticate: %v", httpp.RemoteAddr(ctx), terr.Wrapped)
|
||||||
|
|
||||||
|
// wait some seconds to delay brute force attacks
|
||||||
|
<-time.After(auth.PauseAfterError)
|
||||||
|
|
||||||
|
s.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var terr2 *defs.PathNoStreamAvailableError
|
||||||
|
if errors.As(err, &terr2) {
|
||||||
|
s.writeErrorNoLog(ctx, http.StatusNotFound, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.writeErrorNoLog(ctx, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if cookie, err2 := ctx.Request.Cookie("cookieCheck"); err2 == nil && cookie.Value == "1" {
|
||||||
|
http.SetCookie(ctx.Writer, &http.Cookie{
|
||||||
|
Name: sessionCookieName,
|
||||||
|
Value: sx.secret.String(),
|
||||||
|
})
|
||||||
|
|
||||||
|
http.SetCookie(ctx.Writer, &http.Cookie{
|
||||||
|
Name: sessionCookieName,
|
||||||
|
Value: sx.secret.String(),
|
||||||
|
SameSite: http.SameSiteNoneMode,
|
||||||
|
Secure: true,
|
||||||
|
Partitioned: true,
|
||||||
|
HttpOnly: true,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
q = ctx.Request.URL.Query()
|
||||||
|
q.Set(sessionQueryParamName, sx.secret.String())
|
||||||
|
ctx.Request.URL.RawQuery = q.Encode()
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Writer = &responseWriterCounter{
|
||||||
|
ResponseWriter: ctx.Writer,
|
||||||
|
bytesSent: &sx.bytesSent,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Request.URL.Path = fname
|
||||||
|
|
||||||
|
err = sx.muxer.handleRequest(ctx)
|
||||||
|
if err != nil {
|
||||||
|
s.writeErrorNoLog(ctx, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
muxer, err := s.parent.getMuxer(serverGetMuxerReq{
|
||||||
|
path: dir,
|
||||||
|
create: false,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
// wait some seconds to delay brute force attacks
|
// wait some seconds to delay brute force attacks
|
||||||
<-time.After(auth.PauseAfterError)
|
<-time.After(auth.PauseAfterError)
|
||||||
|
|
||||||
@@ -188,31 +340,26 @@ func (s *httpServer) onRequest(ctx *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s.writeErrorNoLog(ctx, http.StatusInternalServerError, err)
|
sx := muxer.findSession(ctx)
|
||||||
return
|
if sx == nil {
|
||||||
}
|
// wait some seconds to delay brute force attacks
|
||||||
|
<-time.After(auth.PauseAfterError)
|
||||||
|
|
||||||
switch fname {
|
s.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error"))
|
||||||
case "":
|
|
||||||
ctx.Header("Cache-Control", "max-age=3600")
|
|
||||||
ctx.Header("Content-Type", "text/html")
|
|
||||||
ctx.Writer.WriteHeader(http.StatusOK)
|
|
||||||
ctx.Writer.Write(hlsIndex)
|
|
||||||
|
|
||||||
default:
|
|
||||||
var mux *muxer
|
|
||||||
mux, err = s.parent.getMuxer(serverGetMuxerReq{
|
|
||||||
path: dir,
|
|
||||||
remoteAddr: httpp.RemoteAddr(ctx),
|
|
||||||
query: ctx.Request.URL.RawQuery,
|
|
||||||
sourceOnDemand: res.Conf.SourceOnDemand,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
ctx.Writer.WriteHeader(http.StatusNotFound)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx.Writer = &responseWriterCounter{
|
||||||
|
ResponseWriter: ctx.Writer,
|
||||||
|
bytesSent: &sx.bytesSent,
|
||||||
|
}
|
||||||
|
|
||||||
ctx.Request.URL.Path = fname
|
ctx.Request.URL.Path = fname
|
||||||
mux.handleRequest(ctx)
|
|
||||||
|
err = muxer.handleRequest(ctx)
|
||||||
|
if err != nil {
|
||||||
|
s.writeErrorNoLog(ctx, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+218
-89
@@ -4,22 +4,22 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/bluenviron/gortsplib/v5/pkg/format"
|
||||||
"github.com/bluenviron/mediamtx/internal/conf"
|
"github.com/bluenviron/mediamtx/internal/conf"
|
||||||
"github.com/bluenviron/mediamtx/internal/defs"
|
"github.com/bluenviron/mediamtx/internal/defs"
|
||||||
"github.com/bluenviron/mediamtx/internal/logger"
|
"github.com/bluenviron/mediamtx/internal/logger"
|
||||||
"github.com/bluenviron/mediamtx/internal/protocols/hls"
|
"github.com/bluenviron/mediamtx/internal/protocols/hls"
|
||||||
"github.com/bluenviron/mediamtx/internal/stream"
|
"github.com/bluenviron/mediamtx/internal/stream"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
closeCheckPeriod = 1 * time.Second
|
recreateInstancePause = 10 * time.Second
|
||||||
recreatePause = 10 * time.Second
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func emptyTimer() *time.Timer {
|
func emptyTimer() *time.Timer {
|
||||||
@@ -28,20 +28,9 @@ func emptyTimer() *time.Timer {
|
|||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
|
|
||||||
type responseWriterWithCounter struct {
|
type muxerCloseInstanceReq struct {
|
||||||
http.ResponseWriter
|
instance *muxerInstance
|
||||||
bytesSent *atomic.Uint64
|
err error
|
||||||
}
|
|
||||||
|
|
||||||
func (w *responseWriterWithCounter) Write(p []byte) (int, error) {
|
|
||||||
n, err := w.ResponseWriter.Write(p)
|
|
||||||
w.bytesSent.Add(uint64(n))
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
|
|
||||||
type muxerGetInstanceRes struct {
|
|
||||||
instance *muxerInstance
|
|
||||||
cumulatedOutboundFramesDiscarded uint64
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type muxer struct {
|
type muxer struct {
|
||||||
@@ -67,9 +56,12 @@ type muxer struct {
|
|||||||
lastRequestTime atomic.Int64
|
lastRequestTime atomic.Int64
|
||||||
bytesSent atomic.Uint64
|
bytesSent atomic.Uint64
|
||||||
|
|
||||||
instanceMutex sync.RWMutex
|
mutex sync.RWMutex
|
||||||
instance *muxerInstance
|
instance *muxerInstance
|
||||||
cumulatedOutboundFramesDiscarded uint64
|
cumulatedOutboundFramesDiscarded uint64
|
||||||
|
sessionsBySecret map[uuid.UUID]*session
|
||||||
|
|
||||||
|
chCloseInstance chan muxerCloseInstanceReq
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *muxer) initialize() {
|
func (m *muxer) initialize() {
|
||||||
@@ -79,7 +71,8 @@ func (m *muxer) initialize() {
|
|||||||
m.ctxCancel = ctxCancel
|
m.ctxCancel = ctxCancel
|
||||||
m.created = time.Now()
|
m.created = time.Now()
|
||||||
m.lastRequestTime.Store(time.Now().UnixNano())
|
m.lastRequestTime.Store(time.Now().UnixNano())
|
||||||
m.bytesSent.Store(0)
|
m.sessionsBySecret = make(map[uuid.UUID]*session)
|
||||||
|
m.chCloseInstance = make(chan muxerCloseInstanceReq)
|
||||||
|
|
||||||
m.Log(logger.Info, "created %s", func() string {
|
m.Log(logger.Info, "created %s", func() string {
|
||||||
if m.remoteAddr == "" {
|
if m.remoteAddr == "" {
|
||||||
@@ -89,7 +82,7 @@ func (m *muxer) initialize() {
|
|||||||
}())
|
}())
|
||||||
|
|
||||||
// block first request to getInstance() until the first instance is available
|
// block first request to getInstance() until the first instance is available
|
||||||
m.instanceMutex.Lock()
|
m.mutex.Lock()
|
||||||
|
|
||||||
m.wg.Add(1)
|
m.wg.Add(1)
|
||||||
go m.run()
|
go m.run()
|
||||||
@@ -116,9 +109,23 @@ func (m *muxer) run() {
|
|||||||
|
|
||||||
m.ctxCancel()
|
m.ctxCancel()
|
||||||
|
|
||||||
m.parent.closeMuxer(m)
|
if m.instance != nil {
|
||||||
|
m.instance.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
m.mutex.Lock()
|
||||||
|
|
||||||
|
m.instance = nil
|
||||||
|
|
||||||
|
for _, sx := range m.sessionsBySecret {
|
||||||
|
sx.close2(fmt.Errorf("muxer destroyed"))
|
||||||
|
}
|
||||||
|
|
||||||
|
m.mutex.Unlock()
|
||||||
|
|
||||||
m.Log(logger.Info, "destroyed: %v", err)
|
m.Log(logger.Info, "destroyed: %v", err)
|
||||||
|
|
||||||
|
m.parent.closeMuxer(m)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *muxer) runInner() error {
|
func (m *muxer) runInner() error {
|
||||||
@@ -131,7 +138,7 @@ func (m *muxer) runInner() error {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.instanceMutex.Unlock()
|
m.mutex.Unlock()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,71 +149,101 @@ func (m *muxer) runInner() error {
|
|||||||
tmp, err := m.createInstance(res.Stream)
|
tmp, err := m.createInstance(res.Stream)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if m.remoteAddr != "" || errors.Is(err, hls.ErrNoSupportedCodecs) {
|
if m.remoteAddr != "" || errors.Is(err, hls.ErrNoSupportedCodecs) {
|
||||||
m.instanceMutex.Unlock()
|
m.mutex.Unlock()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
m.Log(logger.Error, err.Error())
|
m.Log(logger.Error, "muxer instance crashed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
m.instance = tmp
|
m.instance = tmp
|
||||||
m.instanceMutex.Unlock()
|
m.mutex.Unlock()
|
||||||
|
|
||||||
defer func() {
|
var recreateInstanceTimer *time.Timer
|
||||||
if m.instance != nil {
|
|
||||||
m.closeInstance()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
var instanceError chan error
|
|
||||||
var recreateTimer *time.Timer
|
|
||||||
|
|
||||||
if m.instance != nil {
|
if m.instance != nil {
|
||||||
instanceError = m.instance.errorChan()
|
recreateInstanceTimer = emptyTimer()
|
||||||
recreateTimer = emptyTimer()
|
|
||||||
} else {
|
} else {
|
||||||
instanceError = make(chan error)
|
recreateInstanceTimer = time.NewTimer(recreateInstancePause)
|
||||||
recreateTimer = time.NewTimer(recreatePause)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
recreateInstanceTimer.Stop()
|
||||||
|
}()
|
||||||
|
|
||||||
|
sessionCleanupTicker := time.NewTicker(sessionCleanupPeriod)
|
||||||
|
defer sessionCleanupTicker.Stop()
|
||||||
|
|
||||||
var activityCheckTimer *time.Timer
|
var activityCheckTimer *time.Timer
|
||||||
if m.remoteAddr != "" {
|
if m.remoteAddr != "" {
|
||||||
activityCheckTimer = time.NewTimer(closeCheckPeriod)
|
activityCheckTimer = time.NewTimer(max(time.Duration(m.closeAfter)/3, 1*time.Second))
|
||||||
} else {
|
} else {
|
||||||
activityCheckTimer = emptyTimer()
|
activityCheckTimer = emptyTimer()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
activityCheckTimer.Stop()
|
||||||
|
}()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case err = <-instanceError:
|
case req := <-m.chCloseInstance:
|
||||||
if m.remoteAddr != "" {
|
if m.instance != req.instance {
|
||||||
return err
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
m.Log(logger.Error, err.Error())
|
m.mutex.Lock()
|
||||||
m.closeInstance()
|
m.cumulatedOutboundFramesDiscarded += m.instance.reader.OutboundFramesDiscarded()
|
||||||
instanceError = make(chan error)
|
m.instance = nil
|
||||||
recreateTimer = time.NewTimer(recreatePause)
|
m.mutex.Unlock()
|
||||||
|
|
||||||
case <-recreateTimer.C:
|
if m.remoteAddr != "" {
|
||||||
|
return req.err
|
||||||
|
} else {
|
||||||
|
m.mutex.Lock()
|
||||||
|
for _, sx := range m.sessionsBySecret {
|
||||||
|
sx.close2(fmt.Errorf("muxer instance crashed"))
|
||||||
|
}
|
||||||
|
m.sessionsBySecret = make(map[uuid.UUID]*session)
|
||||||
|
m.mutex.Unlock()
|
||||||
|
|
||||||
|
m.Log(logger.Error, "muxer instance crashed: %v", req.err)
|
||||||
|
}
|
||||||
|
|
||||||
|
recreateInstanceTimer = time.NewTimer(recreateInstancePause)
|
||||||
|
|
||||||
|
case <-recreateInstanceTimer.C:
|
||||||
tmp, err = m.createInstance(res.Stream)
|
tmp, err = m.createInstance(res.Stream)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.Log(logger.Error, err.Error())
|
m.Log(logger.Error, "muxer instance crashed: %v", err)
|
||||||
recreateTimer = time.NewTimer(recreatePause)
|
recreateInstanceTimer = time.NewTimer(recreateInstancePause)
|
||||||
} else {
|
continue
|
||||||
m.instanceMutex.Lock()
|
|
||||||
m.instance = tmp
|
|
||||||
m.instanceMutex.Unlock()
|
|
||||||
|
|
||||||
instanceError = m.instance.errorChan()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m.mutex.Lock()
|
||||||
|
m.instance = tmp
|
||||||
|
m.mutex.Unlock()
|
||||||
|
|
||||||
|
case <-sessionCleanupTicker.C:
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
m.mutex.Lock()
|
||||||
|
for secret, sx := range m.sessionsBySecret {
|
||||||
|
lastRequest := time.Unix(0, sx.lastRequestTime.Load())
|
||||||
|
|
||||||
|
if now.Sub(lastRequest) >= sessionCloseAfter {
|
||||||
|
delete(m.sessionsBySecret, secret)
|
||||||
|
sx.close2(fmt.Errorf("inactive"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.mutex.Unlock()
|
||||||
|
|
||||||
case <-activityCheckTimer.C:
|
case <-activityCheckTimer.C:
|
||||||
t := time.Unix(0, m.lastRequestTime.Load())
|
t := time.Unix(0, m.lastRequestTime.Load())
|
||||||
if time.Since(t) >= time.Duration(m.closeAfter) {
|
if time.Since(t) >= time.Duration(m.closeAfter) {
|
||||||
return fmt.Errorf("not used anymore")
|
return fmt.Errorf("not used anymore")
|
||||||
}
|
}
|
||||||
activityCheckTimer = time.NewTimer(closeCheckPeriod)
|
activityCheckTimer = time.NewTimer(max(time.Duration(m.closeAfter)/3, 1*time.Second))
|
||||||
|
|
||||||
case <-m.ctx.Done():
|
case <-m.ctx.Done():
|
||||||
return errors.New("terminated")
|
return errors.New("terminated")
|
||||||
@@ -214,16 +251,6 @@ func (m *muxer) runInner() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *muxer) closeInstance() {
|
|
||||||
m.instanceMutex.Lock()
|
|
||||||
m.cumulatedOutboundFramesDiscarded += m.instance.reader.OutboundFramesDiscarded()
|
|
||||||
var tmp *muxerInstance
|
|
||||||
tmp, m.instance = m.instance, nil
|
|
||||||
m.instanceMutex.Unlock()
|
|
||||||
|
|
||||||
tmp.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *muxer) createInstance(strm *stream.Stream) (*muxerInstance, error) {
|
func (m *muxer) createInstance(strm *stream.Stream) (*muxerInstance, error) {
|
||||||
mi := &muxerInstance{
|
mi := &muxerInstance{
|
||||||
variant: m.variant,
|
variant: m.variant,
|
||||||
@@ -233,54 +260,107 @@ func (m *muxer) createInstance(strm *stream.Stream) (*muxerInstance, error) {
|
|||||||
segmentMaxSize: m.segmentMaxSize,
|
segmentMaxSize: m.segmentMaxSize,
|
||||||
directory: m.directory,
|
directory: m.directory,
|
||||||
pathName: m.pathName,
|
pathName: m.pathName,
|
||||||
|
bytesSent: &m.bytesSent,
|
||||||
|
wg: m.wg,
|
||||||
stream: strm,
|
stream: strm,
|
||||||
|
server: m.parent,
|
||||||
parent: m,
|
parent: m,
|
||||||
}
|
}
|
||||||
err := mi.initialize()
|
err := mi.initialize()
|
||||||
return mi, err
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return mi, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *muxer) getInstance() muxerGetInstanceRes {
|
func (m *muxer) closeInstance(mi *muxerInstance, err error) {
|
||||||
m.instanceMutex.RLock()
|
select {
|
||||||
defer m.instanceMutex.RUnlock()
|
case m.chCloseInstance <- muxerCloseInstanceReq{instance: mi, err: err}:
|
||||||
|
case <-m.ctx.Done():
|
||||||
return muxerGetInstanceRes{
|
|
||||||
instance: m.instance,
|
|
||||||
cumulatedOutboundFramesDiscarded: m.cumulatedOutboundFramesDiscarded,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// APIReaderDescribe implements reader.
|
// APIReaderDescribe implements reader.
|
||||||
func (m *muxer) APIReaderDescribe() *defs.APIPathReader {
|
func (m *muxer) APIReaderDescribe() *defs.APIPathReader {
|
||||||
return &defs.APIPathReader{
|
return &defs.APIPathReader{
|
||||||
Type: defs.APIPathReaderTypeHLSMuxer,
|
Type: defs.APIPathReaderTypeHidden,
|
||||||
ID: "",
|
ID: "",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *muxer) handleRequest(ctx *gin.Context) {
|
func (m *muxer) addSession(sx *session) ([]format.Format, error) {
|
||||||
|
m.mutex.Lock()
|
||||||
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-m.ctx.Done():
|
||||||
|
return nil, fmt.Errorf("terminated")
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.instance == nil {
|
||||||
|
return nil, fmt.Errorf("muxer instance not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
m.sessionsBySecret[sx.secret] = sx
|
||||||
|
return m.instance.reader.Formats(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *muxer) findSession(ctx *gin.Context) *session {
|
||||||
|
var rawSecret string
|
||||||
|
if cookie, err := ctx.Request.Cookie(sessionCookieName); err == nil {
|
||||||
|
rawSecret = cookie.Value
|
||||||
|
} else {
|
||||||
|
q := ctx.Request.URL.Query()
|
||||||
|
rawSecret = q.Get(sessionQueryParamName)
|
||||||
|
}
|
||||||
|
|
||||||
|
secret, err := uuid.Parse(rawSecret)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
m.mutex.RLock()
|
||||||
|
defer m.mutex.RUnlock()
|
||||||
|
|
||||||
|
sx, ok := m.sessionsBySecret[secret]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.ClientIP() != sx.ip {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sx.lastRequestTime.Store(time.Now().UnixNano())
|
||||||
|
|
||||||
|
return sx
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *muxer) handleRequest(ctx *gin.Context) error {
|
||||||
m.lastRequestTime.Store(time.Now().UnixNano())
|
m.lastRequestTime.Store(time.Now().UnixNano())
|
||||||
|
|
||||||
res := m.getInstance()
|
m.mutex.RLock()
|
||||||
if res.instance == nil {
|
instance := m.instance
|
||||||
ctx.Writer.WriteHeader(http.StatusNotFound)
|
m.mutex.RUnlock()
|
||||||
return
|
|
||||||
|
if instance == nil {
|
||||||
|
return fmt.Errorf("muxer instance not available")
|
||||||
}
|
}
|
||||||
|
|
||||||
w := &responseWriterWithCounter{
|
instance.handleRequest(ctx)
|
||||||
ResponseWriter: ctx.Writer,
|
return nil
|
||||||
bytesSent: &m.bytesSent,
|
|
||||||
}
|
|
||||||
|
|
||||||
res.instance.handleRequest(w, ctx.Request)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *muxer) apiItem() *defs.APIHLSMuxer {
|
func (m *muxer) apiItem() *defs.APIHLSMuxer {
|
||||||
res := m.getInstance()
|
m.mutex.RLock()
|
||||||
|
instance := m.instance
|
||||||
|
cumulatedOutboundFramesDiscarded := m.cumulatedOutboundFramesDiscarded
|
||||||
|
m.mutex.RUnlock()
|
||||||
|
|
||||||
outboundFramesDiscarded := res.cumulatedOutboundFramesDiscarded
|
outboundFramesDiscarded := cumulatedOutboundFramesDiscarded
|
||||||
if res.instance != nil {
|
if instance != nil {
|
||||||
outboundFramesDiscarded += res.instance.reader.OutboundFramesDiscarded()
|
outboundFramesDiscarded += instance.reader.OutboundFramesDiscarded()
|
||||||
}
|
}
|
||||||
|
|
||||||
return &defs.APIHLSMuxer{
|
return &defs.APIHLSMuxer{
|
||||||
@@ -292,3 +372,52 @@ func (m *muxer) apiItem() *defs.APIHLSMuxer {
|
|||||||
BytesSent: m.bytesSent.Load(),
|
BytesSent: m.bytesSent.Load(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *muxer) apiSessionsList() []defs.APIHLSSession {
|
||||||
|
m.mutex.RLock()
|
||||||
|
defer m.mutex.RUnlock()
|
||||||
|
|
||||||
|
sessions := make([]defs.APIHLSSession, 0, len(m.sessionsBySecret))
|
||||||
|
|
||||||
|
for _, sx := range m.sessionsBySecret {
|
||||||
|
sessions = append(sessions, *sx.apiItem())
|
||||||
|
}
|
||||||
|
|
||||||
|
return sessions
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *muxer) findSessionByUUID(uuid uuid.UUID) *session {
|
||||||
|
for _, sx := range m.sessionsBySecret {
|
||||||
|
if sx.uuid == uuid {
|
||||||
|
return sx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *muxer) apiSessionsGet(uuid uuid.UUID) (*defs.APIHLSSession, bool) {
|
||||||
|
m.mutex.RLock()
|
||||||
|
sx := m.findSessionByUUID(uuid)
|
||||||
|
if sx == nil {
|
||||||
|
m.mutex.RUnlock()
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
m.mutex.RUnlock()
|
||||||
|
|
||||||
|
return sx.apiItem(), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *muxer) apiSessionsKick(uuid uuid.UUID) bool {
|
||||||
|
m.mutex.Lock()
|
||||||
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
|
sx := m.findSessionByUUID(uuid)
|
||||||
|
if sx == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
sx.close2(fmt.Errorf("kicked"))
|
||||||
|
delete(m.sessionsBySecret, sx.secret)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
package hls
|
package hls
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"context"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bluenviron/gohlslib/v2"
|
"github.com/bluenviron/gohlslib/v2"
|
||||||
@@ -12,8 +15,21 @@ import (
|
|||||||
"github.com/bluenviron/mediamtx/internal/logger"
|
"github.com/bluenviron/mediamtx/internal/logger"
|
||||||
"github.com/bluenviron/mediamtx/internal/protocols/hls"
|
"github.com/bluenviron/mediamtx/internal/protocols/hls"
|
||||||
"github.com/bluenviron/mediamtx/internal/stream"
|
"github.com/bluenviron/mediamtx/internal/stream"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
sessionCookieName = "hlsSession"
|
||||||
|
sessionQueryParamName = "session"
|
||||||
|
sessionCloseAfter = 30 * time.Second
|
||||||
|
sessionCleanupPeriod = sessionCloseAfter / 3
|
||||||
|
)
|
||||||
|
|
||||||
|
type instanceParent interface {
|
||||||
|
logger.Writer
|
||||||
|
closeInstance(*muxerInstance, error)
|
||||||
|
}
|
||||||
|
|
||||||
type muxerInstance struct {
|
type muxerInstance struct {
|
||||||
variant conf.HLSVariant
|
variant conf.HLSVariant
|
||||||
segmentCount int
|
segmentCount int
|
||||||
@@ -22,14 +38,21 @@ type muxerInstance struct {
|
|||||||
segmentMaxSize conf.StringSize
|
segmentMaxSize conf.StringSize
|
||||||
directory string
|
directory string
|
||||||
pathName string
|
pathName string
|
||||||
|
bytesSent *atomic.Uint64
|
||||||
|
wg *sync.WaitGroup
|
||||||
stream *stream.Stream
|
stream *stream.Stream
|
||||||
parent logger.Writer
|
server logger.Writer
|
||||||
|
parent instanceParent
|
||||||
|
|
||||||
hmuxer *gohlslib.Muxer
|
ctx context.Context
|
||||||
reader *stream.Reader
|
ctxCancel func()
|
||||||
|
hmuxer *gohlslib.Muxer
|
||||||
|
reader *stream.Reader
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mi *muxerInstance) initialize() error {
|
func (mi *muxerInstance) initialize() error {
|
||||||
|
mi.Log(logger.Debug, "instance created")
|
||||||
|
|
||||||
var muxerDirectory string
|
var muxerDirectory string
|
||||||
if mi.directory != "" {
|
if mi.directory != "" {
|
||||||
muxerDirectory = filepath.Join(mi.directory, mi.pathName)
|
muxerDirectory = filepath.Join(mi.directory, mi.pathName)
|
||||||
@@ -49,8 +72,8 @@ func (mi *muxerInstance) initialize() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mi.reader = &stream.Reader{
|
mi.reader = &stream.Reader{
|
||||||
SkipBytesSent: true,
|
SkipOutboundBytes: true,
|
||||||
Parent: mi,
|
Parent: mi,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := hls.FromStream(mi.stream.Desc, mi.reader, mi.hmuxer)
|
err := hls.FromStream(mi.stream.Desc, mi.reader, mi.hmuxer)
|
||||||
@@ -68,6 +91,11 @@ func (mi *muxerInstance) initialize() error {
|
|||||||
|
|
||||||
mi.stream.AddReader(mi.reader)
|
mi.stream.AddReader(mi.reader)
|
||||||
|
|
||||||
|
mi.ctx, mi.ctxCancel = context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
mi.wg.Add(1)
|
||||||
|
go mi.run()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,17 +105,50 @@ func (mi *muxerInstance) Log(level logger.Level, format string, args ...any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (mi *muxerInstance) close() {
|
func (mi *muxerInstance) close() {
|
||||||
|
mi.ctxCancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mi *muxerInstance) run() {
|
||||||
|
defer mi.wg.Done()
|
||||||
|
|
||||||
|
err := mi.runInner()
|
||||||
|
|
||||||
|
mi.ctxCancel()
|
||||||
|
|
||||||
mi.stream.RemoveReader(mi.reader)
|
mi.stream.RemoveReader(mi.reader)
|
||||||
|
|
||||||
mi.hmuxer.Close()
|
mi.hmuxer.Close()
|
||||||
|
|
||||||
if mi.hmuxer.Directory != "" {
|
if mi.hmuxer.Directory != "" {
|
||||||
os.Remove(mi.hmuxer.Directory)
|
os.Remove(mi.hmuxer.Directory)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mi.Log(logger.Debug, "instance destroyed: %v", err)
|
||||||
|
|
||||||
|
mi.parent.closeInstance(mi, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mi *muxerInstance) errorChan() chan error {
|
func (mi *muxerInstance) runInner() error {
|
||||||
return mi.reader.Error()
|
for {
|
||||||
|
select {
|
||||||
|
case <-mi.ctx.Done():
|
||||||
|
return fmt.Errorf("terminated")
|
||||||
|
|
||||||
|
case err := <-mi.reader.Error():
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mi *muxerInstance) handleRequest(w http.ResponseWriter, r *http.Request) {
|
func (mi *muxerInstance) handleRequest(ctx *gin.Context) {
|
||||||
mi.hmuxer.Handle(w, r)
|
w := ctx.Writer
|
||||||
|
|
||||||
|
w = &responseWriterNoCache{ResponseWriter: w}
|
||||||
|
|
||||||
|
w = &responseWriterCounter{
|
||||||
|
ResponseWriter: w,
|
||||||
|
bytesSent: mi.bytesSent,
|
||||||
|
}
|
||||||
|
|
||||||
|
mi.hmuxer.Handle(w, ctx.Request)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package hls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type responseWriterCounter struct {
|
||||||
|
gin.ResponseWriter
|
||||||
|
bytesSent *atomic.Uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *responseWriterCounter) Write(p []byte) (int, error) {
|
||||||
|
n, err := w.ResponseWriter.Write(p)
|
||||||
|
w.bytesSent.Add(uint64(n))
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package hls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type responseWriterNoCache struct {
|
||||||
|
gin.ResponseWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *responseWriterNoCache) WriteHeader(statusCode int) {
|
||||||
|
if statusCode == http.StatusOK {
|
||||||
|
w.ResponseWriter.Header().Set("Cache-Control", "private, no-cache")
|
||||||
|
}
|
||||||
|
|
||||||
|
w.ResponseWriter.WriteHeader(statusCode)
|
||||||
|
}
|
||||||
@@ -11,12 +11,17 @@ import (
|
|||||||
|
|
||||||
"github.com/bluenviron/mediamtx/internal/conf"
|
"github.com/bluenviron/mediamtx/internal/conf"
|
||||||
"github.com/bluenviron/mediamtx/internal/defs"
|
"github.com/bluenviron/mediamtx/internal/defs"
|
||||||
|
"github.com/bluenviron/mediamtx/internal/externalcmd"
|
||||||
"github.com/bluenviron/mediamtx/internal/logger"
|
"github.com/bluenviron/mediamtx/internal/logger"
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrMuxerNotFound is returned when a muxer is not found.
|
// ErrMuxerNotFound is returned when a muxer is not found.
|
||||||
var ErrMuxerNotFound = errors.New("muxer not found")
|
var ErrMuxerNotFound = errors.New("muxer not found")
|
||||||
|
|
||||||
|
// ErrSessionNotFound is returned when a session is not found.
|
||||||
|
var ErrSessionNotFound = errors.New("session not found")
|
||||||
|
|
||||||
func interfaceIsEmpty(i any) bool {
|
func interfaceIsEmpty(i any) bool {
|
||||||
return reflect.ValueOf(i).Kind() != reflect.Pointer || reflect.ValueOf(i).IsNil()
|
return reflect.ValueOf(i).Kind() != reflect.Pointer || reflect.ValueOf(i).IsNil()
|
||||||
}
|
}
|
||||||
@@ -28,9 +33,10 @@ type serverGetMuxerRes struct {
|
|||||||
|
|
||||||
type serverGetMuxerReq struct {
|
type serverGetMuxerReq struct {
|
||||||
path string
|
path string
|
||||||
remoteAddr string
|
create bool
|
||||||
query string
|
remoteAddr string // only if create == true
|
||||||
sourceOnDemand bool
|
query string // only if create == true
|
||||||
|
sourceOnDemand bool // only if create == true
|
||||||
res chan serverGetMuxerRes
|
res chan serverGetMuxerRes
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,6 +59,34 @@ type serverAPIMuxersGetReq struct {
|
|||||||
res chan serverAPIMuxersGetRes
|
res chan serverAPIMuxersGetRes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type serverAPISessionsListRes struct {
|
||||||
|
data *defs.APIHLSSessionList
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
type serverAPISessionsListReq struct {
|
||||||
|
res chan serverAPISessionsListRes
|
||||||
|
}
|
||||||
|
|
||||||
|
type serverAPISessionsGetRes struct {
|
||||||
|
data *defs.APIHLSSession
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
type serverAPISessionsGetReq struct {
|
||||||
|
uuid uuid.UUID
|
||||||
|
res chan serverAPISessionsGetRes
|
||||||
|
}
|
||||||
|
|
||||||
|
type serverAPISessionsKickRes struct {
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
type serverAPISessionsKickReq struct {
|
||||||
|
uuid uuid.UUID
|
||||||
|
res chan serverAPISessionsKickRes
|
||||||
|
}
|
||||||
|
|
||||||
type serverMetrics interface {
|
type serverMetrics interface {
|
||||||
SetHLSServer(defs.APIHLSServer)
|
SetHLSServer(defs.APIHLSServer)
|
||||||
}
|
}
|
||||||
@@ -86,6 +120,7 @@ type Server struct {
|
|||||||
ReadTimeout conf.Duration
|
ReadTimeout conf.Duration
|
||||||
WriteTimeout conf.Duration
|
WriteTimeout conf.Duration
|
||||||
MuxerCloseAfter conf.Duration
|
MuxerCloseAfter conf.Duration
|
||||||
|
ExternalCmdPool *externalcmd.Pool
|
||||||
Metrics serverMetrics
|
Metrics serverMetrics
|
||||||
PathManager serverPathManager
|
PathManager serverPathManager
|
||||||
Parent serverParent
|
Parent serverParent
|
||||||
@@ -97,12 +132,15 @@ type Server struct {
|
|||||||
muxers map[string]*muxer
|
muxers map[string]*muxer
|
||||||
|
|
||||||
// in
|
// in
|
||||||
chPathReady chan defs.Path
|
chPathReady chan defs.Path
|
||||||
chPathNotReady chan defs.Path
|
chPathNotReady chan defs.Path
|
||||||
chGetMuxer chan serverGetMuxerReq
|
chGetMuxer chan serverGetMuxerReq
|
||||||
chCloseMuxer chan *muxer
|
chCloseMuxer chan *muxer
|
||||||
chAPIMuxerList chan serverAPIMuxersListReq
|
chAPIMuxerList chan serverAPIMuxersListReq
|
||||||
chAPIMuxerGet chan serverAPIMuxersGetReq
|
chAPIMuxerGet chan serverAPIMuxersGetReq
|
||||||
|
chAPISessionsList chan serverAPISessionsListReq
|
||||||
|
chAPISessionsGet chan serverAPISessionsGetReq
|
||||||
|
chAPISessionsKick chan serverAPISessionsKickReq
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize initializes the server.
|
// Initialize initializes the server.
|
||||||
@@ -118,6 +156,9 @@ func (s *Server) Initialize() error {
|
|||||||
s.chCloseMuxer = make(chan *muxer)
|
s.chCloseMuxer = make(chan *muxer)
|
||||||
s.chAPIMuxerList = make(chan serverAPIMuxersListReq)
|
s.chAPIMuxerList = make(chan serverAPIMuxersListReq)
|
||||||
s.chAPIMuxerGet = make(chan serverAPIMuxersGetReq)
|
s.chAPIMuxerGet = make(chan serverAPIMuxersGetReq)
|
||||||
|
s.chAPISessionsList = make(chan serverAPISessionsListReq)
|
||||||
|
s.chAPISessionsGet = make(chan serverAPISessionsGetReq)
|
||||||
|
s.chAPISessionsKick = make(chan serverAPISessionsKickReq)
|
||||||
|
|
||||||
s.httpServer = &httpServer{
|
s.httpServer = &httpServer{
|
||||||
address: s.Address,
|
address: s.Address,
|
||||||
@@ -211,6 +252,8 @@ outer:
|
|||||||
switch {
|
switch {
|
||||||
case ok:
|
case ok:
|
||||||
req.res <- serverGetMuxerRes{muxer: mux}
|
req.res <- serverGetMuxerRes{muxer: mux}
|
||||||
|
case !req.create:
|
||||||
|
req.res <- serverGetMuxerRes{err: fmt.Errorf("muxer not found")}
|
||||||
case s.AlwaysRemux && !req.sourceOnDemand:
|
case s.AlwaysRemux && !req.sourceOnDemand:
|
||||||
req.res <- serverGetMuxerRes{err: fmt.Errorf("muxer is waiting to be created")}
|
req.res <- serverGetMuxerRes{err: fmt.Errorf("muxer is waiting to be created")}
|
||||||
default:
|
default:
|
||||||
@@ -248,6 +291,43 @@ outer:
|
|||||||
|
|
||||||
req.res <- serverAPIMuxersGetRes{data: muxer.apiItem()}
|
req.res <- serverAPIMuxersGetRes{data: muxer.apiItem()}
|
||||||
|
|
||||||
|
case req := <-s.chAPISessionsList:
|
||||||
|
data := &defs.APIHLSSessionList{
|
||||||
|
Items: []defs.APIHLSSession{},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, muxer := range s.muxers {
|
||||||
|
data.Items = append(data.Items, muxer.apiSessionsList()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(data.Items, func(i, j int) bool {
|
||||||
|
return data.Items[i].Created.Before(data.Items[j].Created)
|
||||||
|
})
|
||||||
|
|
||||||
|
req.res <- serverAPISessionsListRes{data: data}
|
||||||
|
|
||||||
|
case req := <-s.chAPISessionsGet:
|
||||||
|
for _, muxer := range s.muxers {
|
||||||
|
session, ok := muxer.apiSessionsGet(req.uuid)
|
||||||
|
if ok {
|
||||||
|
req.res <- serverAPISessionsGetRes{data: session}
|
||||||
|
continue outer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
req.res <- serverAPISessionsGetRes{err: ErrSessionNotFound}
|
||||||
|
|
||||||
|
case req := <-s.chAPISessionsKick:
|
||||||
|
for _, muxer := range s.muxers {
|
||||||
|
ok := muxer.apiSessionsKick(req.uuid)
|
||||||
|
if ok {
|
||||||
|
req.res <- serverAPISessionsKickRes{}
|
||||||
|
continue outer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
req.res <- serverAPISessionsKickRes{err: ErrSessionNotFound}
|
||||||
|
|
||||||
case <-s.ctx.Done():
|
case <-s.ctx.Done():
|
||||||
break outer
|
break outer
|
||||||
}
|
}
|
||||||
@@ -349,3 +429,53 @@ func (s *Server) APIMuxersGet(name string) (*defs.APIHLSMuxer, error) {
|
|||||||
return nil, fmt.Errorf("terminated")
|
return nil, fmt.Errorf("terminated")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// APISessionsList implements defs.APIHLSServer.
|
||||||
|
func (s *Server) APISessionsList() (*defs.APIHLSSessionList, error) {
|
||||||
|
req := serverAPISessionsListReq{
|
||||||
|
res: make(chan serverAPISessionsListRes),
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case s.chAPISessionsList <- req:
|
||||||
|
res := <-req.res
|
||||||
|
return res.data, res.err
|
||||||
|
|
||||||
|
case <-s.ctx.Done():
|
||||||
|
return nil, fmt.Errorf("terminated")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// APISessionsGet implements defs.APIHLSServer.
|
||||||
|
func (s *Server) APISessionsGet(uuid uuid.UUID) (*defs.APIHLSSession, error) {
|
||||||
|
req := serverAPISessionsGetReq{
|
||||||
|
uuid: uuid,
|
||||||
|
res: make(chan serverAPISessionsGetRes),
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case s.chAPISessionsGet <- req:
|
||||||
|
res := <-req.res
|
||||||
|
return res.data, res.err
|
||||||
|
|
||||||
|
case <-s.ctx.Done():
|
||||||
|
return nil, fmt.Errorf("terminated")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// APISessionsKick implements defs.APIHLSServer.
|
||||||
|
func (s *Server) APISessionsKick(uuid uuid.UUID) error {
|
||||||
|
req := serverAPISessionsKickReq{
|
||||||
|
uuid: uuid,
|
||||||
|
res: make(chan serverAPISessionsKickRes),
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case s.chAPISessionsKick <- req:
|
||||||
|
res := <-req.res
|
||||||
|
return res.err
|
||||||
|
|
||||||
|
case <-s.ctx.Done():
|
||||||
|
return fmt.Errorf("terminated")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/bluenviron/gohlslib/v2"
|
"github.com/bluenviron/gohlslib/v2"
|
||||||
"github.com/bluenviron/gohlslib/v2/pkg/codecs"
|
"github.com/bluenviron/gohlslib/v2/pkg/codecs"
|
||||||
"github.com/bluenviron/gortsplib/v5/pkg/description"
|
"github.com/bluenviron/gortsplib/v5/pkg/description"
|
||||||
|
"github.com/bluenviron/gortsplib/v5/pkg/format"
|
||||||
"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio"
|
"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio"
|
||||||
"github.com/bluenviron/mediamtx/internal/auth"
|
"github.com/bluenviron/mediamtx/internal/auth"
|
||||||
"github.com/bluenviron/mediamtx/internal/conf"
|
"github.com/bluenviron/mediamtx/internal/conf"
|
||||||
@@ -158,7 +159,7 @@ func TestServerNotFound(t *testing.T) {
|
|||||||
},
|
},
|
||||||
addReaderImpl: func(req defs.PathAddReaderReq) (*defs.PathAddReaderRes, error) {
|
addReaderImpl: func(req defs.PathAddReaderReq) (*defs.PathAddReaderRes, error) {
|
||||||
require.Equal(t, "nonexisting", req.AccessRequest.Name)
|
require.Equal(t, "nonexisting", req.AccessRequest.Name)
|
||||||
return nil, fmt.Errorf("not found")
|
return nil, &defs.PathNoStreamAvailableError{}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,11 +254,22 @@ func TestServerRead(t *testing.T) {
|
|||||||
},
|
},
|
||||||
addReaderImpl: func(req defs.PathAddReaderReq) (*defs.PathAddReaderRes, error) {
|
addReaderImpl: func(req defs.PathAddReaderReq) (*defs.PathAddReaderRes, error) {
|
||||||
require.Equal(t, "teststream", req.AccessRequest.Name)
|
require.Equal(t, "teststream", req.AccessRequest.Name)
|
||||||
if ca == "always remux off" {
|
|
||||||
|
switch req.Author.(type) {
|
||||||
|
case (*muxer):
|
||||||
|
if ca == "always remux off" {
|
||||||
|
require.Equal(t, "param=value", req.AccessRequest.Query)
|
||||||
|
} else {
|
||||||
|
require.Equal(t, "", req.AccessRequest.Query)
|
||||||
|
}
|
||||||
|
|
||||||
|
case *session:
|
||||||
require.Equal(t, "param=value", req.AccessRequest.Query)
|
require.Equal(t, "param=value", req.AccessRequest.Query)
|
||||||
} else {
|
|
||||||
require.Equal(t, "", req.AccessRequest.Query)
|
default:
|
||||||
|
t.Errorf("should not happen")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &defs.PathAddReaderRes{Path: &dummyPath{}, Stream: strm}, nil
|
return &defs.PathAddReaderRes{Path: &dummyPath{}, Stream: strm}, nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -579,7 +591,7 @@ func TestAuthError(t *testing.T) {
|
|||||||
ReadTimeout: conf.Duration(10 * time.Second),
|
ReadTimeout: conf.Duration(10 * time.Second),
|
||||||
WriteTimeout: conf.Duration(10 * time.Second),
|
WriteTimeout: conf.Duration(10 * time.Second),
|
||||||
PathManager: &dummyPathManager{
|
PathManager: &dummyPathManager{
|
||||||
findPathConfImpl: func(req defs.PathFindPathConfReq) (*defs.PathFindPathConfRes, error) {
|
addReaderImpl: func(req defs.PathAddReaderReq) (*defs.PathAddReaderRes, error) {
|
||||||
if req.AccessRequest.Credentials.User == "" && req.AccessRequest.Credentials.Pass == "" {
|
if req.AccessRequest.Credentials.User == "" && req.AccessRequest.Credentials.Pass == "" {
|
||||||
return nil, &auth.Error{AskCredentials: true, Wrapped: fmt.Errorf("auth error")}
|
return nil, &auth.Error{AskCredentials: true, Wrapped: fmt.Errorf("auth error")}
|
||||||
}
|
}
|
||||||
@@ -625,3 +637,133 @@ func TestAuthError(t *testing.T) {
|
|||||||
|
|
||||||
require.Equal(t, 2, n)
|
require.Equal(t, 2, n)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAuthQueryPreservedAcrossRedirect(t *testing.T) {
|
||||||
|
s := &Server{
|
||||||
|
Address: "127.0.0.1:8888",
|
||||||
|
Encryption: false,
|
||||||
|
ServerKey: "",
|
||||||
|
ServerCert: "",
|
||||||
|
AlwaysRemux: true,
|
||||||
|
Variant: conf.HLSVariant(gohlslib.MuxerVariantMPEGTS),
|
||||||
|
SegmentCount: 7,
|
||||||
|
SegmentDuration: conf.Duration(1 * time.Second),
|
||||||
|
PartDuration: conf.Duration(200 * time.Millisecond),
|
||||||
|
SegmentMaxSize: 50 * 1024 * 1024,
|
||||||
|
ReadTimeout: conf.Duration(10 * time.Second),
|
||||||
|
WriteTimeout: conf.Duration(10 * time.Second),
|
||||||
|
PathManager: &dummyPathManager{
|
||||||
|
findPathConfImpl: func(_ defs.PathFindPathConfReq) (*defs.PathFindPathConfRes, error) {
|
||||||
|
return nil, &auth.Error{AskCredentials: true, Wrapped: fmt.Errorf("auth error")}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Parent: test.NilLogger,
|
||||||
|
}
|
||||||
|
err := s.Initialize()
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
client := &http.Client{
|
||||||
|
CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
|
||||||
|
return http.ErrUseLastResponse
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := client.Get("http://127.0.0.1:8888/stream/index.m3u8?jwt=mytoken")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusFound, res.StatusCode)
|
||||||
|
require.Equal(t, "/stream/index.m3u8?cookieCheck=1&jwt=mytoken", res.Header.Get("Location"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServerNoSupportedCodecs(t *testing.T) {
|
||||||
|
for _, ca := range []string{
|
||||||
|
"always remux off",
|
||||||
|
"always remux on",
|
||||||
|
} {
|
||||||
|
t.Run(ca, func(t *testing.T) {
|
||||||
|
desc := &description.Session{Medias: []*description.Media{{
|
||||||
|
Type: description.MediaTypeVideo,
|
||||||
|
Formats: []format.Format{&format.VP8{}},
|
||||||
|
}}}
|
||||||
|
|
||||||
|
strm := &stream.Stream{
|
||||||
|
Desc: desc,
|
||||||
|
WriteQueueSize: 512,
|
||||||
|
RTPMaxPayloadSize: 1450,
|
||||||
|
Parent: test.NilLogger,
|
||||||
|
}
|
||||||
|
err := strm.Initialize()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
pm := &dummyPathManager{
|
||||||
|
findPathConfImpl: func(req defs.PathFindPathConfReq) (*defs.PathFindPathConfRes, error) {
|
||||||
|
require.Equal(t, "teststream", req.AccessRequest.Name)
|
||||||
|
return &defs.PathFindPathConfRes{Conf: &conf.Path{}, User: req.AccessRequest.Credentials.User}, nil
|
||||||
|
},
|
||||||
|
addReaderImpl: func(req defs.PathAddReaderReq) (*defs.PathAddReaderRes, error) {
|
||||||
|
require.Equal(t, "teststream", req.AccessRequest.Name)
|
||||||
|
return &defs.PathAddReaderRes{Path: &dummyPath{}, Stream: strm}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &Server{
|
||||||
|
Address: "127.0.0.1:8888",
|
||||||
|
AlwaysRemux: (ca == "always remux on"),
|
||||||
|
Variant: conf.HLSVariant(gohlslib.MuxerVariantMPEGTS),
|
||||||
|
SegmentCount: 7,
|
||||||
|
SegmentDuration: conf.Duration(1 * time.Second),
|
||||||
|
PartDuration: conf.Duration(200 * time.Millisecond),
|
||||||
|
SegmentMaxSize: 50 * 1024 * 1024,
|
||||||
|
TrustedProxies: conf.IPNetworks{},
|
||||||
|
ReadTimeout: conf.Duration(10 * time.Second),
|
||||||
|
WriteTimeout: conf.Duration(10 * time.Second),
|
||||||
|
PathManager: pm,
|
||||||
|
Parent: test.NilLogger,
|
||||||
|
}
|
||||||
|
err = s.Initialize()
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
client := &http.Client{
|
||||||
|
CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
|
||||||
|
return http.ErrUseLastResponse
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := client.Get("http://127.0.0.1:8888/teststream/index.m3u8")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusFound, res.StatusCode)
|
||||||
|
require.Equal(t, "/teststream/index.m3u8?cookieCheck=1", res.Header.Get("Location"))
|
||||||
|
|
||||||
|
res, err = client.Get("http://127.0.0.1:8888/teststream/index.m3u8?cookieCheck=1")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusInternalServerError, res.StatusCode)
|
||||||
|
require.Contains(t, res.Header.Get("Content-Type"), "application/json")
|
||||||
|
|
||||||
|
byts, err := io.ReadAll(res.Body)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var payload defs.APIError
|
||||||
|
err = json.Unmarshal(byts, &payload)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
if ca == "always remux off" {
|
||||||
|
require.Equal(t, defs.APIError{
|
||||||
|
Status: defs.APIErrorStatusError,
|
||||||
|
Error: "terminated",
|
||||||
|
}, payload)
|
||||||
|
} else {
|
||||||
|
require.Equal(t, defs.APIError{
|
||||||
|
Status: defs.APIErrorStatusError,
|
||||||
|
Error: "muxer is waiting to be created",
|
||||||
|
}, payload)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
package hls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"net"
|
||||||
|
"slices"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bluenviron/mediamtx/internal/auth"
|
||||||
|
"github.com/bluenviron/mediamtx/internal/defs"
|
||||||
|
"github.com/bluenviron/mediamtx/internal/externalcmd"
|
||||||
|
"github.com/bluenviron/mediamtx/internal/hooks"
|
||||||
|
"github.com/bluenviron/mediamtx/internal/logger"
|
||||||
|
"github.com/bluenviron/mediamtx/internal/protocols/httpp"
|
||||||
|
"github.com/bluenviron/mediamtx/internal/stream"
|
||||||
|
"github.com/bluenviron/mediamtx/internal/unit"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type sessionServer interface {
|
||||||
|
logger.Writer
|
||||||
|
getMuxer(serverGetMuxerReq) (*muxer, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type session struct {
|
||||||
|
remoteAddr string
|
||||||
|
pathName string
|
||||||
|
externalCmdPool *externalcmd.Pool
|
||||||
|
pathManager serverPathManager
|
||||||
|
server sessionServer
|
||||||
|
|
||||||
|
uuid uuid.UUID
|
||||||
|
secret uuid.UUID
|
||||||
|
ip string
|
||||||
|
created time.Time
|
||||||
|
query string
|
||||||
|
user string
|
||||||
|
lastRequestTime atomic.Int64
|
||||||
|
bytesSent atomic.Uint64
|
||||||
|
path defs.Path
|
||||||
|
stream *stream.Stream
|
||||||
|
muxer *muxer
|
||||||
|
reader *stream.Reader
|
||||||
|
onUnreadHook func()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *session) initialize(ctx *gin.Context) error {
|
||||||
|
s.uuid = uuid.New()
|
||||||
|
s.secret = uuid.New()
|
||||||
|
s.ip, _, _ = net.SplitHostPort(s.remoteAddr)
|
||||||
|
s.created = time.Now()
|
||||||
|
s.query = ctx.Request.URL.RawQuery
|
||||||
|
s.lastRequestTime.Store(time.Now().UnixNano())
|
||||||
|
|
||||||
|
res, err := s.pathManager.AddReader(defs.PathAddReaderReq{
|
||||||
|
Author: s,
|
||||||
|
AccessRequest: defs.PathAccessRequest{
|
||||||
|
Name: s.pathName,
|
||||||
|
Query: s.query,
|
||||||
|
Publish: false,
|
||||||
|
Proto: auth.ProtocolHLS,
|
||||||
|
ID: &s.uuid,
|
||||||
|
Credentials: httpp.Credentials(ctx.Request),
|
||||||
|
IP: net.ParseIP(ctx.ClientIP()),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.path = res.Path
|
||||||
|
s.stream = res.Stream
|
||||||
|
s.user = res.User
|
||||||
|
|
||||||
|
muxer, err := s.server.getMuxer(serverGetMuxerReq{
|
||||||
|
path: s.pathName,
|
||||||
|
create: true,
|
||||||
|
remoteAddr: s.remoteAddr,
|
||||||
|
query: s.query,
|
||||||
|
sourceOnDemand: res.Path.SafeConf().SourceOnDemand,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
s.path.RemoveReader(defs.PathRemoveReaderReq{Author: s})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.muxer = muxer
|
||||||
|
|
||||||
|
muxerFormats, err := s.muxer.addSession(s)
|
||||||
|
if err != nil {
|
||||||
|
s.path.RemoveReader(defs.PathRemoveReaderReq{Author: s})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.reader = &stream.Reader{
|
||||||
|
Parent: s,
|
||||||
|
}
|
||||||
|
|
||||||
|
// all of this is needed to allow Stream to increase outbound bytes for every HLS session
|
||||||
|
for _, medi := range res.Stream.Desc.Medias {
|
||||||
|
for _, forma := range medi.Formats {
|
||||||
|
if slices.Contains(muxerFormats, forma) {
|
||||||
|
s.reader.OnData(medi, forma, func(_ *unit.Unit) error {
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.Stream.AddReader(s.reader)
|
||||||
|
|
||||||
|
s.Log(logger.Info, "created by %s, reading from muxer '%s'", s.remoteAddr, s.pathName)
|
||||||
|
|
||||||
|
s.onUnreadHook = hooks.OnRead(hooks.OnReadParams{
|
||||||
|
Logger: s,
|
||||||
|
ExternalCmdPool: s.externalCmdPool,
|
||||||
|
Conf: res.Path.SafeConf(),
|
||||||
|
ExternalCmdEnv: res.Path.ExternalCmdEnv(),
|
||||||
|
Reader: *s.APIReaderDescribe(),
|
||||||
|
Query: s.query,
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// called by path or path manager.
|
||||||
|
// not implemented since closing the Muxer is enough to close every associated session.
|
||||||
|
func (s *session) Close() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *session) close2(err error) {
|
||||||
|
s.stream.RemoveReader(s.reader)
|
||||||
|
|
||||||
|
s.path.RemoveReader(defs.PathRemoveReaderReq{Author: s})
|
||||||
|
|
||||||
|
s.onUnreadHook()
|
||||||
|
|
||||||
|
s.Log(logger.Info, "closed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log implements logger.Writer.
|
||||||
|
func (s *session) Log(level logger.Level, format string, args ...any) {
|
||||||
|
id := hex.EncodeToString(s.uuid[:4])
|
||||||
|
s.server.Log(level, "[session %v] "+format, append([]any{id}, args...)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *session) apiItem() *defs.APIHLSSession {
|
||||||
|
outboundBytes := s.bytesSent.Load()
|
||||||
|
|
||||||
|
return &defs.APIHLSSession{
|
||||||
|
ID: s.uuid,
|
||||||
|
Created: s.created,
|
||||||
|
RemoteAddr: s.remoteAddr,
|
||||||
|
Path: s.pathName,
|
||||||
|
Query: s.query,
|
||||||
|
User: s.user,
|
||||||
|
OutboundBytes: outboundBytes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *session) APIReaderDescribe() *defs.APIPathReader {
|
||||||
|
return &defs.APIPathReader{
|
||||||
|
Type: defs.APIPathReaderTypeHLSSession,
|
||||||
|
ID: s.uuid.String(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -170,7 +170,7 @@ func (c *conn) onDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx,
|
|||||||
return res, nil, err2
|
return res, nil, err2
|
||||||
}
|
}
|
||||||
|
|
||||||
var terr2 defs.PathNoStreamAvailableError
|
var terr2 *defs.PathNoStreamAvailableError
|
||||||
if errors.As(res.Err, &terr2) {
|
if errors.As(res.Err, &terr2) {
|
||||||
return &base.Response{
|
return &base.Response{
|
||||||
StatusCode: base.StatusNotFound,
|
StatusCode: base.StatusNotFound,
|
||||||
|
|||||||
@@ -287,7 +287,7 @@ func (s *session) onSetup(c *conn, ctx *gortsplib.ServerHandlerOnSetupCtx,
|
|||||||
return res, nil, err2
|
return res, nil, err2
|
||||||
}
|
}
|
||||||
|
|
||||||
var terr2 defs.PathNoStreamAvailableError
|
var terr2 *defs.PathNoStreamAvailableError
|
||||||
if errors.As(err, &terr2) {
|
if errors.As(err, &terr2) {
|
||||||
return &base.Response{
|
return &base.Response{
|
||||||
StatusCode: base.StatusNotFound,
|
StatusCode: base.StatusNotFound,
|
||||||
|
|||||||
@@ -710,7 +710,7 @@ func TestServerReadNotFound(t *testing.T) {
|
|||||||
return &defs.PathFindPathConfRes{Conf: &conf.Path{}, User: req.AccessRequest.Credentials.User}, nil
|
return &defs.PathFindPathConfRes{Conf: &conf.Path{}, User: req.AccessRequest.Credentials.User}, nil
|
||||||
},
|
},
|
||||||
AddReaderImpl: func(_ defs.PathAddReaderReq) (*defs.PathAddReaderRes, error) {
|
AddReaderImpl: func(_ defs.PathAddReaderReq) (*defs.PathAddReaderRes, error) {
|
||||||
return nil, defs.PathNoStreamAvailableError{}
|
return nil, &defs.PathNoStreamAvailableError{}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -279,21 +279,19 @@ func (s *session) runPublish() (int, error) {
|
|||||||
func (s *session) runRead() (int, error) {
|
func (s *session) runRead() (int, error) {
|
||||||
ip, _, _ := net.SplitHostPort(s.req.remoteAddr)
|
ip, _, _ := net.SplitHostPort(s.req.remoteAddr)
|
||||||
|
|
||||||
req := defs.PathAccessRequest{
|
|
||||||
Name: s.req.pathName,
|
|
||||||
Query: s.req.httpRequest.URL.RawQuery,
|
|
||||||
Proto: auth.ProtocolWebRTC,
|
|
||||||
ID: &s.uuid,
|
|
||||||
Credentials: httpp.Credentials(s.req.httpRequest),
|
|
||||||
IP: net.ParseIP(ip),
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := s.pathManager.AddReader(defs.PathAddReaderReq{
|
res, err := s.pathManager.AddReader(defs.PathAddReaderReq{
|
||||||
Author: s,
|
Author: s,
|
||||||
AccessRequest: req,
|
AccessRequest: defs.PathAccessRequest{
|
||||||
|
Name: s.req.pathName,
|
||||||
|
Query: s.req.httpRequest.URL.RawQuery,
|
||||||
|
Proto: auth.ProtocolWebRTC,
|
||||||
|
ID: &s.uuid,
|
||||||
|
Credentials: httpp.Credentials(s.req.httpRequest),
|
||||||
|
IP: net.ParseIP(ip),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var terr2 defs.PathNoStreamAvailableError
|
var terr2 *defs.PathNoStreamAvailableError
|
||||||
if errors.As(err, &terr2) {
|
if errors.As(err, &terr2) {
|
||||||
return http.StatusNotFound, err
|
return http.StatusNotFound, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ func (r *secondaryReader) Close() {
|
|||||||
// APIReaderDescribe implements reader.
|
// APIReaderDescribe implements reader.
|
||||||
func (*secondaryReader) APIReaderDescribe() *defs.APIPathReader {
|
func (*secondaryReader) APIReaderDescribe() *defs.APIPathReader {
|
||||||
return &defs.APIPathReader{
|
return &defs.APIPathReader{
|
||||||
Type: defs.APIPathReaderTypeRPICameraSecondary,
|
Type: defs.APIPathReaderTypeHidden,
|
||||||
ID: "",
|
ID: "",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -341,7 +341,7 @@ func (s *Source) waitForPrimary(
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var err2 defs.PathNoStreamAvailableError
|
var err2 *defs.PathNoStreamAvailableError
|
||||||
if errors.As(err, &err2) {
|
if errors.As(err, &err2) {
|
||||||
select {
|
select {
|
||||||
case <-time.After(pauseBetweenErrors):
|
case <-time.After(pauseBetweenErrors):
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ type OnDataFunc func(*unit.Unit) error
|
|||||||
|
|
||||||
// Reader is a stream reader.
|
// Reader is a stream reader.
|
||||||
type Reader struct {
|
type Reader struct {
|
||||||
SkipBytesSent bool
|
SkipOutboundBytes bool
|
||||||
Parent logger.Writer
|
Parent logger.Writer
|
||||||
|
|
||||||
onDatas map[*description.Media]map[format.Format]OnDataFunc
|
onDatas map[*description.Media]map[format.Format]OnDataFunc
|
||||||
queueSize int
|
queueSize int
|
||||||
|
|||||||
@@ -394,8 +394,8 @@ func (s *Stream) Initialize() error {
|
|||||||
alwaysAvailable: s.AlwaysAvailable,
|
alwaysAvailable: s.AlwaysAvailable,
|
||||||
rtpMaxPayloadSize: s.RTPMaxPayloadSize,
|
rtpMaxPayloadSize: s.RTPMaxPayloadSize,
|
||||||
replaceNTP: s.ReplaceNTP,
|
replaceNTP: s.ReplaceNTP,
|
||||||
addInboundBytes: s.addInboundBytes,
|
inboundBytes: &s.inboundBytes,
|
||||||
addOutboundBytes: s.addOutboundBytes,
|
outboundBytes: &s.outboundBytes,
|
||||||
updateLastTime: s.updateLastTime,
|
updateLastTime: s.updateLastTime,
|
||||||
writeRTSP: s.writeRTSP,
|
writeRTSP: s.writeRTSP,
|
||||||
inboundFramesInError: s.inboundFramesInError,
|
inboundFramesInError: s.inboundFramesInError,
|
||||||
@@ -574,14 +574,6 @@ func (s *Stream) WaitForReaders() {
|
|||||||
<-s.hasReaders
|
<-s.hasReaders
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Stream) addInboundBytes(v uint64) {
|
|
||||||
s.inboundBytes.Add(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Stream) addOutboundBytes(v uint64) {
|
|
||||||
s.outboundBytes.Add(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Stream) updateLastTime(pts time.Duration) {
|
func (s *Stream) updateLastTime(pts time.Duration) {
|
||||||
s.timeMutex.Lock()
|
s.timeMutex.Lock()
|
||||||
defer s.timeMutex.Unlock()
|
defer s.timeMutex.Unlock()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package stream
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bluenviron/gortsplib/v5/pkg/description"
|
"github.com/bluenviron/gortsplib/v5/pkg/description"
|
||||||
@@ -44,8 +45,8 @@ type streamFormat struct {
|
|||||||
rtpMaxPayloadSize int
|
rtpMaxPayloadSize int
|
||||||
replaceNTP bool
|
replaceNTP bool
|
||||||
inboundFramesInError *errordumper.Dumper
|
inboundFramesInError *errordumper.Dumper
|
||||||
addInboundBytes func(uint64)
|
inboundBytes *atomic.Uint64
|
||||||
addOutboundBytes func(uint64)
|
outboundBytes *atomic.Uint64
|
||||||
updateLastTime func(time.Duration)
|
updateLastTime func(time.Duration)
|
||||||
writeRTSP func(*description.Media, []*rtp.Packet, time.Time)
|
writeRTSP func(*description.Media, []*rtp.Packet, time.Time)
|
||||||
parent logger.Writer
|
parent logger.Writer
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package stream
|
package stream
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bluenviron/gortsplib/v5/pkg/description"
|
"github.com/bluenviron/gortsplib/v5/pkg/description"
|
||||||
@@ -15,8 +16,8 @@ type streamMedia struct {
|
|||||||
alwaysAvailable bool
|
alwaysAvailable bool
|
||||||
rtpMaxPayloadSize int
|
rtpMaxPayloadSize int
|
||||||
replaceNTP bool
|
replaceNTP bool
|
||||||
addInboundBytes func(uint64)
|
inboundBytes *atomic.Uint64
|
||||||
addOutboundBytes func(uint64)
|
outboundBytes *atomic.Uint64
|
||||||
updateLastTime func(time.Duration)
|
updateLastTime func(time.Duration)
|
||||||
writeRTSP func(*description.Media, []*rtp.Packet, time.Time)
|
writeRTSP func(*description.Media, []*rtp.Packet, time.Time)
|
||||||
inboundFramesInError *errordumper.Dumper
|
inboundFramesInError *errordumper.Dumper
|
||||||
@@ -36,8 +37,8 @@ func (sm *streamMedia) initialize() error {
|
|||||||
rtpMaxPayloadSize: sm.rtpMaxPayloadSize,
|
rtpMaxPayloadSize: sm.rtpMaxPayloadSize,
|
||||||
replaceNTP: sm.replaceNTP,
|
replaceNTP: sm.replaceNTP,
|
||||||
inboundFramesInError: sm.inboundFramesInError,
|
inboundFramesInError: sm.inboundFramesInError,
|
||||||
addInboundBytes: sm.addInboundBytes,
|
inboundBytes: sm.inboundBytes,
|
||||||
addOutboundBytes: sm.addOutboundBytes,
|
outboundBytes: sm.outboundBytes,
|
||||||
updateLastTime: sm.updateLastTime,
|
updateLastTime: sm.updateLastTime,
|
||||||
writeRTSP: sm.writeRTSP,
|
writeRTSP: sm.writeRTSP,
|
||||||
parent: sm.parent,
|
parent: sm.parent,
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ func TestStream(t *testing.T) {
|
|||||||
require.Equal(t, uint64(14), strm.OutboundBytes())
|
require.Equal(t, uint64(14), strm.OutboundBytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStreamSkipBytesSent(t *testing.T) {
|
func TestStreamSkipOutboundBytes(t *testing.T) {
|
||||||
desc := &description.Session{Medias: []*description.Media{
|
desc := &description.Session{Medias: []*description.Media{
|
||||||
{
|
{
|
||||||
Type: description.MediaTypeVideo,
|
Type: description.MediaTypeVideo,
|
||||||
@@ -98,7 +98,7 @@ func TestStreamSkipBytesSent(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
r := &Reader{
|
r := &Reader{
|
||||||
SkipBytesSent: true,
|
SkipOutboundBytes: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
recv := make(chan struct{})
|
recv := make(chan struct{})
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ func (ssf *subStreamFormat) writeUnitInner(u *unit.Unit) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
size := unitSize(u)
|
size := unitSize(u)
|
||||||
ssf.streamFormat.addInboundBytes(size)
|
ssf.streamFormat.inboundBytes.Add(size)
|
||||||
|
|
||||||
ssf.streamFormat.writeRTSP(ssf.streamFormat.media, u.RTPPackets, u.NTP)
|
ssf.streamFormat.writeRTSP(ssf.streamFormat.media, u.RTPPackets, u.NTP)
|
||||||
|
|
||||||
@@ -178,8 +178,8 @@ func (ssf *subStreamFormat) writeUnitInner(u *unit.Unit) error {
|
|||||||
csr := sr
|
csr := sr
|
||||||
cOnData := onData
|
cOnData := onData
|
||||||
sr.push(func() error {
|
sr.push(func() error {
|
||||||
if !csr.SkipBytesSent {
|
if !csr.SkipOutboundBytes {
|
||||||
ssf.streamFormat.addOutboundBytes(size)
|
ssf.streamFormat.outboundBytes.Add(size)
|
||||||
}
|
}
|
||||||
return cOnData(u)
|
return cOnData(u)
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user