Skip to content

Commit d84ba91

Browse files
committed
add basic pulseaudio support
1 parent f9ccb2f commit d84ba91

File tree

7 files changed

+489
-0
lines changed

7 files changed

+489
-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
@@ -1051,3 +1051,39 @@ msgstr ""
10511051
msgctxt "#32401"
10521052
msgid "Syncing Disks..."
10531053
msgstr ""
1054+
1055+
msgctxt "#32500"
1056+
msgid "Pulseaudio"
1057+
msgstr ""
1058+
1059+
msgctxt "#32501"
1060+
msgid "Configure pulseaudio devices"
1061+
msgstr ""
1062+
1063+
msgctxt "#32502"
1064+
msgid "Select Profile"
1065+
msgstr ""
1066+
1067+
msgctxt "#32503"
1068+
msgid "Pulseaudio is disabled"
1069+
msgstr ""
1070+
1071+
msgctxt "#32504"
1072+
msgid "No Pulseaudio device found."
1073+
msgstr ""
1074+
1075+
msgctxt "#32505"
1076+
msgid "Change Profile"
1077+
msgstr ""
1078+
1079+
msgctxt "#32506"
1080+
msgid "Driver"
1081+
msgstr ""
1082+
1083+
msgctxt "#32507"
1084+
msgid "Current Profile"
1085+
msgstr ""
1086+
1087+
msgctxt "#32508"
1088+
msgid "Set as Default"
1089+
msgstr ""

resources/lib/dbus_pulseaudio.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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+
INTERFACE_PULSEAUDIO_DEVICE = 'org.PulseAudio.Core1.Device'
14+
15+
def core_get_property(name):
16+
return call_method(BUS_NAME, PATH_PULSEAUDIO_CORE, dbussy.DBUS.INTERFACE_PROPERTIES, 'Get', INTERFACE_PULSEAUDIO_CORE, name)
17+
18+
def core_set_property(name, value):
19+
return call_method(BUS_NAME, PATH_PULSEAUDIO_CORE, dbussy.DBUS.INTERFACE_PROPERTIES, 'Set', INTERFACE_PULSEAUDIO_CORE, name, value)
20+
21+
def core_set_fallback_sink(sink):
22+
return core_set_property('FallbackSink', (dbussy.DBUS.Signature('o'), sink))
23+
24+
def card_get_properties(path):
25+
return call_method(BUS_NAME, path, dbussy.DBUS.INTERFACE_PROPERTIES, 'GetAll', INTERFACE_PULSEAUDIO_CARD)
26+
27+
def card_get_property(path, name):
28+
return call_method(BUS_NAME, path, dbussy.DBUS.INTERFACE_PROPERTIES, 'Get', INTERFACE_PULSEAUDIO_CARD, name)
29+
30+
def card_set_property(path, name, value):
31+
return call_method(BUS_NAME, path, dbussy.DBUS.INTERFACE_PROPERTIES, 'Set', INTERFACE_PULSEAUDIO_CARD, name, value)
32+
33+
def card_set_active_profile(path, profile):
34+
return card_set_property(path, "ActiveProfile", (dbussy.DBUS.Signature('o'), profile))
35+
36+
def profile_get_property(path, name):
37+
return call_method(BUS_NAME, path, dbussy.DBUS.INTERFACE_PROPERTIES, 'Get', INTERFACE_PULSEAUDIO_CARDPROFILE, name)
38+
39+
def sink_get_property(path, name):
40+
return call_method(BUS_NAME, path, dbussy.DBUS.INTERFACE_PROPERTIES, 'Get', INTERFACE_PULSEAUDIO_DEVICE, name)
41+
42+
def system_has_pulseaudio():
43+
return conn is not None
44+
45+
def call_method(bus_name, path, interface, method_name, *args, **kwargs):
46+
interface = BUS[bus_name][path].get_interface(interface)
47+
method = getattr(interface, method_name)
48+
result = method(*args, **kwargs)
49+
first = next(iter(result or []), None)
50+
return dbus_utils.convert_from_dbussy(first)
51+
52+
try:
53+
conn = dbussy.Connection.open('unix:path=/var/run/pulse/dbus-socket', private=False)
54+
conn.bus_unique_name = 'PulseAudio'
55+
BUS = ravel.Connection(conn)
56+
except Exception as e:
57+
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
################################################################################
@@ -104,4 +114,5 @@
104114
'obexd': ['obex.service'],
105115
'crond': ['cron.service'],
106116
'iptables': ['iptables.service'],
117+
'pulseaudio': ['pulseaudio.service'],
107118
}

resources/lib/modules/pulseaudio.py

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

0 commit comments

Comments
 (0)