Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# This file is part of Xpra. 

2# Copyright (C) 2015-2020 Antoine Martin <antoine@xpra.org> 

3# Xpra is released under the terms of the GNU GPL v2, or, at your option, any 

4# later version. See the file COPYING for details. 

5 

6import os 

7import sys 

8 

9from collections import namedtuple 

10from xpra.sound.gstreamer_util import ( 

11 parse_sound_source, get_source_plugins, get_sink_plugins, get_default_sink_plugin, get_default_source, 

12 import_gst, format_element_options, 

13 can_decode, can_encode, get_muxers, get_demuxers, get_all_plugin_names, 

14 ) 

15from xpra.net.subprocess_wrapper import subprocess_caller, subprocess_callee, exec_kwargs, exec_env 

16from xpra.platform.paths import get_sound_command 

17from xpra.os_util import WIN32, OSX, POSIX, BITS, monotonic_time, bytestostr 

18from xpra.util import typedict, parse_simple_dict, envint, envbool 

19from xpra.scripts.config import InitExit, InitException 

20from xpra.log import Logger 

21log = Logger("sound") 

22 

23DEBUG_SOUND = envbool("XPRA_SOUND_DEBUG", False) 

24SUBPROCESS_DEBUG = tuple(x.strip() for x in os.environ.get("XPRA_SOUND_SUBPROCESS_DEBUG", "").split(",") if x.strip()) 

25FAKE_START_FAILURE = envbool("XPRA_SOUND_FAKE_START_FAILURE", False) 

26FAKE_EXIT = envbool("XPRA_SOUND_FAKE_EXIT", False) 

27FAKE_CRASH = envbool("XPRA_SOUND_FAKE_CRASH", False) 

28SOUND_START_TIMEOUT = envint("XPRA_SOUND_START_TIMEOUT", 5000) 

29BUNDLE_METADATA = envbool("XPRA_SOUND_BUNDLE_METADATA", True) 

30 

31DEFAULT_SOUND_COMMAND_ARGS = os.environ.get("XPRA_DEFAULT_SOUND_COMMAND_ARGS", 

32 "--windows=no "+ 

33 "--video-encoders=none "+ 

34 "--csc-modules=none "+ 

35 "--video-decoders=none "+ 

36 "--proxy-video-encoders=none").split(" ") 

37 

38 

39def get_full_sound_command(): 

40 return get_sound_command()+DEFAULT_SOUND_COMMAND_ARGS 

41 

42 

43def get_sound_wrapper_env(): 

44 env = {} 

45 if WIN32: 

46 #disable bencoder to skip warnings with the py3k Sound subapp 

47 env["XPRA_USE_BENCODER"] = "0" 

48 #we don't want the output to go to a log file 

49 env["XPRA_REDIRECT_OUTPUT"] = "0" 

50 elif POSIX and not OSX: 

51 try: 

52 from xpra.sound.pulseaudio.pulseaudio_util import add_audio_tagging_env 

53 add_audio_tagging_env(env) 

54 except ImportError as e: 

55 log.warn("Warning: failed to set pulseaudio tagging:") 

56 log.warn(" %s", e) 

57 return env 

58 

59 

60#this wrapper takes care of launching src.py or sink.py 

61# 

62#the command line should look something like: 

63# xpra MODE IN OUT PLUGIN PLUGIN_OPTIONS CODECS CODEC_OPTIONS VOLUME 

64# * MODE can be _sound_record or _sound_play 

65# * IN is where we read the encoded commands from, specify "-" for stdin 

66# * OUT is where we write the encoded output stream, specify "-" for stdout 

67# * PLUGIN is the sound source (for recording) or sink (for playing) to use, can be omitted (will be auto detected) 

68# ie: pulsesrc, autoaudiosink 

69# * PLUGIN_OPTIONS is a string containing options specific to this plugin 

70# ie: device=somedevice,otherparam=somevalue 

71# * CODECS: the list of codecs that we are willing to support 

72# ie: mp3,flac 

73# * CODECS_OPTIONS: a string containing options to apply to the codec 

74# ie: blocksize=1024,otherparam=othervalue 

75# * VOLUME: optional, a number from 0.0 to 1.0 

76# ie: 1.0 

77# FIXME: CODEC_OPTIONS should allow us to specify different options for each CODEC 

78# The output will be a regular xpra packet, containing serialized signals that we receive 

79# The input can be a regular xpra packet, those are converted into method calls 

80 

81class sound_subprocess(subprocess_callee): 

82 """ Utility superclass for sound subprocess wrappers 

83 (see sound_record and sound_play below) 

84 """ 

