forked from jfreymuth/pulse
-
Notifications
You must be signed in to change notification settings - Fork 0
/
client.go
205 lines (183 loc) · 5.77 KB
/
client.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
package pulse
import (
"fmt"
"net"
"os"
"path"
"sync"
"time"
"github.com/jfreymuth/pulse/proto"
)
// The Client is the connection to the pulseaudio server. An application typically only uses a single client.
type Client struct {
conn net.Conn
c *proto.Client
mu sync.Mutex
playback map[uint32]*PlaybackStream
record map[uint32]*RecordStream
server string
props proto.PropList
timeout time.Duration
}
// NewClient connects to the server.
func NewClient(opts ...ClientOption) (*Client, error) {
c := &Client{
props: proto.PropList{
"media.name": proto.PropListString("go audio"),
"application.name": proto.PropListString(path.Base(os.Args[0])),
"application.icon_name": proto.PropListString("audio-x-generic"),
"application.process.id": proto.PropListString(fmt.Sprintf("%d", os.Getpid())),
"application.process.binary": proto.PropListString(os.Args[0]),
"window.x11.display": proto.PropListString(os.Getenv("DISPLAY")),
},
}
for _, opt := range opts {
opt(c)
}
var err error
c.c, c.conn, err = proto.Connect(c.server)
if err != nil {
return nil, err
}
if c.timeout != 0 {
c.c.SetTimeout(c.timeout)
}
err = c.c.Request(&proto.SetClientName{Props: c.props}, &proto.SetClientNameReply{})
if err != nil {
c.conn.Close()
return nil, err
}
// Listen for changes to the sink input, which includes changes in volume.
err = c.c.Request(&proto.Subscribe{Mask: proto.SubscriptionMaskSinkInput}, nil)
if err != nil {
c.conn.Close()
return nil, err
}
c.playback = make(map[uint32]*PlaybackStream)
c.record = make(map[uint32]*RecordStream)
c.c.Callback = func(msg interface{}) {
switch msg := msg.(type) {
case *proto.Request:
c.mu.Lock()
stream, ok := c.playback[msg.StreamIndex]
c.mu.Unlock()
if ok {
stream.request <- int(msg.Length)
}
case *proto.DataPacket:
c.mu.Lock()
stream, ok := c.record[msg.StreamIndex]
c.mu.Unlock()
if ok {
stream.write(msg.Data)
}
case *proto.Started:
c.mu.Lock()
stream, ok := c.playback[msg.StreamIndex]
c.mu.Unlock()
if ok && stream.state == running && !stream.underflow {
stream.started <- true
}
case *proto.Underflow:
c.mu.Lock()
stream, ok := c.playback[msg.StreamIndex]
c.mu.Unlock()
if ok {
if stream.state == running {
stream.underflow = true
}
}
case *proto.ConnectionClosed:
c.mu.Lock()
for _, p := range c.playback {
close(p.request)
p.err = ErrConnectionClosed
p.state = serverLost
}
for _, r := range c.record {
r.err = ErrConnectionClosed
r.state = serverLost
}
c.playback = make(map[uint32]*PlaybackStream)
c.record = make(map[uint32]*RecordStream)
c.mu.Unlock()
c.conn.Close()
case *proto.SubscribeEvent:
if msg.Event&proto.EventFacilityMask == proto.EventSinkSinkInput {
// Something about the sink input changed, but we don't know
// what exactly. Signal this to the playback stream.
var stream *PlaybackStream
c.mu.Lock()
for _, v := range c.playback {
if msg.Index == v.createReply.SinkInputIndex {
stream = v
}
}
c.mu.Unlock()
if stream != nil {
stream.eventsLock.Lock()
if stream.events != nil {
// Do a non-blocking send.
// Because subscribeEvent is a buffered channel and
// because the channel has no contents (just signals),
// no message will be lost due to races.
select {
case stream.events <- struct{}{}:
default:
}
}
stream.eventsLock.Unlock()
}
}
default:
//fmt.Printf("%#v\n", msg)
}
}
return c, nil
}
// Close closes the client. Calling methods on a closed client may panic.
func (c *Client) Close() {
c.conn.Close()
}
// A ClientOption supplies configuration when creating the client.
type ClientOption func(*Client)
// ClientApplicationName sets the application name.
// This will e.g. be displayed by a volume control application to identity the application.
// It should be human-readable and localized.
func ClientApplicationName(name string) ClientOption {
return func(c *Client) { c.props["application.name"] = proto.PropListString(name) }
}
// ClientApplicationIconName sets the application icon using an xdg icon name.
// This will e.g. be displayed by a volume control application to identity the application.
func ClientApplicationIconName(name string) ClientOption {
return func(c *Client) { c.props["application.icon_name"] = proto.PropListString(name) }
}
// ClientServerString will override the default server strings.
// Server strings are used to connect to the server. For the server string format see
// https://www.freedesktop.org/wiki/Software/PulseAudio/Documentation/User/ServerStrings/
func ClientServerString(s string) ClientOption {
return func(c *Client) { c.server = s }
}
// ClientTimeout sets the timeout of requests to the specified duration.
// If d is 0, the default value (1 s) will be used.
func ClientTimeout(d time.Duration) ClientOption {
return func(c *Client) {
c.timeout = d
}
}
// RawRequest can be used to send arbitrary requests.
//
// req should be one of the request types defined by the proto package.
//
// rpl must be a pointer to the correct reply type or nil. This funcion will panic if rpl has the wrong type.
//
// The returned error can be compared against errors defined by the proto package to check for specific errors.
//
// The function will always block until the server has replied, even if rpl is nil.
func (c *Client) RawRequest(req proto.RequestArgs, rpl proto.Reply) error {
return c.c.Request(req, rpl)
}
// ErrConnectionClosed is a special error value indicating that the server closed the connection.
const ErrConnectionClosed = pulseError("pulseaudio: connection closed")
type pulseError string
func (e pulseError) Error() string { return string(e) }