feat(proto): add distribution model srs-hls/cf/cdn
This commit is contained in:
parent
077226df4c
commit
1f422c7340
|
|
@ -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
|
|
@ -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 := "?"
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue