Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# This file is part of Xpra. 

2# Copyright (C) 2010-2020 Antoine Martin <antoine@xpra.org> 

3# Xpra is released under the terms of the GNU GPL v2, or, at your option, any 

4# later version. See the file COPYING for details. 

5 

6from xpra.platform.paths import get_icon_filename 

7from xpra.scripts.parsing import sound_option 

8from xpra.net.compression import Compressed 

9from xpra.os_util import get_machine_id, get_user_uuid, bytestostr, OSX, POSIX 

10from xpra.util import envint, typedict, csv, updict 

11from xpra.client.mixins.stub_client_mixin import StubClientMixin 

12from xpra.log import Logger 

13 

14avsynclog = Logger("av-sync") 

15log = Logger("client", "sound") 

16 

17AV_SYNC_DELTA = envint("XPRA_AV_SYNC_DELTA") 

18DELTA_THRESHOLD = envint("XPRA_AV_SYNC_DELTA_THRESHOLD", 40) 

19DEFAULT_AV_SYNC_DELAY = envint("XPRA_DEFAULT_AV_SYNC_DELAY", 150) 

20 

21 

22""" 

23Utility superclass for clients that handle audio 

24""" 

25class AudioClient(StubClientMixin): 

26 __signals__ = ["speaker-changed", "microphone-changed"] 

27 

28 def __init__(self): 

29 StubClientMixin.__init__(self) 

30 self.sound_source_plugin = None 

31 self.speaker_allowed = False 

32 self.speaker_enabled = False 

33 self.speaker_codecs = [] 

34 self.microphone_allowed = False 

35 self.microphone_enabled = False 

36 self.microphone_codecs = [] 

37 self.microphone_device = None 

38 self.av_sync = False 

39 self.av_sync_delta = AV_SYNC_DELTA 

40 #sound state: 

41 self.on_sink_ready = None 

42 self.sound_sink = None 

43 self.sound_sink_sequence = 0 

44 self.server_sound_eos_sequence = False 

45 self.sound_source = None 

46 self.sound_source_sequence = 0 

47 self.sound_in_bytecount = 0 

48 self.sound_out_bytecount = 0 

49 self.server_av_sync = False 

50 self.server_pulseaudio_id = None 

51 self.server_pulseaudio_server = None 

52 self.server_sound_decoders = [] 

53 self.server_sound_encoders = [] 

54 self.server_sound_receive = False 

55 self.server_sound_send = False 

56 self.server_sound_bundle_metadata = False 

57 self.queue_used_sent = None 

58 #duplicated from ServerInfo mixin: 

59 self._remote_machine_id = None 

60 

61 def init(self, opts): 

62 self.av_sync = opts.av_sync 

63 self.sound_properties = typedict() 

64 self.speaker_allowed = sound_option(opts.speaker) in ("on", "off") 

65 #ie: "on", "off", "on:Some Device", "off:Some Device" 

66 mic = [x.strip() for x in opts.microphone.split(":", 1)] 

67 self.microphone_allowed = sound_option(mic[0]) in ("on", "off") 

68 self.microphone_device = None 

69 if self.microphone_allowed and len(mic)==2: 

70 self.microphone_device = mic[1] 

71 self.sound_source_plugin = opts.sound_source 

72 def sound_option_or_all(*_args): 

73 return [] 

74 if self.speaker_allowed or self.microphone_allowed: 

75 try: 

76 from xpra.sound import common 

77 assert common 

78 except ImportError as e: 

79 self.may_notify_audio("No Audio", 

80 "audio subsystem is not installed\n" + 

81 " speaker and microphone forwarding are disabled") 

82 self.speaker_allowed = False 

83 self.microphone_allowed = False 

84 else: 

85 try: 

86 from xpra.sound.common import sound_option_or_all 

87 from xpra.sound.wrapper import query_sound 

88 self.sound_properties = query_sound() 

89 assert self.sound_properties, "query did not return any data" 

90 def vinfo(k): 

91 val = self.sound_properties.strtupleget(k) 

