-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathsteamlink.py
304 lines (228 loc) · 10.3 KB
/
steamlink.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
import getpass
import json
from subprocess import Popen, PIPE, STDOUT
import os
import pathlib
import xbmc
import xbmcgui
from xbmcvfs import translatePath
def launch(addon, hostname=None):
# Check if steamlink is installed and offer to install
if is_steamlink_installed() is False:
update(addon)
# If steamlink is still not installed abort
if is_steamlink_installed() is False:
dialog = xbmcgui.Dialog()
dialog.ok(addon.getLocalizedString(30200), addon.getLocalizedString(30201))
return
# Initialise argument vars
systemd_args = []
steamlink_command = f'bash {get_resource_path("bin/launch_steamlink.sh")}'
steamlink_args = []
# Check if systemd-run can be used in user-mode
if os.environ.get('DBUS_SESSION_BUS_ADDRESS') is not None or os.environ.get('XDG_RUNTIME_DIR') is not None:
systemd_args.append('--user')
elif os.geteuid() != 0:
# If systemd user-mode can't be used and the current kodi-user is not root, try sudo for switching (OSMC)
steamlink_command = f'sudo -u {getpass.getuser()} {steamlink_command}'
# Check for a forced EGL display mode
force_mode = addon.getSetting('display_egl_resolution')
if force_mode != "default":
systemd_args.append(f'--setenv=FORCE_EGL_MODE="{force_mode}"')
# Append addon path
systemd_args.append(f'--setenv=ADDON_PROFILE_PATH="{get_addon_data_path()}"')
# Resolve audio output device
try:
service = 'Default'
device_name = 'Default'
kodi_audio_device = get_kodi_audio_device()
if len(kodi_audio_device) == 1:
service = kodi_audio_device[0]
if len(kodi_audio_device) == 2:
service, device_name = kodi_audio_device
if service == 'ALSA':
# Disable PulseAudio output by using a Steamlink environment variable
systemd_args.append('--setenv=PULSE_SERVER="none"')
systemd_args.append('--setenv=SDL_AUDIODRIVER="alsa"')
speaker_setup_write_alsa_config(addon)
elif service == 'PULSE':
# Tell pulse to use a specific device configured in Kodi
systemd_args.append(f'--setenv=PULSE_SINK="{device_name}"')
elif service != 'Default':
# Raise a warning when ALSA and PULSE are not detected
raise RuntimeError(f'Audio service {service} not supported')
except Exception as err:
xbmc.log(
f'Failed to resolve audio output device, audio within Steamlink might not work: {err}', xbmc.LOGWARNING
)
# Create command to launch steamlink
launch_command = 'systemd-run {} {}'.format(' '.join(systemd_args), steamlink_command)
# Prepare the command
command = f'{launch_command} ' + ' '.join(steamlink_args)
# Log the command so debugging problems is easier
xbmc.log(f'Launching steamlink: {command}', xbmc.LOGINFO)
# Show a dialog
game_name = addon.getLocalizedString(30001)
launch_label = addon.getLocalizedString(30202) % {'game': game_name}
p_dialog = xbmcgui.DialogProgress()
p_dialog.create(addon.getLocalizedString(30200), launch_label)
p_dialog.update(50)
# Wait for the dialog to pop up
xbmc.sleep(200)
# Run the command
exitcode = os.system(command)
# If the command was successful wait for steamlink to shut down kodi
if exitcode == 0:
xbmc.sleep(1000)
else:
# If steamlink did not start notify the user
xbmc.log('Launching steamlink failed: ' + command, xbmc.LOGERROR)
p_dialog.close()
dialog = xbmcgui.Dialog()
dialog.ok(addon.getLocalizedString(30200), addon.getLocalizedString(30203))
def update(addon):
if is_steamlink_installed():
install_label = addon.getLocalizedString(30102)
else:
install_label = addon.getLocalizedString(30101)
c_dialog = xbmcgui.Dialog()
confirm_update = c_dialog.yesno(addon.getLocalizedString(30100), install_label)
if confirm_update is False:
return
p_dialog = xbmcgui.DialogProgress()
p_dialog.create(addon.getLocalizedString(30103), addon.getLocalizedString(30104))
xbmc.log('Updating steamlink...', xbmc.LOGDEBUG)
# This is an estimate of how many lines of output there should be to guess the progress
line_max = 2210
line_nr = 1
line = ''
cmd = 'ADDON_PROFILE_PATH="{}" bash {}'.format(get_addon_data_path(), get_resource_path('build/build.sh'))
p = Popen(cmd, stdout=PIPE, stderr=STDOUT, shell=True)
for line in p.stdout:
percent = int(round(line_nr / line_max * 100))
p_dialog.update(percent)
line_nr += 1
p.wait()
# Log update
xbmc.log('Updating steamlink finished with {} lines of output and exit-code {}: {}'.format(
line_nr.__str__(), p.returncode.__str__(), line.decode()
), xbmc.LOGDEBUG)
# Make sure it ends at 100%
p_dialog.update(100)
# Close the progress bar
p_dialog.close()
if p.returncode == 0 and is_steamlink_installed():
finish_label = addon.getLocalizedString(30106)
else:
finish_label = addon.getLocalizedString(30105) % {'error_msg': line.decode()}
dialog = xbmcgui.Dialog()
dialog.ok(addon.getLocalizedString(30103), finish_label)
def speaker_test(addon, speakers):
dialog = xbmcgui.Dialog()
service, device_name = get_kodi_audio_device()
if service == 'ALSA':
p_dialog = xbmcgui.DialogProgress()
p_dialog.create('Speaker test', 'Initializing...')
# Make sure Kodi does not keep the device occupied
streamsilence_user_setting = get_kodi_setting('audiooutput.streamsilence')
set_kodi_setting('audiooutput.streamsilence', 0)
# Write new config file
speaker_setup_write_alsa_config(addon)
# Get Path for steamlink home
home_path = get_steamlink_home_path()
# Get device name foor surround sound
non_lfe_speakers = speakers - 1
device_name = 'surround{}1'.format(non_lfe_speakers)
for speaker in range(speakers):
# Display dialog text
speaker_channel = addon.getSettingInt('alsa_surround_{}1_{}'.format(non_lfe_speakers, speaker))
# Prepare dialog info
dialog_percent = int(round((speaker + 1) / speakers * 100))
dialog_text = 'Testing {} speaker on channel {}...' \
.format(addon.getLocalizedString(30030 + speaker), speaker_channel)
# Prepare command
cmd = 'HOME="{}" speaker-test --nloops 1 --device {} --channels {} --speaker {}' \
.format(home_path, device_name, speakers, speaker + 1)
# For same reason the device is not always available, try until the command succeeds
exit_code = 1
while exit_code != 0:
# Stop if user aborts test dialog
if p_dialog.iscanceled():
break
# Update dialog info
p_dialog.update(dialog_percent, dialog_text)
# Play test sound
xbmc.log(cmd, xbmc.LOGINFO)
exit_code = os.system(cmd)
# If the command failed, tell the user and wait for a short time before retrying
if exit_code != 0:
xbmc.log('Failed executing "{}"'.format(cmd), xbmc.LOGWARNING)
p_dialog.update(
dialog_percent,
'Waiting for {}.1 Surround audio device to become available...'.format(non_lfe_speakers)
)
xbmc.sleep(500)
# Stop if user aborts test dialog
if p_dialog.iscanceled():
break
# Restore user setting
set_kodi_setting('audiooutput.streamsilence', streamsilence_user_setting)
# Close the progress bar
p_dialog.close()
else:
dialog.ok('Speaker test', 'Audio service is {}, not ALSA.\n\nTest aborted.'.format(service))
addon.openSettings()
def speaker_setup_write_alsa_config(addon):
asoundrc_template_path = get_resource_path('template/asoundrc')
asoundrc_dir = "{}/.config/alsa".format(get_steamlink_home_path())
asoundrc_path = "{}/asoundrc".format(asoundrc_dir)
service, device_name = get_kodi_audio_device()
template = pathlib.Path(asoundrc_template_path).read_text()
# Only set default device if a non-default device is configured
if device_name == 'default':
template = template.replace('%default_device%', '')
else:
template = template.replace('%default_device%', 'pcm.!default "{}"'.format(device_name))
# Set the device
template = template.replace('%device%', device_name)
for speakers in [6, 8]:
for speaker in range(speakers):
# Get setting id and channel
setting_id = 'alsa_surround_{}1_{}'.format(speakers - 1, speaker)
template_var = '%{}%'.format(setting_id)
channel = addon.getSetting(setting_id)
# Replace template var
template = template.replace(template_var, channel)
# Ensure dir exists
if not os.path.exists(asoundrc_dir):
os.mkdir(asoundrc_dir)
# Write new config to asoundrc file
pathlib.Path(asoundrc_path).write_text(template)
xbmc.log('New ALSA config file written to {}'.format(asoundrc_path), xbmc.LOGINFO)
def get_resource_path(sub_path):
return translatePath(pathlib.Path(__file__).parent.absolute().__str__() + '/resources/' + sub_path)
def get_addon_data_path(sub_path=''):
return translatePath('special://profile/addon_data/plugin.program.steamlink' + sub_path)
def get_steamlink_home_path():
return "{}/steamlink-home".format(get_addon_data_path())
def is_steamlink_installed():
return os.path.isfile(get_addon_data_path('/steamlink/bin/shell'))
def get_kodi_setting(setting):
request = {
'jsonrpc': '2.0',
'method': 'Settings.GetSettingValue',
'params': {'setting': setting},
'id': 1
}
response = json.loads(xbmc.executeJSONRPC(json.dumps(request)))
return response['result']['value']
def set_kodi_setting(setting, value):
request = {
'jsonrpc': '2.0',
'method': 'Settings.SetSettingValue',
'params': {'setting': setting, 'value': value},
'id': 1
}
json.loads(xbmc.executeJSONRPC(json.dumps(request)))
def get_kodi_audio_device():
return get_kodi_setting('audiooutput.audiodevice').split(':', 1)