Coverage for /home/antoine/projects/xpra-git/dist/python3/lib64/python/xpra/sound/gstreamer_util.py : 62%
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.
7import sys
8import os
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
20log = Logger("sound", "gstreamer")
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"
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
33GST_FLOW_OK = 0 #Gst.FlowReturn.OK
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
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
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"
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 }
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 }
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 ]
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
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")
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")
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 }
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 }
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 ]
219gst = None
221def get_pygst_version():
222 import gi
223 return getattr(gi, "version_info", ())
225def get_gst_version():
226 if not gst:
227 return ()
228 return gst.version()
231def do_import_gst():
232 global gst
233 if gst is not None:
234 return gst
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))
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
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
283def normv(v):
284 if v==2**64-1:
285 return -1
286 return int(v)
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
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
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
321CODECS = None
322ENCODERS = {} #(encoder, payloader, stream-compressor)
323DECODERS = {} #(decoder, depayloader, stream-compressor)
325def get_encoders():
326 init_codecs()
327 global ENCODERS
328 return ENCODERS
330def get_decoders():
331 init_codecs()
332 global DECODERS
333 return DECODERS
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
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)
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)
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
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
419def get_muxers():
420 muxers = []
421 for name,muxer,_ in MUX_OPTIONS:
422 if has_plugins(muxer):
423 muxers.append(name)
424 return muxers
426def get_demuxers():
427 demuxers = []
428 for name,_,demuxer in MUX_OPTIONS:
429 if has_plugins(demuxer):
430 demuxers.append(name)
431 return demuxers
433def get_stream_compressors():
434 return [x for x in ("lz4", "lzo") if has_stream_compressor(x)]
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
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
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)
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)
470def has_codec(name):
471 return has_encoder(name) and has_decoder(name)
473def can_encode():
474 return [x for x in CODEC_ORDER if has_encoder(x)]
476def can_decode():
477 return [x for x in CODEC_ORDER if has_decoder(x)]
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
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
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
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
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
585def get_test_defaults(*_args):
586 return {"wave" : 2, "freq" : 110, "volume" : 0.4}
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)}
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
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
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
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
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
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
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)
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)
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 {}
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 }
817DEFAULT_SINK_PLUGIN_OPTIONS = {
818 "pulse" : get_pulse_sink_defaults,
819 }
822def format_element_options(options):
823 return csv("%s=%s" % (k,v) for k,v in options.items())
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
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
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 ]
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())
930if __name__ == "__main__":
931 main()