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#!/usr/bin/env python 

2# This file is part of Xpra. 

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

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

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

6 

7import sys 

8import os 

9 

10from xpra.sound.common import ( 

11 FLAC_OGG, OPUS_OGG, OPUS_MKA, SPEEX_OGG, VORBIS_OGG, VORBIS_MKA, \ 

12 AAC_MPEG4, WAV_LZ4, WAV_LZO, \ 

13 VORBIS, FLAC, MP3, MP3_MPEG4, OPUS, SPEEX, WAV, WAVPACK, MP3_ID3V2, \ 

14 MPEG4, MKA, OGG, 

15 ) 

16from xpra.os_util import WIN32, OSX, POSIX, bytestostr 

17from xpra.util import csv, engs, parse_simple_dict, reverse_dict, envint, envbool 

18from xpra.log import Logger 

19 

20log = Logger("sound", "gstreamer") 

21 

22 

23#used on the server (reversed): 

24XPRA_PULSE_SOURCE_DEVICE_NAME = "XPRA_PULSE_SOURCE_DEVICE_NAME" 

25XPRA_PULSE_SINK_DEVICE_NAME = "XPRA_PULSE_SINK_DEVICE_NAME" 

26 

27GST_QUEUE_NO_LEAK = 0 

28GST_QUEUE_LEAK_UPSTREAM = 1 

29GST_QUEUE_LEAK_DOWNSTREAM = 2 

30GST_QUEUE_LEAK_DEFAULT = GST_QUEUE_LEAK_DOWNSTREAM 

31MS_TO_NS = 1000000 

32 

33GST_FLOW_OK = 0 #Gst.FlowReturn.OK 

34 

35 

36QUEUE_LEAK = envint("XPRA_SOUND_QUEUE_LEAK", GST_QUEUE_LEAK_DEFAULT) 

37if QUEUE_LEAK not in (GST_QUEUE_NO_LEAK, GST_QUEUE_LEAK_UPSTREAM, GST_QUEUE_LEAK_DOWNSTREAM): 

38 log.error("invalid leak option %s", QUEUE_LEAK) 

39 QUEUE_LEAK = GST_QUEUE_LEAK_DEFAULT 

40 

41def get_queue_time(default_value=450, prefix=""): 

42 queue_time = int(os.environ.get("XPRA_SOUND_QUEUE_%sTIME" % prefix, default_value))*MS_TO_NS 

43 queue_time = max(0, queue_time) 

44 return queue_time 

45 

46 

47ALLOW_SOUND_LOOP = envbool("XPRA_ALLOW_SOUND_LOOP", False) 

48USE_DEFAULT_DEVICE = envbool("XPRA_USE_DEFAULT_DEVICE", True) 

49IGNORED_INPUT_DEVICES = os.environ.get("XPRA_SOUND_IGNORED_INPUT_DEVICES", "bell.ogg,bell.wav").split(",") 

50IGNORED_OUTPUT_DEVICES = os.environ.get("XPRA_SOUND_IGNORED_OUTPUT_DEVICES", "").split(",") 

51def force_enabled(codec_name): 

52 return os.environ.get("XPRA_SOUND_CODEC_ENABLE_%s" % codec_name.upper().replace("+", "_"), "0")=="1" 

53 

54 

55NAME_TO_SRC_PLUGIN = { 

56 "auto" : "autoaudiosrc", 

57 "alsa" : "alsasrc", 

58 "oss" : "osssrc", 

59 "oss4" : "oss4src", 

60 "jack" : "jackaudiosrc", 

61 "osx" : "osxaudiosrc", 

62 "test" : "audiotestsrc", 

63 "pulse" : "pulsesrc", 

64 "direct" : "directsoundsrc", 

65 "wasapi" : "wasapisrc", 

66 } 

67SRC_TO_NAME_PLUGIN = reverse_dict(NAME_TO_SRC_PLUGIN) 

68SRC_HAS_DEVICE_NAME = ["alsasrc", "osssrc", "oss4src", "jackaudiosrc", "pulsesrc", "directsoundsrc", "osxaudiosrc"] 

69PLUGIN_TO_DESCRIPTION = { 

70 "pulsesrc" : "Pulseaudio", 

71 "jacksrc" : "JACK Audio Connection Kit", 

72 } 

73 

74NAME_TO_INFO_PLUGIN = { 

75 "auto" : "Automatic audio source selection", 

76 "alsa" : "ALSA Linux Sound", 

77 "oss" : "OSS sound cards", 

78 "oss4" : "OSS version 4 sound cards", 

79 "jack" : "JACK audio sound server", 

80 "osx" : "Mac OS X sound cards", 

81 "test" : "Test signal", 

82 "pulse" : "PulseAudio", 

83 "direct" : "Microsoft Windows Direct Sound", 

84 "wasapi" : "Windows Audio Session API", 

85 } 

86 

87 

88#format: encoder, container-formatter, decoder, container-parser, stream-compressor 

89#we keep multiple options here for the same encoding 

90#and will populate the ones that are actually available into the "CODECS" dict 

