detached 1618c29

feat(cli): add media diagnostics command and startup checks

commit
1618c29ffcc244b30d3ee94cbafd1c7b68dfe2de
author
brice · · 2025-12-18 18:27 UTC
parent
39be2ea
signature
unsigned
2 files · +152 -0
M cmd/dumber/main.go +10 -0
@@ -12,6 +12,7 @@ import (
1212 "github.com/bnema/dumber/internal/cli/cmd"
1313 "github.com/bnema/dumber/internal/infrastructure/clipboard"
1414 "github.com/bnema/dumber/internal/infrastructure/config"
15+ "github.com/bnema/dumber/internal/infrastructure/media"
1516 "github.com/bnema/dumber/internal/infrastructure/persistence/sqlite"
1617 "github.com/bnema/dumber/internal/infrastructure/webkit"
1718 "github.com/bnema/dumber/internal/logging"
@@ -91,6 +92,15 @@ func runGUI() {
9192 // Create root context with logger
9293 ctx := logging.WithContext(context.Background(), logger)
9394
95+ // Check media playback requirements (GStreamer/VA-API)
96+ mediaDiagAdapter := media.New()
97+ checkMediaUC := usecase.NewCheckMediaUseCase(mediaDiagAdapter)
98+ if _, err := checkMediaUC.Execute(ctx, usecase.CheckMediaInput{
99+ ShowDiagnostics: cfg.Media.ShowDiagnosticsOnStartup,
100+ }); err != nil {
101+ logger.Fatal().Err(err).Msg("media requirements check failed")
102+ }
103+
94104 // Resolve data/cache directories for WebKit
95105 dataDir, err := config.GetDataDir()
96106 if err != nil {
@@ -12,6 +12,7 @@ import (
12 "github.com/bnema/dumber/internal/cli/cmd"12 "github.com/bnema/dumber/internal/cli/cmd"
13 "github.com/bnema/dumber/internal/infrastructure/clipboard"13 "github.com/bnema/dumber/internal/infrastructure/clipboard"
14 "github.com/bnema/dumber/internal/infrastructure/config"14 "github.com/bnema/dumber/internal/infrastructure/config"
15+ "github.com/bnema/dumber/internal/infrastructure/media"
15 "github.com/bnema/dumber/internal/infrastructure/persistence/sqlite"16 "github.com/bnema/dumber/internal/infrastructure/persistence/sqlite"
16 "github.com/bnema/dumber/internal/infrastructure/webkit"17 "github.com/bnema/dumber/internal/infrastructure/webkit"
17 "github.com/bnema/dumber/internal/logging"18 "github.com/bnema/dumber/internal/logging"
@@ -91,6 +92,15 @@ func runGUI() {
91 // Create root context with logger92 // Create root context with logger
92 ctx := logging.WithContext(context.Background(), logger)93 ctx := logging.WithContext(context.Background(), logger)
93 94
95+ // Check media playback requirements (GStreamer/VA-API)
96+ mediaDiagAdapter := media.New()
97+ checkMediaUC := usecase.NewCheckMediaUseCase(mediaDiagAdapter)
98+ if _, err := checkMediaUC.Execute(ctx, usecase.CheckMediaInput{
99+ ShowDiagnostics: cfg.Media.ShowDiagnosticsOnStartup,
100+ }); err != nil {
101+ logger.Fatal().Err(err).Msg("media requirements check failed")
102+ }
103+
94 // Resolve data/cache directories for WebKit104 // Resolve data/cache directories for WebKit
95 dataDir, err := config.GetDataDir()105 dataDir, err := config.GetDataDir()
96 if err != nil {106 if err != nil {
A internal/cli/cmd/media.go +142 -0
@@ -0,0 +1,142 @@
1+package cmd
2+
3+import (
4+ "fmt"
5+ "strings"
6+
7+ "github.com/spf13/cobra"
8+
9+ "github.com/bnema/dumber/internal/infrastructure/media"
10+)
11+
12+var mediaCmd = &cobra.Command{
13+ Use: "media",
14+ Short: "Show media playback diagnostics",
15+ Long: `Display media playback diagnostics including GStreamer plugins and VA-API status.
16+
17+This command checks for hardware video acceleration support and reports:
18+- Available GStreamer decoder plugins (VA, VAAPI, NVCodec)
19+- Hardware video decoders (AV1, H.264, H.265, VP9)
20+- VA-API driver detection and version
21+- Warnings and recommendations
22+
23+Use this to troubleshoot video playback issues (e.g., Twitch Error #4000).
24+
25+Examples:
26+ dumber media # Show full diagnostics`,
27+ RunE: runMedia,
28+}
29+
30+func init() {
31+ rootCmd.AddCommand(mediaCmd)
32+}
33+
34+func runMedia(cmd *cobra.Command, args []string) error {
35+ app := GetApp()
36+ if app == nil {
37+ return fmt.Errorf("app not initialized")
38+ }
39+
40+ // Run diagnostics via adapter
41+ adapter := media.New()
42+ diag := adapter.RunDiagnostics(app.Ctx())
43+
44+ theme := app.Theme
45+
46+ // Header
47+ fmt.Println(theme.Highlight.Render("Media Diagnostics"))
48+ fmt.Println(theme.Subtle.Render(strings.Repeat("─", 40)))
49+ fmt.Println()
50+
51+ // GStreamer Status
52+ gstStatus := "No"
53+ gstStyle := theme.ErrorStyle
54+ if diag.GStreamerAvailable {
55+ gstStatus = "Yes"
56+ gstStyle = theme.SuccessStyle
57+ }
58+ fmt.Printf("%s %s\n", theme.Subtle.Render("GStreamer Installed:"), gstStyle.Render(gstStatus))
59+
60+ // Hardware Acceleration Status
61+ hwStatus := "No"
62+ hwStyle := theme.WarningStyle
63+ if diag.HWAccelAvailable {
64+ hwStatus = "Yes"
65+ hwStyle = theme.SuccessStyle
66+ }
67+ fmt.Printf("%s %s\n", theme.Subtle.Render("Hardware Acceleration:"), hwStyle.Render(hwStatus))
68+
69+ // AV1 Status (preferred codec)
70+ av1Status := "No"
71+ av1Style := theme.WarningStyle
72+ if diag.AV1HWAvailable {
73+ av1Status = "Yes"
74+ av1Style = theme.SuccessStyle
75+ }
76+ fmt.Printf("%s %s %s\n", theme.Subtle.Render("AV1 Hardware Decoder:"), av1Style.Render(av1Status), theme.Subtle.Render("(preferred codec)"))
77+
78+ // VA-API Status
79+ if diag.VAAPIAvailable {
80+ fmt.Printf("%s %s\n", theme.Subtle.Render("VA-API Driver:"), theme.Normal.Render(diag.VAAPIDriver))
81+ if diag.VAAPIVersion != "" {
82+ fmt.Printf("%s %s\n", theme.Subtle.Render("VA-API Version:"), theme.Normal.Render(diag.VAAPIVersion))
83+ }
84+ } else {
85+ fmt.Printf("%s %s\n", theme.Subtle.Render("VA-API:"), theme.WarningStyle.Render("Not detected"))
86+ }
87+
88+ fmt.Println()
89+
90+ // GStreamer Plugins
91+ fmt.Println(theme.Highlight.Render("GStreamer Plugins"))
92+ fmt.Println(theme.Subtle.Render(strings.Repeat("─", 40)))
93+
94+ printPluginStatus := func(name string, available bool, note string) {
95+ status := "Not found"
96+ style := theme.WarningStyle
97+ if available {
98+ status = "Available"
99+ style = theme.SuccessStyle
100+ }
101+ if note != "" {
102+ fmt.Printf(" %s %s %s\n", theme.Subtle.Render(name+":"), style.Render(status), theme.Subtle.Render(note))
103+ } else {
104+ fmt.Printf(" %s %s\n", theme.Subtle.Render(name+":"), style.Render(status))
105+ }
106+ }
107+
108+ printPluginStatus("VA (stateless)", diag.HasVAPlugin, "(modern, recommended)")
109+ printPluginStatus("VAAPI (legacy)", diag.HasVAAPIPlugin, "(gstreamer-vaapi)")
110+ printPluginStatus("NVCodec", diag.HasNVCodecPlugin, "(NVIDIA)")
111+
112+ fmt.Println()
113+
114+ // Hardware Decoders
115+ fmt.Println(theme.Highlight.Render("Hardware Decoders"))
116+ fmt.Println(theme.Subtle.Render(strings.Repeat("─", 40)))
117+
118+ printDecoders := func(codec string, decoders []string) {
119+ if len(decoders) > 0 {
120+ fmt.Printf(" %s %s\n", theme.Subtle.Render(codec+":"), theme.SuccessStyle.Render(strings.Join(decoders, ", ")))
121+ } else {
122+ fmt.Printf(" %s %s\n", theme.Subtle.Render(codec+":"), theme.WarningStyle.Render("None"))
123+ }
124+ }
125+
126+ printDecoders("AV1", diag.AV1Decoders)
127+ printDecoders("H.264", diag.H264Decoders)
128+ printDecoders("H.265", diag.H265Decoders)
129+ printDecoders("VP9", diag.VP9Decoders)
130+
131+ // Warnings
132+ if len(diag.Warnings) > 0 {
133+ fmt.Println()
134+ fmt.Println(theme.WarningStyle.Render("Warnings"))
135+ fmt.Println(theme.Subtle.Render(strings.Repeat("─", 40)))
136+ for _, w := range diag.Warnings {
137+ fmt.Printf(" %s %s\n", theme.WarningStyle.Render("!"), theme.Normal.Render(w))
138+ }
139+ }
140+
141+ return nil
142+}
@@ -0,0 +1,142 @@
1+package cmd
2+
3+import (
4+ "fmt"
5+ "strings"
6+
7+ "github.com/spf13/cobra"
8+
9+ "github.com/bnema/dumber/internal/infrastructure/media"
10+)
11+
12+var mediaCmd = &cobra.Command{
13+ Use: "media",
14+ Short: "Show media playback diagnostics",
15+ Long: `Display media playback diagnostics including GStreamer plugins and VA-API status.
16+
17+This command checks for hardware video acceleration support and reports:
18+- Available GStreamer decoder plugins (VA, VAAPI, NVCodec)
19+- Hardware video decoders (AV1, H.264, H.265, VP9)
20+- VA-API driver detection and version
21+- Warnings and recommendations
22+
23+Use this to troubleshoot video playback issues (e.g., Twitch Error #4000).
24+
25+Examples:
26+ dumber media # Show full diagnostics`,
27+ RunE: runMedia,
28+}
29+
30+func init() {
31+ rootCmd.AddCommand(mediaCmd)
32+}
33+
34+func runMedia(cmd *cobra.Command, args []string) error {
35+ app := GetApp()
36+ if app == nil {
37+ return fmt.Errorf("app not initialized")
38+ }
39+
40+ // Run diagnostics via adapter
41+ adapter := media.New()
42+ diag := adapter.RunDiagnostics(app.Ctx())
43+
44+ theme := app.Theme
45+
46+ // Header
47+ fmt.Println(theme.Highlight.Render("Media Diagnostics"))
48+ fmt.Println(theme.Subtle.Render(strings.Repeat("─", 40)))
49+ fmt.Println()
50+
51+ // GStreamer Status
52+ gstStatus := "No"
53+ gstStyle := theme.ErrorStyle
54+ if diag.GStreamerAvailable {
55+ gstStatus = "Yes"
56+ gstStyle = theme.SuccessStyle
57+ }
58+ fmt.Printf("%s %s\n", theme.Subtle.Render("GStreamer Installed:"), gstStyle.Render(gstStatus))
59+
60+ // Hardware Acceleration Status
61+ hwStatus := "No"
62+ hwStyle := theme.WarningStyle
63+ if diag.HWAccelAvailable {
64+ hwStatus = "Yes"
65+ hwStyle = theme.SuccessStyle
66+ }
67+ fmt.Printf("%s %s\n", theme.Subtle.Render("Hardware Acceleration:"), hwStyle.Render(hwStatus))
68+
69+ // AV1 Status (preferred codec)
70+ av1Status := "No"
71+ av1Style := theme.WarningStyle
72+ if diag.AV1HWAvailable {
73+ av1Status = "Yes"
74+ av1Style = theme.SuccessStyle
75+ }
76+ fmt.Printf("%s %s %s\n", theme.Subtle.Render("AV1 Hardware Decoder:"), av1Style.Render(av1Status), theme.Subtle.Render("(preferred codec)"))
77+
78+ // VA-API Status
79+ if diag.VAAPIAvailable {
80+ fmt.Printf("%s %s\n", theme.Subtle.Render("VA-API Driver:"), theme.Normal.Render(diag.VAAPIDriver))
81+ if diag.VAAPIVersion != "" {
82+ fmt.Printf("%s %s\n", theme.Subtle.Render("VA-API Version:"), theme.Normal.Render(diag.VAAPIVersion))
83+ }
84+ } else {
85+ fmt.Printf("%s %s\n", theme.Subtle.Render("VA-API:"), theme.WarningStyle.Render("Not detected"))
86+ }
87+
88+ fmt.Println()
89+
90+ // GStreamer Plugins
91+ fmt.Println(theme.Highlight.Render("GStreamer Plugins"))
92+ fmt.Println(theme.Subtle.Render(strings.Repeat("─", 40)))
93+
94+ printPluginStatus := func(name string, available bool, note string) {
95+ status := "Not found"
96+ style := theme.WarningStyle
97+ if available {
98+ status = "Available"
99+ style = theme.SuccessStyle
100+ }
101+ if note != "" {
102+ fmt.Printf(" %s %s %s\n", theme.Subtle.Render(name+":"), style.Render(status), theme.Subtle.Render(note))
103+ } else {
104+ fmt.Printf(" %s %s\n", theme.Subtle.Render(name+":"), style.Render(status))
105+ }
106+ }
107+
108+ printPluginStatus("VA (stateless)", diag.HasVAPlugin, "(modern, recommended)")
109+ printPluginStatus("VAAPI (legacy)", diag.HasVAAPIPlugin, "(gstreamer-vaapi)")
110+ printPluginStatus("NVCodec", diag.HasNVCodecPlugin, "(NVIDIA)")
111+
112+ fmt.Println()
113+
114+ // Hardware Decoders
115+ fmt.Println(theme.Highlight.Render("Hardware Decoders"))
116+ fmt.Println(theme.Subtle.Render(strings.Repeat("─", 40)))
117+
118+ printDecoders := func(codec string, decoders []string) {
119+ if len(decoders) > 0 {
120+ fmt.Printf(" %s %s\n", theme.Subtle.Render(codec+":"), theme.SuccessStyle.Render(strings.Join(decoders, ", ")))
121+ } else {
122+ fmt.Printf(" %s %s\n", theme.Subtle.Render(codec+":"), theme.WarningStyle.Render("None"))
123+ }
124+ }
125+
126+ printDecoders("AV1", diag.AV1Decoders)
127+ printDecoders("H.264", diag.H264Decoders)
128+ printDecoders("H.265", diag.H265Decoders)
129+ printDecoders("VP9", diag.VP9Decoders)
130+
131+ // Warnings
132+ if len(diag.Warnings) > 0 {
133+ fmt.Println()
134+ fmt.Println(theme.WarningStyle.Render("Warnings"))
135+ fmt.Println(theme.Subtle.Render(strings.Repeat("─", 40)))
136+ for _, w := range diag.Warnings {
137+ fmt.Printf(" %s %s\n", theme.WarningStyle.Render("!"), theme.Normal.Render(w))
138+ }
139+ }
140+
141+ return nil
142+}

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