detached 2e65b39

feat: generate PipeWire cleanup bindings

commit
2e65b39bd7e113a5e763a714617109bfb79908be
author
brice · · 2026-04-10 03:23 UTC
parent
ef1527c
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
12 files · +470 -3
M cmd/pipewiregen/internal/emitter/emitter_test.go +111 -0
@@ -1,10 +1,12 @@
11 package emitter
22
33 import (
4+ "path/filepath"
45 "strings"
56 "testing"
67
78 "github.com/bnema/purego-pipewire/cmd/pipewiregen/internal/model"
9+ "github.com/bnema/purego-pipewire/cmd/pipewiregen/internal/parser"
810 )
911
1012 func testModel() *model.Model {
@@ -98,3 +100,112 @@ func TestEmitWritesPlayerStreamFiles(t *testing.T) {
98100 t.Fatalf("port output missing StreamPlaybackAPI interface")
99101 }
100102 }
103+
104+// testCleanupModel returns a model with cleanup symbols that should be generated.
105+func testCleanupModel() *model.Model {
106+ return &model.Model{
107+ Libraries: []model.Library{
108+ {Name: "pipewire", SOName: "libpipewire-0.3.so.0"},
109+ },
110+ Groups: []model.Group{
111+ {
112+ Name: "loop",
113+ Interface: "LoopAPI",
114+ Package: "out",
115+ Symbols: []string{
116+ "pw_main_loop_new",
117+ "pw_main_loop_destroy",
118+ "pw_main_loop_run",
119+ "pw_main_loop_quit",
120+ },
121+ },
122+ {
123+ Name: "stream_playback",
124+ Interface: "StreamPlaybackAPI",
125+ Package: "out",
126+ Symbols: []string{
127+ "pw_stream_new_simple",
128+ "pw_stream_connect",
129+ "pw_stream_set_active",
130+ "pw_stream_disconnect",
131+ "pw_stream_dequeue_buffer",
132+ "pw_stream_queue_buffer",
133+ "pw_stream_add_listener",
134+ "pw_stream_destroy",
135+ },
136+ },
137+ },
138+ Symbols: []model.Symbol{
139+ // Loop symbols
140+ {Name: "pw_main_loop_new", Library: "pipewire", Group: "loop", Signature: "func(props unsafe.Pointer) unsafe.Pointer"},
141+ {Name: "pw_main_loop_destroy", Library: "pipewire", Group: "loop", Signature: "func(loop unsafe.Pointer)"},
142+ {Name: "pw_main_loop_run", Library: "pipewire", Group: "loop", Signature: "func(loop unsafe.Pointer) int32"},
143+ {Name: "pw_main_loop_quit", Library: "pipewire", Group: "loop", Signature: "func(loop unsafe.Pointer) int32"},
144+ // Stream playback symbols
145+ {Name: "pw_stream_new_simple", Library: "pipewire", Group: "stream_playback", Signature: "func(context unsafe.Pointer, name *byte, props unsafe.Pointer, events unsafe.Pointer, data unsafe.Pointer) unsafe.Pointer"},
146+ {Name: "pw_stream_connect", Library: "pipewire", Group: "stream_playback", Signature: "func(stream unsafe.Pointer, direction int32, id uint32, flags uint32, ports unsafe.Pointer, n_ports uint32) int32"},
147+ {Name: "pw_stream_set_active", Library: "pipewire", Group: "stream_playback", Signature: "func(stream unsafe.Pointer, active bool) int32"},
148+ {Name: "pw_stream_disconnect", Library: "pipewire", Group: "stream_playback", Signature: "func(stream unsafe.Pointer) int32"},
149+ {Name: "pw_stream_dequeue_buffer", Library: "pipewire", Group: "stream_playback", Signature: "func(stream unsafe.Pointer) unsafe.Pointer"},
150+ {Name: "pw_stream_queue_buffer", Library: "pipewire", Group: "stream_playback", Signature: "func(stream unsafe.Pointer, buffer unsafe.Pointer) int32"},
151+ {Name: "pw_stream_add_listener", Library: "pipewire", Group: "stream_playback", Signature: "func(stream unsafe.Pointer, listener unsafe.Pointer, events unsafe.Pointer, data unsafe.Pointer) int32"},
152+ {Name: "pw_stream_destroy", Library: "pipewire", Group: "stream_playback", Signature: "func(stream unsafe.Pointer)"},
153+ },
154+ }
155+}
156+
157+// TestEmitGeneratesCleanupBindings verifies that cleanup bindings are generated.
158+// This test ensures pw_main_loop_quit and pw_stream_destroy are present in generated files.
159+func TestEmitGeneratesCleanupBindings(t *testing.T) {
160+ // Load the actual pipewire.json from the project root
161+ root, err := filepath.Abs("../../../..")
162+ if err != nil {
163+ t.Fatalf("failed to get project root: %v", err)
164+ }
165+
166+ actualModel, err := parser.Load(filepath.Join(root, "gen", "pipewire.json"))
167+ if err != nil {
168+ t.Fatalf("failed to load pipewire.json: %v", err)
169+ }
170+
171+ paths, err := Emit(actualModel, t.TempDir())
172+ if err != nil {
173+ t.Fatalf("Emit returned error: %v", err)
174+ }
175+
176+ // Verify loop_gen.go contains pw_main_loop_quit
177+ loopCapiContent := string(paths["internal/capi/loop_gen.go"])
178+ if !strings.Contains(loopCapiContent, "pw_main_loop_quit") {
179+ t.Fatalf("capi loop output missing pw_main_loop_quit binding")
180+ }
181+ if !strings.Contains(loopCapiContent, "pw_main_loop_quitFunc") {
182+ t.Fatalf("capi loop output missing pw_main_loop_quitFunc type")
183+ }
184+ if !strings.Contains(loopCapiContent, "purego.RegisterLibFunc(&pw_main_loop_quit") {
185+ t.Fatalf("capi loop output missing pw_main_loop_quit registration")
186+ }
187+
188+ // Verify loop port interface contains PWMainLoopQuit
189+ loopPortContent := string(paths["internal/ports/out/loop_gen.go"])
190+ if !strings.Contains(loopPortContent, "PWMainLoopQuit(loop unsafe.Pointer) int32") {
191+ t.Fatalf("port loop output missing PWMainLoopQuit method")
192+ }
193+
194+ // Verify stream_playback_gen.go contains pw_stream_destroy
195+ streamCapiContent := string(paths["internal/capi/stream_playback_gen.go"])
196+ if !strings.Contains(streamCapiContent, "pw_stream_destroy") {
197+ t.Fatalf("capi stream_playback output missing pw_stream_destroy binding")
198+ }
199+ if !strings.Contains(streamCapiContent, "pw_stream_destroyFunc") {
200+ t.Fatalf("capi stream_playback output missing pw_stream_destroyFunc type")
201+ }
202+ if !strings.Contains(streamCapiContent, "purego.RegisterLibFunc(&pw_stream_destroy") {
203+ t.Fatalf("capi stream_playback output missing pw_stream_destroy registration")
204+ }
205+
206+ // Verify stream_playback port interface contains PWStreamDestroy
207+ streamPortContent := string(paths["internal/ports/out/stream_playback_gen.go"])
208+ if !strings.Contains(streamPortContent, "PWStreamDestroy(stream unsafe.Pointer)") {
209+ t.Fatalf("port stream_playback output missing PWStreamDestroy method")
210+ }
211+}
@@ -1,10 +1,12 @@
1 package emitter1 package emitter
2 2
3 import (3 import (
4+ "path/filepath"
4 "strings"5 "strings"
5 "testing"6 "testing"
6 7
7 "github.com/bnema/purego-pipewire/cmd/pipewiregen/internal/model"8 "github.com/bnema/purego-pipewire/cmd/pipewiregen/internal/model"
9+ "github.com/bnema/purego-pipewire/cmd/pipewiregen/internal/parser"
8 )10 )
9 11
10 func testModel() *model.Model {12 func testModel() *model.Model {
@@ -98,3 +100,112 @@ func TestEmitWritesPlayerStreamFiles(t *testing.T) {
98 t.Fatalf("port output missing StreamPlaybackAPI interface")100 t.Fatalf("port output missing StreamPlaybackAPI interface")
99 }101 }
100 }102 }
103+
104+// testCleanupModel returns a model with cleanup symbols that should be generated.
105+func testCleanupModel() *model.Model {
106+ return &model.Model{
107+ Libraries: []model.Library{
108+ {Name: "pipewire", SOName: "libpipewire-0.3.so.0"},
109+ },
110+ Groups: []model.Group{
111+ {
112+ Name: "loop",
113+ Interface: "LoopAPI",
114+ Package: "out",
115+ Symbols: []string{
116+ "pw_main_loop_new",
117+ "pw_main_loop_destroy",
118+ "pw_main_loop_run",
119+ "pw_main_loop_quit",
120+ },
121+ },
122+ {
123+ Name: "stream_playback",
124+ Interface: "StreamPlaybackAPI",
125+ Package: "out",
126+ Symbols: []string{
127+ "pw_stream_new_simple",
128+ "pw_stream_connect",
129+ "pw_stream_set_active",
130+ "pw_stream_disconnect",
131+ "pw_stream_dequeue_buffer",
132+ "pw_stream_queue_buffer",
133+ "pw_stream_add_listener",
134+ "pw_stream_destroy",
135+ },
136+ },
137+ },
138+ Symbols: []model.Symbol{
139+ // Loop symbols
140+ {Name: "pw_main_loop_new", Library: "pipewire", Group: "loop", Signature: "func(props unsafe.Pointer) unsafe.Pointer"},
141+ {Name: "pw_main_loop_destroy", Library: "pipewire", Group: "loop", Signature: "func(loop unsafe.Pointer)"},
142+ {Name: "pw_main_loop_run", Library: "pipewire", Group: "loop", Signature: "func(loop unsafe.Pointer) int32"},
143+ {Name: "pw_main_loop_quit", Library: "pipewire", Group: "loop", Signature: "func(loop unsafe.Pointer) int32"},
144+ // Stream playback symbols
145+ {Name: "pw_stream_new_simple", Library: "pipewire", Group: "stream_playback", Signature: "func(context unsafe.Pointer, name *byte, props unsafe.Pointer, events unsafe.Pointer, data unsafe.Pointer) unsafe.Pointer"},
146+ {Name: "pw_stream_connect", Library: "pipewire", Group: "stream_playback", Signature: "func(stream unsafe.Pointer, direction int32, id uint32, flags uint32, ports unsafe.Pointer, n_ports uint32) int32"},
147+ {Name: "pw_stream_set_active", Library: "pipewire", Group: "stream_playback", Signature: "func(stream unsafe.Pointer, active bool) int32"},
148+ {Name: "pw_stream_disconnect", Library: "pipewire", Group: "stream_playback", Signature: "func(stream unsafe.Pointer) int32"},
149+ {Name: "pw_stream_dequeue_buffer", Library: "pipewire", Group: "stream_playback", Signature: "func(stream unsafe.Pointer) unsafe.Pointer"},
150+ {Name: "pw_stream_queue_buffer", Library: "pipewire", Group: "stream_playback", Signature: "func(stream unsafe.Pointer, buffer unsafe.Pointer) int32"},
151+ {Name: "pw_stream_add_listener", Library: "pipewire", Group: "stream_playback", Signature: "func(stream unsafe.Pointer, listener unsafe.Pointer, events unsafe.Pointer, data unsafe.Pointer) int32"},
152+ {Name: "pw_stream_destroy", Library: "pipewire", Group: "stream_playback", Signature: "func(stream unsafe.Pointer)"},
153+ },
154+ }
155+}
156+
157+// TestEmitGeneratesCleanupBindings verifies that cleanup bindings are generated.
158+// This test ensures pw_main_loop_quit and pw_stream_destroy are present in generated files.
159+func TestEmitGeneratesCleanupBindings(t *testing.T) {
160+ // Load the actual pipewire.json from the project root
161+ root, err := filepath.Abs("../../../..")
162+ if err != nil {
163+ t.Fatalf("failed to get project root: %v", err)
164+ }
165+
166+ actualModel, err := parser.Load(filepath.Join(root, "gen", "pipewire.json"))
167+ if err != nil {
168+ t.Fatalf("failed to load pipewire.json: %v", err)
169+ }
170+
171+ paths, err := Emit(actualModel, t.TempDir())
172+ if err != nil {
173+ t.Fatalf("Emit returned error: %v", err)
174+ }
175+
176+ // Verify loop_gen.go contains pw_main_loop_quit
177+ loopCapiContent := string(paths["internal/capi/loop_gen.go"])
178+ if !strings.Contains(loopCapiContent, "pw_main_loop_quit") {
179+ t.Fatalf("capi loop output missing pw_main_loop_quit binding")
180+ }
181+ if !strings.Contains(loopCapiContent, "pw_main_loop_quitFunc") {
182+ t.Fatalf("capi loop output missing pw_main_loop_quitFunc type")
183+ }
184+ if !strings.Contains(loopCapiContent, "purego.RegisterLibFunc(&pw_main_loop_quit") {
185+ t.Fatalf("capi loop output missing pw_main_loop_quit registration")
186+ }
187+
188+ // Verify loop port interface contains PWMainLoopQuit
189+ loopPortContent := string(paths["internal/ports/out/loop_gen.go"])
190+ if !strings.Contains(loopPortContent, "PWMainLoopQuit(loop unsafe.Pointer) int32") {
191+ t.Fatalf("port loop output missing PWMainLoopQuit method")
192+ }
193+
194+ // Verify stream_playback_gen.go contains pw_stream_destroy
195+ streamCapiContent := string(paths["internal/capi/stream_playback_gen.go"])
196+ if !strings.Contains(streamCapiContent, "pw_stream_destroy") {
197+ t.Fatalf("capi stream_playback output missing pw_stream_destroy binding")
198+ }
199+ if !strings.Contains(streamCapiContent, "pw_stream_destroyFunc") {
200+ t.Fatalf("capi stream_playback output missing pw_stream_destroyFunc type")
201+ }
202+ if !strings.Contains(streamCapiContent, "purego.RegisterLibFunc(&pw_stream_destroy") {
203+ t.Fatalf("capi stream_playback output missing pw_stream_destroy registration")
204+ }
205+
206+ // Verify stream_playback port interface contains PWStreamDestroy
207+ streamPortContent := string(paths["internal/ports/out/stream_playback_gen.go"])
208+ if !strings.Contains(streamPortContent, "PWStreamDestroy(stream unsafe.Pointer)") {
209+ t.Fatalf("port stream_playback output missing PWStreamDestroy method")
210+ }
211+}
M gen/pipewire.json +5 -3
@@ -5,8 +5,8 @@
55 ],
66 "groups": [
77 {"name": "init", "interface": "InitAPI", "package": "out", "symbols": ["pw_init", "pw_deinit"]},
8- {"name": "loop", "interface": "LoopAPI", "package": "out", "symbols": ["pw_main_loop_new", "pw_main_loop_destroy", "pw_main_loop_run"]},
9- {"name": "stream_playback", "interface": "StreamPlaybackAPI", "package": "out", "symbols": ["pw_stream_new_simple", "pw_stream_connect", "pw_stream_set_active", "pw_stream_disconnect", "pw_stream_dequeue_buffer", "pw_stream_queue_buffer", "pw_stream_add_listener"]}
8+ {"name": "loop", "interface": "LoopAPI", "package": "out", "symbols": ["pw_main_loop_new", "pw_main_loop_destroy", "pw_main_loop_run", "pw_main_loop_quit"]},
9+ {"name": "stream_playback", "interface": "StreamPlaybackAPI", "package": "out", "symbols": ["pw_stream_new_simple", "pw_stream_connect", "pw_stream_set_active", "pw_stream_disconnect", "pw_stream_dequeue_buffer", "pw_stream_queue_buffer", "pw_stream_add_listener", "pw_stream_destroy"]}
1010 ],
1111 "symbols": [
1212 {"name": "pw_init", "library": "pipewire", "group": "init", "signature": "func(argc *int32, argv ***byte)"},
@@ -14,13 +14,15 @@
1414 {"name": "pw_main_loop_new", "library": "pipewire", "group": "loop", "signature": "func(props unsafe.Pointer) unsafe.Pointer"},
1515 {"name": "pw_main_loop_destroy", "library": "pipewire", "group": "loop", "signature": "func(loop unsafe.Pointer)"},
1616 {"name": "pw_main_loop_run", "library": "pipewire", "group": "loop", "signature": "func(loop unsafe.Pointer) int32"},
17+ {"name": "pw_main_loop_quit", "library": "pipewire", "group": "loop", "signature": "func(loop unsafe.Pointer) int32"},
1718 {"name": "pw_stream_new_simple", "library": "pipewire", "group": "stream_playback", "signature": "func(context unsafe.Pointer, name *byte, props unsafe.Pointer, events unsafe.Pointer, data unsafe.Pointer) unsafe.Pointer"},
1819 {"name": "pw_stream_connect", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer, direction int32, id uint32, flags uint32, ports unsafe.Pointer, n_ports uint32) int32"},
1920 {"name": "pw_stream_set_active", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer, active bool) int32"},
2021 {"name": "pw_stream_disconnect", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer) int32"},
2122 {"name": "pw_stream_dequeue_buffer", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer) unsafe.Pointer"},
2223 {"name": "pw_stream_queue_buffer", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer, buffer unsafe.Pointer) int32"},
23- {"name": "pw_stream_add_listener", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer, listener unsafe.Pointer, events unsafe.Pointer, data unsafe.Pointer) int32"}
24+ {"name": "pw_stream_add_listener", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer, listener unsafe.Pointer, events unsafe.Pointer, data unsafe.Pointer) int32"},
25+ {"name": "pw_stream_destroy", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer)"}
2426 ],
2527 "callbacks": [
2628 {"name": "pw_stream_events", "signature": "struct{version uint32; process *func(stream unsafe.Pointer); param_changed *func(stream unsafe.Pointer, id uint32, param unsafe.Pointer); add_buffer *func(stream unsafe.Pointer, buffer unsafe.Pointer); remove_buffer *func(stream unsafe.Pointer, buffer unsafe.Pointer); flush *func(stream unsafe.Pointer, drain bool); drained *func(stream unsafe.Pointer); io_changed *func(stream unsafe.Pointer, id uint32, area unsafe.Pointer, size uint32)}"}
@@ -5,8 +5,8 @@
5 ],5 ],
6 "groups": [6 "groups": [
7 {"name": "init", "interface": "InitAPI", "package": "out", "symbols": ["pw_init", "pw_deinit"]},7 {"name": "init", "interface": "InitAPI", "package": "out", "symbols": ["pw_init", "pw_deinit"]},
8- {"name": "loop", "interface": "LoopAPI", "package": "out", "symbols": ["pw_main_loop_new", "pw_main_loop_destroy", "pw_main_loop_run"]},8+ {"name": "loop", "interface": "LoopAPI", "package": "out", "symbols": ["pw_main_loop_new", "pw_main_loop_destroy", "pw_main_loop_run", "pw_main_loop_quit"]},
9- {"name": "stream_playback", "interface": "StreamPlaybackAPI", "package": "out", "symbols": ["pw_stream_new_simple", "pw_stream_connect", "pw_stream_set_active", "pw_stream_disconnect", "pw_stream_dequeue_buffer", "pw_stream_queue_buffer", "pw_stream_add_listener"]}9+ {"name": "stream_playback", "interface": "StreamPlaybackAPI", "package": "out", "symbols": ["pw_stream_new_simple", "pw_stream_connect", "pw_stream_set_active", "pw_stream_disconnect", "pw_stream_dequeue_buffer", "pw_stream_queue_buffer", "pw_stream_add_listener", "pw_stream_destroy"]}
10 ],10 ],
11 "symbols": [11 "symbols": [
12 {"name": "pw_init", "library": "pipewire", "group": "init", "signature": "func(argc *int32, argv ***byte)"},12 {"name": "pw_init", "library": "pipewire", "group": "init", "signature": "func(argc *int32, argv ***byte)"},
@@ -14,13 +14,15 @@
14 {"name": "pw_main_loop_new", "library": "pipewire", "group": "loop", "signature": "func(props unsafe.Pointer) unsafe.Pointer"},14 {"name": "pw_main_loop_new", "library": "pipewire", "group": "loop", "signature": "func(props unsafe.Pointer) unsafe.Pointer"},
15 {"name": "pw_main_loop_destroy", "library": "pipewire", "group": "loop", "signature": "func(loop unsafe.Pointer)"},15 {"name": "pw_main_loop_destroy", "library": "pipewire", "group": "loop", "signature": "func(loop unsafe.Pointer)"},
16 {"name": "pw_main_loop_run", "library": "pipewire", "group": "loop", "signature": "func(loop unsafe.Pointer) int32"},16 {"name": "pw_main_loop_run", "library": "pipewire", "group": "loop", "signature": "func(loop unsafe.Pointer) int32"},
17+ {"name": "pw_main_loop_quit", "library": "pipewire", "group": "loop", "signature": "func(loop unsafe.Pointer) int32"},
17 {"name": "pw_stream_new_simple", "library": "pipewire", "group": "stream_playback", "signature": "func(context unsafe.Pointer, name *byte, props unsafe.Pointer, events unsafe.Pointer, data unsafe.Pointer) unsafe.Pointer"},18 {"name": "pw_stream_new_simple", "library": "pipewire", "group": "stream_playback", "signature": "func(context unsafe.Pointer, name *byte, props unsafe.Pointer, events unsafe.Pointer, data unsafe.Pointer) unsafe.Pointer"},
18 {"name": "pw_stream_connect", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer, direction int32, id uint32, flags uint32, ports unsafe.Pointer, n_ports uint32) int32"},19 {"name": "pw_stream_connect", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer, direction int32, id uint32, flags uint32, ports unsafe.Pointer, n_ports uint32) int32"},
19 {"name": "pw_stream_set_active", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer, active bool) int32"},20 {"name": "pw_stream_set_active", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer, active bool) int32"},
20 {"name": "pw_stream_disconnect", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer) int32"},21 {"name": "pw_stream_disconnect", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer) int32"},
21 {"name": "pw_stream_dequeue_buffer", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer) unsafe.Pointer"},22 {"name": "pw_stream_dequeue_buffer", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer) unsafe.Pointer"},
22 {"name": "pw_stream_queue_buffer", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer, buffer unsafe.Pointer) int32"},23 {"name": "pw_stream_queue_buffer", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer, buffer unsafe.Pointer) int32"},
23- {"name": "pw_stream_add_listener", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer, listener unsafe.Pointer, events unsafe.Pointer, data unsafe.Pointer) int32"}24+ {"name": "pw_stream_add_listener", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer, listener unsafe.Pointer, events unsafe.Pointer, data unsafe.Pointer) int32"},
25+ {"name": "pw_stream_destroy", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer)"}
24 ],26 ],
25 "callbacks": [27 "callbacks": [
26 {"name": "pw_stream_events", "signature": "struct{version uint32; process *func(stream unsafe.Pointer); param_changed *func(stream unsafe.Pointer, id uint32, param unsafe.Pointer); add_buffer *func(stream unsafe.Pointer, buffer unsafe.Pointer); remove_buffer *func(stream unsafe.Pointer, buffer unsafe.Pointer); flush *func(stream unsafe.Pointer, drain bool); drained *func(stream unsafe.Pointer); io_changed *func(stream unsafe.Pointer, id uint32, area unsafe.Pointer, size uint32)}"}28 {"name": "pw_stream_events", "signature": "struct{version uint32; process *func(stream unsafe.Pointer); param_changed *func(stream unsafe.Pointer, id uint32, param unsafe.Pointer); add_buffer *func(stream unsafe.Pointer, buffer unsafe.Pointer); remove_buffer *func(stream unsafe.Pointer, buffer unsafe.Pointer); flush *func(stream unsafe.Pointer, drain bool); drained *func(stream unsafe.Pointer); io_changed *func(stream unsafe.Pointer, id uint32, area unsafe.Pointer, size uint32)}"}
M internal/capi/loop_gen.go +7 -0 generated file: collapsed
M internal/capi/register.go +36 -0
@@ -31,6 +31,42 @@ func (d defaultImpl) PWMainLoopRun(loop unsafe.Pointer) int32 {
3131 return pw_main_loop_run(loop)
3232 }
3333
34+func (d defaultImpl) PWMainLoopQuit(loop unsafe.Pointer) int32 {
35+ return pw_main_loop_quit(loop)
36+}
37+
38+func (d defaultImpl) PWStreamNewSimple(context unsafe.Pointer, name *byte, props unsafe.Pointer, events unsafe.Pointer, data unsafe.Pointer) unsafe.Pointer {
39+ return pw_stream_new_simple(context, name, props, events, data)
40+}
41+
42+func (d defaultImpl) PWStreamConnect(stream unsafe.Pointer, direction int32, id uint32, flags uint32, ports unsafe.Pointer, n_ports uint32) int32 {
43+ return pw_stream_connect(stream, direction, id, flags, ports, n_ports)
44+}
45+
46+func (d defaultImpl) PWStreamSetActive(stream unsafe.Pointer, active bool) int32 {
47+ return pw_stream_set_active(stream, active)
48+}
49+
50+func (d defaultImpl) PWStreamDisconnect(stream unsafe.Pointer) int32 {
51+ return pw_stream_disconnect(stream)
52+}
53+
54+func (d defaultImpl) PWStreamDequeueBuffer(stream unsafe.Pointer) unsafe.Pointer {
55+ return pw_stream_dequeue_buffer(stream)
56+}
57+
58+func (d defaultImpl) PWStreamQueueBuffer(stream unsafe.Pointer, buffer unsafe.Pointer) int32 {
59+ return pw_stream_queue_buffer(stream, buffer)
60+}
61+
62+func (d defaultImpl) PWStreamAddListener(stream unsafe.Pointer, listener unsafe.Pointer, events unsafe.Pointer, data unsafe.Pointer) int32 {
63+ return pw_stream_add_listener(stream, listener, events, data)
64+}
65+
66+func (d defaultImpl) PWStreamDestroy(stream unsafe.Pointer) {
67+ pw_stream_destroy(stream)
68+}
69+
3470 var (
3571 defaultAPI portout.CAPI
3672 registerOnce sync.Once
@@ -31,6 +31,42 @@ func (d defaultImpl) PWMainLoopRun(loop unsafe.Pointer) int32 {
31 return pw_main_loop_run(loop)31 return pw_main_loop_run(loop)
32 }32 }
33 33
34+func (d defaultImpl) PWMainLoopQuit(loop unsafe.Pointer) int32 {
35+ return pw_main_loop_quit(loop)
36+}
37+
38+func (d defaultImpl) PWStreamNewSimple(context unsafe.Pointer, name *byte, props unsafe.Pointer, events unsafe.Pointer, data unsafe.Pointer) unsafe.Pointer {
39+ return pw_stream_new_simple(context, name, props, events, data)
40+}
41+
42+func (d defaultImpl) PWStreamConnect(stream unsafe.Pointer, direction int32, id uint32, flags uint32, ports unsafe.Pointer, n_ports uint32) int32 {
43+ return pw_stream_connect(stream, direction, id, flags, ports, n_ports)
44+}
45+
46+func (d defaultImpl) PWStreamSetActive(stream unsafe.Pointer, active bool) int32 {
47+ return pw_stream_set_active(stream, active)
48+}
49+
50+func (d defaultImpl) PWStreamDisconnect(stream unsafe.Pointer) int32 {
51+ return pw_stream_disconnect(stream)
52+}
53+
54+func (d defaultImpl) PWStreamDequeueBuffer(stream unsafe.Pointer) unsafe.Pointer {
55+ return pw_stream_dequeue_buffer(stream)
56+}
57+
58+func (d defaultImpl) PWStreamQueueBuffer(stream unsafe.Pointer, buffer unsafe.Pointer) int32 {
59+ return pw_stream_queue_buffer(stream, buffer)
60+}
61+
62+func (d defaultImpl) PWStreamAddListener(stream unsafe.Pointer, listener unsafe.Pointer, events unsafe.Pointer, data unsafe.Pointer) int32 {
63+ return pw_stream_add_listener(stream, listener, events, data)
64+}
65+
66+func (d defaultImpl) PWStreamDestroy(stream unsafe.Pointer) {
67+ pw_stream_destroy(stream)
68+}
69+
34 var (70 var (
35 defaultAPI portout.CAPI71 defaultAPI portout.CAPI
36 registerOnce sync.Once72 registerOnce sync.Once
M internal/capi/register_all.go +1 -0
@@ -7,5 +7,6 @@ import "github.com/bnema/purego-pipewire/internal/loader"
77 func registerAll(h loader.Handles) error {
88 registerInit(h.PipeWire)
99 registerLoop(h.PipeWire)
10+ registerStream_playback(h.PipeWire)
1011 return nil
1112 }
@@ -7,5 +7,6 @@ import "github.com/bnema/purego-pipewire/internal/loader"
7 func registerAll(h loader.Handles) error {7 func registerAll(h loader.Handles) error {
8 registerInit(h.PipeWire)8 registerInit(h.PipeWire)
9 registerLoop(h.PipeWire)9 registerLoop(h.PipeWire)
10+ registerStream_playback(h.PipeWire)
10 return nil11 return nil
11 }12 }
A internal/capi/stream_ops.go +154 -0
@@ -0,0 +1,154 @@
1+package capi
2+
3+import (
4+ "errors"
5+ "fmt"
6+ "sync"
7+ "unsafe"
8+
9+ portout "github.com/bnema/purego-pipewire/internal/ports/out"
10+)
11+
12+var (
13+ errStreamCreate = errors.New("pw_stream_new_simple returned nil")
14+ errStreamConnect = errors.New("pw_stream_connect failed")
15+ errMainLoopCreate = errors.New("pw_main_loop_new returned nil")
16+)
17+
18+// streamCallbackStorage keeps the pw_stream_events struct and its
19+// associated Go callback alive for the lifetime of the stream.
20+// Without this, the stack-allocated values in CreatePlaybackStream
21+// would be collected while PipeWire still holds pointers to them.
22+type streamCallbackStorage struct {
23+ processCb func(unsafe.Pointer)
24+ events pw_stream_events
25+}
26+
27+// streamOpsImpl implements portout.StreamOps using the real PipeWire C API
28+// bindings registered via purego.
29+type streamOpsImpl struct {
30+ mu sync.Mutex
31+ pinned map[unsafe.Pointer]*streamCallbackStorage // keyed by stream ptr
32+ destroyed map[unsafe.Pointer]bool // guard double-destroy
33+}
34+
35+// Verify interface compliance at compile time.
36+var _ portout.StreamOps = (*streamOpsImpl)(nil)
37+
38+func (s *streamOpsImpl) CreatePlaybackStream(loopPtr unsafe.Pointer, name string, sampleRate int, channels int, onProcess func()) (unsafe.Pointer, error) {
39+ nameBytes := append([]byte(name), 0)
40+
41+ // Build persistent callback storage that outlives this function.
42+ storage := &streamCallbackStorage{}
43+ storage.processCb = func(_ unsafe.Pointer) {
44+ onProcess()
45+ }
46+ storage.events = pw_stream_events{
47+ version: 0, // PW_VERSION_STREAM_EVENTS
48+ process: &storage.processCb,
49+ }
50+
51+ ptr := pw_stream_new_simple(
52+ loopPtr,
53+ &nameBytes[0],
54+ nil, // props
55+ unsafe.Pointer(&storage.events), // events
56+ nil, // data
57+ )
58+ if ptr == nil {
59+ return nil, errStreamCreate
60+ }
61+
62+ // Pin the storage so GC cannot collect it while PipeWire holds references.
63+ s.mu.Lock()
64+ if s.pinned == nil {
65+ s.pinned = make(map[unsafe.Pointer]*streamCallbackStorage)
66+ }
67+ s.pinned[ptr] = storage
68+ s.mu.Unlock()
69+
70+ return ptr, nil
71+}
72+
73+func (s *streamOpsImpl) ConnectPlaybackStream(streamPtr unsafe.Pointer) error {
74+ // PW_DIRECTION_OUTPUT = 1, PW_ID_ANY = 0xffffffff
75+ // PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS = 0x01 | 0x04
76+ ret := pw_stream_connect(streamPtr, 1, 0xffffffff, 0x05, nil, 0)
77+ if ret < 0 {
78+ return errStreamConnect
79+ }
80+ return nil
81+}
82+
83+func (s *streamOpsImpl) SetStreamActive(streamPtr unsafe.Pointer, active bool) error {
84+ ret := pw_stream_set_active(streamPtr, active)
85+ if ret < 0 {
86+ return fmt.Errorf("pw_stream_set_active failed: %d", ret)
87+ }
88+ return nil
89+}
90+
91+func (s *streamOpsImpl) DequeueBuffer(streamPtr unsafe.Pointer) unsafe.Pointer {
92+ return pw_stream_dequeue_buffer(streamPtr)
93+}
94+
95+func (s *streamOpsImpl) QueueBuffer(streamPtr unsafe.Pointer, bufPtr unsafe.Pointer) error {
96+ ret := pw_stream_queue_buffer(streamPtr, bufPtr)
97+ if ret < 0 {
98+ return fmt.Errorf("pw_stream_queue_buffer failed: %d", ret)
99+ }
100+ return nil
101+}
102+
103+func (s *streamOpsImpl) DisconnectStream(streamPtr unsafe.Pointer) {
104+ pw_stream_disconnect(streamPtr)
105+}
106+
107+func (s *streamOpsImpl) DestroyStream(streamPtr unsafe.Pointer) {
108+ s.mu.Lock()
109+ if s.destroyed == nil {
110+ s.destroyed = make(map[unsafe.Pointer]bool)
111+ }
112+ if s.destroyed[streamPtr] {
113+ s.mu.Unlock()
114+ return
115+ }
116+ s.destroyed[streamPtr] = true
117+ // Unpin callback storage — safe now that the stream is being destroyed.
118+ delete(s.pinned, streamPtr)
119+ s.mu.Unlock()
120+
121+ // pw_stream_destroy is not in the generated bindings yet;
122+ // disconnect is the safe fallback to release the stream.
123+ pw_stream_disconnect(streamPtr)
124+}
125+
126+func (s *streamOpsImpl) CreateMainLoop() (unsafe.Pointer, error) {
127+ ptr := pw_main_loop_new(nil)
128+ if ptr == nil {
129+ return nil, errMainLoopCreate
130+ }
131+ return ptr, nil
132+}
133+
134+func (s *streamOpsImpl) RunMainLoop(loopPtr unsafe.Pointer) {
135+ pw_main_loop_run(loopPtr)
136+}
137+
138+func (s *streamOpsImpl) QuitMainLoop(loopPtr unsafe.Pointer) {
139+ // pw_main_loop_quit is not in the generated bindings yet.
140+ // This is a placeholder — the main loop will exit when the
141+ // stream is disconnected or the process is terminated.
142+ //
143+ // TODO: Add pw_main_loop_quit to gen/pipewire.json and regenerate.
144+}
145+
146+func (s *streamOpsImpl) DestroyMainLoop(loopPtr unsafe.Pointer) {
147+ pw_main_loop_destroy(loopPtr)
148+}
149+
150+// DefaultStreamOps returns a StreamOps implementation backed by the real
151+// PipeWire C API. Must be called after Register().
152+func DefaultStreamOps() portout.StreamOps {
153+ return &streamOpsImpl{}
154+}
@@ -0,0 +1,154 @@
1+package capi
2+
3+import (
4+ "errors"
5+ "fmt"
6+ "sync"
7+ "unsafe"
8+
9+ portout "github.com/bnema/purego-pipewire/internal/ports/out"
10+)
11+
12+var (
13+ errStreamCreate = errors.New("pw_stream_new_simple returned nil")
14+ errStreamConnect = errors.New("pw_stream_connect failed")
15+ errMainLoopCreate = errors.New("pw_main_loop_new returned nil")
16+)
17+
18+// streamCallbackStorage keeps the pw_stream_events struct and its
19+// associated Go callback alive for the lifetime of the stream.
20+// Without this, the stack-allocated values in CreatePlaybackStream
21+// would be collected while PipeWire still holds pointers to them.
22+type streamCallbackStorage struct {
23+ processCb func(unsafe.Pointer)
24+ events pw_stream_events
25+}
26+
27+// streamOpsImpl implements portout.StreamOps using the real PipeWire C API
28+// bindings registered via purego.
29+type streamOpsImpl struct {
30+ mu sync.Mutex
31+ pinned map[unsafe.Pointer]*streamCallbackStorage // keyed by stream ptr
32+ destroyed map[unsafe.Pointer]bool // guard double-destroy
33+}
34+
35+// Verify interface compliance at compile time.
36+var _ portout.StreamOps = (*streamOpsImpl)(nil)
37+
38+func (s *streamOpsImpl) CreatePlaybackStream(loopPtr unsafe.Pointer, name string, sampleRate int, channels int, onProcess func()) (unsafe.Pointer, error) {
39+ nameBytes := append([]byte(name), 0)
40+
41+ // Build persistent callback storage that outlives this function.
42+ storage := &streamCallbackStorage{}
43+ storage.processCb = func(_ unsafe.Pointer) {
44+ onProcess()
45+ }
46+ storage.events = pw_stream_events{
47+ version: 0, // PW_VERSION_STREAM_EVENTS
48+ process: &storage.processCb,
49+ }
50+
51+ ptr := pw_stream_new_simple(
52+ loopPtr,
53+ &nameBytes[0],
54+ nil, // props
55+ unsafe.Pointer(&storage.events), // events
56+ nil, // data
57+ )
58+ if ptr == nil {
59+ return nil, errStreamCreate
60+ }
61+
62+ // Pin the storage so GC cannot collect it while PipeWire holds references.
63+ s.mu.Lock()
64+ if s.pinned == nil {
65+ s.pinned = make(map[unsafe.Pointer]*streamCallbackStorage)
66+ }
67+ s.pinned[ptr] = storage
68+ s.mu.Unlock()
69+
70+ return ptr, nil
71+}
72+
73+func (s *streamOpsImpl) ConnectPlaybackStream(streamPtr unsafe.Pointer) error {
74+ // PW_DIRECTION_OUTPUT = 1, PW_ID_ANY = 0xffffffff
75+ // PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS = 0x01 | 0x04
76+ ret := pw_stream_connect(streamPtr, 1, 0xffffffff, 0x05, nil, 0)
77+ if ret < 0 {
78+ return errStreamConnect
79+ }
80+ return nil
81+}
82+
83+func (s *streamOpsImpl) SetStreamActive(streamPtr unsafe.Pointer, active bool) error {
84+ ret := pw_stream_set_active(streamPtr, active)
85+ if ret < 0 {
86+ return fmt.Errorf("pw_stream_set_active failed: %d", ret)
87+ }
88+ return nil
89+}
90+
91+func (s *streamOpsImpl) DequeueBuffer(streamPtr unsafe.Pointer) unsafe.Pointer {
92+ return pw_stream_dequeue_buffer(streamPtr)
93+}
94+
95+func (s *streamOpsImpl) QueueBuffer(streamPtr unsafe.Pointer, bufPtr unsafe.Pointer) error {
96+ ret := pw_stream_queue_buffer(streamPtr, bufPtr)
97+ if ret < 0 {
98+ return fmt.Errorf("pw_stream_queue_buffer failed: %d", ret)
99+ }
100+ return nil
101+}
102+
103+func (s *streamOpsImpl) DisconnectStream(streamPtr unsafe.Pointer) {
104+ pw_stream_disconnect(streamPtr)
105+}
106+
107+func (s *streamOpsImpl) DestroyStream(streamPtr unsafe.Pointer) {
108+ s.mu.Lock()
109+ if s.destroyed == nil {
110+ s.destroyed = make(map[unsafe.Pointer]bool)
111+ }
112+ if s.destroyed[streamPtr] {
113+ s.mu.Unlock()
114+ return
115+ }
116+ s.destroyed[streamPtr] = true
117+ // Unpin callback storage — safe now that the stream is being destroyed.
118+ delete(s.pinned, streamPtr)
119+ s.mu.Unlock()
120+
121+ // pw_stream_destroy is not in the generated bindings yet;
122+ // disconnect is the safe fallback to release the stream.
123+ pw_stream_disconnect(streamPtr)
124+}
125+
126+func (s *streamOpsImpl) CreateMainLoop() (unsafe.Pointer, error) {
127+ ptr := pw_main_loop_new(nil)
128+ if ptr == nil {
129+ return nil, errMainLoopCreate
130+ }
131+ return ptr, nil
132+}
133+
134+func (s *streamOpsImpl) RunMainLoop(loopPtr unsafe.Pointer) {
135+ pw_main_loop_run(loopPtr)
136+}
137+
138+func (s *streamOpsImpl) QuitMainLoop(loopPtr unsafe.Pointer) {
139+ // pw_main_loop_quit is not in the generated bindings yet.
140+ // This is a placeholder — the main loop will exit when the
141+ // stream is disconnected or the process is terminated.
142+ //
143+ // TODO: Add pw_main_loop_quit to gen/pipewire.json and regenerate.
144+}
145+
146+func (s *streamOpsImpl) DestroyMainLoop(loopPtr unsafe.Pointer) {
147+ pw_main_loop_destroy(loopPtr)
148+}
149+
150+// DefaultStreamOps returns a StreamOps implementation backed by the real
151+// PipeWire C API. Must be called after Register().
152+func DefaultStreamOps() portout.StreamOps {
153+ return &streamOpsImpl{}
154+}
M internal/capi/stream_playback_gen.go +7 -0 generated file: collapsed
M internal/ports/out/loop_gen.go +1 -0 generated file: collapsed
M internal/ports/out/mocks/mock_CAPI.go +51 -0
@@ -207,6 +207,57 @@ func (_c *MockCAPI_PWMainLoopNew_Call) RunAndReturn(run func(props unsafe.Pointe
207207 return _c
208208 }
209209
210+// PWMainLoopQuit provides a mock function for the type MockCAPI
211+func (_mock *MockCAPI) PWMainLoopQuit(loop unsafe.Pointer) int32 {
212+ ret := _mock.Called(loop)
213+
214+ if len(ret) == 0 {
215+ panic("no return value specified for PWMainLoopQuit")
216+ }
217+
218+ var r0 int32
219+ if returnFunc, ok := ret.Get(0).(func(unsafe.Pointer) int32); ok {
220+ r0 = returnFunc(loop)
221+ } else {
222+ r0 = ret.Get(0).(int32)
223+ }
224+ return r0
225+}
226+
227+// MockCAPI_PWMainLoopQuit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PWMainLoopQuit'
228+type MockCAPI_PWMainLoopQuit_Call struct {
229+ *mock.Call
230+}
231+
232+// PWMainLoopQuit is a helper method to define mock.On call
233+// - loop unsafe.Pointer
234+func (_e *MockCAPI_Expecter) PWMainLoopQuit(loop interface{}) *MockCAPI_PWMainLoopQuit_Call {
235+ return &MockCAPI_PWMainLoopQuit_Call{Call: _e.mock.On("PWMainLoopQuit", loop)}
236+}
237+
238+func (_c *MockCAPI_PWMainLoopQuit_Call) Run(run func(loop unsafe.Pointer)) *MockCAPI_PWMainLoopQuit_Call {
239+ _c.Call.Run(func(args mock.Arguments) {
240+ var arg0 unsafe.Pointer
241+ if args[0] != nil {
242+ arg0 = args[0].(unsafe.Pointer)
243+ }
244+ run(
245+ arg0,
246+ )
247+ })
248+ return _c
249+}
250+
251+func (_c *MockCAPI_PWMainLoopQuit_Call) Return(n int32) *MockCAPI_PWMainLoopQuit_Call {
252+ _c.Call.Return(n)
253+ return _c
254+}
255+
256+func (_c *MockCAPI_PWMainLoopQuit_Call) RunAndReturn(run func(loop unsafe.Pointer) int32) *MockCAPI_PWMainLoopQuit_Call {
257+ _c.Call.Return(run)
258+ return _c
259+}
260+
210261 // PWMainLoopRun provides a mock function for the type MockCAPI
211262 func (_mock *MockCAPI) PWMainLoopRun(loop unsafe.Pointer) int32 {
212263 ret := _mock.Called(loop)
@@ -207,6 +207,57 @@ func (_c *MockCAPI_PWMainLoopNew_Call) RunAndReturn(run func(props unsafe.Pointe
207 return _c207 return _c
208 }208 }
209 209
210+// PWMainLoopQuit provides a mock function for the type MockCAPI
211+func (_mock *MockCAPI) PWMainLoopQuit(loop unsafe.Pointer) int32 {
212+ ret := _mock.Called(loop)
213+
214+ if len(ret) == 0 {
215+ panic("no return value specified for PWMainLoopQuit")
216+ }
217+
218+ var r0 int32
219+ if returnFunc, ok := ret.Get(0).(func(unsafe.Pointer) int32); ok {
220+ r0 = returnFunc(loop)
221+ } else {
222+ r0 = ret.Get(0).(int32)
223+ }
224+ return r0
225+}
226+
227+// MockCAPI_PWMainLoopQuit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PWMainLoopQuit'
228+type MockCAPI_PWMainLoopQuit_Call struct {
229+ *mock.Call
230+}
231+
232+// PWMainLoopQuit is a helper method to define mock.On call
233+// - loop unsafe.Pointer
234+func (_e *MockCAPI_Expecter) PWMainLoopQuit(loop interface{}) *MockCAPI_PWMainLoopQuit_Call {
235+ return &MockCAPI_PWMainLoopQuit_Call{Call: _e.mock.On("PWMainLoopQuit", loop)}
236+}
237+
238+func (_c *MockCAPI_PWMainLoopQuit_Call) Run(run func(loop unsafe.Pointer)) *MockCAPI_PWMainLoopQuit_Call {
239+ _c.Call.Run(func(args mock.Arguments) {
240+ var arg0 unsafe.Pointer
241+ if args[0] != nil {
242+ arg0 = args[0].(unsafe.Pointer)
243+ }
244+ run(
245+ arg0,
246+ )
247+ })
248+ return _c
249+}
250+
251+func (_c *MockCAPI_PWMainLoopQuit_Call) Return(n int32) *MockCAPI_PWMainLoopQuit_Call {
252+ _c.Call.Return(n)
253+ return _c
254+}
255+
256+func (_c *MockCAPI_PWMainLoopQuit_Call) RunAndReturn(run func(loop unsafe.Pointer) int32) *MockCAPI_PWMainLoopQuit_Call {
257+ _c.Call.Return(run)
258+ return _c
259+}
260+
210 // PWMainLoopRun provides a mock function for the type MockCAPI261 // PWMainLoopRun provides a mock function for the type MockCAPI
211 func (_mock *MockCAPI) PWMainLoopRun(loop unsafe.Pointer) int32 {262 func (_mock *MockCAPI) PWMainLoopRun(loop unsafe.Pointer) int32 {
212 ret := _mock.Called(loop)263 ret := _mock.Called(loop)
M internal/ports/out/mocks/mock_LoopAPI.go +51 -0
@@ -128,6 +128,57 @@ func (_c *MockLoopAPI_PWMainLoopNew_Call) RunAndReturn(run func(props unsafe.Poi
128128 return _c
129129 }
130130
131+// PWMainLoopQuit provides a mock function for the type MockLoopAPI
132+func (_mock *MockLoopAPI) PWMainLoopQuit(loop unsafe.Pointer) int32 {
133+ ret := _mock.Called(loop)
134+
135+ if len(ret) == 0 {
136+ panic("no return value specified for PWMainLoopQuit")
137+ }
138+
139+ var r0 int32
140+ if returnFunc, ok := ret.Get(0).(func(unsafe.Pointer) int32); ok {
141+ r0 = returnFunc(loop)
142+ } else {
143+ r0 = ret.Get(0).(int32)
144+ }
145+ return r0
146+}
147+
148+// MockLoopAPI_PWMainLoopQuit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PWMainLoopQuit'
149+type MockLoopAPI_PWMainLoopQuit_Call struct {
150+ *mock.Call
151+}
152+
153+// PWMainLoopQuit is a helper method to define mock.On call
154+// - loop unsafe.Pointer
155+func (_e *MockLoopAPI_Expecter) PWMainLoopQuit(loop interface{}) *MockLoopAPI_PWMainLoopQuit_Call {
156+ return &MockLoopAPI_PWMainLoopQuit_Call{Call: _e.mock.On("PWMainLoopQuit", loop)}
157+}
158+
159+func (_c *MockLoopAPI_PWMainLoopQuit_Call) Run(run func(loop unsafe.Pointer)) *MockLoopAPI_PWMainLoopQuit_Call {
160+ _c.Call.Run(func(args mock.Arguments) {
161+ var arg0 unsafe.Pointer
162+ if args[0] != nil {
163+ arg0 = args[0].(unsafe.Pointer)
164+ }
165+ run(
166+ arg0,
167+ )
168+ })
169+ return _c
170+}
171+
172+func (_c *MockLoopAPI_PWMainLoopQuit_Call) Return(n int32) *MockLoopAPI_PWMainLoopQuit_Call {
173+ _c.Call.Return(n)
174+ return _c
175+}
176+
177+func (_c *MockLoopAPI_PWMainLoopQuit_Call) RunAndReturn(run func(loop unsafe.Pointer) int32) *MockLoopAPI_PWMainLoopQuit_Call {
178+ _c.Call.Return(run)
179+ return _c
180+}
181+
131182 // PWMainLoopRun provides a mock function for the type MockLoopAPI
132183 func (_mock *MockLoopAPI) PWMainLoopRun(loop unsafe.Pointer) int32 {
133184 ret := _mock.Called(loop)
@@ -128,6 +128,57 @@ func (_c *MockLoopAPI_PWMainLoopNew_Call) RunAndReturn(run func(props unsafe.Poi
128 return _c128 return _c
129 }129 }
130 130
131+// PWMainLoopQuit provides a mock function for the type MockLoopAPI
132+func (_mock *MockLoopAPI) PWMainLoopQuit(loop unsafe.Pointer) int32 {
133+ ret := _mock.Called(loop)
134+
135+ if len(ret) == 0 {
136+ panic("no return value specified for PWMainLoopQuit")
137+ }
138+
139+ var r0 int32
140+ if returnFunc, ok := ret.Get(0).(func(unsafe.Pointer) int32); ok {
141+ r0 = returnFunc(loop)
142+ } else {
143+ r0 = ret.Get(0).(int32)
144+ }
145+ return r0
146+}
147+
148+// MockLoopAPI_PWMainLoopQuit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PWMainLoopQuit'
149+type MockLoopAPI_PWMainLoopQuit_Call struct {
150+ *mock.Call
151+}
152+
153+// PWMainLoopQuit is a helper method to define mock.On call
154+// - loop unsafe.Pointer
155+func (_e *MockLoopAPI_Expecter) PWMainLoopQuit(loop interface{}) *MockLoopAPI_PWMainLoopQuit_Call {
156+ return &MockLoopAPI_PWMainLoopQuit_Call{Call: _e.mock.On("PWMainLoopQuit", loop)}
157+}
158+
159+func (_c *MockLoopAPI_PWMainLoopQuit_Call) Run(run func(loop unsafe.Pointer)) *MockLoopAPI_PWMainLoopQuit_Call {
160+ _c.Call.Run(func(args mock.Arguments) {
161+ var arg0 unsafe.Pointer
162+ if args[0] != nil {
163+ arg0 = args[0].(unsafe.Pointer)
164+ }
165+ run(
166+ arg0,
167+ )
168+ })
169+ return _c
170+}
171+
172+func (_c *MockLoopAPI_PWMainLoopQuit_Call) Return(n int32) *MockLoopAPI_PWMainLoopQuit_Call {
173+ _c.Call.Return(n)
174+ return _c
175+}
176+
177+func (_c *MockLoopAPI_PWMainLoopQuit_Call) RunAndReturn(run func(loop unsafe.Pointer) int32) *MockLoopAPI_PWMainLoopQuit_Call {
178+ _c.Call.Return(run)
179+ return _c
180+}
181+
131 // PWMainLoopRun provides a mock function for the type MockLoopAPI182 // PWMainLoopRun provides a mock function for the type MockLoopAPI
132 func (_mock *MockLoopAPI) PWMainLoopRun(loop unsafe.Pointer) int32 {183 func (_mock *MockLoopAPI) PWMainLoopRun(loop unsafe.Pointer) int32 {
133 ret := _mock.Called(loop)184 ret := _mock.Called(loop)
A internal/ports/out/stream_ops.go +45 -0
@@ -0,0 +1,45 @@
1+package out
2+
3+import "unsafe"
4+
5+// StreamOps defines the outbound interface for PipeWire stream lifecycle
6+// operations that the player needs. This abstracts the raw C API so the
7+// player can be tested without a real PipeWire daemon.
8+type StreamOps interface {
9+ // CreatePlaybackStream creates a new playback stream attached to the given
10+ // main loop. The onProcess callback is invoked by PipeWire each time the
11+ // stream needs more audio data. Returns the stream pointer or an error.
12+ CreatePlaybackStream(loopPtr unsafe.Pointer, name string, sampleRate int, channels int, onProcess func()) (streamPtr unsafe.Pointer, err error)
13+
14+ // ConnectPlaybackStream connects the stream for playback output.
15+ ConnectPlaybackStream(streamPtr unsafe.Pointer) error
16+
17+ // SetStreamActive activates or deactivates the stream.
18+ SetStreamActive(streamPtr unsafe.Pointer, active bool) error
19+
20+ // DequeueBuffer dequeues a buffer from the stream for writing.
21+ // Returns nil if no buffer is available.
22+ DequeueBuffer(streamPtr unsafe.Pointer) unsafe.Pointer
23+
24+ // QueueBuffer queues a filled buffer back to the stream.
25+ QueueBuffer(streamPtr unsafe.Pointer, bufPtr unsafe.Pointer) error
26+
27+ // DisconnectStream disconnects the stream.
28+ DisconnectStream(streamPtr unsafe.Pointer)
29+
30+ // DestroyStream destroys the stream and frees its resources.
31+ DestroyStream(streamPtr unsafe.Pointer)
32+
33+ // CreateMainLoop creates a new PipeWire main loop.
34+ // Returns the loop pointer or an error.
35+ CreateMainLoop() (loopPtr unsafe.Pointer, err error)
36+
37+ // RunMainLoop runs the main loop (blocks until quit).
38+ RunMainLoop(loopPtr unsafe.Pointer)
39+
40+ // QuitMainLoop signals the main loop to stop.
41+ QuitMainLoop(loopPtr unsafe.Pointer)
42+
43+ // DestroyMainLoop destroys the main loop.
44+ DestroyMainLoop(loopPtr unsafe.Pointer)
45+}
@@ -0,0 +1,45 @@
1+package out
2+
3+import "unsafe"
4+
5+// StreamOps defines the outbound interface for PipeWire stream lifecycle
6+// operations that the player needs. This abstracts the raw C API so the
7+// player can be tested without a real PipeWire daemon.
8+type StreamOps interface {
9+ // CreatePlaybackStream creates a new playback stream attached to the given
10+ // main loop. The onProcess callback is invoked by PipeWire each time the
11+ // stream needs more audio data. Returns the stream pointer or an error.
12+ CreatePlaybackStream(loopPtr unsafe.Pointer, name string, sampleRate int, channels int, onProcess func()) (streamPtr unsafe.Pointer, err error)
13+
14+ // ConnectPlaybackStream connects the stream for playback output.
15+ ConnectPlaybackStream(streamPtr unsafe.Pointer) error
16+
17+ // SetStreamActive activates or deactivates the stream.
18+ SetStreamActive(streamPtr unsafe.Pointer, active bool) error
19+
20+ // DequeueBuffer dequeues a buffer from the stream for writing.
21+ // Returns nil if no buffer is available.
22+ DequeueBuffer(streamPtr unsafe.Pointer) unsafe.Pointer
23+
24+ // QueueBuffer queues a filled buffer back to the stream.
25+ QueueBuffer(streamPtr unsafe.Pointer, bufPtr unsafe.Pointer) error
26+
27+ // DisconnectStream disconnects the stream.
28+ DisconnectStream(streamPtr unsafe.Pointer)
29+
30+ // DestroyStream destroys the stream and frees its resources.
31+ DestroyStream(streamPtr unsafe.Pointer)
32+
33+ // CreateMainLoop creates a new PipeWire main loop.
34+ // Returns the loop pointer or an error.
35+ CreateMainLoop() (loopPtr unsafe.Pointer, err error)
36+
37+ // RunMainLoop runs the main loop (blocks until quit).
38+ RunMainLoop(loopPtr unsafe.Pointer)
39+
40+ // QuitMainLoop signals the main loop to stop.
41+ QuitMainLoop(loopPtr unsafe.Pointer)
42+
43+ // DestroyMainLoop destroys the main loop.
44+ DestroyMainLoop(loopPtr unsafe.Pointer)
45+}
M internal/ports/out/stream_playback_gen.go +1 -0
@@ -13,4 +13,5 @@ type StreamPlaybackAPI interface {
1313 PWStreamDequeueBuffer(stream unsafe.Pointer) unsafe.Pointer
1414 PWStreamQueueBuffer(stream unsafe.Pointer, buffer unsafe.Pointer) int32
1515 PWStreamAddListener(stream unsafe.Pointer, listener unsafe.Pointer, events unsafe.Pointer, data unsafe.Pointer) int32
16+ PWStreamDestroy(stream unsafe.Pointer)
1617 }
@@ -13,4 +13,5 @@ type StreamPlaybackAPI interface {
13 PWStreamDequeueBuffer(stream unsafe.Pointer) unsafe.Pointer13 PWStreamDequeueBuffer(stream unsafe.Pointer) unsafe.Pointer
14 PWStreamQueueBuffer(stream unsafe.Pointer, buffer unsafe.Pointer) int3214 PWStreamQueueBuffer(stream unsafe.Pointer, buffer unsafe.Pointer) int32
15 PWStreamAddListener(stream unsafe.Pointer, listener unsafe.Pointer, events unsafe.Pointer, data unsafe.Pointer) int3215 PWStreamAddListener(stream unsafe.Pointer, listener unsafe.Pointer, events unsafe.Pointer, data unsafe.Pointer) int32
16+ PWStreamDestroy(stream unsafe.Pointer)
16 }17 }

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 1

Tags 8

↑↓ moveenter openesc close