91CODEC_OPTIONS = [ 

92 (VORBIS_MKA , "vorbisenc", "matroskamux", "vorbisdec", "matroskademux"), 

93 (VORBIS_MKA , "vorbisenc", "webmmux", "vorbisdec", "matroskademux"), 

94 #those two used to fail silently (older versions of gstreamer?) 

95 (VORBIS_OGG , "vorbisenc", "oggmux", "vorbisparse ! vorbisdec", "oggdemux"), 

96 (VORBIS , "vorbisenc", None, "vorbisparse ! vorbisdec", None), 

97 (FLAC , "flacenc", None, "flacparse ! flacdec", None), 

98 (FLAC_OGG , "flacenc", "oggmux", "flacparse ! flacdec", "oggdemux"), 

99 (MP3 , "lamemp3enc", None, "mpegaudioparse ! mad", None), 

100 (MP3_ID3V2 , "lamemp3enc", "id3v2mux", "mpegaudioparse ! mpg123audiodec", "id3demux"), 

101 (MP3 , "lamemp3enc", None, "mpegaudioparse ! mpg123audiodec", None), 

102 (MP3_MPEG4 , "lamemp3enc", "mp4mux", "mpegaudioparse ! mad", "qtdemux"), 

103 (WAV , "wavenc", None, "wavparse", None), 

104 (WAV_LZ4 , "wavenc", None, "wavparse", None, "lz4"), 

105 (WAV_LZO , "wavenc", None, "wavparse", None, "lzo"), 

106 (OPUS_OGG , "opusenc", "oggmux", "opusdec", "oggdemux"), 

107 (OPUS , "opusenc", None, "opusparse ! opusdec", None), 

108 #this can cause "could not link opusenc0 to webmmux0" 

109 (OPUS_MKA , "opusenc", "matroskamux", "opusdec", "matroskademux"), 

110 (OPUS_MKA , "opusenc", "webmmux", "opusdec", "matroskademux"), 

111 (SPEEX_OGG , "speexenc", "oggmux", "speexdec", "oggdemux"), 

112 (WAVPACK , "wavpackenc", None, "wavpackparse ! wavpackdec", None), 

113 (AAC_MPEG4 , "faac", "mp4mux", "faad", "qtdemux"), 

114 (AAC_MPEG4 , "avenc_aac", "mp4mux", "avdec_aac", "qtdemux"), 

115 ] 

116 

117MUX_OPTIONS = [ 

118 (OGG, "oggmux", "oggdemux"), 

119 (MKA, "webmmux", "matroskademux"), 

120 (MKA, "matroskamux", "matroskademux"), 

121 (MPEG4, "mp4mux", "qtdemux"), 

122 ] 

123emux = [x for x in os.environ.get("XPRA_MUXER_OPTIONS", "").split(",") if len(x.strip())>0] 

124if emux: 

125 mo = [v for v in MUX_OPTIONS if v[0] in emux] 

126 if mo: 

127 MUX_OPTIONS = mo 

128 else: 

129 log.warn("Warning: invalid muxer options %s", emux) 

130 del mo 

131del emux 

132 

133 

134#these encoders require an "audioconvert" element: 

135ENCODER_NEEDS_AUDIOCONVERT = ("flacenc", "wavpackenc") 

136#if this is lightweight enough, maybe we should include it unconditionally? 

137SOURCE_NEEDS_AUDIOCONVERT = ("directsoundsrc", "osxaudiosrc", "autoaudiosrc", "wasapisrc") 

138 

139CUTTER_NEEDS_RESAMPLE = ("opusenc", ) 

140#those don't work anyway: 

141CUTTER_NEEDS_CONVERT = ("vorbisenc", "wavpackenc", "avenc_aac") 

142ENCODER_CANNOT_USE_CUTTER = ("vorbisenc", "wavpackenc", "avenc_aac", "wavenc") 

143 

144 

145#options we use to tune for low latency: 

146OGG_DELAY = 20*MS_TO_NS 

147ENCODER_DEFAULT_OPTIONS_COMMON = { 

148 "lamemp3enc" : { 

149 "encoding-engine-quality" : 0, 

150 }, #"fast" 

151 "wavpackenc" : { 

152 "mode" : 1, #"fast" (0 aka "very fast" is not supported) 

153 "bitrate" : 256000, 

154 }, 

155 "flacenc" : { 

156 "quality" : 0, #"fast" 

157 }, 

158 "avenc_aac" : { 

159 "compliance" : 1, #allows experimental 

160 "perfect-timestamp" : 1, 

161 }, 

162 "faac" : { 

163 "perfect-timestamp" : 1, 

164 }, 

165 #"vorbisenc" : {"perfect-timestamp" : 1}, 

166 } 

167ENCODER_DEFAULT_OPTIONS = { 

168 "opusenc" : { 

169 #only available with 1.6 onwards? 

170 "bitrate-type" : 1, #vbr 

171 "complexity" : 0 

172 }, 

173 } 

174#we may want to review this if/when we implement UDP transport: 

175MUXER_DEFAULT_OPTIONS = { 

176 "oggmux" : { 

177 "max-delay" : OGG_DELAY, 

178 "max-page-delay" : OGG_DELAY, 

179 }, 

180 "webmmux" : { 

181 "writing-app" : "Xpra", 

182 "streamable" : 1, 

183 #"min-index-interval" : 0, 

184 }, 

185 "matroskamux" : { 

186 "writing-app" : "Xpra", 

187 "streamable" : 1, 

188 }, 

189 "mp4mux" : { 

190 "faststart" : 1, 

191 "streamable" : 1, 

192 "fragment-duration" : 20, 

193 "presentation-time" : 0, 

194 } 

195 } 