85 def __init__(self, wrapped_object, method_whitelist, exports_list): 

86 #add bits common to both record and play: 

87 methods = method_whitelist+["set_volume", "cleanup"] 

88 exports = ["state-changed", "info", "error"] + exports_list 

89 super().__init__(wrapped_object=wrapped_object, method_whitelist=methods) 

90 for x in exports: 

91 self.connect_export(x) 

92 

93 def start(self): 

94 if not FAKE_START_FAILURE: 

95 self.idle_add(self.wrapped_object.start) 

96 if FAKE_EXIT>0: 

97 def process_exit(): 

98 self.cleanup() 

99 self.timeout_add(250, self.stop) 

100 self.timeout_add(FAKE_EXIT*1000, process_exit) 

101 if FAKE_CRASH>0: 

102 def force_exit(): 

103 sys.exit(1) 

104 self.timeout_add(FAKE_CRASH*1000, force_exit) 

105 super().start() 

106 

107 def cleanup(self): 

108 wo = self.wrapped_object 

109 log("cleanup() wrapped object=%s", wo) 

110 if wo: 

111 #this will stop the sound pipeline: 

112 self.wrapped_object = None 

113 wo.cleanup() 

114 self.timeout_add(1000, self.do_stop) 

115 

116 def export_info(self): 

117 wo = self.wrapped_object 

118 if wo: 

119 self.send("info", wo.get_info()) 

120 return wo is not None 

121 

122 

123class sound_record(sound_subprocess): 

124 """ wraps SoundSource as a subprocess """ 

125 def __init__(self, *pipeline_args): 

126 from xpra.sound.src import SoundSource 

127 sound_pipeline = SoundSource(*pipeline_args) 

128 super().__init__(sound_pipeline, [], ["new-stream", "new-buffer"]) 

129 self.large_packets = [b"new-buffer"] 

130 

131class sound_play(sound_subprocess): 

132 """ wraps SoundSink as a subprocess """ 

133 def __init__(self, *pipeline_args): 

134 from xpra.sound.sink import SoundSink 

135 sound_pipeline = SoundSink(*pipeline_args) 

136 super().__init__(sound_pipeline, ["add_data"], []) 

137 

138 

139def run_sound(mode, error_cb, options, args): 

140 """ this function just parses command line arguments to feed into the sound subprocess class, 

141 which in turn just feeds them into the sound pipeline class (sink.py or src.py) 

142 """ 

143 gst = import_gst() 

144 if not gst: 

145 return 1 

146 info = mode.replace("_sound_", "") #ie: "_sound_record" -> "record" 

147 from xpra.platform import program_context 

148 with program_context("Xpra-Audio-%s" % info, "Xpra Audio %s" % info): 

149 log("run_sound(%s, %s, %s, %s) gst=%s", mode, error_cb, options, args, gst) 

150 if mode=="_sound_record": 

151 subproc = sound_record 

152 elif mode=="_sound_play": 

153 subproc = sound_play 

154 elif mode=="_sound_query": 

155 plugins = get_all_plugin_names() 

156 sources = [x for x in get_source_plugins() if x in plugins] 

157 sinks = [x for x in get_sink_plugins() if x in plugins] 

158 from xpra.sound.gstreamer_util import get_gst_version, get_pygst_version 

159 d = { 

160 "encoders" : can_encode(), 

161 "decoders" : can_decode(), 

162 "sources" : sources, 

163 "source.default" : get_default_source() or "", 

164 "sinks" : sinks, 

165 "sink.default" : get_default_sink_plugin() or "", 

166 "muxers" : get_muxers(), 

167 "demuxers" : get_demuxers(), 

168 "gst.version" : [int(x) for x in get_gst_version()], 

169 "pygst.version" : get_pygst_version(), 

170 "plugins" : plugins, 

171 "python.version" : sys.version_info[:3], 

172 "python.bits" : BITS, 

173 } 

174 if BUNDLE_METADATA: 

175 d["bundle-metadata"] = True 

176 for k,v in d.items(): 

177 if isinstance(v, (list, tuple)): 

178 v = ",".join(str(x) for x in v) 

179 print("%s=%s" % (k, v)) 

180 return 0 

181 else: 

182 log.error("unknown mode: %s" % mode) 

183 return 1 

184 assert len(args)>=6, "not enough arguments" 

185 

186 #the plugin to use (ie: 'pulsesrc' for src.py or 'autoaudiosink' for sink.py) 

187 plugin = args[2] 

188 #plugin options (ie: "device=monitor_device,something=value") 

