201 lines
7.0 KiB
Go
201 lines
7.0 KiB
Go
package mtx
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"sort"
|
|
"strings"
|
|
|
|
mediamtxapi "yovision/sense/internal/mtx/generated"
|
|
)
|
|
|
|
var ErrPathNotFound = errors.New("MediaMTX path not found")
|
|
|
|
type APIError struct {
|
|
Operation string
|
|
StatusCode int
|
|
}
|
|
|
|
func (e *APIError) Error() string {
|
|
return fmt.Sprintf("MediaMTX %s failed with HTTP status %d", e.Operation, e.StatusCode)
|
|
}
|
|
|
|
type PathConfig struct {
|
|
Name string
|
|
Source string
|
|
}
|
|
|
|
type pathAPI interface {
|
|
ConfigPathsAddWithResponse(context.Context, string, mediamtxapi.ConfigPathsAddJSONRequestBody, ...mediamtxapi.RequestEditorFn) (*mediamtxapi.ConfigPathsAddResponse, error)
|
|
ConfigPathsGetWithResponse(context.Context, string, ...mediamtxapi.RequestEditorFn) (*mediamtxapi.ConfigPathsGetResponse, error)
|
|
ConfigPathsPatchWithResponse(context.Context, string, mediamtxapi.ConfigPathsPatchJSONRequestBody, ...mediamtxapi.RequestEditorFn) (*mediamtxapi.ConfigPathsPatchResponse, error)
|
|
ConfigPathsDeleteWithResponse(context.Context, string, ...mediamtxapi.RequestEditorFn) (*mediamtxapi.ConfigPathsDeleteResponse, error)
|
|
ConfigPathsListWithResponse(context.Context, *mediamtxapi.ConfigPathsListParams, ...mediamtxapi.RequestEditorFn) (*mediamtxapi.ConfigPathsListResponse, error)
|
|
PathsGetWithResponse(context.Context, string, ...mediamtxapi.RequestEditorFn) (*mediamtxapi.PathsGetResponse, error)
|
|
}
|
|
|
|
// ListPathNames enumerates only configuration names. Sources are deliberately
|
|
// discarded so inventory and orphan reports cannot expose stream URIs.
|
|
func (c *Client) ListPathNames(ctx context.Context) ([]string, error) {
|
|
const (
|
|
itemsPerPage = 100
|
|
maxPages = 1000
|
|
)
|
|
result := make(map[string]struct{})
|
|
seenPages := make(map[string]struct{})
|
|
for page := 0; page < maxPages; page++ {
|
|
pageValue, limitValue := page, itemsPerPage
|
|
response, err := c.api.ConfigPathsListWithResponse(ctx, &mediamtxapi.ConfigPathsListParams{
|
|
Page: &pageValue, ItemsPerPage: &limitValue,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("MediaMTX list paths transport: %w", err)
|
|
}
|
|
if response.StatusCode() != http.StatusOK || response.JSON200 == nil ||
|
|
response.JSON200.PageCount == nil || response.JSON200.Items == nil {
|
|
return nil, &APIError{Operation: "list paths", StatusCode: response.StatusCode()}
|
|
}
|
|
pageCount := *response.JSON200.PageCount
|
|
if pageCount < 0 || pageCount > maxPages {
|
|
return nil, &APIError{Operation: "list paths pagination", StatusCode: response.StatusCode()}
|
|
}
|
|
pageNames := make([]string, 0, len(*response.JSON200.Items))
|
|
for _, item := range *response.JSON200.Items {
|
|
if item.Name == nil || strings.TrimSpace(*item.Name) == "" {
|
|
return nil, &APIError{Operation: "list paths response", StatusCode: response.StatusCode()}
|
|
}
|
|
pageNames = append(pageNames, *item.Name)
|
|
result[*item.Name] = struct{}{}
|
|
}
|
|
sort.Strings(pageNames)
|
|
signature := strings.Join(pageNames, "\x00")
|
|
if page > 0 && signature != "" {
|
|
if _, duplicate := seenPages[signature]; duplicate {
|
|
return nil, &APIError{Operation: "list paths repeated page", StatusCode: response.StatusCode()}
|
|
}
|
|
}
|
|
seenPages[signature] = struct{}{}
|
|
if int64(page+1) >= pageCount {
|
|
values := make([]string, 0, len(result))
|
|
for name := range result {
|
|
values = append(values, name)
|
|
}
|
|
sort.Strings(values)
|
|
return values, nil
|
|
}
|
|
}
|
|
return nil, &APIError{Operation: "list paths page limit", StatusCode: http.StatusOK}
|
|
}
|
|
|
|
type Client struct {
|
|
api pathAPI
|
|
}
|
|
|
|
func NewClient(baseURL string, httpClient *http.Client) (*Client, error) {
|
|
options := make([]mediamtxapi.ClientOption, 0, 1)
|
|
if httpClient != nil {
|
|
options = append(options, mediamtxapi.WithHTTPClient(httpClient))
|
|
}
|
|
generated, err := mediamtxapi.NewClientWithResponses(strings.TrimRight(baseURL, "/"), options...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create MediaMTX client: %w", err)
|
|
}
|
|
return &Client{api: generated}, nil
|
|
}
|
|
|
|
func newClientWithAPI(api pathAPI) *Client {
|
|
return &Client{api: api}
|
|
}
|
|
|
|
func (c *Client) CreatePath(ctx context.Context, name, source string) error {
|
|
response, err := c.api.ConfigPathsAddWithResponse(ctx, name, mediamtxapi.PathConf{Source: &source})
|
|
if err != nil {
|
|
return fmt.Errorf("MediaMTX create path transport: %w", err)
|
|
}
|
|
if response.StatusCode() != http.StatusOK {
|
|
return &APIError{Operation: "create path", StatusCode: response.StatusCode()}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) GetPath(ctx context.Context, name string) (PathConfig, error) {
|
|
response, err := c.api.ConfigPathsGetWithResponse(ctx, name)
|
|
if err != nil {
|
|
return PathConfig{}, fmt.Errorf("MediaMTX read path transport: %w", err)
|
|
}
|
|
if response.StatusCode() == http.StatusNotFound {
|
|
return PathConfig{}, ErrPathNotFound
|
|
}
|
|
if response.StatusCode() != http.StatusOK || response.JSON200 == nil {
|
|
return PathConfig{}, &APIError{Operation: "read path", StatusCode: response.StatusCode()}
|
|
}
|
|
result := PathConfig{Name: name}
|
|
if response.JSON200.Name != nil {
|
|
result.Name = *response.JSON200.Name
|
|
}
|
|
if response.JSON200.Source != nil {
|
|
result.Source = *response.JSON200.Source
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (c *Client) DeletePath(ctx context.Context, name string) error {
|
|
response, err := c.api.ConfigPathsDeleteWithResponse(ctx, name)
|
|
if err != nil {
|
|
return fmt.Errorf("MediaMTX delete path transport: %w", err)
|
|
}
|
|
if response.StatusCode() == http.StatusNotFound {
|
|
// Deletion is an idempotent convergence operation. A missing exact path
|
|
// already satisfies the disabled desired state.
|
|
return nil
|
|
}
|
|
if response.StatusCode() != http.StatusOK {
|
|
return &APIError{Operation: "delete path", StatusCode: response.StatusCode()}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// EnsurePath converges one desired path. It never enumerates or deletes orphans.
|
|
func (c *Client) EnsurePath(ctx context.Context, name, source string) (bool, error) {
|
|
current, err := c.GetPath(ctx, name)
|
|
if errors.Is(err, ErrPathNotFound) {
|
|
if err := c.CreatePath(ctx, name, source); err != nil {
|
|
return false, err
|
|
}
|
|
return true, nil
|
|
}
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if current.Source == source {
|
|
return false, nil
|
|
}
|
|
response, err := c.api.ConfigPathsPatchWithResponse(ctx, name, mediamtxapi.PathConf{Source: &source})
|
|
if err != nil {
|
|
return false, fmt.Errorf("MediaMTX patch path transport: %w", err)
|
|
}
|
|
if response.StatusCode() != http.StatusOK {
|
|
return false, &APIError{Operation: "patch path", StatusCode: response.StatusCode()}
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func (c *Client) PathReady(ctx context.Context, name string) (bool, error) {
|
|
response, err := c.api.PathsGetWithResponse(ctx, name)
|
|
if err != nil {
|
|
return false, fmt.Errorf("MediaMTX probe path transport: %w", err)
|
|
}
|
|
if response.StatusCode() == http.StatusNotFound {
|
|
return false, ErrPathNotFound
|
|
}
|
|
if response.StatusCode() != http.StatusOK || response.JSON200 == nil {
|
|
return false, &APIError{Operation: "probe path", StatusCode: response.StatusCode()}
|
|
}
|
|
if response.JSON200.Online == nil || response.JSON200.Available == nil {
|
|
return false, &APIError{Operation: "probe path response", StatusCode: response.StatusCode()}
|
|
}
|
|
return *response.JSON200.Online && *response.JSON200.Available, nil
|
|
}
|