feat: generate PipeWire cleanup bindings
- commit
- 2e65b39bd7e113a5e763a714617109bfb79908be
- author
- brice · · 2026-04-10 03:23 UTC
- parent
- ef1527c
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 @@ | |||
| 1 | 1 | package emitter | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "path/filepath" | |
| 4 | 5 | "strings" | |
| 5 | 6 | "testing" | |
| 6 | 7 | ||
| 7 | 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 | 12 | func testModel() *model.Model { | |
| @@ -98,3 +100,112 @@ func TestEmitWritesPlayerStreamFiles(t *testing.T) { | |||
| 98 | 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 | + | } | |
| @@ -1,10 +1,12 @@ | |||||
| 1 | package emitter | 1 | 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 @@ | |||
| 5 | 5 | ], | |
| 6 | 6 | "groups": [ | |
| 7 | 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"]}, | |
| 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"]} | |
| 10 | 10 | ], | |
| 11 | 11 | "symbols": [ | |
| 12 | 12 | {"name": "pw_init", "library": "pipewire", "group": "init", "signature": "func(argc *int32, argv ***byte)"}, | |
| @@ -14,13 +14,15 @@ | |||
| 14 | 14 | {"name": "pw_main_loop_new", "library": "pipewire", "group": "loop", "signature": "func(props unsafe.Pointer) unsafe.Pointer"}, | |
| 15 | 15 | {"name": "pw_main_loop_destroy", "library": "pipewire", "group": "loop", "signature": "func(loop unsafe.Pointer)"}, | |
| 16 | 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 | 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 | 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 | 20 | {"name": "pw_stream_set_active", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer, active bool) int32"}, | |
| 20 | 21 | {"name": "pw_stream_disconnect", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer) int32"}, | |
| 21 | 22 | {"name": "pw_stream_dequeue_buffer", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer) unsafe.Pointer"}, | |
| 22 | 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 | 27 | "callbacks": [ | |
| 26 | 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)}"} | |
| @@ -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 { | |||
| 31 | 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 | 70 | var ( | |
| 35 | 71 | defaultAPI portout.CAPI | |
| 36 | 72 | 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.CAPI | 71 | defaultAPI portout.CAPI | ||
| 36 | registerOnce sync.Once | 72 | registerOnce sync.Once | ||
M internal/capi/register_all.go +1 -0
| @@ -7,5 +7,6 @@ import "github.com/bnema/purego-pipewire/internal/loader" | |||
| 7 | 7 | func registerAll(h loader.Handles) error { | |
| 8 | 8 | registerInit(h.PipeWire) | |
| 9 | 9 | registerLoop(h.PipeWire) | |
| 10 | + | registerStream_playback(h.PipeWire) | |
| 10 | 11 | return nil | |
| 11 | 12 | } | |
| @@ -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 nil | 11 | 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
| @@ -9,4 +9,5 @@ type LoopAPI interface { | |||
| 9 | 9 | PWMainLoopNew(props unsafe.Pointer) unsafe.Pointer | |
| 10 | 10 | PWMainLoopDestroy(loop unsafe.Pointer) | |
| 11 | 11 | PWMainLoopRun(loop unsafe.Pointer) int32 | |
| 12 | + | PWMainLoopQuit(loop unsafe.Pointer) int32 | |
| 12 | 13 | } | |
| @@ -9,4 +9,5 @@ type LoopAPI interface { | |||||
| 9 | PWMainLoopNew(props unsafe.Pointer) unsafe.Pointer | 9 | PWMainLoopNew(props unsafe.Pointer) unsafe.Pointer | ||
| 10 | PWMainLoopDestroy(loop unsafe.Pointer) | 10 | PWMainLoopDestroy(loop unsafe.Pointer) | ||
| 11 | PWMainLoopRun(loop unsafe.Pointer) int32 | 11 | PWMainLoopRun(loop unsafe.Pointer) int32 | ||
| 12 | + | PWMainLoopQuit(loop unsafe.Pointer) int32 | |||
| 12 | } | 13 | } | ||
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 | |||
| 207 | 207 | 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 | 261 | // PWMainLoopRun provides a mock function for the type MockCAPI | |
| 211 | 262 | func (_mock *MockCAPI) PWMainLoopRun(loop unsafe.Pointer) int32 { | |
| 212 | 263 | ret := _mock.Called(loop) | |
| @@ -207,6 +207,57 @@ func (_c *MockCAPI_PWMainLoopNew_Call) RunAndReturn(run func(props unsafe.Pointe | |||||
| 207 | return _c | 207 | 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 MockCAPI | 261 | // 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 | |||
| 128 | 128 | 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 | 182 | // PWMainLoopRun provides a mock function for the type MockLoopAPI | |
| 132 | 183 | func (_mock *MockLoopAPI) PWMainLoopRun(loop unsafe.Pointer) int32 { | |
| 133 | 184 | ret := _mock.Called(loop) | |
| @@ -128,6 +128,57 @@ func (_c *MockLoopAPI_PWMainLoopNew_Call) RunAndReturn(run func(props unsafe.Poi | |||||
| 128 | return _c | 128 | 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 MockLoopAPI | 182 | // 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 | + | } | |||