92 assert val, "%s not found in sound properties" % k 

93 return ".".join(val[:3]) 

94 bits = self.sound_properties.intget("python.bits", 32) 

95 log.info("GStreamer version %s for Python %s %s-bit", 

96 vinfo("gst.version"), vinfo("python.version"), bits) 

97 except Exception as e: 

98 log("failed to query sound", exc_info=True) 

99 log.error("Error: failed to query sound subsystem:") 

100 log.error(" %s", e) 

101 self.speaker_allowed = False 

102 self.microphone_allowed = False 

103 encoders = self.sound_properties.strtupleget("encoders") 

104 decoders = self.sound_properties.strtupleget("decoders") 

105 self.speaker_codecs = sound_option_or_all("speaker-codec", opts.speaker_codec, decoders) 

106 self.microphone_codecs = sound_option_or_all("microphone-codec", opts.microphone_codec, encoders) 

107 if not self.speaker_codecs: 

108 self.speaker_allowed = False 

109 if not self.microphone_codecs: 

110 self.microphone_allowed = False 

111 self.speaker_enabled = self.speaker_allowed and sound_option(opts.speaker)=="on" 

112 self.microphone_enabled = self.microphone_allowed and opts.microphone.lower()=="on" 

113 log("speaker: codecs=%s, allowed=%s, enabled=%s", encoders, self.speaker_allowed, csv(self.speaker_codecs)) 

114 log("microphone: codecs=%s, allowed=%s, enabled=%s, default device=%s", 

115 decoders, self.microphone_allowed, csv(self.microphone_codecs), self.microphone_device) 

116 log("av-sync=%s", self.av_sync) 

117 if POSIX and not OSX: 

118 try: 

119 from xpra.sound.pulseaudio.pulseaudio_util import get_info as get_pa_info 

120 pa_info = get_pa_info() 

121 log("pulseaudio info=%s", pa_info) 

122 self.sound_properties.update(pa_info) 

123 except ImportError as e: 

124 log.warn("Warning: no pulseaudio information available") 

125 log.warn(" %s", e) 

126 except Exception: 

127 log.error("failed to add pulseaudio info", exc_info=True) 

128 #audio tagging: 

129 self.init_audio_tagging(opts.tray_icon) 

130 

131 

132 def cleanup(self): 

133 self.stop_all_sound() 

134 

135 

136 def stop_all_sound(self): 

137 if self.sound_source: 

138 self.stop_sending_sound() 

139 if self.sound_sink: 

140 self.stop_receiving_sound() 

141 

142 

143 def get_info(self) -> dict: 

144 info = { 

145 "speaker" : self.speaker_enabled, 

146 "microphone" : self.microphone_enabled, 

147 "properties" : dict(self.sound_properties), 

148 } 

149 ss = self.sound_source 

150 if ss: 

151 info["src"] = ss.get_info() 

152 ss = self.sound_sink 

153 if ss: 

154 info["sink"] = ss.get_info() 

155 return {"audio" : info} 

156 

157 

158 def get_caps(self) -> dict: 

159 d = {} 

160 updict(d, "av-sync", self.get_avsync_capabilities()) 

161 updict(d, "sound", self.get_audio_capabilities()) 

162 return d 

163 

164 def get_audio_capabilities(self) -> dict: 

165 if not self.sound_properties: 

166 return {} 

167 #we don't know if the server supports new codec names, 

168 #so always add legacy names in hello: 

169 caps = { 

170 "codec-full-names" : True, 

171 "decoders" : self.speaker_codecs, 

172 "encoders" : self.microphone_codecs, 

173 "send" : self.microphone_allowed, 

174 "receive" : self.speaker_allowed, 

175 } 

176 caps.update(self.sound_properties) 

177 log("audio capabilities: %s", caps) 

178 return caps 

179 

180 def get_avsync_capabilities(self) -> dict: 

181 if not self.av_sync: 

182 return {} 