196 

197#based on the encoder options above: 

198RECORD_PIPELINE_LATENCY = 25 

199ENCODER_LATENCY = { 

200 VORBIS : 0, 

201 VORBIS_OGG : 0, 

202 VORBIS_MKA : 0, 

203 MP3 : 250, 

204 FLAC : 50, 

205 WAV : 0, 

206 WAVPACK : 600, 

207 OPUS : 0, 

208 SPEEX : 0, 

209 } 

210 

211CODEC_ORDER = [ 

212 OPUS, OPUS_OGG, VORBIS_MKA, VORBIS_OGG, VORBIS, 

213 MP3, MP3_ID3V2, FLAC_OGG, AAC_MPEG4, 

214 WAV_LZ4, WAV_LZO, WAV, WAVPACK, 

215 SPEEX_OGG, VORBIS, OPUS_MKA, FLAC, MP3_MPEG4, 

216 ] 

217 

218 

219gst = None 

220 

221def get_pygst_version(): 

222 import gi 

223 return getattr(gi, "version_info", ()) 

224 

225def get_gst_version(): 

226 if not gst: 

227 return () 

228 return gst.version() 

229 

230 

231def do_import_gst(): 

232 global gst 

233 if gst is not None: 

234 return gst 

235 

236 #hacks to locate gstreamer plugins on win32 and osx: 

237 if WIN32: 

238 frozen = getattr(sys, "frozen", None) in ("windows_exe", "console_exe", True) 

239 log("gstreamer_util: frozen=%s", frozen) 

240 if frozen: 

241 from xpra.platform.paths import get_app_dir 

242 gst_dir = os.path.join(get_app_dir(), "lib", "gstreamer-1.0") #ie: C:\Program Files\Xpra\lib\gstreamer-1.0 

243 os.environ["GST_PLUGIN_PATH"] = gst_dir 

244 elif OSX: 

245 bundle_contents = os.environ.get("GST_BUNDLE_CONTENTS") 

246 log("OSX: GST_BUNDLE_CONTENTS=%s", bundle_contents) 

247 if bundle_contents: 

248 rsc_dir = os.path.join(bundle_contents, "Resources") 

249 os.environ["GST_PLUGIN_PATH"] = os.path.join(rsc_dir, "lib", "gstreamer-1.0") 

250 os.environ["GST_PLUGIN_SCANNER"] = os.path.join(rsc_dir, "bin", "gst-plugin-scanner-1.0") 

251 log("GStreamer 1.x environment: %s", 

252 dict((k,v) for k,v in os.environ.items() if (k.startswith("GST") or k.startswith("GI") or k=="PATH"))) 

253 log("GStreamer 1.x sys.path=%s", csv(sys.path)) 

254 

255 try: 

256 log("import gi") 

257 import gi 

258 gi.require_version('Gst', '1.0') 

259 from gi.repository import Gst #@UnresolvedImport 

260 log("Gst=%s", Gst) 

261 Gst.init(None) 

262 gst = Gst 

263 except Exception as e: 

264 log("Warning failed to import GStreamer 1.x", exc_info=True) 

265 log.warn("Warning: failed to import GStreamer 1.x:") 

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

267 return None 

268 return gst 

269import_gst = do_import_gst 

270 

271def prevent_import(): 

272 global gst 

273 global import_gst 

274 if gst or "gst" in sys.modules or "gi.repository.Gst" in sys.modules: 

275 raise Exception("cannot prevent the import of the GStreamer bindings, already loaded: %s" % gst) 

276 def fail_import(): 

277 raise Exception("importing of the GStreamer bindings is not allowed!") 

278 import_gst = fail_import 

279 sys.modules["gst"] = None 

280 sys.modules["gi.repository.Gst"]= None 

281 

282 

283def normv(v): 

284 if v==2**64-1: 

285 return -1 

286 return int(v) 

287 

288 

289all_plugin_names = [] 

290def get_all_plugin_names(): 

291 global all_plugin_names, gst 

292 if not all_plugin_names and gst: 

293 registry = gst.Registry.get() 

294 all_plugin_names = [el.get_name() for el in registry.get_feature_list(gst.ElementFactory)] 

295 all_plugin_names.sort() 

296 log("found the following plugins: %s", all_plugin_names) 

297 return all_plugin_names 

298 

299def has_plugins(*names): 

300 allp = get_all_plugin_names() 

301 #support names that contain a gstreamer chain, ie: "flacparse ! flacdec" 

302 snames = [] 

303 for x in names: 

304 if not x: 

305 continue 

306 snames += [v.strip() for v in x.split("!")] 

307 missing = [name for name in snames if (name is not None and name not in allp)] 

308 if missing: 

309 log("missing %s from %s", missing, names) 

310 return len(missing)==0 

311 

312def get_encoder_default_options(encoder): 

313 global ENCODER_DEFAULT_OPTIONS_COMMON, ENCODER_DEFAULT_OPTIONS 

314 #strip the muxer: 

