Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
* detect sound loops using the pulseaudio cookie if we don't have the server id (often not available if we're setting pulseaudio=no)
* make sure we run the loop detection code whenever we start microphone or speaker forwarding, both client side and server side
* if we find a loop is likely, use the notifications system to warn the user
* fix some bugs with unflattened dicts sent in hello packets
* init pulseaudio attributes early in UI client's init_sound method

git-svn-id: https://xpra.org/svn/Xpra/trunk@18468 3bb7dfac-3a0b-4e04-842a-767bc560f471
  • Loading branch information
totaam committed Feb 18, 2018
1 parent 3f2dcc5 commit c8fce11
Show file tree
Hide file tree
Showing 6 changed files with 158 additions and 63 deletions.
70 changes: 49 additions & 21 deletions src/xpra/client/ui_client_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1609,12 +1609,7 @@ def make_hello(self):
"send" : self.microphone_allowed,
"receive" : self.speaker_allowed,
})
try:
from xpra.sound.pulseaudio.pulseaudio_util import get_info as get_pa_info
sound_caps.update(get_pa_info())
except Exception:
pass
updict(capabilities, "sound", sound_caps)
updict(capabilities, "sound", sound_caps, flatten_dicts=True)
soundlog("sound capabilities: %s", sound_caps)
#batch options:
for bprop in ("always", "min_delay", "max_delay", "delay", "max_events", "max_pixels", "time_unit"):
Expand Down Expand Up @@ -2240,6 +2235,8 @@ def send_lock_enabled(self):
self.send("lock-toggle", self.client_lock)


######################################################################
# webcam:
def init_webcam(self, opts):
self.webcam_option = opts.webcam
self.webcam_forwarding = self.webcam_option.lower() not in FALSE_OPTIONS
Expand Down Expand Up @@ -2471,7 +2468,8 @@ def send_webcam_frame(self):
finally:
self.webcam_lock.release()


######################################################################
# audio:
def init_audio_tagging(self, tray_icon):
if not POSIX:
return
Expand Down Expand Up @@ -2534,6 +2532,18 @@ def vinfo(k):
soundlog("speaker: codecs=%s, allowed=%s, enabled=%s", encoders, self.speaker_allowed, csv(self.speaker_codecs))
soundlog("microphone: codecs=%s, allowed=%s, enabled=%s, default device=%s", decoders, self.microphone_allowed, csv(self.microphone_codecs), self.microphone_device)
soundlog("av-sync=%s", self.av_sync)
if POSIX:
try:
from xpra.sound.pulseaudio.pulseaudio_util import get_info as get_pa_info
pa_info = get_pa_info()
soundlog("pulseaudio info=%s", pa_info)
self.sound_properties.update(pa_info)
except ImportError as e:
soundlog.warn("Warning: no pulseaudio information available")
soundlog.warn(" %s", e)
except Exception:
soundlog.error("failed to add pulseaudio info", exc_info=True)


def get_matching_codecs(self, local_codecs, server_codecs):
matching_codecs = [x for x in local_codecs if x in server_codecs]
Expand All @@ -2548,6 +2558,34 @@ def may_notify_audio(self, summary, body):
else:
self.may_notify(XPRA_AUDIO_NOTIFICATION_ID, summary, body, icon_name="audio")

def audio_loop_check(self, mode="speaker"):
from xpra.sound.gstreamer_util import ALLOW_SOUND_LOOP, loop_warning_messages
if ALLOW_SOUND_LOOP:
return True
if self._remote_machine_id:
if self._remote_machine_id!=get_machine_id():
#not the same machine, so OK
return True
if self._remote_uuid!=get_user_uuid():
#different user, assume different pulseaudio server
return True
#check pulseaudio id if we have it
pulseaudio_id = self.sound_properties.get("pulseaudio", {}).get("id")
if not pulseaudio_id or not self.server_pulseaudio_id:
#not available, assume no pulseaudio so no loop?
return True
if self.server_pulseaudio_id!=pulseaudio_id:
#different pulseaudio server
return True
msgs = loop_warning_messages(mode)
summary = msgs[0]
body = "\n".join(msgs[1:])
self.may_notify_audio(summary, body)
log.warn("Warning: %s", summary)
for x in msgs[1:]:
log.warn(" %s", x)
return False

