forked from veldenb/plugin.program.moonlight-qt
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmoonlight.py
379 lines (284 loc) · 12.7 KB
/
moonlight.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
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
import getpass
import json
import os
import pathlib
import platform
import re
from subprocess import PIPE
from subprocess import Popen
from subprocess import STDOUT
from subprocess import check_output
import xbmc
import xbmcgui
from xbmcvfs import translatePath
def launch(addon, hostname=None, game_name=None):
# Check if moonlight is installed and offer to install
if is_moonlight_installed() is False:
update(addon)
# If moonlight is still not installed abort
if is_moonlight_installed() is False:
dialog = xbmcgui.Dialog()
dialog.ok(addon.getLocalizedString(30200), addon.getLocalizedString(30201))
return
# Initialise argument vars
systemd_args = []
moonlight_command = f'bash {get_resource_path("bin/launch_moonlight-qt.sh")}'
moonlight_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)
moonlight_command = f'sudo -u {getpass.getuser()} {moonlight_command}'
# Check for a forced EGL display mode
display_setup_write_egl_config(addon)
# 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 Moonlight 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 Moonlight might not work: {err}', xbmc.LOGWARNING
)
# Create command to launch moonlight
launch_command = 'systemd-run {} {}'.format(' '.join(systemd_args), moonlight_command)
# Check if at least a host is set
if hostname and game_name:
moonlight_args.extend(['stream', f'"{hostname}"', f'"{game_name}"'])
# Prepare the command
command = f'{launch_command} ' + ' '.join(moonlight_args)
# Log the command so debugging problems is easier
xbmc.log(f'Launching moonlight-qt: {command}', xbmc.LOGINFO)
# Show a dialog
if not game_name:
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 moonlight to shut down kodi
if exitcode == 0:
xbmc.sleep(1000)
else:
# If moonlight did not start notify the user
xbmc.log('Launching moonlight-qt failed: ' + command, xbmc.LOGERROR)
p_dialog.close()
dialog = xbmcgui.Dialog()
dialog.ok(addon.getLocalizedString(30200), addon.getLocalizedString(30203))
def update(addon):
if is_moonlight_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 moonlight-qt...', 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 {} | tee {}'.format(
get_addon_data_path(),
get_resource_path('build/build.sh'),
get_addon_data_path('/build.log')
)
xbmc.log(f'Launching moonlight-qt update: {cmd}', xbmc.LOGINFO)
# Ensure path for logging exists
if not os.path.exists(get_addon_data_path()):
os.mkdir(get_addon_data_path())
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 moonlight-qt 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_moonlight_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 moonlight home
home_path = get_moonlight_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()
# See for details: https://doc.qt.io/qt-5/embedded-linux.html
def display_setup_write_egl_config(addon):
# Write new config to QT_QPA_EGLFS_KMS_CONFIG file
kms_config_path = get_addon_data_path('/qt_qpa_eglfs_kms_config.json')
force_card = addon.getSetting('display_egl_card') or None
force_output = addon.getSetting('display_egl_output') or None
force_mode = addon.getSetting('display_egl_resolution') or None
if force_card == '-':
force_card = None
if force_output == '-':
force_output = None
if not force_output or force_mode == '-':
force_mode = None
# Hack for Raspberry Pi 5 and some ARM-devices where card0 or card1 needs to be forced
force_device = '/dev/dri/{}'.format(force_card) if force_card else None
if force_device or force_output:
kms_config = {}
kms_output = {}
if force_device:
kms_config["device"] = force_device
if force_output:
kms_output["name"] = force_output
kms_output["primary"] = True
if force_mode:
kms_output["mode"] = force_mode
if kms_output:
kms_config["outputs"] = [kms_output]
with open(kms_config_path, 'w') as f:
json.dump(kms_config, f, ensure_ascii=False, indent=2)
xbmc.log('Forcing EGL-mode to device: {}, output: {}, mode: {}'
.format(force_device or 'default', force_output or 'default', force_mode or 'default')
, xbmc.LOGINFO)
elif os.path.exists(kms_config_path):
os.remove(kms_config_path)
def speaker_setup_write_alsa_config(addon):
asoundrc_template_path = get_resource_path('template/asoundrc')
asoundrc_dir = "{}/.config/alsa".format(get_moonlight_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.moonlight-qt' + sub_path)
def get_moonlight_home_path():
return "{}/moonlight-home".format(get_addon_data_path())
def is_moonlight_installed():
return os.path.isfile(get_addon_data_path('/moonlight-qt/bin/moonlight-qt'))
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():
audio_device = get_kodi_setting('audiooutput.audiodevice').split(':', 1)
if audio_device[1]:
# Remove breaking part of the device name (Kodi 21)
audio_device[1] = re.sub('\\|.*', '', audio_device[1])
if audio_device[1][0] == '@':
# Replace @ with default device
audio_device[1] = audio_device[1].replace('@:', 'sysdefault:').replace(',DEV=0', '')
return audio_device