315 enc = encoder.split("+")[0] 

316 options = ENCODER_DEFAULT_OPTIONS_COMMON.get(enc, {}).copy() 

317 options.update(ENCODER_DEFAULT_OPTIONS.get(enc, {})) 

318 return options 

319 

320 

321CODECS = None 

322ENCODERS = {} #(encoder, payloader, stream-compressor) 

323DECODERS = {} #(decoder, depayloader, stream-compressor) 

324 

325def get_encoders(): 

326 init_codecs() 

327 global ENCODERS 

328 return ENCODERS 

329 

330def get_decoders(): 

331 init_codecs() 

332 global DECODERS 

333 return DECODERS 

334 

335def init_codecs(): 

336 global CODECS, ENCODERS, DECODERS 

337 if CODECS is not None or gst is None: 

338 return CODECS or {} 

339 #populate CODECS: 

340 CODECS = {} 

341 for elements in CODEC_OPTIONS: 

342 if not validate_encoding(elements): 

343 continue 

344 try: 

345 encoding, encoder, payloader, decoder, depayloader, stream_compressor = (list(elements)+[None])[:6] 

346 except ValueError as e: 

347 log.error("Error: invalid codec entry: %s", e) 

348 log.error(" %s", elements) 

349 continue 

350 add_encoder(encoding, encoder, payloader, stream_compressor) 

351 add_decoder(encoding, decoder, depayloader, stream_compressor) 

352 log("initialized sound codecs:") 

353 def ci(v): 

354 return "%-22s" % (v or "") 

355 log(" - %s", "".join(ci(v) for v in ("encoder/decoder", "(de)payloader", "stream-compressor"))) 

356 for k in CODEC_ORDER: 

357 if k in ENCODERS or k in DECODERS: 

358 CODECS[k] = True 

359 log("* %s :", k) 

360 if k in ENCODERS: 

361 log(" - %s", "".join([ci(v) for v in ENCODERS[k]])) 

362 if k in DECODERS: 

363 log(" - %s", "".join([ci(v) for v in DECODERS[k]])) 

364 return CODECS 

365 

366def add_encoder(encoding, encoder, payloader, stream_compressor): 

367 global ENCODERS 

368 if encoding in ENCODERS: 

369 return 

370 if OSX and encoding in (OPUS_OGG, ): 

371 log("avoiding %s on Mac OS X", encoding) 

372 return 

373 if has_plugins(encoder, payloader): 

374 ENCODERS[encoding] = (encoder, payloader, stream_compressor) 

375 

376def add_decoder(encoding, decoder, depayloader, stream_compressor): 

377 global DECODERS 

378 if encoding in DECODERS: 

379 return 

380 if has_plugins(decoder, depayloader): 

381 DECODERS[encoding] = (decoder, depayloader, stream_compressor) 

382 

383def validate_encoding(elements): 

384 #generic platform validation of encodings and plugins 

385 #full of quirks 

386 encoding = elements[0] 

387 if force_enabled(encoding): 

388 log.info("sound codec %s force enabled", encoding) 

389 return True 

390 if encoding in (VORBIS_OGG, VORBIS) and get_gst_version()<(1, 12): 

391 log("skipping %s - not sure which GStreamer versions support it", encoding) 

392 return False 

393 if encoding.startswith(OPUS): 

394 if encoding==OPUS_MKA and get_gst_version()<(1, 8): 

395 #this causes "could not link opusenc0 to webmmux0" 

396 #(not sure which versions are affected, but 1.8.x is not) 

397 log("skipping %s with GStreamer %s", encoding, get_gst_version()) 

398 return False 

399 try: 

400 stream_compressor = elements[5] 

401 except IndexError: 

402 stream_compressor = None 

403 if stream_compressor and not has_stream_compressor(stream_compressor): 

404 log("skipping %s: missing %s", encoding, stream_compressor) 

405 return False 

406 return True 

407 

408def has_stream_compressor(stream_compressor): 

409 if stream_compressor not in ("lz4", "lzo"): 

410 log.warn("Warning: invalid stream compressor '%s'", stream_compressor) 

411 return False 

412 from xpra.net.compression import use 

413 if stream_compressor=="lz4" and not use("lz4"): 

414 return False 

415 if stream_compressor=="lzo" and not use("lzo"): 

416 return False 

417 return True 

418 

419def get_muxers(): 

420 muxers = [] 

421 for name,muxer,_ in MUX_OPTIONS: 

422 if has_plugins(muxer): 

423 muxers.append(name) 

424 return muxers 

425 

426def get_demuxers(): 

427 demuxers = [] 

428 for name,_,demuxer in MUX_OPTIONS: 

429 if has_plugins(demuxer): 

430 demuxers.append(name) 

431 return demuxers 

432 

433def get_stream_compressors(): 

434 return [x for x in ("lz4", "lzo") if has_stream_compressor(x)] 

435 

436def get_encoder_elements(name): 

437 encoders = get_encoders() 

438 assert name in encoders, "invalid codec: %s (should be one of: %s)" % (name, encoders.keys()) 

439 encoder, formatter, stream_compressor = encoders.get(name) 

440 if stream_compressor: 

441 assert has_stream_compressor(stream_compressor), "stream-compressor %s not found" % stream_compressor 

