140 lines
5.3 KiB
Go
140 lines
5.3 KiB
Go
// 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
|
||
}
|