Exemple #1
0
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)
Exemple #2
0
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)
Exemple #3
0
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)
Exemple #4
0
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)
Exemple #5
0
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)
Exemple #6
0
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)
Exemple #7
0
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
Exemple #8
0
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)
Exemple #9
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.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
Exemple #10
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
Exemple #11
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
Exemple #12
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)
Exemple #13
0
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
Exemple #14
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=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()