mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-08-07 14:25:17 +03:00
693 lines
23 KiB
Go
693 lines
23 KiB
Go
package dion
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/gorilla/websocket"
|
|
"github.com/sagernet/sing-box/transport/call/common"
|
|
"github.com/sagernet/sing/common/logger"
|
|
M "github.com/sagernet/sing/common/metadata"
|
|
N "github.com/sagernet/sing/common/network"
|
|
)
|
|
|
|
const (
|
|
MethodServerConnected = "server:notify:main:connected"
|
|
MethodServerYouJoined = "server:you_joined"
|
|
MethodServerSubscribeResponse = "server:main:response:subscribe:conference"
|
|
MethodServerSDPAnswer = "server:sdp_answer"
|
|
MethodServerSpeakerJoined = "server:speaker_joined"
|
|
MethodServerSpeakerDisconnected = "server:speaker_disconnected"
|
|
MethodServerHeartbeat = "server:notify:main:heartbeat"
|
|
MethodServerSpeakersResponse = "server:response:speakers"
|
|
MethodServerSpeakersResponseZip = "server:response:speakers_zip"
|
|
|
|
MethodClientSubscribeConference = "client:main:request:subscribe:conference"
|
|
MethodClientSDPOffer = "client:request:media:sdp_offer"
|
|
MethodClientSendICECandidates = "client:request:send_ice_candidates_zip"
|
|
MethodClientPCICEStat = "client:request:pc_ice_stat"
|
|
MethodClientTrace = "client:trace"
|
|
MethodClientConfSpeakersState = "client:request:conf_speakers_state_zip"
|
|
MethodServerConfSpeakersState = "server:response:conf_speakers_state_zip"
|
|
MethodClientGetVideoFromUser = "client:request:get_video_from_user"
|
|
MethodClientStopVideoFromUser = "client:request:stop_video_from_user"
|
|
MethodServerGetVideoFromUser = "server:response:get_video_from_user"
|
|
MethodServerStopVideoFromUser = "server:response:stop_video_from_user"
|
|
MethodClientCamStateChange = "client:request:cam_state_change"
|
|
MethodClientMicStateChange = "client:request:mic_state_change"
|
|
MethodClientScreenSharingSwitchOn = "client:request:screensharing_switch_on"
|
|
MethodClientScreenSharingSwitchOff = "client:request:screensharing_switch_off"
|
|
MethodClientGetScreenSharingFromUser = "client:request:get_screensharing_from_user"
|
|
MethodClientStopScreenSharingFromUser = "client:request:stop_screensharing_from_user"
|
|
MethodClientScreensharingQualityChange = "client:request:screensharing_quality_change"
|
|
MethodServerGetScreenSharingFromUser = "server:response:get_screensharing_from_user"
|
|
MethodClientClientStatZip = "client:request:client_stat_zip"
|
|
MethodClientKickOne = "client:request:kick_one"
|
|
MethodServerKickOneResponse = "server:response:kick_one"
|
|
MethodServerYouKicked = "server:you_kicked"
|
|
MethodServerYourCamStateChanged = "server:response:your_cam_state_changed"
|
|
MethodServerYourMicStateChanged = "server:response:your_mic_state_changed"
|
|
MethodServerSpeakerCamStateChanged = "server:speaker_cam_state_changed"
|
|
MethodServerSpeakerMicStateChanged = "server:speaker_mic_state_changed"
|
|
|
|
ProductVersion = "6.14.0"
|
|
SubscriptionVersion = "2.0"
|
|
)
|
|
|
|
type Frame struct {
|
|
JSONRPC string `json:"jsonrpc"`
|
|
Method string `json:"method,omitempty"`
|
|
Params json.RawMessage `json:"params,omitempty"`
|
|
Result json.RawMessage `json:"result,omitempty"`
|
|
Error *RPCError `json:"error,omitempty"`
|
|
ID json.RawMessage `json:"id,omitempty"`
|
|
}
|
|
|
|
type RPCError struct {
|
|
Code int `json:"code"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
type TransceiverDesc struct {
|
|
TransceiverID string `json:"transceiver_id"`
|
|
SessionID string `json:"session_id"`
|
|
Direction string `json:"direction"`
|
|
Ctype string `json:"ctype"`
|
|
}
|
|
|
|
type DataChannelDesc struct {
|
|
ID int `json:"id"`
|
|
Label string `json:"label"`
|
|
}
|
|
|
|
type SDPEnvelope struct {
|
|
Type string `json:"type"`
|
|
SDP string `json:"sdp"`
|
|
}
|
|
|
|
type SDPOfferParams struct {
|
|
MicState bool `json:"mic_state"`
|
|
CamState bool `json:"cam_state"`
|
|
NoiseSuppressionState bool `json:"noise_suppression_state"`
|
|
VideoQuality *string `json:"video_quality"`
|
|
ScreenSharingQuality string `json:"screen_sharing_quality"`
|
|
Datachannels []DataChannelDesc `json:"datachannels"`
|
|
Transceivers []TransceiverDesc `json:"transceivers"`
|
|
Offer string `json:"offer"`
|
|
}
|
|
|
|
type SDPAnswerParams struct {
|
|
Answer string `json:"answer"`
|
|
Transceivers []TransceiverDesc `json:"transceivers"`
|
|
}
|
|
|
|
type ICEServerEntry struct {
|
|
URLs []string `json:"urls"`
|
|
Username string `json:"username"`
|
|
Credential string `json:"credential"`
|
|
}
|
|
|
|
type YouJoinedParams struct {
|
|
IcePolicy string `json:"ice_policy"`
|
|
IceServers []ICEServerEntry `json:"ice_servers"`
|
|
Event json.RawMessage `json:"event"`
|
|
EventParams json.RawMessage `json:"event_params"`
|
|
PreferredCodecs json.RawMessage `json:"preferred_codecs"`
|
|
}
|
|
|
|
type SpeakerJoinedParams struct {
|
|
SessionID string `json:"session_id"`
|
|
UserID string `json:"user_id,omitempty"`
|
|
Name string `json:"name,omitempty"`
|
|
CamState bool `json:"cam_state"`
|
|
MicState bool `json:"mic_state"`
|
|
Extra json.RawMessage `json:"-"`
|
|
}
|
|
|
|
type SpeakerCamStateChangedParams struct {
|
|
SessionID string `json:"session_id"`
|
|
CamState bool `json:"cam_state"`
|
|
}
|
|
|
|
type SpeakerMicStateChangedParams struct {
|
|
SessionID string `json:"session_id"`
|
|
MicState bool `json:"mic_state"`
|
|
}
|
|
|
|
type SpeakerEntry struct {
|
|
SessionID string `json:"session_id"`
|
|
UserID string `json:"user_id"`
|
|
Name string `json:"name"`
|
|
MicState bool `json:"mic_state"`
|
|
CamState bool `json:"cam_state"`
|
|
Role string `json:"role"`
|
|
WebinarRole string `json:"webinar_role"`
|
|
IsGuest bool `json:"is_guest"`
|
|
}
|
|
|
|
type ConfSpeakersStateResponse struct {
|
|
SpeakersCount int `json:"speakers_count"`
|
|
WebinarSpeakersCount int `json:"webinar_speakers_count"`
|
|
Speakers []SpeakerEntry `json:"speakers"`
|
|
}
|
|
|
|
type ConfSpeakersStateRequest struct {
|
|
SessionIDs []string `json:"session_ids"`
|
|
TileParams ConfSpeakersTileParams `json:"tile_params"`
|
|
InputVideoQuality string `json:"input_video_quality"`
|
|
ScreenParams ConfSpeakersScreenParams `json:"screen_params"`
|
|
}
|
|
|
|
type ConfSpeakersTileParams struct {
|
|
Mode string `json:"mode"`
|
|
MosaicParams ConfSpeakersMosaicParams `json:"mosaic_params"`
|
|
IsModeBlocked bool `json:"is_mode_blocked"`
|
|
}
|
|
|
|
type ConfSpeakersMosaicParams struct {
|
|
MaxTilesCount int `json:"max_tiles_count"`
|
|
}
|
|
|
|
type ConfSpeakersScreenParams struct {
|
|
Height int `json:"height"`
|
|
Width int `json:"width"`
|
|
}
|
|
|
|
type SpeakerDisconnectedParams struct {
|
|
SessionID string `json:"session_id"`
|
|
}
|
|
|
|
type ICECandidateJSON struct {
|
|
Candidate string `json:"candidate"`
|
|
SDPMid *string `json:"sdpMid"`
|
|
SDPMLineIndex *uint16 `json:"sdpMLineIndex"`
|
|
UsernameFragment string `json:"usernameFragment,omitempty"`
|
|
}
|
|
|
|
type GetVideoFromUserRequest struct {
|
|
SessionID string `json:"session_id"`
|
|
TransceiverID string `json:"transceiver_id"`
|
|
UserID string `json:"user_id"`
|
|
Username string `json:"username"`
|
|
}
|
|
|
|
type GetVideoFromUserResponse struct {
|
|
SessionID string `json:"session_id"`
|
|
TransceiverID string `json:"transceiver_id"`
|
|
}
|
|
|
|
type GetScreenSharingFromUserRequest struct {
|
|
SessionID string `json:"session_id"`
|
|
TransceiverID string `json:"transceiver_id"`
|
|
UserID string `json:"user_id"`
|
|
}
|
|
|
|
type GetScreenSharingFromUserResponse struct {
|
|
SessionID string `json:"session_id"`
|
|
TransceiverID string `json:"transceiver_id"`
|
|
}
|
|
|
|
type ClientStatVideoIn struct {
|
|
BytesReceived int64 `json:"bytes_received"`
|
|
Codec string `json:"codec"`
|
|
IsEnabled bool `json:"is_enabled"`
|
|
JitterBufferDelay float64 `json:"jitter_buffer_delay"`
|
|
JitterBufferEmittedCount int `json:"jitter_buffer_emitted_count"`
|
|
Jitter float64 `json:"jitter"`
|
|
Mid int `json:"mid"`
|
|
PacketsLost int `json:"packets_lost"`
|
|
PacketsReceived int `json:"packets_received"`
|
|
Framerate int `json:"framerate"`
|
|
FreezeCount int `json:"freeze_count"`
|
|
Resolution ClientStatResolution `json:"resolution"`
|
|
Rid string `json:"rid"`
|
|
TotalFreezesDuration int `json:"total_freezes_duration"`
|
|
SessionID string `json:"session_id"`
|
|
}
|
|
|
|
type ClientStatVideoOut struct {
|
|
Mid int `json:"mid"`
|
|
BytesSent int64 `json:"bytes_sent"`
|
|
Codec string `json:"codec"`
|
|
IsEnabled bool `json:"is_enabled"`
|
|
PacketsSent int `json:"packets_sent"`
|
|
RemoteStats ClientStatRemoteStats `json:"remote_stats"`
|
|
TargetBitrate int `json:"target_bitrate"`
|
|
Framerate int `json:"framerate"`
|
|
FreezeCount int `json:"freeze_count"`
|
|
Resolution ClientStatResolution `json:"resolution"`
|
|
Rid string `json:"rid"`
|
|
TotalFreezesDuration int `json:"total_freezes_duration"`
|
|
SessionID string `json:"session_id"`
|
|
ScalabilityMode string `json:"scalability_mode"`
|
|
}
|
|
|
|
type ClientStatResolution struct {
|
|
Height int `json:"height"`
|
|
Width int `json:"width"`
|
|
}
|
|
|
|
type ClientStatRemoteStats struct {
|
|
Jitter float64 `json:"jitter"`
|
|
FractionPacketsLost float64 `json:"fraction_packets_lost"`
|
|
PacketsLost int `json:"packets_lost"`
|
|
RTT float64 `json:"rtt"`
|
|
}
|
|
|
|
type ClientStatAudioIn struct {
|
|
BytesReceived int64 `json:"bytes_received"`
|
|
Codec string `json:"codec"`
|
|
IsEnabled bool `json:"is_enabled"`
|
|
JitterBufferDelay float64 `json:"jitter_buffer_delay"`
|
|
JitterBufferEmittedCount int `json:"jitter_buffer_emitted_count"`
|
|
Jitter float64 `json:"jitter"`
|
|
Mid int `json:"mid"`
|
|
PacketsLost int `json:"packets_lost"`
|
|
PacketsReceived int `json:"packets_received"`
|
|
}
|
|
|
|
type ClientStatConnection struct {
|
|
BytesReceived int64 `json:"bytes_received"`
|
|
BytesSent int64 `json:"bytes_sent"`
|
|
CurrentRTT float64 `json:"current_rtt"`
|
|
}
|
|
|
|
type ClientStatReport struct {
|
|
ReportTimeUnixMS int64 `json:"report_time_unix_ms"`
|
|
Connection ClientStatConnection `json:"connection"`
|
|
Audio struct {
|
|
In ClientStatAudioIn `json:"in"`
|
|
} `json:"audio"`
|
|
Video struct {
|
|
In []ClientStatVideoIn `json:"in"`
|
|
OutV2 []ClientStatVideoOut `json:"out_v2"`
|
|
Out ClientStatVideoOut `json:"out"`
|
|
} `json:"video"`
|
|
Screensharing struct{} `json:"screensharing"`
|
|
}
|
|
|
|
type SignalingDialOptions struct {
|
|
UserAgent string
|
|
Origin string
|
|
Logger logger.ContextLogger
|
|
Dialer N.Dialer
|
|
}
|
|
|
|
type SignalingClient struct {
|
|
conn *websocket.Conn
|
|
writeMu sync.Mutex
|
|
closed atomic.Bool
|
|
logger logger.ContextLogger
|
|
sessionID string
|
|
eventID string
|
|
|
|
OnYouJoined func(YouJoinedParams)
|
|
OnSubscribeResponse func()
|
|
OnSDPAnswer func(answerSDP string, transceivers []TransceiverDesc)
|
|
OnSpeakerJoined func(SpeakerJoinedParams)
|
|
OnSpeakerDisconnected func(SpeakerDisconnectedParams)
|
|
OnConfSpeakersState func(ConfSpeakersStateResponse)
|
|
OnSpeakerCamStateChanged func(SpeakerCamStateChangedParams)
|
|
OnSpeakerMicStateChanged func(SpeakerMicStateChangedParams)
|
|
OnGetVideoFromUserResponse func(resp GetVideoFromUserResponse, errCode int, errMessage string)
|
|
OnGetScreenSharingFromUserResponse func(resp GetScreenSharingFromUserResponse, errCode int, errMessage string)
|
|
OnHeartbeat func()
|
|
OnUnknown func(method string, params json.RawMessage)
|
|
OnDataChannelMessage func(method string, params json.RawMessage)
|
|
}
|
|
|
|
func DialSignaling(wssURL string, opts SignalingDialOptions) (*SignalingClient, error) {
|
|
if !strings.Contains(wssURL, "socket_version=") {
|
|
joiner := "&"
|
|
if !strings.Contains(wssURL, "?") {
|
|
joiner = "?"
|
|
}
|
|
wssURL = wssURL + joiner + "socket_version=2.0"
|
|
}
|
|
dialer := websocket.Dialer{HandshakeTimeout: 10 * time.Second}
|
|
if opts.Dialer != nil {
|
|
dialer.NetDialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
return opts.Dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
|
}
|
|
}
|
|
headers := http.Header{}
|
|
if opts.UserAgent != "" {
|
|
headers.Set("User-Agent", opts.UserAgent)
|
|
}
|
|
if opts.Origin != "" {
|
|
headers.Set("Origin", opts.Origin)
|
|
} else {
|
|
headers.Set("Origin", Origin)
|
|
}
|
|
log := opts.Logger
|
|
if log == nil {
|
|
log = logger.NOP()
|
|
}
|
|
conn, resp, err := dialer.Dial(wssURL, headers)
|
|
if err != nil {
|
|
status := 0
|
|
if resp != nil {
|
|
status = resp.StatusCode
|
|
}
|
|
return nil, fmt.Errorf("ws dial: %w status=%d url=%s", err, status, wssURL)
|
|
}
|
|
if resp != nil {
|
|
log.Debug(fmt.Sprintf("dion: ws dial status=%d", resp.StatusCode))
|
|
}
|
|
return &SignalingClient{conn: conn, logger: log}, nil
|
|
}
|
|
|
|
func (c *SignalingClient) Close() error {
|
|
if !c.closed.CompareAndSwap(false, true) {
|
|
return nil
|
|
}
|
|
common.CloseWS(c.conn)
|
|
return nil
|
|
}
|
|
|
|
func (c *SignalingClient) WaitConnected(timeout time.Duration) error {
|
|
c.conn.SetReadDeadline(time.Now().Add(timeout))
|
|
_, raw, err := c.conn.ReadMessage()
|
|
if err != nil {
|
|
return fmt.Errorf("read connected: %w", err)
|
|
}
|
|
var frame Frame
|
|
if err := json.Unmarshal(raw, &frame); err != nil {
|
|
return fmt.Errorf("decode connected: %w", err)
|
|
}
|
|
if frame.Method != MethodServerConnected {
|
|
return fmt.Errorf("expected %s, got %s", MethodServerConnected, frame.Method)
|
|
}
|
|
c.logger.Debug("dion: signaling connected")
|
|
return nil
|
|
}
|
|
|
|
func (c *SignalingClient) Subscribe(eventID, sessionID string) error {
|
|
c.eventID = eventID
|
|
c.sessionID = sessionID
|
|
return c.sendFrame(MethodClientSubscribeConference, map[string]any{
|
|
"event_id": eventID,
|
|
"conf_user_session_id": sessionID,
|
|
"main_user_session_id": nil,
|
|
"product_version": ProductVersion,
|
|
"subscription_version": SubscriptionVersion,
|
|
})
|
|
}
|
|
|
|
func (c *SignalingClient) SendTrace(deviceInfo map[string]any) error {
|
|
data, err := json.Marshal(deviceInfo)
|
|
if err != nil {
|
|
return fmt.Errorf("marshal trace: %w", err)
|
|
}
|
|
return c.sendFrame(MethodClientTrace, map[string]any{"data": string(data)})
|
|
}
|
|
|
|
func (c *SignalingClient) SendSDPOffer(params SDPOfferParams) error {
|
|
return c.sendFrame(MethodClientSDPOffer, params)
|
|
}
|
|
|
|
func (c *SignalingClient) SendConfSpeakersState(request ConfSpeakersStateRequest) error {
|
|
encoded, err := ZipEncode(request)
|
|
if err != nil {
|
|
return fmt.Errorf("zip conf_speakers_state: %w", err)
|
|
}
|
|
return c.sendFrame(MethodClientConfSpeakersState, encoded)
|
|
}
|
|
|
|
func (c *SignalingClient) SendGetVideoFromUser(request GetVideoFromUserRequest) error {
|
|
return c.sendFrame(MethodClientGetVideoFromUser, request)
|
|
}
|
|
|
|
func (c *SignalingClient) SendStopVideoFromUser(request GetVideoFromUserRequest) error {
|
|
return c.sendFrame(MethodClientStopVideoFromUser, request)
|
|
}
|
|
|
|
func (c *SignalingClient) SendCamStateChange(state bool) error {
|
|
return c.sendFrame(MethodClientCamStateChange, map[string]any{"state": state})
|
|
}
|
|
|
|
func (c *SignalingClient) SendMicStateChange(state bool) error {
|
|
return c.sendFrame(MethodClientMicStateChange, map[string]any{"state": state})
|
|
}
|
|
|
|
func (c *SignalingClient) SendScreenSharingSwitchOn() error {
|
|
return c.sendFrame(MethodClientScreenSharingSwitchOn, map[string]any{})
|
|
}
|
|
|
|
func (c *SignalingClient) SendScreenSharingSwitchOff() error {
|
|
return c.sendFrame(MethodClientScreenSharingSwitchOff, map[string]any{})
|
|
}
|
|
|
|
func (c *SignalingClient) SendGetScreenSharingFromUser(request GetScreenSharingFromUserRequest) error {
|
|
return c.sendFrame(MethodClientGetScreenSharingFromUser, request)
|
|
}
|
|
|
|
func (c *SignalingClient) SendScreensharingQualityChange(quality string) error {
|
|
return c.sendFrame(MethodClientScreensharingQualityChange, map[string]any{"quality": quality})
|
|
}
|
|
|
|
func (c *SignalingClient) SendKickOne(sessionID string) error {
|
|
return c.sendFrame(MethodClientKickOne, map[string]any{"session_id": sessionID})
|
|
}
|
|
|
|
func (c *SignalingClient) SendPCIceStat() error {
|
|
return c.sendFrame(MethodClientPCICEStat, map[string]any{"device": "web"})
|
|
}
|
|
|
|
func (c *SignalingClient) SendClientStatZip(report ClientStatReport) error {
|
|
encoded, err := ZipEncode(report)
|
|
if err != nil {
|
|
return fmt.Errorf("zip client_stat: %w", err)
|
|
}
|
|
return c.sendFrame(MethodClientClientStatZip, encoded)
|
|
}
|
|
|
|
func (c *SignalingClient) SendICECandidates(candidates []ICECandidateJSON) error {
|
|
encoded := make([]string, 0, len(candidates))
|
|
for _, candidate := range candidates {
|
|
raw, err := EncodeICECandidate(candidate)
|
|
if err != nil {
|
|
return fmt.Errorf("encode candidate: %w", err)
|
|
}
|
|
encoded = append(encoded, raw)
|
|
}
|
|
zipped, err := ZipEncode(map[string]any{"candidates": encoded})
|
|
if err != nil {
|
|
return fmt.Errorf("zip candidates: %w", err)
|
|
}
|
|
return c.sendFrame(MethodClientSendICECandidates, zipped)
|
|
}
|
|
|
|
func (c *SignalingClient) ReadLoop() error {
|
|
for {
|
|
if c.closed.Load() {
|
|
return nil
|
|
}
|
|
c.conn.SetReadDeadline(time.Now().Add(60 * time.Second))
|
|
_, raw, err := c.conn.ReadMessage()
|
|
if err != nil {
|
|
if c.closed.Load() {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("ws read: %w", err)
|
|
}
|
|
var frame Frame
|
|
if err := json.Unmarshal(raw, &frame); err != nil {
|
|
c.logger.Debug(fmt.Sprintf("dion: drop non-json frame: %v", err))
|
|
continue
|
|
}
|
|
if frame.Error != nil {
|
|
c.logger.Debug(fmt.Sprintf("dion: <- %s ERROR code=%d message=%q", frame.Method, frame.Error.Code, frame.Error.Message))
|
|
}
|
|
c.dispatch(frame)
|
|
}
|
|
}
|
|
|
|
func EncodeICECandidate(candidate ICECandidateJSON) (string, error) {
|
|
plain, err := json.Marshal(candidate)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return base64.StdEncoding.EncodeToString(plain), nil
|
|
}
|
|
|
|
func DecodeICECandidate(encoded string) (ICECandidateJSON, error) {
|
|
raw, err := base64.StdEncoding.DecodeString(encoded)
|
|
if err != nil {
|
|
return ICECandidateJSON{}, fmt.Errorf("base64: %w", err)
|
|
}
|
|
var out ICECandidateJSON
|
|
if err := json.Unmarshal(raw, &out); err != nil {
|
|
return ICECandidateJSON{}, fmt.Errorf("unmarshal: %w", err)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func BuildSDPOfferEnvelope(offerSDP string) (string, error) {
|
|
return ZipEncode(SDPEnvelope{Type: "offer", SDP: offerSDP})
|
|
}
|
|
|
|
func DecodeSDPAnswerInner(answerZipped string) (string, error) {
|
|
var inner SDPEnvelope
|
|
if err := ZipDecode(answerZipped, &inner); err != nil {
|
|
return "", err
|
|
}
|
|
return inner.SDP, nil
|
|
}
|
|
|
|
func DefaultConfSpeakersStateRequest() ConfSpeakersStateRequest {
|
|
return ConfSpeakersStateRequest{
|
|
SessionIDs: []string{},
|
|
TileParams: ConfSpeakersTileParams{
|
|
Mode: "mosaic",
|
|
MosaicParams: ConfSpeakersMosaicParams{MaxTilesCount: 9},
|
|
IsModeBlocked: false,
|
|
},
|
|
InputVideoQuality: "auto",
|
|
ScreenParams: ConfSpeakersScreenParams{Height: 720, Width: 1280},
|
|
}
|
|
}
|
|
|
|
func (c *SignalingClient) sendFrame(method string, params any) error {
|
|
c.writeMu.Lock()
|
|
defer c.writeMu.Unlock()
|
|
if c.closed.Load() {
|
|
return fmt.Errorf("signaling closed")
|
|
}
|
|
payload := map[string]any{
|
|
"jsonrpc": "2.0",
|
|
"method": method,
|
|
"params": params,
|
|
}
|
|
raw, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return fmt.Errorf("marshal frame: %w", err)
|
|
}
|
|
return c.conn.WriteMessage(websocket.TextMessage, raw)
|
|
}
|
|
|
|
func (c *SignalingClient) dispatch(frame Frame) {
|
|
switch frame.Method {
|
|
case MethodServerConnected:
|
|
c.logger.Debug("dion: late server:notify:main:connected")
|
|
case MethodServerYouJoined:
|
|
var youJoined YouJoinedParams
|
|
if err := json.Unmarshal(frame.Params, &youJoined); err != nil {
|
|
c.logger.Debug(fmt.Sprintf("dion: decode you_joined: %v", err))
|
|
return
|
|
}
|
|
if c.OnYouJoined != nil {
|
|
c.OnYouJoined(youJoined)
|
|
}
|
|
case MethodServerSubscribeResponse:
|
|
if c.OnSubscribeResponse != nil {
|
|
c.OnSubscribeResponse()
|
|
}
|
|
case MethodServerSDPAnswer:
|
|
var answer SDPAnswerParams
|
|
if err := json.Unmarshal(frame.Params, &answer); err != nil {
|
|
c.logger.Debug(fmt.Sprintf("dion: decode sdp_answer: %v", err))
|
|
return
|
|
}
|
|
var inner SDPEnvelope
|
|
if err := ZipDecode(answer.Answer, &inner); err != nil {
|
|
c.logger.Debug(fmt.Sprintf("dion: decode sdp_answer envelope: %v", err))
|
|
return
|
|
}
|
|
if c.OnSDPAnswer != nil {
|
|
c.OnSDPAnswer(inner.SDP, answer.Transceivers)
|
|
}
|
|
case MethodServerSpeakerJoined:
|
|
var joined SpeakerJoinedParams
|
|
if err := json.Unmarshal(frame.Params, &joined); err != nil {
|
|
c.logger.Debug(fmt.Sprintf("dion: decode speaker_joined: %v", err))
|
|
return
|
|
}
|
|
joined.Extra = frame.Params
|
|
if c.OnSpeakerJoined != nil {
|
|
c.OnSpeakerJoined(joined)
|
|
}
|
|
case MethodServerSpeakerDisconnected:
|
|
var left SpeakerDisconnectedParams
|
|
if err := json.Unmarshal(frame.Params, &left); err != nil {
|
|
c.logger.Debug(fmt.Sprintf("dion: decode speaker_disconnected: %v", err))
|
|
return
|
|
}
|
|
if c.OnSpeakerDisconnected != nil {
|
|
c.OnSpeakerDisconnected(left)
|
|
}
|
|
case MethodServerSpeakerCamStateChanged:
|
|
var changed SpeakerCamStateChangedParams
|
|
if err := json.Unmarshal(frame.Params, &changed); err != nil {
|
|
c.logger.Debug(fmt.Sprintf("dion: decode speaker_cam_state_changed: %v", err))
|
|
return
|
|
}
|
|
if c.OnSpeakerCamStateChanged != nil {
|
|
c.OnSpeakerCamStateChanged(changed)
|
|
}
|
|
case MethodServerSpeakerMicStateChanged:
|
|
var changed SpeakerMicStateChangedParams
|
|
if err := json.Unmarshal(frame.Params, &changed); err != nil {
|
|
c.logger.Debug(fmt.Sprintf("dion: decode speaker_mic_state_changed: %v", err))
|
|
return
|
|
}
|
|
if c.OnSpeakerMicStateChanged != nil {
|
|
c.OnSpeakerMicStateChanged(changed)
|
|
}
|
|
case MethodServerConfSpeakersState:
|
|
var encoded string
|
|
if err := json.Unmarshal(frame.Params, &encoded); err != nil {
|
|
c.logger.Debug(fmt.Sprintf("dion: decode conf_speakers_state envelope: %v", err))
|
|
return
|
|
}
|
|
var response ConfSpeakersStateResponse
|
|
if err := ZipDecode(encoded, &response); err != nil {
|
|
c.logger.Debug(fmt.Sprintf("dion: decode conf_speakers_state body: %v", err))
|
|
return
|
|
}
|
|
if c.OnConfSpeakersState != nil {
|
|
c.OnConfSpeakersState(response)
|
|
}
|
|
case MethodServerHeartbeat:
|
|
if c.OnHeartbeat != nil {
|
|
c.OnHeartbeat()
|
|
}
|
|
case MethodServerGetVideoFromUser:
|
|
var resp GetVideoFromUserResponse
|
|
_ = json.Unmarshal(frame.Params, &resp)
|
|
errCode := 0
|
|
errMsg := ""
|
|
if frame.Error != nil {
|
|
errCode = frame.Error.Code
|
|
errMsg = frame.Error.Message
|
|
}
|
|
if c.OnGetVideoFromUserResponse != nil {
|
|
c.OnGetVideoFromUserResponse(resp, errCode, errMsg)
|
|
}
|
|
case MethodServerGetScreenSharingFromUser:
|
|
var resp GetScreenSharingFromUserResponse
|
|
_ = json.Unmarshal(frame.Params, &resp)
|
|
errCode := 0
|
|
errMsg := ""
|
|
if frame.Error != nil {
|
|
errCode = frame.Error.Code
|
|
errMsg = frame.Error.Message
|
|
}
|
|
if c.OnGetScreenSharingFromUserResponse != nil {
|
|
c.OnGetScreenSharingFromUserResponse(resp, errCode, errMsg)
|
|
}
|
|
default:
|
|
if c.OnUnknown != nil {
|
|
c.OnUnknown(frame.Method, frame.Params)
|
|
}
|
|
}
|
|
}
|