1// Package config provides configuration management for dumber with Viper integration.
2package config
3
4import (
5 "encoding/json"
6 "errors"
7 "fmt"
8 "os"
9 "path/filepath"
10 "strings"
11 "sync"
12 "time"
13
14 "github.com/bnema/dumber/pkg/gpu"
15 "github.com/fsnotify/fsnotify"
16 "github.com/spf13/viper"
17)
18
19// File permission constants
20const (
21 dirPerm = 0755 // Standard directory permissions (rwxr-xr-x)
22 filePerm = 0644 // Standard file permissions (rw-r--r--)
23)
24
25// Config represents the complete configuration for dumber.
26type Config struct {
27 Database DatabaseConfig `mapstructure:"database" yaml:"database"`
28 History HistoryConfig `mapstructure:"history" yaml:"history"`
29 SearchShortcuts map[string]SearchShortcut `mapstructure:"search_shortcuts" yaml:"search_shortcuts"`
30 Dmenu DmenuConfig `mapstructure:"dmenu" yaml:"dmenu"`
31 Logging LoggingConfig `mapstructure:"logging" yaml:"logging"`
32 Appearance AppearanceConfig `mapstructure:"appearance" yaml:"appearance"`
33 VideoAcceleration VideoAccelerationConfig `mapstructure:"video_acceleration" yaml:"video_acceleration"`
34 CodecPreferences CodecConfig `mapstructure:"codec_preferences" yaml:"codec_preferences"`
35 // RenderingMode controls GPU/CPU rendering selection for WebKit
36 RenderingMode RenderingMode `mapstructure:"rendering_mode" yaml:"rendering_mode"`
37}
38
39// RenderingMode selects GPU vs CPU rendering.
40type RenderingMode string
41
42const (
43 RenderingModeAuto RenderingMode = "auto"
44 RenderingModeGPU RenderingMode = "gpu"
45 RenderingModeCPU RenderingMode = "cpu"
46)
47
48// DatabaseConfig holds database-related configuration.
49type DatabaseConfig struct {
50 Path string `mapstructure:"path" yaml:"path"`
51 MaxConnections int `mapstructure:"max_connections" yaml:"max_connections"`
52 MaxIdleTime time.Duration `mapstructure:"max_idle_time" yaml:"max_idle_time"`
53 QueryTimeout time.Duration `mapstructure:"query_timeout" yaml:"query_timeout"`
54}
55
56// HistoryConfig holds history-related configuration.
57type HistoryConfig struct {
58 MaxEntries int `mapstructure:"max_entries" yaml:"max_entries"`
59 RetentionPeriodDays int `mapstructure:"retention_period_days" yaml:"retention_period_days"`
60 CleanupIntervalDays int `mapstructure:"cleanup_interval_days" yaml:"cleanup_interval_days"`
61}
62
63// SearchShortcut represents a search shortcut configuration.
64type SearchShortcut struct {
65 URL string `mapstructure:"url" yaml:"url" json:"url"`
66 Description string `mapstructure:"description" yaml:"description" json:"description"`
67}
68
69// DmenuConfig holds dmenu/rofi integration configuration.
70type DmenuConfig struct {
71 MaxHistoryItems int `mapstructure:"max_history_items" yaml:"max_history_items"`
72 ShowVisitCount bool `mapstructure:"show_visit_count" yaml:"show_visit_count"`
73 ShowLastVisited bool `mapstructure:"show_last_visited" yaml:"show_last_visited"`
74 HistoryPrefix string `mapstructure:"history_prefix" yaml:"history_prefix"`
75 ShortcutPrefix string `mapstructure:"shortcut_prefix" yaml:"shortcut_prefix"`
76 URLPrefix string `mapstructure:"url_prefix" yaml:"url_prefix"`
77 DateFormat string `mapstructure:"date_format" yaml:"date_format"`
78 SortByVisitCount bool `mapstructure:"sort_by_visit_count" yaml:"sort_by_visit_count"`
79}
80
81// LoggingConfig holds logging configuration.
82type LoggingConfig struct {
83 Level string `mapstructure:"level" yaml:"level"`
84 Format string `mapstructure:"format" yaml:"format"`
85 Filename string `mapstructure:"filename" yaml:"filename"`
86 MaxSize int `mapstructure:"max_size" yaml:"max_size"`
87 MaxBackups int `mapstructure:"max_backups" yaml:"max_backups"`
88 MaxAge int `mapstructure:"max_age" yaml:"max_age"`
89 Compress bool `mapstructure:"compress" yaml:"compress"`
90
91 // File output configuration
92 LogDir string `mapstructure:"log_dir" yaml:"log_dir"`
93 EnableFileLog bool `mapstructure:"enable_file_log" yaml:"enable_file_log"`
94
95 // Capture settings
96 CaptureStdout bool `mapstructure:"capture_stdout" yaml:"capture_stdout"`
97 CaptureStderr bool `mapstructure:"capture_stderr" yaml:"capture_stderr"`
98 CaptureCOutput bool `mapstructure:"capture_c_output" yaml:"capture_c_output"`
99
100 // Debug output
101 DebugFile string `mapstructure:"debug_file" yaml:"debug_file"`
102 VerboseWebKit bool `mapstructure:"verbose_webkit" yaml:"verbose_webkit"`
103}
104
105// AppearanceConfig holds UI/rendering preferences.
106type AppearanceConfig struct {
107 // Default fonts for pages that do not specify fonts.
108 SansFont string `mapstructure:"sans_font" yaml:"sans_font"`
109 SerifFont string `mapstructure:"serif_font" yaml:"serif_font"`
110 MonospaceFont string `mapstructure:"monospace_font" yaml:"monospace_font"`
111 // Default font size in CSS pixels (approx).
112 DefaultFontSize int `mapstructure:"default_font_size" yaml:"default_font_size"`
113}
114
115// VideoAccelerationConfig holds video hardware acceleration preferences.
116type VideoAccelerationConfig struct {
117 EnableVAAPI bool `mapstructure:"enable_vaapi" yaml:"enable_vaapi"`
118 AutoDetectGPU bool `mapstructure:"auto_detect_gpu" yaml:"auto_detect_gpu"`
119 VAAPIDriverName string `mapstructure:"vaapi_driver_name" yaml:"vaapi_driver_name"`
120 EnableAllDrivers bool `mapstructure:"enable_all_drivers" yaml:"enable_all_drivers"`
121 LegacyVAAPI bool `mapstructure:"legacy_vaapi" yaml:"legacy_vaapi"`
122}
123
124// CodecConfig holds video codec preferences and handling
125type CodecConfig struct {
126 // Codec preference order (e.g., "av1,h264,vp8")
127 PreferredCodecs string `mapstructure:"preferred_codecs" yaml:"preferred_codecs"`
128
129 // Force specific codec for platforms
130 ForceAV1 bool `mapstructure:"force_av1" yaml:"force_av1"`
131
132 // Block problematic codecs
133 BlockVP9 bool `mapstructure:"block_vp9" yaml:"block_vp9"`
134 BlockVP8 bool `mapstructure:"block_vp8" yaml:"block_vp8"`
135
136 // Hardware acceleration per codec
137 AV1HardwareOnly bool `mapstructure:"av1_hardware_only" yaml:"av1_hardware_only"`
138 DisableVP9Hardware bool `mapstructure:"disable_vp9_hardware" yaml:"disable_vp9_hardware"`
139
140 // Buffer configuration for smooth playback
141 VideoBufferSizeMB int `mapstructure:"video_buffer_size_mb" yaml:"video_buffer_size_mb"`
142 QueueBufferTimeSec int `mapstructure:"queue_buffer_time_sec" yaml:"queue_buffer_time_sec"`
143
144 // Custom User-Agent for codec negotiation
145 CustomUserAgent string `mapstructure:"custom_user_agent" yaml:"custom_user_agent"`
146
147 // Maximum resolution for AV1 codec (720p, 1080p, 1440p, 4k, unlimited)
148 AV1MaxResolution string `mapstructure:"av1_max_resolution" yaml:"av1_max_resolution"`
149
150 // Site-specific codec control settings
151 DisableTwitchCodecControl bool `mapstructure:"disable_twitch_codec_control" yaml:"disable_twitch_codec_control"`
152}
153
154// Manager handles configuration loading, watching, and reloading.
155type Manager struct {
156 config *Config
157 viper *viper.Viper
158 mu sync.RWMutex
159 callbacks []func(*Config)
160 watching bool
161}
162
163// NewManager creates a new configuration manager.
164func NewManager() (*Manager, error) {
165 v := viper.New()
166
167 // Configure Viper - supports yaml, json, toml automatically
168 v.SetConfigName("config") // Will find config.yaml, config.json, config.toml, etc.
169
170 // Add config paths
171 configDir, err := GetConfigDir()
172 if err != nil {
173 return nil, fmt.Errorf("failed to get config directory: %w", err)
174 }
175 v.AddConfigPath(configDir)
176 v.AddConfigPath(".") // Current directory for development
177
178 // Set up environment variable support
179 v.SetEnvPrefix("DUMB_BROWSER")
180 v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
181 v.AutomaticEnv()
182
183 // Bind specific environment variables
184 bindings := map[string]string{
185 "database.path": "DATABASE_PATH",
186 "database.max_connections": "DATABASE_MAX_CONNECTIONS",
187 "database.max_idle_time": "DATABASE_MAX_IDLE_TIME",
188 "database.query_timeout": "DATABASE_QUERY_TIMEOUT",
189 "browser.command": "BROWSER_COMMAND",
190 "browser.timeout": "BROWSER_TIMEOUT",
191 "browser.detach_process": "BROWSER_DETACH_PROCESS",
192 "history.max_entries": "HISTORY_MAX_ENTRIES",
193 "history.retention_period": "HISTORY_RETENTION_PERIOD",
194 "history.cleanup_interval": "HISTORY_CLEANUP_INTERVAL",
195 "dmenu.max_history_items": "DMENU_MAX_HISTORY_ITEMS",
196 "dmenu.show_visit_count": "DMENU_SHOW_VISIT_COUNT",
197 "dmenu.show_last_visited": "DMENU_SHOW_LAST_VISITED",
198 "dmenu.history_prefix": "DMENU_HISTORY_PREFIX",
199 "dmenu.shortcut_prefix": "DMENU_SHORTCUT_PREFIX",
200 "dmenu.url_prefix": "DMENU_URL_PREFIX",
201 "dmenu.date_format": "DMENU_DATE_FORMAT",
202 "dmenu.sort_by_visit_count": "DMENU_SORT_BY_VISIT_COUNT",
203 "logging.level": "LOGGING_LEVEL",
204 "logging.format": "LOGGING_FORMAT",
205 "logging.filename": "LOGGING_FILENAME",
206 "logging.max_size": "LOGGING_MAX_SIZE",
207 "logging.max_backups": "LOGGING_MAX_BACKUPS",
208 "logging.max_age": "LOGGING_MAX_AGE",
209 "logging.compress": "LOGGING_COMPRESS",
210 }
211
212 for key, env := range bindings {
213 if err := v.BindEnv(key, "DUMB_BROWSER_"+env); err != nil {
214 return nil, fmt.Errorf("failed to bind environment variable %s: %w", env, err)
215 }
216 }
217
218 // Explicit binding for rendering mode via dedicated env var
219 if err := v.BindEnv("rendering_mode", "DUMBER_RENDERING_MODE"); err != nil {
220 return nil, fmt.Errorf("failed to bind DUMBER_RENDERING_MODE: %w", err)
221 }
222
223 // Video acceleration environment variable bindings
224 videoAccelEnvBindings := map[string]string{
225 "video_acceleration.enable_vaapi": "DUMBER_VIDEO_ACCELERATION_ENABLE",
226 "video_acceleration.auto_detect_gpu": "DUMBER_VIDEO_AUTO_DETECT",
227 "video_acceleration.vaapi_driver_name": "LIBVA_DRIVER_NAME",
228 "video_acceleration.enable_all_drivers": "GST_VAAPI_ALL_DRIVERS",
229 "video_acceleration.legacy_vaapi": "WEBKIT_GST_ENABLE_LEGACY_VAAPI",
230 }
231
232 for key, env := range videoAccelEnvBindings {
233 if err := v.BindEnv(key, env); err != nil {
234 return nil, fmt.Errorf("failed to bind environment variable %s: %w", env, err)
235 }
236 }
237
238 // Codec preferences environment variable bindings
239 codecEnvBindings := map[string]string{
240 "codec_preferences.preferred_codecs": "DUMBER_PREFERRED_CODECS",
241 "codec_preferences.force_av1": "DUMBER_FORCE_AV1",
242 "codec_preferences.block_vp9": "DUMBER_BLOCK_VP9",
243 "codec_preferences.block_vp8": "DUMBER_BLOCK_VP8",
244 "codec_preferences.av1_hardware_only": "DUMBER_AV1_HW_ONLY",
245 "codec_preferences.disable_vp9_hardware": "DUMBER_DISABLE_VP9_HW",
246 "codec_preferences.video_buffer_size_mb": "DUMBER_VIDEO_BUFFER_MB",
247 "codec_preferences.queue_buffer_time_sec": "DUMBER_QUEUE_BUFFER_SEC",
248 "codec_preferences.custom_user_agent": "DUMBER_CUSTOM_UA",
249 "codec_preferences.av1_max_resolution": "DUMBER_AV1_MAX_RES",
250 "codec_preferences.disable_twitch_codec_control": "DUMBER_DISABLE_TWITCH_CODEC",
251 }
252
253 for key, env := range codecEnvBindings {
254 if err := v.BindEnv(key, env); err != nil {
255 return nil, fmt.Errorf("failed to bind environment variable %s: %w", env, err)
256 }
257 }
258
259 return &Manager{
260 viper: v,
261 callbacks: make([]func(*Config), 0),
262 }, nil
263}
264
265// Load loads the configuration from file and environment variables.
266func (m *Manager) Load() error {
267 m.mu.Lock()
268 defer m.mu.Unlock()
269
270 // Ensure directories exist
271 if err := EnsureDirectories(); err != nil {
272 return fmt.Errorf("failed to ensure directories: %w", err)
273 }
274
275 // Set defaults
276 m.setDefaults()
277
278 // Read config file if it exists
279 if err := m.viper.ReadInConfig(); err != nil {
280 var configFileNotFoundError viper.ConfigFileNotFoundError
281 if errors.As(err, &configFileNotFoundError) {
282 // Config file not found, create default one
283 if err := m.createDefaultConfig(); err != nil {
284 return fmt.Errorf("failed to create default config: %w", err)
285 }
286 } else {
287 return fmt.Errorf("failed to read config file: %w", err)
288 }
289 }
290
291 // Unmarshal into config struct
292 config := &Config{}
293 if err := m.viper.Unmarshal(config); err != nil {
294 return fmt.Errorf("failed to unmarshal config: %w", err)
295 }
296
297 // Set database path if not specified
298 if config.Database.Path == "" {
299 dbPath, err := GetDatabaseFile()
300 if err != nil {
301 return fmt.Errorf("failed to get database path: %w", err)
302 }
303 config.Database.Path = dbPath
304 }
305
306 // Normalize/validate rendering mode
307 switch strings.ToLower(string(config.RenderingMode)) {
308 case "", string(RenderingModeAuto):
309 config.RenderingMode = RenderingModeAuto
310 case string(RenderingModeGPU):
311 config.RenderingMode = RenderingModeGPU
312 case string(RenderingModeCPU):
313 config.RenderingMode = RenderingModeCPU
314 default:
315 config.RenderingMode = RenderingModeAuto
316 }
317
318 // Auto-detect GPU if enabled and driver name is not set
319 if config.VideoAcceleration.AutoDetectGPU && config.VideoAcceleration.VAAPIDriverName == "" {
320 gpuInfo := gpu.DetectGPU()
321 if gpuInfo.SupportsVAAPI() {
322 config.VideoAcceleration.VAAPIDriverName = gpuInfo.GetVAAPIDriverName()
323 }
324 }
325
326 // Validate and configure codec preferences
327 config = m.validateAndConfigureCodecPreferences(config)
328
329 m.config = config
330 return nil
331}
332
333// Get returns the current configuration (thread-safe).
334func (m *Manager) Get() *Config {
335 m.mu.RLock()
336 defer m.mu.RUnlock()
337
338 // Return a copy to prevent external modification
339 configCopy := *m.config
340 return &configCopy
341}
342
343// Watch starts watching the config file for changes and reloads automatically.
344func (m *Manager) Watch() error {
345 m.mu.Lock()
346 defer m.mu.Unlock()
347
348 if m.watching {
349 return nil // Already watching
350 }
351
352 m.viper.WatchConfig()
353 m.viper.OnConfigChange(func(_ fsnotify.Event) {
354 // Reload config
355 if err := m.reload(); err != nil {
356 fmt.Fprintf(os.Stderr, "Warning: failed to reload config: %v\n", err)
357 return
358 }
359
360 // Notify callbacks
361 m.mu.RLock()
362 config := m.config
363 callbacks := make([]func(*Config), len(m.callbacks))
364 copy(callbacks, m.callbacks)
365 m.mu.RUnlock()
366
367 for _, callback := range callbacks {
368 callback(config)
369 }
370 })
371
372 m.watching = true
373 return nil
374}
375
376// OnConfigChange registers a callback function to be called when config changes.
377func (m *Manager) OnConfigChange(callback func(*Config)) {
378 m.mu.Lock()
379 defer m.mu.Unlock()
380
381 m.callbacks = append(m.callbacks, callback)
382}
383
384// reload reloads the configuration (internal method, must be called with lock).
385func (m *Manager) reload() error {
386 if err := m.viper.ReadInConfig(); err != nil {
387 return err
388 }
389
390 config := &Config{}
391 if err := m.viper.Unmarshal(config); err != nil {
392 return err
393 }
394
395 // Set database path if not specified
396 if config.Database.Path == "" {
397 dbPath, err := GetDatabaseFile()
398 if err != nil {
399 return fmt.Errorf("failed to get database path: %w", err)
400 }
401 config.Database.Path = dbPath
402 }
403
404 // Normalize/validate rendering mode
405 switch strings.ToLower(string(config.RenderingMode)) {
406 case "", string(RenderingModeAuto):
407 config.RenderingMode = RenderingModeAuto
408 case string(RenderingModeGPU):
409 config.RenderingMode = RenderingModeGPU
410 case string(RenderingModeCPU):
411 config.RenderingMode = RenderingModeCPU
412 default:
413 config.RenderingMode = RenderingModeAuto
414 }
415
416 // Auto-detect GPU if enabled and driver name is not set
417 if config.VideoAcceleration.AutoDetectGPU && config.VideoAcceleration.VAAPIDriverName == "" {
418 gpuInfo := gpu.DetectGPU()
419 if gpuInfo.SupportsVAAPI() {
420 config.VideoAcceleration.VAAPIDriverName = gpuInfo.GetVAAPIDriverName()
421 }
422 }
423
424 // Validate and configure codec preferences
425 config = m.validateAndConfigureCodecPreferences(config)
426
427 m.config = config
428 return nil
429}
430
431// setDefaults sets default configuration values in Viper.
432func (m *Manager) setDefaults() {
433 defaults := DefaultConfig()
434
435 // Database defaults
436 m.viper.SetDefault("database.max_connections", defaults.Database.MaxConnections)
437 m.viper.SetDefault("database.max_idle_time", defaults.Database.MaxIdleTime)
438 m.viper.SetDefault("database.query_timeout", defaults.Database.QueryTimeout)
439
440 // Note: Browser config removed - we build our own browser
441
442 // History defaults
443 m.viper.SetDefault("history.max_entries", defaults.History.MaxEntries)
444 m.viper.SetDefault("history.retention_period_days", defaults.History.RetentionPeriodDays)
445 m.viper.SetDefault("history.cleanup_interval_days", defaults.History.CleanupIntervalDays)
446
447 // Search shortcuts defaults
448 m.viper.SetDefault("search_shortcuts", defaults.SearchShortcuts)
449
450 // Dmenu defaults
451 m.viper.SetDefault("dmenu.max_history_items", defaults.Dmenu.MaxHistoryItems)
452 m.viper.SetDefault("dmenu.show_visit_count", defaults.Dmenu.ShowVisitCount)
453 m.viper.SetDefault("dmenu.show_last_visited", defaults.Dmenu.ShowLastVisited)
454 m.viper.SetDefault("dmenu.history_prefix", defaults.Dmenu.HistoryPrefix)
455 m.viper.SetDefault("dmenu.shortcut_prefix", defaults.Dmenu.ShortcutPrefix)
456 m.viper.SetDefault("dmenu.url_prefix", defaults.Dmenu.URLPrefix)
457 m.viper.SetDefault("dmenu.date_format", defaults.Dmenu.DateFormat)
458 m.viper.SetDefault("dmenu.sort_by_visit_count", defaults.Dmenu.SortByVisitCount)
459
460 // Logging defaults
461 m.viper.SetDefault("logging.level", defaults.Logging.Level)
462 m.viper.SetDefault("logging.format", defaults.Logging.Format)
463 m.viper.SetDefault("logging.filename", defaults.Logging.Filename)
464 m.viper.SetDefault("logging.max_size", defaults.Logging.MaxSize)
465 m.viper.SetDefault("logging.max_backups", defaults.Logging.MaxBackups)
466 m.viper.SetDefault("logging.max_age", defaults.Logging.MaxAge)
467 m.viper.SetDefault("logging.compress", defaults.Logging.Compress)
468 m.viper.SetDefault("logging.log_dir", defaults.Logging.LogDir)
469 m.viper.SetDefault("logging.enable_file_log", defaults.Logging.EnableFileLog)
470 m.viper.SetDefault("logging.capture_stdout", defaults.Logging.CaptureStdout)
471 m.viper.SetDefault("logging.capture_stderr", defaults.Logging.CaptureStderr)
472 m.viper.SetDefault("logging.capture_c_output", defaults.Logging.CaptureCOutput)
473 m.viper.SetDefault("logging.debug_file", defaults.Logging.DebugFile)
474 m.viper.SetDefault("logging.verbose_webkit", defaults.Logging.VerboseWebKit)
475
476 // Appearance defaults
477 m.viper.SetDefault("appearance.sans_font", defaults.Appearance.SansFont)
478 m.viper.SetDefault("appearance.serif_font", defaults.Appearance.SerifFont)
479 m.viper.SetDefault("appearance.monospace_font", defaults.Appearance.MonospaceFont)
480 m.viper.SetDefault("appearance.default_font_size", defaults.Appearance.DefaultFontSize)
481
482 // Video acceleration defaults
483 m.viper.SetDefault("video_acceleration.enable_vaapi", defaults.VideoAcceleration.EnableVAAPI)
484 m.viper.SetDefault("video_acceleration.auto_detect_gpu", defaults.VideoAcceleration.AutoDetectGPU)
485 m.viper.SetDefault("video_acceleration.vaapi_driver_name", defaults.VideoAcceleration.VAAPIDriverName)
486 m.viper.SetDefault("video_acceleration.enable_all_drivers", defaults.VideoAcceleration.EnableAllDrivers)
487 m.viper.SetDefault("video_acceleration.legacy_vaapi", defaults.VideoAcceleration.LegacyVAAPI)
488
489 // Codec preferences defaults
490 m.viper.SetDefault("codec_preferences.preferred_codecs", defaults.CodecPreferences.PreferredCodecs)
491 m.viper.SetDefault("codec_preferences.force_av1", defaults.CodecPreferences.ForceAV1)
492 m.viper.SetDefault("codec_preferences.block_vp9", defaults.CodecPreferences.BlockVP9)
493 m.viper.SetDefault("codec_preferences.block_vp8", defaults.CodecPreferences.BlockVP8)
494 m.viper.SetDefault("codec_preferences.av1_hardware_only", defaults.CodecPreferences.AV1HardwareOnly)
495 m.viper.SetDefault("codec_preferences.disable_vp9_hardware", defaults.CodecPreferences.DisableVP9Hardware)
496 m.viper.SetDefault("codec_preferences.video_buffer_size_mb", defaults.CodecPreferences.VideoBufferSizeMB)
497 m.viper.SetDefault("codec_preferences.queue_buffer_time_sec", defaults.CodecPreferences.QueueBufferTimeSec)
498 m.viper.SetDefault("codec_preferences.custom_user_agent", defaults.CodecPreferences.CustomUserAgent)
499 m.viper.SetDefault("codec_preferences.av1_max_resolution", defaults.CodecPreferences.AV1MaxResolution)
500 m.viper.SetDefault("codec_preferences.disable_twitch_codec_control", defaults.CodecPreferences.DisableTwitchCodecControl)
501
502 // Rendering defaults
503 m.viper.SetDefault("rendering_mode", string(RenderingModeAuto))
504}
505
506// createDefaultConfig creates a default configuration file.
507func (m *Manager) createDefaultConfig() error {
508 configFile, err := GetConfigFile()
509 if err != nil {
510 return err
511 }
512
513 // Create config directory if it doesn't exist
514 if err := os.MkdirAll(filepath.Dir(configFile), dirPerm); err != nil {
515 return err
516 }
517
518 // Get the default configuration
519 defaultConfig := DefaultConfig()
520
521 // Marshal to JSON with proper indentation
522 configData, err := json.MarshalIndent(defaultConfig, "", " ")
523 if err != nil {
524 return fmt.Errorf("failed to marshal default config: %w", err)
525 }
526
527 // Write JSON config file
528 if err := os.WriteFile(configFile, configData, filePerm); err != nil {
529 return fmt.Errorf("failed to write config file: %w", err)
530 }
531
532 fmt.Printf("Created default configuration file: %s\n", configFile)
533 return nil
534}
535
536// GetConfigFile returns the path to the configuration file being used.
537func (m *Manager) GetConfigFile() string {
538 return m.viper.ConfigFileUsed()
539}
540
541// Global configuration manager instance
542var globalManager *Manager
543var globalManagerOnce sync.Once
544
545// Init initializes the global configuration manager.
546func Init() error {
547 var err error
548 globalManagerOnce.Do(func() {
549 globalManager, err = NewManager()
550 if err != nil {
551 return
552 }
553 err = globalManager.Load()
554 })
555 return err
556}
557
558// Get returns the global configuration.
559func Get() *Config {
560 if globalManager == nil {
561 // Return defaults if not initialized
562 return DefaultConfig()
563 }
564 return globalManager.Get()
565}
566
567// Watch starts watching the global configuration for changes.
568func Watch() error {
569 if globalManager == nil {
570 return fmt.Errorf("configuration not initialized")
571 }
572 return globalManager.Watch()
573}
574
575// OnConfigChange registers a callback for global configuration changes.
576func OnConfigChange(callback func(*Config)) {
577 if globalManager == nil {
578 return
579 }
580 globalManager.OnConfigChange(callback)
581}
582
583// validateAndConfigureCodecPreferences validates and auto-configures codec preferences based on GPU capabilities
584func (m *Manager) validateAndConfigureCodecPreferences(config *Config) *Config {
585 // Validate codec preferences
586 if config.CodecPreferences.PreferredCodecs == "" {
587 config.CodecPreferences.PreferredCodecs = "av1,h264,vp8"
588 fmt.Printf("Config: Set default codec preferences: %s\n", config.CodecPreferences.PreferredCodecs)
589 }
590
591 // Auto-configure based on GPU capabilities
592 if config.VideoAcceleration.AutoDetectGPU {
593 gpuInfo := gpu.DetectGPU()
594
595 // Check AV1 hardware support
596 if config.CodecPreferences.AV1HardwareOnly && !gpuInfo.SupportsAV1Hardware() {
597 // Disable AV1 hardware-only if GPU doesn't support it
598 config.CodecPreferences.AV1HardwareOnly = false
599 fmt.Printf("Config: GPU doesn't support AV1 hardware, enabling software fallback\n")
600 }
601
602 // Get detailed AV1 capabilities
603 av1Caps := gpuInfo.GetAV1HardwareCapabilities()
604 if av1Caps["decode"] {
605 fmt.Printf("Config: GPU supports AV1 hardware decode\n")
606
607 // If GPU supports AV1, prefer it over VP9
608 if !config.CodecPreferences.ForceAV1 &&
609 !strings.HasPrefix(config.CodecPreferences.PreferredCodecs, "av1") {
610 config.CodecPreferences.PreferredCodecs = "av1," + config.CodecPreferences.PreferredCodecs
611 fmt.Printf("Config: Auto-enabled AV1 preference due to hardware support\n")
612 }
613 } else {
614 // If no AV1 support, ensure VP9 hardware is disabled to prevent issues
615 if !config.CodecPreferences.DisableVP9Hardware {
616 config.CodecPreferences.DisableVP9Hardware = true
617 fmt.Printf("Config: Auto-disabled VP9 hardware acceleration (no AV1 fallback)\n")
618 }
619 }
620
621 // Log GPU-specific codec capabilities
622 fmt.Printf("Config: %s GPU codec capabilities - AV1: decode=%t, encode=%t, 10bit=%t\n",
623 gpuInfo.Vendor, av1Caps["decode"], av1Caps["encode"], av1Caps["10bit"])
624 }
625
626 return config
627}