detached 39be2ea

feat(media): add MediaDiagnostics port and CheckMediaUseCase

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

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