Skip to content

Commit b23849f

Browse files
committed
add basic pulseaudio support
1 parent 0e4e1c5 commit b23849f

File tree

7 files changed

+473
-0
lines changed

7 files changed

+473
-0
lines changed

resources/language/resource.language.en_gb/strings.po

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1047,3 +1047,39 @@ msgstr ""
10471047
msgctxt "#32400"
10481048
msgid "Idle Timeout"
10491049
msgstr ""
1050+
1051+
msgctxt "#32500"
1052+
msgid "Pulseaudio"
1053+
msgstr ""
1054+
1055+
msgctxt "#32501"
1056+
msgid "Configure pulseaudio devices"
1057+
msgstr ""
1058+
1059+
msgctxt "#32502"
1060+
msgid "Select Profile"
1061+
msgstr ""
1062+
1063+
msgctxt "#32503"
1064+
msgid "Pulseaudio is disabled"
1065+
msgstr ""
1066+
1067+
msgctxt "#32504"
1068+
msgid "No Pulseaudio device found."
1069+
msgstr ""
1070+
1071+
msgctxt "#32505"
1072+
msgid "Change Profile"
1073+
msgstr ""
1074+
1075+
msgctxt "#32506"
1076+
msgid "Driver"
1077+
msgstr ""
1078+
1079+
msgctxt "#32507"
1080+
msgid "Current Profile"
1081+
msgstr ""
1082+
1083+
msgctxt "#32508"
1084+
msgid "Set as Default"
1085+
msgstr ""

resources/lib/dbus_pulseaudio.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# SPDX-License-Identifier: GPL-2.0
2+
# Copyright (C) 2020-present Team LibreELEC
3+
4+
import dbus_utils
5+
import dbussy
6+
import ravel
7+
8+
BUS_NAME = 'org.pulseaudio.Server'
9+
PATH_PULSEAUDIO_CORE = '/org/pulseaudio/core1'
10+
INTERFACE_PULSEAUDIO_CORE = 'org.PulseAudio.Core1'
11+
INTERFACE_PULSEAUDIO_CARD = 'org.PulseAudio.Core1.Card'
12+
INTERFACE_PULSEAUDIO_CARDPROFILE = 'org.PulseAudio.Core1.CardProfile'
13+
14+
def core_get_property(name):
15+
return call_method(BUS_NAME, PATH_PULSEAUDIO_CORE, dbussy.DBUS.INTERFACE_PROPERTIES, 'Get', INTERFACE_PULSEAUDIO_CORE, name)
16+
17+
def core_set_property(name, value):
18+
return call_method(BUS_NAME, PATH_PULSEAUDIO_CORE, dbussy.DBUS.INTERFACE_PROPERTIES, 'Set', INTERFACE_PULSEAUDIO_CORE, name, value)
19+
20+
def core_set_fallback_sink(sink):
21+
return core_set_property('FallbackSink', (dbussy.DBUS.Signature('o'), sink))
22+
23+
def card_get_properties(path):
24+
return call_method(BUS_NAME, path, dbussy.DBUS.INTERFACE_PROPERTIES, 'GetAll', INTERFACE_PULSEAUDIO_CARD)
25+
26+
def card_get_property(path, name):
27+
return call_method(BUS_NAME, path, dbussy.DBUS.INTERFACE_PROPERTIES, 'Get', INTERFACE_PULSEAUDIO_CARD, name)
28+
29+
def card_set_property(path, name, value):
30+
return call_method(BUS_NAME, path, dbussy.DBUS.INTERFACE_PROPERTIES, 'Set', INTERFACE_PULSEAUDIO_CARD, name, value)
31+
32+
def card_set_active_profile(path, profile):
33+
return card_set_property(path, "ActiveProfile", (dbussy.DBUS.Signature('o'), profile))
34+
35+
def profile_get_property(path, name):
36+
return call_method(BUS_NAME, path, dbussy.DBUS.INTERFACE_PROPERTIES, 'Get', INTERFACE_PULSEAUDIO_CARDPROFILE, name)
37+
38+
def system_has_pulseaudio():
39+
return conn is not None
40+
41+
def call_method(bus_name, path, interface, method_name, *args, **kwargs):
42+
interface = BUS[bus_name][path].get_interface(interface)
43+
method = getattr(interface, method_name)
44+
result = method(*args, **kwargs)
45+
first = next(iter(result or []), None)
46+
return dbus_utils.convert_from_dbussy(first)
47+
48+
try:
49+
conn = dbussy.Connection.open('unix:path=/var/run/pulse/dbus-socket', private=False)
50+
conn.bus_unique_name = 'PulseAudio'
51+
BUS = ravel.Connection(conn)
52+
except Exception as e:
53+
pass

resources/lib/defaults.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,16 @@
3636
}
3737
bluetooth['ENABLED'] = bluetooth['ENABLED']()
3838

