diff --git a/README.md b/README.md index 340147c..102d151 100644 --- a/README.md +++ b/README.md @@ -382,6 +382,62 @@ enabled = true country = "US" ``` +### Streaming reference + +The in-browser player on torrentclaw.com streams from the daemon over WebRTC +(low-latency P2P) or HLS (HTTP fragments + ffmpeg transcode for codecs the +browser can't decode natively). Both are enabled by default — a fresh install +"just works" without editing the TOML. Disable surgically only if you have a +reason. + +```toml +[downloads.webrtc] +enabled = true # master switch +trackers = ["wss://tracker.torrentclaw.com"] # signaling trackers +stun_servers = [ # NAT traversal + "stun:stun.l.google.com:19302", + "stun:stun1.l.google.com:19302", +] +turn_servers = [] # optional TURN relays +turn_user = "" +turn_pass = "" + +[downloads.transcode] +enabled = true # master switch +hw_accel = "auto" # auto | none | nvenc | qsv | vaapi | videotoolbox +preset = "veryfast" # libx264 preset +video_bitrate = "" # e.g. "5M" caps -b:v; empty = engine fallback (5M) +audio_bitrate = "192k" # e.g. "128k", "192k", "256k" +max_height = 0 # 0 = no cap; e.g. 720 forces 720p max +max_concurrent = 2 # max simultaneous ffmpeg processes +``` + +#### `[downloads.webrtc]` + +| Key | Type | Default | Notes | +|-----|------|---------|-------| +| `enabled` | bool | `true` | Browser↔daemon WebRTC peer for the in-browser P2P player. Disable to skip WebRTC tracker signalling (saves ~5MB RAM, blocks WebRTC streaming — HLS still works). | +| `trackers` | `[]string` | `["wss://tracker.torrentclaw.com"]` | Signaling trackers for peer discovery. | +| `stun_servers` | `[]string` | Google public STUN ×2 | ICE candidate gathering. | +| `turn_servers` | `[]string` | `[]` | Optional TURN relays for symmetric-NAT users. | +| `turn_user` / `turn_pass` | string | `""` | Credentials for authed TURN servers. Applied to all `turn_servers`. | + +#### `[downloads.transcode]` + +| Key | Type | Default | Notes | +|-----|------|---------|-------| +| `enabled` | bool | `true` | Real-time HLS transcoding when source codec is browser-incompatible (HEVC, AV1, AC3, DTS). Requires `ffmpeg` + `ffprobe` on PATH. | +| `hw_accel` | string | `"auto"` | Hardware accel: `"auto"`, `"none"`, `"nvenc"` (NVIDIA), `"qsv"` (Intel), `"vaapi"` (Linux), `"videotoolbox"` (macOS). | +| `preset` | string | `"veryfast"` | libx264 preset. Slower preset = smaller files but higher CPU. Options: `ultrafast`, `superfast`, `veryfast`, `faster`, `fast`, `medium`, `slow`, `slower`, `veryslow`. | +| `video_bitrate` | string | `""` | E.g. `"5M"` caps `-b:v`. Empty falls back to the engine default (`5M`). | +| `audio_bitrate` | string | `"192k"` | E.g. `"128k"`, `"256k"`. | +| `max_height` | int | `0` | `0` = no cap. E.g. `720` forces 720p max — useful on weak GPUs. | +| `max_concurrent` | int | `2` | Max simultaneous ffmpeg processes. Increase if hosting multiple users on a beefy box. | + +If `transcode.enabled = true` but `ffmpeg` / `ffprobe` aren't on PATH, the +daemon logs a warning at startup and HLS sessions are rejected at runtime +with a clear error — install ffmpeg or set `enabled = false`. + ### Environment variables Environment variables override config file values: diff --git a/internal/config/config.go b/internal/config/config.go index b7ee27d..d5b0f91 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -106,7 +106,9 @@ type LibraryConfig struct { AllowDelete bool `toml:"allow_delete"` // allow web UI to request file deletion from disk } -// Default returns a Config with sensible defaults. +// Default returns a Config with sensible defaults. Used both for fresh +// installs (no config file yet) and as the baseline for Load — fields not +// present in the user's TOML keep their Default() value. func Default() Config { return Config{ Auth: AuthConfig{ @@ -117,7 +119,7 @@ func Default() Config { MaxConcurrent: 3, StreamPort: 11818, WebRTC: WebRTCConfig{ - Enabled: false, + Enabled: true, Trackers: []string{"wss://tracker.torrentclaw.com"}, STUNServers: []string{"stun:stun.l.google.com:19302", "stun:stun1.l.google.com:19302"}, }, @@ -125,7 +127,6 @@ func Default() Config { Enabled: true, HWAccel: "auto", Preset: "veryfast", - VideoBitrate: "5M", AudioBitrate: "192k", MaxConcurrent: 2, }, @@ -167,67 +168,66 @@ func Load(path string) (Config, error) { return cfg, fmt.Errorf("read config: %w", err) } - if err := toml.Unmarshal(data, &cfg); err != nil { + meta, err := toml.Decode(string(data), &cfg) + if err != nil { return cfg, fmt.Errorf("parse config: %w", err) } - // Re-apply defaults for zero values that should have defaults - if cfg.Auth.APIURL == "" { + applyDefaults(&cfg, meta) + return cfg, nil +} + +// applyDefaults fills in sensible defaults for keys that the user did not +// define in the TOML file. We use MetaData (rather than zero-value checks) so +// that explicitly setting a field to its zero value (e.g. `enabled = false`) +// is respected — only truly missing keys get defaulted. This lets a fresh +// install work out of the box for streaming without forcing every user to +// edit the TOML, while still letting power users disable features. +func applyDefaults(cfg *Config, meta toml.MetaData) { + if !meta.IsDefined("auth", "api_url") { cfg.Auth.APIURL = "https://torrentclaw.com" } - if cfg.Download.PreferredMethod == "" { + if !meta.IsDefined("downloads", "preferred_method") { cfg.Download.PreferredMethod = "auto" } - if cfg.Download.MaxConcurrent == 0 { + if !meta.IsDefined("downloads", "max_concurrent") { cfg.Download.MaxConcurrent = 3 } - if cfg.General.Country == "" { - cfg.General.Country = "US" - } - if cfg.Download.StreamPort == 0 { + if !meta.IsDefined("downloads", "stream_port") { cfg.Download.StreamPort = 11818 } - // Re-apply WebRTC defaults only when the user enabled WebRTC but didn't - // supply trackers/STUN — leave both empty if disabled to keep config diffs clean. - if cfg.Download.WebRTC.Enabled { - if len(cfg.Download.WebRTC.Trackers) == 0 { - cfg.Download.WebRTC.Trackers = []string{"wss://tracker.torrentclaw.com"} - } - if len(cfg.Download.WebRTC.STUNServers) == 0 { - cfg.Download.WebRTC.STUNServers = []string{ - "stun:stun.l.google.com:19302", - "stun:stun1.l.google.com:19302", - } - } - // Auto-enable transcode for the in-browser player when WebRTC is on - // AND the user hasn't explicitly opted out. The struct's Enabled - // field is `false` for legacy configs because the field didn't - // exist when they were written; we treat "no transcode section at - // all" as "use defaults" rather than "off". - tc := &cfg.Download.Transcode - if !tc.Enabled && tc.HWAccel == "" && tc.Preset == "" && tc.VideoBitrate == "" { - tc.Enabled = true - } - if tc.Enabled { - if tc.HWAccel == "" { - tc.HWAccel = "auto" - } - if tc.Preset == "" { - tc.Preset = "veryfast" - } - if tc.VideoBitrate == "" { - tc.VideoBitrate = "5M" - } - if tc.AudioBitrate == "" { - tc.AudioBitrate = "192k" - } - if tc.MaxConcurrent == 0 { - tc.MaxConcurrent = 2 - } + if !meta.IsDefined("general", "country") { + cfg.General.Country = "US" + } + + if !meta.IsDefined("downloads", "webrtc", "enabled") { + cfg.Download.WebRTC.Enabled = true + } + if !meta.IsDefined("downloads", "webrtc", "trackers") { + cfg.Download.WebRTC.Trackers = []string{"wss://tracker.torrentclaw.com"} + } + if !meta.IsDefined("downloads", "webrtc", "stun_servers") { + cfg.Download.WebRTC.STUNServers = []string{ + "stun:stun.l.google.com:19302", + "stun:stun1.l.google.com:19302", } } - return cfg, nil + if !meta.IsDefined("downloads", "transcode", "enabled") { + cfg.Download.Transcode.Enabled = true + } + if !meta.IsDefined("downloads", "transcode", "hw_accel") { + cfg.Download.Transcode.HWAccel = "auto" + } + if !meta.IsDefined("downloads", "transcode", "preset") { + cfg.Download.Transcode.Preset = "veryfast" + } + if !meta.IsDefined("downloads", "transcode", "audio_bitrate") { + cfg.Download.Transcode.AudioBitrate = "192k" + } + if !meta.IsDefined("downloads", "transcode", "max_concurrent") { + cfg.Download.Transcode.MaxConcurrent = 2 + } } // Save writes config to the default or specified path using atomic write. diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 6685fbc..02fcdc4 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -190,6 +190,76 @@ func TestParseSpeed(t *testing.T) { } } +func TestLoadMinimalTOMLAppliesStreamingDefaults(t *testing.T) { + tmp := t.TempDir() + path := filepath.Join(tmp, "config.toml") + + // Minimal config — only auth + agent. Nothing about webrtc / transcode. + os.WriteFile(path, []byte(`[auth] +api_key = "tc_minimal" + +[agent] +id = "agent-uuid" +name = "Test" +`), 0o644) + + cfg, err := Load(path) + if err != nil { + t.Fatalf("Load failed: %v", err) + } + + // WebRTC should be on by default for fresh installs. + if !cfg.Download.WebRTC.Enabled { + t.Error("WebRTC.Enabled should default to true when [downloads.webrtc] is absent") + } + if len(cfg.Download.WebRTC.Trackers) == 0 { + t.Error("WebRTC.Trackers should default to torrentclaw tracker when absent") + } + if len(cfg.Download.WebRTC.STUNServers) == 0 { + t.Error("WebRTC.STUNServers should default to public STUN list when absent") + } + + // Transcode should be on by default. + if !cfg.Download.Transcode.Enabled { + t.Error("Transcode.Enabled should default to true when [downloads.transcode] is absent") + } + if cfg.Download.Transcode.HWAccel != "auto" { + t.Errorf("Transcode.HWAccel = %q, want auto", cfg.Download.Transcode.HWAccel) + } + if cfg.Download.Transcode.Preset != "veryfast" { + t.Errorf("Transcode.Preset = %q, want veryfast", cfg.Download.Transcode.Preset) + } + if cfg.Download.Transcode.MaxConcurrent != 2 { + t.Errorf("Transcode.MaxConcurrent = %d, want 2", cfg.Download.Transcode.MaxConcurrent) + } +} + +func TestLoadRespectsExplicitlyDisabledStreaming(t *testing.T) { + tmp := t.TempDir() + path := filepath.Join(tmp, "config.toml") + + // User explicitly opted out of webrtc + transcode. Defaults must NOT + // override them — that would silently re-enable features the user disabled. + os.WriteFile(path, []byte(`[downloads.webrtc] +enabled = false + +[downloads.transcode] +enabled = false +`), 0o644) + + cfg, err := Load(path) + if err != nil { + t.Fatalf("Load failed: %v", err) + } + + if cfg.Download.WebRTC.Enabled { + t.Error("WebRTC.Enabled = true, want false (user explicitly disabled)") + } + if cfg.Download.Transcode.Enabled { + t.Error("Transcode.Enabled = true, want false (user explicitly disabled)") + } +} + func TestLoadInvalidTOML(t *testing.T) { tmp := t.TempDir() path := filepath.Join(tmp, "config.toml")