149 lines
4.9 KiB
Go
149 lines
4.9 KiB
Go
package liveview
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"net/url"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"yovision.local/sense/app/sense/admission"
|
|
"yovision.local/sense/app/sense/device"
|
|
"yovision.local/sense/app/sense/media"
|
|
)
|
|
|
|
type Route struct {
|
|
ID string `json:"id"`
|
|
DeviceID string `json:"device_id"`
|
|
DeviceName string `json:"device_name"`
|
|
DeviceLocation string `json:"device_location"`
|
|
ProfileToken string `json:"profile_token"`
|
|
ProfileName string `json:"profile_name"`
|
|
ProfileKind string `json:"profile_kind"`
|
|
Desired string `json:"desired"`
|
|
Actual string `json:"actual"`
|
|
Detail string `json:"detail"`
|
|
Readers int `json:"readers"`
|
|
}
|
|
|
|
type Session struct {
|
|
ID string `json:"id"`
|
|
OwnerID string `json:"-"`
|
|
RouteID string `json:"route_id"`
|
|
DeviceID string `json:"device_id"`
|
|
DeviceName string `json:"device_name"`
|
|
ProfileToken string `json:"profile_token"`
|
|
ProfileName string `json:"profile_name"`
|
|
ProfileKind string `json:"profile_kind"`
|
|
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)
|
|
refresh 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, refresh: media.RefreshPlaybackRoute, routes: media.PlaybackRoutes, sessions: map[string]Session{}, now: time.Now}, nil
|
|
}
|
|
func (s *Service) Routes(ctx context.Context) ([]Route, error) {
|
|
items, err := s.routes(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result := make([]Route, 0, len(items))
|
|
for _, item := range items {
|
|
if item.Desired == "running" {
|
|
view := Route{ID: item.ID, DeviceID: item.DeviceID, DeviceName: item.DeviceID, ProfileToken: item.ProfileToken, ProfileName: item.ProfileToken, Desired: item.Desired, Actual: item.Actual, Detail: item.Detail, Readers: item.Readers}
|
|
if info, describeErr := device.Describe(ctx, item.DeviceID); describeErr == nil {
|
|
view.DeviceName = info.Name
|
|
view.DeviceLocation = info.Location
|
|
}
|
|
if profile, profileErr := admission.VerifiedProfile(item.DeviceID, item.ProfileToken); profileErr == nil {
|
|
view.ProfileName = profile.Name
|
|
view.ProfileKind = profile.Kind
|
|
}
|
|
result = append(result, view)
|
|
}
|
|
}
|
|
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, DeviceName: route.DeviceID, ProfileToken: route.ProfileToken, ProfileName: route.ProfileToken, PlayerURL: "/api/v1/liveview/sessions/" + id + "/player", Status: route.Actual, Detail: route.Detail, ExpiresAt: s.now().UTC().Add(s.ttl)}
|
|
if info, describeErr := device.Describe(ctx, route.DeviceID); describeErr == nil {
|
|
session.DeviceName = info.Name
|
|
}
|
|
if profile, profileErr := admission.VerifiedProfile(route.DeviceID, route.ProfileToken); profileErr == nil {
|
|
session.ProfileName = profile.Name
|
|
session.ProfileKind = profile.Kind
|
|
}
|
|
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.refresh(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)
|
|
}
|