feat(proto): add distribution model srs-hls/cf/cdn

This commit is contained in:
noelorin 2026-08-23 13:56:31 +08:00
parent 077226df4c
commit 1f422c7340
5 changed files with 555 additions and 153 deletions

View File

@ -30,6 +30,13 @@ enum BackendKind {
BACKEND_KIND_SRS = 2; // SRS(次级 / 本地对照) BACKEND_KIND_SRS = 2; // SRS(次级 / 本地对照)
} }
enum DistributionKind {
DISTRIBUTION_KIND_UNSPECIFIED = 0;
DISTRIBUTION_KIND_SRS_HLS = 1; // SRS HLS 直推(主干 remux 原地出)
DISTRIBUTION_KIND_CF_SFU = 2; // Cloudflare SFU 分发(中继)
DISTRIBUTION_KIND_CDN = 3; // 第三方直播 CDN 分发(RTMP/SRT Forward)
}
message IceServer { message IceServer {
repeated string urls = 1; repeated string urls = 1;
string username = 2; string username = 2;
@ -45,10 +52,26 @@ message BackendInfo {
bool primary = 4; bool primary = 4;
} }
message DistributorInfo {
DistributionKind kind = 1;
string name = 2;
bool configured = 3;
bool enabled = 4;
}
message DistributionTarget {
DistributionKind kind = 1;
string url = 2;
string status = 3; // forwarding | ready | error
int64 updated_at = 4;
}
message GetConfigResponse { message GetConfigResponse {
repeated BackendInfo backends = 1; repeated BackendInfo backends = 1;
string candidate = 2; string candidate = 2;
bool token_required = 3; bool token_required = 3;
BackendInfo trunk = 4;
repeated DistributorInfo distributors = 5;
} }
message StreamTarget { message StreamTarget {
@ -63,6 +86,7 @@ message StreamTarget {
message Room { message Room {
string name = 1; string name = 1;
repeated StreamTarget targets = 2; repeated StreamTarget targets = 2;
repeated DistributionTarget distributions = 3;
} }
message ListRoomsRequest {} message ListRoomsRequest {}
@ -109,6 +133,7 @@ message WatchRoomRequest {
message RoomEvent { message RoomEvent {
string room = 1; string room = 1;
repeated StreamTarget targets = 2; repeated StreamTarget targets = 2;
repeated DistributionTarget distributions = 3;
} }
// ===== Auth & RBAC (Casbin) ===== // ===== Auth & RBAC (Casbin) =====

File diff suppressed because it is too large Load Diff

View File

@ -64,6 +64,10 @@ type Config struct {
AuthBossEnabled bool AuthBossEnabled bool
Distributors string // 逗号分隔的分发方式,如 "srs-hls,cf,cdn"
CDNRTMPURL string // 第三方 CDN RTMP 推流地址模板,如 rtmp://cdn.example.com/live
CDNName string
DatabaseURL string // 嵌入式 DB DSN(TURSO_DATABASE_URL / DATABASE_URL),默认 file:./data/sync-live.db DatabaseURL string // 嵌入式 DB DSN(TURSO_DATABASE_URL / DATABASE_URL),默认 file:./data/sync-live.db
AuthToken string // 远程 libSQL 凭证,嵌入模式可为空 AuthToken string // 远程 libSQL 凭证,嵌入模式可为空
} }
@ -123,6 +127,10 @@ func Load() *Config {
AuthBossEnabled: getenv("AUTHBOSS_ENABLED", "0") == "1", AuthBossEnabled: getenv("AUTHBOSS_ENABLED", "0") == "1",
Distributors: getenv("DISTRIBUTORS", "srs-hls,cf,cdn"),
CDNRTMPURL: getenv("CDN_RTMP_URL", ""),
CDNName: getenv("CDN_NAME", "cdn"),
DatabaseURL: getenv("TURSO_DATABASE_URL", "file:./data/sync-live.db?cache=shared&_journal_mode=WAL"), DatabaseURL: getenv("TURSO_DATABASE_URL", "file:./data/sync-live.db?cache=shared&_journal_mode=WAL"),
AuthToken: getenv("TURSO_AUTH_TOKEN", ""), AuthToken: getenv("TURSO_AUTH_TOKEN", ""),
} }
@ -167,6 +175,15 @@ func (c *Config) Validate() error {
return nil return nil
} }
func (c *Config) ValidateDistributors() error {
for _, d := range c.DistributorList() {
if d == "cdn" && c.CDNRTMPURL == "" {
return fmt.Errorf("CDN 分发已启用但 CDN_RTMP_URL 未配置")
}
}
return nil
}
func getenv(k, def string) string { func getenv(k, def string) string {
if v := os.Getenv(k); v != "" { if v := os.Getenv(k); v != "" {
@ -221,6 +238,34 @@ func (c *Config) ProviderList() []string {
return out return out
} }
func (c *Config) DistributorList() []string {
parts := strings.Split(c.Distributors, ",")
out := make([]string, 0, len(parts))
seen := make(map[string]struct{}, len(parts))
for _, p := range parts {
p = strings.TrimSpace(strings.ToLower(p))
if p == "" {
continue
}
// normalize aliases
if p == "srs" || p == "hls" {
p = "srs-hls"
}
if p == "cloudflare" || p == "cf-sfu" {
p = "cf"
}
if _, ok := seen[p]; !ok {
seen[p] = struct{}{}
out = append(out, p)
}
}
return out
}
func (c *Config) CDNEnabled() bool {
return c.CDNRTMPURL != ""
}
func (c *Config) DSN() string { func (c *Config) DSN() string {
if c.AuthToken != "" && strings.HasPrefix(c.DatabaseURL, "libsql://") && !strings.Contains(c.DatabaseURL, "authToken") { if c.AuthToken != "" && strings.HasPrefix(c.DatabaseURL, "libsql://") && !strings.Contains(c.DatabaseURL, "authToken") {
sep := "?" sep := "?"

View File

@ -0,0 +1,57 @@
package config
import "testing"
func TestDistributorListParsing(t *testing.T) {
c := &Config{Distributors: "srs-hls,cf,cdn"}
got := c.DistributorList()
if len(got) != 3 || got[0] != "srs-hls" {
t.Fatalf("unexpected distributors %v", got)
}
if got[1] != "cf" || got[2] != "cdn" {
t.Fatalf("unexpected order %v", got)
}
if c.CDNEnabled() {
t.Fatalf("should not be enabled without CDN_RTMP_URL")
}
// alias normalization
c2 := &Config{Distributors: "srs,cloudflare,cdn"}
got2 := c2.DistributorList()
if got2[0] != "srs-hls" || got2[1] != "cf" {
t.Fatalf("alias normalize failed %v", got2)
}
}
func TestDistributorListDedup(t *testing.T) {
c := &Config{Distributors: "srs-hls,srs-hls,cf"}
got := c.DistributorList()
if len(got) != 2 {
t.Fatalf("want dedup 2, got %v", got)
}
}
func TestValidateCDNRequiresURLWhenEnabled(t *testing.T) {
c := &Config{Distributors: "cdn", CDNRTMPURL: ""}
if err := c.ValidateDistributors(); err == nil {
t.Fatal("expected error when cdn enabled without URL")
}
c2 := &Config{Distributors: "cdn", CDNRTMPURL: "rtmp://cdn.example.com/live"}
if err := c2.ValidateDistributors(); err != nil {
t.Fatalf("unexpected error %v", err)
}
c3 := &Config{Distributors: "srs-hls,cf", CDNRTMPURL: ""}
if err := c3.ValidateDistributors(); err != nil {
t.Fatalf("should not require CDN URL when cdn not in list, got %v", err)
}
}
func TestCDNEnabled(t *testing.T) {
c := &Config{CDNRTMPURL: ""}
if c.CDNEnabled() {
t.Fatal("empty should be disabled")
}
c2 := &Config{CDNRTMPURL: "rtmp://a/b"}
if !c2.CDNEnabled() {
t.Fatal("non-empty should be enabled")
}
}

View File

@ -16,10 +16,32 @@ export interface BackendInfo {
primary: boolean primary: boolean
} }
export type DistributionKind =
| 'DISTRIBUTION_KIND_UNSPECIFIED'
| 'DISTRIBUTION_KIND_SRS_HLS'
| 'DISTRIBUTION_KIND_CF_SFU'
| 'DISTRIBUTION_KIND_CDN'
export interface DistributorInfo {
kind: DistributionKind
name: string
configured: boolean
enabled: boolean
}
export interface DistributionTarget {
kind: DistributionKind
url: string
status: string
updated_at: number
}
export interface GetConfigResponse { export interface GetConfigResponse {
backends: BackendInfo[] backends: BackendInfo[]
candidate: string candidate: string
token_required: boolean token_required: boolean
trunk?: BackendInfo
distributors?: DistributorInfo[]
} }
export interface StreamTarget { export interface StreamTarget {
@ -60,6 +82,7 @@ export interface AutoPlaySettings {
export interface Room { export interface Room {
name: string name: string
targets: StreamTarget[] targets: StreamTarget[]
distributions?: DistributionTarget[]
display_name?: string display_name?: string
description?: string description?: string
creator?: string creator?: string
@ -127,6 +150,7 @@ export interface RoomMember {
export interface RoomEvent { export interface RoomEvent {
room: string room: string
targets: StreamTarget[] targets: StreamTarget[]
distributions?: DistributionTarget[]
} }
export interface PublishResponse { export interface PublishResponse {