113 lines
3.2 KiB
Go
113 lines
3.2 KiB
Go
package liveview
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"net/url"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"yovision.local/sense/app/sense/media"
|
|
)
|
|
|
|
type Session struct {
|
|
ID string `json:"id"`
|
|
OwnerID string `json:"-"`
|
|
RouteID string `json:"route_id"`
|
|
DeviceID string `json:"device_id"`
|
|
ProfileToken string `json:"profile_token"`
|
|
PlayerURL string `json:"player_url"`
|
|
Status string `json:"status"`
|
|
Detail string `json:"detail"`
|
|
ExpiresAt time.Time `json:"expires_at"`
|
|
}
|
|
type Service struct {
|
|
base *url.URL
|
|
ttl time.Duration
|
|
route func(context.Context, string) (media.Route, error)
|
|
routes func(context.Context) ([]media.Route, error)
|
|
mu sync.RWMutex
|
|
sessions map[string]Session
|
|
now func() time.Time
|
|
}
|
|
|
|
func NewService(rawBase string, ttl time.Duration) (*Service, error) {
|
|
parsed, err := url.Parse(strings.TrimRight(rawBase, "/"))
|
|
if err != nil || parsed.Scheme == "" || parsed.Host == "" || parsed.User != nil {
|
|
return nil, fmt.Errorf("invalid MediaMTX WebRTC base URL")
|
|
}
|
|
if parsed.Scheme != "http" && parsed.Scheme != "https" {
|
|
return nil, fmt.Errorf("unsupported MediaMTX WebRTC scheme")
|
|
}
|
|
if ttl <= 0 || ttl > 10*time.Minute {
|
|
ttl = 2 * time.Minute
|
|
}
|
|
return &Service{base: parsed, ttl: ttl, route: media.PlaybackRoute, routes: media.PlaybackRoutes, sessions: map[string]Session{}, now: time.Now}, nil
|
|
}
|
|
func (s *Service) Routes(ctx context.Context) ([]media.Route, error) {
|
|
items, err := s.routes(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result := items[:0]
|
|
for _, item := range items {
|
|
if item.Desired == "running" {
|
|
result = append(result, item)
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
func (s *Service) Create(ctx context.Context, owner, routeID string) (Session, error) {
|
|
route, err := s.route(ctx, routeID)
|
|
if err != nil {
|
|
return Session{}, err
|
|
}
|
|
if route.Desired != "running" {
|
|
return Session{}, fmt.Errorf("media route is stopped")
|
|
}
|
|
id := newID()
|
|
session := Session{ID: id, OwnerID: owner, RouteID: route.ID, DeviceID: route.DeviceID, ProfileToken: route.ProfileToken, PlayerURL: "/api/v1/liveview/sessions/" + id + "/player", Status: route.Actual, Detail: route.Detail, ExpiresAt: s.now().UTC().Add(s.ttl)}
|
|
s.mu.Lock()
|
|
s.sessions[id] = session
|
|
s.mu.Unlock()
|
|
return session, nil
|
|
}
|
|
func (s *Service) Get(ctx context.Context, owner, id string) (Session, error) {
|
|
s.mu.RLock()
|
|
session, ok := s.sessions[id]
|
|
s.mu.RUnlock()
|
|
if !ok || session.OwnerID != owner || !s.now().Before(session.ExpiresAt) {
|
|
return Session{}, fmt.Errorf("playback session expired")
|
|
}
|
|
route, err := s.route(ctx, session.RouteID)
|
|
if err != nil {
|
|
return Session{}, err
|
|
}
|
|
session.Status = route.Actual
|
|
session.Detail = route.Detail
|
|
return session, nil
|
|
}
|
|
func (s *Service) EmbedURL(ctx context.Context, owner, id string) (string, error) {
|
|
session, err := s.Get(ctx, owner, id)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
route, err := s.route(ctx, session.RouteID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
target := *s.base
|
|
target.Path = strings.TrimRight(target.Path, "/") + "/" + route.Path
|
|
return target.String(), nil
|
|
}
|
|
func newID() string {
|
|
b := make([]byte, 16)
|
|
if _, err := rand.Read(b); err != nil {
|
|
panic(err)
|
|
}
|
|
return "view_" + hex.EncodeToString(b)
|
|
}
|