183 return { 

184 "" : True, 

185 "delay.default" : max(0, DEFAULT_AV_SYNC_DELAY + AV_SYNC_DELTA), 

186 } 

187 

188 

189 def parse_server_capabilities(self, c : typedict) -> bool: 

190 self.server_av_sync = c.boolget("av-sync.enabled") 

191 avsynclog("av-sync: server=%s, client=%s", self.server_av_sync, self.av_sync) 

192 self.server_pulseaudio_id = c.strget("sound.pulseaudio.id") 

193 self.server_pulseaudio_server = c.strget("sound.pulseaudio.server") 

194 self.server_sound_decoders = c.strtupleget("sound.decoders") 

195 self.server_sound_encoders = c.strtupleget("sound.encoders") 

196 self.server_sound_receive = c.boolget("sound.receive") 

197 self.server_sound_send = c.boolget("sound.send") 

198 self.server_sound_bundle_metadata = c.boolget("sound.bundle-metadata") 

199 log("pulseaudio id=%s, server=%s, sound decoders=%s, sound encoders=%s, receive=%s, send=%s", 

200 self.server_pulseaudio_id, self.server_pulseaudio_server, 

201 csv(self.server_sound_decoders), csv(self.server_sound_encoders), 

202 self.server_sound_receive, self.server_sound_send) 

203 if self.server_sound_send and self.speaker_enabled: 

204 self.show_progress(90, "starting speaker forwarding") 

205 self.start_receiving_sound() 

206 if self.server_sound_receive and self.microphone_enabled: 

207 self.start_sending_sound() 

208 return True 

209 

210 

211 ###################################################################### 

212 # audio: 

213 def init_audio_tagging(self, tray_icon): 

214 if not POSIX: 

215 return 

216 try: 

217 from xpra import sound 

218 assert sound 

219 except ImportError: 

220 log("no sound module, skipping pulseaudio tagging setup") 

221 return 

222 try: 

223 from xpra.sound.pulseaudio.pulseaudio_util import set_icon_path 

224 tray_icon_filename = get_icon_filename(tray_icon or "xpra") 

225 set_icon_path(tray_icon_filename) 

226 except ImportError as e: 

227 if not OSX: 

228 log.warn("Warning: failed to set pulseaudio tagging icon:") 

229 log.warn(" %s", e) 

230 

231 

232 def get_matching_codecs(self, local_codecs, server_codecs): 

233 matching_codecs = tuple(x for x in local_codecs if x in server_codecs) 

234 log("get_matching_codecs(%s, %s)=%s", local_codecs, server_codecs, matching_codecs) 

235 return matching_codecs 

236 

237 def may_notify_audio(self, summary, body): 

238 #overriden in UI client subclass 

239 pass 

240 

241 def audio_loop_check(self, mode="speaker"): 

242 from xpra.sound.gstreamer_util import ALLOW_SOUND_LOOP, loop_warning_messages 

243 if ALLOW_SOUND_LOOP: 

244 return True 

245 if self._remote_machine_id: 

246 if self._remote_machine_id!=get_machine_id(): 

247 #not the same machine, so OK 

248 return True 

249 if self._remote_uuid!=get_user_uuid(): 

250 #different user, assume different pulseaudio server 

251 return True 

252 #check pulseaudio id if we have it 

253 pulseaudio_id = self.sound_properties.get("pulseaudio", {}).get("id") 

254 if not pulseaudio_id or not self.server_pulseaudio_id: 

255 #not available, assume no pulseaudio so no loop? 

256 return True 

257 if self.server_pulseaudio_id!=pulseaudio_id: 

258 #different pulseaudio server 

259 return True 

260 msgs = loop_warning_messages(mode) 

261 summary = msgs[0] 

262 body = "\n".join(msgs[1:]) 

263 self.may_notify_audio(summary, body) 

264 log.warn("Warning: %s", summary) 

265 for x in msgs[1:]: 

266 log.warn(" %s", x) 

267 return False 

268 

269 def no_matching_codec_error(self, forwarding="speaker", server_codecs=(), client_codecs=()): 

