658 lines
18 KiB
Go
658 lines
18 KiB
Go
package gateway
|
|
|
|
import (
|
|
"context"
|
|
"encoding/xml"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
protocol "git.sechmachine.io.vn/sechmachine/VerseVDI-Protocol/gen/go/protocol"
|
|
)
|
|
|
|
const (
|
|
ProviderProfileApollo = "apollo"
|
|
ProviderStateStarting = "starting"
|
|
ProviderStateReady = "ready"
|
|
ProviderStateDisconnected = "disconnected"
|
|
ProviderStateTerminating = "terminating"
|
|
ProviderStateTerminated = "terminated"
|
|
ProviderStateCleanup = "cleanup_pending"
|
|
ProviderStateFailed = "failed"
|
|
)
|
|
|
|
var (
|
|
ErrProviderIdentity = errors.New("provider identity rejected")
|
|
ErrProviderMalformed = errors.New("provider response malformed")
|
|
ErrProviderTimeout = errors.New("provider operation timed out")
|
|
ErrProviderDisconnected = errors.New("provider disconnected")
|
|
ErrProviderCleanup = errors.New("provider cleanup pending")
|
|
ErrProviderTerminated = errors.New("provider session terminated")
|
|
)
|
|
|
|
type ProviderIdentity struct {
|
|
UniqueID string
|
|
Fingerprint string
|
|
NotBefore time.Time
|
|
NotAfter time.Time
|
|
}
|
|
|
|
func (i ProviderIdentity) Key() string {
|
|
return i.UniqueID + "#" + i.Fingerprint
|
|
}
|
|
|
|
func providerIdentityFromKey(value string) (ProviderIdentity, bool) {
|
|
uniqueID, fingerprint, ok := strings.Cut(strings.TrimSpace(value), "#")
|
|
if !ok || uniqueID == "" || fingerprint == "" || strings.Contains(fingerprint, "#") || len(uniqueID) > 128 || len(fingerprint) > 256 {
|
|
return ProviderIdentity{}, false
|
|
}
|
|
return ProviderIdentity{UniqueID: uniqueID, Fingerprint: fingerprint}, true
|
|
}
|
|
|
|
func (i ProviderIdentity) Validate(now time.Time, expected ProviderIdentity) error {
|
|
if i.UniqueID == "" || expected.UniqueID == "" || i.UniqueID != expected.UniqueID ||
|
|
(i.Fingerprint != "" && i.Fingerprint != expected.Fingerprint) {
|
|
return ErrProviderIdentity
|
|
}
|
|
if !i.NotBefore.IsZero() && now.Before(i.NotBefore) {
|
|
return ErrProviderIdentity
|
|
}
|
|
if !i.NotAfter.IsZero() && !now.Before(i.NotAfter) {
|
|
return ErrProviderIdentity
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type ManagementInfo struct {
|
|
Identity ProviderIdentity
|
|
Name string
|
|
ServerCodecModeSupport uint32
|
|
MaxLumaPixelsHEVC uint64
|
|
HasServerCodecModeSupport bool
|
|
HasMaxLumaPixelsHEVC bool
|
|
}
|
|
|
|
func ParseManagementXML(data []byte) (ManagementInfo, error) {
|
|
if len(data) == 0 || len(data) > 64*1024 {
|
|
return ManagementInfo{}, ErrProviderMalformed
|
|
}
|
|
var document struct {
|
|
XMLName xml.Name `xml:"root"`
|
|
UniqueID string `xml:"uniqueid"`
|
|
LegacyID string `xml:"unique_id"`
|
|
Fingerprint string `xml:"fingerprint"`
|
|
NotBefore string `xml:"not_before"`
|
|
NotAfter string `xml:"not_after"`
|
|
Name string `xml:"name"`
|
|
CodecModes string `xml:"ServerCodecModeSupport"`
|
|
MaxHEVCLuma string `xml:"MaxLumaPixelsHEVC"`
|
|
}
|
|
decoder := xml.NewDecoder(strings.NewReader(string(data)))
|
|
decoder.Strict = true
|
|
if err := decoder.Decode(&document); err != nil {
|
|
return ManagementInfo{}, fmt.Errorf("%w: %v", ErrProviderMalformed, err)
|
|
}
|
|
if document.UniqueID == "" {
|
|
document.UniqueID = document.LegacyID
|
|
}
|
|
identity := ProviderIdentity{UniqueID: document.UniqueID, Fingerprint: document.Fingerprint}
|
|
var err error
|
|
if document.NotBefore != "" {
|
|
identity.NotBefore, err = time.Parse(time.RFC3339Nano, document.NotBefore)
|
|
if err != nil {
|
|
return ManagementInfo{}, ErrProviderMalformed
|
|
}
|
|
}
|
|
if document.NotAfter != "" {
|
|
identity.NotAfter, err = time.Parse(time.RFC3339Nano, document.NotAfter)
|
|
if err != nil {
|
|
return ManagementInfo{}, ErrProviderMalformed
|
|
}
|
|
}
|
|
if identity.UniqueID == "" || len(identity.UniqueID) > 128 || len(identity.Fingerprint) > 256 {
|
|
return ManagementInfo{}, ErrProviderMalformed
|
|
}
|
|
info := ManagementInfo{Identity: identity, Name: document.Name}
|
|
if document.CodecModes != "" {
|
|
value, parseErr := strconv.ParseUint(document.CodecModes, 10, 32)
|
|
if parseErr != nil {
|
|
return ManagementInfo{}, ErrProviderMalformed
|
|
}
|
|
info.ServerCodecModeSupport = uint32(value)
|
|
info.HasServerCodecModeSupport = true
|
|
}
|
|
if document.MaxHEVCLuma != "" {
|
|
value, parseErr := strconv.ParseUint(document.MaxHEVCLuma, 10, 64)
|
|
if parseErr != nil {
|
|
return ManagementInfo{}, ErrProviderMalformed
|
|
}
|
|
info.MaxLumaPixelsHEVC = value
|
|
info.HasMaxLumaPixelsHEVC = true
|
|
}
|
|
return info, nil
|
|
}
|
|
|
|
type RTSPResponse struct {
|
|
StatusCode int
|
|
Session string
|
|
Transport string
|
|
}
|
|
|
|
func ParseRTSPResponse(data []byte) (RTSPResponse, error) {
|
|
if len(data) == 0 || len(data) > 16*1024 {
|
|
return RTSPResponse{}, ErrProviderMalformed
|
|
}
|
|
text := string(data)
|
|
if !strings.Contains(text, "\r\n") {
|
|
return RTSPResponse{}, ErrProviderMalformed
|
|
}
|
|
lines := strings.Split(text, "\r\n")
|
|
if len(lines) < 2 {
|
|
return RTSPResponse{}, ErrProviderMalformed
|
|
}
|
|
statusParts := strings.SplitN(lines[0], " ", 3)
|
|
if len(statusParts) < 2 || statusParts[0] != "RTSP/1.0" {
|
|
return RTSPResponse{}, ErrProviderMalformed
|
|
}
|
|
var response RTSPResponse
|
|
if _, err := fmt.Sscanf(statusParts[1], "%d", &response.StatusCode); err != nil || response.StatusCode != 200 {
|
|
return RTSPResponse{}, ErrProviderMalformed
|
|
}
|
|
for _, line := range lines[1:] {
|
|
if line == "" {
|
|
break
|
|
}
|
|
key, value, ok := strings.Cut(line, ":")
|
|
if !ok {
|
|
return RTSPResponse{}, ErrProviderMalformed
|
|
}
|
|
switch strings.ToLower(strings.TrimSpace(key)) {
|
|
case "session":
|
|
response.Session = strings.TrimSpace(value)
|
|
case "transport":
|
|
response.Transport = strings.TrimSpace(value)
|
|
}
|
|
}
|
|
if response.Session == "" || response.Transport == "" || len(response.Session) > 256 || len(response.Transport) > 1024 {
|
|
return RTSPResponse{}, ErrProviderMalformed
|
|
}
|
|
return response, nil
|
|
}
|
|
|
|
type LaunchRequest struct {
|
|
SessionID string
|
|
Capabilities protocol.CapabilityProfile
|
|
ProviderProfile string
|
|
ProviderIdentity string
|
|
ProviderWork protocol.ProviderSessionWork
|
|
}
|
|
|
|
type InputEvent struct {
|
|
Sequence uint32
|
|
Device string
|
|
Code int32
|
|
Pressed bool
|
|
Payload []byte
|
|
}
|
|
|
|
type Feedback struct {
|
|
Sequence uint32
|
|
Kind FeedbackKind
|
|
Payload []byte
|
|
}
|
|
|
|
type ProviderEventKind uint8
|
|
|
|
const (
|
|
ProviderEventTerminated ProviderEventKind = iota + 1
|
|
ProviderEventRumble
|
|
ProviderEventHDR
|
|
ProviderEventDisconnected
|
|
)
|
|
|
|
type ProviderEvent struct {
|
|
Kind ProviderEventKind
|
|
Payload []byte
|
|
}
|
|
|
|
// ProviderTelemetry holds measured provider-channel state only; it never
|
|
// contains provider routes, credentials, or payload bytes.
|
|
type ProviderTelemetry struct {
|
|
State string
|
|
ControlRTT time.Duration
|
|
ControlJitter time.Duration
|
|
ReliableSent uint64
|
|
ReliableRetransmits uint64
|
|
PendingReliable uint64
|
|
MediaDrops uint64
|
|
}
|
|
|
|
type ProviderMedia struct {
|
|
Payload []byte
|
|
ReceivedAt time.Time
|
|
EnqueuedAt time.Time
|
|
queueID uint64
|
|
expiry *time.Timer
|
|
accounting *providerMediaQueueAccounting
|
|
}
|
|
|
|
type providerMediaQueueAccounting struct {
|
|
released atomic.Bool
|
|
bytes int64
|
|
total *atomic.Int64
|
|
}
|
|
|
|
func (media ProviderMedia) releaseQueue() {
|
|
if media.accounting != nil && media.accounting.released.CompareAndSwap(false, true) {
|
|
media.accounting.total.Add(-media.accounting.bytes)
|
|
}
|
|
}
|
|
|
|
type Provider interface {
|
|
Start(context.Context, LaunchRequest) (ProviderSession, error)
|
|
}
|
|
|
|
type ProviderSession interface {
|
|
Ready(context.Context) error
|
|
Video() <-chan ProviderMedia
|
|
Audio() <-chan ProviderMedia
|
|
Events() <-chan ProviderEvent
|
|
Input(context.Context, InputEvent) error
|
|
Feedback(context.Context, Feedback) error
|
|
ReadClipboard(context.Context) (string, error)
|
|
WriteClipboard(context.Context, string) error
|
|
Telemetry() ProviderTelemetry
|
|
ReleaseAll(context.Context) error
|
|
Terminate(context.Context) error
|
|
State() protocol.ProviderState
|
|
}
|
|
|
|
type ApolloBackend interface {
|
|
Management(context.Context, LaunchRequest) ([]byte, error)
|
|
Setup(context.Context, LaunchRequest, []byte) ([]byte, error)
|
|
Open(context.Context, LaunchRequest, RTSPResponse) (ProviderSession, error)
|
|
}
|
|
|
|
type ApolloAdapter struct {
|
|
backend ApolloBackend
|
|
expected ProviderIdentity
|
|
now func() time.Time
|
|
readyTTL time.Duration
|
|
}
|
|
|
|
func NewApolloAdapter(backend ApolloBackend, expected ProviderIdentity) *ApolloAdapter {
|
|
return &ApolloAdapter{backend: backend, expected: expected, now: time.Now, readyTTL: 2 * time.Second}
|
|
}
|
|
|
|
func (a *ApolloAdapter) Start(ctx context.Context, request LaunchRequest) (ProviderSession, error) {
|
|
if a == nil || a.backend == nil || request.ProviderProfile != ProviderProfileApollo {
|
|
return nil, ErrProviderIdentity
|
|
}
|
|
management, err := a.backend.Management(ctx, request)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
info, err := ParseManagementXML(management)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
expected := a.expected
|
|
if request.ProviderWork.ProviderIdentity != "" {
|
|
parsed, ok := providerIdentityFromKey(request.ProviderWork.ProviderIdentity)
|
|
if !ok {
|
|
return nil, ErrProviderIdentity
|
|
}
|
|
expected = parsed
|
|
}
|
|
if err := info.Identity.Validate(a.now(), expected); err != nil {
|
|
return nil, err
|
|
}
|
|
if request.ProviderIdentity != "" && info.Identity.UniqueID != expected.UniqueID {
|
|
return nil, ErrProviderIdentity
|
|
}
|
|
rawRTSP, err := a.backend.Setup(ctx, request, management)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rtsp, err := ParseRTSPResponse(rawRTSP)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
session, err := a.backend.Open(ctx, request, rtsp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
readyCtx, cancel := context.WithTimeout(ctx, a.readyTTL)
|
|
defer cancel()
|
|
if err := session.Ready(readyCtx); err != nil {
|
|
cleanupCtx, cleanupCancel := context.WithTimeout(context.Background(), time.Second)
|
|
cleanupErr := session.Terminate(cleanupCtx)
|
|
cleanupCancel()
|
|
if cleanupErr != nil {
|
|
return nil, fmt.Errorf("%w: %v", ErrProviderCleanup, err)
|
|
}
|
|
if errors.Is(err, context.DeadlineExceeded) {
|
|
return nil, ErrProviderTimeout
|
|
}
|
|
return nil, err
|
|
}
|
|
return session, nil
|
|
}
|
|
|
|
type FakeFailure string
|
|
|
|
const (
|
|
FakeFailureNone FakeFailure = ""
|
|
FakeFailureIdentity FakeFailure = "identity"
|
|
FakeFailureMalformed FakeFailure = "malformed"
|
|
FakeFailureReadinessTimeout FakeFailure = "readiness-timeout"
|
|
FakeFailureProviderDisconnect FakeFailure = "provider-disconnect"
|
|
FakeFailureTerminationTimeout FakeFailure = "termination-timeout"
|
|
)
|
|
|
|
type FakeApolloConfig struct {
|
|
Identity ProviderIdentity
|
|
Failure FakeFailure
|
|
Video [][]byte
|
|
Audio [][]byte
|
|
Now time.Time
|
|
}
|
|
|
|
type FakeApollo struct {
|
|
config FakeApolloConfig
|
|
mu sync.Mutex
|
|
last *fakeSession
|
|
}
|
|
|
|
func NewFakeApollo(config FakeApolloConfig) *FakeApollo {
|
|
if config.Identity.UniqueID == "" {
|
|
config.Identity.UniqueID = "apollo-fixture-1"
|
|
}
|
|
if config.Identity.Fingerprint == "" {
|
|
config.Identity.Fingerprint = "sha256:fixture-apollo-1"
|
|
}
|
|
if config.Now.IsZero() {
|
|
config.Now = time.Date(2026, time.January, 1, 0, 0, 0, 0, time.UTC)
|
|
}
|
|
if len(config.Video) == 0 {
|
|
config.Video = [][]byte{[]byte{0x00, 0x00, 0x01, 0x65, 0x01, 0x02}}
|
|
}
|
|
if len(config.Audio) == 0 {
|
|
config.Audio = [][]byte{[]byte{0x4f, 0x70, 0x75, 0x73, 0x01}}
|
|
}
|
|
return &FakeApollo{config: config}
|
|
}
|
|
|
|
func (f *FakeApollo) Management(context.Context, LaunchRequest) ([]byte, error) {
|
|
if f.config.Failure == FakeFailureMalformed {
|
|
return []byte("<root>"), nil
|
|
}
|
|
identity := f.config.Identity
|
|
if f.config.Failure == FakeFailureIdentity {
|
|
identity.Fingerprint = "sha256:changed-fixture"
|
|
}
|
|
return []byte(fmt.Sprintf("<root><unique_id>%s</unique_id><fingerprint>%s</fingerprint><not_before>%s</not_before><not_after>%s</not_after><name>fixture-apollo</name></root>", identity.UniqueID, identity.Fingerprint, f.config.Now.Add(-time.Hour).Format(time.RFC3339), f.config.Now.Add(time.Hour).Format(time.RFC3339))), nil
|
|
}
|
|
|
|
func (f *FakeApollo) Setup(context.Context, LaunchRequest, []byte) ([]byte, error) {
|
|
if f.config.Failure == FakeFailureMalformed {
|
|
return []byte("RTSP/1.0 200 OK\r\n\r\n"), nil
|
|
}
|
|
return []byte("RTSP/1.0 200 OK\r\nSession: fixture-session\r\nTransport: unicast;server_port=43000\r\n\r\n"), nil
|
|
}
|
|
|
|
func (f *FakeApollo) Open(_ context.Context, request LaunchRequest, _ RTSPResponse) (ProviderSession, error) {
|
|
session := &fakeSession{
|
|
failure: f.config.Failure,
|
|
video: make(chan ProviderMedia, 16),
|
|
audio: make(chan ProviderMedia, 16),
|
|
events: make(chan ProviderEvent, 16),
|
|
clipboardWrites: make(chan string, 1),
|
|
state: protocol.ProviderState{Version: "1", SessionID: request.SessionID, State: ProviderStateStarting, Channels: []string{"video", "audio", "input", "feedback"}},
|
|
pressed: make(map[string]struct{}),
|
|
}
|
|
for _, payload := range f.config.Video {
|
|
session.EmitVideo(payload)
|
|
}
|
|
for _, payload := range f.config.Audio {
|
|
session.EmitAudio(payload)
|
|
}
|
|
f.mu.Lock()
|
|
f.last = session
|
|
f.mu.Unlock()
|
|
return session, nil
|
|
}
|
|
|
|
func (f *FakeApollo) Start(ctx context.Context, request LaunchRequest) (ProviderSession, error) {
|
|
adapter := NewApolloAdapter(f, f.config.Identity)
|
|
adapter.now = func() time.Time { return f.config.Now }
|
|
return adapter.Start(ctx, request)
|
|
}
|
|
|
|
func (f *FakeApollo) LastSession() ProviderSession {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
return f.last
|
|
}
|
|
|
|
func (f *FakeApollo) DisconnectProvider() {
|
|
f.mu.Lock()
|
|
session := f.last
|
|
f.mu.Unlock()
|
|
if session != nil {
|
|
session.Disconnect()
|
|
}
|
|
}
|
|
|
|
type fakeSession struct {
|
|
mu sync.Mutex
|
|
failure FakeFailure
|
|
video chan ProviderMedia
|
|
audio chan ProviderMedia
|
|
events chan ProviderEvent
|
|
state protocol.ProviderState
|
|
pressed map[string]struct{}
|
|
inputs []InputEvent
|
|
feedback []Feedback
|
|
clipboard string
|
|
clipboardWrites chan string
|
|
releaseAll int
|
|
closeOnce sync.Once
|
|
}
|
|
|
|
func (s *fakeSession) Ready(ctx context.Context) error {
|
|
s.mu.Lock()
|
|
if s.failure == FakeFailureReadinessTimeout {
|
|
s.mu.Unlock()
|
|
<-ctx.Done()
|
|
return ctx.Err()
|
|
}
|
|
defer s.mu.Unlock()
|
|
if s.state.State == ProviderStateDisconnected {
|
|
return ErrProviderDisconnected
|
|
}
|
|
s.state.State = ProviderStateReady
|
|
return nil
|
|
}
|
|
|
|
func (s *fakeSession) Video() <-chan ProviderMedia { return s.video }
|
|
func (s *fakeSession) Audio() <-chan ProviderMedia { return s.audio }
|
|
func (s *fakeSession) Events() <-chan ProviderEvent { return s.events }
|
|
|
|
func (s *fakeSession) EmitEvent(event ProviderEvent) {
|
|
select {
|
|
case s.events <- ProviderEvent{Kind: event.Kind, Payload: append([]byte(nil), event.Payload...)}:
|
|
default:
|
|
}
|
|
}
|
|
|
|
func (s *fakeSession) EmitVideo(payload []byte) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if s.state.State == ProviderStateTerminating || s.state.State == ProviderStateTerminated || s.state.State == ProviderStateDisconnected {
|
|
return
|
|
}
|
|
now := time.Now()
|
|
media := ProviderMedia{Payload: append([]byte(nil), payload...), ReceivedAt: now, EnqueuedAt: now}
|
|
select {
|
|
case s.video <- media:
|
|
default:
|
|
select {
|
|
case <-s.video:
|
|
default:
|
|
}
|
|
select {
|
|
case s.video <- media:
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *fakeSession) EmitAudio(payload []byte) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if s.state.State == ProviderStateTerminating || s.state.State == ProviderStateTerminated || s.state.State == ProviderStateDisconnected {
|
|
return
|
|
}
|
|
now := time.Now()
|
|
media := ProviderMedia{Payload: append([]byte(nil), payload...), ReceivedAt: now, EnqueuedAt: now}
|
|
select {
|
|
case s.audio <- media:
|
|
default:
|
|
select {
|
|
case <-s.audio:
|
|
default:
|
|
}
|
|
select {
|
|
case s.audio <- media:
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *fakeSession) Input(_ context.Context, event InputEvent) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if s.state.State != ProviderStateReady {
|
|
return ErrProviderDisconnected
|
|
}
|
|
s.inputs = append(s.inputs, event)
|
|
key := fmt.Sprintf("%s:%d", event.Device, event.Code)
|
|
if event.Pressed {
|
|
s.pressed[key] = struct{}{}
|
|
} else {
|
|
delete(s.pressed, key)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *fakeSession) Feedback(_ context.Context, feedback Feedback) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if s.state.State != ProviderStateReady {
|
|
return ErrProviderDisconnected
|
|
}
|
|
s.feedback = append(s.feedback, feedback)
|
|
return nil
|
|
}
|
|
|
|
func (s *fakeSession) ReadClipboard(ctx context.Context) (string, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return "", err
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if s.state.State != ProviderStateReady {
|
|
return "", ErrProviderDisconnected
|
|
}
|
|
return s.clipboard, nil
|
|
}
|
|
|
|
func (s *fakeSession) WriteClipboard(ctx context.Context, value string) error {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
s.mu.Lock()
|
|
if s.state.State != ProviderStateReady {
|
|
s.mu.Unlock()
|
|
return ErrProviderDisconnected
|
|
}
|
|
s.clipboard = value
|
|
s.mu.Unlock()
|
|
select {
|
|
case s.clipboardWrites <- value:
|
|
default:
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *fakeSession) ReleaseAll(_ context.Context) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if s.failure == FakeFailureProviderDisconnect {
|
|
return ErrProviderDisconnected
|
|
}
|
|
s.pressed = make(map[string]struct{})
|
|
s.releaseAll++
|
|
return nil
|
|
}
|
|
|
|
func (s *fakeSession) Terminate(ctx context.Context) error {
|
|
s.mu.Lock()
|
|
terminationTimeout := s.failure == FakeFailureTerminationTimeout
|
|
s.mu.Unlock()
|
|
if terminationTimeout {
|
|
<-ctx.Done()
|
|
s.mu.Lock()
|
|
s.state.State = ProviderStateCleanup
|
|
s.state.CleanupPending = true
|
|
s.mu.Unlock()
|
|
return ctx.Err()
|
|
}
|
|
s.mu.Lock()
|
|
if s.state.State == ProviderStateTerminated {
|
|
s.mu.Unlock()
|
|
return nil
|
|
}
|
|
disconnected := s.state.State == ProviderStateDisconnected
|
|
s.state.State = ProviderStateTerminating
|
|
s.closeOnce.Do(func() {
|
|
close(s.video)
|
|
close(s.audio)
|
|
})
|
|
if disconnected {
|
|
s.state.State = ProviderStateDisconnected
|
|
} else {
|
|
s.state.State = ProviderStateTerminated
|
|
}
|
|
s.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
func (s *fakeSession) State() protocol.ProviderState {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
return s.state
|
|
}
|
|
|
|
func (s *fakeSession) Telemetry() ProviderTelemetry {
|
|
return ProviderTelemetry{State: s.State().State}
|
|
}
|
|
|
|
func (s *fakeSession) Disconnect() {
|
|
s.mu.Lock()
|
|
s.state.State = ProviderStateDisconnected
|
|
s.mu.Unlock()
|
|
s.EmitEvent(ProviderEvent{Kind: ProviderEventDisconnected})
|
|
}
|
|
|
|
func (s *fakeSession) ReleaseCount() int {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
return s.releaseAll
|
|
}
|