def no_matching_codec_error(self, forwarding="speaker", server_codecs=[], client_codecs=[]):
summary = "Failed to start %s forwarding" % forwarding
body = "No matching codecs between client and server"
Expand All @@ -2563,12 +2601,8 @@ def start_sending_sound(self, device=None):
try:
assert self.microphone_allowed, "microphone forwarding is disabled"
assert self.server_sound_receive, "client support for receiving sound is disabled"
from xpra.sound.gstreamer_util import ALLOW_SOUND_LOOP, loop_warning
if self._remote_machine_id and self._remote_machine_id==get_machine_id() and not ALLOW_SOUND_LOOP:
#looks like we're on the same machine, verify it's a different user:
if self._remote_uuid==get_user_uuid():
loop_warning("microphone", self._remote_uuid)
return
if not self.audio_loop_check("microphone"):
return
ss = self.sound_source
if ss:
if ss.get_state()=="active":
Expand Down Expand Up @@ -2657,6 +2691,8 @@ def start_receiving_sound(self):
elif not self.server_sound_send:
log.error("Error receiving sound: support not enabled on the server")
return
if not self.audio_loop_check("speaker"):
return
#choose a codec:
matching_codecs = self.get_matching_codecs(self.speaker_codecs, self.server_sound_encoders)
soundlog("start_receiving_sound() matching codecs: %s", csv(matching_codecs))
Expand Down Expand Up @@ -3145,14 +3181,6 @@ def deiconify_windows(self):
window.deiconify()


def reinit_window_icons(self):
#make sure the window icons are the ones we want:
iconlog("reinit_window_icons()")
for window in self._id_to_window.values():
reset_icon = getattr(window, "reset_icon", None)
if reset_icon:
reset_icon()

def reinit_windows(self, new_size_fn=None):
def fake_send(*args):
log("fake_send%s", args)
Expand Down
122 changes: 88 additions & 34 deletions src/xpra/server/source.py
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,7 @@ def init_vars(self):
self.wants_default_cursor = False
#sound props:
self.pulseaudio_id = None
self.pulseaudio_cookie_hash = None
self.pulseaudio_server = None
self.sound_decoders = ()
self.sound_encoders = ()
Expand Down Expand Up @@ -872,6 +873,7 @@ def parse_batch_int(value, varname):

#sound stuff:
self.pulseaudio_id = c.strget("sound.pulseaudio.id")
self.pulseaudio_cookie_hash = c.strget("sound.pulseaudio.cookie-hash")
self.pulseaudio_server = c.strget("sound.pulseaudio.server")
self.codec_full_names = c.boolget("sound.codec-full-names")
try:
Expand All @@ -889,8 +891,8 @@ def conv(v):
self.sound_bundle_metadata = c.boolget("sound.bundle-metadata")
av_sync = c.boolget("av-sync")
self.set_av_sync_delay(int(self.av_sync and av_sync) * c.intget("av-sync.delay.default", 150))
soundlog("pulseaudio id=%s, server=%s, full-names=%s, sound decoders=%s, sound encoders=%s, receive=%s, send=%s",
self.pulseaudio_id, self.pulseaudio_server, self.codec_full_names, self.sound_decoders, self.sound_encoders, self.sound_receive, self.sound_send)
soundlog("pulseaudio id=%s, cookie-hash=%s, server=%s, full-names=%s, sound decoders=%s, sound encoders=%s, receive=%s, send=%s",
self.pulseaudio_id, self.pulseaudio_cookie_hash, self.pulseaudio_server, self.codec_full_names, self.sound_decoders, self.sound_encoders, self.sound_receive, self.sound_send)
avsynclog("av-sync: server=%s, client=%s, total=%s", self.av_sync, av_sync, self.av_sync_delay_total)
log("cursors=%s (encodings=%s), bell=%s, notifications=%s", self.send_cursors, self.cursor_encodings, self.send_bell, self.send_notifications)
log("client uuid %s", self.uuid)
Expand Down Expand Up @@ -1112,44 +1114,84 @@ def startup_complete(self):
log("startup_complete()")
self.send("startup-complete")


