chore: refresh gramsrv public release

This commit is contained in:
A 2026-06-30 14:37:43 +08:00
parent 75cebe8dbf
commit 70b6820474
1274 changed files with 378751 additions and 59919 deletions

140
internal/sfu/sfu.go Normal file
View file

@ -0,0 +1,140 @@
// Package sfu 是群通话媒体面(Selective Forwarding Unit)的接口与实现。
//
// 角色契约(来自 tgcalls GroupNetworkManager 的硬性要求):客户端 ICE=CONTROLLED、
// DTLS setup="passive"(等待握手);SFU 必须跑 full-ICE CONTROLLING(ICE-Lite 不可用)
// 并以 DTLS client(setup="active")主动发起握手。上行 join JSON 不含 ICE candidates,
// SFU 只能凭 ufrag/pwd 认证的 STUN Binding Request 学到客户端地址(peer-reflexive)。
//
// M0 提供 Disabled 实现(纯信令联调):下发语法完备的 ufrag/pwd/sha-256 指纹与空
// candidates——客户端解析成功后停留在 Connecting 态(持续 4s checkGroupCall 心跳),
// 属预期行为;M1 换 pion 实现后客户端无感切换。
package sfu
import (
"context"
"crypto/rand"
"encoding/hex"
"fmt"
"strings"
)
// SsrcGroup 是上行 join JSON 的 ssrc-groups 条目:semantics ∈ {"SIM","FID"}。
// SIM 的 sources 按质量低→高排列;FID 是 [媒体 ssrc, RTX 重传 ssrc] 对。
type SsrcGroup struct {
Semantics string
Sources []uint32
}
// EndpointKind 区分同一参与者的两条媒体连接(M4:屏幕共享是独立的第二连接,
// 独立 ICE/DTLS/ssrc 集,但 TL 层归属同一 participant)。
type EndpointKind int
const (
EndpointMain EndpointKind = iota
EndpointPresentation
)
// ClientOffer 解析自 phone.joinGroupCall / joinGroupCallPresentation 的上行 params JSON。
type ClientOffer struct {
AudioSSRC uint32
Ufrag string
Pwd string
FingerprintSHA256 string // 形如 "AA:BB:..."(RFC 4572 hex)
// SsrcGroups 是视频 simulcast/RTX 源组(主 join 即带,无论摄像头开关)。
// SFU 据此做层选择:订阅端只为 SIM[0](最低层)建解码 sink,发到其它层
// 原始 ssrc 的包会被客户端静默丢弃——v1 转发策略=只转 SIM[0] 层与其 FID
// RTX 伙伴,其余层在 SFU 侧丢弃(省下行且行为正确)。
SsrcGroups []SsrcGroup
}
// Candidate 是下发给客户端的 ICE 候选(updateGroupCallConnection JSON 的
// transport.candidates 条目,字段语义照抄 tgcalls 解析代码)。
type Candidate struct {
IP string
Port int
Protocol string // "udp"
Type string // "host"
}
// ServerAnswer 是 SFU 端的传输参数,由 rpc 层组装进下行 JSON。
type ServerAnswer struct {
Ufrag string
Pwd string
FingerprintSHA256 string
Candidates []Candidate
}
// Service 是信令层⇄媒体面的边界(未来横向扩展/远端 SFU 的替换点)。
type Service interface {
// Enabled 报告媒体面是否真实可用(false=M0 纯信令模式)。
Enabled() bool
// Join 为参与者分配/重建媒体 endpoint,返回 SFU 端传输参数。
// kind=EndpointPresentation 时是同一参与者的第二连接(独立 ICE/DTLS)。
Join(ctx context.Context, callID, userID int64, kind EndpointKind, offer ClientOffer) (ServerAnswer, error)
// Leave 拆除参与者的 endpoint;kind=EndpointMain 时联动拆其 presentation
//(客户端整体离会从不补发 leaveGroupCallPresentation)。
Leave(ctx context.Context, callID, userID int64, kind EndpointKind) error
CloseRoom(ctx context.Context, callID int64) error
// AliveUserIDs 返回 callID 房间内媒体面仍存活(ICE consent/近期 SRTP 收包)
// 的参与者。sweeper 的判死条件 = 心跳过期 ∧ 媒体面不存活(双过期)。
AliveUserIDs(callID int64) []int64
}
// disabled 是 M0 纯信令实现。
type disabled struct{}
// Disabled 返回纯信令模式的 SFU。
func Disabled() Service {
return disabled{}
}
func (disabled) Enabled() bool { return false }
func (disabled) Join(_ context.Context, _, _ int64, _ EndpointKind, _ ClientOffer) (ServerAnswer, error) {
ufrag, err := randomICEString(8)
if err != nil {
return ServerAnswer{}, err
}
pwd, err := randomICEString(24)
if err != nil {
return ServerAnswer{}, err
}
fp, err := randomFingerprint()
if err != nil {
return ServerAnswer{}, err
}
// 空 candidates:客户端无可连地址,保持 Connecting(M0 预期)。
return ServerAnswer{Ufrag: ufrag, Pwd: pwd, FingerprintSHA256: fp, Candidates: nil}, nil
}
func (disabled) Leave(context.Context, int64, int64, EndpointKind) error { return nil }
func (disabled) CloseRoom(context.Context, int64) error { return nil }
func (disabled) AliveUserIDs(int64) []int64 { return nil }
const iceAlphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
func randomICEString(n int) (string, error) {
buf := make([]byte, n)
if _, err := rand.Read(buf); err != nil {
return "", fmt.Errorf("sfu: random ice string: %w", err)
}
var b strings.Builder
for _, v := range buf {
b.WriteByte(iceAlphabet[int(v)%len(iceAlphabet)])
}
return b.String(), nil
}
// randomFingerprint 生成语法合法的 sha-256 指纹串(32 字节 hex,冒号分隔)。
// M0 无 DTLS 端点,指纹值不会被验证(客户端连不上任何 candidate)。
func randomFingerprint() (string, error) {
buf := make([]byte, 32)
if _, err := rand.Read(buf); err != nil {
return "", fmt.Errorf("sfu: random fingerprint: %w", err)
}
parts := make([]string, len(buf))
for i, v := range buf {
parts[i] = strings.ToUpper(hex.EncodeToString([]byte{v}))
}
return strings.Join(parts, ":"), nil
}