-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathplayer.go
281 lines (238 loc) · 7.18 KB
/
player.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
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
package main
import (
"bufio"
"bytes"
"context"
"fmt"
"io"
"net"
"net/http"
"os"
"strings"
"github.com/maghul/go.raopd"
)
type SqueezePlayer struct {
playerHandler *http.ServeMux
serviceInfo *raopd.SinkInfo
pipeReader io.Reader
apService *raopd.Source
coverData []byte
metaData string
h *host
name string
}
var allSqueezePlayers = newSyncMap()
func (sp *SqueezePlayer) addHandlerFunc(suburl string, handler func(http.ResponseWriter, *http.Request)) {
sp.playerHandler.HandleFunc(fmt.Sprintf("/%s/%s", sp.Id(), suburl), handler)
}
func startPlayer(name, id string, h *host) (*SqueezePlayer, error) {
ff := func() (interface{}, error) {
hwaddr, err := net.ParseMAC(id)
if err != nil {
return nil, err
}
si := &raopd.SinkInfo{
SupportsCoverArt: true,
SupportsMetaData: "JSON",
Name: name,
HardwareAddress: hwaddr,
Port: 0,
}
sp := &SqueezePlayer{nil, si, nil, nil, nil, "", h, name}
sp.initPlayer()
sp.apService, err = airplayers.Register(sp)
if err != nil {
return nil, err
}
return sp, nil
}
spi, err := allSqueezePlayers.Get(id, ff)
if err != nil {
return nil, err
}
return spi.(*SqueezePlayer), err
}
func stopPlayer(name, id string) error {
spi, err := allSqueezePlayers.Remove(id)
if err != nil {
return err
}
sp := spi.(*SqueezePlayer)
sp.shutdown()
return nil
}
func (sp *SqueezePlayer) Id() string {
return sp.serviceInfo.HardwareAddress.String()
}
func (sp *SqueezePlayer) Name() string {
return sp.serviceInfo.Name
}
func (sp *SqueezePlayer) initPlayer() {
url := fmt.Sprintf("/%s/", sp.Id())
sp.playerHandler = http.NewServeMux()
slog.Debug.Println("Registering Player base URL=", url)
sp.addHandlerFunc("metadata.json", sp.metadata)
sp.addHandlerFunc("cover.jpg", sp.cover)
sp.addHandlerFunc("audio.pcm", sp.audio)
sp.addHandlerFunc("audio.wav", sp.audio)
sp.addHandlerFunc("time/", sp.seek)
sp.addHandlerFunc("control/volume/", sp.volume)
sp.addHandlerFunc("control/", sp.control)
}
func (sp *SqueezePlayer) close() {
}
func (sp *SqueezePlayer) shutdown() {
airplayers.Unregister(sp)
}
func (sp *SqueezePlayer) metadata(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "text/javascript")
io.WriteString(w, sp.metaData)
}
func (sp *SqueezePlayer) cover(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "image/jpeg")
w.Write(sp.coverData)
}
func (sp *SqueezePlayer) audio(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(os.Stderr, "Starting audio\n")
hj, ok := w.(http.Hijacker)
if !ok {
http.Error(w, "webserver doesn't support hijacking", http.StatusInternalServerError)
return
}
// OK, hijack the connection and start transferring PCM.
// TODO: Ensure we get at a sample boundary or there might be hell of
// a racket coming from the speakers...
conn, bufrw, err := hj.Hijack()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Don't forget to close the connection:
defer conn.Close()
mimetype := "audio/x-pcm"
bufrw.WriteString("HTTP/1.1 200 OK\r\n")
// bufrw.WriteString("Transfer-Encoding: binary\r\n")
bufrw.WriteString("Content-Type: ")
bufrw.WriteString(mimetype)
bufrw.WriteString("\r\n")
bufrw.WriteString("\r\n")
bufrw.Flush()
audioCtx := context.Background()
slog.Debug.Println("Audio pipe started, holding HTTP connection", r.RemoteAddr)
sp.apService.NewAudioStream(audioCtx, conn)
<-audioCtx.Done()
slog.Debug.Println("Audio pipe closed, ending HTTP connection ", r.RemoteAddr)
}
func (sp *SqueezePlayer) getTheCommand(w http.ResponseWriter, r *http.Request) string {
bw := bufio.NewWriter(w)
url := r.URL.String()
is := strings.LastIndex(url, "/")
if is < 0 {
w.WriteHeader(400)
fmt.Fprintln(w, "Could not parse command URL: ", url)
slog.Debug.Println(w, "Could not parse command URL: ", url)
return ""
}
url = url[is+1:]
bw.Flush()
return url
}
func (sp *SqueezePlayer) control(w http.ResponseWriter, r *http.Request) {
command := sp.getTheCommand(w, r)
slog.Debug.Println("control command=", command)
sp.apService.Command(command)
}
func (sp *SqueezePlayer) volume(w http.ResponseWriter, r *http.Request) {
cmd := sp.getTheCommand(w, r)
slog.Debug.Println("volume command ", cmd)
switch cmd {
case "relative":
slog.Debug.Println("Setting volume to relative for ", sp)
sp.apService.VolumeMode(false)
case "absolute":
slog.Debug.Println("Setting volume to absolute for ", sp)
sp.apService.VolumeMode(true)
default:
v, err := toRaopVolume(cmd)
if err != nil {
slog.Info.Println("Error converting volume ", cmd, ":", err)
w.WriteHeader(400)
fmt.Fprintln(w, "Error converting volume ", cmd, ":", err)
return
}
sp.apService.Volume(v)
}
}
func (sp *SqueezePlayer) notifyString(data string) {
buf := bytes.NewBufferString("{ \"")
buf.WriteString(sp.Id())
buf.WriteString("\":")
buf.WriteString(data)
buf.WriteString("}")
// TODO: Send the client as part of the notification to avoid slushing bytes about
sp.h.txNotification(buf.Bytes())
}
func (sp *SqueezePlayer) notify(data []byte) {
buf := bytes.NewBufferString("{ \"")
buf.WriteString(sp.Id())
buf.WriteString("\":")
buf.Write(data)
buf.WriteString("}")
// TODO: Send the client as part of the notification to avoid slushing bytes about
sp.h.txNotification(buf.Bytes())
}
// --- raopd.Sink implementation
// Get the service info for the service.
func (sp *SqueezePlayer) Info() *raopd.SinkInfo {
return sp.serviceInfo
}
func (sp *SqueezePlayer) SetCoverArt(mimetype string, content []byte) {
slog.Debug.Println("SetCoverArt:", mimetype, " buffer size=", len(content))
sp.coverData = content
sp.notifyString("\"coverart\"")
}
func (sp *SqueezePlayer) SetMetadata(metadata string) {
slog.Debug.Println("SetMetadata:", metadata)
sp.metaData = metadata
sp.notifyString(metadata)
}
// Set the volume of the output device. The volume value may be an absolute
// value from 0 - 100, or it may be up down values using UP=1000 and DOWN=-1000
func (sp *SqueezePlayer) SetVolume(volume float32) {
switch volume {
case 1000:
sp.notifyString("{ \"volume\": \"+2\" }")
case -1000:
sp.notifyString("{ \"volume\": \"-2\" }")
default:
sp.notifyString(fmt.Sprintf("{ \"volume\": %d }", int(ios2decVolume(volume))))
}
}
// Shows the progress of the track in milliseconds.
// pos is the current position, length is the total length of the current track
func (sp *SqueezePlayer) SetProgress(pos, length int) {
sp.notifyString(fmt.Sprintf("{ \"progress\": { \"current\": %d, \"length\": %d }}", pos, length))
}
// Called when a device has connected, specifically the DACP/Control connection.
func (sp *SqueezePlayer) Connected(name string) {
sp.notifyString(fmt.Sprintf("{ \"source\": \"%s\" }", name))
}
// Called when the stream is started.
func (sp *SqueezePlayer) Play() {
sp.notifyString("\"play\"")
}
// Called when the stream is paused
func (sp *SqueezePlayer) Pause() {
sp.notifyString("\"pause\"")
}
// Called when the connection to source is terminated
func (sp *SqueezePlayer) Stopped() {
sp.notifyString("\"stop\"")
}
// Called when the sink has been removed
func (sp *SqueezePlayer) Closed() {
}
// A name for the player
func (sp *SqueezePlayer) String() string {
return sp.name
}