442 assert encoder is None or has_plugins(encoder), "encoder %s not found" % encoder 

443 assert formatter is None or has_plugins(formatter), "formatter %s not found" % formatter 

444 return encoder, formatter, stream_compressor 

445 

446def get_decoder_elements(name): 

447 decoders = get_decoders() 

448 assert name in decoders, "invalid codec: %s (should be one of: %s)" % (name, decoders.keys()) 

449 decoder, parser, stream_compressor = decoders.get(name) 

450 if stream_compressor: 

451 assert has_stream_compressor(stream_compressor), "stream-compressor %s not found" % stream_compressor 

452 assert decoder is None or has_plugins(decoder), "decoder %s not found" % decoder 

453 assert parser is None or has_plugins(parser), "parser %s not found" % parser 

454 return decoder, parser, stream_compressor 

455 

456def has_encoder(name): 

457 encoders = get_encoders() 

458 if name not in encoders: 

459 return False 

460 encoder, fmt, _ = encoders.get(name) 

461 return has_plugins(encoder, fmt) 

462 

463def has_decoder(name): 

464 decoders = get_decoders() 

465 if name not in decoders: 

466 return False 

467 decoder, parser, _ = decoders.get(name) 

468 return has_plugins(decoder, parser) 

469 

470def has_codec(name): 

471 return has_encoder(name) and has_decoder(name) 

472 

473def can_encode(): 

474 return [x for x in CODEC_ORDER if has_encoder(x)] 

475 

476def can_decode(): 

477 return [x for x in CODEC_ORDER if has_decoder(x)] 

478 

479def plugin_str(plugin, options): 

480 if plugin is None: 

481 return None 

482 s = "%s" % plugin 

483 def qstr(v): 

484 #only quote strings 

485 if isinstance(v, str): 

486 return "\"%s\"" % v 

487 return v 

488 if options: 

489 s += " " 

490 s += " ".join([("%s=%s" % (k,qstr(v))) for k,v in options.items()]) 

491 return s 

492 

493 

494def get_source_plugins(): 

495 sources = [] 

496 if POSIX and not OSX: 

497 try: 

498 from xpra.sound.pulseaudio.pulseaudio_util import has_pa 

499 #we have to put pulsesrc first if pulseaudio is installed 

500 #because using autoaudiosource does not work properly for us: 

501 #it may still choose pulse, but without choosing the right device. 

502 if has_pa(): 

503 sources.append("pulsesrc") 

504 except ImportError as e: 

505 log("get_source_plugins() no pulsesrc: %s", e) 

506 if OSX: 

507 sources.append("osxaudiosrc") 

508 elif WIN32: 

509 sources.append("directsoundsrc") 

510 sources.append("wasapisrc") 

511 sources.append("autoaudiosrc") 

512 if POSIX: 

513 sources += ["alsasrc", 

514 "osssrc", "oss4src", 

515 "jackaudiosrc"] 

516 sources.append("audiotestsrc") 

517 return sources 

518 

519def get_default_source(): 

520 source = os.environ.get("XPRA_SOUND_SRC") 

521 sources = get_source_plugins() 

522 if source: 

523 if source not in sources: 

524 log.error("invalid default sound source: '%s' is not in %s", source, csv(sources)) 

525 else: 

526 return source 

527 if POSIX and not OSX: 

528 try: 

529 from xpra.sound.pulseaudio.pulseaudio_util import has_pa, get_pactl_server 

530 if has_pa(): 

531 s = get_pactl_server() 

532 if not s: 

533 log("cannot connect to pulseaudio server?") 

534 else: 

535 return "pulsesrc" 

536 except ImportError as e: 

537 log("get_default_source() no pulsesrc: %s", e) 

538 for source in sources: 

539 if has_plugins(source): 

540 return source 

541 return None 

542 

543def get_sink_plugins(): 

544 SINKS = [] 

545 if OSX: 

546 SINKS.append("osxaudiosink") 

547 elif WIN32: 

548 SINKS.append("directsoundsink") 

549 SINKS.append("wasapisink") 

550 SINKS.append("autoaudiosink") 

551 try: 

552 from xpra.sound.pulseaudio.pulseaudio_util import has_pa 

553 if has_pa(): 

554 SINKS.append("pulsesink") 

555 except ImportError as e: 

556 log("get_sink_plugins() no pulsesink: %s", e) 

557 if POSIX: 

558 SINKS += ["alsasink", "osssink", "oss4sink", "jackaudiosink"] 

559 return SINKS 

560 

561def get_default_sink_plugin(): 

562 sink = os.environ.get("XPRA_SOUND_SINK") 

563 sinks = get_sink_plugins() 

564 if sink: 

565 if sink not in sinks: 

566 log.error("invalid default sound sink: '%s' is not in %s", sink, csv(sinks)) 

567 else: 

568 return sink 

569 try: 

570 from xpra.sound.pulseaudio.pulseaudio_util import has_pa, get_pactl_server 

571 if has_pa(): 

572 s = get_pactl_server() 

573 if not s: 

574 log("cannot connect to pulseaudio server?") 

575 else: 

576 return "pulsesink" 

577 except ImportError as e: 

578 log("get_default_sink_plugin() no pulsesink: %s", e) 

579 for sink in sinks: 

