Coverage for /home/antoine/projects/xpra-git/dist/python3/lib64/python/xpra/sound/sound_pipeline.py : 65%
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) 2010-2019 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.
6#must be done before importing gobject!
7#pylint: disable=wrong-import-position
9import os
11from xpra.sound.gstreamer_util import import_gst, GST_FLOW_OK
12gst = import_gst()
13from gi.repository import GLib, GObject
15from xpra.util import envint, AtomicInteger, noerr
16from xpra.os_util import monotonic_time, register_SIGUSR_signals
17from xpra.gtk_common.gobject_util import one_arg_signal
18from xpra.log import Logger
20log = Logger("sound")
21gstlog = Logger("gstreamer")
24KNOWN_TAGS = set((
25 "bitrate", "codec", "audio-codec", "mode",
26 "container-format", "encoder", "description", "language-code",
27 "minimum-bitrate", "maximum-bitrate", "channel-mode",
28 ))
30FAULT_RATE = envint("XPRA_SOUND_FAULT_INJECTION_RATE")
31SAVE_AUDIO = os.environ.get("XPRA_SAVE_AUDIO")
33_counter = 0
34def inject_fault():
35 global FAULT_RATE
36 if FAULT_RATE<=0:
37 return False
38 global _counter
39 _counter += 1
40 return (_counter % FAULT_RATE)==0
43class SoundPipeline(GObject.GObject):
45 generation = AtomicInteger()
47 __generic_signals__ = {
48 "state-changed" : one_arg_signal,
49 "error" : one_arg_signal,
50 "new-stream" : one_arg_signal,
51 "info" : one_arg_signal,
52 }
54 def __init__(self, codec):
55 GObject.GObject.__init__(self)
56 self.stream_compressor = None
57 self.codec = codec
58 self.codec_description = ""
59 self.codec_mode = ""
60 self.container_format = ""
61 self.container_description = ""
62 self.bus = None
63 self.bus_message_handler_id = None
64 self.bitrate = -1
65 self.pipeline = None
66 self.pipeline_str = ""
67 self.start_time = 0
68 self.state = "stopped"
69 self.buffer_count = 0
70 self.byte_count = 0
71 self.emit_info_timer = None
72 self.info = {
73 "codec" : self.codec,
74 "state" : self.state,
75 }
76 self.idle_add = GLib.idle_add
77 self.timeout_add = GLib.timeout_add
78 self.source_remove = GLib.source_remove
79 self.file = None
81 def init_file(self, codec):
82 gen = self.generation.increase()
83 log("init_file(%s) generation=%s, SAVE_AUDIO=%s", codec, gen, SAVE_AUDIO)
84 if SAVE_AUDIO is not None:
85 parts = codec.split("+")
86 if len(parts)>1:
87 filename = SAVE_AUDIO+str(gen)+"-"+parts[0]+".%s" % parts[1]
88 else:
89 filename = SAVE_AUDIO+str(gen)+".%s" % codec
90 self.file = open(filename, 'wb')
91 log.info("saving %s stream to %s", codec, filename)
93 def save_to_file(self, *buffers):
94 f = self.file
95 if f and buffers:
96 for x in buffers:
97 self.file.write(x)
98 self.file.flush()
101 def idle_emit(self, sig, *args):
102 self.idle_add(self.emit, sig, *args)
104 def emit_info(self):
105 if self.emit_info_timer:
106 return
107 def do_emit_info():
108 self.emit_info_timer = None
109 if self.pipeline:
110 info = self.get_info()
111 #reset info:
112 self.info = {}
113 self.emit("info", info)
114 self.emit_info_timer = self.timeout_add(200, do_emit_info)
116 def cancel_emit_info_timer(self):
117 eit = self.emit_info_timer
118 if eit:
119 self.emit_info_timer = None
120 self.source_remove(eit)
123 def get_info(self) -> dict:
124 info = self.info.copy()
125 if inject_fault():
126 info["INJECTING_NONE_FAULT"] = None
127 log.warn("injecting None fault: get_info()=%s", info)
128 return info
130 def setup_pipeline_and_bus(self, elements):
131 gstlog("pipeline elements=%s", elements)
132 self.pipeline_str = " ! ".join([x for x in elements if x is not None])
133 gstlog("pipeline=%s", self.pipeline_str)
134 self.start_time = monotonic_time()
135 try:
136 self.pipeline = gst.parse_launch(self.pipeline_str)
137 except Exception as e:
138 self.pipeline = None
139 gstlog.error("Error setting up the sound pipeline:")
140 gstlog.error(" %s", e)
141 gstlog.error(" GStreamer pipeline for %s:", self.codec)
142 for i,x in enumerate(elements):
143 gstlog.error(" %s%s", x, ["", " ! \\"][int(i<(len(elements)-1))])
144 self.cleanup()
145 return False
146 self.bus = self.pipeline.get_bus()
147 self.bus_message_handler_id = self.bus.connect("message", self.on_message)
148 self.bus.add_signal_watch()
149 self.info["pipeline"] = self.pipeline_str
150 return True
152 def do_get_state(self, state):
153 if not self.pipeline:
154 return "stopped"
155 return {gst.State.PLAYING : "active",
156 gst.State.PAUSED : "paused",
157 gst.State.NULL : "stopped",
158 gst.State.READY : "ready"}.get(state, "unknown")
160 def get_state(self):
161 return self.state
163 def update_bitrate(self, new_bitrate):
164 if new_bitrate==self.bitrate:
165 return
166 self.bitrate = new_bitrate
167 log("new bitrate: %s", self.bitrate)
168 self.info["bitrate"] = new_bitrate
170 def update_state(self, state):
171 log("update_state(%s)", state)
172 self.state = state
173 self.info["state"] = state
175 def inc_buffer_count(self, inc=1):
176 self.buffer_count += inc
177 self.info["buffer_count"] = self.buffer_count
179 def inc_byte_count(self, count):
180 self.byte_count += count
181 self.info["bytes"] = self.byte_count
184 def set_volume(self, volume=100):
185 if self.volume:
186 self.volume.set_property("volume", volume/100.0)
187 self.info["volume"] = volume
189 def get_volume(self):
190 if self.volume:
191 return int(self.volume.get_property("volume")*100)
192 return GST_FLOW_OK
195 def start(self):
196 if not self.pipeline:
197 log.error("cannot start")
198 return
199 register_SIGUSR_signals(self.idle_add)
200 log("SoundPipeline.start() codec=%s", self.codec)
201 self.idle_emit("new-stream", self.codec)
202 self.update_state("active")
203 self.pipeline.set_state(gst.State.PLAYING)
204 if self.stream_compressor:
205 self.info["stream-compressor"] = self.stream_compressor
206 self.emit_info()
207 #we may never get the stream start, synthesize codec event so we get logging:
208 parts = self.codec.split("+")
209 self.timeout_add(1000, self.new_codec_description, parts[0])
210 if len(parts)>1 and parts[1]!=self.stream_compressor:
211 self.timeout_add(1000, self.new_container_description, parts[1])
212 elif self.container_format:
213 self.timeout_add(1000, self.new_container_description, self.container_format)
214 if self.stream_compressor:
215 def logsc():
216 self.gstloginfo("using stream compression %s", self.stream_compressor)
217 self.timeout_add(1000, logsc)
218 log("SoundPipeline.start() done")
220 def stop(self):
221 p = self.pipeline
222 self.pipeline = None
223 if not p:
224 return
225 log("SoundPipeline.stop() state=%s", self.state)
226 #uncomment this to see why we end up calling stop()
227 #import traceback
228 #for x in traceback.format_stack():
229 # for s in x.split("\n"):
230 # v = s.replace("\r", "").replace("\n", "")
231 # if v:
232 # log(v)
233 if self.state not in ("starting", "stopped", "ready", None):
234 log.info("stopping")
235 self.update_state("stopped")
236 p.set_state(gst.State.NULL)
237 log("SoundPipeline.stop() done")
239 def cleanup(self):
240 log("SoundPipeline.cleanup()")
241 self.cancel_emit_info_timer()
242 self.stop()
243 b = self.bus
244 self.bus = None
245 log("SoundPipeline.cleanup() bus=%s", b)
246 if not b:
247 return
248 b.remove_signal_watch()
249 bmhid = self.bus_message_handler_id
250 log("SoundPipeline.cleanup() bus_message_handler_id=%s", bmhid)
251 if bmhid:
252 self.bus_message_handler_id = None
253 b.disconnect(bmhid)
254 self.pipeline = None
255 self.codec = None
256 self.bitrate = -1
257 self.state = None
258 self.volume = None
259 self.info = {}
260 f = self.file
261 if f:
262 self.file = None
263 noerr(f.close)
264 log("SoundPipeline.cleanup() done")
267 def gstloginfo(self, msg, *args):
268 if self.state!="stopped":
269 gstlog.info(msg, *args)
270 else:
271 gstlog(msg, *args)
273 def gstlogwarn(self, msg, *args):
274 if self.state!="stopped":
275 gstlog.warn(msg, *args)
276 else:
277 gstlog(msg, *args)
279 def new_codec_description(self, desc):
280 log("new_codec_description(%s) current codec description=%s", desc, self.codec_description)
281 if not desc:
282 return
283 dl = desc.lower()
284 if dl=="wav" and self.codec_description:
285 return
286 cdl = self.codec_description.lower()
287 if not cdl or (cdl!=dl and dl.find(cdl)<0 and cdl.find(dl)<0):
288 self.gstloginfo("using '%s' audio codec", dl)
289 self.codec_description = dl
290 self.info["codec_description"] = dl
292 def new_container_description(self, desc):
293 log("new_container_description(%s) current container description=%s", desc, self.container_description)
294 if not desc:
295 return
296 cdl = self.container_description.lower()
297 dl = {
298 "mka" : "matroska",
299 "mpeg4" : "iso fmp4",
300 }.get(desc.lower(), desc.lower())
301 if not cdl or (cdl!=dl and dl.find(cdl)<0 and cdl.find(dl)<0):
302 self.gstloginfo("using '%s' container format", dl)
303 self.container_description = dl
304 self.info["container_description"] = dl
307 def on_message(self, _bus, message):
308 #log("on_message(%s, %s)", bus, message)
309 gstlog("on_message: %s", message)
310 t = message.type
311 if t == gst.MessageType.EOS:
312 self.pipeline.set_state(gst.State.NULL)
313 self.gstloginfo("EOS")
314 self.update_state("stopped")
315 self.idle_emit("state-changed", self.state)
316 elif t == gst.MessageType.ERROR:
317 self.pipeline.set_state(gst.State.NULL)
318 err, details = message.parse_error()
319 gstlog.error("Gstreamer pipeline error: %s", err.message)
320 for l in err.args:
321 if l!=err.message:
322 gstlog(" %s", l)
323 try:
324 #prettify (especially on win32):
325 p = details.find("\\Source\\")
326 if p>0:
327 details = details[p+len("\\Source\\"):]
328 for d in details.split(": "):
329 for dl in d.splitlines():
330 if dl.strip():
331 gstlog.error(" %s", dl.strip())
332 except Exception:
333 gstlog.error(" %s", details)
334 self.update_state("error")
335 self.idle_emit("error", str(err))
336 #exit
337 self.cleanup()
338 elif t == gst.MessageType.TAG:
339 try:
340 self.parse_message(message)
341 except Exception as e:
342 self.gstlogwarn("Warning: failed to parse gstreamer message:")
343 self.gstlogwarn(" %s: %s", type(e), e)
344 elif t == gst.MessageType.ELEMENT:
345 try:
346 self.parse_element_message(message)
347 except Exception as e:
348 self.gstlogwarn("Warning: failed to parse gstreamer element message:")
349 self.gstlogwarn(" %s: %s", type(e), e)
350 elif t == gst.MessageType.STREAM_STATUS:
351 gstlog("stream status: %s", message)
352 elif t == gst.MessageType.STREAM_START:
353 log("stream start: %s", message)
354 #with gstreamer 1.x, we don't always get the "audio-codec" message..
355 #so print the codec from here instead (and assume gstreamer is using what we told it to)
356 #after a delay, just in case we do get the real "audio-codec" message!
357 self.timeout_add(500, self.new_codec_description, self.codec.split("+")[0])
358 elif t in (gst.MessageType.ASYNC_DONE, gst.MessageType.NEW_CLOCK):
359 gstlog("%s", message)
360 elif t == gst.MessageType.STATE_CHANGED:
361 _, new_state, _ = message.parse_state_changed()
362 gstlog("state-changed on %s: %s", message.src, gst.Element.state_get_name(new_state))
363 state = self.do_get_state(new_state)
364 if isinstance(message.src, gst.Pipeline):
365 self.update_state(state)
366 self.idle_emit("state-changed", state)
367 elif t == gst.MessageType.DURATION_CHANGED:
368 gstlog("duration changed: %s", message)
369 elif t == gst.MessageType.LATENCY:
370 gstlog("latency message from %s: %s", message.src, message)
371 elif t == gst.MessageType.INFO:
372 self.gstloginfo("pipeline message: %s", message)
373 elif t == gst.MessageType.WARNING:
374 w = message.parse_warning()
375 self.gstlogwarn("pipeline warning: %s", w[0].message)
376 for x in w[1:]:
377 for l in x.split(":"):
378 if l:
379 if l.startswith("\n"):
380 l = l.strip("\n")+" "
381 for lp in l.split(". "):
382 lp = lp.strip()
383 if lp:
384 self.gstlogwarn(" %s", lp)
385 else:
386 self.gstlogwarn(" %s", l.strip("\n\r"))
387 else:
388 self.gstlogwarn("unhandled bus message type %s: %s", t, message)
389 self.emit_info()
390 return GST_FLOW_OK
392 def parse_element_message(self, message):
393 structure = message.get_structure()
394 props = {
395 "seqnum" : int(message.seqnum),
396 }
397 for i in range(structure.n_fields()):
398 name = structure.nth_field_name(i)
399 props[name] = structure.get_value(name)
400 self.do_parse_element_message(message, message.src.get_name(), props)
402 def do_parse_element_message(self, message, name, props=None):
403 gstlog("do_parse_element_message%s", (message, name, props))
405 def parse_message(self, message):
406 #message parsing code for GStreamer 1.x
407 taglist = message.parse_tag()
408 tags = [taglist.nth_tag_name(x) for x in range(taglist.n_tags())]
409 gstlog("bus message with tags=%s", tags)
410 if not tags:
411 #ignore it
412 return
413 if "bitrate" in tags:
414 new_bitrate = taglist.get_uint("bitrate")
415 if new_bitrate[0] is True:
416 self.update_bitrate(new_bitrate[1])
417 gstlog("bitrate: %s", new_bitrate[1])
418 if "codec" in tags:
419 desc = taglist.get_string("codec")
420 if desc[0] is True:
421 self.new_codec_description(desc[1])
422 if "audio-codec" in tags:
423 desc = taglist.get_string("audio-codec")
424 if desc[0] is True:
425 self.new_codec_description(desc[1])
426 gstlog("audio-codec: %s", desc[1])
427 if "mode" in tags:
428 mode = taglist.get_string("mode")
429 if mode[0] is True and self.codec_mode!=mode[1]:
430 gstlog("mode: %s", mode[1])
431 self.codec_mode = mode[1]
432 self.info["codec_mode"] = self.codec_mode
433 if "container-format" in tags:
434 cf = taglist.get_string("container-format")
435 if cf[0] is True:
436 self.new_container_description(cf[1])
437 for x in ("encoder", "description", "language-code"):
438 if x in tags:
439 desc = taglist.get_string(x)
440 gstlog("%s: %s", x, desc[1])
441 if not set(tags).intersection(KNOWN_TAGS):
442 structure = message.get_structure()
443 self.gstloginfo("unknown sound pipeline tag message: %s, tags=%s", structure.to_string(), tags)