v0.30.0 eb3068f

diagnostics.go RawHistoryFolder

1// Package media provides video playback diagnostics and hardware acceleration detection.
2package media
3
4import (
5	"context"
6	"os"
7	"os/exec"
8	"strings"
9
10	"github.com/bnema/dumber/internal/application/port"
11	"github.com/bnema/dumber/internal/logging"
12)
13
14// Adapter implements port.MediaDiagnostics using system tools.
15type Adapter struct{}
16
17// New creates a new media diagnostics adapter.
18func New() port.MediaDiagnostics {
19	return &Adapter{}
20}
21
22// RunDiagnostics checks GStreamer plugins and VA-API availability.
23func (a *Adapter) RunDiagnostics(ctx context.Context) *port.MediaDiagnosticsResult {
24	log := logging.FromContext(ctx)
25	result := &port.MediaDiagnosticsResult{}
26
27	// Check GStreamer plugins via gst-inspect-1.0
28	a.checkGStreamerPlugins(ctx, result)
29
30	// Check VA-API via vainfo
31	a.checkVAAPI(ctx, result)
32
33	// Determine overall hardware accel availability
34	result.HWAccelAvailable = result.HasVAPlugin || result.HasVAAPIPlugin || result.HasNVCodecPlugin
35	result.AV1HWAvailable = len(result.AV1Decoders) > 0
36
37	// Generate user-friendly warnings
38	a.generateWarnings(result)
39
40	log.Info().
41		Bool("hw_accel", result.HWAccelAvailable).
42		Bool("av1_hw", result.AV1HWAvailable).
43		Bool("va_plugin", result.HasVAPlugin).
44		Bool("vaapi_plugin", result.HasVAAPIPlugin).
45		Bool("nvcodec_plugin", result.HasNVCodecPlugin).
46		Str("vaapi_driver", result.VAAPIDriver).
47		Msg("media diagnostics complete")
48
49	return result
50}
51
52// checkGStreamerPlugins detects available GStreamer decoder plugins.
53func (a *Adapter) checkGStreamerPlugins(ctx context.Context, r *port.MediaDiagnosticsResult) {
54	log := logging.FromContext(ctx)
55
56	// Check if gst-inspect-1.0 is available
57	gstInspect, err := exec.LookPath("gst-inspect-1.0")
58	if err != nil {
59		log.Warn().Msg("gst-inspect-1.0 not found - GStreamer not installed, video playback will fail")
60		r.GStreamerAvailable = false
61		return
62	}
63	r.GStreamerAvailable = true
64
65	// Check 'va' plugin (modern stateless decoders)
66	// Package: gst-plugin-va (Arch) or gstreamer1.0-plugins-bad (Debian/Ubuntu)
67	if out, err := exec.CommandContext(ctx, gstInspect, "va").Output(); err == nil {
68		r.HasVAPlugin = true
69		a.parseVADecoders(string(out), r)
70	}
71
72	// Check 'vaapi' plugin (legacy gstreamer-vaapi)
73	if out, err := exec.CommandContext(ctx, gstInspect, "vaapi").Output(); err == nil {
74		r.HasVAAPIPlugin = true
75		a.parseVAAPIDecoders(string(out), r)
76	}
77
78	// Check 'nvcodec' plugin (NVIDIA)
79	if out, err := exec.CommandContext(ctx, gstInspect, "nvcodec").Output(); err == nil {
80		r.HasNVCodecPlugin = true
81		a.parseNVCodecDecoders(string(out), r)
82	}
83
84	log.Debug().
85		Bool("va", r.HasVAPlugin).
86		Bool("vaapi", r.HasVAAPIPlugin).
87		Bool("nvcodec", r.HasNVCodecPlugin).
88		Msg("gstreamer plugins checked")
89}
90
91// parseVADecoders extracts decoder names from gst-inspect-1.0 va output.
92func (a *Adapter) parseVADecoders(output string, r *port.MediaDiagnosticsResult) {
93	if a == nil {
94		return
95	}
96	lines := strings.Split(output, "\n")
97	for _, line := range lines {
98		line = strings.TrimSpace(line)
99		// Format: "  vah264dec: VA-API H.264 Decoder"
100		if strings.HasPrefix(line, "vaav1dec") {
101			r.AV1Decoders = append(r.AV1Decoders, "vaav1dec")
102		}
103		if strings.HasPrefix(line, "vah264dec") {
104			r.H264Decoders = append(r.H264Decoders, "vah264dec")
105		}
106		if strings.HasPrefix(line, "vah265dec") {
107			r.H265Decoders = append(r.H265Decoders, "vah265dec")
108		}
109		if strings.HasPrefix(line, "vavp9dec") {
110			r.VP9Decoders = append(r.VP9Decoders, "vavp9dec")
111		}
112	}
113}
114
115// parseVAAPIDecoders extracts decoder names from gst-inspect-1.0 vaapi output.
116func (a *Adapter) parseVAAPIDecoders(output string, r *port.MediaDiagnosticsResult) {
117	if a == nil {
118		return
119	}
120	lines := strings.Split(output, "\n")
121	for _, line := range lines {
122		line = strings.TrimSpace(line)
123		// Format: "  vaapih264dec: VA-API H264 decoder"
124		if strings.HasPrefix(line, "vaapiav1dec") {
125			r.AV1Decoders = append(r.AV1Decoders, "vaapiav1dec")
126		}
127		if strings.HasPrefix(line, "vaapih264dec") {
128			r.H264Decoders = append(r.H264Decoders, "vaapih264dec")
129		}
130		if strings.HasPrefix(line, "vaapih265dec") {
131			r.H265Decoders = append(r.H265Decoders, "vaapih265dec")
132		}
133		if strings.HasPrefix(line, "vaapivp9dec") {
134			r.VP9Decoders = append(r.VP9Decoders, "vaapivp9dec")
135		}
136	}
137}
138
139// parseNVCodecDecoders extracts decoder names from gst-inspect-1.0 nvcodec output.
140func (a *Adapter) parseNVCodecDecoders(output string, r *port.MediaDiagnosticsResult) {
141	if a == nil {
142		return
143	}
144	lines := strings.Split(output, "\n")
145	for _, line := range lines {
146		line = strings.TrimSpace(line)
147		// Format: "  nvav1dec: NVDEC AV1 Decoder"
148		if strings.HasPrefix(line, "nvav1dec") {
149			r.AV1Decoders = append(r.AV1Decoders, "nvav1dec")
150		}
151		if strings.HasPrefix(line, "nvh264dec") {
152			r.H264Decoders = append(r.H264Decoders, "nvh264dec")
153		}
154		if strings.HasPrefix(line, "nvh265dec") {
155			r.H265Decoders = append(r.H265Decoders, "nvh265dec")
156		}
157		if strings.HasPrefix(line, "nvvp9dec") {
158			r.VP9Decoders = append(r.VP9Decoders, "nvvp9dec")
159		}
160	}
161}
162
163// checkVAAPI detects VA-API driver and version.
164func (a *Adapter) checkVAAPI(ctx context.Context, r *port.MediaDiagnosticsResult) {
165	log := logging.FromContext(ctx)
166	if a == nil {
167		return
168	}
169
170	// Check LIBVA_DRIVER_NAME environment first
171	if driver := os.Getenv("LIBVA_DRIVER_NAME"); driver != "" {
172		r.VAAPIDriver = driver
173	}
174
175	// Run vainfo for detailed information
176	vainfo, err := exec.LookPath("vainfo")
177	if err != nil {
178		log.Debug().Msg("vainfo not found, skipping VA-API driver detection")
179		return
180	}
181
182	out, err := exec.CommandContext(ctx, vainfo).CombinedOutput()
183	if err != nil {
184		log.Debug().Err(err).Msg("vainfo failed")
185		return
186	}
187
188	output := string(out)
189	r.VAAPIAvailable = true
190
191	// Parse driver name and version from vainfo output
192	for _, line := range strings.Split(output, "\n") {
193		line = strings.TrimSpace(line)
194
195		// Extract driver from "vainfo: Driver version: ..."
196		if strings.Contains(line, "Driver version:") {
197			lowerLine := strings.ToLower(line)
198			if strings.Contains(lowerLine, "radeonsi") || strings.Contains(lowerLine, "radeon") {
199				r.VAAPIDriver = "radeonsi"
200			} else if strings.Contains(lowerLine, "i965") {
201				r.VAAPIDriver = "i965"
202			} else if strings.Contains(lowerLine, "ihd") || strings.Contains(lowerLine, "intel") {
203				r.VAAPIDriver = "iHD"
204			} else if strings.Contains(lowerLine, "nvidia") {
205				r.VAAPIDriver = "nvidia"
206			}
207		}
208
209		// Extract VA-API version from "vainfo: VA-API version: X.XX"
210		if strings.Contains(line, "VA-API version:") {
211			parts := strings.Split(line, ":")
212			if len(parts) >= 2 {
213				r.VAAPIVersion = strings.TrimSpace(parts[len(parts)-1])
214			}
215		}
216	}
217
218	log.Debug().
219		Bool("available", r.VAAPIAvailable).
220		Str("driver", r.VAAPIDriver).
221		Str("version", r.VAAPIVersion).
222		Msg("vaapi status")
223}
224
225// generateWarnings creates user-friendly warning messages.
226//
227//nolint:revive // receiver required for interface consistency
228func (a *Adapter) generateWarnings(r *port.MediaDiagnosticsResult) {
229	// Critical: GStreamer not installed
230	if !r.GStreamerAvailable {
231		r.Warnings = append(r.Warnings,
232			"CRITICAL: GStreamer not installed. Video playback will not work!",
233			"Install: gst-plugins-base gst-plugins-good gst-plugins-bad gst-plugins-ugly gst-libav",
234		)
235		return // No point checking other things
236	}
237
238	if !r.HWAccelAvailable {
239		r.Warnings = append(r.Warnings,
240			"No hardware video acceleration detected. Video will use software decoding (higher CPU).",
241			"Install VA plugin: gst-plugin-va (Arch) or gstreamer1.0-plugins-bad (Debian/Ubuntu)",
242			"Install VA-API driver: libva-mesa-driver (AMD) | intel-media-driver (Intel) | nvidia-vaapi-driver (NVIDIA)",
243		)
244	}
245
246	// AV1 is the preferred codec - warn if not available
247	if !r.AV1HWAvailable && r.HWAccelAvailable {
248		r.Warnings = append(r.Warnings,
249			"AV1 hardware decoder not found. AV1 streams will use software decoding.")
250	}
251
252	// Check for essential codecs for Twitch
253	if len(r.H264Decoders) == 0 && len(r.VP9Decoders) == 0 {
254		r.Warnings = append(r.Warnings,
255			"No H.264 or VP9 hardware decoders found. Twitch/YouTube may have degraded performance.")
256	}
257
258	// Prefer modern VA plugin over legacy VAAPI
259	if r.HasVAAPIPlugin && !r.HasVAPlugin {
260		r.Warnings = append(r.Warnings,
261			"Using legacy gstreamer-vaapi. Install gst-plugin-va (Arch) or "+
262				"gstreamer1.0-plugins-bad (Debian/Ubuntu) for modern VA stateless decoders.")
263	}
264}

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