owpengram-server/internal/sfu/sfu.go

140 lines
5.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package sfu 是群通话媒体面Selective Forwarding Unit的接口与实现。
//
// 角色契约(来自 tgcalls GroupNetworkManager 的硬性要求):客户端 ICE=CONTROLLED、
// DTLS setup="passive"等待握手SFU 必须跑 full-ICE CONTROLLINGICE-Lite 不可用)
// 并以 DTLS clientsetup="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 拆除参与者的 endpointkind=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客户端无可连地址保持 ConnectingM0 预期)。
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
}