######################################################################
# audio:
def audio_loop_check(self, mode="speaker"):
from xpra.sound.gstreamer_util import ALLOW_SOUND_LOOP, loop_warning_messages
if ALLOW_SOUND_LOOP:
return True
if self.machine_id:
if self.machine_id!=get_machine_id():
#not the same machine, so OK
return True
if self.uuid!=get_user_uuid():
#different user, assume different pulseaudio server
return True
#check pulseaudio id if we have it
pulseaudio_id = self.sound_properties.get("pulseaudio", {}).get("id")
pulseaudio_cookie_hash = self.sound_properties.get("pulseaudio", {}).get("cookie-hash")
if pulseaudio_id and self.pulseaudio_id:
if self.pulseaudio_id!=pulseaudio_id:
return True
elif pulseaudio_cookie_hash and self.pulseaudio_cookie_hash:
if self.pulseaudio_cookie_hash!=pulseaudio_cookie_hash:
return True
else:
#no cookie or id, so probably not a pulseaudio setup,
#hope for the best:
return True
msgs = loop_warning_messages(mode)
summary = msgs[0]
body = "\n".join(msgs[1:])
try:
from xpra.platform.paths import get_icon_filename
from xpra.notifications.common import XPRA_AUDIO_NOTIFICATION_ID, parse_image_path
except ImportError as e:
notifylog("audio_loop_warning(%s) %s", mode, e)
else:
nid = XPRA_AUDIO_NOTIFICATION_ID
icon = parse_image_path(get_icon_filename(mode))
self.notify("", nid, "Xpra", 0, "", summary, body, [], {}, 10*1000, icon)
log.warn("Warning: %s", summary)
for x in msgs[1:]:
log.warn(" %s", x)
return False

def start_sending_sound(self, codec=None, volume=1.0, new_stream=None, new_buffer=None, skip_client_codec_check=False):
assert self.hello_sent
soundlog("start_sending_sound(%s)", codec)
if self.suspended:
soundlog.warn("Warning: not starting sound whilst in suspended state")
return None
if not self.supports_speaker:
soundlog.error("Error sending sound: support not enabled on the server")
return None
if self.sound_source:
soundlog.error("Error sending sound: forwarding already in progress")
return None
if not self.sound_receive:
soundlog.error("Error sending sound: support is not enabled on the client")
return None
if not self.codec_full_names:
codec = NEW_CODEC_NAMES.get(codec, codec)
if codec is None:
codecs = [x for x in self.sound_decoders if x in self.speaker_codecs]
if not codecs:
soundlog.error("Error sending sound: no codecs in common")
return None
codec = codecs[0]
elif codec not in self.speaker_codecs:
soundlog.warn("Warning: invalid codec specified: %s", codec)
return None
elif (codec not in self.sound_decoders) and not skip_client_codec_check:
soundlog.warn("Error sending sound: invalid codec '%s'", codec)
soundlog.warn(" is not in the list of decoders supported by the client: %s", csv(self.sound_decoders))
return None
ss = None
try:
from xpra.sound.gstreamer_util import ALLOW_SOUND_LOOP, loop_warning
if self.machine_id and self.machine_id==get_machine_id() and not ALLOW_SOUND_LOOP:
#looks like we're on the same machine, verify it's a different user:
if self.uuid==get_user_uuid():
loop_warning("speaker", self.uuid)
if self.suspended:
soundlog.warn("Warning: not starting sound whilst in suspended state")
return None
if not self.supports_speaker:
soundlog.error("Error sending sound: support not enabled on the server")
return None
if self.sound_source:
soundlog.error("Error sending sound: forwarding already in progress")
return None
if not self.sound_receive:
soundlog.error("Error sending sound: support is not enabled on the client")
return None
if not self.codec_full_names:
codec = NEW_CODEC_NAMES.get(codec, codec)
if codec is None:
codecs = [x for x in self.sound_decoders if x in self.speaker_codecs]
if not codecs:
soundlog.error("Error sending sound: no codecs in common")
return None
codec = codecs[0]
elif codec not in self.speaker_codecs:
soundlog.warn("Warning: invalid codec specified: %s", codec)
return None
elif (codec not in self.sound_decoders) and not skip_client_codec_check:
soundlog.warn("Error sending sound: invalid codec '%s'", codec)
soundlog.warn(" is not in the list of decoders supported by the client: %s", csv(self.sound_decoders))
return None
if not self.audio_loop_check("speaker"):
return None
from xpra.sound.wrapper import start_sending_sound
plugins = self.sound_properties.strlistget("plugins", [])
ss = start_sending_sound(plugins, self.sound_source_plugin, None, codec, volume, True, [codec], self.pulseaudio_server, self.pulseaudio_id)
Expand Down Expand Up @@ -1396,6 +1438,16 @@ def sound_data(self, codec, data, metadata, packet_metadata=()):
self.stop_receiving_sound()
return
if not self.sound_sink:
if not self.audio_loop_check("microphone"):
#make a fake object so we don't fire the audio loop check warning repeatedly
from xpra.util import AdHocStruct
self.sound_sink = AdHocStruct()
self.sound_sink.codec = codec
def noop(*args):
pass
self.sound_sink.add_data = noop
self.sound_sink.cleanup = noop
return
try:
def sound_sink_error(*args):
soundlog("sound_sink_error%s", args)
Expand All @@ -1422,6 +1474,8 @@ def sound_sink_error(*args):
self.sound_sink.add_data(data, metadata, packet_metadata)