189 options = parse_simple_dict(args[3]) 

190 #codecs: 

191 codecs = [x.strip() for x in args[4].split(",")] 

192 #codec options: 

193 codec_options = parse_simple_dict(args[5]) 

194 #volume (optional): 

195 try: 

196 volume = int(args[6]) 

197 except (ValueError, IndexError): 

198 volume = 1.0 

199 

200 ss = None 

201 try: 

202 ss = subproc(plugin, options, codecs, codec_options, volume) 

203 ss.start() 

204 return 0 

205 except InitExit as e: 

206 log.error("%s: %s", info, e) 

207 return e.status 

208 except InitException as e: 

209 log.error("%s: %s", info, e) 

210 return 1 

211 except Exception: 

212 log.error("run_sound%s error", (mode, error_cb, options, args), exc_info=True) 

213 return 1 

214 finally: 

215 if ss: 

216 ss.stop() 

217 

218 

219def _add_debug_args(command): 

220 from xpra.log import debug_enabled_categories 

221 debug = list(SUBPROCESS_DEBUG) 

222 for f in ("sound", "gstreamer"): 

223 if (DEBUG_SOUND or f in debug_enabled_categories) and (f not in debug): 

224 debug.append(f) 

225 if debug: 

226 #forward debug flags: 

227 command += ["-d", ",".join(debug)] 

228 

229 

230class sound_subprocess_wrapper(subprocess_caller): 

231 """ This utility superclass deals with the caller side of the sound subprocess wrapper: 

232 * starting the wrapper subprocess 

233 * handling state-changed signal so we have a local copy of the current value ready 

234 * handle "info" packets so we have a cached copy 

235 * forward get/set volume calls (get_volume uses the value found in "info") 

236 """ 

237 def __init__(self, description): 

238 super().__init__(description) 

239 self.state = "stopped" 

240 self.codec = "unknown" 

241 self.codec_description = "" 

242 self.info = {} 

243 #hook some default packet handlers: 

244 self.connect("state-changed", self.state_changed) 

245 self.connect("info", self.info_update) 

246 self.connect("signal", self.subprocess_signal) 

247 

248 def get_env(self): 

249 env = subprocess_caller.get_env(self) 

250 env.update(get_sound_wrapper_env()) 

251 return env 

252 

253 def start(self): 

254 self.state = "starting" 

255 subprocess_caller.start(self) 

256 log("start() %s subprocess(%s)=%s", self.description, self.command, self.process.pid) 

257 self.timeout_add(SOUND_START_TIMEOUT, self.verify_started) 

258 

259 

260 def cleanup(self): 

261 log("cleanup() sending cleanup request to %s", self.description) 

262 self.send("cleanup") 

263 #cleanup should cause the process to exit 

264 self.timeout_add(500, self.send, "stop") 

265 self.timeout_add(1000, self.send, "exit") 

266 self.timeout_add(1500, self.stop) 

267 

268 

269 def verify_started(self): 

270 p = self.process 

271 log("verify_started() process=%s, info=%s, codec=%s", p, self.info, self.codec) 

272 if p is None or p.poll() is not None: 

273 #process has terminated already 

274 return 

275 #if we don't get an "info" packet, then the pipeline must have failed to start 

276 if not self.info: 

277 log.warn("Warning: the %s process has failed to start", self.description) 

278 self.cleanup() 

279 

280 

281 def subprocess_signal(self, _wrapper, proc): 

282 log("subprocess_signal: %s", proc) 

283 #call via idle_add to prevent deadlocks on win32! 

284 self.idle_add(self.stop_protocol) 

285 

286 

287 def state_changed(self, _wrapper, new_state): 

288 self.state = new_state 

289 

290 def get_state(self): 

291 return self.state 

292 

293 

294 def get_info(self) -> dict: 

295 return self.info 

296 

297 def info_update(self, _wrapper, info): 

298 log("info_update: %s", info) 

299 self.info.update(info) 

300 self.info["time"] = int(monotonic_time()) 

301 p = self.process 

302 if p and not p.poll(): 

303 self.info["pid"] = p.pid 

304 self.codec_description = info.get("codec_description") 

305 

306 

307 def set_volume(self, v): 

308 self.send("set_volume", int(v*100)) 

309 

310 def get_volume(self): 

311 return self.info.get("volume", 100)/100.0 

312 

313 

314class source_subprocess_wrapper(sound_subprocess_wrapper): 

315 

316 def __init__(self, plugin, options, codecs, volume, element_options): 

317 super().__init__("audio capture") 

