Coverage for /home/antoine/projects/xpra-git/dist/python3/lib64/python/xpra/client/mixins/audio.py : 53%
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.
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
14avsynclog = Logger("av-sync")
15log = Logger("client", "sound")
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)
22"""
23Utility superclass for clients that handle audio
24"""
25class AudioClient(StubClientMixin):
26 __signals__ = ["speaker-changed", "microphone-changed"]
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
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)
132 def cleanup(self):
133 self.stop_all_sound()
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()
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}
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
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
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 }
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
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)
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
237 def may_notify_audio(self, summary, body):
238 #overriden in UI client subclass
239 pass
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
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))
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)
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
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 })
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()
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)
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)
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()
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()
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
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)
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)
527 def send_sound_sync(self, v):
528 self.send("sound-control", "sync", v)
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
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)
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)