mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-08-07 14:25:17 +03:00
758 lines
22 KiB
Go
758 lines
22 KiB
Go
package vk
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/pion/webrtc/v4"
|
|
"github.com/sagernet/sing-box/adapter"
|
|
"github.com/sagernet/sing-box/common/dialer"
|
|
"github.com/sagernet/sing-box/transport/call/common"
|
|
"github.com/sagernet/sing-box/transport/call/tunnel"
|
|
"github.com/sagernet/sing-box/transport/call/wtsignal"
|
|
"github.com/sagernet/sing/common/logger"
|
|
M "github.com/sagernet/sing/common/metadata"
|
|
N "github.com/sagernet/sing/common/network"
|
|
)
|
|
|
|
const (
|
|
vkReconnectInitialDelay = time.Second
|
|
vkReconnectMaxDelay = 16 * time.Second
|
|
vkMaxReconnectAttempts = 10
|
|
)
|
|
|
|
const vkTopologyDirect = "DIRECT"
|
|
|
|
type vkAuthRottenError struct {
|
|
Code string
|
|
Msg string
|
|
}
|
|
|
|
func (e *vkAuthRottenError) Error() string {
|
|
if e.Msg != "" {
|
|
return fmt.Sprintf("auth rotten: %s %s", e.Code, e.Msg)
|
|
}
|
|
return fmt.Sprintf("auth rotten: %s", e.Code)
|
|
}
|
|
|
|
type VKAuthParams struct {
|
|
SessionKey string `json:"sessionKey"`
|
|
ApplicationKey string `json:"applicationKey"`
|
|
APIBaseURL string `json:"apiBaseURL"`
|
|
JoinLink string `json:"joinLink"`
|
|
AnonymToken string `json:"anonymToken"`
|
|
AppVersion string `json:"appVersion"`
|
|
ProtocolVersion string `json:"protocolVersion"`
|
|
TunnelMode string `json:"tunnelMode"`
|
|
VP8FPS int `json:"vp8Fps"`
|
|
VP8Batch int `json:"vp8Batch"`
|
|
DualTrack bool `json:"dualTrack"`
|
|
}
|
|
|
|
type VKJoinResponse struct {
|
|
Endpoint string `json:"endpoint"`
|
|
WtEndpoint string `json:"wt_endpoint"`
|
|
Token string `json:"token"`
|
|
TurnServer struct {
|
|
URLs []string `json:"urls"`
|
|
Username string `json:"username"`
|
|
Credential string `json:"credential"`
|
|
} `json:"turn_server"`
|
|
StunServer struct {
|
|
URLs []string `json:"urls"`
|
|
} `json:"stun_server"`
|
|
}
|
|
|
|
type VKJoiner struct {
|
|
logger logger.ContextLogger
|
|
OnConnected func(tunnel.DataTunnel)
|
|
OnRemoteCandidate func(target int, candidateOrSDP string)
|
|
PCConfig common.PeerConnectionConfigurer
|
|
AddTracks common.AddTunnelTracksFunc
|
|
ReadTrackFn common.ReadTrackFunc
|
|
Dialer N.Dialer
|
|
DNSRouter adapter.DNSRouter
|
|
|
|
authParams *VKAuthParams
|
|
joinResp *VKJoinResponse
|
|
sfu *wtsignal.Conn
|
|
vkMu sync.Mutex
|
|
vkSeq int
|
|
remotePeerID *int64
|
|
|
|
pc *webrtc.PeerConnection
|
|
sampleTrack *webrtc.TrackLocalStaticSample
|
|
dc *webrtc.DataChannel
|
|
vp8tunnel *tunnel.VP8DataTunnel
|
|
sym *tunnel.SymmetricScreenTunnel
|
|
producerScreen screenUplink
|
|
obf *tunnel.TunnelObfuscator
|
|
vp8FPS int
|
|
vp8Batch int
|
|
dualTrack bool
|
|
remoteSet bool
|
|
pendingICE []webrtc.ICECandidateInit
|
|
|
|
configAck tunnel.ConfigAckTracker
|
|
reconnectAttempt atomic.Int32
|
|
stopCh chan struct{}
|
|
stopOnce sync.Once
|
|
}
|
|
|
|
func NewVKJoiner(logger logger.ContextLogger, pcConfig common.PeerConnectionConfigurer, addTracks common.AddTunnelTracksFunc, readTrackFn common.ReadTrackFunc, dialer N.Dialer, dnsRouter adapter.DNSRouter) *VKJoiner {
|
|
return &VKJoiner{
|
|
logger: logger,
|
|
PCConfig: pcConfig,
|
|
AddTracks: addTracks,
|
|
ReadTrackFn: readTrackFn,
|
|
Dialer: dialer,
|
|
DNSRouter: dnsRouter,
|
|
stopCh: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
func (h *VKJoiner) RunWithParams(jsonParams string) {
|
|
var params VKAuthParams
|
|
if err := json.Unmarshal([]byte(jsonParams), ¶ms); err != nil {
|
|
h.logger.Error(fmt.Sprintf("vk-joiner: failed to parse auth params: %v", err))
|
|
return
|
|
}
|
|
h.authParams = ¶ms
|
|
obf, err := tunnel.NewTunnelObfuscator(tunnel.DeriveSecretFromJoinLink(params.JoinLink))
|
|
if err != nil {
|
|
h.logger.Error(fmt.Sprintf("vk-joiner: obfuscator init failed: %v", err))
|
|
return
|
|
}
|
|
h.obf = obf
|
|
h.vp8FPS = params.VP8FPS
|
|
h.vp8Batch = params.VP8Batch
|
|
// h.dualTrack = params.DualTrack // temporarily disabled for VK joiners
|
|
h.logger.Debug("vk-joiner: auth params received")
|
|
h.logger.Debug(fmt.Sprintf("vk-joiner: obf key-source=%q localEpoch=0x%08x", params.JoinLink, obf.LocalEpoch()))
|
|
h.logger.Debug(fmt.Sprintf("vk-joiner: appVersion=%s protocolVersion=%s vp8Fps=%d vp8Batch=%d",
|
|
params.AppVersion, params.ProtocolVersion, params.VP8FPS, params.VP8Batch))
|
|
h.logger.Info("vk-joiner: connecting")
|
|
if err := h.runOnce(); err != nil {
|
|
h.logger.Error(fmt.Sprintf("vk-joiner: %v", err))
|
|
return
|
|
}
|
|
for {
|
|
if h.isClosed() {
|
|
return
|
|
}
|
|
h.logger.Info("vk-joiner: tunnel lost")
|
|
if !h.waitBeforeRetry(int(h.reconnectAttempt.Load())) {
|
|
return
|
|
}
|
|
attempt := h.reconnectAttempt.Add(1)
|
|
if h.isClosed() {
|
|
return
|
|
}
|
|
if int(attempt) > vkMaxReconnectAttempts {
|
|
h.logger.Warn(fmt.Sprintf("vk-joiner: gave up after %d consecutive reconnect attempts", vkMaxReconnectAttempts))
|
|
return
|
|
}
|
|
h.logger.Info(fmt.Sprintf("vk-joiner: reconnect attempt #%d", attempt))
|
|
if err := h.runOnce(); err != nil {
|
|
var authRotten *vkAuthRottenError
|
|
if errors.As(err, &authRotten) {
|
|
h.logger.Error(fmt.Sprintf("vk-joiner: %v, surrendering", err))
|
|
return
|
|
}
|
|
h.logger.Warn(fmt.Sprintf("vk-joiner: %v, will retry", err))
|
|
}
|
|
}
|
|
}
|
|
|
|
func (h *VKJoiner) Close() {
|
|
h.stopOnce.Do(func() { close(h.stopCh) })
|
|
StopCaptchaProxy()
|
|
h.vkMu.Lock()
|
|
sfu := h.sfu
|
|
h.sfu = nil
|
|
h.vkMu.Unlock()
|
|
if sfu != nil {
|
|
sfu.Close()
|
|
}
|
|
if h.vp8tunnel != nil {
|
|
h.vp8tunnel.Stop()
|
|
}
|
|
if h.pc != nil {
|
|
h.pc.Close()
|
|
}
|
|
}
|
|
|
|
func (h *VKJoiner) closeTransport() {
|
|
h.vkMu.Lock()
|
|
sfu := h.sfu
|
|
h.vkMu.Unlock()
|
|
if sfu != nil {
|
|
sfu.Close()
|
|
}
|
|
}
|
|
|
|
func (h *VKJoiner) runOnce() error {
|
|
h.resetSessionState()
|
|
if err := h.joinCall(); err != nil {
|
|
return err
|
|
}
|
|
h.connectSFU()
|
|
return nil
|
|
}
|
|
|
|
func (h *VKJoiner) MarkConfigAcked() { h.configAck.Mark() }
|
|
|
|
func (h *VKJoiner) waitBeforeRetry(attempt int) bool {
|
|
delay := common.BackoffWithJitter(attempt, vkReconnectInitialDelay, vkReconnectMaxDelay)
|
|
h.logger.Debug(fmt.Sprintf("vk-joiner: waiting %s before reconnect", delay))
|
|
timer := time.NewTimer(delay)
|
|
defer timer.Stop()
|
|
select {
|
|
case <-timer.C:
|
|
return !h.isClosed()
|
|
case <-h.stopCh:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (h *VKJoiner) isClosed() bool {
|
|
select {
|
|
case <-h.stopCh:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (h *VKJoiner) resetSessionState() {
|
|
h.vkMu.Lock()
|
|
sfu := h.sfu
|
|
h.sfu = nil
|
|
h.vkSeq = 0
|
|
h.vkMu.Unlock()
|
|
if sfu != nil {
|
|
sfu.Close()
|
|
}
|
|
if h.sym != nil {
|
|
h.sym.Stop()
|
|
h.sym = nil
|
|
}
|
|
if h.vp8tunnel != nil {
|
|
h.vp8tunnel.Stop()
|
|
h.vp8tunnel = nil
|
|
}
|
|
h.producerScreen.reset()
|
|
if h.dc != nil {
|
|
h.dc.Close()
|
|
h.dc = nil
|
|
}
|
|
if h.pc != nil {
|
|
h.pc.Close()
|
|
h.pc = nil
|
|
}
|
|
h.sampleTrack = nil
|
|
h.remoteSet = false
|
|
h.pendingICE = nil
|
|
h.remotePeerID = nil
|
|
h.joinResp = nil
|
|
}
|
|
|
|
func (h *VKJoiner) dialContext(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
return h.Dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
|
}
|
|
|
|
func (h *VKJoiner) resolveHost(host string) (string, error) {
|
|
if ip := net.ParseIP(host); ip != nil {
|
|
return host, nil
|
|
}
|
|
rd, hasRD := h.Dialer.(dialer.ResolveDialer)
|
|
if h.DNSRouter == nil || !hasRD {
|
|
return "", fmt.Errorf("no DNS router available to resolve %s", host)
|
|
}
|
|
addrs, err := h.DNSRouter.Lookup(context.Background(), host, rd.QueryOptions())
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if len(addrs) == 0 {
|
|
return "", fmt.Errorf("no addresses for %s", host)
|
|
}
|
|
return addrs[0].String(), nil
|
|
}
|
|
|
|
func (h *VKJoiner) joinCall() error {
|
|
apiURL := h.authParams.APIBaseURL
|
|
parsed, err := url.Parse(apiURL)
|
|
if err != nil {
|
|
return fmt.Errorf("bad apiBaseURL: %w", err)
|
|
}
|
|
screenFlag := "false"
|
|
if h.dualTrack {
|
|
screenFlag = "true"
|
|
}
|
|
body := url.Values{
|
|
"method": {"vchat.joinConversationByLink"},
|
|
"session_key": {h.authParams.SessionKey},
|
|
"application_key": {h.authParams.ApplicationKey},
|
|
"joinLink": {h.authParams.JoinLink},
|
|
"anonymToken": {h.authParams.AnonymToken},
|
|
"isVideo": {"true"},
|
|
"isAudio": {"false"},
|
|
"mediaSettings": {`{"isAudioEnabled":false,"isVideoEnabled":true,"isScreenSharingEnabled":` + screenFlag + `}`},
|
|
"format": {"json"},
|
|
}
|
|
client := &http.Client{
|
|
Timeout: 15 * time.Second,
|
|
Transport: &http.Transport{
|
|
TLSClientConfig: &tls.Config{ServerName: parsed.Hostname()},
|
|
DialContext: h.dialContext,
|
|
},
|
|
}
|
|
req, err := http.NewRequest("POST", apiURL, strings.NewReader(body.Encode()))
|
|
if err != nil {
|
|
return fmt.Errorf("new request: %w", err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
req.Header.Set("User-Agent", common.UserAgent)
|
|
h.logger.Debug("vk-joiner: calling joinConversationByLink...")
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("joinConversationByLink: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
raw, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return fmt.Errorf("read join response: %w", err)
|
|
}
|
|
var joinResp VKJoinResponse
|
|
if jsonErr := json.Unmarshal(raw, &joinResp); jsonErr != nil {
|
|
return fmt.Errorf("decode join response: %w (body: %s)", jsonErr, truncateBody(raw))
|
|
}
|
|
if joinResp.Endpoint == "" {
|
|
if rotten := detectVKAuthRotten(raw); rotten != nil {
|
|
return rotten
|
|
}
|
|
return fmt.Errorf("empty endpoint in join response: %s", truncateBody(raw))
|
|
}
|
|
h.joinResp = &joinResp
|
|
h.logger.Debug(fmt.Sprintf("vk-joiner: joined, turn=%v", joinResp.TurnServer.URLs))
|
|
return nil
|
|
}
|
|
|
|
func (h *VKJoiner) connectSFU() {
|
|
endpoint := h.joinResp.WtEndpoint
|
|
if endpoint == "" {
|
|
h.logger.Error("vk-joiner: no wt_endpoint in join response, cannot connect")
|
|
return
|
|
}
|
|
parsed, err := url.Parse(endpoint)
|
|
if err != nil {
|
|
h.logger.Error(fmt.Sprintf("vk-joiner: bad endpoint URL: %s", common.MaskError(err)))
|
|
return
|
|
}
|
|
hostname := parsed.Hostname()
|
|
resolvedIP, err := h.resolveHost(hostname)
|
|
if err != nil {
|
|
h.logger.Error(fmt.Sprintf("vk-joiner: DNS resolve failed: %s", common.MaskError(err)))
|
|
return
|
|
}
|
|
h.logger.Debug(fmt.Sprintf("vk-joiner: resolved %s -> %s", common.MaskAddr(hostname), common.MaskAddr(resolvedIP)))
|
|
capabilities := "2F7F"
|
|
wtURL := endpoint +
|
|
"&platform=WEB" +
|
|
"&appVersion=" + h.authParams.AppVersion +
|
|
"&version=" + h.authParams.ProtocolVersion +
|
|
"&device=browser&capabilities=" + capabilities + "&clientType=VK&tgt=join&compression=deflate-raw"
|
|
sfu, err := wtsignal.Dial(wtURL, hostname, resolvedIP)
|
|
if err != nil {
|
|
h.logger.Error(fmt.Sprintf("vk-joiner: WebTransport connect failed: %s", common.MaskError(err)))
|
|
return
|
|
}
|
|
h.vkMu.Lock()
|
|
h.sfu = sfu
|
|
h.vkSeq = 0
|
|
h.vkMu.Unlock()
|
|
h.logger.Debug("vk-joiner: WebTransport connected")
|
|
h.vkSend("update-media-modifiers", map[string]interface{}{
|
|
"mediaModifiers": map[string]interface{}{"denoise": true, "denoiseAnn": true},
|
|
})
|
|
h.vkSend("change-media-settings", map[string]interface{}{
|
|
"mediaSettings": map[string]interface{}{
|
|
"isAudioEnabled": false, "isVideoEnabled": true,
|
|
"isScreenSharingEnabled": h.dualTrack, "isFastScreenSharingEnabled": false,
|
|
"isAudioSharingEnabled": false, "isAnimojiEnabled": false,
|
|
},
|
|
})
|
|
h.readLoop()
|
|
}
|
|
|
|
func (h *VKJoiner) vkSend(command string, extra map[string]interface{}) {
|
|
h.vkMu.Lock()
|
|
defer h.vkMu.Unlock()
|
|
if h.sfu == nil {
|
|
return
|
|
}
|
|
h.vkSeq++
|
|
extra["command"] = command
|
|
extra["sequence"] = h.vkSeq
|
|
out, _ := json.Marshal(extra)
|
|
h.sfu.Send(out)
|
|
h.logger.Debug(fmt.Sprintf("vk-joiner: -> %s", command))
|
|
}
|
|
|
|
func (h *VKJoiner) vkSendTransmitData(participantId int64, payload map[string]interface{}) {
|
|
h.vkMu.Lock()
|
|
defer h.vkMu.Unlock()
|
|
if h.sfu == nil {
|
|
return
|
|
}
|
|
h.vkSeq++
|
|
payloadJSON, _ := json.Marshal(payload)
|
|
out := fmt.Sprintf(`{"command":"transmit-data","sequence":%d,"participantId":%d,"data":%s}`,
|
|
h.vkSeq, participantId, payloadJSON)
|
|
h.sfu.Send([]byte(out))
|
|
}
|
|
|
|
func (h *VKJoiner) readLoop() {
|
|
h.vkMu.Lock()
|
|
sfu := h.sfu
|
|
h.vkMu.Unlock()
|
|
if sfu == nil {
|
|
return
|
|
}
|
|
for {
|
|
msg, err := sfu.Recv()
|
|
if err != nil {
|
|
h.logger.Debug(fmt.Sprintf("vk-joiner: WebTransport closed: %s", common.MaskError(err)))
|
|
return
|
|
}
|
|
if string(msg) == "ping" {
|
|
sfu.Send([]byte("pong"))
|
|
continue
|
|
}
|
|
h.handleVKMessage(msg)
|
|
}
|
|
}
|
|
|
|
func (h *VKJoiner) handleVKMessage(raw []byte) {
|
|
var msg map[string]interface{}
|
|
if err := json.Unmarshal(raw, &msg); err != nil {
|
|
return
|
|
}
|
|
msgType, _ := msg["type"].(string)
|
|
switch msgType {
|
|
case "notification":
|
|
notif, _ := msg["notification"].(string)
|
|
switch notif {
|
|
case "connection":
|
|
h.handleConnection(msg)
|
|
case "transmitted-data":
|
|
data, _ := msg["data"].(map[string]interface{})
|
|
if data != nil {
|
|
if pid, ok := msg["participantId"].(float64); ok && h.remotePeerID == nil {
|
|
h.onRegisteredPeer(int64(pid))
|
|
}
|
|
h.onTransmittedData(data)
|
|
}
|
|
case "registered-peer":
|
|
if pid, ok := msg["participantId"].(float64); ok {
|
|
h.onRegisteredPeer(int64(pid))
|
|
}
|
|
case "topology-changed":
|
|
topo, _ := msg["topology"].(string)
|
|
h.logger.Debug(fmt.Sprintf("vk-joiner: topology: %s", topo))
|
|
if topo != "" && topo != vkTopologyDirect {
|
|
h.logger.Debug(fmt.Sprintf("vk-joiner: %s topology -> closing transport to reconnect and recover DIRECT", topo))
|
|
h.closeTransport()
|
|
}
|
|
case "participant-joined", "participant-added":
|
|
h.logger.Debug(fmt.Sprintf("vk-joiner: <- %s", notif))
|
|
case "participant-left":
|
|
h.logger.Debug(fmt.Sprintf("vk-joiner: <- %s", notif))
|
|
case "hungup":
|
|
h.logger.Debug("vk-joiner: peer hungup -> closing transport to reconnect")
|
|
h.closeTransport()
|
|
}
|
|
case "response":
|
|
seq, _ := msg["sequence"].(float64)
|
|
h.logger.Debug(fmt.Sprintf("vk-joiner: <- response seq=%d", int(seq)))
|
|
case "error":
|
|
errMsg, _ := msg["message"].(string)
|
|
errCode, _ := msg["error"].(string)
|
|
h.logger.Warn(fmt.Sprintf("vk-joiner: ERROR: %s %s", errCode, errMsg))
|
|
}
|
|
}
|
|
|
|
func (h *VKJoiner) handleConnection(msg map[string]interface{}) {
|
|
if conv, ok := msg["conversation"].(map[string]interface{}); ok {
|
|
topo, _ := conv["topology"].(string)
|
|
h.logger.Debug(fmt.Sprintf("vk-joiner: connection topology=%q", topo))
|
|
}
|
|
convParams, ok := msg["conversationParams"].(map[string]interface{})
|
|
if !ok {
|
|
return
|
|
}
|
|
turn, ok := convParams["turn"].(map[string]interface{})
|
|
if !ok {
|
|
return
|
|
}
|
|
urlsRaw, _ := turn["urls"].([]interface{})
|
|
var urls []string
|
|
for _, u := range urlsRaw {
|
|
if s, ok := u.(string); ok {
|
|
urls = append(urls, s)
|
|
}
|
|
}
|
|
username, _ := turn["username"].(string)
|
|
credential, _ := turn["credential"].(string)
|
|
h.joinResp.TurnServer.URLs = urls
|
|
h.joinResp.TurnServer.Username = username
|
|
h.joinResp.TurnServer.Credential = credential
|
|
h.logger.Debug(fmt.Sprintf("vk-joiner: TURN from connection: %v", urls))
|
|
if h.pc == nil {
|
|
h.initPC()
|
|
}
|
|
}
|
|
|
|
func (h *VKJoiner) initPC() {
|
|
var iceServers []webrtc.ICEServer
|
|
if len(h.joinResp.StunServer.URLs) > 0 {
|
|
iceServers = append(iceServers, webrtc.ICEServer{URLs: h.joinResp.StunServer.URLs})
|
|
}
|
|
if len(h.joinResp.TurnServer.URLs) > 0 {
|
|
iceServers = append(iceServers, webrtc.ICEServer{
|
|
URLs: h.joinResp.TurnServer.URLs,
|
|
Username: h.joinResp.TurnServer.Username,
|
|
Credential: h.joinResp.TurnServer.Credential,
|
|
})
|
|
}
|
|
mode := h.authParams.TunnelMode
|
|
settingEngine := webrtc.SettingEngine{}
|
|
settingEngine.DisableCloseByDTLS(true)
|
|
settingEngine.DetachDataChannels()
|
|
if h.PCConfig != nil {
|
|
h.PCConfig.ConfigureSettingEngine(&settingEngine)
|
|
}
|
|
pc, err := webrtc.NewAPI(webrtc.WithSettingEngine(settingEngine)).NewPeerConnection(webrtc.Configuration{
|
|
ICEServers: iceServers,
|
|
})
|
|
if err != nil {
|
|
h.logger.Error(fmt.Sprintf("vk-joiner: failed to create PC: %v", err))
|
|
return
|
|
}
|
|
h.pc = pc
|
|
h.logger.Debug(fmt.Sprintf("vk-joiner: tunnel mode: %s", mode))
|
|
if mode == "video" {
|
|
h.sampleTrack = h.AddTracks(pc, h.logger, "vk-joiner")
|
|
}
|
|
negotiated := true
|
|
dcID := uint16(2)
|
|
dc, err := pc.CreateDataChannel("tunnel", &webrtc.DataChannelInit{
|
|
Negotiated: &negotiated,
|
|
ID: &dcID,
|
|
})
|
|
if err != nil {
|
|
h.logger.Warn(fmt.Sprintf("vk-joiner: could not create tunnel DC: %v", err))
|
|
} else {
|
|
h.dc = dc
|
|
dc.OnOpen(func() {
|
|
h.logger.Debug("vk-joiner: tunnel DC open")
|
|
if mode == "dc" {
|
|
h.reconnectAttempt.Store(0)
|
|
h.logger.Info("vk-joiner: === DC TUNNEL CONNECTED ===")
|
|
if h.OnConnected != nil {
|
|
h.OnConnected(tunnel.NewDCTunnel(dc, h.obf, common.RTPBufSize, h.logger))
|
|
}
|
|
}
|
|
})
|
|
dc.OnClose(func() {
|
|
h.logger.Debug("vk-joiner: tunnel DC closed")
|
|
})
|
|
}
|
|
pc.OnICECandidate(func(candidate *webrtc.ICECandidate) {
|
|
if candidate == nil {
|
|
return
|
|
}
|
|
h.onLocalICECandidate(candidate)
|
|
})
|
|
pc.OnConnectionStateChange(func(state webrtc.PeerConnectionState) {
|
|
h.logger.Debug(fmt.Sprintf("vk-joiner: PC state: %s", state.String()))
|
|
if state == webrtc.PeerConnectionStateFailed || state == webrtc.PeerConnectionStateDisconnected {
|
|
h.logger.Debug(fmt.Sprintf("vk-joiner: PC %s, closing transport to trigger reconnect", state.String()))
|
|
h.closeTransport()
|
|
}
|
|
if mode == "video" && state == webrtc.PeerConnectionStateConnected && h.vp8tunnel == nil {
|
|
h.reconnectAttempt.Store(0)
|
|
h.logger.Info("vk-joiner: === TUNNEL CONNECTED ===")
|
|
h.vp8tunnel = tunnel.NewVP8DataTunnel(h.sampleTrack, h.obf, h.logger)
|
|
h.vp8tunnel.Start(h.vp8FPS, h.vp8Batch)
|
|
var downlink tunnel.DataTunnel = h.vp8tunnel
|
|
trackCount := 1
|
|
if h.dualTrack {
|
|
writer := tunnel.NewScreenWriter(h.obf, "screen-up", h.logger)
|
|
writer.Reconfigure(h.vp8tunnel.FPS(), h.vp8tunnel.Batch())
|
|
writer.SetSend(h.producerScreen.send)
|
|
h.sym = tunnel.NewSymmetricScreenTunnel(h.vp8tunnel, writer, h.obf, h.producerScreen.ready, h.logger)
|
|
h.sym.SetTrackCount(2)
|
|
downlink = h.sym
|
|
trackCount = 2
|
|
h.logger.Info("vk-joiner: === SYMMETRIC DUAL-TRACK: camera VP8 + screen DCs ===")
|
|
}
|
|
vp8tun := h.vp8tunnel
|
|
if !h.configAck.Acknowledged() {
|
|
acked, cancel := h.configAck.Arm()
|
|
go tunnel.SendVP8ConfigUntilAcked(acked, cancel, h.stopCh, vp8tun,
|
|
vp8tun.FPS(), vp8tun.Batch(), trackCount, h.logger, "vk-joiner")
|
|
h.logger.Debug(fmt.Sprintf("vk-joiner: pushed vp8 config to creator fps=%d batch=%d trackCount=%d", vp8tun.FPS(), vp8tun.Batch(), trackCount))
|
|
}
|
|
if h.OnConnected != nil {
|
|
h.OnConnected(downlink)
|
|
}
|
|
}
|
|
})
|
|
if mode == "video" {
|
|
pc.OnDataChannel(func(dc *webrtc.DataChannel) {
|
|
h.logger.Debug(fmt.Sprintf("vk-joiner: remote DataChannel: label=%q id=%v", dc.Label(), dc.ID()))
|
|
if !h.dualTrack {
|
|
return
|
|
}
|
|
switch dc.Label() {
|
|
case "consumerScreenShare":
|
|
readScreenDataChannel(dc, func(frame []byte) {
|
|
if h.sym != nil {
|
|
h.sym.HandleScreenFrame(frame)
|
|
}
|
|
}, h.logger)
|
|
case "producerScreenShare":
|
|
attachScreenWriterDC(dc, h.producerScreen.attach, h.logger)
|
|
}
|
|
})
|
|
pc.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {
|
|
h.logger.Debug(fmt.Sprintf("vk-joiner: remote track: codec=%s ssrc=%d", track.Codec().MimeType, track.SSRC()))
|
|
go h.ReadTrackFn(track, func(frame []byte) {
|
|
if h.vp8tunnel != nil {
|
|
h.vp8tunnel.HandleFrame(frame)
|
|
}
|
|
}, h.logger, "vk-joiner")
|
|
})
|
|
}
|
|
h.logger.Debug("vk-joiner: PC ready, waiting for remote offer")
|
|
}
|
|
|
|
func (h *VKJoiner) onRegisteredPeer(pid int64) {
|
|
h.remotePeerID = &pid
|
|
h.logger.Debug(fmt.Sprintf("vk-joiner: peer registered: %d", pid))
|
|
}
|
|
|
|
func (h *VKJoiner) onLocalICECandidate(candidate *webrtc.ICECandidate) {
|
|
if h.remotePeerID == nil {
|
|
return
|
|
}
|
|
candidateJSON := candidate.ToJSON()
|
|
raw, _ := json.Marshal(candidateJSON)
|
|
var parsed interface{}
|
|
json.Unmarshal(raw, &parsed)
|
|
h.vkSendTransmitData(*h.remotePeerID, map[string]interface{}{"candidate": parsed})
|
|
}
|
|
|
|
func (h *VKJoiner) onTransmittedData(data map[string]interface{}) {
|
|
if h.pc == nil {
|
|
return
|
|
}
|
|
if candidate, ok := data["candidate"]; ok {
|
|
candidateJSON, _ := json.Marshal(candidate)
|
|
var candidateInit webrtc.ICECandidateInit
|
|
json.Unmarshal(candidateJSON, &candidateInit)
|
|
if h.OnRemoteCandidate != nil {
|
|
h.OnRemoteCandidate(0, candidateInit.Candidate)
|
|
}
|
|
if h.remoteSet {
|
|
h.pc.AddICECandidate(candidateInit)
|
|
} else {
|
|
h.pendingICE = append(h.pendingICE, candidateInit)
|
|
}
|
|
}
|
|
if sdp, ok := data["sdp"].(map[string]interface{}); ok {
|
|
sdpType, _ := sdp["type"].(string)
|
|
sdpStr, _ := sdp["sdp"].(string)
|
|
if h.OnRemoteCandidate != nil {
|
|
h.OnRemoteCandidate(-1, sdpStr)
|
|
}
|
|
h.logger.Debug(fmt.Sprintf("vk-joiner: remote SDP: %s", sdpType))
|
|
if sdpType == "answer" {
|
|
h.pc.SetRemoteDescription(webrtc.SessionDescription{Type: webrtc.SDPTypeAnswer, SDP: sdpStr})
|
|
h.remoteSet = true
|
|
for _, candidate := range h.pendingICE {
|
|
h.pc.AddICECandidate(candidate)
|
|
}
|
|
h.pendingICE = nil
|
|
} else if sdpType == "offer" {
|
|
h.pc.SetRemoteDescription(webrtc.SessionDescription{Type: webrtc.SDPTypeOffer, SDP: sdpStr})
|
|
h.remoteSet = true
|
|
for _, candidate := range h.pendingICE {
|
|
h.pc.AddICECandidate(candidate)
|
|
}
|
|
h.pendingICE = nil
|
|
answer, err := h.pc.CreateAnswer(nil)
|
|
if err != nil || h.remotePeerID == nil {
|
|
h.logger.Warn(fmt.Sprintf("vk-joiner: create answer failed: %v", err))
|
|
return
|
|
}
|
|
h.pc.SetLocalDescription(answer)
|
|
sdpJSON, _ := json.Marshal(answer.SDP)
|
|
h.vkMu.Lock()
|
|
if h.sfu != nil {
|
|
h.vkSeq++
|
|
raw := fmt.Sprintf(`{"command":"transmit-data","sequence":%d,"participantId":%d,"data":{"sdp":{"sdp":%s,"type":%q},"animojiVersion":2},"participantType":"USER"}`,
|
|
h.vkSeq, *h.remotePeerID, sdpJSON, answer.Type.String())
|
|
h.sfu.Send([]byte(raw))
|
|
h.logger.Debug(fmt.Sprintf("vk-joiner: -> answer (seq=%d)", h.vkSeq))
|
|
}
|
|
h.vkMu.Unlock()
|
|
}
|
|
}
|
|
}
|
|
|
|
func detectVKAuthRotten(raw []byte) *vkAuthRottenError {
|
|
var generic map[string]interface{}
|
|
if err := json.Unmarshal(raw, &generic); err != nil {
|
|
return nil
|
|
}
|
|
errCode, _ := generic["error_code"].(string)
|
|
if errCode == "" {
|
|
if codeNum, ok := generic["error_code"].(float64); ok {
|
|
errCode = fmt.Sprintf("%.0f", codeNum)
|
|
}
|
|
}
|
|
if errCode == "" {
|
|
errCode, _ = generic["errorCode"].(string)
|
|
}
|
|
if errCode == "" {
|
|
return nil
|
|
}
|
|
switch errCode {
|
|
case "SESSION_EXPIRED", "AUTH_LOGIN", "SESSION_NOT_FOUND", "INVALID_SESSION_KEY":
|
|
errMsg, _ := generic["error_msg"].(string)
|
|
return &vkAuthRottenError{Code: errCode, Msg: errMsg}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func truncateBody(raw []byte) string {
|
|
const maxLen = 200
|
|
if len(raw) > maxLen {
|
|
return string(raw[:maxLen]) + "..."
|
|
}
|
|
return string(raw)
|
|
}
|