detached ab84ac9

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"]
274274 | `media.hardware_decoding` | string | `"auto"` | `auto`, `force`, `disable` | Hardware video decoding mode |
275275 | `media.prefer_av1` | bool | `true` | - | Prefer AV1 codec when available |
276276 | `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 |
277282
278283 **Hardware decoding modes:**
279284 - `auto` (recommended): Hardware preferred with software fallback - fixes Twitch Error #4000
280285 - `force`: Hardware only - fails if unavailable
281286 - `disable`: Software only - higher CPU usage
282287
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+
283304 **Example:**
284305 ```toml
285306 [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
289315 ```
290316
291317 **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 #4000284 - `auto` (recommended): Hardware preferred with software fallback - fixes Twitch Error #4000
280 - `force`: Hardware only - fails if unavailable285 - `force`: Hardware only - fails if unavailable
281 - `disable`: Software only - higher CPU usage286 - `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 ```toml305 ```toml
285 [media]306 [media]
286-hardware_decoding = "auto" # HW preferred, SW fallback307+hardware_decoding = "auto" # HW preferred, SW fallback
287-prefer_av1 = true # AV1 is most efficient codec308+prefer_av1 = true # AV1 is most efficient codec
288-show_diagnostics = true # Log warnings if HW accel unavailable309+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 (
55 "path/filepath"
66
77 "github.com/bnema/dumber/assets"
8+ "github.com/bnema/dumber/internal/application/port"
89 "github.com/bnema/dumber/internal/application/usecase"
910 "github.com/bnema/dumber/internal/infrastructure/config"
1011 "github.com/bnema/dumber/internal/infrastructure/filtering"
12+ "github.com/bnema/dumber/internal/infrastructure/media"
1113 "github.com/bnema/dumber/internal/infrastructure/webkit"
1214 "github.com/bnema/dumber/internal/ui/theme"
1315 "github.com/rs/zerolog"
@@ -30,6 +32,25 @@ func BuildWebKitStack(
3032 themeManager *theme.Manager,
3133 logger zerolog.Logger,
3234 ) 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+
3354 wkCtx, err := webkit.NewWebKitContext(ctx, dataDir, cacheDir)
3455 if err != nil {
3556 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 {
181181 InitialBehavior: defaultOmniboxInitialBehavior,
182182 },
183183 Media: MediaConfig{
184- HardwareDecodingMode: HardwareDecodingAuto, // THE FIX: auto allows sw fallback
184+ HardwareDecodingMode: HardwareDecodingAuto, // auto allows sw fallback
185185 PreferAV1: true, // AV1 is most efficient codec
186186 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
187192 },
188193 Runtime: RuntimeConfig{
189194 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 fallback184+ HardwareDecodingMode: HardwareDecodingAuto, // auto allows sw fallback
185 PreferAV1: true, // AV1 is most efficient codec185 PreferAV1: true, // AV1 is most efficient codec
186 ShowDiagnosticsOnStartup: true, // Warn users if HW accel unavailable186 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) {
188188 config.Media.HardwareDecodingMode = HardwareDecodingAuto
189189 }
190190
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+
191204 config.Runtime.Prefix = strings.TrimSpace(config.Runtime.Prefix)
192205 }
193206
@@ -385,6 +398,11 @@ func (m *Manager) setMediaDefaults(defaults *Config) {
385398 m.viper.SetDefault("media.hardware_decoding", string(defaults.Media.HardwareDecodingMode))
386399 m.viper.SetDefault("media.prefer_av1", defaults.Media.PreferAV1)
387400 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)
388406 }
389407
390408 func (m *Manager) setRuntimeDefaults(defaults *Config) {
@@ -188,6 +188,19 @@ func normalizeConfig(config *Config) {
188 config.Media.HardwareDecodingMode = HardwareDecodingAuto188 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 (
5050 HardwareDecodingDisable HardwareDecodingMode = "disable"
5151 )
5252
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+
5367 // ThemeDefault is the default theme setting (follows system).
5468 const ThemeDefault = "default"
5569
@@ -69,6 +83,19 @@ type MediaConfig struct {
6983 PreferAV1 bool `mapstructure:"prefer_av1" yaml:"prefer_av1" toml:"prefer_av1"`
7084 // ShowDiagnosticsOnStartup shows media capability warnings at startup
7185 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"`
7299 }
73100
74101 // 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 startup84 // 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)

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close