270 summary = "Failed to start %s forwarding" % forwarding 

271 body = "No matching codecs between client and server" 

272 self.may_notify_audio(summary, body) 

273 log.error("Error: %s", summary) 

274 log.error(" server supports: %s", csv(server_codecs)) 

275 log.error(" client supports: %s", csv(client_codecs)) 

276 

277 def start_sending_sound(self, device=None): 

278 """ (re)start a sound source and emit client signal """ 

279 log("start_sending_sound(%s)", device) 

280 enabled = False 

281 try: 

282 assert self.microphone_allowed, "microphone forwarding is disabled" 

283 assert self.server_sound_receive, "client support for receiving sound is disabled" 

284 if not self.audio_loop_check("microphone"): 

285 return 

286 ss = self.sound_source 

287 if ss: 

288 if ss.get_state()=="active": 

289 log.error("Error: microphone forwarding is already active") 

290 enabled = True 

291 return 

292 ss.start() 

293 else: 

294 enabled = self.start_sound_source(device) 

295 finally: 

296 if enabled!=self.microphone_enabled: 

297 self.microphone_enabled = enabled 

298 self.emit("microphone-changed") 

299 log("start_sending_sound(%s) done, microphone_enabled=%s", device, enabled) 

300 

301 def start_sound_source(self, device=None): 

302 log("start_sound_source(%s)", device) 

303 assert self.sound_source is None 

304 def sound_source_state_changed(*_args): 

305 self.emit("microphone-changed") 

306 #find the matching codecs: 

307 matching_codecs = self.get_matching_codecs(self.microphone_codecs, self.server_sound_decoders) 

308 log("start_sound_source(%s) matching codecs: %s", device, csv(matching_codecs)) 

309 if not matching_codecs: 

310 self.no_matching_codec_error("microphone", self.server_sound_decoders, self.microphone_codecs) 

311 return False 

312 try: 

313 from xpra.sound.wrapper import start_sending_sound 

314 plugins = self.sound_properties.get("plugins") 

315 ss = start_sending_sound(plugins, self.sound_source_plugin, device or self.microphone_device, 

316 None, 1.0, False, matching_codecs, 

317 self.server_pulseaudio_server, self.server_pulseaudio_id) 

318 if not ss: 

319 return False 

320 self.sound_source = ss 

321 ss.sequence = self.sound_source_sequence 

322 ss.connect("new-buffer", self.new_sound_buffer) 

323 ss.connect("state-changed", sound_source_state_changed) 

324 ss.connect("new-stream", self.new_stream) 

325 ss.start() 

326 log("start_sound_source(%s) sound source %s started", device, ss) 

327 return True 

328 except Exception as e: 

329 self.may_notify_audio("Failed to start microphone forwarding", "%s" % e) 

330 log.error("Error setting up microphone forwarding:") 

331 log.error(" %s", e) 

332 return False 

333 

334 def new_stream(self, sound_source, codec): 

335 log("new_stream(%s)", codec) 

336 if self.sound_source!=sound_source: 

337 log("dropping new-stream signal (current source=%s, signal source=%s)", self.sound_source, sound_source) 

338 return 

339 codec = codec or sound_source.codec 

340 sound_source.codec = codec 

341 #tell the server this is the start: 

342 self.send("sound-data", codec, "", 

343 { 

344 "start-of-stream" : True, 

345 "codec" : codec, 

346 }) 

347 

348 def stop_sending_sound(self): 

349 """ stop the sound source and emit client signal """ 

350 log("stop_sending_sound() sound source=%s", self.sound_source) 

351 ss = self.sound_source 

352 if self.microphone_enabled: 

353 self.microphone_enabled = False 

354 self.emit("microphone-changed") 

355 self.sound_source = None 

356 if ss is None: 

357 log.warn("Warning: cannot stop audio capture which has not been started") 

358 return 

359 #tell the server to stop: 

360 self.send("sound-data", ss.codec or "", "", { 

361 "end-of-stream" : True, 

362 "sequence" : ss.sequence, 

363 }) 