318 self.large_packets = [b"new-buffer"] 

319 self.command = get_full_sound_command()+[ 

320 "_sound_record", "-", "-", 

321 plugin or "", format_element_options(element_options), 

322 ",".join(codecs), "", 

323 str(volume), 

324 ] 

325 _add_debug_args(self.command) 

326 

327 def __repr__(self): 

328 proc = self.process 

329 if proc: 

330 try: 

331 return "source_subprocess_wrapper(%s)" % proc.pid 

332 except AttributeError: 

333 pass 

334 return "source_subprocess_wrapper(%s)" % proc 

335 

336 

337class sink_subprocess_wrapper(sound_subprocess_wrapper): 

338 

339 def __init__(self, plugin, codec, volume, element_options): 

340 super().__init__("audio playback") 

341 self.large_packets = [b"add_data"] 

342 self.codec = codec 

343 self.command = get_full_sound_command()+[ 

344 "_sound_play", "-", "-", 

345 plugin or "", format_element_options(element_options), 

346 codec, "", 

347 str(volume), 

348 ] 

349 _add_debug_args(self.command) 

350 

351 def add_data(self, data, metadata=None, packet_metadata=()): 

352 if DEBUG_SOUND: 

353 log("add_data(%s bytes, %s, %s) forwarding to %s", len(data), metadata, len(packet_metadata), self.protocol) 

354 self.send("add_data", data, dict(metadata or {}), packet_metadata) 

355 

356 def __repr__(self): 

357 proc = self.process 

358 if proc: 

359 try: 

360 return "sink_subprocess_wrapper(%s)" % proc.pid 

361 except AttributeError: 

362 pass 

363 return "sink_subprocess_wrapper(%s)" % proc 

364 

365 

366def start_sending_sound(plugins, sound_source_plugin, device, codec, volume, want_monitor_device, remote_decoders, remote_pulseaudio_server, remote_pulseaudio_id): 

367 log("start_sending_sound%s", 

368 (plugins, sound_source_plugin, device, codec, volume, want_monitor_device, remote_decoders, remote_pulseaudio_server, remote_pulseaudio_id)) 

369 try: 

370 #info about the remote end: 

371 PAInfo = namedtuple("PAInfo", "pulseaudio_server,pulseaudio_id,remote_decoders") 

372 remote = PAInfo(pulseaudio_server=remote_pulseaudio_server, 

373 pulseaudio_id=remote_pulseaudio_id, 

374 remote_decoders=remote_decoders) 

375 plugin, options = parse_sound_source(plugins, sound_source_plugin, device, want_monitor_device, remote) 

376 if not plugin: 

377 log.error("failed to setup '%s' sound stream source", (sound_source_plugin or "auto")) 

378 return None 

379 log("parsed '%s':", sound_source_plugin) 

380 log("plugin=%s", plugin) 

381 log("options=%s", options) 

382 return source_subprocess_wrapper(plugin, options, remote_decoders, volume, options) 

383 except Exception as e: 

384 log.error("error setting up sound: %s", e, exc_info=True) 

385 return None 

386 

387 

388def start_receiving_sound(codec): 

389 log("start_receiving_sound(%s)", codec) 

390 try: 

391 return sink_subprocess_wrapper(None, codec, 1.0, {}) 

392 except Exception: 

393 log.error("failed to start sound sink", exc_info=True) 

394 return None 

395 

396def query_sound(): 

397 import subprocess 

398 command = get_full_sound_command()+["_sound_query"] 

399 _add_debug_args(command) 

400 kwargs = exec_kwargs() 

401 env = exec_env() 

402 env.update(get_sound_wrapper_env()) 

403 log("query_sound() command=%s, env=%s, kwargs=%s", command, env, kwargs) 

404 proc = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, env=env, **kwargs) 

405 out, err = proc.communicate(None) 

406 log("query_sound() process returned %s", proc.returncode) 

407 log("query_sound() out=%s, err=%s", out, err) 

408 if proc.returncode!=0: 

409 return typedict() 

410 d = typedict() 

411 for x in out.splitlines(): 

412 kv = x.split(b"=", 1) 

413 if len(kv)==2: 

414 #ie: kv = ["decoders", "mp3,vorbis"] 

415 k,v = kv 

416 #fugly warning: all the other values are lists.. but this one is not: 

417 if k!=b"python.bits": 

418 v = [bytestostr(x) for x in v.split(b",")] 

419 #d["decoders"] = ["mp3", "vorbis"] 

420 d[bytestostr(k)] = v 

421 log("query_sound()=%s", d) 

422 return d