580 if has_plugins(sink): 

581 return sink 

582 return None 

583 

584 

585def get_test_defaults(*_args): 

586 return {"wave" : 2, "freq" : 110, "volume" : 0.4} 

587 

588WARNED_MULTIPLE_DEVICES = False 

589def get_pulse_defaults(device_name_match=None, want_monitor_device=True, 

590 input_or_output=None, remote=None, env_device_name=None): 

591 try: 

592 device = get_pulse_device(device_name_match, want_monitor_device, input_or_output, remote, env_device_name) 

593 except Exception as e: 

594 log("get_pulse_defaults%s", 

595 (device_name_match, want_monitor_device, input_or_output, remote, env_device_name), exc_info=True) 

596 log.warn("Warning: failed to identify the pulseaudio default device to use") 

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

598 return {} 

599 if not device: 

600 return {} 

601 #make sure it is not muted: 

602 try: 

603 from xpra.sound.pulseaudio.pulseaudio_util import has_pa, set_source_mute, set_sink_mute 

604 if has_pa(): 

605 if input_or_output is True or want_monitor_device: 

606 set_source_mute(device, mute=False) 

607 elif input_or_output is False: 

608 set_sink_mute(device, mute=False) 

609 except Exception as e: 

610 log("device %s may still be muted: %s", device, e) 

611 return {"device" : bytestostr(device)} 

612 

613def get_pulse_device(device_name_match=None, want_monitor_device=True, 

614 input_or_output=None, remote=None, env_device_name=None): 

615 """ 

616 choose the device to use 

617 """ 

618 log("get_pulse_device%s", (device_name_match, want_monitor_device, input_or_output, remote, env_device_name)) 

619 try: 

620 from xpra.sound.pulseaudio.pulseaudio_util import ( 

621 has_pa, get_pa_device_options, 

622 get_default_sink, get_pactl_server, 

623 get_pulse_id, 

624 ) 

625 if not has_pa(): 

626 log.warn("Warning: pulseaudio is not available!") 

627 return None 

628 except ImportError as e: 

629 log.warn("Warning: pulseaudio is not available!") 

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

631 return None 

632 pa_server = get_pactl_server() 

633 log("get_pactl_server()=%s", pa_server) 

634 if remote: 

635 log("start sound, remote pulseaudio server=%s, local pulseaudio server=%s", remote.pulseaudio_server, pa_server) 

636 #only worth comparing if we have a real server string 

637 #one that starts with {UUID}unix:/.. 

638 if pa_server and pa_server.startswith("{") and \ 

639 remote.pulseaudio_server and remote.pulseaudio_server==pa_server: 

640 log.error("Error: sound is disabled to prevent a sound loop") 

641 log.error(" identical Pulseaudio server '%s'", pa_server) 

642 return None 

643 pa_id = get_pulse_id() 

644 log("start sound, client id=%s, server id=%s", remote.pulseaudio_id, pa_id) 

645 if remote.pulseaudio_id and remote.pulseaudio_id==pa_id: 

646 log.error("Error: sound is disabled to prevent a sound loop") 

647 log.error(" identical Pulseaudio ID '%s'", pa_id) 

648 return None 

649 

650 device_type_str = "" 

651 if input_or_output is not None: 

652 device_type_str = "input" if input_or_output else "output" 

653 if want_monitor_device: 

654 device_type_str += " monitor" 

655 #def get_pa_device_options(monitors=False, input_or_output=None, ignored_devices=["bell-window-system"]) 

656 devices = get_pa_device_options(want_monitor_device, input_or_output) 

657 log("found %i pulseaudio %s device%s: %s", len(devices), device_type_str, engs(devices), devices) 

658 ignore = () 

659 if input_or_output is True: 

660 ignore = IGNORED_INPUT_DEVICES 

661 elif input_or_output is False: 

662 ignore = IGNORED_OUTPUT_DEVICES 

663 else: 

664 ignore = IGNORED_INPUT_DEVICES+IGNORED_OUTPUT_DEVICES 

665 if ignore and devices: 

666 #filter out the ignore list: 

667 filtered = {} 

668 for k,v in devices.items(): 

669 kl = bytestostr(k).strip().lower() 

670 vl = bytestostr(v).strip().lower() 

671 if kl not in ignore and vl not in ignore: 

672 filtered[k] = v 

673 devices = filtered 

674 

675 if not devices: 

676 log.error("Error: sound forwarding is disabled") 

677 log.error(" could not detect any Pulseaudio %s devices", device_type_str) 

678 return None 

679 

680 env_device = None 

681 if env_device_name: 

682 env_device = os.environ.get(env_device_name) 

683 #try to match one of the devices using the device name filters: 

684 if len(devices)>1: 

685 filters = [] 

686 matches = [] 

687 for match in (device_name_match, env_device): 

688 if not match: 

689 continue 

690 if match!=env_device: 

691 filters.append(match) 

692 match = match.lower() 

693 log("trying to match '%s' in devices=%s", match, devices) 

694 matches = dict((k,v) for k,v in devices.items() 

695 if (bytestostr(k).strip().lower().find(match)>=0 or 

696 bytestostr(v).strip().lower().find(match)>=0)) 

697 #log("matches(%s, %s)=%s", devices, match, matches) 