364 self.sound_source_sequence += 1 

365 ss.cleanup() 

366 

367 def start_receiving_sound(self): 

368 """ ask the server to start sending sound and emit the client signal """ 

369 log("start_receiving_sound() sound sink=%s", self.sound_sink) 

370 enabled = False 

371 try: 

372 if self.sound_sink is not None: 

373 log("start_receiving_sound: we already have a sound sink") 

374 enabled = True 

375 return 

376 if not self.server_sound_send: 

377 log.error("Error receiving sound: support not enabled on the server") 

378 return 

379 if not self.audio_loop_check("speaker"): 

380 return 

381 #choose a codec: 

382 matching_codecs = self.get_matching_codecs(self.speaker_codecs, self.server_sound_encoders) 

383 log("start_receiving_sound() matching codecs: %s", csv(matching_codecs)) 

384 if not matching_codecs: 

385 self.no_matching_codec_error("speaker", self.server_sound_encoders, self.speaker_codecs) 

386 return 

387 codec = matching_codecs[0] 

388 def sink_ready(*args): 

389 scodec = codec 

390 log("sink_ready(%s) codec=%s (server codec name=%s)", args, codec, scodec) 

391 self.send("sound-control", "start", scodec) 

392 return False 

393 self.on_sink_ready = sink_ready 

394 enabled = self.start_sound_sink(codec) 

395 finally: 

396 if self.speaker_enabled!=enabled: 

397 self.speaker_enabled = enabled 

398 self.emit("speaker-changed") 

399 log("start_receiving_sound() done, speaker_enabled=%s", enabled) 

400 

401 def stop_receiving_sound(self, tell_server=True): 

402 """ ask the server to stop sending sound, toggle flag so we ignore further packets and emit client signal """ 

403 log("stop_receiving_sound(%s) sound sink=%s", tell_server, self.sound_sink) 

404 ss = self.sound_sink 

405 if self.speaker_enabled: 

406 self.speaker_enabled = False 

407 self.emit("speaker-changed") 

408 if not ss: 

409 return 

410 if tell_server and ss.sequence==self.sound_sink_sequence: 

411 self.send("sound-control", "stop", self.sound_sink_sequence) 

412 self.sound_sink_sequence += 1 

413 self.send("sound-control", "new-sequence", self.sound_sink_sequence) 

414 self.sound_sink = None 

415 log("stop_receiving_sound(%s) calling %s", tell_server, ss.cleanup) 

416 ss.cleanup() 

417 log("stop_receiving_sound(%s) done", tell_server) 

418 

419 def sound_sink_state_changed(self, sound_sink, state): 

420 if sound_sink!=self.sound_sink: 

421 log("sound_sink_state_changed(%s, %s) not the current sink, ignoring it", sound_sink, state) 

422 return 

423 log("sound_sink_state_changed(%s, %s) on_sink_ready=%s", sound_sink, state, self.on_sink_ready) 

424 if state==b"ready" and self.on_sink_ready: 

425 if not self.on_sink_ready(): 

426 self.on_sink_ready = None 

427 self.emit("speaker-changed") 

428 def sound_sink_bitrate_changed(self, sound_sink, bitrate): 

429 if sound_sink!=self.sound_sink: 

430 log("sound_sink_bitrate_changed(%s, %s) not the current sink, ignoring it", sound_sink, bitrate) 

431 return 

432 log("sound_sink_bitrate_changed(%s, %s)", sound_sink, bitrate) 

433 #not shown in the UI, so don't bother with emitting a signal: 

434 #self.emit("speaker-changed") 

435 def sound_sink_error(self, sound_sink, error): 

436 log("sound_sink_error(%s, %s) exit_code=%s, current sink=%s", sound_sink, error, self.exit_code, self.sound_sink) 

437 if self.exit_code is not None: 

438 #exiting 

439 return 

440 if sound_sink!=self.sound_sink: 

441 log("sound_sink_error(%s, %s) not the current sink, ignoring it", sound_sink, error) 

