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) 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. 

5 

6#must be done before importing gobject! 

7#pylint: disable=wrong-import-position 

8 

9import os 

10 

11from xpra.sound.gstreamer_util import import_gst, GST_FLOW_OK 

12gst = import_gst() 

13from gi.repository import GLib, GObject 

14 

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 

19 

20log = Logger("sound") 

21gstlog = Logger("gstreamer") 

22 

23 

24KNOWN_TAGS = set(( 

25 "bitrate", "codec", "audio-codec", "mode", 

26 "container-format", "encoder", "description", "language-code", 

27 "minimum-bitrate", "maximum-bitrate", "channel-mode", 

28 )) 

29 

30FAULT_RATE = envint("XPRA_SOUND_FAULT_INJECTION_RATE") 

31SAVE_AUDIO = os.environ.get("XPRA_SAVE_AUDIO") 

32 

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 

41 

42 

43class SoundPipeline(GObject.GObject): 

44 

45 generation = AtomicInteger() 

46 

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 } 

53 

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 

80 

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) 

92 

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() 

99 

100 

101 def idle_emit(self, sig, *args): 

102 self.idle_add(self.emit, sig, *args) 

103 

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) 

115 

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) 

121 

122 

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 

129 

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 

151 

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") 

159 

160 def get_state(self): 

161 return self.state 

162 

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 

169 

170 def update_state(self, state): 

171 log("update_state(%s)", state) 

172 self.state = state 

173 self.info["state"] = state 

174 

175 def inc_buffer_count(self, inc=1): 

176 self.buffer_count += inc 

177 self.info["buffer_count"] = self.buffer_count 

178 

179 def inc_byte_count(self, count): 

180 self.byte_count += count 

181 self.info["bytes"] = self.byte_count 

182 

183 

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 

188 

189 def get_volume(self): 

190 if self.volume: 

191 return int(self.volume.get_property("volume")*100) 

192 return GST_FLOW_OK 

193 

194 

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") 

219 

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") 

238 

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") 

265 

266 

267 def gstloginfo(self, msg, *args): 

268 if self.state!="stopped": 

269 gstlog.info(msg, *args) 

270 else: 

271 gstlog(msg, *args) 

272 

273 def gstlogwarn(self, msg, *args): 

274 if self.state!="stopped": 

275 gstlog.warn(msg, *args) 

276 else: 

277 gstlog(msg, *args) 

278 

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 

291 

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 

305 

306 

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 

391 

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) 

401 

402 def do_parse_element_message(self, message, name, props=None): 

403 gstlog("do_parse_element_message%s", (message, name, props)) 

404 

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)