package config import ( "os" "strings" "time" ) // Config 承载直播 SFU 分流 Demo 的运行参数。 // 优先级:环境变量 > 默认值。Cloudflare Realtime 为主 SFU,SRS 为本地对照后端。 // 默认 DB 使用 Turso 嵌入式 (libSQL file) 持久化房间拓扑;同时支持 JWT/Casbin 登录。 type Config struct { HTTPPort string GRPCPort string ProviderOrder string // 逗号分隔的后端顺序,如 "cloudflare,srs" SRSBaseURL string SRSHttpURL string SRSApp string SRSSecret string SRSCandidate string CFAppID string CFAppSecret string CFBaseURL string CFStunURL string TokenSecret string TokenRequired bool JWTSecret string JWTTTL time.Duration AuthModel string AuthPolicy string AuthUserFile string AllowRegister bool DatabaseURL string // TURSO_DATABASE_URL 或 DATABASE_URL,默认 file:./data/live-sfu.db AuthToken string // 兼容远程 Turso,嵌入模式可为空 } func Load() *Config { c := &Config{ HTTPPort: getenv("HTTP_PORT", "8088"), GRPCPort: getenv("GRPC_PORT", "9090"), ProviderOrder: getenv("SFU_PROVIDER", "cloudflare,srs"), SRSBaseURL: getenv("SRS_API_BASE", "http://localhost:1985"), SRSHttpURL: getenv("SRS_HTTP_BASE", "http://localhost:8080"), SRSApp: getenv("SRS_APP", "live"), SRSSecret: getenv("SRS_SECRET", ""), SRSCandidate: getenv("SRS_CANDIDATE", "127.0.0.1"), CFAppID: getenv("CF_APP_ID", ""), CFAppSecret: getenv("CF_APP_SECRET", ""), CFBaseURL: getenv("CF_BASE_URL", "https://rtc.live.cloudflare.com/v1"), CFStunURL: getenv("CF_STUN_URL", "stun:stun.cloudflare.com:3478"), TokenSecret: getenv("DEMO_TOKEN_SECRET", ""), TokenRequired: getenv("SFU_TOKEN_REQUIRED", "0") == "1", JWTSecret: getenv("JWT_SECRET", ""), JWTTTL: parseDuration(getenv("JWT_TTL", "2h"), 2*time.Hour), AuthModel: getenv("CASBIN_MODEL", "internal/auth/model.conf"), AuthPolicy: getenv("CASBIN_POLICY", "internal/auth/policy.csv"), AuthUserFile: getenv("AUTH_USER_FILE", "data/users.json"), AllowRegister: getenv("ALLOW_REGISTER", "1") == "1", DatabaseURL: getenv("TURSO_DATABASE_URL", "file:./data/live-sfu.db?cache=shared&_journal_mode=WAL"), AuthToken: getenv("TURSO_AUTH_TOKEN", ""), } if c.TokenSecret == "" { c.TokenSecret = "insecure-demo-secret-change-me" } if c.JWTSecret == "" { c.JWTSecret = c.TokenSecret if c.JWTSecret == "" { c.JWTSecret = "insecure-jwt-secret-change-me" } } if c.DatabaseURL == "file:./data/live-sfu.db?cache=shared&_journal_mode=WAL" { if v := os.Getenv("DATABASE_URL"); v != "" { c.DatabaseURL = v } else if v := os.Getenv("LIBSQL_URL"); v != "" { c.DatabaseURL = v } } if c.AuthToken == "" { if v := os.Getenv("TURSO_AUTH_TOKEN"); v != "" { c.AuthToken = v } else if v := os.Getenv("LIBSQL_AUTH_TOKEN"); v != "" { c.AuthToken = v } } return c } func getenv(k, def string) string { if v := os.Getenv(k); v != "" { return v } return def } func parseDuration(s string, def time.Duration) time.Duration { if s == "" { return def } if d, err := time.ParseDuration(s); err == nil { return d } return def } func (c *Config) ProviderList() []string { parts := strings.Split(c.ProviderOrder, ",") 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 == "" || p == "both" || p == "all" { if p == "both" || p == "all" { for _, b := range []string{"cloudflare", "srs"} { if _, ok := seen[b]; !ok { seen[b] = struct{}{} out = append(out, b) } } } continue } if _, ok := seen[p]; !ok { seen[p] = struct{}{} out = append(out, p) } } return out } func (c *Config) DSN() string { if c.AuthToken != "" && strings.HasPrefix(c.DatabaseURL, "libsql://") && !strings.Contains(c.DatabaseURL, "authToken") { sep := "?" if strings.Contains(c.DatabaseURL, "?") { sep = "&" } return c.DatabaseURL + sep + "authToken=" + c.AuthToken } return c.DatabaseURL } func (c *Config) IsRemoteTurso() bool { return strings.HasPrefix(c.DatabaseURL, "libsql://") || strings.HasPrefix(c.DatabaseURL, "https://") }