442 return 

443 estr = bytestostr(error).replace("gst-resource-error-quark: ", "") 

444 self.may_notify_audio("Speaker forwarding error", estr) 

445 log.warn("Error: stopping speaker:") 

446 log.warn(" %s", estr) 

447 self.stop_receiving_sound() 

448 def sound_process_stopped(self, sound_sink, *args): 

449 if self.exit_code is not None: 

450 #exiting 

451 return 

452 if sound_sink!=self.sound_sink: 

453 log("sound_process_stopped(%s, %s) not the current sink, ignoring it", sound_sink, args) 

454 return 

455 log.warn("Warning: the sound process has stopped") 

456 self.stop_receiving_sound() 

457 

458 def sound_sink_exit(self, sound_sink, *args): 

459 log("sound_sink_exit(%s, %s) sound_sink=%s", sound_sink, args, self.sound_sink) 

460 if self.exit_code is not None: 

461 #exiting 

462 return 

463 ss = self.sound_sink 

464 if sound_sink!=ss: 

465 log("sound_sink_exit() not the current sink, ignoring it") 

466 return 

467 if ss and ss.codec: 

468 #the mandatory "I've been naughty warning": 

469 #we use the "codec" field as guard to ensure we only print this warning once.. 

470 log.warn("Warning: the %s sound sink has stopped", ss.codec) 

471 ss.codec = "" 

472 self.stop_receiving_sound() 

473 

474 def start_sound_sink(self, codec): 

475 log("start_sound_sink(%s)", codec) 

476 assert self.sound_sink is None, "sound sink already exists!" 

477 try: 

478 log("starting %s sound sink", codec) 

479 from xpra.sound.wrapper import start_receiving_sound 

480 ss = start_receiving_sound(codec) 

481 if not ss: 

482 return False 

483 ss.sequence = self.sound_sink_sequence 

484 self.sound_sink = ss 

485 ss.connect("state-changed", self.sound_sink_state_changed) 

486 ss.connect("error", self.sound_sink_error) 

487 ss.connect("exit", self.sound_sink_exit) 

488 from xpra.net.protocol import Protocol 

489 ss.connect(Protocol.CONNECTION_LOST, self.sound_process_stopped) 

490 ss.start() 

491 log("%s sound sink started", codec) 

492 return True 

493 except Exception as e: 

494 log.error("Error: failed to start sound sink", exc_info=True) 

495 self.sound_sink_error(self.sound_sink, e) 

496 return False 

497 

498 def new_sound_buffer(self, sound_source, data, metadata, packet_metadata=()): 

499 log("new_sound_buffer(%s, %s, %s, %s)", sound_source, len(data or ()), metadata, packet_metadata) 

500 if sound_source.sequence<self.sound_source_sequence: 

501 log("sound buffer dropped: old sequence number: %s (current is %s)", 

502 sound_source.sequence, self.sound_source_sequence) 

503 return 

504 self.sound_out_bytecount += len(data) 

505 for x in packet_metadata: 

506 self.sound_out_bytecount += len(x) 

507 metadata["sequence"] = sound_source.sequence 

508 if packet_metadata: 

509 if not self.server_sound_bundle_metadata: 

510 #server does not support bundling, send packet metadata as individual packets before the main packet: 

511 for x in packet_metadata: 

512 self.send_sound_data(sound_source, x, metadata) 

513 packet_metadata = () 

514 else: 

515 #the packet metadata is compressed already: 

516 packet_metadata = Compressed("packet metadata", packet_metadata, can_inline=True) 

517 self.send_sound_data(sound_source, data, metadata, packet_metadata) 

518 

519 def send_sound_data(self, sound_source, data, metadata, packet_metadata=None): 

520 codec = sound_source.codec 

521 packet_data = [codec, Compressed(codec, data), metadata] 

522 if packet_metadata: 

523 assert self.server_sound_bundle_metadata 

524 packet_data.append(packet_metadata) 

525 self.send("sound-data", *packet_data) 

