feat(media): add video buffering configuration for smooth playback
- commit
- ab84ac97693c78e6834645ba66bbe826f996149a
- author
- bnema · · 2025-12-22 21:42 UTC
- parent
- d0dbbe7
- signature
- unsigned
7 files · +327 -4
M docs/CONFIG.md +29 -3
| @@ -274,18 +274,44 @@ cancel = ["escape"] | |||
| 274 | 274 | | `media.hardware_decoding` | string | `"auto"` | `auto`, `force`, `disable` | Hardware video decoding mode | | |
| 275 | 275 | | `media.prefer_av1` | bool | `true` | - | Prefer AV1 codec when available | | |
| 276 | 276 | | `media.show_diagnostics` | bool | `true` | - | Show media diagnostics warnings at startup | | |
| 277 | + | | `media.force_vsync` | bool | `false` | - | Force VSync for video playback (may help with tearing) | | |
| 278 | + | | `media.gl_rendering_mode` | string | `"auto"` | `auto`, `gles2`, `gl3`, `none` | OpenGL API selection for video rendering | | |
| 279 | + | | `media.gstreamer_debug_level` | int | `0` | `0-5` | GStreamer debug verbosity (0=off) | | |
| 280 | + | | `media.video_buffer_size_mb` | int | `64` | > 0 | Video buffer size in MB for smoother streaming | | |
| 281 | + | | `media.queue_buffer_time_sec` | int | `20` | > 0 | Queue prebuffer time in seconds | | |
| 277 | 282 | ||
| 278 | 283 | **Hardware decoding modes:** | |
| 279 | 284 | - `auto` (recommended): Hardware preferred with software fallback - fixes Twitch Error #4000 | |
| 280 | 285 | - `force`: Hardware only - fails if unavailable | |
| 281 | 286 | - `disable`: Software only - higher CPU usage | |
| 282 | 287 | ||
| 288 | + | **GL rendering modes:** | |
| 289 | + | - `auto` (default): Let GStreamer choose the best OpenGL API | |
| 290 | + | - `gles2`: Force GLES2 (better for some mobile/embedded GPUs) | |
| 291 | + | - `gl3`: Force OpenGL 3.x desktop | |
| 292 | + | - `none`: Disable GL-based rendering | |
| 293 | + | ||
| 294 | + | **GPU auto-detection:** | |
| 295 | + | Dumber automatically detects your GPU vendor (AMD/Intel/NVIDIA) and sets optimal VA-API driver settings: | |
| 296 | + | - **AMD**: Uses `radeonsi` driver | |
| 297 | + | - **Intel**: Uses `iHD` driver (modern, for Broadwell+) | |
| 298 | + | - **NVIDIA**: Uses `nvidia` driver with EGL platform | |
| 299 | + | ||
| 300 | + | **Buffering settings:** | |
| 301 | + | - `video_buffer_size_mb`: Controls GStreamer buffer size. Larger buffers reduce rebuffering on bursty streams (Twitch, YouTube). Uses more memory. | |
| 302 | + | - `queue_buffer_time_sec`: Controls prebuffer duration. Higher values allow more data to be buffered ahead of playback. | |
| 303 | + | ||
| 283 | 304 | **Example:** | |
| 284 | 305 | ```toml | |
| 285 | 306 | [media] | |
| 286 | - | hardware_decoding = "auto" # HW preferred, SW fallback | |
| 287 | - | prefer_av1 = true # AV1 is most efficient codec | |
| 288 | - | show_diagnostics = true # Log warnings if HW accel unavailable | |
| 307 | + | hardware_decoding = "auto" # HW preferred, SW fallback | |
| 308 | + | prefer_av1 = true # AV1 is most efficient codec | |
| 309 | + | show_diagnostics = true # Log warnings if HW accel unavailable | |
| 310 | + | force_vsync = false # Let compositor handle VSync | |
| 311 | + | gl_rendering_mode = "auto" # GStreamer picks best GL API | |
| 312 | + | gstreamer_debug_level = 0 # Increase to 3-5 for debugging | |
| 313 | + | video_buffer_size_mb = 64 # 64 MB buffer for smooth streaming | |
| 314 | + | queue_buffer_time_sec = 20 # 20 seconds prebuffer | |
| 289 | 315 | ``` | |
| 290 | 316 | ||
| 291 | 317 | **Diagnostics CLI:** | |
| @@ -274,18 +274,44 @@ cancel = ["escape"] | |||||
| 274 | | `media.hardware_decoding` | string | `"auto"` | `auto`, `force`, `disable` | Hardware video decoding mode | | 274 | | `media.hardware_decoding` | string | `"auto"` | `auto`, `force`, `disable` | Hardware video decoding mode | | ||
| 275 | | `media.prefer_av1` | bool | `true` | - | Prefer AV1 codec when available | | 275 | | `media.prefer_av1` | bool | `true` | - | Prefer AV1 codec when available | | ||
| 276 | | `media.show_diagnostics` | bool | `true` | - | Show media diagnostics warnings at startup | | 276 | | `media.show_diagnostics` | bool | `true` | - | Show media diagnostics warnings at startup | | ||
| 277 | + | | `media.force_vsync` | bool | `false` | - | Force VSync for video playback (may help with tearing) | | |||
| 278 | + | | `media.gl_rendering_mode` | string | `"auto"` | `auto`, `gles2`, `gl3`, `none` | OpenGL API selection for video rendering | | |||
| 279 | + | | `media.gstreamer_debug_level` | int | `0` | `0-5` | GStreamer debug verbosity (0=off) | | |||
| 280 | + | | `media.video_buffer_size_mb` | int | `64` | > 0 | Video buffer size in MB for smoother streaming | | |||
| 281 | + | | `media.queue_buffer_time_sec` | int | `20` | > 0 | Queue prebuffer time in seconds | | |||
| 277 | 282 | ||||
| 278 | **Hardware decoding modes:** | 283 | **Hardware decoding modes:** | ||
| 279 | - `auto` (recommended): Hardware preferred with software fallback - fixes Twitch Error #4000 | 284 | - `auto` (recommended): Hardware preferred with software fallback - fixes Twitch Error #4000 | ||
| 280 | - `force`: Hardware only - fails if unavailable | 285 | - `force`: Hardware only - fails if unavailable | ||
| 281 | - `disable`: Software only - higher CPU usage | 286 | - `disable`: Software only - higher CPU usage | ||
| 282 | 287 | ||||
| 288 | + | **GL rendering modes:** | |||
| 289 | + | - `auto` (default): Let GStreamer choose the best OpenGL API | |||
| 290 | + | - `gles2`: Force GLES2 (better for some mobile/embedded GPUs) | |||
| 291 | + | - `gl3`: Force OpenGL 3.x desktop | |||
| 292 | + | - `none`: Disable GL-based rendering | |||
| 293 | + | ||||
| 294 | + | **GPU auto-detection:** | |||
| 295 | + | Dumber automatically detects your GPU vendor (AMD/Intel/NVIDIA) and sets optimal VA-API driver settings: | |||
| 296 | + | - **AMD**: Uses `radeonsi` driver | |||
| 297 | + | - **Intel**: Uses `iHD` driver (modern, for Broadwell+) | |||
| 298 | + | - **NVIDIA**: Uses `nvidia` driver with EGL platform | |||
| 299 | + | ||||
| 300 | + | **Buffering settings:** | |||
| 301 | + | - `video_buffer_size_mb`: Controls GStreamer buffer size. Larger buffers reduce rebuffering on bursty streams (Twitch, YouTube). Uses more memory. | |||
| 302 | + | - `queue_buffer_time_sec`: Controls prebuffer duration. Higher values allow more data to be buffered ahead of playback. | |||
| 303 | + | ||||
| 283 | **Example:** | 304 | **Example:** | ||
| 284 | ```toml | 305 | ```toml | ||
| 285 | [media] | 306 | [media] | ||
| 286 | - | hardware_decoding = "auto" # HW preferred, SW fallback | 307 | + | hardware_decoding = "auto" # HW preferred, SW fallback |
| 287 | - | prefer_av1 = true # AV1 is most efficient codec | 308 | + | prefer_av1 = true # AV1 is most efficient codec |
| 288 | - | show_diagnostics = true # Log warnings if HW accel unavailable | 309 | + | show_diagnostics = true # Log warnings if HW accel unavailable |
| 310 | + | force_vsync = false # Let compositor handle VSync | |||
| 311 | + | gl_rendering_mode = "auto" # GStreamer picks best GL API | |||
| 312 | + | gstreamer_debug_level = 0 # Increase to 3-5 for debugging | |||
| 313 | + | video_buffer_size_mb = 64 # 64 MB buffer for smooth streaming | |||
| 314 | + | queue_buffer_time_sec = 20 # 20 seconds prebuffer | |||
| 289 | ``` | 315 | ``` | ||
| 290 | 316 | ||||
| 291 | **Diagnostics CLI:** | 317 | **Diagnostics CLI:** | ||
A internal/application/port/gstreamer_env.go +51 -0
| @@ -0,0 +1,51 @@ | |||
| 1 | + | // Package port defines interfaces for external dependencies. | |
| 2 | + | package port | |
| 3 | + | ||
| 4 | + | import "context" | |
| 5 | + | ||
| 6 | + | // GPUVendor identifies the GPU manufacturer for vendor-specific optimizations. | |
| 7 | + | type GPUVendor string | |
| 8 | + | ||
| 9 | + | const ( | |
| 10 | + | // GPUVendorAMD represents AMD/ATI GPUs (vendor ID 0x1002) | |
| 11 | + | GPUVendorAMD GPUVendor = "amd" | |
| 12 | + | // GPUVendorIntel represents Intel integrated/discrete GPUs (vendor ID 0x8086) | |
| 13 | + | GPUVendorIntel GPUVendor = "intel" | |
| 14 | + | // GPUVendorNVIDIA represents NVIDIA GPUs (vendor ID 0x10de) | |
| 15 | + | GPUVendorNVIDIA GPUVendor = "nvidia" | |
| 16 | + | // GPUVendorUnknown represents undetected or unsupported GPUs | |
| 17 | + | GPUVendorUnknown GPUVendor = "unknown" | |
| 18 | + | ) | |
| 19 | + | ||
| 20 | + | // GStreamerEnvSettings contains settings for GStreamer environment configuration. | |
| 21 | + | // This is a port-local type to avoid import cycles with config package. | |
| 22 | + | type GStreamerEnvSettings struct { | |
| 23 | + | // ForceVSync enables vertical sync for video playback | |
| 24 | + | ForceVSync bool | |
| 25 | + | // GLRenderingMode controls OpenGL API selection: "auto", "gles2", "gl3", "none" | |
| 26 | + | GLRenderingMode string | |
| 27 | + | // GStreamerDebugLevel sets GStreamer debug verbosity (0-5) | |
| 28 | + | GStreamerDebugLevel int | |
| 29 | + | // VideoBufferSizeMB sets buffer size in megabytes for video streaming | |
| 30 | + | VideoBufferSizeMB int | |
| 31 | + | // QueueBufferTimeSec sets queue buffer time in seconds for prebuffering | |
| 32 | + | QueueBufferTimeSec int | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | // GStreamerEnvManager configures GStreamer environment variables | |
| 36 | + | // based on system configuration and detected GPU vendor. | |
| 37 | + | // Environment variables must be set BEFORE GTK/WebKit initialization. | |
| 38 | + | type GStreamerEnvManager interface { | |
| 39 | + | // DetectGPUVendor identifies the primary GPU vendor from system info. | |
| 40 | + | // Uses /sys/class/drm/card*/device/vendor as primary detection method. | |
| 41 | + | DetectGPUVendor(ctx context.Context) GPUVendor | |
| 42 | + | ||
| 43 | + | // ApplyEnvironment sets GStreamer environment variables based on | |
| 44 | + | // the provided settings and detected GPU vendor. | |
| 45 | + | // Must be called before any GTK/GStreamer initialization. | |
| 46 | + | ApplyEnvironment(ctx context.Context, settings GStreamerEnvSettings) error | |
| 47 | + | ||
| 48 | + | // GetAppliedVars returns a map of environment variables that were set. | |
| 49 | + | // Useful for logging and debugging. | |
| 50 | + | GetAppliedVars() map[string]string | |
| 51 | + | } | |
| @@ -0,0 +1,51 @@ | |||||
| 1 | + | // Package port defines interfaces for external dependencies. | |||
| 2 | + | package port | |||
| 3 | + | ||||
| 4 | + | import "context" | |||
| 5 | + | ||||
| 6 | + | // GPUVendor identifies the GPU manufacturer for vendor-specific optimizations. | |||
| 7 | + | type GPUVendor string | |||
| 8 | + | ||||
| 9 | + | const ( | |||
| 10 | + | // GPUVendorAMD represents AMD/ATI GPUs (vendor ID 0x1002) | |||
| 11 | + | GPUVendorAMD GPUVendor = "amd" | |||
| 12 | + | // GPUVendorIntel represents Intel integrated/discrete GPUs (vendor ID 0x8086) | |||
| 13 | + | GPUVendorIntel GPUVendor = "intel" | |||
| 14 | + | // GPUVendorNVIDIA represents NVIDIA GPUs (vendor ID 0x10de) | |||
| 15 | + | GPUVendorNVIDIA GPUVendor = "nvidia" | |||
| 16 | + | // GPUVendorUnknown represents undetected or unsupported GPUs | |||
| 17 | + | GPUVendorUnknown GPUVendor = "unknown" | |||
| 18 | + | ) | |||
| 19 | + | ||||
| 20 | + | // GStreamerEnvSettings contains settings for GStreamer environment configuration. | |||
| 21 | + | // This is a port-local type to avoid import cycles with config package. | |||
| 22 | + | type GStreamerEnvSettings struct { | |||
| 23 | + | // ForceVSync enables vertical sync for video playback | |||
| 24 | + | ForceVSync bool | |||
| 25 | + | // GLRenderingMode controls OpenGL API selection: "auto", "gles2", "gl3", "none" | |||
| 26 | + | GLRenderingMode string | |||
| 27 | + | // GStreamerDebugLevel sets GStreamer debug verbosity (0-5) | |||
| 28 | + | GStreamerDebugLevel int | |||
| 29 | + | // VideoBufferSizeMB sets buffer size in megabytes for video streaming | |||
| 30 | + | VideoBufferSizeMB int | |||
| 31 | + | // QueueBufferTimeSec sets queue buffer time in seconds for prebuffering | |||
| 32 | + | QueueBufferTimeSec int | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | // GStreamerEnvManager configures GStreamer environment variables | |||
| 36 | + | // based on system configuration and detected GPU vendor. | |||
| 37 | + | // Environment variables must be set BEFORE GTK/WebKit initialization. | |||
| 38 | + | type GStreamerEnvManager interface { | |||
| 39 | + | // DetectGPUVendor identifies the primary GPU vendor from system info. | |||
| 40 | + | // Uses /sys/class/drm/card*/device/vendor as primary detection method. | |||
| 41 | + | DetectGPUVendor(ctx context.Context) GPUVendor | |||
| 42 | + | ||||
| 43 | + | // ApplyEnvironment sets GStreamer environment variables based on | |||
| 44 | + | // the provided settings and detected GPU vendor. | |||
| 45 | + | // Must be called before any GTK/GStreamer initialization. | |||
| 46 | + | ApplyEnvironment(ctx context.Context, settings GStreamerEnvSettings) error | |||
| 47 | + | ||||
| 48 | + | // GetAppliedVars returns a map of environment variables that were set. | |||
| 49 | + | // Useful for logging and debugging. | |||
| 50 | + | GetAppliedVars() map[string]string | |||
| 51 | + | } | |||
M internal/bootstrap/webkit_stack.go +21 -0
| @@ -5,9 +5,11 @@ import ( | |||
| 5 | 5 | "path/filepath" | |
| 6 | 6 | ||
| 7 | 7 | "github.com/bnema/dumber/assets" | |
| 8 | + | "github.com/bnema/dumber/internal/application/port" | |
| 8 | 9 | "github.com/bnema/dumber/internal/application/usecase" | |
| 9 | 10 | "github.com/bnema/dumber/internal/infrastructure/config" | |
| 10 | 11 | "github.com/bnema/dumber/internal/infrastructure/filtering" | |
| 12 | + | "github.com/bnema/dumber/internal/infrastructure/media" | |
| 11 | 13 | "github.com/bnema/dumber/internal/infrastructure/webkit" | |
| 12 | 14 | "github.com/bnema/dumber/internal/ui/theme" | |
| 13 | 15 | "github.com/rs/zerolog" | |
| @@ -30,6 +32,25 @@ func BuildWebKitStack( | |||
| 30 | 32 | themeManager *theme.Manager, | |
| 31 | 33 | logger zerolog.Logger, | |
| 32 | 34 | ) WebKitStack { | |
| 35 | + | // CRITICAL: Configure GStreamer environment BEFORE WebKit/GTK initialization. | |
| 36 | + | // Environment variables must be set before GStreamer initializes its pipelines. | |
| 37 | + | gstEnv := media.NewEnvManager() | |
| 38 | + | gpuVendor := gstEnv.DetectGPUVendor(ctx) | |
| 39 | + | gstSettings := port.GStreamerEnvSettings{ | |
| 40 | + | ForceVSync: cfg.Media.ForceVSync, | |
| 41 | + | GLRenderingMode: string(cfg.Media.GLRenderingMode), | |
| 42 | + | GStreamerDebugLevel: cfg.Media.GStreamerDebugLevel, | |
| 43 | + | VideoBufferSizeMB: cfg.Media.VideoBufferSizeMB, | |
| 44 | + | QueueBufferTimeSec: cfg.Media.QueueBufferTimeSec, | |
| 45 | + | } | |
| 46 | + | if err := gstEnv.ApplyEnvironment(ctx, gstSettings); err != nil { | |
| 47 | + | logger.Warn().Err(err).Msg("failed to apply gstreamer environment") | |
| 48 | + | } | |
| 49 | + | logger.Info(). | |
| 50 | + | Str("gpu", string(gpuVendor)). | |
| 51 | + | Interface("vars", gstEnv.GetAppliedVars()). | |
| 52 | + | Msg("gstreamer environment configured") | |
| 53 | + | ||
| 33 | 54 | wkCtx, err := webkit.NewWebKitContext(ctx, dataDir, cacheDir) | |
| 34 | 55 | if err != nil { | |
| 35 | 56 | logger.Fatal().Err(err).Msg("failed to initialize WebKit context") | |
| @@ -5,9 +5,11 @@ import ( | |||||
| 5 | "path/filepath" | 5 | "path/filepath" | ||
| 6 | 6 | ||||
| 7 | "github.com/bnema/dumber/assets" | 7 | "github.com/bnema/dumber/assets" | ||
| 8 | + | "github.com/bnema/dumber/internal/application/port" | |||
| 8 | "github.com/bnema/dumber/internal/application/usecase" | 9 | "github.com/bnema/dumber/internal/application/usecase" | ||
| 9 | "github.com/bnema/dumber/internal/infrastructure/config" | 10 | "github.com/bnema/dumber/internal/infrastructure/config" | ||
| 10 | "github.com/bnema/dumber/internal/infrastructure/filtering" | 11 | "github.com/bnema/dumber/internal/infrastructure/filtering" | ||
| 12 | + | "github.com/bnema/dumber/internal/infrastructure/media" | |||
| 11 | "github.com/bnema/dumber/internal/infrastructure/webkit" | 13 | "github.com/bnema/dumber/internal/infrastructure/webkit" | ||
| 12 | "github.com/bnema/dumber/internal/ui/theme" | 14 | "github.com/bnema/dumber/internal/ui/theme" | ||
| 13 | "github.com/rs/zerolog" | 15 | "github.com/rs/zerolog" | ||
| @@ -30,6 +32,25 @@ func BuildWebKitStack( | |||||
| 30 | themeManager *theme.Manager, | 32 | themeManager *theme.Manager, | ||
| 31 | logger zerolog.Logger, | 33 | logger zerolog.Logger, | ||
| 32 | ) WebKitStack { | 34 | ) WebKitStack { | ||
| 35 | + | // CRITICAL: Configure GStreamer environment BEFORE WebKit/GTK initialization. | |||
| 36 | + | // Environment variables must be set before GStreamer initializes its pipelines. | |||
| 37 | + | gstEnv := media.NewEnvManager() | |||
| 38 | + | gpuVendor := gstEnv.DetectGPUVendor(ctx) | |||
| 39 | + | gstSettings := port.GStreamerEnvSettings{ | |||
| 40 | + | ForceVSync: cfg.Media.ForceVSync, | |||
| 41 | + | GLRenderingMode: string(cfg.Media.GLRenderingMode), | |||
| 42 | + | GStreamerDebugLevel: cfg.Media.GStreamerDebugLevel, | |||
| 43 | + | VideoBufferSizeMB: cfg.Media.VideoBufferSizeMB, | |||
| 44 | + | QueueBufferTimeSec: cfg.Media.QueueBufferTimeSec, | |||
| 45 | + | } | |||
| 46 | + | if err := gstEnv.ApplyEnvironment(ctx, gstSettings); err != nil { | |||
| 47 | + | logger.Warn().Err(err).Msg("failed to apply gstreamer environment") | |||
| 48 | + | } | |||
| 49 | + | logger.Info(). | |||
| 50 | + | Str("gpu", string(gpuVendor)). | |||
| 51 | + | Interface("vars", gstEnv.GetAppliedVars()). | |||
| 52 | + | Msg("gstreamer environment configured") | |||
| 53 | + | ||||
| 33 | wkCtx, err := webkit.NewWebKitContext(ctx, dataDir, cacheDir) | 54 | wkCtx, err := webkit.NewWebKitContext(ctx, dataDir, cacheDir) | ||
| 34 | if err != nil { | 55 | if err != nil { | ||
| 35 | logger.Fatal().Err(err).Msg("failed to initialize WebKit context") | 56 | logger.Fatal().Err(err).Msg("failed to initialize WebKit context") | ||
M internal/infrastructure/config/defaults.go +6 -1
| @@ -181,9 +181,14 @@ func DefaultConfig() *Config { | |||
| 181 | 181 | InitialBehavior: defaultOmniboxInitialBehavior, | |
| 182 | 182 | }, | |
| 183 | 183 | Media: MediaConfig{ | |
| 184 | - | HardwareDecodingMode: HardwareDecodingAuto, // THE FIX: auto allows sw fallback | |
| 184 | + | HardwareDecodingMode: HardwareDecodingAuto, // auto allows sw fallback | |
| 185 | 185 | PreferAV1: true, // AV1 is most efficient codec | |
| 186 | 186 | ShowDiagnosticsOnStartup: true, // Warn users if HW accel unavailable | |
| 187 | + | ForceVSync: false, // Let compositor handle VSync | |
| 188 | + | GLRenderingMode: GLRenderingModeAuto, // GStreamer picks best GL API | |
| 189 | + | GStreamerDebugLevel: 0, // Disabled by default | |
| 190 | + | VideoBufferSizeMB: 64, // Larger buffer for bursty streams (Twitch, YouTube) | |
| 191 | + | QueueBufferTimeSec: 20, // More prebuffering for smooth playback | |
| 187 | 192 | }, | |
| 188 | 193 | Runtime: RuntimeConfig{ | |
| 189 | 194 | Prefix: "", | |
| @@ -181,9 +181,14 @@ func DefaultConfig() *Config { | |||||
| 181 | InitialBehavior: defaultOmniboxInitialBehavior, | 181 | InitialBehavior: defaultOmniboxInitialBehavior, | ||
| 182 | }, | 182 | }, | ||
| 183 | Media: MediaConfig{ | 183 | Media: MediaConfig{ | ||
| 184 | - | HardwareDecodingMode: HardwareDecodingAuto, // THE FIX: auto allows sw fallback | 184 | + | HardwareDecodingMode: HardwareDecodingAuto, // auto allows sw fallback |
| 185 | PreferAV1: true, // AV1 is most efficient codec | 185 | PreferAV1: true, // AV1 is most efficient codec | ||
| 186 | ShowDiagnosticsOnStartup: true, // Warn users if HW accel unavailable | 186 | ShowDiagnosticsOnStartup: true, // Warn users if HW accel unavailable | ||
| 187 | + | ForceVSync: false, // Let compositor handle VSync | |||
| 188 | + | GLRenderingMode: GLRenderingModeAuto, // GStreamer picks best GL API | |||
| 189 | + | GStreamerDebugLevel: 0, // Disabled by default | |||
| 190 | + | VideoBufferSizeMB: 64, // Larger buffer for bursty streams (Twitch, YouTube) | |||
| 191 | + | QueueBufferTimeSec: 20, // More prebuffering for smooth playback | |||
| 187 | }, | 192 | }, | ||
| 188 | Runtime: RuntimeConfig{ | 193 | Runtime: RuntimeConfig{ | ||
| 189 | Prefix: "", | 194 | Prefix: "", | ||
M internal/infrastructure/config/loader.go +18 -0
| @@ -188,6 +188,19 @@ func normalizeConfig(config *Config) { | |||
| 188 | 188 | config.Media.HardwareDecodingMode = HardwareDecodingAuto | |
| 189 | 189 | } | |
| 190 | 190 | ||
| 191 | + | switch strings.ToLower(string(config.Media.GLRenderingMode)) { | |
| 192 | + | case "", string(GLRenderingModeAuto): | |
| 193 | + | config.Media.GLRenderingMode = GLRenderingModeAuto | |
| 194 | + | case string(GLRenderingModeGLES2): | |
| 195 | + | config.Media.GLRenderingMode = GLRenderingModeGLES2 | |
| 196 | + | case string(GLRenderingModeGL3): | |
| 197 | + | config.Media.GLRenderingMode = GLRenderingModeGL3 | |
| 198 | + | case string(GLRenderingModeNone): | |
| 199 | + | config.Media.GLRenderingMode = GLRenderingModeNone | |
| 200 | + | default: | |
| 201 | + | config.Media.GLRenderingMode = GLRenderingModeAuto | |
| 202 | + | } | |
| 203 | + | ||
| 191 | 204 | config.Runtime.Prefix = strings.TrimSpace(config.Runtime.Prefix) | |
| 192 | 205 | } | |
| 193 | 206 | ||
| @@ -385,6 +398,11 @@ func (m *Manager) setMediaDefaults(defaults *Config) { | |||
| 385 | 398 | m.viper.SetDefault("media.hardware_decoding", string(defaults.Media.HardwareDecodingMode)) | |
| 386 | 399 | m.viper.SetDefault("media.prefer_av1", defaults.Media.PreferAV1) | |
| 387 | 400 | m.viper.SetDefault("media.show_diagnostics", defaults.Media.ShowDiagnosticsOnStartup) | |
| 401 | + | m.viper.SetDefault("media.force_vsync", defaults.Media.ForceVSync) | |
| 402 | + | m.viper.SetDefault("media.gl_rendering_mode", string(defaults.Media.GLRenderingMode)) | |
| 403 | + | m.viper.SetDefault("media.gstreamer_debug_level", defaults.Media.GStreamerDebugLevel) | |
| 404 | + | m.viper.SetDefault("media.video_buffer_size_mb", defaults.Media.VideoBufferSizeMB) | |
| 405 | + | m.viper.SetDefault("media.queue_buffer_time_sec", defaults.Media.QueueBufferTimeSec) | |
| 388 | 406 | } | |
| 389 | 407 | ||
| 390 | 408 | func (m *Manager) setRuntimeDefaults(defaults *Config) { | |
| @@ -188,6 +188,19 @@ func normalizeConfig(config *Config) { | |||||
| 188 | config.Media.HardwareDecodingMode = HardwareDecodingAuto | 188 | config.Media.HardwareDecodingMode = HardwareDecodingAuto | ||
| 189 | } | 189 | } | ||
| 190 | 190 | ||||
| 191 | + | switch strings.ToLower(string(config.Media.GLRenderingMode)) { | |||
| 192 | + | case "", string(GLRenderingModeAuto): | |||
| 193 | + | config.Media.GLRenderingMode = GLRenderingModeAuto | |||
| 194 | + | case string(GLRenderingModeGLES2): | |||
| 195 | + | config.Media.GLRenderingMode = GLRenderingModeGLES2 | |||
| 196 | + | case string(GLRenderingModeGL3): | |||
| 197 | + | config.Media.GLRenderingMode = GLRenderingModeGL3 | |||
| 198 | + | case string(GLRenderingModeNone): | |||
| 199 | + | config.Media.GLRenderingMode = GLRenderingModeNone | |||
| 200 | + | default: | |||
| 201 | + | config.Media.GLRenderingMode = GLRenderingModeAuto | |||
| 202 | + | } | |||
| 203 | + | ||||
| 191 | config.Runtime.Prefix = strings.TrimSpace(config.Runtime.Prefix) | 204 | config.Runtime.Prefix = strings.TrimSpace(config.Runtime.Prefix) | ||
| 192 | } | 205 | } | ||
| 193 | 206 | ||||
| @@ -385,6 +398,11 @@ func (m *Manager) setMediaDefaults(defaults *Config) { | |||||
| 385 | m.viper.SetDefault("media.hardware_decoding", string(defaults.Media.HardwareDecodingMode)) | 398 | m.viper.SetDefault("media.hardware_decoding", string(defaults.Media.HardwareDecodingMode)) | ||
| 386 | m.viper.SetDefault("media.prefer_av1", defaults.Media.PreferAV1) | 399 | m.viper.SetDefault("media.prefer_av1", defaults.Media.PreferAV1) | ||
| 387 | m.viper.SetDefault("media.show_diagnostics", defaults.Media.ShowDiagnosticsOnStartup) | 400 | m.viper.SetDefault("media.show_diagnostics", defaults.Media.ShowDiagnosticsOnStartup) | ||
| 401 | + | m.viper.SetDefault("media.force_vsync", defaults.Media.ForceVSync) | |||
| 402 | + | m.viper.SetDefault("media.gl_rendering_mode", string(defaults.Media.GLRenderingMode)) | |||
| 403 | + | m.viper.SetDefault("media.gstreamer_debug_level", defaults.Media.GStreamerDebugLevel) | |||
| 404 | + | m.viper.SetDefault("media.video_buffer_size_mb", defaults.Media.VideoBufferSizeMB) | |||
| 405 | + | m.viper.SetDefault("media.queue_buffer_time_sec", defaults.Media.QueueBufferTimeSec) | |||
| 388 | } | 406 | } | ||
| 389 | 407 | ||||
| 390 | func (m *Manager) setRuntimeDefaults(defaults *Config) { | 408 | func (m *Manager) setRuntimeDefaults(defaults *Config) { | ||
M internal/infrastructure/config/schema.go +27 -0
| @@ -50,6 +50,20 @@ const ( | |||
| 50 | 50 | HardwareDecodingDisable HardwareDecodingMode = "disable" | |
| 51 | 51 | ) | |
| 52 | 52 | ||
| 53 | + | // GLRenderingMode controls OpenGL API selection for video rendering. | |
| 54 | + | type GLRenderingMode string | |
| 55 | + | ||
| 56 | + | const ( | |
| 57 | + | // GLRenderingModeAuto lets GStreamer choose the best GL API | |
| 58 | + | GLRenderingModeAuto GLRenderingMode = "auto" | |
| 59 | + | // GLRenderingModeGLES2 forces GLES2 (better for some mobile/embedded GPUs) | |
| 60 | + | GLRenderingModeGLES2 GLRenderingMode = "gles2" | |
| 61 | + | // GLRenderingModeGL3 forces OpenGL 3.x desktop | |
| 62 | + | GLRenderingModeGL3 GLRenderingMode = "gl3" | |
| 63 | + | // GLRenderingModeNone disables GL-based rendering | |
| 64 | + | GLRenderingModeNone GLRenderingMode = "none" | |
| 65 | + | ) | |
| 66 | + | ||
| 53 | 67 | // ThemeDefault is the default theme setting (follows system). | |
| 54 | 68 | const ThemeDefault = "default" | |
| 55 | 69 | ||
| @@ -69,6 +83,19 @@ type MediaConfig struct { | |||
| 69 | 83 | PreferAV1 bool `mapstructure:"prefer_av1" yaml:"prefer_av1" toml:"prefer_av1"` | |
| 70 | 84 | // ShowDiagnosticsOnStartup shows media capability warnings at startup | |
| 71 | 85 | ShowDiagnosticsOnStartup bool `mapstructure:"show_diagnostics" yaml:"show_diagnostics" toml:"show_diagnostics"` | |
| 86 | + | // ForceVSync forces vertical sync for video playback (may help with tearing) | |
| 87 | + | ForceVSync bool `mapstructure:"force_vsync" yaml:"force_vsync" toml:"force_vsync"` | |
| 88 | + | // GLRenderingMode controls OpenGL API selection for video rendering | |
| 89 | + | // Values: "auto" (default), "gles2", "gl3", "none" | |
| 90 | + | GLRenderingMode GLRenderingMode `mapstructure:"gl_rendering_mode" yaml:"gl_rendering_mode" toml:"gl_rendering_mode"` | |
| 91 | + | // GStreamerDebugLevel sets GStreamer debug verbosity (0=off, 1-5=increasing verbosity) | |
| 92 | + | GStreamerDebugLevel int `mapstructure:"gstreamer_debug_level" yaml:"gstreamer_debug_level" toml:"gstreamer_debug_level"` | |
| 93 | + | // VideoBufferSizeMB sets the video buffer size in megabytes for smoother streaming | |
| 94 | + | // Higher values reduce rebuffering but use more memory. Default: 64 MB | |
| 95 | + | VideoBufferSizeMB int `mapstructure:"video_buffer_size_mb" yaml:"video_buffer_size_mb" toml:"video_buffer_size_mb"` | |
| 96 | + | // QueueBufferTimeSec sets the queue buffer time in seconds | |
| 97 | + | // Higher values allow more prebuffering for bursty streams. Default: 20 seconds | |
| 98 | + | QueueBufferTimeSec int `mapstructure:"queue_buffer_time_sec" yaml:"queue_buffer_time_sec" toml:"queue_buffer_time_sec"` | |
| 72 | 99 | } | |
| 73 | 100 | ||
| 74 | 101 | // RuntimeConfig holds optional runtime overrides. | |
| @@ -50,6 +50,20 @@ const ( | |||||
| 50 | HardwareDecodingDisable HardwareDecodingMode = "disable" | 50 | HardwareDecodingDisable HardwareDecodingMode = "disable" | ||
| 51 | ) | 51 | ) | ||
| 52 | 52 | ||||
| 53 | + | // GLRenderingMode controls OpenGL API selection for video rendering. | |||
| 54 | + | type GLRenderingMode string | |||
| 55 | + | ||||
| 56 | + | const ( | |||
| 57 | + | // GLRenderingModeAuto lets GStreamer choose the best GL API | |||
| 58 | + | GLRenderingModeAuto GLRenderingMode = "auto" | |||
| 59 | + | // GLRenderingModeGLES2 forces GLES2 (better for some mobile/embedded GPUs) | |||
| 60 | + | GLRenderingModeGLES2 GLRenderingMode = "gles2" | |||
| 61 | + | // GLRenderingModeGL3 forces OpenGL 3.x desktop | |||
| 62 | + | GLRenderingModeGL3 GLRenderingMode = "gl3" | |||
| 63 | + | // GLRenderingModeNone disables GL-based rendering | |||
| 64 | + | GLRenderingModeNone GLRenderingMode = "none" | |||
| 65 | + | ) | |||
| 66 | + | ||||
| 53 | // ThemeDefault is the default theme setting (follows system). | 67 | // ThemeDefault is the default theme setting (follows system). | ||
| 54 | const ThemeDefault = "default" | 68 | const ThemeDefault = "default" | ||
| 55 | 69 | ||||
| @@ -69,6 +83,19 @@ type MediaConfig struct { | |||||
| 69 | PreferAV1 bool `mapstructure:"prefer_av1" yaml:"prefer_av1" toml:"prefer_av1"` | 83 | PreferAV1 bool `mapstructure:"prefer_av1" yaml:"prefer_av1" toml:"prefer_av1"` | ||
| 70 | // ShowDiagnosticsOnStartup shows media capability warnings at startup | 84 | // ShowDiagnosticsOnStartup shows media capability warnings at startup | ||
| 71 | ShowDiagnosticsOnStartup bool `mapstructure:"show_diagnostics" yaml:"show_diagnostics" toml:"show_diagnostics"` | 85 | ShowDiagnosticsOnStartup bool `mapstructure:"show_diagnostics" yaml:"show_diagnostics" toml:"show_diagnostics"` | ||
| 86 | + | // ForceVSync forces vertical sync for video playback (may help with tearing) | |||
| 87 | + | ForceVSync bool `mapstructure:"force_vsync" yaml:"force_vsync" toml:"force_vsync"` | |||
| 88 | + | // GLRenderingMode controls OpenGL API selection for video rendering | |||
| 89 | + | // Values: "auto" (default), "gles2", "gl3", "none" | |||
| 90 | + | GLRenderingMode GLRenderingMode `mapstructure:"gl_rendering_mode" yaml:"gl_rendering_mode" toml:"gl_rendering_mode"` | |||
| 91 | + | // GStreamerDebugLevel sets GStreamer debug verbosity (0=off, 1-5=increasing verbosity) | |||
| 92 | + | GStreamerDebugLevel int `mapstructure:"gstreamer_debug_level" yaml:"gstreamer_debug_level" toml:"gstreamer_debug_level"` | |||
| 93 | + | // VideoBufferSizeMB sets the video buffer size in megabytes for smoother streaming | |||
| 94 | + | // Higher values reduce rebuffering but use more memory. Default: 64 MB | |||
| 95 | + | VideoBufferSizeMB int `mapstructure:"video_buffer_size_mb" yaml:"video_buffer_size_mb" toml:"video_buffer_size_mb"` | |||
| 96 | + | // QueueBufferTimeSec sets the queue buffer time in seconds | |||
| 97 | + | // Higher values allow more prebuffering for bursty streams. Default: 20 seconds | |||
| 98 | + | QueueBufferTimeSec int `mapstructure:"queue_buffer_time_sec" yaml:"queue_buffer_time_sec" toml:"queue_buffer_time_sec"` | |||
| 72 | } | 99 | } | ||
| 73 | 100 | ||||
| 74 | // RuntimeConfig holds optional runtime overrides. | 101 | // RuntimeConfig holds optional runtime overrides. | ||
A internal/infrastructure/media/env_manager.go +175 -0
| @@ -0,0 +1,175 @@ | |||
| 1 | + | // Package media provides video playback configuration and diagnostics. | |
| 2 | + | package media | |
| 3 | + | ||
| 4 | + | import ( | |
| 5 | + | "context" | |
| 6 | + | "fmt" | |
| 7 | + | "os" | |
| 8 | + | "path/filepath" | |
| 9 | + | "strings" | |
| 10 | + | ||
| 11 | + | "github.com/bnema/dumber/internal/application/port" | |
| 12 | + | "github.com/bnema/dumber/internal/logging" | |
| 13 | + | ) | |
| 14 | + | ||
| 15 | + | // EnvManager implements port.GStreamerEnvManager for configuring | |
| 16 | + | // GStreamer environment variables based on GPU vendor and user config. | |
| 17 | + | type EnvManager struct { | |
| 18 | + | appliedVars map[string]string | |
| 19 | + | gpuVendor port.GPUVendor | |
| 20 | + | } | |
| 21 | + | ||
| 22 | + | // NewEnvManager creates a new GStreamer environment manager. | |
| 23 | + | func NewEnvManager() *EnvManager { | |
| 24 | + | return &EnvManager{ | |
| 25 | + | appliedVars: make(map[string]string), | |
| 26 | + | gpuVendor: port.GPUVendorUnknown, | |
| 27 | + | } | |
| 28 | + | } | |
| 29 | + | ||
| 30 | + | // DetectGPUVendor identifies the primary GPU vendor from system info. | |
| 31 | + | func (e *EnvManager) DetectGPUVendor(ctx context.Context) port.GPUVendor { | |
| 32 | + | log := logging.FromContext(ctx) | |
| 33 | + | ||
| 34 | + | // Try reading from /sys/class/drm/card*/device/vendor | |
| 35 | + | vendor := e.detectFromDRM() | |
| 36 | + | if vendor != port.GPUVendorUnknown { | |
| 37 | + | e.gpuVendor = vendor | |
| 38 | + | log.Debug().Str("vendor", string(vendor)).Msg("detected GPU vendor from DRM") | |
| 39 | + | return vendor | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | log.Debug().Msg("could not detect GPU vendor, using unknown") | |
| 43 | + | e.gpuVendor = port.GPUVendorUnknown | |
| 44 | + | return port.GPUVendorUnknown | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | // detectFromDRM reads GPU vendor from /sys/class/drm/card*/device/vendor. | |
| 48 | + | func (*EnvManager) detectFromDRM() port.GPUVendor { | |
| 49 | + | const drmBase = "/sys/class/drm" | |
| 50 | + | // Check card0 first (primary GPU), then card1 if needed | |
| 51 | + | for _, card := range []string{"card0", "card1"} { | |
| 52 | + | vendorPath := filepath.Join(drmBase, card, "device", "vendor") | |
| 53 | + | data, err := os.ReadFile(vendorPath) | |
| 54 | + | if err != nil { | |
| 55 | + | continue | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | vendorID := strings.TrimSpace(string(data)) | |
| 59 | + | switch vendorID { | |
| 60 | + | case "0x1002": | |
| 61 | + | return port.GPUVendorAMD | |
| 62 | + | case "0x8086": | |
| 63 | + | return port.GPUVendorIntel | |
| 64 | + | case "0x10de": | |
| 65 | + | return port.GPUVendorNVIDIA | |
| 66 | + | } | |
| 67 | + | } | |
| 68 | + | return port.GPUVendorUnknown | |
| 69 | + | } | |
| 70 | + | ||
| 71 | + | // ApplyEnvironment sets GStreamer environment variables based on settings and GPU. | |
| 72 | + | // Must be called before GTK/WebKit initialization. | |
| 73 | + | func (e *EnvManager) ApplyEnvironment(ctx context.Context, settings port.GStreamerEnvSettings) error { | |
| 74 | + | log := logging.FromContext(ctx) | |
| 75 | + | ||
| 76 | + | // Clear previous applied vars | |
| 77 | + | e.appliedVars = make(map[string]string) | |
| 78 | + | ||
| 79 | + | // 1. Apply VSync settings | |
| 80 | + | if settings.ForceVSync { | |
| 81 | + | e.setEnv("__GL_SYNC_TO_VBLANK", "1") | |
| 82 | + | e.setEnv("vblank_mode", "3") // Mesa DRI config: always sync | |
| 83 | + | } | |
| 84 | + | ||
| 85 | + | // 2. Apply GL rendering mode | |
| 86 | + | switch strings.ToLower(settings.GLRenderingMode) { | |
| 87 | + | case "gles2": | |
| 88 | + | e.setEnv("GST_GL_API", "gles2") | |
| 89 | + | case "gl3": | |
| 90 | + | e.setEnv("GST_GL_API", "opengl3") | |
| 91 | + | case "none": | |
| 92 | + | e.setEnv("GST_GL_API", "none") | |
| 93 | + | case "auto", "": | |
| 94 | + | // Don't set - let GStreamer decide | |
| 95 | + | } | |
| 96 | + | ||
| 97 | + | // 3. GStreamer debug level | |
| 98 | + | if settings.GStreamerDebugLevel > 0 { | |
| 99 | + | level := settings.GStreamerDebugLevel | |
| 100 | + | if level > 5 { | |
| 101 | + | level = 5 // Cap at max verbosity | |
| 102 | + | } | |
| 103 | + | e.setEnv("GST_DEBUG", fmt.Sprintf("%d", level)) | |
| 104 | + | } | |
| 105 | + | ||
| 106 | + | // 4. GPU-specific optimizations (only if not already set by user) | |
| 107 | + | e.applyGPUSpecificEnv() | |
| 108 | + | ||
| 109 | + | // 5. Video buffer size (for smoother streaming) | |
| 110 | + | if settings.VideoBufferSizeMB > 0 { | |
| 111 | + | bufferBytes := settings.VideoBufferSizeMB * 1024 * 1024 | |
| 112 | + | e.setEnv("GST_BUFFER_SIZE", fmt.Sprintf("%d", bufferBytes)) | |
| 113 | + | e.setEnv("GST_QUEUE2_MAX_SIZE_BYTES", fmt.Sprintf("%d", bufferBytes)) | |
| 114 | + | } | |
| 115 | + | ||
| 116 | + | // 6. Queue buffer time (prebuffering duration) | |
| 117 | + | if settings.QueueBufferTimeSec > 0 { | |
| 118 | + | // Convert seconds to nanoseconds for GStreamer | |
| 119 | + | bufferTimeNs := settings.QueueBufferTimeSec * 1_000_000_000 | |
| 120 | + | e.setEnv("GST_QUEUE2_MAX_SIZE_TIME", fmt.Sprintf("%d", bufferTimeNs)) | |
| 121 | + | } | |
| 122 | + | ||
| 123 | + | log.Debug(). | |
| 124 | + | Interface("vars", e.appliedVars). | |
| 125 | + | Str("gpu", string(e.gpuVendor)). | |
| 126 | + | Msg("applied gstreamer environment") | |
| 127 | + | ||
| 128 | + | return nil | |
| 129 | + | } | |
| 130 | + | ||
| 131 | + | // applyGPUSpecificEnv sets GPU vendor-specific environment variables. | |
| 132 | + | func (e *EnvManager) applyGPUSpecificEnv() { | |
| 133 | + | // Only set LIBVA_DRIVER_NAME if not already set by user | |
| 134 | + | if os.Getenv("LIBVA_DRIVER_NAME") != "" { | |
| 135 | + | return | |
| 136 | + | } | |
| 137 | + | ||
| 138 | + | switch e.gpuVendor { | |
| 139 | + | case port.GPUVendorAMD: | |
| 140 | + | // AMD uses radeonsi driver (Mesa VA-API) | |
| 141 | + | e.setEnv("LIBVA_DRIVER_NAME", "radeonsi") | |
| 142 | + | ||
| 143 | + | case port.GPUVendorIntel: | |
| 144 | + | // Intel uses iHD (modern) or i965 (legacy) | |
| 145 | + | // iHD is preferred for Gen 8+ (Broadwell and newer) | |
| 146 | + | e.setEnv("LIBVA_DRIVER_NAME", "iHD") | |
| 147 | + | ||
| 148 | + | case port.GPUVendorNVIDIA: | |
| 149 | + | // NVIDIA uses nvidia-vaapi-driver | |
| 150 | + | e.setEnv("LIBVA_DRIVER_NAME", "nvidia") | |
| 151 | + | // NVIDIA benefits from EGL platform for better performance | |
| 152 | + | if os.Getenv("GST_GL_PLATFORM") == "" { | |
| 153 | + | e.setEnv("GST_GL_PLATFORM", "egl") | |
| 154 | + | } | |
| 155 | + | } | |
| 156 | + | } | |
| 157 | + | ||
| 158 | + | // setEnv sets an environment variable and tracks it. | |
| 159 | + | func (e *EnvManager) setEnv(key, value string) { | |
| 160 | + | _ = os.Setenv(key, value) // Error ignored: setenv rarely fails | |
| 161 | + | e.appliedVars[key] = value | |
| 162 | + | } | |
| 163 | + | ||
| 164 | + | // GetAppliedVars returns a map of environment variables that were set. | |
| 165 | + | func (e *EnvManager) GetAppliedVars() map[string]string { | |
| 166 | + | // Return a copy to prevent mutation | |
| 167 | + | result := make(map[string]string, len(e.appliedVars)) | |
| 168 | + | for k, v := range e.appliedVars { | |
| 169 | + | result[k] = v | |
| 170 | + | } | |
| 171 | + | return result | |
| 172 | + | } | |
| 173 | + | ||
| 174 | + | // Ensure EnvManager implements port.GStreamerEnvManager | |
| 175 | + | var _ port.GStreamerEnvManager = (*EnvManager)(nil) | |
| @@ -0,0 +1,175 @@ | |||||
| 1 | + | // Package media provides video playback configuration and diagnostics. | |||
| 2 | + | package media | |||
| 3 | + | ||||
| 4 | + | import ( | |||
| 5 | + | "context" | |||
| 6 | + | "fmt" | |||
| 7 | + | "os" | |||
| 8 | + | "path/filepath" | |||
| 9 | + | "strings" | |||
| 10 | + | ||||
| 11 | + | "github.com/bnema/dumber/internal/application/port" | |||
| 12 | + | "github.com/bnema/dumber/internal/logging" | |||
| 13 | + | ) | |||
| 14 | + | ||||
| 15 | + | // EnvManager implements port.GStreamerEnvManager for configuring | |||
| 16 | + | // GStreamer environment variables based on GPU vendor and user config. | |||
| 17 | + | type EnvManager struct { | |||
| 18 | + | appliedVars map[string]string | |||
| 19 | + | gpuVendor port.GPUVendor | |||
| 20 | + | } | |||
| 21 | + | ||||
| 22 | + | // NewEnvManager creates a new GStreamer environment manager. | |||
| 23 | + | func NewEnvManager() *EnvManager { | |||
| 24 | + | return &EnvManager{ | |||
| 25 | + | appliedVars: make(map[string]string), | |||
| 26 | + | gpuVendor: port.GPUVendorUnknown, | |||
| 27 | + | } | |||
| 28 | + | } | |||
| 29 | + | ||||
| 30 | + | // DetectGPUVendor identifies the primary GPU vendor from system info. | |||
| 31 | + | func (e *EnvManager) DetectGPUVendor(ctx context.Context) port.GPUVendor { | |||
| 32 | + | log := logging.FromContext(ctx) | |||
| 33 | + | ||||
| 34 | + | // Try reading from /sys/class/drm/card*/device/vendor | |||
| 35 | + | vendor := e.detectFromDRM() | |||
| 36 | + | if vendor != port.GPUVendorUnknown { | |||
| 37 | + | e.gpuVendor = vendor | |||
| 38 | + | log.Debug().Str("vendor", string(vendor)).Msg("detected GPU vendor from DRM") | |||
| 39 | + | return vendor | |||
| 40 | + | } | |||
| 41 | + | ||||
| 42 | + | log.Debug().Msg("could not detect GPU vendor, using unknown") | |||
| 43 | + | e.gpuVendor = port.GPUVendorUnknown | |||
| 44 | + | return port.GPUVendorUnknown | |||
| 45 | + | } | |||
| 46 | + | ||||
| 47 | + | // detectFromDRM reads GPU vendor from /sys/class/drm/card*/device/vendor. | |||
| 48 | + | func (*EnvManager) detectFromDRM() port.GPUVendor { | |||
| 49 | + | const drmBase = "/sys/class/drm" | |||
| 50 | + | // Check card0 first (primary GPU), then card1 if needed | |||
| 51 | + | for _, card := range []string{"card0", "card1"} { | |||
| 52 | + | vendorPath := filepath.Join(drmBase, card, "device", "vendor") | |||
| 53 | + | data, err := os.ReadFile(vendorPath) | |||
| 54 | + | if err != nil { | |||
| 55 | + | continue | |||
| 56 | + | } | |||
| 57 | + | ||||
| 58 | + | vendorID := strings.TrimSpace(string(data)) | |||
| 59 | + | switch vendorID { | |||
| 60 | + | case "0x1002": | |||
| 61 | + | return port.GPUVendorAMD | |||
| 62 | + | case "0x8086": | |||
| 63 | + | return port.GPUVendorIntel | |||
| 64 | + | case "0x10de": | |||
| 65 | + | return port.GPUVendorNVIDIA | |||
| 66 | + | } | |||
| 67 | + | } | |||
| 68 | + | return port.GPUVendorUnknown | |||
| 69 | + | } | |||
| 70 | + | ||||
| 71 | + | // ApplyEnvironment sets GStreamer environment variables based on settings and GPU. | |||
| 72 | + | // Must be called before GTK/WebKit initialization. | |||
| 73 | + | func (e *EnvManager) ApplyEnvironment(ctx context.Context, settings port.GStreamerEnvSettings) error { | |||
| 74 | + | log := logging.FromContext(ctx) | |||
| 75 | + | ||||
| 76 | + | // Clear previous applied vars | |||
| 77 | + | e.appliedVars = make(map[string]string) | |||
| 78 | + | ||||
| 79 | + | // 1. Apply VSync settings | |||
| 80 | + | if settings.ForceVSync { | |||
| 81 | + | e.setEnv("__GL_SYNC_TO_VBLANK", "1") | |||
| 82 | + | e.setEnv("vblank_mode", "3") // Mesa DRI config: always sync | |||
| 83 | + | } | |||
| 84 | + | ||||
| 85 | + | // 2. Apply GL rendering mode | |||
| 86 | + | switch strings.ToLower(settings.GLRenderingMode) { | |||
| 87 | + | case "gles2": | |||
| 88 | + | e.setEnv("GST_GL_API", "gles2") | |||
| 89 | + | case "gl3": | |||
| 90 | + | e.setEnv("GST_GL_API", "opengl3") | |||
| 91 | + | case "none": | |||
| 92 | + | e.setEnv("GST_GL_API", "none") | |||
| 93 | + | case "auto", "": | |||
| 94 | + | // Don't set - let GStreamer decide | |||
| 95 | + | } | |||
| 96 | + | ||||
| 97 | + | // 3. GStreamer debug level | |||
| 98 | + | if settings.GStreamerDebugLevel > 0 { | |||
| 99 | + | level := settings.GStreamerDebugLevel | |||
| 100 | + | if level > 5 { | |||
| 101 | + | level = 5 // Cap at max verbosity | |||
| 102 | + | } | |||
| 103 | + | e.setEnv("GST_DEBUG", fmt.Sprintf("%d", level)) | |||
| 104 | + | } | |||
| 105 | + | ||||
| 106 | + | // 4. GPU-specific optimizations (only if not already set by user) | |||
| 107 | + | e.applyGPUSpecificEnv() | |||
| 108 | + | ||||
| 109 | + | // 5. Video buffer size (for smoother streaming) | |||
| 110 | + | if settings.VideoBufferSizeMB > 0 { | |||
| 111 | + | bufferBytes := settings.VideoBufferSizeMB * 1024 * 1024 | |||
| 112 | + | e.setEnv("GST_BUFFER_SIZE", fmt.Sprintf("%d", bufferBytes)) | |||
| 113 | + | e.setEnv("GST_QUEUE2_MAX_SIZE_BYTES", fmt.Sprintf("%d", bufferBytes)) | |||
| 114 | + | } | |||
| 115 | + | ||||
| 116 | + | // 6. Queue buffer time (prebuffering duration) | |||
| 117 | + | if settings.QueueBufferTimeSec > 0 { | |||
| 118 | + | // Convert seconds to nanoseconds for GStreamer | |||
| 119 | + | bufferTimeNs := settings.QueueBufferTimeSec * 1_000_000_000 | |||
| 120 | + | e.setEnv("GST_QUEUE2_MAX_SIZE_TIME", fmt.Sprintf("%d", bufferTimeNs)) | |||
| 121 | + | } | |||
| 122 | + | ||||
| 123 | + | log.Debug(). | |||
| 124 | + | Interface("vars", e.appliedVars). | |||
| 125 | + | Str("gpu", string(e.gpuVendor)). | |||
| 126 | + | Msg("applied gstreamer environment") | |||
| 127 | + | ||||
| 128 | + | return nil | |||
| 129 | + | } | |||
| 130 | + | ||||
| 131 | + | // applyGPUSpecificEnv sets GPU vendor-specific environment variables. | |||
| 132 | + | func (e *EnvManager) applyGPUSpecificEnv() { | |||
| 133 | + | // Only set LIBVA_DRIVER_NAME if not already set by user | |||
| 134 | + | if os.Getenv("LIBVA_DRIVER_NAME") != "" { | |||
| 135 | + | return | |||
| 136 | + | } | |||
| 137 | + | ||||
| 138 | + | switch e.gpuVendor { | |||
| 139 | + | case port.GPUVendorAMD: | |||
| 140 | + | // AMD uses radeonsi driver (Mesa VA-API) | |||
| 141 | + | e.setEnv("LIBVA_DRIVER_NAME", "radeonsi") | |||
| 142 | + | ||||
| 143 | + | case port.GPUVendorIntel: | |||
| 144 | + | // Intel uses iHD (modern) or i965 (legacy) | |||
| 145 | + | // iHD is preferred for Gen 8+ (Broadwell and newer) | |||
| 146 | + | e.setEnv("LIBVA_DRIVER_NAME", "iHD") | |||
| 147 | + | ||||
| 148 | + | case port.GPUVendorNVIDIA: | |||
| 149 | + | // NVIDIA uses nvidia-vaapi-driver | |||
| 150 | + | e.setEnv("LIBVA_DRIVER_NAME", "nvidia") | |||
| 151 | + | // NVIDIA benefits from EGL platform for better performance | |||
| 152 | + | if os.Getenv("GST_GL_PLATFORM") == "" { | |||
| 153 | + | e.setEnv("GST_GL_PLATFORM", "egl") | |||
| 154 | + | } | |||
| 155 | + | } | |||
| 156 | + | } | |||
| 157 | + | ||||
| 158 | + | // setEnv sets an environment variable and tracks it. | |||
| 159 | + | func (e *EnvManager) setEnv(key, value string) { | |||
| 160 | + | _ = os.Setenv(key, value) // Error ignored: setenv rarely fails | |||
| 161 | + | e.appliedVars[key] = value | |||
| 162 | + | } | |||
| 163 | + | ||||
| 164 | + | // GetAppliedVars returns a map of environment variables that were set. | |||
| 165 | + | func (e *EnvManager) GetAppliedVars() map[string]string { | |||
| 166 | + | // Return a copy to prevent mutation | |||
| 167 | + | result := make(map[string]string, len(e.appliedVars)) | |||
| 168 | + | for k, v := range e.appliedVars { | |||
| 169 | + | result[k] = v | |||
| 170 | + | } | |||
| 171 | + | return result | |||
| 172 | + | } | |||
| 173 | + | ||||
| 174 | + | // Ensure EnvManager implements port.GStreamerEnvManager | |||
| 175 | + | var _ port.GStreamerEnvManager = (*EnvManager)(nil) | |||