39+
################################################################################
40+
# Pulseaudio Module
41+
################################################################################
42+
43+
pulseaudio = {
44+
'PULSEAUDIO_DAEMON': '/usr/bin/pulseaudio',
45+
'ENABLED': lambda : (True if os.path.exists(pulseaudio['PULSEAUDIO_DAEMON']) else False)
46+
}
47+
pulseaudio['ENABLED'] = pulseaudio['ENABLED']()
48+
3949
################################################################################
4050
# Service Module
4151
################################################################################
@@ -99,4 +109,5 @@
99109
'obexd': ['obex.service'],
100110
'crond': ['cron.service'],
101111
'iptables': ['iptables.service'],
112+
'pulseaudio': ['pulseaudio.service'],
102113
}

resources/lib/modules/pulseaudio.py

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
# SPDX-License-Identifier: GPL-2.0
2+
# Copyright (C) 2021-present Team LibreELEC (https://libreelec.tv)
3+
4+
import dbus_pulseaudio
5+
import log
6+
import modules
7+
import oe
8+
import xbmcgui
9+
10+
class pulseaudio(modules.Module):
11+
12+
menu = {'7': {
13+
'name': 32500,
14+
'menuLoader': 'menu_connections',
15+
'listTyp': 'palist',
16+
'InfoText': 32501,
17+
}}
18+
ENABLED = False
19+
PULSEAUDIO_DAEMON = None
20+
21+
@log.log_function()
22+
def __init__(self, oeMain):
23+
super().__init__()
24+
self.visible = False
25+
self.listItems = {}
26+
27+
@log.log_function()
28+
def do_init(self):
29+
self.visible = True
30+
31+
@log.log_function()
32+
def start_service(self):
33+
pass
34+
35+
@log.log_function()
36+
def stop_service(self):
37+
pass
38+
39+
@log.log_function()
40+
def exit(self):
41+
self.clear_list()
42+
self.visible = False
43+
44+
# ###################################################################
45+
# # Pulseaudio Core
46+
# ###################################################################
47+
48+
@log.log_function()
49+
def get_cards(self):
50+
cards = {}
51+
for card in dbus_pulseaudio.core_get_property('Cards'):
52+
cards[card] = dbus_pulseaudio.card_get_properties(card)
53+
54+
return cards
55+
56+
# ###################################################################
57+
# # Pulseaudio Card
58+
# ###################################################################
59+
60+
@log.log_function()
61+
def set_fallback_sink(self, listItem=None):
62+
if listItem is None:
63+
listItem = oe.winOeMain.getControl(oe.listObject['palist']).getSelectedItem()
64+
if listItem is None:
65+
return
66+
67+
sink = listItem.getProperty('Sink')
68+
dbus_pulseaudio.core_set_fallback_sink(sink)
69+
70+
@log.log_function()
71+
def change_profile(self, listItem=None):
72+
if listItem is None:
73+
listItem = oe.winOeMain.getControl(oe.listObject['palist']).getSelectedItem()
74+
if listItem is None:
75+
return
76+
77+
card = listItem.getProperty('entry')
78+
profiles = dbus_pulseaudio.card_get_property(card, 'Profiles')
79+
80+
items = []
81+
82+
# we only want to list the available profiles
83+
profiles = [profile for profile in profiles if dbus_pulseaudio.profile_get_property(profile, 'Available') == 1]
84+
items = [dbus_pulseaudio.profile_get_property(profile, 'Description') for profile in profiles]
85+
86+
try:
87+
active = profiles.index(listItem.getProperty('ActiveProfile'))
88+
except ValueError:
89+
active = 0
90+
91+
select_window = xbmcgui.Dialog()
92+
title = oe._(32502)
93+
result = select_window.select(title, items, preselect=active, useDetails=True)
94+
if result >= 0:
95+
dbus_pulseaudio.card_set_active_profile(card, profiles[result])
96+
97+
# ###################################################################
98+
# # Pulseaudio GUI
99+
# ###################################################################
100+
101+
@log.log_function()
102+
def clear_list(self):
103+
remove = [entry for entry in self.listItems]
104+
for entry in remove:
105+
del self.listItems[entry]
106+
107+
@log.log_function()
108+
def menu_connections(self, focusItem=None):
109+
if not hasattr(oe, 'winOeMain'):
110+
return 0
111+
if not oe.winOeMain.visible:
112+
return 0
113+
if not dbus_pulseaudio.system_has_pulseaudio():
114+
oe.winOeMain.getControl(1601).setLabel(oe._(32503))
115+
self.clear_list()
116+
oe.winOeMain.getControl(int(oe.listObject['palist'])).reset()
117+
oe.dbg_log('pulseaudio::menu_connections', 'exit_function (PA Disabled)', oe.LOGDEBUG)
118+
return
119+
oe.winOeMain.getControl(1601).setLabel(oe._(32504))
120+
dictProperties = {}
121+
122+
# type 1=int, 2=string
123+
124+
properties = [
125+
{
126+
'type': 2,
127+
'value': 'Driver',
128+
},
129+
{
130+
'type': 2,
131+
'value': 'ActiveProfile',
132+
},
133+
]
134+
135+
rebuildList = 0
136+
self.dbusDevices = self.get_cards()
137+
for dbusDevice in self.dbusDevices:
138+
rebuildList = 1
139+
oe.winOeMain.getControl(int(oe.listObject['palist'])).reset()
140+
self.clear_list()
141+
break
142+
143+
fallbackSink = dbus_pulseaudio.core_get_property('FallbackSink')
144+
145+
for dbusDevice in self.dbusDevices:
146+
dictProperties = {}
147+
cardName = ''
148+
dictProperties['entry'] = dbusDevice
149+
dictProperties['modul'] = self.__class__.__name__
150+
dictProperties['action'] = 'open_context_menu'
151+
152+
if 'ActiveProfile' in self.dbusDevices[dbusDevice]:
153+
activeProfile = self.dbusDevices[dbusDevice]['ActiveProfile']
154+
dictProperties['ActiveProfileName'] = dbus_pulseaudio.profile_get_property(activeProfile, 'Name')
155+
156+
if fallbackSink is not None:
157+
dictProperties['FallbackSink'] = '0'
158+
159+
if 'Sinks' in self.dbusDevices[dbusDevice]:
160+
for sink in self.dbusDevices[dbusDevice]['Sinks']:
161+
dictProperties['Sink'] = sink
162+
if sink == fallbackSink:
163+
dictProperties['FallbackSink'] = '1'
164+
165+
# we can only handle one sink per card right now
166+
break
167+
168+
if 'PropertyList' in self.dbusDevices[dbusDevice]:
169+
if 'device.description' in self.dbusDevices[dbusDevice]['PropertyList']:
170+
cardName = bytearray(self.dbusDevices[dbusDevice]['PropertyList']['device.description']).decode().strip('\x00')
171+
172+
if cardName == '':
173+
cardName = self.dbusDevices[dbusDevice]['Name']
174+
175+
for prop in properties:
176+
name = prop['value']
177+
if name in self.dbusDevices[dbusDevice]:
178+
value = self.dbusDevices[dbusDevice][name]
179+
if prop['type'] == 1:
180+
value = str(int(value))
181+
if prop['type'] == 2:
182+
value = str(value)
183+
if prop['type'] == 3:
184+
value = str(len(value))
185+
if prop['type'] == 4:
186+
value = str(int(value))
187+
dictProperties[name] = value
188+
if rebuildList == 1:
189+
self.listItems[dbusDevice] = oe.winOeMain.addConfigItem(cardName, dictProperties, oe.listObject['palist'])
190+
else:
191+
if self.listItems[dbusDevice] != None:
192+
self.listItems[dbusDevice].setLabel(cardName)
193+
for dictProperty in dictProperties:
194+
self.listItems[dbusDevice].setProperty(dictProperty, dictProperties[dictProperty])
195+
196+
@log.log_function()
197+
def open_context_menu(self, listItem):
198+
values = {}
199+
if listItem is None:
200+
listItem = oe.winOeMain.getControl(oe.listObject['palist']).getSelectedItem()
201+
if listItem.getProperty('ActiveProfile') != '':
202+
values[1] = {
203+
'text': oe._(32505),
204+
'action': 'change_profile',
205+
}
206+
if listItem.getProperty('FallbackSink') != '1':
207+
values[2] = {
208+
'text': oe._(32508),
209+
'action': 'set_fallback_sink',
210+
}
211+
items = []
212+
actions = []
213+
for key in list(values.keys()):
214+
items.append(values[key]['text'])
215+
actions.append(values[key]['action'])
216+
select_window = xbmcgui.Dialog()
217+
title = oe._(32012)
218+
result = select_window.select(title, items)
219+
if result >= 0:
220+
getattr(self, actions[result])(listItem)

resources/lib/oe.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
'list': 1100,
4747
'netlist': 1200,
4848
'btlist': 1300,
49+
'palist': 1600,
4950
'other': 1900,
5051
'test': 900,
5152
}

resources/lib/oeWindows.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,14 @@ def __init__(self, *args, **kwargs):
3434
self.guiList = 1100
3535
self.guiNetList = 1200
3636
self.guiBtList = 1300
37+
self.guiPaList = 1600
3738
self.guiOther = 1900
3839
self.guiLists = [
3940
1000,
4041
1100,
4142
1200,
4243
1300,
44+
1600,
4345
]
4446
self.buttons = {
4547
1: {

0 commit comments

Comments
 (0)