1//go:build integration
2
3package integration
4
5import (
6 "testing"
7 "time"
8
9 "github.com/bnema/purego-pipewire/pipewire"
10)
11
12// TestPlayerStartStopClose verifies the player lifecycle against a real PipeWire
13// daemon. This test requires a running PipeWire runtime and only validates
14// externally visible state transitions (Idle -> Playing -> Stopped -> Closed).
15// Deeper cleanup internals are covered by unit tests.
16func TestPlayerStartStopClose(t *testing.T) {
17 // Initialize PipeWire
18 initResult, err := pipewire.Init()
19 if err != nil {
20 t.Fatalf("Init returned error: %v", err)
21 }
22 defer initResult.Deinit()
23
24 // Create player with specified configuration
25 config := pipewire.PlayerConfig{
26 SampleRate: 48000,
27 Channels: 2,
28 FramesPerBuffer: 256,
29 SampleFormat: pipewire.SampleFormatF32,
30 UnderrunPolicy: pipewire.UnderrunFillSilence,
31 }
32
33 callbacks := pipewire.PlayerCallbacks{
34 Fill: func(buf *pipewire.PCMBuffer) (int, error) {
35 return buf.Frames, nil
36 },
37 }
38
39 player, err := pipewire.NewPlayer(config, callbacks)
40 if err != nil {
41 t.Fatalf("NewPlayer returned error: %v", err)
42 }
43
44 // Verify initial state is Idle
45 if state := player.State(); state != pipewire.PlayerStateIdle {
46 t.Errorf("Expected initial state Idle, got %v", state)
47 }
48
49 // Test Start - should transition to Playing state
50 if err := player.Start(); err != nil {
51 t.Errorf("Start returned error: %v", err)
52 }
53 if state := player.State(); state != pipewire.PlayerStatePlaying {
54 t.Errorf("After Start: expected state Playing, got %v", state)
55 }
56
57 // Test Stop - should transition to Stopped state
58 if err := player.Stop(); err != nil {
59 t.Errorf("Stop returned error: %v", err)
60 }
61 if state := player.State(); state != pipewire.PlayerStateStopped {
62 t.Errorf("After Stop: expected state Stopped, got %v", state)
63 }
64
65 // Test Close - should transition to Closed state
66 if err := player.Close(); err != nil {
67 t.Errorf("Close returned error: %v", err)
68 }
69 if state := player.State(); state != pipewire.PlayerStateClosed {
70 t.Errorf("After Close: expected state Closed, got %v", state)
71 }
72}
73
74// TestPlayerPlaybackInvokesFill proves that starting a Player under a live
75// PipeWire daemon causes the process callback to invoke the user-supplied Fill
76// callback. A buffered channel synchronizes the test instead of sleeping.
77func TestPlayerPlaybackInvokesFill(t *testing.T) {
78 initResult, err := pipewire.Init()
79 if err != nil {
80 t.Fatalf("Init returned error: %v", err)
81 }
82 defer initResult.Deinit()
83
84 filled := make(chan struct{}, 1) // buffered so Fill never blocks
85
86 config := pipewire.PlayerConfig{
87 SampleRate: 48000,
88 Channels: 2,
89 FramesPerBuffer: 512,
90 SampleFormat: pipewire.SampleFormatF32,
91 UnderrunPolicy: pipewire.UnderrunFillSilence,
92 }
93
94 callbacks := pipewire.PlayerCallbacks{
95 Fill: func(buf *pipewire.PCMBuffer) (int, error) {
96 // Write a small constant so the buffer isn't silent.
97 const val = 0.001
98 for ch := range buf.Samples {
99 for i := range buf.Samples[ch] {
100 buf.Samples[ch][i] = val
101 }
102 }
103 // Signal exactly once.
104 select {
105 case filled <- struct{}{}:
106 default:
107 }
108 return buf.Frames, nil
109 },
110 }
111
112 player, err := pipewire.NewPlayer(config, callbacks)
113 if err != nil {
114 t.Fatalf("NewPlayer returned error: %v", err)
115 }
116
117 if err := player.Start(); err != nil {
118 t.Fatalf("Start returned error: %v", err)
119 }
120
121 // Wait for the Fill callback to fire at least once.
122 select {
123 case <-filled:
124 // success — the real PipeWire process callback invoked Fill
125 case <-time.After(5 * time.Second):
126 t.Fatal("Timed out waiting for Fill callback; PipeWire process callback did not fire")
127 }
128
129 if err := player.Stop(); err != nil {
130 t.Errorf("Stop returned error: %v", err)
131 }
132 if err := player.Close(); err != nil {
133 t.Errorf("Close returned error: %v", err)
134 }
135}