######################################################################
# a/v sync:
def set_av_sync_delta(self, delta):
avsynclog("set_av_sync_delta(%i)", delta)
self.av_sync_delta = delta
Expand Down
16 changes: 8 additions & 8 deletions src/xpra/sound/gstreamer_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -720,7 +720,7 @@ def get_pulse_device(device_name_match=None, want_monitor_device=True, input_or_
#default to first one:
device, device_name = tuple(devices.items())[0]
log.info("using pulseaudio device:")
log.info(" '%s'", device_name)
log.info(" '%s'", bytestostr(device_name))
return device

def get_pulse_source_defaults(device_name_match=None, want_monitor_device=True, remote=None):
Expand Down Expand Up @@ -853,13 +853,13 @@ def parse_sound_source(all_plugins, sound_source_plugin, device, want_monitor_de
return gst_sound_source_plugin, options


def loop_warning(mode="speaker", machine_id=""):
log.warn("Warning: cannot start %s forwarding:", mode)
log.warn(" user and server environment are identical,")
log.warn(" this would create a sound loop")
log.warn(" use XPRA_ALLOW_SOUND_LOOP=1 to force enable it")
if machine_id:
log(" '%s'", machine_id)
def loop_warning_messages(mode="speaker"):
return [
"Cannot start %s forwarding:" % mode,
"client and server environment are identical,",
"this would be likely to create an audio feedback loop",
#" use XPRA_ALLOW_SOUND_LOOP=1 to force enable it",
]


def main():
Expand Down
3 changes: 3 additions & 0 deletions src/xpra/sound/pulseaudio/pulseaudio_none_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ def get_pulse_server():
def get_pulse_id():
return ""

def get_pulse_cookie_hash():
return ""

def get_pactl_server():
return ""

Expand Down
9 changes: 9 additions & 0 deletions src/xpra/sound/pulseaudio/pulseaudio_pactl_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,14 @@ def get_default_sink():
def get_pactl_server():
return get_pactl_info_line(b"Server String:")

def get_pulse_cookie_hash():
v = get_pactl_info_line(b"Cookie:")
try:
import hashlib
return strtobytes(hashlib.sha256(v).hexdigest())
except:
pass
return ""

def get_pulse_server(may_start_it=True):
xp = get_pulse_server_x11_property()
Expand Down Expand Up @@ -185,6 +193,7 @@ def get_info():
"found" : bool(has_pa()),
"id" : get_pulse_id(),
"server" : get_pulse_server(False),
"cookie-hash" : get_pulse_cookie_hash(),
}
}
log("pulseaudio_pactl_util.get_info()=%s", info)
Expand Down
1 change: 1 addition & 0 deletions src/xpra/sound/pulseaudio/pulseaudio_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ def add_audio_tagging_env(env_dict=os.environ, icon_path=None):
get_default_sink = _pulseaudio_util.get_default_sink
get_pulse_server = _pulseaudio_util.get_pulse_server
get_pulse_id = _pulseaudio_util.get_pulse_id
get_pulse_cookie_hash = _pulseaudio_util.get_pulse_cookie_hash
get_pactl_server = _pulseaudio_util.get_pactl_server
set_source_mute = _pulseaudio_util.set_source_mute
set_sink_mute = _pulseaudio_util.set_sink_mute
Expand Down

0 comments on commit c8fce11

Please sign in to comment.