16 | | def which(name): |
17 | | if sys.platform.startswith("win"): |
18 | | return "" |
19 | | cmd = ["which", name] |
20 | | try: |
21 | | returncode, out, _ = safe_exec(cmd, log_errors=False) |
22 | | log("safe_exec(%s)=%s", cmd, (returncode, out)) |
23 | | if returncode!=0 or not out: |
24 | | return "" |
25 | | c = out.decode("utf8").replace("\n", "").replace("\r", "") |
26 | | if os.path.exists(c) and os.path.isfile(c): |
27 | | if os.name=="posix" and not os.access(c, os.X_OK): |
28 | | #odd, it's there but we can't run it!? |
29 | | return "" |
30 | | return c |
31 | | return "" |
32 | | except: |
33 | | log.error("which(%s) error", name, exc_info=True) |
34 | | return "" |
35 | | |
36 | | pactl_bin = None |
37 | | has_pulseaudio = None |
38 | | |
39 | | def get_pactl_bin(): |
40 | | global pactl_bin |
41 | | if pactl_bin is None: |
42 | | if sys.platform.startswith("win") or sys.platform.startswith("darwin"): |
43 | | pactl_bin = "" |
44 | | else: |
45 | | pactl_bin = which("pactl") |
46 | | return pactl_bin |
47 | | |
48 | | def pactl_output(log_errors=True, *pactl_args): |
49 | | pactl_bin = get_pactl_bin() |
50 | | if not pactl_bin: |
51 | | return -1, None |
52 | | #ie: "pactl list" |
53 | | cmd = [pactl_bin] + list(pactl_args) |
54 | | try: |
55 | | code, out, _ = safe_exec(cmd, log_errors=log_errors) |
56 | | log("pactl_output%s returned %s", pactl_args, code) |
57 | | return code, out |
58 | | except Exception as e: |
59 | | if log_errors: |
60 | | log.error("failed to execute %s: %s", cmd, e) |
61 | | else: |
62 | | log("failed to execute %s: %s", cmd, e) |
63 | | return -1, None |
64 | | |
65 | | def get_x11_property(atom_name): |
66 | | if sys.platform.startswith("darwin") or sys.platform.startswith("win"): |
67 | | return "" |
68 | | try: |
69 | | from gtk import gdk |
70 | | root = gdk.get_default_root_window() |
71 | | atom = gdk.atom_intern(atom_name) |
72 | | p = root.property_get(atom) |
73 | | if p is None: |
74 | | return "" |
75 | | v = p[2] |
76 | | log("get_x11_property(%s)=%s", atom_name, v) |
77 | | return v |
78 | | except: |
79 | | return "" |
80 | | |
81 | | def has_pa_x11_property(): |
82 | | #try root window property (faster) |
83 | | ps = get_x11_property(b"PULSE_SERVER") |
84 | | log("has_pa_x11_property: get_x11_property(PULSE_SERVER)=%s", ps) |
85 | | return len(ps)>0 |
86 | | |
87 | | def is_pa_installed(): |
88 | | pactl_bin = get_pactl_bin() |
89 | | log("is_pa_installed() pactl_bin=%s", pactl_bin) |
90 | | return len(pactl_bin)>0 |
91 | | |
92 | | def has_pa(): |
93 | | global has_pulseaudio |
94 | | if has_pulseaudio is None: |
95 | | has_pulseaudio = has_pa_x11_property() or is_pa_installed() |
96 | | return has_pulseaudio |
97 | | |
98 | | |
99 | | def set_source_mute(device, mute=False): |
100 | | code, out = pactl_output(True, "set-source-mute", device, str(int(mute))) |
101 | | log("set_source_mute: output=%s", out) |
102 | | return code==0 |
103 | | |
104 | | def get_pactl_stat_line(prefix): |
105 | | if not has_pa(): |
106 | | return "" |
107 | | code, out = pactl_output(True, "stat") |
108 | | if code!=0: |
109 | | return "" |
110 | | stat = "" |
111 | | for line in out.splitlines(): |
112 | | if line.startswith(prefix): |
113 | | stat = line[len(prefix):].strip() |
114 | | break |
115 | | log("get_pactl_stat_line(%s)=%s", prefix, stat) |
116 | | return stat |
117 | | |
118 | | def get_default_sink(): |
119 | | return get_pactl_stat_line("Default Sink:") |
120 | | |
121 | | def get_pactl_server(): |
122 | | return get_pactl_stat_line("Server String:") |
123 | | |
124 | | |
125 | | def get_pulse_server(may_start_it=True): |
126 | | xp = get_x11_property("PULSE_SERVER") |
127 | | if xp or not may_start_it: |
128 | | return xp |
129 | | return get_pactl_server() |
130 | | |
131 | | def get_pulse_id(): |
132 | | return get_x11_property("PULSE_ID") |
133 | | |
134 | | |
147 | | def get_pa_device_options(monitors=False, input_or_output=None, ignored_devices=["bell-window-system"], log_errors=True): |
148 | | """ |
149 | | Finds the list of devices, monitors=False allows us to filter out monitors |
150 | | (which could create sound loops if we use them) |
151 | | set input_or_output=True to get inputs only |
152 | | set input_or_output=False to get outputs only |
153 | | set input_or_output=None to get both |
154 | | Same goes for monitors (False|True|None) |
155 | | Returns the a dict() with the PulseAudio name as key and a description as value |
156 | | """ |
157 | | if sys.platform.startswith("win") or sys.platform.startswith("darwin"): |
158 | | return {} |
159 | | status, out = pactl_output(False, "list") |
160 | | if status!=0 or not out: |
161 | | return {} |
162 | | device_class = None |
163 | | device_description = None |
164 | | name = None |
165 | | devices = {} |
166 | | for line in out.splitlines(): |
167 | | if not line.startswith(" ") and not line.startswith("\t"): #clear vars when we encounter a new section |
168 | | if name and device_class: |
169 | | if name in ignored_devices: |
170 | | continue |
171 | | #Verify against monitor flag if set: |
172 | | if monitors is not None: |
173 | | is_monitor = device_class=='"monitor"' |
174 | | if is_monitor!=monitors: |
175 | | continue |
176 | | #Verify against input flag (if set): |
177 | | if input_or_output is not None: |
178 | | is_input = name.find("input")>=0 |
179 | | if is_input is True and input_or_output is False: |
180 | | continue |
181 | | is_output = name.find("output")>=0 |
182 | | if is_output is True and input_or_output is True: |
183 | | continue |
184 | | if not device_description: |
185 | | device_description = name |
186 | | devices[name] = device_description |
187 | | name = None; device_class = None |
188 | | line = line.strip() |
189 | | if line.startswith("Name: "): |
190 | | name = line[len("Name: "):] |
191 | | if line.startswith("device.class = "): |
192 | | device_class = line[len("device-class = "):] |
193 | | if line.startswith("device.description = "): |
194 | | device_description = line[len("device.description = "):].strip('"') |
195 | | return devices |
| 26 | #prefer the palib option which does everything in process: |
| 27 | try: |
| 28 | if os.environ.get("XPRA_USE_PACTL", "0")=="1": |
| 29 | raise ImportError("environment override: not using palib") |
| 30 | from xpra.sound import pulseaudio_palib_util as _pulseaudio_util |
| 31 | except ImportError as e: |
| 32 | #fallback forks a process and parses the output: |
| 33 | log.warn("palib not available, using legacy pactl fallback") |
| 34 | from xpra.sound import pulseaudio_pactl_util as _pulseaudio_util #@Reimport |
197 | | def get_info(): |
198 | | info = { |
199 | | "pulseaudio.id" : get_pulse_id(), |
200 | | "pulseaudio.server" : get_pulse_server(False), |
201 | | } |
202 | | i = 0 |
203 | | for monitors in (True, False): |
204 | | for io in (True, False): |
205 | | devices = get_pa_device_options(monitors, io, log_errors=False) |
206 | | for d,name in devices.items(): |
207 | | info["device.%s" % d] = name |
208 | | i += 1 |
209 | | info["devices"] = i |
210 | | return info |
| 36 | get_info = _pulseaudio_util.get_info |
| 37 | has_pa = _pulseaudio_util.has_pa |
| 38 | get_pa_device_options = _pulseaudio_util.get_pa_device_options |
| 39 | get_default_sink = _pulseaudio_util.get_default_sink |
| 40 | get_pulse_server = _pulseaudio_util.get_pulse_server |
| 41 | get_pulse_id = _pulseaudio_util.get_pulse_id |
| 42 | set_source_mute = _pulseaudio_util.set_source_mute |
| 43 | get_pa_device_options = _pulseaudio_util.get_pa_device_options |