package mtx import ( "context" "errors" "fmt" "net/http" "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) PathsGetWithResponse(context.Context, string, ...mediamtxapi.RequestEditorFn) (*mediamtxapi.PathsGetResponse, error) } 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 }