// Package teamspeak 提供 gomobile 友好的 TeamSpeak 客户端 API。 // // gomobile 限制: // - 不支持 []string、[]*T 等复杂切片类型导出 // - 不支持 Go 的 error 类型,改用 string 返回错误 // - 回调必须通过 interface 定义 // - 复杂数据通过 JSON 字符串传递 package teamspeak import ( "context" "encoding/json" "fmt" "log" "sync" "time" ts "github.com/honeybbq/teamspeak-go" "github.com/honeybbq/teamspeak-go/crypto" ) // queuedEvent 封装待处理的事件,用于 JNI 回调串行化 type queuedEvent struct { evtType string data any } // eventQueue owns one callback consumer. A new connection receives a fresh // instance so a stopped consumer can never resume against a later session. const maxQueuedPCMEvents = 12 type eventQueue struct { mu sync.Mutex cond *sync.Cond events []queuedEvent callback EventCallback stopped bool pcmCount int stopOnce sync.Once } func newEventQueue(callback EventCallback) *eventQueue { q := &eventQueue{callback: callback} q.cond = sync.NewCond(&q.mu) return q } // ─── 内部数据类型(不导出给 gomobile) ───────────────────── // serverInfoJSON 用于 JSON 序列化的服务器信息 type serverInfoJSON struct { Name string `json:"name"` WelcomeMessage string `json:"welcomeMessage"` MaxClients int `json:"maxClients"` ClientsOnline int `json:"clientsOnline"` ChannelsOnline int `json:"channelsOnline"` Uptime string `json:"uptime"` Version string `json:"version"` Platform string `json:"platform"` Created string `json:"created"` IconID string `json:"iconId"` DefaultServerGroup int `json:"defaultServerGroup"` DefaultChannelGroup int `json:"defaultChannelGroup"` } // channelDetailedInfoJSON 用于 JSON 序列化的单频道详情(来自 channelinfo cid=X) type channelDetailedInfoJSON struct { ID string `json:"id"` ParentID string `json:"parentId"` Name string `json:"name"` Topic string `json:"topic"` Description string `json:"description"` Codec int `json:"codec"` CodecQuality int `json:"codecQuality"` MaxClients int `json:"maxClients"` MaxFamilyClients int `json:"maxFamilyClients"` NeededTalkPower int `json:"neededTalkPower"` IconID string `json:"iconId"` IsPermanent bool `json:"isPermanent"` IsSemiPermanent bool `json:"isSemiPermanent"` IsDefault bool `json:"isDefault"` IsPassword bool `json:"isPassword"` Order string `json:"order"` BannerGfxURL string `json:"bannerGfxUrl"` } // clientDetailedInfoJSON 用于 JSON 序列化的单客户端详情(来自 clientinfo clid=X) type clientDetailedInfoJSON struct { ID string `json:"id"` Nickname string `json:"nickname"` UID string `json:"uid"` ChannelID string `json:"channelId"` Type int `json:"type"` ServerGroups []string `json:"serverGroups"` Away bool `json:"away"` AwayMessage string `json:"awayMessage"` InputMuted bool `json:"inputMuted"` OutputMuted bool `json:"outputMuted"` Platform string `json:"platform"` Version string `json:"version"` IP string `json:"ip"` Created string `json:"created"` LastConnected string `json:"lastConnected"` TotalConnections int `json:"totalConnections"` Description string `json:"description"` IconID string `json:"iconId"` } // dbClientJSON 用于 JSON 序列化的数据库客户端 type dbClientJSON struct { DBID string `json:"dbid"` UID string `json:"uid"` Nickname string `json:"nickname"` Created string `json:"created"` LastConnected string `json:"lastConnected"` TotalConnections int `json:"totalConnections"` Description string `json:"description"` } // banEntryJSON 用于 JSON 序列化的 ban 条目 type banEntryJSON struct { BanID string `json:"banId"` IP string `json:"ip"` Name string `json:"name"` UID string `json:"uid"` Created string `json:"created"` InvokerName string `json:"invokerName"` InvokerUID string `json:"invokerUid"` Reason string `json:"reason"` Enforcement bool `json:"enforcement"` } // fileEntryJSON 用于 JSON 序列化的文件条目 type fileEntryJSON struct { Name string `json:"name"` Size string `json:"size"` DateTime string `json:"dateTime"` IsFile bool `json:"isFile"` } // tokenEntryJSON 用于 JSON 序列化的 token 条目 type tokenEntryJSON struct { Token string `json:"token"` TokenType int `json:"tokenType"` TokenID1 string `json:"tokenId1"` TokenID2 string `json:"tokenId2"` Created string `json:"created"` Description string `json:"description"` } // complaintEntryJSON 用于 JSON 序列化的投诉条目 type complaintEntryJSON struct { FromDBID string `json:"fromDbid"` ToDBID string `json:"toDbid"` Message string `json:"message"` Timestamp string `json:"timestamp"` } // channelJSON 用于 JSON 序列化的频道信息 type channelJSON struct { ID string `json:"id"` Name string `json:"name"` ParentID string `json:"parentId"` Description string `json:"description"` IsPassword bool `json:"isPassword"` } // channelDetailedJSON 用于 JSON 序列化的详细频道信息(含 flags、voice、limits、icon) type channelDetailedJSON struct { ID string `json:"id"` Name string `json:"name"` ParentID string `json:"parentId"` Topic string `json:"topic"` Order string `json:"order"` Codec int `json:"codec"` CodecQuality int `json:"codecQuality"` NeededTalkPower int `json:"neededTalkPower"` MaxClients int `json:"maxClients"` MaxFamilyClients int `json:"maxFamilyClients"` IsMaxClientsUnlimited bool `json:"isMaxClientsUnlimited"` IsMaxFamilyClientsUnlimited bool `json:"isMaxFamilyClientsUnlimited"` IsPermanent bool `json:"isPermanent"` IsSemiPermanent bool `json:"isSemiPermanent"` IsDefault bool `json:"isDefault"` IsPassword bool `json:"isPassword"` IsOrdered bool `json:"isOrdered"` IconID string `json:"iconId"` NeededModifyPower int `json:"neededModifyPower"` } // clientJSON 用于 JSON 序列化的客户端信息 type clientJSON struct { ID int `json:"id"` Nickname string `json:"nickname"` UID string `json:"uid"` ChannelID string `json:"channelId"` ServerGroups []string `json:"serverGroups"` IsSelf bool `json:"isSelf"` } // ─── 数据类型(gomobile 可导出) ──────────────────────────── // Channel 频道信息(gomobile 导出) type Channel struct { ID string Name string ParentID string Description string } // Client 客户端信息(gomobile 导出) type Client struct { ID int Nickname string UID string ChannelID string IsSelf bool } // TextMsg 文字消息(gomobile 导出) type TextMsg struct { InvokerName string InvokerUID string Message string TargetMode int TargetID string } // PokeEvent Poke 事件(gomobile 导出) type PokeEvent struct { InvokerID int InvokerName string InvokerUID string Message string } // ─── 事件回调接口 ───────────────────────────────────────── // EventCallback 事件回调接口,Android 端实现此接口接收事件 type EventCallback interface { OnConnected() OnDisconnected(message string) OnTextMessage(msg *TextMsg) OnClientEnter(client *Client) OnClientLeave(id int, reasonMsg string) OnClientMoved(id int, targetChannelID string) OnKicked(reason string) OnVoiceData(clientID int, data []byte, codec int, sequence int, isWhisper bool) // OnMixedVoicePCM receives a 20 ms, 48 kHz interleaved stereo PCM16 little-endian frame. OnMixedVoicePCM(pcm []byte) OnClientSpeaking(clientID int, speaking bool) OnPoked(event *PokeEvent) } // ─── TSClient 主类 ──────────────────────────────────────── // TSClient TeamSpeak 客户端封装 type TSClient struct { mu sync.Mutex client *ts.Client callback EventCallback connected bool host string // 服务器地址(用于文件传输 TCP 连接) // 事件队列:保证所有 JNI 回调在同一个协程中顺序执行 evtQueueMu sync.Mutex // protects eventQueue replacement evtQueue *eventQueue // receiveFactory is platform-owned and intentionally private so gomobile // never exposes the native decoder implementation. Android supplies a // libopus-backed factory; unsupported host builds leave it nil. receiveFactory voiceDecoderFactory receive *receiveAudio } // NewClient 创建新的 TeamSpeak 客户端 func NewClient() *TSClient { factory := newPlatformVoiceDecoderFactory() log.Printf("[TSVoice] platform decoder factory=%T available=%t", factory, factory != nil) return &TSClient{receiveFactory: factory} } // Connect 连接到 TeamSpeak 服务器 // 返回空字符串表示成功,非空为错误信息 func (c *TSClient) Connect(host, nickname, password, defaultChannel, defaultChannelPassword string, cb EventCallback) string { // Ensure repeated attempts on this wrapper cannot leave a prior transport alive. c.Disconnect() // A prior failed or disconnected session may have left its consumer waiting. // Stop it before replacing the callback and starting this session's queue. c.stopEventQueue() c.mu.Lock() c.callback = cb c.connected = false c.host = host c.mu.Unlock() c.initEventQueue() c.ensureReceiveFactory() c.StartReceiveAudio() identity, err := crypto.GenerateIdentity(8) if err != nil { return fmt.Sprintf("生成身份失败: %v", err) } opts := []ts.ClientOption{} if password != "" { opts = append(opts, ts.WithServerPassword(password)) } if defaultChannel != "" { opts = append(opts, ts.WithDefaultChannel(defaultChannel)) } if defaultChannelPassword != "" { opts = append(opts, ts.WithDefaultChannelPassword(defaultChannelPassword)) } client := ts.NewClient(identity, host, nickname, opts...) c.mu.Lock() c.client = client c.mu.Unlock() // 注册事件 c.registerEvents(client) if err := client.Connect(); err != nil { c.Disconnect() return fmt.Sprintf("连接失败: %v", err) } ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() if err := client.WaitConnected(ctx); err != nil { c.Disconnect() return fmt.Sprintf("连接失败: %v", err) } return "" } // Disconnect 断开连接 func (c *TSClient) Disconnect() { c.StopReceiveAudio() // 停止事件消费协程 c.stopEventQueue() c.mu.Lock() client := c.client c.client = nil c.connected = false c.mu.Unlock() if client != nil { if err := client.Disconnect(); err != nil { log.Printf("[TSBridge] disconnect error: %v", err) } } } // IsConnected 是否已连接 func (c *TSClient) IsConnected() bool { c.mu.Lock() defer c.mu.Unlock() return c.connected && c.client != nil } // GetChannelsJSON 获取频道列表(JSON 格式) // gomobile 不支持返回 []*Channel,所以用 JSON 字符串传递 func (c *TSClient) GetChannelsJSON() string { c.mu.Lock() client := c.client c.mu.Unlock() if client == nil { log.Printf("[TSBridge] GetChannelsJSON: client is nil") return "[]" } log.Printf("[TSBridge] GetChannelsJSON: calling ListChannelsDetailed...") channels, err := client.ListChannelsDetailed() if err != nil { log.Printf("[TSBridge] ListChannelsDetailed error: %v", err) return "[]" } log.Printf("[TSBridge] ListChannelsDetailed returned %d channels", len(channels)) result := make([]channelJSON, len(channels)) for i, ch := range channels { parentID := "0" if ch.ParentID != 0 { parentID = fmt.Sprintf("%d", ch.ParentID) } result[i] = channelJSON{ ID: fmt.Sprintf("%d", ch.ID), Name: ch.Name, ParentID: parentID, IsPassword: ch.IsPassword, } } data, err := json.Marshal(result) if err != nil { log.Printf("[TSBridge] JSON marshal error: %v", err) return "[]" } return string(data) } // GetChannelsDetailedJSON 获取详细频道列表(JSON 格式) // 通过 channellist -topic -flags -voice -limits -icon 获取更丰富的频道属性, // 包括:密码标志、编解码器、人数限制、永久/半永久标志、图标 ID 等。 // gomobile 不支持返回 []*ChannelInfoDetailed,所以用 JSON 字符串传递。 func (c *TSClient) GetChannelsDetailedJSON() string { c.mu.Lock() client := c.client c.mu.Unlock() if client == nil { return "[]" } channels, err := client.ListChannelsDetailed() if err != nil { log.Printf("[TSBridge] ListChannelsDetailed error: %v", err) return "[]" } result := make([]channelDetailedJSON, len(channels)) for i, ch := range channels { parentID := "0" if ch.ParentID != 0 { parentID = fmt.Sprintf("%d", ch.ParentID) } result[i] = channelDetailedJSON{ ID: fmt.Sprintf("%d", ch.ID), Name: ch.Name, ParentID: parentID, Topic: ch.Topic, Order: fmt.Sprintf("%d", ch.Order), Codec: ch.Codec, CodecQuality: ch.CodecQuality, NeededTalkPower: ch.NeededTalkPower, MaxClients: ch.MaxClients, MaxFamilyClients: ch.MaxFamilyClients, IsMaxClientsUnlimited: ch.IsMaxClientsUnlimited, IsMaxFamilyClientsUnlimited: ch.IsMaxFamilyClientsUnlimited, IsPermanent: ch.IsPermanent, IsSemiPermanent: ch.IsSemiPermanent, IsDefault: ch.IsDefault, IsPassword: ch.IsPassword, IsOrdered: ch.Order > 0, IconID: fmt.Sprintf("%d", ch.IconID), NeededModifyPower: ch.NeededModifyPower, } } data, err := json.Marshal(result) if err != nil { log.Printf("[TSBridge] JSON marshal error: %v", err) return "[]" } return string(data) } // GetClientsJSON 获取在线客户端列表(JSON 格式) // gomobile 不支持返回 []*Client,所以用 JSON 字符串传递 func (c *TSClient) GetClientsJSON() string { c.mu.Lock() client := c.client c.mu.Unlock() if client == nil { return "[]" } clients, err := client.ListClients() if err != nil { log.Printf("[TSBridge] ListClients error: %v", err) return "[]" } myID := client.ClientID() result := make([]clientJSON, len(clients)) for i, cl := range clients { result[i] = clientJSON{ ID: int(cl.ID), Nickname: cl.Nickname, UID: cl.UID, ChannelID: fmt.Sprintf("%d", cl.ChannelID), ServerGroups: cl.ServerGroups, IsSelf: cl.ID == myID, } } data, err := json.Marshal(result) if err != nil { log.Printf("[TSBridge] JSON marshal error: %v", err) return "[]" } return string(data) } // ─── 服务器信息 ──────────────────────────────────────────── // GetServerInfoJSON 获取服务器信息(JSON 格式) // gomobile 不支持返回 *ServerInfo,所以用 JSON 字符串传递 func (c *TSClient) GetServerInfoJSON() string { c.mu.Lock() client := c.client c.mu.Unlock() if client == nil { return "{}" } info, err := client.GetServerInfo() if err != nil { log.Printf("[TSBridge] GetServerInfo error: %v", err) return "{}" } result := serverInfoJSON{ Name: info.Name, WelcomeMessage: info.WelcomeMessage, MaxClients: info.MaxClients, ClientsOnline: info.ClientsOnline, ChannelsOnline: info.ChannelsOnline, Uptime: fmt.Sprintf("%d", info.Uptime), Version: info.Version, Platform: info.Platform, Created: fmt.Sprintf("%d", info.Created), IconID: fmt.Sprintf("%d", info.IconID), DefaultServerGroup: info.DefaultServerGroup, DefaultChannelGroup: info.DefaultChannelGroup, } data, err := json.Marshal(result) if err != nil { log.Printf("[TSBridge] JSON marshal error: %v", err) return "{}" } return string(data) } // ─── 频道详情 ────────────────────────────────────────────── // GetChannelDetailInfoJSON 获取单个频道的详细信息(JSON 格式) // 通过 channelinfo cid=X 获取完整的频道属性(含 description)。 // 返回空字符串表示错误。 func (c *TSClient) GetChannelDetailInfoJSON(channelIDStr string) string { c.mu.Lock() client := c.client c.mu.Unlock() if client == nil { return "{}" } var channelID uint64 if _, err := fmt.Sscanf(channelIDStr, "%d", &channelID); err != nil { return "{}" } info, err := client.GetChannelInfo(channelID) if err != nil { log.Printf("[TSBridge] GetChannelInfo error: %v", err) return "{}" } result := channelDetailedInfoJSON{ ID: fmt.Sprintf("%d", info.ID), ParentID: fmt.Sprintf("%d", info.ParentID), Name: info.Name, Topic: info.Topic, Description: info.Description, Codec: info.Codec, CodecQuality: info.CodecQuality, MaxClients: info.MaxClients, MaxFamilyClients: info.MaxFamilyClients, NeededTalkPower: info.NeededTalkPower, IconID: fmt.Sprintf("%d", info.IconID), IsPermanent: info.IsPermanent, IsSemiPermanent: info.IsSemiPermanent, IsDefault: info.IsDefault, IsPassword: info.IsPassword, Order: fmt.Sprintf("%d", info.Order), BannerGfxURL: info.BannerGfxURL, } data, err := json.Marshal(result) if err != nil { log.Printf("[TSBridge] JSON marshal error: %v", err) return "{}" } return string(data) } // ─── 客户端详情 ──────────────────────────────────────────── // GetClientDetailInfoJSON 获取单个客户端的详细信息(JSON 格式) // 通过 clientinfo clid=X 获取完整的客户端属性。 func (c *TSClient) GetClientDetailInfoJSON(clid int) string { c.mu.Lock() client := c.client c.mu.Unlock() if client == nil { return "{}" } info, err := client.GetClientDetailInfo(uint16(clid)) if err != nil { log.Printf("[TSBridge] GetClientDetailInfo error: %v", err) return "{}" } result := clientDetailedInfoJSON{ ID: fmt.Sprintf("%d", info.ID), Nickname: info.Nickname, UID: info.UID, ChannelID: fmt.Sprintf("%d", info.ChannelID), Type: info.Type, ServerGroups: info.ServerGroups, Away: info.Away, AwayMessage: info.AwayMessage, InputMuted: info.InputMuted, OutputMuted: info.OutputMuted, Platform: info.Platform, Version: info.Version, IP: info.IP, Created: fmt.Sprintf("%d", info.Created), LastConnected: fmt.Sprintf("%d", info.LastConnected), TotalConnections: info.TotalConnections, Description: info.Description, IconID: fmt.Sprintf("%d", info.IconID), } data, err := json.Marshal(result) if err != nil { log.Printf("[TSBridge] JSON marshal error: %v", err) return "{}" } return string(data) } // ─── 数据库客户端列表 ────────────────────────────────────── // ListDBClientsJSON 获取服务器数据库中的客户端列表(JSON 格式) // start: 起始位置, duration: 返回数量(0=全部) func (c *TSClient) ListDBClientsJSON(start, duration int) string { c.mu.Lock() client := c.client c.mu.Unlock() if client == nil { return "[]" } clients, err := client.ListDBClients(start, duration) if err != nil { log.Printf("[TSBridge] ListDBClients error: %v", err) return "[]" } result := make([]dbClientJSON, len(clients)) for i, cl := range clients { result[i] = dbClientJSON{ DBID: fmt.Sprintf("%d", cl.DBID), UID: cl.UID, Nickname: cl.Nickname, Created: fmt.Sprintf("%d", cl.Created), LastConnected: fmt.Sprintf("%d", cl.LastConnected), TotalConnections: cl.TotalConnections, Description: cl.Description, } } data, err := json.Marshal(result) if err != nil { log.Printf("[TSBridge] JSON marshal error: %v", err) return "[]" } return string(data) } // ─── Ban 管理 ───────────────────────────────────────────── // ListBansJSON 获取 ban 列表(JSON 格式) func (c *TSClient) ListBansJSON() string { c.mu.Lock() client := c.client c.mu.Unlock() if client == nil { return "[]" } bans, err := client.ListBans() if err != nil { log.Printf("[TSBridge] ListBans error: %v", err) return "[]" } result := make([]banEntryJSON, len(bans)) for i, b := range bans { result[i] = banEntryJSON{ BanID: fmt.Sprintf("%d", b.BanID), IP: b.IP, Name: b.Name, UID: b.UID, Created: fmt.Sprintf("%d", b.Created), InvokerName: b.InvokerName, InvokerUID: b.InvokerUID, Reason: b.Reason, Enforcement: b.Enforcement, } } data, err := json.Marshal(result) if err != nil { log.Printf("[TSBridge] JSON marshal error: %v", err) return "[]" } return string(data) } // AddBan 添加 ban 条目。ip/name/uid 至少指定一个。timeSeconds=0 为永久。 // 返回空字符串表示成功。 func (c *TSClient) AddBan(ip, name, uid string, timeSeconds int, reason string) string { c.mu.Lock() client := c.client c.mu.Unlock() if client == nil { return "未连接" } if err := client.AddBan(ip, name, uid, timeSeconds, reason); err != nil { return fmt.Sprintf("添加失败: %v", err) } return "" } // DeleteBan 删除 ban 条目。返回空字符串表示成功。 func (c *TSClient) DeleteBan(banIDStr string) string { c.mu.Lock() client := c.client c.mu.Unlock() if client == nil { return "未连接" } var banID int64 if _, err := fmt.Sscanf(banIDStr, "%d", &banID); err != nil { return fmt.Sprintf("无效的 ban ID: %v", err) } if err := client.DeleteBan(banID); err != nil { return fmt.Sprintf("删除失败: %v", err) } return "" } // ─── Token 管理 ─────────────────────────────────────────── // ListTokensJSON 获取 token 列表(JSON 格式) func (c *TSClient) ListTokensJSON() string { c.mu.Lock() client := c.client c.mu.Unlock() if client == nil { return "[]" } tokens, err := client.ListTokens() if err != nil { log.Printf("[TSBridge] ListTokens error: %v", err) return "[]" } result := make([]tokenEntryJSON, len(tokens)) for i, t := range tokens { result[i] = tokenEntryJSON{ Token: t.Token, TokenType: t.TokenType, TokenID1: fmt.Sprintf("%d", t.TokenID1), TokenID2: fmt.Sprintf("%d", t.TokenID2), Created: fmt.Sprintf("%d", t.Created), Description: t.Description, } } data, err := json.Marshal(result) if err != nil { log.Printf("[TSBridge] JSON marshal error: %v", err) return "[]" } return string(data) } // UseToken 使用一个 privilege key。返回空字符串表示成功。 func (c *TSClient) UseToken(token string) string { c.mu.Lock() client := c.client c.mu.Unlock() if client == nil { return "未连接" } if err := client.UseToken(token); err != nil { return fmt.Sprintf("使用失败: %v", err) } return "" } // ─── 文件列表 ───────────────────────────────────────────── // ListFilesJSON 获取频道目录下的文件列表(JSON 格式) // path: 虚拟路径,根目录为 "/" func (c *TSClient) ListFilesJSON(channelIDStr, path string) string { c.mu.Lock() client := c.client c.mu.Unlock() if client == nil { return "[]" } var channelID uint64 if _, err := fmt.Sscanf(channelIDStr, "%d", &channelID); err != nil { return "[]" } files, err := client.ListFiles(channelID, path) if err != nil { log.Printf("[TSBridge] ListFiles error: %v", err) return "[]" } result := make([]fileEntryJSON, len(files)) for i, f := range files { result[i] = fileEntryJSON{ Name: f.Name, Size: fmt.Sprintf("%d", f.Size), DateTime: fmt.Sprintf("%d", f.DateTime), IsFile: f.IsFile, } } data, err := json.Marshal(result) if err != nil { log.Printf("[TSBridge] JSON marshal error: %v", err) return "[]" } return string(data) } // ─── 投诉管理 ───────────────────────────────────────────── // ListComplaintsJSON 获取投诉列表(JSON 格式) // targetDBIDStr: 目标客户端 DBID,"0" 表示返回全部投诉 func (c *TSClient) ListComplaintsJSON(targetDBIDStr string) string { c.mu.Lock() client := c.client c.mu.Unlock() if client == nil { return "[]" } var targetDBID uint64 if targetDBIDStr != "" && targetDBIDStr != "0" { if _, err := fmt.Sscanf(targetDBIDStr, "%d", &targetDBID); err != nil { return "[]" } } complaints, err := client.ListComplaints(targetDBID) if err != nil { log.Printf("[TSBridge] ListComplaints error: %v", err) return "[]" } result := make([]complaintEntryJSON, len(complaints)) for i, c := range complaints { result[i] = complaintEntryJSON{ FromDBID: fmt.Sprintf("%d", c.FromDBID), ToDBID: fmt.Sprintf("%d", c.ToDBID), Message: c.Message, Timestamp: fmt.Sprintf("%d", c.Timestamp), } } data, err := json.Marshal(result) if err != nil { log.Printf("[TSBridge] JSON marshal error: %v", err) return "[]" } return string(data) } // AddComplaint 提交投诉。返回空字符串表示成功。 func (c *TSClient) AddComplaint(targetDBIDStr, message string) string { c.mu.Lock() client := c.client c.mu.Unlock() if client == nil { return "未连接" } var targetDBID uint64 if _, err := fmt.Sscanf(targetDBIDStr, "%d", &targetDBID); err != nil { return fmt.Sprintf("无效的 DBID: %v", err) } if err := client.AddComplaint(targetDBID, message); err != nil { return fmt.Sprintf("提交失败: %v", err) } return "" } // ─── 频道管理(创建/编辑/删除) ─────────────────────────── // CreateChannelJSON 创建新频道。propertiesJSON 为 JSON 对象, // 包含可选的频道属性,如 channel_topic、channel_flag_permanent 等。 // 返回新频道的 ID 字符串,空字符串表示失败。 func (c *TSClient) CreateChannelJSON(name, propertiesJSON string) string { c.mu.Lock() client := c.client c.mu.Unlock() if client == nil { return "" } var props map[string]string if propertiesJSON != "" && propertiesJSON != "{}" { if err := json.Unmarshal([]byte(propertiesJSON), &props); err != nil { log.Printf("[TSBridge] CreateChannel properties parse error: %v", err) return "" } } cid, err := client.CreateChannel(name, props) if err != nil { log.Printf("[TSBridge] CreateChannel error: %v", err) return "" } return fmt.Sprintf("%d", cid) } // EditChannelJSON 编辑频道属性。propertiesJSON 为 JSON 对象。 // 返回空字符串表示成功,非空为错误。 func (c *TSClient) EditChannelJSON(channelIDStr, propertiesJSON string) string { c.mu.Lock() client := c.client c.mu.Unlock() if client == nil { return "未连接" } var channelID uint64 if _, err := fmt.Sscanf(channelIDStr, "%d", &channelID); err != nil { return fmt.Sprintf("无效的频道 ID: %v", err) } var props map[string]string if err := json.Unmarshal([]byte(propertiesJSON), &props); err != nil { return fmt.Sprintf("属性解析失败: %v", err) } if err := client.EditChannel(channelID, props); err != nil { return fmt.Sprintf("编辑失败: %v", err) } return "" } // DeleteChannel 删除频道。force=true 强制删除(含子频道)。 // 返回空字符串表示成功。 func (c *TSClient) DeleteChannel(channelIDStr string, force bool) string { c.mu.Lock() client := c.client c.mu.Unlock() if client == nil { return "未连接" } var channelID uint64 if _, err := fmt.Sscanf(channelIDStr, "%d", &channelID); err != nil { return fmt.Sprintf("无效的频道 ID: %v", err) } if err := client.DeleteChannel(channelID, force); err != nil { return fmt.Sprintf("删除失败: %v", err) } return "" } // UpdateSelfJSON 更新当前客户端属性。propertiesJSON 为 JSON 对象, // 如 {"client_away": "1", "client_away_message": "AFK"}。 // 返回空字符串表示成功。 func (c *TSClient) UpdateSelfJSON(propertiesJSON string) string { c.mu.Lock() client := c.client c.mu.Unlock() if client == nil { return "未连接" } var props map[string]string if err := json.Unmarshal([]byte(propertiesJSON), &props); err != nil { return fmt.Sprintf("属性解析失败: %v", err) } if err := client.UpdateSelf(props); err != nil { return fmt.Sprintf("更新失败: %v", err) } return "" } // KickClient 踢出客户端。reasonID: 4=频道踢出, 5=服务器踢出。 // 返回空字符串表示成功。 func (c *TSClient) KickClient(clid int, reasonID int, reasonMsg string) string { c.mu.Lock() client := c.client c.mu.Unlock() if client == nil { return "未连接" } if err := client.KickClient(uint16(clid), reasonID, reasonMsg); err != nil { return fmt.Sprintf("踢出失败: %v", err) } return "" } // GetClientID 获取当前客户端 ID func (c *TSClient) GetClientID() int { c.mu.Lock() defer c.mu.Unlock() if c.client == nil { return 0 } return int(c.client.ClientID()) } // GetChannelID 获取当前频道 ID func (c *TSClient) GetChannelID() string { c.mu.Lock() client := c.client c.mu.Unlock() if client == nil { return "0" } // 通过 GetClientInfo 获取当前频道 ID info, err := client.GetClientInfo(client.ClientID()) if err != nil { return "0" } if cid, ok := info["cid"]; ok { return cid } return "0" } // SendChannelMessage 发送频道文字消息 // 返回空字符串表示成功 func (c *TSClient) SendChannelMessage(channelIDStr, message string) string { c.mu.Lock() client := c.client c.mu.Unlock() if client == nil { return "未连接" } var channelID uint64 if _, err := fmt.Sscanf(channelIDStr, "%d", &channelID); err != nil { return fmt.Sprintf("无效的频道 ID: %v", err) } if err := client.SendTextMessage(2, channelID, message); err != nil { return fmt.Sprintf("发送失败: %v", err) } return "" } // MoveToChannel 移动到指定频道 // 返回空字符串表示成功 func (c *TSClient) MoveToChannel(channelIDStr, password string) string { c.mu.Lock() client := c.client c.mu.Unlock() if client == nil { return "未连接" } var channelID uint64 if _, err := fmt.Sscanf(channelIDStr, "%d", &channelID); err != nil { return fmt.Sprintf("无效的频道 ID: %v", err) } if err := client.ClientMove(client.ClientID(), channelID, password); err != nil { return fmt.Sprintf("移动失败: %v", err) } return "" } // SendTextMessage 发送文字消息 // targetMode: 1=私聊, 2=频道, 3=服务器 // 返回空字符串表示成功 func (c *TSClient) SendTextMessage(targetMode int, targetIDStr string, message string) string { c.mu.Lock() client := c.client c.mu.Unlock() if client == nil { return "未连接" } var targetID uint64 if _, err := fmt.Sscanf(targetIDStr, "%d", &targetID); err != nil { return fmt.Sprintf("无效的目标 ID: %v", err) } if err := client.SendTextMessage(targetMode, targetID, message); err != nil { return fmt.Sprintf("发送失败: %v", err) } return "" } // Poke 向指定客户端发送 Poke 消息 // 返回空字符串表示成功 func (c *TSClient) Poke(clidStr string, message string) string { c.mu.Lock() client := c.client c.mu.Unlock() if client == nil { return "未连接" } var clid uint64 if _, err := fmt.Sscanf(clidStr, "%d", &clid); err != nil { return fmt.Sprintf("无效的客户端 ID: %v", err) } if err := client.Poke(uint16(clid), message); err != nil { return fmt.Sprintf("Poke 失败: %v", err) } return "" } // setReceiveDecoderFactory installs the decoder provider used by // StartReceiveAudio. It remains internal because VoiceDecoderFactory is a Go // implementation detail and must not be exported through gomobile. func (c *TSClient) ensureReceiveFactory() { c.mu.Lock() if c.receiveFactory == nil { c.receiveFactory = newPlatformVoiceDecoderFactory() log.Printf("[TSVoice] receive factory initialized lazily type=%T available=%t", c.receiveFactory, c.receiveFactory != nil) } c.mu.Unlock() } func (c *TSClient) setReceiveDecoderFactory(factory voiceDecoderFactory) { c.mu.Lock() c.receiveFactory = factory if c.receive == nil && factory != nil { c.receive = newReceiveAudio(factory, c.queueMixedPCM, c.queueSpeaking) } c.mu.Unlock() } // StartReceiveAudio starts the Go-owned receive pipeline when this platform // provides a decoder factory. A fresh worker is created after a prior stop, so // speaker toggles and reconnects can safely restart receiving. func (c *TSClient) StartReceiveAudio() { c.ensureReceiveFactory() c.mu.Lock() if c.receive == nil && c.receiveFactory != nil { c.receive = newReceiveAudio(c.receiveFactory, c.queueMixedPCM, c.queueSpeaking) } r := c.receive factoryAvailable := c.receiveFactory != nil c.mu.Unlock() log.Printf("[TSVoice] StartReceiveAudio worker=%t factory=%t", r != nil, factoryAvailable) if r != nil { r.start() } } // StopReceiveAudio stops the receive worker and waits for it to release all // decoders. Repeated calls are harmless. func (c *TSClient) StopReceiveAudio() { c.mu.Lock() r := c.receive c.receive = nil c.mu.Unlock() if r != nil { r.stopAndWait() } } // SetRemoteClientMuted excludes a remote client from the final mix while it // continues receiving, decoding, and reporting speaking state. func (c *TSClient) SetRemoteClientMuted(clientID int, muted bool) { c.receiveCommand(audioCommand{kind: "mute", clientID: clientID, muted: muted}) } func (c *TSClient) RemoveRemoteAudioClient(clientID int) { c.receiveCommand(audioCommand{kind: "remove", clientID: clientID}) } func (c *TSClient) ClearRemoteAudioClients() { c.receiveCommand(audioCommand{kind: "clear"}) } func (c *TSClient) receiveCommand(cmd audioCommand) { c.mu.Lock() r := c.receive c.mu.Unlock() if r != nil { r.command(cmd) } } // SendVoice 发送 Opus 语音帧 // codec: 4=Opus Voice, 5=Opus Music // 返回空字符串表示成功 func (c *TSClient) SendVoice(data []byte, codec int64) string { c.mu.Lock() client := c.client c.mu.Unlock() if client == nil { return "未连接" } if err := client.SendVoice(data, byte(codec)); err != nil { return fmt.Sprintf("发送失败: %v", err) } return "" } // initEventQueue initializes a fresh consumer for this connection session. func (c *TSClient) initEventQueue() { c.mu.Lock() callback := c.callback c.mu.Unlock() q := newEventQueue(callback) c.evtQueueMu.Lock() c.evtQueue = q c.evtQueueMu.Unlock() c.startEventLoop(q) } // stopEventQueue stops and joins the current consumer. It is safe to call // repeatedly and prevents stale sessions from dispatching into a new callback. func (c *TSClient) stopEventQueue() { c.evtQueueMu.Lock() q := c.evtQueue c.evtQueue = nil c.evtQueueMu.Unlock() if q == nil { return } q.stopOnce.Do(func() { q.mu.Lock() q.stopped = true q.cond.Broadcast() q.mu.Unlock() }) } // startEventLoop starts the callback consumer for q. func (c *TSClient) startEventLoop(q *eventQueue) { go func() { for { q.mu.Lock() for len(q.events) == 0 && !q.stopped { q.cond.Wait() } if q.stopped { q.mu.Unlock() return } events := q.events q.events = nil q.pcmCount = 0 q.mu.Unlock() for _, evt := range events { c.processEvent(q.callback, evt) } } }() } // queueEvent queues an event only while its session's consumer remains active. func (c *TSClient) queueEvent(evt queuedEvent) { c.evtQueueMu.Lock() q := c.evtQueue c.evtQueueMu.Unlock() if q == nil { return } q.mu.Lock() if !q.stopped { q.events = append(q.events, evt) q.cond.Signal() } q.mu.Unlock() } // processEvent 处理单个事件(在消费协程中顺序执行) func (c *TSClient) processEvent(cb EventCallback, evt queuedEvent) { if cb == nil { return } switch evt.evtType { case "connected": log.Printf("[TSEvent] connected") cb.OnConnected() case "disconnected": msg := evt.data.(string) log.Printf("[TSEvent] disconnected: %q", msg) cb.OnDisconnected(msg) case "textmessage": m := evt.data.(*TextMsg) log.Printf("[TSEvent] textmessage from=%q uid=%q mode=%d target=%s msg=%q", m.InvokerName, m.InvokerUID, m.TargetMode, m.TargetID, m.Message) cb.OnTextMessage(m) case "cliententer": cl := evt.data.(*Client) log.Printf("[TSEvent] cliententer id=%d nick=%q uid=%q channel=%s", cl.ID, cl.Nickname, cl.UID, cl.ChannelID) cb.OnClientEnter(cl) case "clientleave": d := evt.data.(struct { ID int ReasonMsg string }) log.Printf("[TSEvent] clientleave id=%d reason=%q", d.ID, d.ReasonMsg) cb.OnClientLeave(d.ID, d.ReasonMsg) case "clientmoved": d := evt.data.(struct { ID int TargetChannelID string }) log.Printf("[TSEvent] clientmoved id=%d targetChannel=%s", d.ID, d.TargetChannelID) cb.OnClientMoved(d.ID, d.TargetChannelID) case "kicked": reason := evt.data.(string) log.Printf("[TSEvent] kicked reason=%q", reason) cb.OnKicked(reason) case "voicedata": d := evt.data.(struct { ClientID int Data []byte Codec int Sequence int IsWhisper bool }) cb.OnVoiceData(d.ClientID, d.Data, d.Codec, d.Sequence, d.IsWhisper) case "mixedvoicepcm": cb.OnMixedVoicePCM(evt.data.([]byte)) case "clientspeaking": d := evt.data.(struct { ClientID int Speaking bool }) cb.OnClientSpeaking(d.ClientID, d.Speaking) case "poked": p := evt.data.(*PokeEvent) log.Printf("[TSEvent] poked from=%q (id=%d uid=%q) msg=%q", p.InvokerName, p.InvokerID, p.InvokerUID, p.Message) cb.OnPoked(p) } } func (c *TSClient) queueMixedPCM(pcm []byte) { c.evtQueueMu.Lock() q := c.evtQueue c.evtQueueMu.Unlock() if q == nil { return } q.mu.Lock() if !q.stopped { if q.pcmCount >= maxQueuedPCMEvents { for i, evt := range q.events { if evt.evtType == "mixedvoicepcm" { copy(q.events[i:], q.events[i+1:]) q.events = q.events[:len(q.events)-1] q.pcmCount-- break } } } q.events = append(q.events, queuedEvent{evtType: "mixedvoicepcm", data: pcm}) q.pcmCount++ q.cond.Signal() } q.mu.Unlock() } func (c *TSClient) queueSpeaking(clientID int, speaking bool) { c.queueEvent(queuedEvent{evtType: "clientspeaking", data: struct { ClientID int Speaking bool }{clientID, speaking}}) } // registerEvents 注册事件处理 // 所有事件通过队列传递,在消费协程中顺序调用 JNI 回调 func (c *TSClient) isCurrentClient(client *ts.Client) bool { c.mu.Lock() defer c.mu.Unlock() return c.client == client } func (c *TSClient) queueClientEvent(client *ts.Client, evt queuedEvent) { if c.isCurrentClient(client) { c.queueEvent(evt) } } func (c *TSClient) registerEvents(client *ts.Client) { client.OnConnected(func() { c.mu.Lock() if c.client != client { c.mu.Unlock() return } c.connected = true c.mu.Unlock() c.queueEvent(queuedEvent{evtType: "connected"}) }) client.OnDisconnected(func(err error) { c.StopReceiveAudio() c.mu.Lock() c.connected = false c.client = nil c.mu.Unlock() msg := "" if err != nil { msg = err.Error() } c.queueEvent(queuedEvent{evtType: "disconnected", data: msg}) }) client.OnTextMessage(func(msg ts.TextMessage) { c.queueEvent(queuedEvent{ evtType: "textmessage", data: &TextMsg{ InvokerName: msg.InvokerName, InvokerUID: msg.InvokerUID, Message: msg.Message, TargetMode: msg.TargetMode, TargetID: fmt.Sprintf("%d", msg.TargetID), }, }) }) client.OnClientEnter(func(info ts.ClientInfo) { c.queueEvent(queuedEvent{ evtType: "cliententer", data: &Client{ ID: int(info.ID), Nickname: info.Nickname, UID: info.UID, ChannelID: fmt.Sprintf("%d", info.ChannelID), IsSelf: false, }, }) }) client.OnClientLeave(func(data ts.ClientLeftViewEvent) { c.RemoveRemoteAudioClient(int(data.ID)) c.queueEvent(queuedEvent{ evtType: "clientleave", data: struct { ID int ReasonMsg string }{ ID: int(data.ID), ReasonMsg: data.ReasonMsg, }, }) }) client.OnClientMoved(func(data ts.ClientMovedEvent) { c.queueEvent(queuedEvent{ evtType: "clientmoved", data: struct { ID int TargetChannelID string }{ ID: int(data.ID), TargetChannelID: fmt.Sprintf("%d", data.TargetChannelID), }, }) }) client.OnKicked(func(reason string) { c.StopReceiveAudio() c.mu.Lock() c.connected = false c.client = nil c.mu.Unlock() c.queueEvent(queuedEvent{evtType: "kicked", data: reason}) }) client.OnVoiceData(func(evt ts.VoiceDataEvent) { c.mu.Lock() r := c.receive c.mu.Unlock() if r != nil { r.enqueue(int(evt.ClientID), int(evt.Codec), evt.Sequence, evt.Data) return } // Keep the existing raw bridge active until an Android libopus decoder // factory has been supplied and the Go receiver explicitly started. c.queueEvent(queuedEvent{ evtType: "voicedata", data: struct { ClientID int Data []byte Codec int Sequence int IsWhisper bool }{ ClientID: int(evt.ClientID), Data: evt.Data, Codec: int(evt.Codec), Sequence: int(evt.Sequence), IsWhisper: evt.IsWhisper, }, }) }) client.OnPoked(func(evt ts.PokeEvent) { c.queueEvent(queuedEvent{ evtType: "poked", data: &PokeEvent{ InvokerID: int(evt.InvokerID), InvokerName: evt.InvokerName, InvokerUID: evt.InvokerUID, Message: evt.Message, }, }) }) }