feat(data-plane): implement phase3c gateway
This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
protocol "git.sechmachine.io.vn/sechmachine/VerseVDI-Protocol/gen/go/protocol"
|
||||
)
|
||||
|
||||
// NativeApolloBackend keeps provider sockets inside the gateway process. The
|
||||
// RTSP endpoint is configuration owned by the gateway and is never serialized
|
||||
// into a client manifest or authority.
|
||||
type NativeApolloBackend struct {
|
||||
ManagementURL string
|
||||
RTSPAddress string
|
||||
RTSPURL string
|
||||
HTTPClient *http.Client
|
||||
Dialer *net.Dialer
|
||||
TLSConfig *tls.Config
|
||||
|
||||
mu sync.Mutex
|
||||
pending map[string]net.Conn
|
||||
}
|
||||
|
||||
func NewNativeApolloBackend(managementURL, rtspAddress, rtspURL string, client *http.Client) *NativeApolloBackend {
|
||||
if client == nil {
|
||||
client = &http.Client{Timeout: 5 * time.Second}
|
||||
}
|
||||
return &NativeApolloBackend{ManagementURL: managementURL, RTSPAddress: rtspAddress, RTSPURL: rtspURL, HTTPClient: client, Dialer: &net.Dialer{Timeout: 5 * time.Second}, pending: make(map[string]net.Conn)}
|
||||
}
|
||||
|
||||
func (b *NativeApolloBackend) Management(ctx context.Context) ([]byte, error) {
|
||||
if b.ManagementURL == "" {
|
||||
return nil, ErrProviderMalformed
|
||||
}
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, b.ManagementURL, nil)
|
||||
if err != nil {
|
||||
return nil, ErrProviderMalformed
|
||||
}
|
||||
response, err := b.HTTPClient.Do(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("provider management status %d", response.StatusCode)
|
||||
}
|
||||
return readBounded(response.Body, 64*1024)
|
||||
}
|
||||
|
||||
func (b *NativeApolloBackend) Setup(ctx context.Context, request LaunchRequest) ([]byte, error) {
|
||||
if b.RTSPAddress == "" || b.RTSPURL == "" || request.SessionID == "" {
|
||||
return nil, ErrProviderMalformed
|
||||
}
|
||||
conn, err := b.Dialer.DialContext(ctx, "tcp", b.RTSPAddress)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
_ = conn.SetDeadline(deadline)
|
||||
}
|
||||
requestText := "SETUP " + b.RTSPURL + " RTSP/1.0\r\nCSeq: 1\r\nTransport: RTP/AVP/TCP;interleaved=0-1\r\nSession: " + request.SessionID + "\r\n\r\n"
|
||||
if _, err := io.WriteString(conn, requestText); err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
response, err := readRTSPHeaders(conn, 16*1024)
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
b.mu.Lock()
|
||||
b.pending[request.SessionID] = conn
|
||||
b.mu.Unlock()
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (b *NativeApolloBackend) Open(_ context.Context, request LaunchRequest, _ RTSPResponse) (ProviderSession, error) {
|
||||
b.mu.Lock()
|
||||
conn := b.pending[request.SessionID]
|
||||
delete(b.pending, request.SessionID)
|
||||
b.mu.Unlock()
|
||||
if conn == nil {
|
||||
return nil, ErrProviderDisconnected
|
||||
}
|
||||
session := newNativeApolloSession(conn, request.SessionID)
|
||||
go session.readMedia()
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func readBounded(reader io.Reader, max int) ([]byte, error) {
|
||||
data, err := io.ReadAll(io.LimitReader(reader, int64(max)+1))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(data) > max {
|
||||
return nil, ErrProviderMalformed
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func readRTSPHeaders(conn net.Conn, max int) ([]byte, error) {
|
||||
reader := bufio.NewReaderSize(conn, 4096)
|
||||
var response []byte
|
||||
for len(response) < max {
|
||||
line, err := reader.ReadBytes('\n')
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response = append(response, line...)
|
||||
if strings.HasSuffix(string(response), "\r\n\r\n") {
|
||||
return response, nil
|
||||
}
|
||||
}
|
||||
return nil, ErrProviderMalformed
|
||||
}
|
||||
|
||||
type nativeApolloSession struct {
|
||||
conn net.Conn
|
||||
sessionID string
|
||||
video chan []byte
|
||||
audio chan []byte
|
||||
mu sync.Mutex
|
||||
state protocol.ProviderState
|
||||
closeOnce sync.Once
|
||||
done chan struct{}
|
||||
readDone chan struct{}
|
||||
}
|
||||
|
||||
func newNativeApolloSession(conn net.Conn, sessionID string) *nativeApolloSession {
|
||||
return &nativeApolloSession{conn: conn, sessionID: sessionID, video: make(chan []byte, 16), audio: make(chan []byte, 16), state: protocol.ProviderState{Version: "1", SessionID: sessionID, State: ProviderStateStarting, Channels: []string{"video", "audio", "input", "feedback"}}, done: make(chan struct{}), readDone: make(chan struct{})}
|
||||
}
|
||||
|
||||
func (s *nativeApolloSession) Ready(context.Context) error {
|
||||
s.mu.Lock()
|
||||
s.state.State = ProviderStateReady
|
||||
s.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *nativeApolloSession) Video() <-chan []byte { return s.video }
|
||||
func (s *nativeApolloSession) Audio() <-chan []byte { return s.audio }
|
||||
|
||||
func (s *nativeApolloSession) Input(ctx context.Context, event InputEvent) error {
|
||||
payload, err := EncodeInputEvent(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.writeControl(ctx, ControlPacket{Kind: 1, Sequence: event.Sequence, Payload: payload})
|
||||
}
|
||||
|
||||
func (s *nativeApolloSession) Feedback(ctx context.Context, feedback Feedback) error {
|
||||
return s.writeControl(ctx, ControlPacket{Kind: 3, Sequence: feedback.Sequence, Payload: feedback.Payload})
|
||||
}
|
||||
|
||||
func (s *nativeApolloSession) Reconnect(ctx context.Context) error {
|
||||
return s.writeControl(ctx, ControlPacket{Kind: 4, Payload: []byte("RECN")})
|
||||
}
|
||||
|
||||
func (s *nativeApolloSession) ReleaseAll(ctx context.Context) error {
|
||||
return s.writeControl(ctx, ControlPacket{Kind: 2, Payload: []byte("RELEASE_ALL")})
|
||||
}
|
||||
|
||||
func (s *nativeApolloSession) Terminate(ctx context.Context) error {
|
||||
_ = s.writeControl(ctx, ControlPacket{Kind: 5, Payload: []byte("TEAR")})
|
||||
var timedOut bool
|
||||
s.closeOnce.Do(func() {
|
||||
close(s.done)
|
||||
_ = s.conn.Close()
|
||||
select {
|
||||
case <-s.readDone:
|
||||
case <-ctx.Done():
|
||||
timedOut = true
|
||||
}
|
||||
if !timedOut {
|
||||
close(s.video)
|
||||
close(s.audio)
|
||||
}
|
||||
})
|
||||
s.mu.Lock()
|
||||
if timedOut {
|
||||
s.state.State = ProviderStateCleanup
|
||||
s.state.CleanupPending = true
|
||||
} else {
|
||||
s.state.State = ProviderStateTerminated
|
||||
}
|
||||
s.mu.Unlock()
|
||||
if timedOut {
|
||||
return ctx.Err()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *nativeApolloSession) State() protocol.ProviderState {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.state
|
||||
}
|
||||
|
||||
func (s *nativeApolloSession) writeControl(ctx context.Context, packet ControlPacket) error {
|
||||
encoded, err := EncodeControlPacket(packet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
_ = s.conn.SetWriteDeadline(deadline)
|
||||
}
|
||||
if _, err := s.conn.Write(encoded); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *nativeApolloSession) readMedia() {
|
||||
defer close(s.readDone)
|
||||
header := make([]byte, 4)
|
||||
for {
|
||||
if _, err := io.ReadFull(s.conn, header); err != nil {
|
||||
return
|
||||
}
|
||||
if header[0] != '$' || (header[1] != 0 && header[1] != 1) {
|
||||
return
|
||||
}
|
||||
length := int(header[2])<<8 | int(header[3])
|
||||
if length > 65536 {
|
||||
return
|
||||
}
|
||||
payload := make([]byte, length)
|
||||
if _, err := io.ReadFull(s.conn, payload); err != nil {
|
||||
return
|
||||
}
|
||||
if header[1] == 0 {
|
||||
pushLatest(s.video, payload)
|
||||
} else {
|
||||
pushLatest(s.audio, payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func pushLatest(channel chan []byte, payload []byte) {
|
||||
select {
|
||||
case channel <- payload:
|
||||
default:
|
||||
select {
|
||||
case <-channel:
|
||||
default:
|
||||
}
|
||||
select {
|
||||
case channel <- payload:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var _ ApolloBackend = (*NativeApolloBackend)(nil)
|
||||
var _ ProviderSession = (*nativeApolloSession)(nil)
|
||||
Reference in New Issue
Block a user