526 

527 def send_sound_sync(self, v): 

528 self.send("sound-control", "sync", v) 

529 

530 

531 ###################################################################### 

532 #packet handlers 

533 def _process_sound_data(self, packet): 

534 codec, data, metadata = packet[1:4] 

535 codec = bytestostr(codec) 

536 metadata = typedict(metadata) 

537 if data: 

538 self.sound_in_bytecount += len(data) 

539 #verify sequence number if present: 

540 seq = metadata.intget("sequence", -1) 

541 if self.sound_sink_sequence>0 and 0<=seq<self.sound_sink_sequence: 

542 log("ignoring sound data with old sequence number %s (now on %s)", seq, self.sound_sink_sequence) 

543 return 

544 

545 if not self.speaker_enabled: 

546 if metadata.boolget("start-of-stream"): 

547 #server is asking us to start playing sound 

548 if not self.speaker_allowed: 

549 #no can do! 

550 log.warn("Warning: cannot honour the request to start the speaker") 

551 log.warn(" speaker forwarding is disabled") 

552 self.stop_receiving_sound(True) 

553 return 

554 self.speaker_enabled = True 

555 self.emit("speaker-changed") 

556 self.on_sink_ready = None 

557 codec = metadata.strget("codec") 

558 log("starting speaker on server request using codec %s", codec) 

559 self.start_sound_sink(codec) 

560 else: 

561 log("speaker is now disabled - dropping packet") 

562 return 

563 ss = self.sound_sink 

564 if ss is None: 

565 log("no sound sink to process sound data, dropping it") 

566 return 

567 if metadata.boolget("end-of-stream"): 

568 log("server sent end-of-stream for sequence %s, closing sound pipeline", seq) 

569 self.stop_receiving_sound(False) 

570 return 

571 if codec!=ss.codec: 

572 log.error("Error: sound codec change is not supported!") 

573 log.error(" stream tried to switch from %s to %s", ss.codec, codec) 

574 self.stop_receiving_sound() 

575 return 

576 if ss.get_state()=="stopped": 

577 log("sound data received, sound sink is stopped - telling server to stop") 

578 self.stop_receiving_sound() 

579 return 

580 #the server may send packet_metadata, which is pushed before the actual sound data: 

581 packet_metadata = () 

582 if len(packet)>4: 

583 packet_metadata = packet[4] 

584 if not self.sound_properties.get("bundle-metadata"): 

585 #we don't handle bundling, so push individually: 

586 for x in packet_metadata: 

587 ss.add_data(x) 

588 packet_metadata = () 

589 #(some packets (ie: sos, eos) only contain metadata) 

590 if data or packet_metadata: 

591 ss.add_data(data, metadata, packet_metadata) 

592 if self.av_sync and self.server_av_sync: 

593 qinfo = typedict(ss.get_info()).dictget("queue") 

594 queue_used = typedict(qinfo or {}).intget("cur", None) 

595 if queue_used is None: 

596 return 

597 delta = (self.queue_used_sent or 0)-queue_used 

598 #avsynclog("server sound sync: queue info=%s, last sent=%s, delta=%s", 

599 # dict((k,v) for (k,v) in info.items() if k.startswith("queue")), self.queue_used_sent, delta) 

600 if self.queue_used_sent is None or abs(delta)>=DELTA_THRESHOLD: 

601 avsynclog("server sound sync: sending updated queue.used=%i (was %s)", 

602 queue_used, (self.queue_used_sent or "unset")) 

603 self.queue_used_sent = queue_used 

604 v = queue_used + self.av_sync_delta 

605 if self.av_sync_delta: 

606 avsynclog(" adjusted value=%i with sync delta=%i", v, self.av_sync_delta) 

607 self.send_sound_sync(v) 

608 

609 

610 def init_authenticated_packet_handlers(self): 

611 log("init_authenticated_packet_handlers()") 

612 #these handlers can run directly from the network thread: 

613 self.add_packet_handler("sound-data", self._process_sound_data, False)