feat: wire public player wrapper to internal core
- commit
- 73b2a2aa61a0deca82c46ab82ea08da210f000f8
- author
- brice · · 2026-04-09 15:45 UTC
- parent
- 958d550
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +148 -3
M internal/core/player.go +1 -1
| @@ -73,7 +73,7 @@ func (p *player) transition(next PlayerState) error { | |||
| 73 | 73 | func isValidTransition(current, next PlayerState) bool { | |
| 74 | 74 | switch current { | |
| 75 | 75 | case PlayerStateIdle: | |
| 76 | - | return next == PlayerStateStarting | |
| 76 | + | return next == PlayerStateStarting || next == PlayerStateClosing | |
| 77 | 77 | case PlayerStateStarting: | |
| 78 | 78 | return next == PlayerStatePlaying || next == PlayerStateError | |
| 79 | 79 | case PlayerStatePlaying: | |
| @@ -73,7 +73,7 @@ func (p *player) transition(next PlayerState) error { | |||||
| 73 | func isValidTransition(current, next PlayerState) bool { | 73 | func isValidTransition(current, next PlayerState) bool { | ||
| 74 | switch current { | 74 | switch current { | ||
| 75 | case PlayerStateIdle: | 75 | case PlayerStateIdle: | ||
| 76 | - | return next == PlayerStateStarting | 76 | + | return next == PlayerStateStarting || next == PlayerStateClosing |
| 77 | case PlayerStateStarting: | 77 | case PlayerStateStarting: | ||
| 78 | return next == PlayerStatePlaying || next == PlayerStateError | 78 | return next == PlayerStatePlaying || next == PlayerStateError | ||
| 79 | case PlayerStatePlaying: | 79 | case PlayerStatePlaying: | ||
M pipewire/player.go +44 -2
| @@ -154,6 +154,15 @@ func NewPlayer(config PlayerConfig, callbacks PlayerCallbacks) (Player, error) { | |||
| 154 | 154 | return nil, ErrInvalidPlayerConfig | |
| 155 | 155 | } | |
| 156 | 156 | ||
| 157 | + | // Convert public UnderrunPolicy to internal UnderrunPolicy | |
| 158 | + | var coreUnderrunPolicy core.UnderrunPolicy | |
| 159 | + | switch config.UnderrunPolicy { | |
| 160 | + | case UnderrunFail: | |
| 161 | + | coreUnderrunPolicy = core.UnderrunFailFast | |
| 162 | + | default: | |
| 163 | + | coreUnderrunPolicy = core.UnderrunFillSilence | |
| 164 | + | } | |
| 165 | + | ||
| 157 | 166 | // Convert public callbacks to internal callbacks | |
| 158 | 167 | coreCallbacks := core.PlayerCallbacks{ | |
| 159 | 168 | OnStateChange: func(state core.PlayerState) { | |
| @@ -161,10 +170,43 @@ func NewPlayer(config PlayerConfig, callbacks PlayerCallbacks) (Player, error) { | |||
| 161 | 170 | callbacks.OnStateChange(PlayerState(state)) | |
| 162 | 171 | } | |
| 163 | 172 | }, | |
| 173 | + | Fill: func(buf *core.PCMBuffer) (int, error) { | |
| 174 | + | if callbacks.Fill == nil { | |
| 175 | + | return 0, nil | |
| 176 | + | } | |
| 177 | + | // Adapt internal PCMBuffer to public PCMBuffer | |
| 178 | + | publicBuf := &PCMBuffer{ | |
| 179 | + | Frames: buf.Frames, | |
| 180 | + | Channels: buf.Channels, | |
| 181 | + | Stride: buf.Stride, | |
| 182 | + | Samples: buf.Samples, | |
| 183 | + | } | |
| 184 | + | return callbacks.Fill(publicBuf) | |
| 185 | + | }, | |
| 186 | + | OnUnderrun: func(frames int) { | |
| 187 | + | if callbacks.OnUnderrun != nil { | |
| 188 | + | callbacks.OnUnderrun(frames) | |
| 189 | + | } | |
| 190 | + | }, | |
| 191 | + | OnDrain: func() { | |
| 192 | + | if callbacks.OnDrain != nil { | |
| 193 | + | callbacks.OnDrain() | |
| 194 | + | } | |
| 195 | + | }, | |
| 196 | + | OnError: func(err error) { | |
| 197 | + | if callbacks.OnError != nil { | |
| 198 | + | callbacks.OnError(err) | |
| 199 | + | } | |
| 200 | + | }, | |
| 164 | 201 | } | |
| 165 | 202 | ||
| 166 | - | // Create internal player | |
| 167 | - | internalPlayer := core.NewPlayer(core.PlayerConfig{}, coreCallbacks) | |
| 203 | + | // Create internal player with mapped config | |
| 204 | + | coreConfig := core.PlayerConfig{ | |
| 205 | + | FramesPerBuffer: config.FramesPerBuffer, | |
| 206 | + | Channels: config.Channels, | |
| 207 | + | UnderrunPolicy: coreUnderrunPolicy, | |
| 208 | + | } | |
| 209 | + | internalPlayer := core.NewPlayer(coreConfig, coreCallbacks) | |
| 168 | 210 | ||
| 169 | 211 | return &playerImpl{ | |
| 170 | 212 | config: config, | |
| @@ -154,6 +154,15 @@ func NewPlayer(config PlayerConfig, callbacks PlayerCallbacks) (Player, error) { | |||||
| 154 | return nil, ErrInvalidPlayerConfig | 154 | return nil, ErrInvalidPlayerConfig | ||
| 155 | } | 155 | } | ||
| 156 | 156 | ||||
| 157 | + | // Convert public UnderrunPolicy to internal UnderrunPolicy | |||
| 158 | + | var coreUnderrunPolicy core.UnderrunPolicy | |||
| 159 | + | switch config.UnderrunPolicy { | |||
| 160 | + | case UnderrunFail: | |||
| 161 | + | coreUnderrunPolicy = core.UnderrunFailFast | |||
| 162 | + | default: | |||
| 163 | + | coreUnderrunPolicy = core.UnderrunFillSilence | |||
| 164 | + | } | |||
| 165 | + | ||||
| 157 | // Convert public callbacks to internal callbacks | 166 | // Convert public callbacks to internal callbacks | ||
| 158 | coreCallbacks := core.PlayerCallbacks{ | 167 | coreCallbacks := core.PlayerCallbacks{ | ||
| 159 | OnStateChange: func(state core.PlayerState) { | 168 | OnStateChange: func(state core.PlayerState) { | ||
| @@ -161,10 +170,43 @@ func NewPlayer(config PlayerConfig, callbacks PlayerCallbacks) (Player, error) { | |||||
| 161 | callbacks.OnStateChange(PlayerState(state)) | 170 | callbacks.OnStateChange(PlayerState(state)) | ||
| 162 | } | 171 | } | ||
| 163 | }, | 172 | }, | ||
| 173 | + | Fill: func(buf *core.PCMBuffer) (int, error) { | |||
| 174 | + | if callbacks.Fill == nil { | |||
| 175 | + | return 0, nil | |||
| 176 | + | } | |||
| 177 | + | // Adapt internal PCMBuffer to public PCMBuffer | |||
| 178 | + | publicBuf := &PCMBuffer{ | |||
| 179 | + | Frames: buf.Frames, | |||
| 180 | + | Channels: buf.Channels, | |||
| 181 | + | Stride: buf.Stride, | |||
| 182 | + | Samples: buf.Samples, | |||
| 183 | + | } | |||
| 184 | + | return callbacks.Fill(publicBuf) | |||
| 185 | + | }, | |||
| 186 | + | OnUnderrun: func(frames int) { | |||
| 187 | + | if callbacks.OnUnderrun != nil { | |||
| 188 | + | callbacks.OnUnderrun(frames) | |||
| 189 | + | } | |||
| 190 | + | }, | |||
| 191 | + | OnDrain: func() { | |||
| 192 | + | if callbacks.OnDrain != nil { | |||
| 193 | + | callbacks.OnDrain() | |||
| 194 | + | } | |||
| 195 | + | }, | |||
| 196 | + | OnError: func(err error) { | |||
| 197 | + | if callbacks.OnError != nil { | |||
| 198 | + | callbacks.OnError(err) | |||
| 199 | + | } | |||
| 200 | + | }, | |||
| 164 | } | 201 | } | ||
| 165 | 202 | ||||
| 166 | - | // Create internal player | 203 | + | // Create internal player with mapped config |
| 167 | - | internalPlayer := core.NewPlayer(core.PlayerConfig{}, coreCallbacks) | 204 | + | coreConfig := core.PlayerConfig{ |
| 205 | + | FramesPerBuffer: config.FramesPerBuffer, | |||
| 206 | + | Channels: config.Channels, | |||
| 207 | + | UnderrunPolicy: coreUnderrunPolicy, | |||
| 208 | + | } | |||
| 209 | + | internalPlayer := core.NewPlayer(coreConfig, coreCallbacks) | |||
| 168 | 210 | ||||
| 169 | return &playerImpl{ | 211 | return &playerImpl{ | ||
| 170 | config: config, | 212 | config: config, | ||
M pipewire/player_test.go +103 -0
| @@ -31,6 +31,22 @@ func TestNewPlayerAcceptsValidConfig(t *testing.T) { | |||
| 31 | 31 | } | |
| 32 | 32 | } | |
| 33 | 33 | ||
| 34 | + | func TestPlayerStateReflectsLifecycle(t *testing.T) { | |
| 35 | + | config := PlayerConfig{ | |
| 36 | + | SampleRate: 44100, | |
| 37 | + | Channels: 2, | |
| 38 | + | FramesPerBuffer: 256, | |
| 39 | + | SampleFormat: SampleFormatF32, | |
| 40 | + | } | |
| 41 | + | player, err := NewPlayer(config, PlayerCallbacks{}) | |
| 42 | + | if err != nil { | |
| 43 | + | t.Fatalf("NewPlayer returned error: %v", err) | |
| 44 | + | } | |
| 45 | + | if player.State() != PlayerStateIdle { | |
| 46 | + | t.Fatalf("expected initial state PlayerStateIdle, got %v", player.State()) | |
| 47 | + | } | |
| 48 | + | } | |
| 49 | + | ||
| 34 | 50 | func TestPlayerStateReflectsInitialState(t *testing.T) { | |
| 35 | 51 | config := PlayerConfig{ | |
| 36 | 52 | SampleRate: 44100, | |
| @@ -138,3 +154,90 @@ func TestPlayerCloseIsIdempotent(t *testing.T) { | |||
| 138 | 154 | t.Fatalf("expected Closed state after second close, got %v", player.State()) | |
| 139 | 155 | } | |
| 140 | 156 | } | |
| 157 | + | ||
| 158 | + | func TestPlayerCallbacksAreWired(t *testing.T) { | |
| 159 | + | var stateChanges []PlayerState | |
| 160 | + | var fillCalled bool | |
| 161 | + | var underrunFrames int | |
| 162 | + | var drainCalled bool | |
| 163 | + | ||
| 164 | + | config := PlayerConfig{ | |
| 165 | + | SampleRate: 44100, | |
| 166 | + | Channels: 2, | |
| 167 | + | FramesPerBuffer: 256, | |
| 168 | + | SampleFormat: SampleFormatF32, | |
| 169 | + | UnderrunPolicy: UnderrunFillSilence, | |
| 170 | + | } | |
| 171 | + | ||
| 172 | + | callbacks := PlayerCallbacks{ | |
| 173 | + | Fill: func(buf *PCMBuffer) (int, error) { | |
| 174 | + | fillCalled = true | |
| 175 | + | _ = fillCalled // Suppress unused warning for now | |
| 176 | + | return buf.Frames, nil | |
| 177 | + | }, | |
| 178 | + | OnUnderrun: func(frames int) { | |
| 179 | + | underrunFrames = frames | |
| 180 | + | _ = underrunFrames // Suppress unused warning for now | |
| 181 | + | }, | |
| 182 | + | OnDrain: func() { | |
| 183 | + | drainCalled = true | |
| 184 | + | _ = drainCalled // Suppress unused warning for now | |
| 185 | + | }, | |
| 186 | + | OnStateChange: func(state PlayerState) { | |
| 187 | + | stateChanges = append(stateChanges, state) | |
| 188 | + | }, | |
| 189 | + | } | |
| 190 | + | ||
| 191 | + | player, err := NewPlayer(config, callbacks) | |
| 192 | + | if err != nil { | |
| 193 | + | t.Fatalf("NewPlayer returned error: %v", err) | |
| 194 | + | } | |
| 195 | + | ||
| 196 | + | // Verify initial state | |
| 197 | + | if player.State() != PlayerStateIdle { | |
| 198 | + | t.Fatalf("expected PlayerStateIdle, got %v", player.State()) | |
| 199 | + | } | |
| 200 | + | ||
| 201 | + | // Start should trigger state change callback | |
| 202 | + | if err := player.Start(); err != nil { | |
| 203 | + | t.Fatalf("Start failed: %v", err) | |
| 204 | + | } | |
| 205 | + | ||
| 206 | + | // Verify we got state change callbacks | |
| 207 | + | if len(stateChanges) == 0 { | |
| 208 | + | t.Fatal("expected OnStateChange callbacks, got none") | |
| 209 | + | } | |
| 210 | + | ||
| 211 | + | // Clean up | |
| 212 | + | if err := player.Close(); err != nil { | |
| 213 | + | t.Fatalf("Close failed: %v", err) | |
| 214 | + | } | |
| 215 | + | } | |
| 216 | + | ||
| 217 | + | func TestPlayerConfigMapsToCore(t *testing.T) { | |
| 218 | + | config := PlayerConfig{ | |
| 219 | + | SampleRate: 48000, | |
| 220 | + | Channels: 4, | |
| 221 | + | FramesPerBuffer: 512, | |
| 222 | + | SampleFormat: SampleFormatF32, | |
| 223 | + | UnderrunPolicy: UnderrunFail, | |
| 224 | + | } | |
| 225 | + | ||
| 226 | + | player, err := NewPlayer(config, PlayerCallbacks{}) | |
| 227 | + | if err != nil { | |
| 228 | + | t.Fatalf("NewPlayer returned error: %v", err) | |
| 229 | + | } | |
| 230 | + | if player == nil { | |
| 231 | + | t.Fatal("NewPlayer returned nil player") | |
| 232 | + | } | |
| 233 | + | ||
| 234 | + | // Player should be usable after creation | |
| 235 | + | if player.State() != PlayerStateIdle { | |
| 236 | + | t.Fatalf("expected PlayerStateIdle, got %v", player.State()) | |
| 237 | + | } | |
| 238 | + | ||
| 239 | + | // Clean up | |
| 240 | + | if err := player.Close(); err != nil { | |
| 241 | + | t.Fatalf("Close failed: %v", err) | |
| 242 | + | } | |
| 243 | + | } | |
| @@ -31,6 +31,22 @@ func TestNewPlayerAcceptsValidConfig(t *testing.T) { | |||||
| 31 | } | 31 | } | ||
| 32 | } | 32 | } | ||
| 33 | 33 | ||||
| 34 | + | func TestPlayerStateReflectsLifecycle(t *testing.T) { | |||
| 35 | + | config := PlayerConfig{ | |||
| 36 | + | SampleRate: 44100, | |||
| 37 | + | Channels: 2, | |||
| 38 | + | FramesPerBuffer: 256, | |||
| 39 | + | SampleFormat: SampleFormatF32, | |||
| 40 | + | } | |||
| 41 | + | player, err := NewPlayer(config, PlayerCallbacks{}) | |||
| 42 | + | if err != nil { | |||
| 43 | + | t.Fatalf("NewPlayer returned error: %v", err) | |||
| 44 | + | } | |||
| 45 | + | if player.State() != PlayerStateIdle { | |||
| 46 | + | t.Fatalf("expected initial state PlayerStateIdle, got %v", player.State()) | |||
| 47 | + | } | |||
| 48 | + | } | |||
| 49 | + | ||||
| 34 | func TestPlayerStateReflectsInitialState(t *testing.T) { | 50 | func TestPlayerStateReflectsInitialState(t *testing.T) { | ||
| 35 | config := PlayerConfig{ | 51 | config := PlayerConfig{ | ||
| 36 | SampleRate: 44100, | 52 | SampleRate: 44100, | ||
| @@ -138,3 +154,90 @@ func TestPlayerCloseIsIdempotent(t *testing.T) { | |||||
| 138 | t.Fatalf("expected Closed state after second close, got %v", player.State()) | 154 | t.Fatalf("expected Closed state after second close, got %v", player.State()) | ||
| 139 | } | 155 | } | ||
| 140 | } | 156 | } | ||
| 157 | + | ||||
| 158 | + | func TestPlayerCallbacksAreWired(t *testing.T) { | |||
| 159 | + | var stateChanges []PlayerState | |||
| 160 | + | var fillCalled bool | |||
| 161 | + | var underrunFrames int | |||
| 162 | + | var drainCalled bool | |||
| 163 | + | ||||
| 164 | + | config := PlayerConfig{ | |||
| 165 | + | SampleRate: 44100, | |||
| 166 | + | Channels: 2, | |||
| 167 | + | FramesPerBuffer: 256, | |||
| 168 | + | SampleFormat: SampleFormatF32, | |||
| 169 | + | UnderrunPolicy: UnderrunFillSilence, | |||
| 170 | + | } | |||
| 171 | + | ||||
| 172 | + | callbacks := PlayerCallbacks{ | |||
| 173 | + | Fill: func(buf *PCMBuffer) (int, error) { | |||
| 174 | + | fillCalled = true | |||
| 175 | + | _ = fillCalled // Suppress unused warning for now | |||
| 176 | + | return buf.Frames, nil | |||
| 177 | + | }, | |||
| 178 | + | OnUnderrun: func(frames int) { | |||
| 179 | + | underrunFrames = frames | |||
| 180 | + | _ = underrunFrames // Suppress unused warning for now | |||
| 181 | + | }, | |||
| 182 | + | OnDrain: func() { | |||
| 183 | + | drainCalled = true | |||
| 184 | + | _ = drainCalled // Suppress unused warning for now | |||
| 185 | + | }, | |||
| 186 | + | OnStateChange: func(state PlayerState) { | |||
| 187 | + | stateChanges = append(stateChanges, state) | |||
| 188 | + | }, | |||
| 189 | + | } | |||
| 190 | + | ||||
| 191 | + | player, err := NewPlayer(config, callbacks) | |||
| 192 | + | if err != nil { | |||
| 193 | + | t.Fatalf("NewPlayer returned error: %v", err) | |||
| 194 | + | } | |||
| 195 | + | ||||
| 196 | + | // Verify initial state | |||
| 197 | + | if player.State() != PlayerStateIdle { | |||
| 198 | + | t.Fatalf("expected PlayerStateIdle, got %v", player.State()) | |||
| 199 | + | } | |||
| 200 | + | ||||
| 201 | + | // Start should trigger state change callback | |||
| 202 | + | if err := player.Start(); err != nil { | |||
| 203 | + | t.Fatalf("Start failed: %v", err) | |||
| 204 | + | } | |||
| 205 | + | ||||
| 206 | + | // Verify we got state change callbacks | |||
| 207 | + | if len(stateChanges) == 0 { | |||
| 208 | + | t.Fatal("expected OnStateChange callbacks, got none") | |||
| 209 | + | } | |||
| 210 | + | ||||
| 211 | + | // Clean up | |||
| 212 | + | if err := player.Close(); err != nil { | |||
| 213 | + | t.Fatalf("Close failed: %v", err) | |||
| 214 | + | } | |||
| 215 | + | } | |||
| 216 | + | ||||
| 217 | + | func TestPlayerConfigMapsToCore(t *testing.T) { | |||
| 218 | + | config := PlayerConfig{ | |||
| 219 | + | SampleRate: 48000, | |||
| 220 | + | Channels: 4, | |||
| 221 | + | FramesPerBuffer: 512, | |||
| 222 | + | SampleFormat: SampleFormatF32, | |||
| 223 | + | UnderrunPolicy: UnderrunFail, | |||
| 224 | + | } | |||
| 225 | + | ||||
| 226 | + | player, err := NewPlayer(config, PlayerCallbacks{}) | |||
| 227 | + | if err != nil { | |||
| 228 | + | t.Fatalf("NewPlayer returned error: %v", err) | |||
| 229 | + | } | |||
| 230 | + | if player == nil { | |||
| 231 | + | t.Fatal("NewPlayer returned nil player") | |||
| 232 | + | } | |||
| 233 | + | ||||
| 234 | + | // Player should be usable after creation | |||
| 235 | + | if player.State() != PlayerStateIdle { | |||
| 236 | + | t.Fatalf("expected PlayerStateIdle, got %v", player.State()) | |||
| 237 | + | } | |||
| 238 | + | ||||
| 239 | + | // Clean up | |||
| 240 | + | if err := player.Close(); err != nil { | |||
| 241 | + | t.Fatalf("Close failed: %v", err) | |||
| 242 | + | } | |||
| 243 | + | } | |||