698 if len(matches)==1: 

699 log("found name match for '%s': %s", match, tuple(matches.items())[0]) 

700 break 

701 elif len(matches)>1: 

702 log.warn("Warning: Pulseaudio %s device name filter '%s'", device_type_str, match) 

703 log.warn(" matched %i devices", len(matches)) 

704 if filters or matches: 

705 if not matches: 

706 log.warn("Warning: Pulseaudio %s device name filter%s:", device_type_str, engs(filters)) 

707 log.warn(" %s", csv("'%s'" % x for x in filters)) 

708 log.warn(" did not match any of the devices found:") 

709 for k,v in devices.items(): 

710 log.warn(" * '%s'", k) 

711 log.warn(" '%s'", v) 

712 return None 

713 devices = matches 

714 

715 #still have too many devices to choose from? 

716 if len(devices)>1: 

717 if want_monitor_device: 

718 #use the monitor of the default sink if we find it: 

719 default_sink = get_default_sink() 

720 default_monitor = default_sink+".monitor" 

721 if default_monitor in devices: 

722 device_name = devices.get(default_monitor) 

723 log.info("using monitor of default sink: %s", device_name) 

724 return default_monitor 

725 

726 global WARNED_MULTIPLE_DEVICES 

727 if not WARNED_MULTIPLE_DEVICES: 

728 WARNED_MULTIPLE_DEVICES = True 

729 dtype = "audio" 

730 if want_monitor_device: 

731 dtype = "output monitor" 

732 elif input_or_output is False: 

733 dtype = "audio output" 

734 elif input_or_output is True: 

735 dtype = "audio input" 

736 log.info("found %i %s devices:", len(devices), dtype) 

737 for k,v in devices.items(): 

738 log.info(" * %s", bytestostr(v)) 

739 log.info(" %s", bytestostr(k)) 

740 if not env_device: #used already! 

741 log.info(" to select a specific one,") 

742 log.info(" use the environment variable '%s'", env_device_name) 

743 #default to first one: 

744 if USE_DEFAULT_DEVICE: 

745 log.info("using default pulseaudio device") 

746 return None 

747 #default to first one: 

748 device, device_name = tuple(devices.items())[0] 

749 log.info("using pulseaudio device:") 

750 log.info(" '%s'", bytestostr(device_name)) 

751 return device 

752 

753def get_pulse_source_defaults(device_name_match=None, want_monitor_device=True, remote=None): 

754 return get_pulse_defaults(device_name_match, want_monitor_device, 

755 input_or_output=not want_monitor_device, remote=remote, 

756 env_device_name=XPRA_PULSE_SOURCE_DEVICE_NAME) 

757 

758def get_pulse_sink_defaults(): 

759 return get_pulse_defaults(want_monitor_device=False, input_or_output=False, 

760 env_device_name=XPRA_PULSE_SINK_DEVICE_NAME) 

761 

762def get_directsound_source_defaults(device_name_match=None, want_monitor_device=True, remote=None): 

763 try: 

764 from xpra.platform.win32.directsound import get_devices, get_capture_devices 

765 if not want_monitor_device: 

766 devices = get_devices() 

767 log("DirectSoundEnumerate found %i device%s", len(devices), engs(devices)) 

768 else: 

769 devices = get_capture_devices() 

770 log("DirectSoundCaptureEnumerate found %i device%s", len(devices), engs(devices)) 

771 names = [] 

772 if devices: 

773 for guid, name in devices: 

774 if guid: 

775 log("* %-32s %s", name, guid) 

776 else: 

777 log("* %s", name) 

778 names.append(name) 

779 device_name = None 

780 if device_name_match: 

781 for name in names: 

782 if name.lower().find(device_name_match)>=0: 

783 device_name = name 

784 break 

785 if device_name is None: 

786 for name in names: 

787 if name.lower().find("primary")>=0: 

788 device_name = name 

789 break 

790 log("best matching %sdevice: %s", ["","capture "][want_monitor_device], device_name) 

791 if device_name is None and want_monitor_device: 

792 #we have to choose one because the default device 

793 #may not be a capture device? 

794 device_name = names[0] 

795 if device_name: 

796 log.info("using directsound %sdevice:", ["","capture "][want_monitor_device]) 

797 log.info(" '%s'", device_name) 

798 return { 

799 "device-name" : device_name, 

800 } 

801 except Exception as e: 

802 log("get_directsound_source_defaults%s", (device_name_match, want_monitor_device, remote), exc_info=True) 

803 log.error("Error quering sound devices:") 

804 log.error(" %s", e) 

805 return {} 

806 

807 

808#a list of functions to call to get the plugin options 

809#at runtime (so we can perform runtime checks on remote data, 

810# to avoid sound loops for example) 

811DEFAULT_SRC_PLUGIN_OPTIONS = { 

812 "test" : get_test_defaults, 

813 "pulse" : get_pulse_source_defaults, 

814 "direct" : get_directsound_source_defaults, 

815 } 

816 

817DEFAULT_SINK_PLUGIN_OPTIONS = { 

818 "pulse" : get_pulse_sink_defaults, 

819 } 

820 

821 

822def format_element_options(options): 

823 return csv("%s=%s" % (k,v) for k,v in options.items()) 

