chore: refresh gramsrv public release
This commit is contained in:
parent
75cebe8dbf
commit
70b6820474
1274 changed files with 378751 additions and 59919 deletions
140
internal/sfu/sfu.go
Normal file
140
internal/sfu/sfu.go
Normal 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue