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.SplitSeq(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.SplitSeq(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.SplitSeq(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.SplitSeq(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}