824 

825 

826def get_sound_source_options(plugin, options_str, device, want_monitor_device, remote): 

827 """ 

828 Given a plugin (short name), options string and remote info, 

829 return the options for the plugin given, 

830 using the dynamic defaults (which may use remote info) 

831 and applying the options string on top. 

832 """ 

833 #ie: get_sound_source_options("audiotestsrc", "wave=4,freq=220", {remote_pulseaudio_server=XYZ}): 

834 #use the defaults as starting point: 

835 defaults_fn = DEFAULT_SRC_PLUGIN_OPTIONS.get(plugin) 

836 log("DEFAULT_SRC_PLUGIN_OPTIONS(%s)=%s", plugin, defaults_fn) 

837 if defaults_fn: 

838 options = defaults_fn(device, want_monitor_device, remote) 

839 log("%s%s=%s", defaults_fn, (device, want_monitor_device, remote), options) 

840 if options is None: 

841 #means failure 

842 return None 

843 else: 

844 options = {} 

845 #if we add support for choosing devices in the GUI, 

846 #this code will then get used: 

847 if device and plugin in SRC_HAS_DEVICE_NAME: 

848 #assume the user knows the "device-name"... 

849 #(since I have no idea where to get the "device" string) 

850 options["device-name"] = device 

851 options.update(parse_simple_dict(options_str)) 

852 return options 

853 

854 

855def parse_sound_source(all_plugins, sound_source_plugin, device, want_monitor_device, remote): 

856 #format: PLUGINNAME:options 

857 #ie: test:wave=2,freq=110,volume=0.4 

858 #ie: pulse:device=device.alsa_input.pci-0000_00_14.2.analog-stereo 

859 plugin = sound_source_plugin.split(":")[0] 

860 options_str = (sound_source_plugin+":").split(":",1)[1].rstrip(":") 

861 simple_str = (plugin).lower().strip() 

862 if not simple_str: 

863 simple_str = get_default_source() 

864 if not simple_str: 

865 #choose the first one from 

866 options = [x for x in get_source_plugins() if x in all_plugins] 

867 if not options: 

868 log.error("no source plugins available") 

869 return None, {} 

870 log("parse_sound_source: no plugin specified, using default: %s", options[0]) 

871 simple_str = options[0] 

872 for s in ("src", "sound", "audio"): 

873 if simple_str.endswith(s): 

874 simple_str = simple_str[:-len(s)] 

875 gst_sound_source_plugin = NAME_TO_SRC_PLUGIN.get(simple_str) 

876 if not gst_sound_source_plugin: 

877 log.error("unknown source plugin: '%s' / '%s'", simple_str, sound_source_plugin) 

878 return None, {} 

879 log("parse_sound_source(%s, %s, %s) plugin=%s", all_plugins, sound_source_plugin, remote, gst_sound_source_plugin) 

880 options = get_sound_source_options(simple_str, options_str, device, want_monitor_device, remote) 

881 log("get_sound_source_options%s=%s", (simple_str, options_str, remote), options) 

882 if options is None: 

883 #means error 

884 return None, {} 

885 return gst_sound_source_plugin, options 

886 

887 

888def loop_warning_messages(mode="speaker"): 

889 return [ 

890 "Cannot start %s forwarding:" % mode, 

891 "client and server environment are identical,", 

892 "this would be likely to create an audio feedback loop", 

893 #" use XPRA_ALLOW_SOUND_LOOP=1 to force enable it", 

894 ] 

895 

896 

897def main(): 

898 from xpra.platform import program_context 

899 from xpra.log import enable_color 

900 with program_context("GStreamer-Info", "GStreamer Information"): 

901 enable_color() 

902 if "-v" in sys.argv or "--verbose" in sys.argv: 

903 log.enable_debug() 

904 import_gst() 

905 v = get_gst_version() 

906 if v[-1]==0: 

907 v = v[:-1] 

908 gst_vinfo = ".".join((str(x) for x in v)) 

909 print("Loaded Python GStreamer version %s for Python %s.%s" % ( 

910 gst_vinfo, sys.version_info[0], sys.version_info[1]) 

911 ) 

912 apn = get_all_plugin_names() 

913 print("GStreamer plugins found: %s" % csv(apn)) 

914 print("") 

915 print("GStreamer version: %s" % ".".join([str(x) for x in get_gst_version()])) 

916 print("PyGStreamer version: %s" % ".".join([str(x) for x in get_pygst_version()])) 

917 print("") 

918 encs = [x for x in CODEC_ORDER if has_encoder(x)] 

919 decs = [x for x in CODEC_ORDER if has_decoder(x)] 

920 print("encoders: %s" % csv(encs)) 

921 print("decoders: %s" % csv(decs)) 

922 print("muxers: %s" % csv(get_muxers())) 

923 print("demuxers: %s" % csv(get_demuxers())) 

924 print("stream compressors: %s" % csv(get_stream_compressors())) 

925 print("source plugins: %s" % csv([x for x in get_source_plugins() if x in apn])) 

926 print("sink plugins: %s" % csv([x for x in get_sink_plugins() if x in apn])) 

927 print("default sink: %s" % get_default_sink_plugin()) 

928 

929 

930if __name__ == "__main__": 

931 main()