136 lines
3.8 KiB
Go
136 lines
3.8 KiB
Go
package mediamtx
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type Source struct {
|
|
Path string
|
|
URI string
|
|
Username string
|
|
Password string
|
|
}
|
|
type PathStatus struct {
|
|
Name string `json:"name"`
|
|
Exists bool `json:"exists"`
|
|
Ready bool `json:"ready"`
|
|
Readers int `json:"readers"`
|
|
BytesReceived int64 `json:"bytes_received"`
|
|
}
|
|
type Controller interface {
|
|
Apply(context.Context, Source) error
|
|
Status(context.Context, string) (PathStatus, error)
|
|
}
|
|
type HTTPController struct {
|
|
base string
|
|
client *http.Client
|
|
}
|
|
|
|
func NewHTTPController(base string) *HTTPController {
|
|
return &HTTPController{base: strings.TrimRight(base, "/"), client: &http.Client{Timeout: 5 * time.Second}}
|
|
}
|
|
func (c *HTTPController) Apply(ctx context.Context, source Source) error {
|
|
parsed, err := url.Parse(source.URI)
|
|
if err != nil || parsed.User != nil {
|
|
return fmt.Errorf("invalid credential-free media source")
|
|
}
|
|
if source.Username != "" {
|
|
parsed.User = url.UserPassword(source.Username, source.Password)
|
|
}
|
|
payload := map[string]any{"source": parsed.String(), "sourceOnDemand": true, "rtspTransport": "tcp"}
|
|
data, _ := json.Marshal(payload)
|
|
configured, err := c.configured(ctx, source.Path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
action := "add"
|
|
if configured {
|
|
action = "replace"
|
|
}
|
|
endpoint := c.base + "/v3/config/paths/" + action + "/" + url.PathEscape(source.Path)
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(data))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
res, err := c.client.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("mediamtx control unavailable: %w", err)
|
|
}
|
|
defer res.Body.Close()
|
|
io.Copy(io.Discard, io.LimitReader(res.Body, 1<<20))
|
|
if res.StatusCode < 200 || res.StatusCode >= 300 {
|
|
return fmt.Errorf("mediamtx apply returned %d", res.StatusCode)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *HTTPController) configured(ctx context.Context, path string) (bool, error) {
|
|
endpoint := c.base + "/v3/config/paths/get/" + url.PathEscape(path)
|
|
deadline := time.Now().Add(5 * time.Second)
|
|
var res *http.Response
|
|
for {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
res, err = c.client.Do(req)
|
|
if err == nil {
|
|
break
|
|
}
|
|
if !time.Now().Before(deadline) {
|
|
return false, fmt.Errorf("mediamtx control unavailable: %w", err)
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return false, ctx.Err()
|
|
case <-time.After(100 * time.Millisecond):
|
|
}
|
|
}
|
|
defer res.Body.Close()
|
|
io.Copy(io.Discard, io.LimitReader(res.Body, 1<<20))
|
|
if res.StatusCode == http.StatusNotFound {
|
|
return false, nil
|
|
}
|
|
if res.StatusCode < 200 || res.StatusCode >= 300 {
|
|
return false, fmt.Errorf("mediamtx config lookup returned %d", res.StatusCode)
|
|
}
|
|
return true, nil
|
|
}
|
|
func (c *HTTPController) Status(ctx context.Context, path string) (PathStatus, error) {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.base+"/v3/paths/get/"+url.PathEscape(path), nil)
|
|
if err != nil {
|
|
return PathStatus{}, err
|
|
}
|
|
res, err := c.client.Do(req)
|
|
if err != nil {
|
|
return PathStatus{}, err
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode == http.StatusNotFound {
|
|
return PathStatus{Name: path, Exists: false}, nil
|
|
}
|
|
if res.StatusCode < 200 || res.StatusCode >= 300 {
|
|
return PathStatus{}, fmt.Errorf("mediamtx status returned %d", res.StatusCode)
|
|
}
|
|
var raw struct {
|
|
Name string `json:"name"`
|
|
Ready bool `json:"ready"`
|
|
BytesReceived int64 `json:"bytesReceived"`
|
|
Readers []any `json:"readers"`
|
|
}
|
|
if err := json.NewDecoder(io.LimitReader(res.Body, 1<<20)).Decode(&raw); err != nil {
|
|
return PathStatus{}, err
|
|
}
|
|
return PathStatus{Name: raw.Name, Exists: true, Ready: raw.Ready, Readers: len(raw.Readers), BytesReceived: raw.BytesReceived}, nil
|
|
}
|
|
|