88import os
99import io
1010import sys
11+ import threading
12+ import json
1113
1214
1315def write_sp_config_file (filename , config ):
@@ -43,7 +45,29 @@ def __init__(self, name: str, ap2: bool, disabled: bool = False, mock: bool = Fa
4345 self ._connect_time = 0.0
4446 self ._coverart_dir = ''
4547 self ._log_file : Optional [io .TextIOBase ] = None
46- self .volume_process : Optional [subprocess .Popen ] = None
48+ self .src_config_folder : Optional [str ] = None
49+ self .volume_watcher_process : Optional [threading .Thread ] = None # Populates the fifo that the vol sync script depends on
50+ self .volume_sync_process : Optional [subprocess .Popen ] = None
51+ self ._volume_fifo : Optional [str ] = None
52+
53+ def watch_vol (self ):
54+ """Creates and supplies a FIFO with volume data for volume sync"""
55+ while True :
56+ try :
57+ if self .src is not None :
58+ if self ._volume_fifo is None and self .src_config_folder is not None :
59+ fifo_path = f"{ self .src_config_folder } /vol"
60+ if not os .path .isfile (fifo_path ):
61+ os .mkfifo (fifo_path )
62+ self ._volume_fifo = os .open (fifo_path , os .O_WRONLY , os .O_NONBLOCK )
63+ data = json .dumps ({
64+ 'zones' : self .connected_zones ,
65+ 'volume' : self .volume ,
66+ })
67+ os .write (self ._volume_fifo , bytearray (f"{ data } \r \n " , encoding = "utf8" ))
68+ except Exception as e :
69+ logger .error (f"{ self .name } volume thread ran into exception: { e } " )
70+ time .sleep (0.1 )
4771
4872 def reconfig (self , ** kwargs ):
4973 self .validate_stream (** kwargs )
@@ -73,9 +97,9 @@ def _activate(self, vsrc: int):
7397 logger .info (f'Another Airplay 2 stream is already in use, unable to start { self .name } , mocking connection' )
7498 return
7599
76- src_config_folder = f'{ utils .get_folder ("config" )} /srcs/v{ vsrc } '
100+ self . src_config_folder = f'{ utils .get_folder ("config" )} /srcs/v{ vsrc } '
77101 try :
78- os .remove (f'{ src_config_folder } /currentSong' )
102+ os .remove (f'{ self . src_config_folder } /currentSong' )
79103 except FileNotFoundError :
80104 pass
81105 self ._connect_time = time .time ()
@@ -101,7 +125,8 @@ def _activate(self, vsrc: int):
101125 'alsa' : {
102126 'output_device' : utils .virtual_output_device (vsrc ), # alsa output device
103127 # If set too small, buffer underflow occurs on low-powered machines. Too long and the response times with software mixer become annoying.
104- 'audio_backend_buffer_desired_length' : 11025
128+ 'audio_backend_buffer_desired_length' : 11025 ,
129+ 'software_mixer' : 'yes'
105130 },
106131 }
107132
@@ -111,10 +136,10 @@ def _activate(self, vsrc: int):
111136 except FileNotFoundError :
112137 pass
113138 os .makedirs (self ._coverart_dir , exist_ok = True )
114- os .makedirs (src_config_folder , exist_ok = True )
115- config_file = f'{ src_config_folder } /shairport.conf'
139+ os .makedirs (self . src_config_folder , exist_ok = True )
140+ config_file = f'{ self . src_config_folder } /shairport.conf'
116141 write_sp_config_file (config_file , config )
117- self ._log_file = open (f'{ src_config_folder } /log' , mode = 'w' )
142+ self ._log_file = open (f'{ self . src_config_folder } /log' , mode = 'w' )
118143 shairport_args = f"{ utils .get_folder ('streams' )} /shairport-sync{ '-ap2' if self .ap2 else '' } -c { config_file } " .split (' ' )
119144 logger .info (f'shairport_args: { shairport_args } ' )
120145
@@ -127,12 +152,15 @@ def _activate(self, vsrc: int):
127152 # shairport sync only adds the pid to the mpris name if it cannot use the default name
128153 if len (os .popen ("pgrep shairport-sync" ).read ().strip ().splitlines ()) > 1 :
129154 mpris_name += f".i{ self .proc .pid } "
130- self .mpris = MPRIS (mpris_name , f'{ src_config_folder } /metadata.txt' )
155+ self .mpris = MPRIS (mpris_name , f'{ self . src_config_folder } /metadata.txt' )
131156
132157 vol_sync = f"{ utils .get_folder ('streams' )} /shairport_volume_handler.py"
133- vol_args = [sys .executable , vol_sync , mpris_name , str (self .id )]
158+ vol_args = [sys .executable , vol_sync , mpris_name , f"{ utils .get_folder ('config' )} /srcs/v{ self .vsrc } " ]
159+
134160 logger .info (f'{ self .name } : starting vol synchronizer: { vol_args } ' )
135- self .volume_process = subprocess .Popen (args = vol_args , stdout = self ._log_file , stderr = self ._log_file )
161+ self .volume_watcher_process = threading .Thread (target = self .watch_vol , daemon = True )
162+ self .volume_watcher_process .start ()
163+ self .volume_sync_process = subprocess .Popen (args = vol_args , stdout = self ._log_file , stderr = self ._log_file )
136164 except Exception as exc :
137165 logger .exception (f'Error starting airplay MPRIS reader: { exc } ' )
138166
@@ -142,18 +170,22 @@ def _deactivate(self):
142170 self .mpris = None
143171 if self ._is_running ():
144172 self .proc .stdin .close ()
173+
145174 logger .info ('stopping shairport-sync' )
146175 self .proc .terminate ()
176+ if self .volume_sync_process is not None :
177+ self .volume_sync_process .terminate ()
178+
147179 if self .proc .wait (1 ) != 0 :
148180 logger .info ('killing shairport-sync' )
149181 self .proc .kill ()
150182 self .proc .communicate ()
151183
152- if self .volume_process is not None :
153- self .volume_process . terminate ()
154- if self . volume_process . wait ( 1 ) != 0 :
155- logger . info ( 'killing shairport vol sync' )
156- self . volume_process . kill ()
184+ if self .volume_sync_process is not None :
185+ if self .volume_sync_process . wait ( 1 ) != 0 :
186+ logger . info ( 'killing shairport vol sync' )
187+ self . volume_sync_process . kill ( )
188+
157189 if '_log_file' in self .__dir__ () and self ._log_file :
158190 self ._log_file .close ()
159191 if self .src :
@@ -162,8 +194,11 @@ def _deactivate(self):
162194 except Exception as e :
163195 logger .exception (f'Error removing airplay config files: { e } ' )
164196 self ._disconnect ()
197+
165198 self .proc = None
166- self .volume_process = None
199+ self .volume_sync_process = None
200+ self .volume_watcher_process = None
201+ self ._volume_fifo = None
167202
168203 def info (self ) -> models .SourceInfo :
169204 source = models .SourceInfo (
0 commit comments