/
player.py
301 lines (261 loc) · 8.63 KB
/
player.py
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
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
# Copyright 2009-2011 Klas Lindberg <klas.lindberg@gmail.com>
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 3, as published
# by the Free Software Foundation.
import traceback
import sys
import os.path
import struct
import time
from protocol import(Strm, StrmStartMpeg, StrmStartFlac, StrmStop, StrmFlush,
StrmSkip, Stat, StrmPause, StrmUnpause)
from render import NowPlayingRender
from menu import CmAudio, Link
class Player(object):
guid = None # used when telling the device how to present itself
wire = None
playing = None # NowPlaying instance
repeat = False
shuffle = False
def __init__(self, wire, guid, repeat=False, shuffle=False):
assert type(repeat) == bool
assert type(shuffle) == bool
self.guid = guid
self.wire = wire
self.repeat = repeat
self.shuffle = shuffle
self.stop()
def dump_settings(self):
return { 'repeat': self.repeat, 'shuffle': self.shuffle }
@classmethod
def dump_defaults(cls):
return { 'repeat': False, 'shuffle': False }
def toggle_repeat(self):
self.repeat = not self.repeat
def toggle_shuffle(self):
self.shuffle = not self.shuffle
def get_in_threshold(self, size):
if size < 10*1024:
tmp = size / 1024 # 'size' is a natural number so the result is too
if tmp > 0:
return tmp - 1
return 10 # I'm just guessing
def play(self, item, seek=0):
link = None
if isinstance(item, Link):
link = item
item = item.target
if not isinstance(item, CmAudio):
return False
if not item.cm:
return False
if seek > item.duration:
return False
# always send stop command before initiating a new stream.
self.stop()
if link:
self.playing = NowPlaying(link, item.duration, seek)
else:
self.playing = NowPlaying(item, item.duration, seek)
if item.format == 'mp3':
Cls = StrmStartMpeg
elif item.format == 'flac':
Cls = StrmStartFlac
strm = Cls(item.cm.stream_ip, item.cm.stream_port, item.guid, seek)
strm.in_threshold = self.get_in_threshold(item.size)
time.sleep(0.1) # necessary to make sure the device doesn't get confused
self.wire.send(strm.serialize())
return True
def jump(self, position):
self.play(self.playing.item, position)
def duration(self):
return self.playing.duration
def position(self):
return self.playing.position()
# def flush_buffer(self):
# self.wire.send(StrmFlush().serialize())
# self.playing = None
# def stream_background(self):
# strm = StrmStartMpeg(0, self.cm.port, path, True)
# self.wire.send(strm.serialize())
def stop(self):
self.wire.send(StrmStop().serialize())
self.playing = None
def pause(self):
if not self.playing:
return
try:
if not self.playing.paused():
self.playing.enter_state(NowPlaying.PAUSED)
self.wire.send(StrmPause().serialize())
else:
self.playing.enter_state(NowPlaying.BUFFERING)
self.wire.send(StrmUnpause().serialize())
except Exception, e:
print e
traceback.print_stack()
# def skip(self, msecs):
# if self.playing.state != NowPlaying.PLAYING:
# return
# self.wire.send(StrmSkip(msecs).serialize())
def set_progress(self, msecs, in_fill, out_fill):
if not self.playing:
return
if out_fill > 0:
self.playing.set_progress(msecs)
return None
elif self.playing.state == NowPlaying.BUFFERING:
return None
else:
try:
return self.playing.item.next(self.repeat, self.shuffle)
except:
return None
def get_progress(self):
return self.playing.position()
def get_playing(self):
if not self.playing:
return None
return self.playing.item
def ticker(self, curry=False):
if self.playing:
if curry:
self.playing.curry()
return (self.playing.item.guid, self.playing.render)
else:
return (None, None)
def next_render_mode(self):
if not self.playing:
return
self.playing.render.next_mode()
def handle_resp(self, resp):
if resp.http_header.startswith('HTTP/1.0 200 OK'):
return
if resp.http_header.startswith('HTTP/1.0 404 Not Found'):
self.stop()
return
print('INTERNAL ERROR: Unknown HTTP response: %s' % resp.http_header)
def handle_stat(self, stat):
if not isinstance(stat, Stat):
raise Exception('Invalid Player.handle_stat(stat): %s' % str(stat))
#print(stat.log(level=1))
if stat.event == 'STMt':
# SBS sources calls this the "timer" event. it seems to mean that
# the device has a periodic timeout going, because the STMt is sent
# with an interval of about 1-2 seconds. the interesting content is
# buffer fullness.
next = self.set_progress(stat.msecs, stat.in_fill, stat.out_fill)
if next:
self.stop()
#print 'STMt next = %s' % unicode(next)
return next
if stat.event == 'STMo':
# find next item to play, if any
try:
next = self.playing.item.next(self.repeat, self.shuffle)
#print 'STMo next = %s' % unicode(next)
except:
next = None
#print 'STMo next = None'
# finish the currently playing track
self.set_progress(stat.msecs, stat.in_fill, stat.out_fill)
self.stop()
return next
if stat.event == '\0\0\0\0':
# undocumented but always received right after the device connects
# to the server. probably just a state indication without any event
# semantics.
return None
if stat.event == 'stat':
# undocumented event. received when the undocumented 'stat' command
# is sent to the device. all STAT fields have well defined contents
# but we don't care much as it is only received when the device and
# server are otherwise idle.
return None
if stat.event == 'STMf':
# SBS sources say "closed", but this makes no sense as it will be
# received whether the device was previously connected to a streamer
# or not. it's also not about flushed buffers as those are typically
# reported as unaffected.
#print('Device closed the stream connection')
return None
if stat.event == 'STMc':
# SBS sources say this means "connected", but to what? probably the
# streamer even though it is received before ACK of strm command.
#print('Device connected to streamer')
return None
if stat.event == 'STMe':
# connection established with streamer. i.e. more than just an open
# socket.
#print('Device established connection to streamer')
return None
if stat.event == 'STMh':
# "end of headers", but which ones? probably the header sent by the
# streamer in response to HTTP GET.
#print('Device finished reading headers')
return None
if stat.event == 'STMs':
#print('Device started playing')
self.playing.enter_state(NowPlaying.PLAYING)
return None
if stat.event == 'STMp':
#print('Device paused playback')
return None
if stat.event == 'STMr':
#print('Device resumed playback')
return None
# simple ACKs of commands sent to the device. ignore:
if stat.event in ['strm', 'aude', 'audg']:
return None
bytes = struct.unpack('%dB' % len(stat.event), stat.event)
print('UNHANDLED STAT EVENT: %s' % str(bytes))
print str(stat)
return None
class NowPlaying(object):
# state definitions
BUFFERING = 0 # forced pause to give a device's buffers time to fill up
PLAYING = 1
PAUSED = 2
item = None # playable menu item (e.g. an CmAudio object)
render = None
state = BUFFERING
start = 0 # current playback position (in milliseconds) is calculated
progress = 0 # as the start position plus the progress. position should
duration = 0 # of course never be greater than the duration.
def __init__(self, item, duration, start=0):
self.item = item
self.duration = duration
self.start = start
self.render = NowPlayingRender()
self.curry()
def enter_state(self, state):
if state == self.state:
return
if state == NowPlaying.BUFFERING:
if ((self.state != NowPlaying.PLAYING)
and (self.state != NowPlaying.PAUSED)):
raise Exception, 'Must enter BUFFERING from PLAYING or PAUSED'
elif state == NowPlaying.PLAYING:
if ((self.state != NowPlaying.BUFFERING)
and (self.state != NowPlaying.PAUSED)):
raise Exception, 'Must enter PLAYING from BUFFERING or PAUSED'
elif state == NowPlaying.PAUSED:
if self.state != NowPlaying.PLAYING:
raise Exception, 'Must enter PAUSED from PLAYING'
elif state == NowPlaying.STOPPED:
pass
self.state = state
def set_progress(self, progress):
self.enter_state(NowPlaying.PLAYING)
self.progress = progress
self.render.curry(self.position() / float(self.duration), None)
def paused(self):
return self.state == NowPlaying.PAUSED
def position(self):
return self.start + self.progress
def curry(self):
if type(self.item) == Link:
item = self.item.target
else:
item = self.item
self.render.curry(self.position() / float(self.duration), item)