class XRootPropWatcher(gobject.GObject): __gsignals__ = { "root-prop-changed": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ( gobject.TYPE_STRING, gobject.TYPE_STRING, )), "xpra-property-notify-event": n_arg_signal(1), } def __init__(self, props): gobject.GObject.__init__(self) self._props = props self._root = gtk.gdk.get_default_root_window() add_event_receiver(self._root, self) def do_xpra_property_notify_event(self, event): log("XRootPropWatcher.do_xpra_property_notify_event(%s) props=%s", event, self._props) if event.atom in self._props: self._notify(event.atom) def _notify(self, prop): v = prop_get(gtk.gdk.get_default_root_window(), prop, "latin1", ignore_errors=True) self.emit("root-prop-changed", prop, str(v)) def notify_all(self): for prop in self._props: self._notify(prop)
class XRootPropWatcher(gobject.GObject): __gsignals__ = { "root-prop-changed": (SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING, )), "xpra-property-notify-event": n_arg_signal(1), } def __init__(self, props, root_window): gobject.GObject.__init__(self) self._props = props self._root = root_window self._saved_event_mask = self._root.get_events() self._root.set_events(self._saved_event_mask | PROPERTY_CHANGE_MASK) add_event_receiver(self._root, self) def cleanup(self): #this must be called from the UI thread! remove_event_receiver(self._root, self) self._root.set_events(self._saved_event_mask) def do_xpra_property_notify_event(self, event): log("XRootPropWatcher.do_xpra_property_notify_event(%s)", event) if event.atom in self._props: self.do_notify(str(event.atom)) def do_notify(self, prop): log("XRootPropWatcher.do_notify(%s)", prop) self.emit("root-prop-changed", prop) def notify_all(self): for prop in self._props: self.do_notify(prop)
class XRootPropWatcher(gobject.GObject): __gsignals__ = { "root-prop-changed": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING, )), "xpra-property-notify-event": n_arg_signal(1), } def __init__(self, props): gobject.GObject.__init__(self) self._props = props self._root = gtk.gdk.get_default_root_window() self._saved_event_mask = self._root.get_events() self._root.set_events(self._saved_event_mask | gtk.gdk.PROPERTY_CHANGE_MASK) self._own_x11_filter = init_x11_filter() add_event_receiver(self._root, self) def cleanup(self): #this must be called from the UI thread! remove_event_receiver(self._root, self) self._root.set_events(self._saved_event_mask) if self._own_x11_filter: #only remove the x11 filter if we initialized it (ie: when running in client) try: trap.call_synced(cleanup_x11_filter) except Exception, e: log.error("failed to remove x11 event filter: %s", e) try: trap.call_synced(cleanup_all_event_receivers) except Exception, e: log.error("failed to remove event receivers: %s", e)
class XRootPropWatcher(gobject.GObject): __gsignals__ = { "root-prop-changed": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING, )), "xpra-property-notify-event": n_arg_signal(1), } def __init__(self, props): gobject.GObject.__init__(self) self._props = props self._root = gtk.gdk.get_default_root_window() self._saved_event_mask = self._root.get_events() self._root.set_events(self._saved_event_mask | gtk.gdk.PROPERTY_CHANGE_MASK) init_x11_filter() add_event_receiver(self._root, self) def cleanup(self): remove_event_receiver(self._root, self) self._root.set_events(self._saved_event_mask) def do_xpra_property_notify_event(self, event): log("XRootPropWatcher.do_xpra_property_notify_event(%s)", event) if event.atom in self._props: self.do_notify(event.atom) def do_notify(self, prop): log("XRootPropWatcher.do_notify(%s)", prop) self.emit("root-prop-changed", prop) def notify_all(self): for prop in self._props: self.do_notify(prop)
class XRootPropWatcher(gobject.GObject): __gsignals__ = { "root-prop-changed": (SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING, )), "xpra-property-notify-event": n_arg_signal(1), } def __init__(self, props): gobject.GObject.__init__(self) self._props = props self._root = gtk.gdk.get_default_root_window() self._saved_event_mask = self._root.get_events() self._root.set_events(self._saved_event_mask | gtk.gdk.PROPERTY_CHANGE_MASK) add_event_receiver(self._root, self) def cleanup(self): #this must be called from the UI thread! remove_event_receiver(self._root, self) self._root.set_events(self._saved_event_mask) #try a few times: #errors happen because windows are being destroyed #(even more so when we cleanup) #and we don't really care too much about this for l in (log, log, log, log, log.warn): try: with xsync: cleanup_all_event_receivers() #all went well, we're done return except Exception as e: l("failed to remove event receivers: %s", e) def do_xpra_property_notify_event(self, event): log("XRootPropWatcher.do_xpra_property_notify_event(%s)", event) if event.atom in self._props: self.do_notify(event.atom) def do_notify(self, prop): log("XRootPropWatcher.do_notify(%s)", prop) self.emit("root-prop-changed", prop) def notify_all(self): for prop in self._props: self.do_notify(prop)
class GTKClipboardProxy(ClipboardProxyCore, GObject.GObject): __gsignals__ = { "send-clipboard-token": one_arg_signal, "send-clipboard-request": n_arg_signal(2), } def __init__(self, selection="CLIPBOARD"): ClipboardProxyCore.__init__(self, selection) GObject.GObject.__init__(self) self._block_owner_change = monotonic() self._want_targets = False self.clipboard = Gtk.Clipboard.get(Gdk.Atom.intern(selection, False)) self.clipboard.connect("owner-change", self.owner_change) def __repr__(self): return "GTKClipboardProxy(%s)" % self._selection def got_token(self, targets, target_data=None, claim=True, synchronous_client=False): # the remote end now owns the clipboard self.cancel_emit_token() if not self._enabled: return self._got_token_events += 1 log( "got token, selection=%s, targets=%s, target data=%s, claim=%s, synchronous_client=%s, can-receive=%s", self._selection, targets, target_data, claim, synchronous_client, self._can_receive) if claim: self._have_token = True if not self._can_receive: return if target_data and claim: targets = target_data.keys() text_targets = tuple(x for x in targets if x in TEXT_TARGETS) for text_target in text_targets: dtype, dformat, data = target_data.get(text_target) if dformat != 8: continue text = net_utf8(data) log("setting text data %s / %s of size %i: %s", dtype, dformat, len(text), ellipsizer(text)) self._block_owner_change = monotonic() self.clipboard.set_text(text, len(text)) return #we should handle more datatypes here.. ############################################################################ # forward local requests to the remote clipboard: ############################################################################ def schedule_emit_token(self): def send_token(*token_data): self._have_token = False self.emit("send-clipboard-token", token_data) return if not (self._want_targets or self._greedy_client): send_token() return #we need the targets: targets = self.clipboard.wait_for_targets() if not targets: send_token() return if not self._greedy_client: send_token(targets) return #for now we only handle text targets: text_targets = tuple(x for x in targets if x in TEXT_TARGETS) if text_targets: text = self.clipboard.wait_for_text() if text: #should verify the target is actually utf8... text_target = text_targets[0] send_token(targets, (text_target, "UTF8_STRING", 8, text)) return send_token(text_targets) def owner_change(self, clipboard, event): log("owner_change(%s, %s) window=%s, selection=%s", clipboard, event, event.window, event.selection) self.do_owner_changed() def do_owner_changed(self): elapsed = monotonic() - self._block_owner_change log("do_owner_changed() enabled=%s, elapsed=%s", self._enabled, elapsed) if not self._enabled or elapsed < BLOCK_DELAY: return self.schedule_emit_token() def get_contents(self, target, got_contents, time=0): log("get_contents(%s, %s, %i) have-token=%s", target, got_contents, time, self._have_token) if target == "TARGETS": r = self.clipboard.wait_for_targets() if r and len(r) == 2 and r[0]: targets = r[1] atoms = tuple(x.name() for x in targets) got_contents("ATOM", 32, atoms) return elif target in TEXT_TARGETS: text = self.clipboard.wait_for_text() if text: got_contents(target, 8, text) return else: #data = wait_for_contents(target)? pass got_contents(target, 0, None)
class ClipboardProxy(ClipboardProxyCore, GObject.GObject): __gsignals__ = { "xpra-client-message-event": one_arg_signal, "xpra-selection-request": one_arg_signal, "xpra-selection-clear": one_arg_signal, "xpra-property-notify-event": one_arg_signal, "xpra-xfixes-selection-notify-event": one_arg_signal, # "send-clipboard-token": one_arg_signal, "send-clipboard-request": n_arg_signal(2), } def __init__(self, xid, selection="CLIPBOARD"): ClipboardProxyCore.__init__(self, selection) GObject.GObject.__init__(self) self.xid = xid self.owned = False self._want_targets = False self.remote_requests = {} self.local_requests = {} self.local_request_counter = 0 self.targets = () self.target_data = {} self.reset_incr_data() def __repr__(self): return "X11ClipboardProxy(%s)" % self._selection def cleanup(self): log("%s.cleanup()", self) #give up selection: #(disabled because this crashes GTK3 on exit) #if self.owned: # self.owned = False # with xswallow: # X11Window.XSetSelectionOwner(0, self._selection) #empty replies for all pending requests, #this will also cancel any pending timers: rr = self.remote_requests self.remote_requests = {} for target in rr: self.got_contents(target) lr = self.local_requests self.local_requests = {} for target in lr: self.got_local_contents(target) def got_token(self, targets, target_data=None, claim=True, synchronous_client=False): # the remote end now owns the clipboard self.cancel_emit_token() if not self._enabled: return self._got_token_events += 1 log( "got token, selection=%s, targets=%s, target data=%s, claim=%s, can-receive=%s", self._selection, targets, target_data, claim, self._can_receive) if claim: self._have_token = True if self._can_receive: self.targets = tuple(bytestostr(x) for x in (targets or ())) self.target_data = target_data or {} if targets and claim: xatoms = strings_to_xatoms(targets) self.got_contents("TARGETS", "ATOM", 32, xatoms) if target_data and synchronous_client and claim: targets = target_data.keys() text_targets = tuple(x for x in targets if x in TEXT_TARGETS) if text_targets: target = text_targets[0] dtype, dformat, data = target_data.get(target) dtype = bytestostr(dtype) self.got_contents(target, dtype, dformat, data) if self._can_receive and claim: self.claim() def claim(self): time = 0 try: with xsync: owner = X11Window.XGetSelectionOwner(self._selection) if owner == self.xid: log("claim() we already own the '%s' selection", self._selection) return setsel = X11Window.XSetSelectionOwner(self.xid, self._selection, time) owner = X11Window.XGetSelectionOwner(self._selection) self.owned = owner == self.xid log( "claim_selection: set selection owner returned %s, owner=%#x, owned=%s", setsel, owner, self.owned) event_mask = StructureNotifyMask if not self.owned: log.warn( "Warning: we failed to get ownership of the '%s' clipboard selection", self._selection) return #send announcement: log("claim_selection: sending message to root window") root = get_default_root_window() root_xid = root.get_xid() X11Window.sendClientMessage(root_xid, root_xid, False, event_mask, "MANAGER", time or CurrentTime, self._selection, self.xid) log("claim_selection: done, owned=%s", self.owned) except Exception: log("failed to claim selection '%s'", self._selection, exc_info=True) raise def do_xpra_client_message_event(self, event): if event.message_type == "_GTK_LOAD_ICONTHEMES": #ignore this crap return log.info("clipboard window %#x received an X11 message", event.window.get_xid()) log.info(" %s", event) def get_wintitle(self, xid): data = X11Window.XGetWindowProperty(xid, "WM_NAME", "STRING") if data: return data.decode("latin1") data = X11Window.XGetWindowProperty(xid, "_NET_WM_NAME", "STRING") if data: return data.decode("utf8") xid = X11Window.getParent(xid) return None def get_wininfo(self, xid): with xswallow: title = self.get_wintitle(xid) if title: return "'%s'" % title with xswallow: while xid: title = self.get_wintitle(xid) if title: return "child of '%s'" % title xid = X11Window.getParent(xid) return hex(xid) ############################################################################ # forward local requests to the remote clipboard: ############################################################################ def do_selection_request_event(self, event): #an app is requesting clipboard data from us log("do_selection_request_event(%s)", event) requestor = event.requestor if not requestor: log.warn( "Warning: clipboard selection request without a window, dropped" ) return wininfo = self.get_wininfo(requestor.get_xid()) prop = event.property target = str(event.target) log("clipboard request for %s from window %#x: %s, target=%s, prop=%s", self._selection, requestor.get_xid(), wininfo, target, prop) if not target: log.warn("Warning: ignoring clipboard request without a TARGET") log.warn(" coming from %s", wininfo) return if not prop: log.warn("Warning: ignoring clipboard request without a property") log.warn(" coming from %s", wininfo) return def nodata(): self.set_selection_response(requestor, target, prop, "STRING", 8, b"", time=event.time) if not self._enabled: nodata() return if wininfo and wininfo.strip("'") in BLACKLISTED_CLIPBOARD_CLIENTS: if first_time("clipboard-blacklisted:%s" % wininfo.strip("'")): log.warn( "receiving clipboard requests from blacklisted client %s", wininfo) log.warn(" all requests will be silently ignored") log("responding with nodata for blacklisted client '%s'", wininfo) return if not self.owned: log.warn("Warning: clipboard selection request received,") log.warn(" coming from %s", wininfo) log.warn(" but we don't own the selection,") log.warn(" sending an empty reply") nodata() return if not self._can_receive: log.warn("Warning: clipboard selection request received,") log.warn(" coming from %s", wininfo) log.warn(" but receiving remote data is disabled,") log.warn(" sending an empty reply") nodata() return if must_discard(target): log.info("clipboard %s rejecting request for invalid target '%s'", self._selection, target) log.info(" coming from %s", wininfo) nodata() return if target == "TARGETS": if self.targets: log("using existing TARGETS value as response: %s", self.targets) xatoms = strings_to_xatoms(self.targets) self.set_selection_response(requestor, target, prop, "ATOM", 32, xatoms, event.time) return if "TARGETS" not in self.remote_requests: self.emit("send-clipboard-request", self._selection, "TARGETS") #when appending, the time may not be honoured #and we may reply with data from an older request self.remote_requests.setdefault("TARGETS", []).append( (requestor, target, prop, event.time)) return req_target = target if self.targets and target not in self.targets: if first_time("client-%s-invalidtarget-%s" % (wininfo, target)): l = log.info else: l = log.debug l("client %s is requesting an unknown target: '%s'", wininfo, target) translated_targets = TRANSLATED_TARGETS.get(target, ()) can_translate = tuple(x for x in translated_targets if x in self.targets) if can_translate: req_target = can_translate[0] l(" using '%s' instead", req_target) else: l(" valid targets: %s", csv(self.targets)) if must_discard_extra(target): l(" dropping the request") nodata() return target_data = self.target_data.get(req_target) if target_data and self._have_token: #we have it already dtype, dformat, data = target_data dtype = bytestostr(dtype) log("setting target data for '%s': %s, %s, %s (%s)", target, dtype, dformat, ellipsizer(data), type(data)) self.set_selection_response(requestor, target, prop, dtype, dformat, data, event.time) return waiting = self.remote_requests.setdefault(req_target, []) if waiting: log("already waiting for '%s' remote request: %s", req_target, waiting) else: self.emit("send-clipboard-request", self._selection, req_target) waiting.append((requestor, target, prop, event.time)) def set_selection_response(self, requestor, target, prop, dtype, dformat, data, time=0): log("set_selection_response(%s, %s, %s, %s, %s, %r, %i)", requestor, target, prop, dtype, dformat, ellipsizer(data), time) #answer the selection request: try: xid = requestor.get_xid() if not prop: log.warn("Warning: cannot set clipboard response") log.warn(" property is unset for requestor %s", self.get_wininfo(xid)) return with xsync: if data is not None: if isinstance(data, str): #the data is already in the correct format, #but the cython bindings require real 'bytes' data = strtobytes(data) X11Window.XChangeProperty(xid, prop, dtype, dformat, data) else: #maybe even delete the property? #X11Window.XDeleteProperty(xid, prop) prop = None X11Window.sendSelectionNotify(xid, self._selection, target, prop, time) except XError as e: log("failed to set selection", exc_info=True) log.warn("Warning: failed to set selection for target '%s'", target) log.warn(" on requestor %s", self.get_wininfo(xid)) log.warn(" property '%s'", prop) log.warn(" %s", e) def got_contents(self, target, dtype=None, dformat=None, data=None): #if this is the special target 'TARGETS', cache the result: if target == "TARGETS" and dtype == "ATOM" and dformat == 32: self.targets = xatoms_to_strings(data) #the remote peer sent us a response, #find all the pending requests for this target #and give them the response they are waiting for: pending = self.remote_requests.pop(target, []) log("got_contents%s pending=%s", (target, dtype, dformat, ellipsizer(data)), csv(pending)) for requestor, actual_target, prop, time in pending: if log.is_debug_enabled(): log( "setting response %s as '%s' on property '%s' of window %s as %s", ellipsizer(data), actual_target, prop, self.get_wininfo(requestor.get_xid()), dtype) if actual_target != target and dtype == target: dtype = actual_target self.set_selection_response(requestor, actual_target, prop, dtype, dformat, data, time) ############################################################################ # local clipboard events, which may or may not be sent to the remote end ############################################################################ def do_selection_notify_event(self, event): owned = self.owned xid = 0 if event.owner: xid = event.owner.get_xid() self.owned = xid and xid == self.xid log( "do_selection_notify_event(%s) owned=%s, was %s (owner=%#x, xid=%#x), enabled=%s, can-send=%s", event, self.owned, owned, xid, self.xid, self._enabled, self._can_send) if not self._enabled: return if self.owned or not self._can_send or xid == 0: return self.do_owner_changed() self.schedule_emit_token() def schedule_emit_token(self): if not (self._want_targets or self._greedy_client): self._have_token = False self.emit("send-clipboard-token", ()) return #we need the targets, and the target data for greedy clients: def send_token_with_targets(): token_data = (self.targets, ) self._have_token = False self.emit("send-clipboard-token", token_data) def with_targets(targets): if not self._greedy_client: send_token_with_targets() return #find the preferred targets: targets = self.choose_targets(targets) if not targets: send_token_with_targets() return target = targets[0] def got_text_target(dtype, dformat, data): log("got_text_target(%s, %s, %s)", dtype, dformat, ellipsizer(data)) if not (dtype and dformat and data): send_token_with_targets() return token_data = (targets, (target, dtype, dformat, data)) self._have_token = False self.emit("send-clipboard-token", token_data) self.get_contents(target, got_text_target) if self.targets: with_targets(self.targets) return def got_targets(dtype, dformat, data): assert dtype == "ATOM" and dformat == 32 self.targets = xatoms_to_strings(data) log("got_targets: %s", self.targets) with_targets(self.targets) self.get_contents("TARGETS", got_targets) def choose_targets(self, targets): if self.preferred_targets: #prefer PNG, but only if supported by the client: fmts = [] for img_fmt in ("image/png", "image/jpeg"): if img_fmt in targets and img_fmt in self.preferred_targets: fmts.append(img_fmt) if fmts: return fmts #if we can't choose a text target, at least choose a supported one: if not any(x for x in targets if x in TEXT_TARGETS and x in self.preferred_targets): return tuple(x for x in targets if x in self.preferred_targets) #otherwise choose a text target: return tuple(x for x in targets if x in TEXT_TARGETS) def do_selection_clear_event(self, event): log("do_xpra_selection_clear(%s) was owned=%s", event, self.owned) if not self._enabled: return self.owned = False self.do_owner_changed() def do_owner_changed(self): log("do_owner_changed()") self.target_data = {} self.targets = () def get_contents(self, target, got_contents): log("get_contents(%s, %s) owned=%s, have-token=%s", target, got_contents, self.owned, self._have_token) if target == "TARGETS": if self.targets: xatoms = strings_to_xatoms(self.targets) got_contents("ATOM", 32, xatoms) return else: target_data = self.target_data.get(target) if target_data: dtype, dformat, value = target_data got_contents(dtype, dformat, value) return prop = "%s-%s" % (self._selection, target) with xsync: owner = X11Window.XGetSelectionOwner(self._selection) self.owned = owner == self.xid if self.owned: #we are the clipboard owner! log("we are the %s selection owner, using empty reply", self._selection) got_contents(None, None, None) return request_id = self.local_request_counter self.local_request_counter += 1 timer = GLib.timeout_add(CONVERT_TIMEOUT, self.timeout_get_contents, target, request_id) self.local_requests.setdefault(target, {})[request_id] = (timer, got_contents) log("requesting local XConvertSelection from %s as '%s' into '%s'", self.get_wininfo(owner), target, prop) X11Window.ConvertSelection(self._selection, target, prop, self.xid, time=CurrentTime) def timeout_get_contents(self, target, request_id): try: target_requests = self.local_requests.get(target) if target_requests is None: return timer, got_contents = target_requests.pop(request_id) if not target_requests: del self.local_requests[target] except KeyError: return GLib.source_remove(timer) log.warn("Warning: %s selection request for '%s' timed out", self._selection, target) log.warn(" request %i", request_id) if target == "TARGETS": got_contents("ATOM", 32, b"") else: got_contents(None, None, None) def do_property_notify(self, event): log("do_property_notify(%s)", event) if not self._enabled: return #ie: atom="PRIMARY-TARGETS", atom="PRIMARY-STRING" parts = event.atom.split("-", 1) assert len(parts) == 2 #selection = parts[0] #ie: PRIMARY target = parts[1] #ie: VALUE dtype = "" dformat = 8 try: with xsync: dtype, dformat = X11Window.GetWindowPropertyType( self.xid, event.atom, True) dtype = bytestostr(dtype) MAX_DATA_SIZE = 4 * 1024 * 1024 data = X11Window.XGetWindowProperty(self.xid, event.atom, dtype, None, MAX_DATA_SIZE, True) #all the code below deals with INCRemental transfers: if dtype == "INCR" and not self.incr_data_size: #start of an incremental transfer, extract the size assert dformat == 32 self.incr_data_size = struct.unpack("@L", data)[0] self.incr_data_chunks = [] self.incr_data_type = None log("incremental clipboard data of size %s", self.incr_data_size) self.reschedule_incr_data_timer() return if self.incr_data_size > 0: #incremental is now in progress: if not self.incr_data_type: self.incr_data_type = dtype elif self.incr_data_type != dtype: log.error("Error: invalid change of data type") log.error(" from %s to %s", self.incr_data_type, dtype) self.reset_incr_data() self.cancel_incr_data_timer() return if data: log("got incremental data: %i bytes", len(data)) self.incr_data_chunks.append(data) self.reschedule_incr_data_timer() return self.cancel_incr_data_timer() data = b"".join(self.incr_data_chunks) log( "got incremental data termination, total size=%i bytes", len(data)) self.reset_incr_data() self.got_local_contents(target, dtype, dformat, data) return except PropertyError: log("do_property_notify() property '%s' is gone?", event.atom, exc_info=True) return log("%s=%s (%s : %s)", event.atom, ellipsizer(data), dtype, dformat) if target == "TARGETS": self.targets = xatoms_to_strings(data or b"") self.got_local_contents(target, dtype, dformat, data) def got_local_contents(self, target, dtype=None, dformat=None, data=None): data = self.filter_data(dtype, dformat, data) target_requests = self.local_requests.pop(target, {}) for timer, got_contents in target_requests.values(): if log.is_debug_enabled(): log("got_local_contents: calling %s%s", got_contents, (dtype, dformat, ellipsizer(data))) GLib.source_remove(timer) got_contents(dtype, dformat, data) def reschedule_incr_data_timer(self): self.cancel_incr_data_timer() self.incr_data_timer = GLib.timeout_add(1 * 1000, self.incr_data_timeout) def cancel_incr_data_timer(self): idt = self.incr_data_timer if idt: self.incr_data_timer = None GLib.source_remove(idt) def incr_data_timeout(self): self.incr_data_timer = None log.warn("Warning: incremental data timeout") self.incr_data = None def reset_incr_data(self): self.incr_data_size = 0 self.incr_data_type = None self.incr_data_chunks = None self.incr_data_timer = None
class SoundSource(SoundPipeline): __gsignals__ = SoundPipeline.__generic_signals__.copy() __gsignals__.update({ "new-buffer" : n_arg_signal(2), }) def __init__(self, src_type=DEFAULT_SRC, src_options={}, codec=MP3, volume=1.0, encoder_options={}): assert src_type in SOURCES encoder, fmt = get_encoder_formatter(codec) SoundPipeline.__init__(self, codec) self.src_type = src_type source_str = plugin_str(src_type, src_options) encoder_str = plugin_str(encoder, encoder_options) fmt_str = plugin_str(fmt, MUXER_DEFAULT_OPTIONS.get(fmt, {})) pipeline_els = [source_str] if AUDIOCONVERT: pipeline_els += ["audioconvert"] if AUDIORESAMPLE: pipeline_els += [ "audioresample", "audio/x-raw-int,rate=44100,channels=2"] pipeline_els.append("volume name=volume volume=%s" % volume) pipeline_els += [encoder_str, fmt_str, "appsink name=sink"] self.setup_pipeline_and_bus(pipeline_els) self.volume = self.pipeline.get_by_name("volume") self.sink = self.pipeline.get_by_name("sink") self.sink.set_property("emit-signals", True) self.sink.set_property("max-buffers", 10) self.sink.set_property("drop", False) self.sink.set_property("sync", True) self.sink.set_property("qos", False) try: #Gst 1.0: self.sink.connect("new-sample", self.on_new_sample) self.sink.connect("new-preroll", self.on_new_preroll1) except: #Gst 0.10: self.sink.connect("new-buffer", self.on_new_buffer) self.sink.connect("new-preroll", self.on_new_preroll0) def set_volume(self, volume=1.0): if self.sink and self.volume: self.volume.set_property("volume", volume) def get_volume(self): if self.sink and self.volume: return self.volume.get_property("volume") return 0 def cleanup(self): SoundPipeline.cleanup(self) self.src_type = "" self.sink = None def on_new_preroll1(self, appsink): sample = appsink.emit('pull-preroll') log('new preroll1: %s', sample) self.emit_buffer1(sample) def on_new_sample(self, bus): #Gst 1.0 sample = self.sink.emit("pull-sample") self.emit_buffer1(sample) def emit_buffer1(self, sample): buf = sample.get_buffer() #info = sample.get_info() size = buf.get_size() data = buf.extract_dup(0, size) self.do_emit_buffer(data, {"timestamp" : normv(buf.pts), "duration" : normv(buf.duration)}) def on_new_preroll0(self, appsink): buf = appsink.emit('pull-preroll') log('new preroll0: %s bytes', len(buf)) self.emit_buffer0(buf) def on_new_buffer(self, bus): #pygst 0.10 buf = self.sink.emit("pull-buffer") self.emit_buffer0(buf) def emit_buffer0(self, buf, metadata={}): """ convert pygst structure into something more generic for the wire """ #none of the metadata is really needed at present, but it may be in the future: #metadata = {"caps" : buf.get_caps().to_string(), # "size" : buf.size, # "timestamp" : buf.timestamp, # "duration" : buf.duration, # "offset" : buf.offset, # "offset_end": buf.offset_end} self.do_emit_buffer(buf.data, {"timestamp" : normv(buf.timestamp), "duration" : normv(buf.duration)}) def do_emit_buffer(self, data, metadata={}): self.buffer_count += 1 self.byte_count += len(data) self.emit("new-buffer", data, metadata)
class SoundSource(SoundPipeline): __gsignals__ = SoundPipeline.__generic_signals__.copy() __gsignals__.update({ "new-buffer": n_arg_signal(3), }) def __init__(self, src_type=None, src_options={}, codecs=get_encoders(), codec_options={}, volume=1.0): if not src_type: try: from xpra.sound.pulseaudio.pulseaudio_util import get_pa_device_options monitor_devices = get_pa_device_options(True, False) log.info("found pulseaudio monitor devices: %s", monitor_devices) except ImportError as e: log.warn("Warning: pulseaudio is not available!") log.warn(" %s", e) monitor_devices = [] if len(monitor_devices) == 0: log.warn("could not detect any pulseaudio monitor devices") log.warn(" a test source will be used instead") src_type = "audiotestsrc" default_src_options = {"wave": 2, "freq": 100, "volume": 0.4} else: monitor_device = monitor_devices.items()[0][0] log.info("using pulseaudio source device:") log.info(" '%s'", monitor_device) src_type = "pulsesrc" default_src_options = {"device": monitor_device} src_options = default_src_options if src_type not in get_source_plugins(): raise InitExit( 1, "invalid source plugin '%s', valid options are: %s" % (src_type, ",".join(get_source_plugins()))) matching = [ x for x in CODEC_ORDER if (x in codecs and x in get_encoders()) ] log("SoundSource(..) found matching codecs %s", matching) if not matching: raise InitExit( 1, "no matching codecs between arguments '%s' and supported list '%s'" % (csv(codecs), csv(get_encoders().keys()))) codec = matching[0] encoder, fmt, stream_compressor = get_encoder_elements(codec) SoundPipeline.__init__(self, codec) self.queue = None self.caps = None self.volume = None self.sink = None self.src = None self.src_type = src_type self.pending_metadata = [] self.buffer_latency = True self.jitter_queue = None self.file = None self.container_format = (fmt or "").replace("mux", "").replace("pay", "") self.stream_compressor = stream_compressor src_options["name"] = "src" source_str = plugin_str(src_type, src_options) #FIXME: this is ugly and relies on the fact that we don't pass any codec options to work! pipeline_els = [source_str] if SOURCE_QUEUE_TIME > 0: queue_el = [ "queue", "name=queue", "min-threshold-time=0", "max-size-buffers=0", "max-size-bytes=0", "max-size-time=%s" % (SOURCE_QUEUE_TIME * MS_TO_NS), "leaky=%s" % GST_QUEUE_LEAK_DOWNSTREAM ] pipeline_els += [" ".join(queue_el)] if encoder in ENCODER_NEEDS_AUDIOCONVERT or src_type in SOURCE_NEEDS_AUDIOCONVERT: pipeline_els += ["audioconvert"] pipeline_els.append("volume name=volume volume=%s" % volume) if encoder: encoder_str = plugin_str( encoder, codec_options or get_encoder_default_options(encoder)) pipeline_els.append(encoder_str) if fmt: fmt_str = plugin_str(fmt, MUXER_DEFAULT_OPTIONS.get(fmt, {})) pipeline_els.append(fmt_str) pipeline_els.append(APPSINK) if not self.setup_pipeline_and_bus(pipeline_els): return self.volume = self.pipeline.get_by_name("volume") self.sink = self.pipeline.get_by_name("sink") if SOURCE_QUEUE_TIME > 0: self.queue = self.pipeline.get_by_name("queue") if self.queue: try: self.queue.set_property("silent", True) except Exception as e: log("cannot make queue silent: %s", e) try: if get_gst_version() < (1, 0): self.sink.set_property("enable-last-buffer", False) else: self.sink.set_property("enable-last-sample", False) except Exception as e: log("failed to disable last buffer: %s", e) self.skipped_caps = set() if JITTER > 0: self.jitter_queue = Queue() try: #Gst 1.0: self.sink.connect("new-sample", self.on_new_sample) self.sink.connect("new-preroll", self.on_new_preroll1) except: #Gst 0.10: self.sink.connect("new-buffer", self.on_new_buffer) self.sink.connect("new-preroll", self.on_new_preroll0) self.src = self.pipeline.get_by_name("src") try: for x in ("actual-buffer-time", "actual-latency-time"): #don't comment this out, it is used to verify the attributes are present: try: gstlog("initial %s: %s", x, self.src.get_property(x)) except Exception as e: self.buffer_latency = False except Exception as e: log.info( "source %s does not support 'buffer-time' or 'latency-time':", self.src_type) log.info(" %s", e) else: #if the env vars have been set, try to honour the settings: global BUFFER_TIME, LATENCY_TIME if BUFFER_TIME > 0: if BUFFER_TIME < LATENCY_TIME: log.warn( "Warning: latency (%ims) must be lower than the buffer time (%ims)", LATENCY_TIME, BUFFER_TIME) else: log( "latency tuning for %s, will try to set buffer-time=%i, latency-time=%i", src_type, BUFFER_TIME, LATENCY_TIME) def settime(attr, v): try: cval = self.src.get_property(attr) gstlog("default: %s=%i", attr, cval // 1000) if v >= 0: self.src.set_property(attr, v * 1000) gstlog("overriding with: %s=%i", attr, v) except Exception as e: log.warn("source %s does not support '%s': %s", self.src_type, attr, e) settime("buffer-time", BUFFER_TIME) settime("latency-time", LATENCY_TIME) gen = generation.increase() if SAVE_TO_FILE is not None: parts = codec.split("+") if len(parts) > 1: filename = SAVE_TO_FILE + str( gen) + "-" + parts[0] + ".%s" % parts[1] else: filename = SAVE_TO_FILE + str(gen) + ".%s" % codec self.file = open(filename, 'wb') log.info("saving %s stream to %s", codec, filename) def __repr__(self): return "SoundSource('%s' - %s)" % (self.pipeline_str, self.state) def cleanup(self): SoundPipeline.cleanup(self) self.src_type = "" self.sink = None self.caps = None f = self.file if f: self.file = None f.close() def get_info(self): info = SoundPipeline.get_info(self) if self.queue: info["queue"] = { "cur": self.queue.get_property("current-level-time") // MS_TO_NS } if self.buffer_latency: for x in ("actual-buffer-time", "actual-latency-time"): v = self.src.get_property(x) if v >= 0: info[x] = v return info def on_new_preroll1(self, appsink): gstlog('new preroll') return 0 def on_new_sample(self, bus): #Gst 1.0 sample = self.sink.emit("pull-sample") return self.emit_buffer1(sample) def emit_buffer1(self, sample): buf = sample.get_buffer() #info = sample.get_info() size = buf.get_size() extract_dup = getattr(buf, "extract_dup", None) if extract_dup: data = extract_dup(0, size) else: #crappy gi bindings detected, using workaround: from xpra.sound.gst_hacks import map_gst_buffer with map_gst_buffer(buf) as a: data = bytes(a[:]) pts = normv(buf.pts) duration = normv(buf.duration) if pts == -1 and duration == -1 and BUNDLE_METADATA and len( self.pending_metadata) < 10: self.pending_metadata.append(data) return 0 return self.emit_buffer(data, { "timestamp": pts, "duration": duration, }) def on_new_preroll0(self, appsink): gstlog('new preroll') return 0 def on_new_buffer(self, bus): #pygst 0.10 buf = self.sink.emit("pull-buffer") return self.emit_buffer0(buf) def caps_to_dict(self, caps): if not caps: return {} d = {} try: for cap in caps: name = cap.get_name() capd = {} for k in cap.keys(): v = cap[k] if type(v) in (str, int): capd[k] = cap[k] elif k not in self.skipped_caps: log("skipping %s cap key %s=%s of type %s", name, k, v, type(v)) d[name] = capd except Exception as e: log.error("Error parsing '%s':", caps) log.error(" %s", e) return d def emit_buffer0(self, buf): """ convert pygst structure into something more generic for the wire """ #none of the metadata is really needed at present, but it may be in the future: #metadata = {"caps" : buf.get_caps().to_string(), # "size" : buf.size, # "timestamp" : buf.timestamp, # "duration" : buf.duration, # "offset" : buf.offset, # "offset_end": buf.offset_end} log("emit buffer: %s bytes, timestamp=%s", len(buf.data), buf.timestamp // MS_TO_NS) metadata = { "timestamp": normv(buf.timestamp), "duration": normv(buf.duration) } d = self.caps_to_dict(buf.get_caps()) if not self.caps or self.caps != d: self.caps = d self.info["caps"] = self.caps metadata["caps"] = self.caps return self.emit_buffer(buf.data, metadata) def emit_buffer(self, data, metadata={}): if self.stream_compressor and data: data = compressed_wrapper("sound", data, level=9, zlib=False, lz4=(self.stream_compressor == "lz4"), lzo=(self.stream_compressor == "lzo"), can_inline=True) #log("compressed using %s from %i bytes down to %i bytes", self.stream_compressor, len(odata), len(data)) metadata["compress"] = self.stream_compressor f = self.file if f: for x in self.pending_metadata: self.file.write(x) if data: self.file.write(data) self.file.flush() if self.state == "stopped": #don't bother return 0 if JITTER > 0: #will actually emit the buffer after a random delay if self.jitter_queue.empty(): #queue was empty, schedule a timer to flush it from random import randint jitter = randint(1, JITTER) self.timeout_add(jitter, self.flush_jitter_queue) log("emit_buffer: will flush jitter queue in %ims", jitter) for x in self.pending_metadata: self.jitter_queue.put((x, {})) self.pending_metadata = [] self.jitter_queue.put((data, metadata)) return 0 log("emit_buffer data=%s, len=%i, metadata=%s", type(data), len(data), metadata) return self.do_emit_buffer(data, metadata) def flush_jitter_queue(self): while not self.jitter_queue.empty(): d, m = self.jitter_queue.get(False) self.do_emit_buffer(d, m) def do_emit_buffer(self, data, metadata={}): self.inc_buffer_count() self.inc_byte_count(len(data)) for x in self.pending_metadata: self.inc_buffer_count() self.inc_byte_count(len(x)) metadata["time"] = int(time.time() * 1000) self.idle_emit("new-buffer", data, metadata, self.pending_metadata) self.pending_metadata = [] self.emit_info() return 0
class SoundSource(SoundPipeline): __gsignals__ = SoundPipeline.__generic_signals__.copy() __gsignals__.update({ "new-buffer" : n_arg_signal(2), }) def __init__(self, src_type=None, src_options={}, codecs=get_codecs(), codec_options={}, volume=1.0): if not src_type: from xpra.sound.pulseaudio_util import get_pa_device_options monitor_devices = get_pa_device_options(True, False) log.info("found pulseaudio monitor devices: %s", monitor_devices) if len(monitor_devices)==0: log.warn("could not detect any pulseaudio monitor devices") log.warn(" a test source will be used instead") src_type = "audiotestsrc" default_src_options = {"wave":2, "freq":100, "volume":0.4} else: monitor_device = monitor_devices.items()[0][0] log.info("using pulseaudio source device:") log.info(" '%s'", monitor_device) src_type = "pulsesrc" default_src_options = {"device" : monitor_device} src_options = default_src_options if src_type not in get_source_plugins(): raise InitExit(1, "invalid source plugin '%s', valid options are: %s" % (src_type, ",".join(get_source_plugins()))) matching = [x for x in CODEC_ORDER if (x in codecs and x in get_codecs())] log("SoundSource(..) found matching codecs %s", matching) if not matching: raise InitExit(1, "no matching codecs between arguments '%s' and supported list '%s'" % (csv(codecs), csv(get_codecs().keys()))) codec = matching[0] encoder, fmt = get_encoder_formatter(codec) SoundPipeline.__init__(self, codec) self.src_type = src_type source_str = plugin_str(src_type, src_options) #FIXME: this is ugly and relies on the fact that we don't pass any codec options to work! encoder_str = plugin_str(encoder, codec_options or ENCODER_DEFAULT_OPTIONS.get(encoder, {})) fmt_str = plugin_str(fmt, MUXER_DEFAULT_OPTIONS.get(fmt, {})) pipeline_els = [source_str] if encoder in ENCODER_NEEDS_AUDIOCONVERT or src_type in SOURCE_NEEDS_AUDIOCONVERT: pipeline_els += ["audioconvert"] pipeline_els.append("volume name=volume volume=%s" % volume) pipeline_els += [encoder_str, fmt_str, APPSINK] self.setup_pipeline_and_bus(pipeline_els) self.volume = self.pipeline.get_by_name("volume") self.sink = self.pipeline.get_by_name("sink") try: if get_gst_version()<(1,0): self.sink.set_property("enable-last-buffer", False) else: self.sink.set_property("enable-last-sample", False) except Exception as e: log("failed to disable last buffer: %s", e) self.caps = None self.skipped_caps = set() if JITTER>0: self.jitter_queue = Queue() try: #Gst 1.0: self.sink.connect("new-sample", self.on_new_sample) self.sink.connect("new-preroll", self.on_new_preroll1) except: #Gst 0.10: self.sink.connect("new-buffer", self.on_new_buffer) self.sink.connect("new-preroll", self.on_new_preroll0) def __repr__(self): return "SoundSource('%s' - %s)" % (self.pipeline_str, self.state) def cleanup(self): SoundPipeline.cleanup(self) self.src_type = "" self.sink = None self.caps = None def get_info(self): info = SoundPipeline.get_info(self) if self.caps: info["caps"] = self.caps return info def on_new_preroll1(self, appsink): sample = appsink.emit('pull-preroll') log('new preroll1: %s', sample) return self.emit_buffer1(sample) def on_new_sample(self, bus): #Gst 1.0 sample = self.sink.emit("pull-sample") return self.emit_buffer1(sample) def emit_buffer1(self, sample): buf = sample.get_buffer() #info = sample.get_info() size = buf.get_size() extract_dup = getattr(buf, "extract_dup", None) if extract_dup: data = extract_dup(0, size) else: #crappy gi bindings detected, using workaround: from xpra.sound.gst_hacks import map_gst_buffer with map_gst_buffer(buf) as a: data = bytes(a[:]) return self.emit_buffer(data, {"timestamp" : normv(buf.pts), "duration" : normv(buf.duration), }) def on_new_preroll0(self, appsink): buf = appsink.emit('pull-preroll') log('new preroll0: %s bytes', len(buf)) return self.emit_buffer0(buf) def on_new_buffer(self, bus): #pygst 0.10 buf = self.sink.emit("pull-buffer") return self.emit_buffer0(buf) def caps_to_dict(self, caps): if not caps: return {} d = {} try: for cap in caps: name = cap.get_name() capd = {} for k in cap.keys(): v = cap[k] if type(v) in (str, int): capd[k] = cap[k] elif k not in self.skipped_caps: log("skipping %s cap key %s=%s of type %s", name, k, v, type(v)) d[name] = capd except Exception as e: log.error("Error parsing '%s':", caps) log.error(" %s", e) return d def emit_buffer0(self, buf): """ convert pygst structure into something more generic for the wire """ #none of the metadata is really needed at present, but it may be in the future: #metadata = {"caps" : buf.get_caps().to_string(), # "size" : buf.size, # "timestamp" : buf.timestamp, # "duration" : buf.duration, # "offset" : buf.offset, # "offset_end": buf.offset_end} log("emit buffer: %s bytes, timestamp=%s", len(buf.data), buf.timestamp//MS_TO_NS) metadata = { "timestamp" : normv(buf.timestamp), "duration" : normv(buf.duration) } d = self.caps_to_dict(buf.get_caps()) if not self.caps or self.caps!=d: self.caps = d metadata["caps"] = self.caps return self.emit_buffer(buf.data, metadata) def emit_buffer(self, data, metadata={}): if JITTER>0: #will actually emit the buffer after a random delay if self.jitter_queue.empty(): #queue was empty, schedule a timer to flush it from random import randint jitter = randint(1, JITTER) self.timeout_add(jitter, self.flush_jitter_queue) log("emit_buffer: will flush jitter queue in %ims", jitter) self.jitter_queue.put((data, metadata)) return 0 log("emit_buffer data=%s, len=%i, metadata=%s", type(data), len(data), metadata) return self.do_emit_buffer(data, metadata) def flush_jitter_queue(self): while not self.jitter_queue.empty(): d,m = self.jitter_queue.get(False) self.do_emit_buffer(d, m) def do_emit_buffer(self, data, metadata={}): self.buffer_count += 1 self.byte_count += len(data) metadata["time"] = int(time.time()*1000) self.idle_emit("new-buffer", data, metadata) self.emit_info() return 0
class SoundSource(SoundPipeline): __gsignals__ = SoundPipeline.__generic_signals__.copy() __gsignals__.update({ "new-buffer": n_arg_signal(3), }) def __init__(self, src_type=None, src_options={}, codecs=get_encoders(), codec_options={}, volume=1.0): if not src_type: try: from xpra.sound.pulseaudio.pulseaudio_util import get_pa_device_options monitor_devices = get_pa_device_options(True, False) log.info("found pulseaudio monitor devices: %s", monitor_devices) except ImportError as e: log.warn("Warning: pulseaudio is not available!") log.warn(" %s", e) monitor_devices = [] if len(monitor_devices) == 0: log.warn("could not detect any pulseaudio monitor devices") log.warn(" a test source will be used instead") src_type = "audiotestsrc" default_src_options = {"wave": 2, "freq": 100, "volume": 0.4} else: monitor_device = monitor_devices.items()[0][0] log.info("using pulseaudio source device:") log.info(" '%s'", monitor_device) src_type = "pulsesrc" default_src_options = {"device": monitor_device} src_options = default_src_options if src_type not in get_source_plugins(): raise InitExit( 1, "invalid source plugin '%s', valid options are: %s" % (src_type, ",".join(get_source_plugins()))) matching = [ x for x in CODEC_ORDER if (x in codecs and x in get_encoders()) ] log("SoundSource(..) found matching codecs %s", matching) if not matching: raise InitExit( 1, "no matching codecs between arguments '%s' and supported list '%s'" % (csv(codecs), csv(get_encoders().keys()))) codec = matching[0] encoder, fmt, stream_compressor = get_encoder_elements(codec) SoundPipeline.__init__(self, codec) self.queue = None self.caps = None self.volume = None self.sink = None self.src = None self.src_type = src_type self.timestamp = None self.min_timestamp = 0 self.max_timestamp = 0 self.pending_metadata = [] self.buffer_latency = True self.jitter_queue = None self.file = None self.container_format = (fmt or "").replace("mux", "").replace("pay", "") self.stream_compressor = stream_compressor src_options["name"] = "src" source_str = plugin_str(src_type, src_options) #FIXME: this is ugly and relies on the fact that we don't pass any codec options to work! pipeline_els = [source_str] log("has plugin(timestamp)=%s", has_plugins("timestamp")) if has_plugins("timestamp"): pipeline_els.append("timestamp name=timestamp") if SOURCE_QUEUE_TIME > 0: queue_el = [ "queue", "name=queue", "min-threshold-time=0", "max-size-buffers=0", "max-size-bytes=0", "max-size-time=%s" % (SOURCE_QUEUE_TIME * MS_TO_NS), "leaky=%s" % GST_QUEUE_LEAK_DOWNSTREAM ] pipeline_els += [" ".join(queue_el)] if encoder in ENCODER_NEEDS_AUDIOCONVERT or src_type in SOURCE_NEEDS_AUDIOCONVERT: pipeline_els += ["audioconvert"] if CUTTER_THRESHOLD > 0 and encoder not in ENCODER_CANNOT_USE_CUTTER and not fmt: pipeline_els.append( "cutter threshold=%.4f run-length=%i pre-length=%i leaky=false name=cutter" % (CUTTER_THRESHOLD, CUTTER_RUN_LENGTH * MS_TO_NS, CUTTER_PRE_LENGTH * MS_TO_NS)) if encoder in CUTTER_NEEDS_CONVERT: pipeline_els.append("audioconvert") if encoder in CUTTER_NEEDS_RESAMPLE: pipeline_els.append("audioresample") pipeline_els.append("volume name=volume volume=%s" % volume) if encoder: encoder_str = plugin_str( encoder, codec_options or get_encoder_default_options(encoder)) pipeline_els.append(encoder_str) if fmt: fmt_str = plugin_str(fmt, MUXER_DEFAULT_OPTIONS.get(fmt, {})) pipeline_els.append(fmt_str) pipeline_els.append(APPSINK) if not self.setup_pipeline_and_bus(pipeline_els): return self.timestamp = self.pipeline.get_by_name("timestamp") self.volume = self.pipeline.get_by_name("volume") self.sink = self.pipeline.get_by_name("sink") if SOURCE_QUEUE_TIME > 0: self.queue = self.pipeline.get_by_name("queue") if self.queue: try: self.queue.set_property("silent", True) except Exception as e: log("cannot make queue silent: %s", e) self.sink.set_property("enable-last-sample", False) self.skipped_caps = set() if JITTER > 0: self.jitter_queue = Queue() #Gst 1.0: self.sink.connect("new-sample", self.on_new_sample) self.sink.connect("new-preroll", self.on_new_preroll) self.src = self.pipeline.get_by_name("src") for x in ("actual-buffer-time", "actual-latency-time"): try: gstlog("initial %s: %s", x, self.src.get_property(x)) except Exception as e: gstlog("no %s property on %s: %s", x, self.src, e) self.buffer_latency = False #if the env vars have been set, try to honour the settings: global BUFFER_TIME, LATENCY_TIME if BUFFER_TIME > 0: if BUFFER_TIME < LATENCY_TIME: log.warn( "Warning: latency (%ims) must be lower than the buffer time (%ims)", LATENCY_TIME, BUFFER_TIME) else: log( "latency tuning for %s, will try to set buffer-time=%i, latency-time=%i", src_type, BUFFER_TIME, LATENCY_TIME) def settime(attr, v): try: cval = self.src.get_property(attr) gstlog("default: %s=%i", attr, cval // 1000) if v >= 0: self.src.set_property(attr, v * 1000) gstlog("overriding with: %s=%i", attr, v) except Exception as e: log.warn("source %s does not support '%s': %s", self.src_type, attr, e) settime("buffer-time", BUFFER_TIME) settime("latency-time", LATENCY_TIME) gen = generation.increase() if SAVE_TO_FILE is not None: parts = codec.split("+") if len(parts) > 1: filename = SAVE_TO_FILE + str( gen) + "-" + parts[0] + ".%s" % parts[1] else: filename = SAVE_TO_FILE + str(gen) + ".%s" % codec self.file = open(filename, 'wb') log.info("saving %s stream to %s", codec, filename) def __repr__(self): return "SoundSource('%s' - %s)" % (self.pipeline_str, self.state) def cleanup(self): SoundPipeline.cleanup(self) self.src_type = "" self.sink = None self.caps = None f = self.file if f: self.file = None f.close() def get_info(self): info = SoundPipeline.get_info(self) if self.queue: info["queue"] = { "cur": self.queue.get_property("current-level-time") // MS_TO_NS } if CUTTER_THRESHOLD > 0 and (self.min_timestamp or self.max_timestamp): info["cutter.min-timestamp"] = self.min_timestamp info["cutter.max-timestamp"] = self.max_timestamp if self.buffer_latency: for x in ("actual-buffer-time", "actual-latency-time"): v = self.src.get_property(x) if v >= 0: info[x] = v return info def do_parse_element_message(self, _message, name, props={}): if name == "cutter": above = props.get("above") ts = props.get("timestamp", 0) if above is False: self.max_timestamp = ts self.min_timestamp = 0 elif above is True: self.max_timestamp = 0 self.min_timestamp = ts if LOG_CUTTER: l = gstlog.info else: l = gstlog l("cutter message, above=%s, min-timestamp=%s, max-timestamp=%s", above, self.min_timestamp, self.max_timestamp) def on_new_preroll(self, _appsink): gstlog('new preroll') return 0 def on_new_sample(self, _bus): #Gst 1.0 sample = self.sink.emit("pull-sample") return self.emit_buffer(sample) def emit_buffer(self, sample): buf = sample.get_buffer() pts = normv(buf.pts) if self.min_timestamp > 0 and pts < self.min_timestamp: gstlog("cutter: skipping buffer with pts=%s (min-timestamp=%s)", pts, self.min_timestamp) return 0 elif self.max_timestamp > 0 and pts > self.max_timestamp: gstlog("cutter: skipping buffer with pts=%s (max-timestamp=%s)", pts, self.max_timestamp) return 0 size = buf.get_size() data = buf.extract_dup(0, size) duration = normv(buf.duration) metadata = { "timestamp": pts, "duration": duration, } if self.timestamp: delta = self.timestamp.get_property("delta") ts = (pts + delta) // 1000000 #ns to ms now = monotonic_time() latency = int(1000 * now) - ts #log.info("emit_buffer: delta=%i, pts=%i, ts=%s, time=%s, latency=%ims", delta, pts, ts, now, (latency//1000000)) ts_info = { "ts": ts, "latency": latency, } metadata.update(ts_info) self.info.update(ts_info) if pts == -1 and duration == -1 and BUNDLE_METADATA and len( self.pending_metadata) < 10: self.pending_metadata.append(data) return 0 return self._emit_buffer(data, metadata) def _emit_buffer(self, data, metadata={}): if self.stream_compressor and data: cdata = compressed_wrapper("sound", data, level=9, zlib=False, lz4=(self.stream_compressor == "lz4"), lzo=(self.stream_compressor == "lzo"), can_inline=True) if len(cdata) < len(data) * 90 // 100: log("compressed using %s from %i bytes down to %i bytes", self.stream_compressor, len(data), len(cdata)) metadata["compress"] = self.stream_compressor data = cdata else: log( "skipped inefficient %s stream compression: %i bytes down to %i bytes", self.stream_compressor, len(data), len(cdata)) f = self.file if f: for x in self.pending_metadata: self.file.write(x) if data: self.file.write(data) self.file.flush() if self.state == "stopped": #don't bother return 0 if JITTER > 0: #will actually emit the buffer after a random delay if self.jitter_queue.empty(): #queue was empty, schedule a timer to flush it from random import randint jitter = randint(1, JITTER) self.timeout_add(jitter, self.flush_jitter_queue) log("emit_buffer: will flush jitter queue in %ims", jitter) for x in self.pending_metadata: self.jitter_queue.put((x, {})) self.pending_metadata = [] self.jitter_queue.put((data, metadata)) return 0 log("emit_buffer data=%s, len=%i, metadata=%s", type(data), len(data), metadata) return self.do_emit_buffer(data, metadata) def caps_to_dict(self, caps): if not caps: return {} d = {} try: for cap in caps: name = cap.get_name() capd = {} for k in cap.keys(): v = cap[k] if type(v) in (str, int): capd[k] = cap[k] elif k not in self.skipped_caps: log("skipping %s cap key %s=%s of type %s", name, k, v, type(v)) d[name] = capd except Exception as e: log.error("Error parsing '%s':", caps) log.error(" %s", e) return d def flush_jitter_queue(self): while not self.jitter_queue.empty(): d, m = self.jitter_queue.get(False) self.do_emit_buffer(d, m) def do_emit_buffer(self, data, metadata={}): self.inc_buffer_count() self.inc_byte_count(len(data)) for x in self.pending_metadata: self.inc_buffer_count() self.inc_byte_count(len(x)) metadata["time"] = int(monotonic_time() * 1000) self.idle_emit("new-buffer", data, metadata, self.pending_metadata) self.pending_metadata = [] self.emit_info() return 0
class ClipboardProxy(gtk.Invisible): __gsignals__ = { # arguments: (selection, target) "get-clipboard-from-remote": ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_PYOBJECT, (gobject.TYPE_PYOBJECT, ) * 2, ), # arguments: (selection,) "send-clipboard-token": n_arg_signal(1), } def __init__(self, selection): gtk.Invisible.__init__(self) self.add_events(PROPERTY_CHANGE_MASK) self._selection = selection self._clipboard = GetClipboard(selection) self._enabled = True self._have_token = False #this workaround is only needed on win32 AFAIK: self._strip_nullbyte = sys.platform.startswith("win") #clients that need a new token for every owner-change: (ie: win32 and osx) #(forces the client to request new contents - prevents stale clipboard data) self._greedy_client = False #semaphore to block the sending of the token when we change the owner ourselves: self._block_owner_change = False #counters for info: self._selection_request_events = 0 self._selection_get_events = 0 self._selection_clear_events = 0 self._sent_token_events = 0 self._got_token_events = 0 self._get_contents_events = 0 self._request_contents_events = 0 try: from xpra.x11.gtk_x11.prop import prop_get self.prop_get = prop_get except ImportError: self.prop_get = None self._clipboard.connect("owner-change", self.do_owner_changed) def get_info(self): info = { "have_token": self._have_token, "enabled": self._enabled, "greedy_client": self._greedy_client, "blocked_owner_change": self._block_owner_change, "event.selection_request": self._selection_request_events, "event.selection_get": self._selection_get_events, "event.selection_clear": self._selection_clear_events, "event.got_token": self._got_token_events, "event.sent_token": self._sent_token_events, "event.get_contents": self._get_contents_events, "event.request_contents": self._request_contents_events, } return info def cleanup(self): if not self._have_token: self._clipboard.store() self.destroy() def is_enabled(self): return self._enabled def set_enabled(self, enabled): log("%s.set_enabled(%s)", self, enabled) self._enabled = enabled def set_greedy_client(self, greedy): log("%s.set_greedy_client(%s)", self, greedy) self._greedy_client = greedy def __str__(self): return "ClipboardProxy(%s)" % self._selection def do_owner_changed(self, *args): log("do_owner_changed(%s) greedy_client=%s, block_owner_change=%s", args, self._greedy_client, self._block_owner_change) if self._enabled and self._greedy_client and not self._block_owner_change: self._block_owner_change = True self._have_token = False self.emit("send-clipboard-token", self._selection) self._sent_token_events += 1 gobject.idle_add(self.remove_block) def do_selection_request_event(self, event): log("do_selection_request_event(%s)", event) self._selection_request_events += 1 if not self._enabled: gtk.Invisible.do_selection_request_event(self, event) return # Black magic: the superclass default handler for this signal # implements all the hards parts of selection handling, occasionally # calling back to the do_selection_get handler (below) to actually get # the data to be sent. However, it only does this for targets that # have been registered ahead of time; other targets fall through to a # default implementation that cannot be overridden. So, we swoop in # ahead of time and add whatever target was requested to the list of # targets we want to handle! # # Special cases (magic targets defined by ICCCM): # TIMESTAMP: the remote side has a different timeline than us, so # sending TIMESTAMPS across the wire doesn't make any sense. We # ignore TIMESTAMP requests, and let them fall through to GTK+'s # default handler. # TARGET: GTK+ has default handling for this, but we don't want to # use it. Fortunately, if we tell GTK+ that we can handle TARGET # requests, then it will pass them on to us rather than fall # through to the default handler. # MULTIPLE: Ugh. To handle this properly, we need to go out # ourselves and fetch the magic property off the requesting window # (with proper error trapping and all), and interpret its # contents. Probably doable (FIXME), just a pain. # # Another special case is that if an app requests the contents of a # clipboard that it currently owns, then GTK+ will short-circuit the # normal logic and request the contents directly (i.e. it calls # gtk_selection_invoke_handler) -- without giving us a chance to # assert that we can handle the requested sort of target. Fortunately, # Xpra never needs to request the clipboard when it owns it, so that's # okay. assert str(event.selection) == self._selection target = str(event.target) if target == "TIMESTAMP": pass elif target == "MULTIPLE": if not self.prop_get: log( "MULTIPLE for property '%s' not handled due to missing xpra.x11.gtk_x11 bindings", event.property) gtk.Invisible.do_selection_request_event(self, event) return atoms = self.prop_get(event.window, event.property, ["multiple-conversion"]) log("MULTIPLE clipboard atoms: %r", atoms) if atoms: targets = atoms[::2] for t in targets: self.selection_add_target(self._selection, t, 0) else: log("target for %s: %r", self._selection, target) self.selection_add_target(self._selection, target, 0) log("do_selection_request_event(%s) target=%s, selection=%s", event, target, self._selection) gtk.Invisible.do_selection_request_event(self, event) # This function is called by GTK+ when we own the clipboard and a local # app is requesting its contents: def do_selection_get(self, selection_data, info, time): # Either call selection_data.set() or don't, and then return. # In practice, send a call across the wire, then block in a recursive # main loop. if not self._enabled: return log("do_selection_get(%s, %s, %s) selection=%s", selection_data, info, time, selection_data.selection) self._selection_get_events += 1 assert self._selection == str(selection_data.selection) target = str(selection_data.target) self._request_contents_events += 1 result = self.emit("get-clipboard-from-remote", self._selection, target) if result is None or result["type"] is None: log("remote selection fetch timed out or empty") selection_data.set("STRING", 8, "") return data = result["data"] dformat = result["format"] dtype = result["type"] log( "do_selection_get(%s,%s,%s) calling selection_data.set(%s, %s, %s:%s)", selection_data, info, time, dtype, dformat, type(data), len(data or "")) selection_data.set(dtype, dformat, data) def do_selection_clear_event(self, event): # Someone else on our side has the selection log( "do_selection_clear_event(%s) have_token=%s, block_owner_change=%s selection=%s", event, self._have_token, self._block_owner_change, self._selection) self._selection_clear_events += 1 if self._enabled: #if greedy_client is set, do_owner_changed will fire the token #so don't bother sending it now (same if we don't have it) send = ((self._greedy_client and not self._block_owner_change) or self._have_token) self._have_token = False # Emit a signal -> send a note to the other side saying "hey its # ours now" # Send off the anti-token. if send: boc = self._block_owner_change self._block_owner_change = True self.emit("send-clipboard-token", self._selection) if boc is False: gobject.idle_add(self.remove_block) gtk.Invisible.do_selection_clear_event(self, event) def got_token(self, targets, target_data): # We got the anti-token. log("got token, selection=%s, targets=%s, target_data=%s", self._selection, targets, target_data) if not self._enabled: return self._got_token_events += 1 self._have_token = True if self._greedy_client: self._block_owner_change = True self.claim() if self._block_owner_change: #re-enable the flag via idle_add so events like do_owner_changed #get a chance to run first. gobject.idle_add(self.remove_block) def remove_block(self, *args): log("remove_block(%s)", args) self._block_owner_change = False def claim(self): if self._enabled and not self.selection_owner_set(self._selection): # I don't know how this can actually fail, given that we pass # CurrentTime, but just in case: log.warn("Failed to acquire local clipboard %s; " % (self._selection, ) + "will not be able to pass local apps " + "contents of remote clipboard") # This function is called by the xpra core when the peer has requested the # contents of this clipboard: def get_contents(self, target, cb): log("get_contents(%s,%s) selection=%s", target, cb, self._selection) if not self._enabled: cb(None, None, None) return self._get_contents_events += 1 if self._have_token: log.warn("Our peer requested the contents of the clipboard, but " + "*I* thought *they* had it... weird.") cb(None, None, None) return if target == "TARGETS": #handle TARGETS using "request_targets" def got_targets(c, targets, *args): log("got_targets(%s, %s, %s)", c, targets, args) cb("ATOM", 32, targets) self._clipboard.request_targets(got_targets) return def unpack(clipboard, selection_data, user_data): log("unpack %s: %s", clipboard, type(selection_data)) if selection_data is None: cb(None, None, None) return log("unpack: %s", selection_data) data = selection_data.data log("unpack(..) type=%s, format=%s, data=%s:%s", selection_data.type, selection_data.format, type(data), len(data or "")) if self._strip_nullbyte and selection_data.type in ( "UTF8_STRING", "STRING") and selection_data.format == 8: #we may have to strip the nullbyte: if data and data[-1] == '\0': log("stripping end of string null byte") data = data[:-1] cb(str(selection_data.type), selection_data.format, data) self._clipboard.request_contents(target, unpack)
class ClipboardProxy(ClipboardProxyCore, gobject.GObject): __gsignals__ = { "xpra-client-message-event": one_arg_signal, "xpra-selection-request": one_arg_signal, "xpra-selection-clear": one_arg_signal, "xpra-property-notify-event": one_arg_signal, "xpra-xfixes-selection-notify-event": one_arg_signal, # "send-clipboard-token": one_arg_signal, "send-clipboard-request": n_arg_signal(2), } def __init__(self, xid, selection="CLIPBOARD"): ClipboardProxyCore.__init__(self, selection) gobject.GObject.__init__(self) self.xid = xid self.owned = False self._want_targets = False self.remote_requests = {} self.local_requests = {} self.local_request_counter = 0 self.targets = () self.target_data = {} self.reset_incr_data() def __repr__(self): return "X11ClipboardProxy(%s)" % self._selection def cleanup(self): log("%s.cleanup()", self) #give up selection: #(disabled because this crashes GTK3 on exit) #if self.owned: # self.owned = False # with xswallow: # X11Window.XSetSelectionOwner(0, self._selection) #empty replies for all pending requests, #this will also cancel any pending timers: rr = self.remote_requests self.remote_requests = {} for target in rr: self.got_contents(target) lr = self.local_requests self.local_requests = {} for target in lr: self.got_local_contents(target) def init_uuid(self): ClipboardProxyCore.init_uuid(self) self.claim() def set_want_targets(self, want_targets): self._want_targets = want_targets def got_token(self, targets, target_data=None, claim=True, synchronous_client=False): # the remote end now owns the clipboard self.cancel_emit_token() if not self._enabled: return self._got_token_events += 1 log( "got token, selection=%s, targets=%s, target data=%s, claim=%s, can-receive=%s", self._selection, targets, target_data, claim, self._can_receive) if claim: self._have_token = True if self._can_receive: self.targets = tuple(bytestostr(x) for x in (targets or ())) self.target_data = target_data or {} if targets and claim: xatoms = strings_to_xatoms(targets) self.got_contents("TARGETS", "ATOM", 32, xatoms) if target_data and synchronous_client and claim: targets = target_data.keys() text_targets = tuple(x for x in targets if x in TEXT_TARGETS) if text_targets: target = text_targets[0] dtype, dformat, data = target_data.get(target) dtype = bytestostr(dtype) self.got_contents(target, dtype, dformat, data) if self._can_receive and claim: self.claim() def claim(self, time=0): try: with xsync: setsel = X11Window.XSetSelectionOwner(self.xid, self._selection, time) log( "claim_selection: set selection owner returned %s, owner=%#x", setsel, X11Window.XGetSelectionOwner(self._selection)) event_mask = StructureNotifyMask log("claim_selection: sending message to root window") owner = X11Window.XGetSelectionOwner(self._selection) self.owned = owner == self.xid if not self.owned: log.warn( "we failed to get ownership of the '%s' selection", self._selection) else: #send announcement: root = get_default_root_window() root_xid = get_xwindow(root) X11Window.sendClientMessage(root_xid, root_xid, False, event_mask, "MANAGER", CurrentTime, self._selection, self.xid) log("claim_selection: done, owned=%s", self.owned) except Exception: log("failed to claim selection '%s'", self._selection, exc_info=True) raise def do_xpra_client_message_event(self, event): if event.message_type == "_GTK_LOAD_ICONTHEMES": #ignore this crap return log.info("clipboard window %#x received an X11 message", get_xwindow(event.window)) log.info(" %s", event) def get_wintitle(self, xid): data = X11Window.XGetWindowProperty(xid, "WM_NAME", "STRING") if data: return data.decode("latin1") data = X11Window.XGetWindowProperty(xid, "_NET_WM_NAME", "STRING") if data: return data.decode("utf8") xid = X11Window.getParent(xid) return None def get_wininfo(self, xid): with xswallow: title = self.get_wintitle(xid) if title: return "'%s'" % title with xswallow: while xid: title = self.get_wintitle(xid) if title: return "child of '%s'" % title xid = X11Window.getParent(xid) return hex(xid) ############################################################################ # forward local requests to the remote clipboard: ############################################################################ def do_selection_request_event(self, event): #an app is requesting clipboard data from us log("do_selection_request_event(%s)", event) requestor = event.requestor assert requestor wininfo = self.get_wininfo(get_xwindow(requestor)) prop = event.property target = str(event.target) log("clipboard request for %s from window %#x: %s, target=%s, prop=%s", self._selection, get_xwindow(requestor), wininfo, target, prop) def nodata(): self.set_selection_response(requestor, target, prop, "STRING", 8, b"", time=event.time) if not self._enabled: nodata() return if wininfo and wininfo.strip("'") in BLACKLISTED_CLIPBOARD_CLIENTS: if first_time("clipboard-blacklisted:%s" % wininfo.strip("'")): log.warn( "receiving clipboard requests from blacklisted client %s", wininfo) log.warn(" all requests will be silently ignored") log("responding with nodata for blacklisted client '%s'", wininfo) nodata() return if not self.owned: log.warn("Warning: clipboard selection request received,") log.warn(" but we don't own the selection,") log.warn(" sending an empty reply") nodata() return if not self._can_receive: log.warn("Warning: clipboard selection request received,") log.warn(" but receiving remote data is disabled,") log.warn(" sending an empty reply") nodata() return if must_discard(target): log.info("clipboard %s discarding invalid target '%s'", self._selection, target) nodata() return if target == "TARGETS": if self.targets: log("using existing TARGETS value as response: %s", self.targets) xatoms = strings_to_xatoms(self.targets) self.set_selection_response(requestor, target, prop, "ATOM", 32, xatoms, event.time) return if "TARGETS" not in self.remote_requests: self.emit("send-clipboard-request", self._selection, "TARGETS") #when appending, the time may not be honoured #and we may reply with data from an older request self.remote_requests.setdefault("TARGETS", []).append( (requestor, prop, event.time)) return if self.targets and target not in self.targets: log.info("client is requesting an unknown target: '%s'", target) translated_targets = TRANSLATED_TARGETS.get(target, ()) can_translate = tuple(x for x in translated_targets if x in self.targets) if can_translate: target = can_translate[0] log.info(" using '%s' instead", target) else: log.info(" valid targets: %s", csv(self.targets)) if must_discard_extra(target): log.info(" dropping the request") nodata() return target_data = self.target_data.get(target) if target_data and not self._have_token: #we have it already dtype, dformat, data = target_data dtype = bytestostr(dtype) log("setting target data for '%s': %s, %s, %s (%s)", target, dtype, dformat, repr_ellipsized(str(data)), type(data)) self.set_selection_response(requestor, target, prop, dtype, dformat, data, event.time) return if target not in self.remote_requests: self.emit("send-clipboard-request", self._selection, target) self.remote_requests.setdefault(target, []).append( (requestor, prop, event.time)) def set_selection_response(self, requestor, target, prop, dtype, dformat, data, time=0): log("set_selection_response(%s, %s, %s, %s, %s, %r, %i)", requestor, target, prop, dtype, dformat, repr_ellipsized(bytestostr(data)), time) #answer the selection request: with xsync: xid = get_xwindow(requestor) if data is not None: X11Window.XChangeProperty(xid, prop, dtype, dformat, data) else: #maybe even delete the property? #X11Window.XDeleteProperty(xid, prop) prop = None X11Window.sendSelectionNotify(xid, self._selection, target, prop, time) def got_contents(self, target, dtype=None, dformat=None, data=None): #if this is the special target 'TARGETS', cache the result: if target == "TARGETS" and dtype == "ATOM" and dformat == 32: self.targets = xatoms_to_strings(data) #the remote peer sent us a response, #find all the pending requests for this target #and give them the response they are waiting for: pending = self.remote_requests.pop(target, []) log("got_contents%s pending=%s", (target, dtype, dformat, repr_ellipsized(str(data))), csv(pending)) for requestor, prop, time in pending: if log.is_debug_enabled(): log("setting response %s to property %s of window %s as %s", repr_ellipsized(bytestostr(data)), prop, self.get_wininfo(get_xwindow(requestor)), dtype) self.set_selection_response(requestor, target, prop, dtype, dformat, data, time) ############################################################################ # local clipboard events, which may or may not be sent to the remote end ############################################################################ def do_selection_notify_event(self, event): owned = self.owned self.owned = event.owner and get_xwindow(event.owner) == self.xid log( "do_selection_notify_event(%s) owned=%s, was %s, enabled=%s, can-send=%s", event, self.owned, owned, self._enabled, self._can_send) if not self._enabled: return if self.owned or not self._can_send: return self.schedule_emit_token() def schedule_emit_token(self): if not (self._want_targets or self._greedy_client): self._have_token = False self.emit("send-clipboard-token", ()) return #we need the targets, and the target data for greedy clients: def send_token_with_targets(): token_data = (self.targets, ) self._have_token = False self.emit("send-clipboard-token", token_data) def with_targets(targets): if not self._greedy_client: send_token_with_targets() return #find the preferred targets: targets = self.choose_targets(targets) if not targets: send_token_with_targets() return target = targets[0] def got_text_target(dtype, dformat, data): log("got_text_target(%s, %s, %s)", dtype, dformat, repr_ellipsized(str(data))) if not (dtype and dformat and data): send_token_with_targets() return token_data = (targets, (target, dtype, dformat, data)) self._have_token = False self.emit("send-clipboard-token", token_data) self.get_contents(target, got_text_target) if self.targets: with_targets(self.targets) return def got_targets(dtype, dformat, data): assert dtype == "ATOM" and dformat == 32 self.targets = xatoms_to_strings(data) log("got_targets: %s", self.targets) with_targets(self.targets) self.get_contents("TARGETS", got_targets) def choose_targets(self, targets): if self.preferred_targets: #prefer PNG, but only if supported by the client: if "image/png" in targets and "image/png" in self.preferred_targets: return ("image/png", ) #if we can't choose a text target, at least choose a supported one: if not any(x for x in targets if x in TEXT_TARGETS and x in self.preferred_targets): return tuple(x for x in targets if x in self.preferred_targets) #otherwise choose a text target: return tuple(x for x in targets if x in TEXT_TARGETS) def do_selection_clear_event(self, event): log("do_xpra_selection_clear(%s) was owned=%s", event, self.owned) if not self._enabled: return self.owned = False self.do_owner_changed() def do_owner_changed(self): log("do_owner_changed()") if not self._enabled: return self.target_data = {} self.targets = () def get_contents(self, target, got_contents, time=0): log("get_contents(%s, %s, %i) owned=%s, have-token=%s", target, got_contents, time, self.owned, self._have_token) if target == "TARGETS": if self.targets: xatoms = strings_to_xatoms(self.targets) got_contents("ATOM", 32, xatoms) return else: target_data = self.target_data.get(target) if target_data: dtype, dformat, value = target_data got_contents(dtype, dformat, value) return prop = "%s-%s" % (self._selection, target) with xsync: owner = X11Window.XGetSelectionOwner(self._selection) self.owned = owner == self.xid if self.owned: #we are the clipboard owner! log("we are the %s selection owner, using empty reply", self._selection) got_contents(None, None, None) return request_id = self.local_request_counter self.local_request_counter += 1 timer = glib.timeout_add(CONVERT_TIMEOUT, self.timeout_get_contents, target, request_id) self.local_requests.setdefault( target, {})[request_id] = (timer, got_contents, time) log("requesting local XConvertSelection from %s as '%s' into '%s'", self.get_wininfo(owner), target, prop) X11Window.ConvertSelection(self._selection, target, prop, self.xid, time=time) def timeout_get_contents(self, target, request_id): try: target_requests = self.local_requests.get(target) if target_requests is None: return timer, got_contents, time = target_requests.pop(request_id) if not target_requests: del self.local_requests[target] except KeyError: return glib.source_remove(timer) log.warn("Warning: %s selection request for '%s' timed out", self._selection, target) log.warn(" request %i at time=%i", request_id, time) if target == "TARGETS": got_contents("ATOM", 32, b"") else: got_contents(None, None, None) def do_property_notify(self, event): log("do_property_notify(%s)", event) if not self._enabled: return #ie: atom="PRIMARY-TARGETS", atom="PRIMARY-STRING" parts = event.atom.split("-", 1) assert len(parts) == 2 #selection = parts[0] #ie: PRIMARY target = parts[1] #ie: VALUE dtype = "" dformat = 8 try: with xsync: dtype, dformat = X11Window.GetWindowPropertyType( self.xid, event.atom, True) dtype = bytestostr(dtype) MAX_DATA_SIZE = 4 * 1024 * 1024 data = X11Window.XGetWindowProperty(self.xid, event.atom, dtype, None, MAX_DATA_SIZE, True) #all the code below deals with INCRemental transfers: if dtype == "INCR" and not self.incr_data_size: #start of an incremental transfer, extract the size assert dformat == 32 self.incr_data_size = struct.unpack("@L", data)[0] self.incr_data_chunks = [] self.incr_data_type = None log("incremental clipboard data of size %s", self.incr_data_size) self.reschedule_incr_data_timer() return if self.incr_data_size > 0: #incremental is now in progress: if not self.incr_data_type: self.incr_data_type = dtype elif self.incr_data_type != dtype: log.error("Error: invalid change of data type") log.error(" from %s to %s", self.incr_data_type, dtype) self.reset_incr_data() self.cancel_incr_data_timer() return if data: log("got incremental data: %i bytes", len(data)) self.incr_data_chunks.append(data) self.reschedule_incr_data_timer() return self.cancel_incr_data_timer() data = b"".join(self.incr_data_chunks) log( "got incremental data termination, total size=%i bytes", len(data)) self.reset_incr_data() self.got_local_contents(target, dtype, dformat, data) return except PropertyError: log("do_property_notify() property '%s' is gone?", event.atom, exc_info=True) return log("%s=%s (%s : %s)", event.atom, repr_ellipsized(bytestostr(data)), dtype, dformat) if target == "TARGETS": self.targets = xatoms_to_strings(data or b"") self.got_local_contents(target, dtype, dformat, data) def got_local_contents(self, target, dtype=None, dformat=None, data=None): data = self.filter_data(target, dtype, dformat, data) target_requests = self.local_requests.pop(target, {}) for timer, got_contents, time in target_requests.values(): if log.is_debug_enabled(): log("got_local_contents: calling %s%s, time=%i", got_contents, (dtype, dformat, repr_ellipsized(str(data))), time) glib.source_remove(timer) got_contents(dtype, dformat, data) def filter_data(self, target, dtype=None, dformat=None, data=None): log("filter_data(%s, %s, %s, ..)", target, dtype, dformat) IMAGE_OVERLAY = os.environ.get("XPRA_CLIPBOARD_IMAGE_OVERLAY", None) if IMAGE_OVERLAY and not os.path.exists(IMAGE_OVERLAY): IMAGE_OVERLAY = None IMAGE_STAMP = envbool("XPRA_CLIPBOARD_IMAGE_STAMP", True) if dtype in ("image/png", ) and (IMAGE_STAMP or IMAGE_OVERLAY): from xpra.codecs.pillow.decoder import open_only img = open_only(data, ("png", )) has_alpha = img.mode == "RGBA" if not has_alpha and IMAGE_OVERLAY: img = img.convert("RGBA") w, h = img.size if IMAGE_OVERLAY: from PIL import Image #@UnresolvedImport overlay = Image.open(IMAGE_OVERLAY) if overlay.mode != "RGBA": log.warn("Warning: cannot use overlay image '%s'", IMAGE_OVERLAY) log.warn(" invalid mode '%s'", overlay.mode) else: log("adding clipboard image overlay to %s", dtype) overlay_resized = overlay.resize((w, h), Image.ANTIALIAS) composite = Image.alpha_composite(img, overlay_resized) if not has_alpha and img.mode == "RGBA": composite = composite.convert("RGB") img = composite if IMAGE_STAMP: log("adding clipboard image stamp to %s", dtype) from datetime import datetime from PIL import ImageDraw img_draw = ImageDraw.Draw(img) w, h = img.size img_draw.text((10, max(0, h // 2 - 16)), 'via Xpra, %s' % datetime.now().isoformat(), fill='black') buf = BytesIO() img.save(buf, "PNG") data = buf.getvalue() buf.close() return data def reschedule_incr_data_timer(self): self.cancel_incr_data_timer() self.incr_data_timer = glib.timeout_add(1 * 1000, self.incr_data_timeout) def cancel_incr_data_timer(self): idt = self.incr_data_timer if idt: self.incr_data_timer = None glib.source_remove(idt) def incr_data_timeout(self): self.incr_data_timer = None log.warn("Warning: incremental data timeout") self.incr_data = None def reset_incr_data(self): self.incr_data_size = 0 self.incr_data_type = None self.incr_data_chunks = None self.incr_data_timer = None
class SoundSource(SoundPipeline): __gsignals__ = SoundPipeline.__generic_signals__.copy() __gsignals__.update({ "new-buffer": n_arg_signal(2), }) def __init__(self, src_type=None, src_options={}, codecs=CODECS, codec_options={}, volume=1.0): if not src_type: from xpra.sound.pulseaudio_util import get_pa_device_options monitor_devices = get_pa_device_options(True, False) log.info("found pulseaudio monitor devices: %s", monitor_devices) if len(monitor_devices) == 0: log.warn( "could not detect any pulseaudio monitor devices - will use a test source" ) src_type = "audiotestsrc" default_src_options = {"wave": 2, "freq": 100, "volume": 0.4} else: monitor_device = monitor_devices.items()[0][0] log.info("using pulseaudio source device: %s", monitor_device) src_type = "pulsesrc" default_src_options = {"device": monitor_device} src_options = default_src_options src_options.update(src_options) assert src_type in get_source_plugins( ), "invalid source plugin '%s'" % src_type matching = [x for x in CODEC_ORDER if (x in codecs and x in CODECS)] log("SoundSource(..) found matching codecs %s", matching) assert len( matching ) > 0, "no matching codecs between arguments %s and supported list %s" % ( codecs, CODECS) codec = matching[0] encoder, fmt = get_encoder_formatter(codec) SoundPipeline.__init__(self, codec) self.src_type = src_type source_str = plugin_str(src_type, src_options) encoder_str = plugin_str(encoder, codec_options) fmt_str = plugin_str(fmt, MUXER_DEFAULT_OPTIONS.get(fmt, {})) pipeline_els = [source_str] if AUDIOCONVERT: pipeline_els += ["audioconvert"] if AUDIORESAMPLE: pipeline_els += [ "audioresample", "audio/x-raw-int,rate=44100,channels=2" ] pipeline_els.append("volume name=volume volume=%s" % volume) if QUEUE_TIME > 0: queue_el = [ "queue", "name=queue", "max-size-buffers=0", "max-size-bytes=0", "max-size-time=%s" % QUEUE_TIME, "leaky=%s" % QUEUE_LEAK ] pipeline_els.append(" ".join(queue_el)) pipeline_els += [encoder_str, fmt_str, "appsink name=sink"] self.setup_pipeline_and_bus(pipeline_els) self.volume = self.pipeline.get_by_name("volume") self.sink = self.pipeline.get_by_name("sink") self.sink.set_property("emit-signals", True) self.sink.set_property("max-buffers", 10) #0? self.sink.set_property("drop", False) self.sink.set_property("sync", True) #False? self.sink.set_property("qos", False) try: #Gst 1.0: self.sink.connect("new-sample", self.on_new_sample) self.sink.connect("new-preroll", self.on_new_preroll1) except: #Gst 0.10: self.sink.connect("new-buffer", self.on_new_buffer) self.sink.connect("new-preroll", self.on_new_preroll0) def __repr__(self): return "SoundSource('%s' - %s)" % (self.pipeline_str, self.state) def cleanup(self): SoundPipeline.cleanup(self) self.src_type = "" self.sink = None def on_new_preroll1(self, appsink): sample = appsink.emit('pull-preroll') log('new preroll1: %s', sample) self.emit_buffer1(sample) def on_new_sample(self, bus): #Gst 1.0 sample = self.sink.emit("pull-sample") self.emit_buffer1(sample) def emit_buffer1(self, sample): buf = sample.get_buffer() #info = sample.get_info() size = buf.get_size() data = buf.extract_dup(0, size) self.do_emit_buffer(data, { "timestamp": normv(buf.pts), "duration": normv(buf.duration), }) def on_new_preroll0(self, appsink): buf = appsink.emit('pull-preroll') log('new preroll0: %s bytes', len(buf)) self.emit_buffer0(buf) def on_new_buffer(self, bus): #pygst 0.10 buf = self.sink.emit("pull-buffer") self.emit_buffer0(buf) def emit_buffer0(self, buf, metadata={}): """ convert pygst structure into something more generic for the wire """ #none of the metadata is really needed at present, but it may be in the future: #metadata = {"caps" : buf.get_caps().to_string(), # "size" : buf.size, # "timestamp" : buf.timestamp, # "duration" : buf.duration, # "offset" : buf.offset, # "offset_end": buf.offset_end} self.do_emit_buffer( buf.data, { #"caps" : buf.get_caps().to_string(), "timestamp": normv(buf.timestamp), "duration": normv(buf.duration) }) def do_emit_buffer(self, data, metadata={}): self.buffer_count += 1 self.byte_count += len(data) metadata["time"] = int(time.time() * 1000) self.idle_emit("new-buffer", data, metadata) self.emit_info()