]> git.basschouten.com Git - openhab-addons.git/blob
94bf0d28e61829391c3e6dffa166de77274a3ef7
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.pulseaudio.internal;
14
15 import static org.openhab.binding.pulseaudio.internal.PulseaudioBindingConstants.*;
16
17 import java.io.IOException;
18 import java.io.InputStream;
19 import java.io.PrintStream;
20 import java.math.BigDecimal;
21 import java.net.NoRouteToHostException;
22 import java.net.Socket;
23 import java.net.SocketException;
24 import java.net.SocketTimeoutException;
25 import java.net.UnknownHostException;
26 import java.util.ArrayList;
27 import java.util.List;
28 import java.util.Optional;
29 import java.util.Random;
30
31 import org.eclipse.jdt.annotation.NonNull;
32 import org.eclipse.jdt.annotation.NonNullByDefault;
33 import org.eclipse.jdt.annotation.Nullable;
34 import org.openhab.binding.pulseaudio.internal.cli.Parser;
35 import org.openhab.binding.pulseaudio.internal.items.AbstractAudioDeviceConfig;
36 import org.openhab.binding.pulseaudio.internal.items.AbstractAudioDeviceConfig.State;
37 import org.openhab.binding.pulseaudio.internal.items.Module;
38 import org.openhab.binding.pulseaudio.internal.items.Sink;
39 import org.openhab.binding.pulseaudio.internal.items.SinkInput;
40 import org.openhab.binding.pulseaudio.internal.items.Source;
41 import org.openhab.binding.pulseaudio.internal.items.SourceOutput;
42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
44
45 /**
46  * The client connects to a pulseaudio server via TCP. It reads the current state of the
47  * pulseaudio server (available sinks, sources,...) and can send commands to the server.
48  * The syntax of the commands is the same as for the pactl command line tool provided by pulseaudio.
49  *
50  * On the pulseaudio server the module-cli-protocol-tcp has to be loaded.
51  *
52  * @author Tobias Bräutigam - Initial contribution
53  * @author Miguel Álvarez - changes for loading audio source module and nullability annotations
54  */
55 @NonNullByDefault
56 public class PulseaudioClient {
57
58     private final Logger logger = LoggerFactory.getLogger(PulseaudioClient.class);
59
60     private String host;
61     private int port;
62     private @Nullable Socket client;
63
64     private List<AbstractAudioDeviceConfig> items;
65     private List<Module> modules;
66
67     /**
68      * Corresponding to the global binding configuration
69      */
70     private PulseAudioBindingConfiguration configuration;
71
72     /**
73      * corresponding name to execute actions on sink items
74      */
75     private static final String ITEM_SINK = "sink";
76
77     /**
78      * corresponding name to execute actions on source items
79      */
80     private static final String ITEM_SOURCE = "source";
81
82     /**
83      * corresponding name to execute actions on sink-input items
84      */
85     private static final String ITEM_SINK_INPUT = "sink-input";
86
87     /**
88      * corresponding name to execute actions on source-output items
89      */
90     private static final String ITEM_SOURCE_OUTPUT = "source-output";
91
92     /**
93      * command to list the loaded modules
94      */
95     private static final String CMD_LIST_MODULES = "list-modules";
96
97     /**
98      * command to list the sinks
99      */
100     private static final String CMD_LIST_SINKS = "list-sinks";
101
102     /**
103      * command to list the sources
104      */
105     private static final String CMD_LIST_SOURCES = "list-sources";
106
107     /**
108      * command to list the sink-inputs
109      */
110     private static final String CMD_LIST_SINK_INPUTS = "list-sink-inputs";
111
112     /**
113      * command to list the source-outputs
114      */
115     private static final String CMD_LIST_SOURCE_OUTPUTS = "list-source-outputs";
116
117     /**
118      * command to load a module
119      */
120     private static final String CMD_LOAD_MODULE = "load-module";
121
122     /**
123      * command to unload a module
124      */
125     private static final String CMD_UNLOAD_MODULE = "unload-module";
126
127     /**
128      * name of the module-combine-sink
129      */
130     private static final String MODULE_COMBINE_SINK = "module-combine-sink";
131
132     public PulseaudioClient(String host, int port, PulseAudioBindingConfiguration configuration) throws IOException {
133         this.host = host;
134         this.port = port;
135         this.configuration = configuration;
136
137         items = new ArrayList<>();
138         modules = new ArrayList<>();
139
140         connect();
141         update();
142     }
143
144     public boolean isConnected() {
145         return client != null ? client.isConnected() : false;
146     }
147
148     /**
149      * updates the item states and their relationships
150      */
151     public synchronized void update() {
152         // one step copy
153         modules = new ArrayList<Module>(Parser.parseModules(listModules()));
154
155         List<AbstractAudioDeviceConfig> newItems = new ArrayList<>(); // prepare new list before assigning it
156         newItems.clear();
157         if (configuration.sink) {
158             logger.debug("reading sinks");
159             newItems.addAll(Parser.parseSinks(listSinks(), this));
160         }
161         if (configuration.source) {
162             logger.debug("reading sources");
163             newItems.addAll(Parser.parseSources(listSources(), this));
164         }
165         if (configuration.sinkInput) {
166             logger.debug("reading sink-inputs");
167             newItems.addAll(Parser.parseSinkInputs(listSinkInputs(), this));
168         }
169         if (configuration.sourceOutput) {
170             logger.debug("reading source-outputs");
171             newItems.addAll(Parser.parseSourceOutputs(listSourceOutputs(), this));
172         }
173         logger.debug("Pulseaudio server {}: {} modules and {} items updated", host, modules.size(), newItems.size());
174         items = newItems;
175     }
176
177     private String listModules() {
178         return this.sendRawRequest(CMD_LIST_MODULES);
179     }
180
181     private String listSinks() {
182         return this.sendRawRequest(CMD_LIST_SINKS);
183     }
184
185     private String listSources() {
186         return this.sendRawRequest(CMD_LIST_SOURCES);
187     }
188
189     private String listSinkInputs() {
190         return this.sendRawRequest(CMD_LIST_SINK_INPUTS);
191     }
192
193     private String listSourceOutputs() {
194         return this.sendRawRequest(CMD_LIST_SOURCE_OUTPUTS);
195     }
196
197     /**
198      * retrieves a module by its id
199      *
200      * @param id
201      * @return the corresponding {@link Module} to the given <code>id</code>
202      */
203     public @Nullable Module getModule(int id) {
204         for (Module module : modules) {
205             if (module.getId() == id) {
206                 return module;
207             }
208         }
209         return null;
210     }
211
212     /**
213      * send the command directly to the pulseaudio server
214      * for a list of available commands please take a look at
215      * http://www.freedesktop.org/wiki/Software/PulseAudio/Documentation/User/CLI
216      *
217      * @param command
218      */
219     public void sendCommand(String command) {
220         sendRawCommand(command);
221     }
222
223     /**
224      * retrieves a {@link Sink} by its name
225      *
226      * @return the corresponding {@link Sink} to the given <code>name</code>
227      */
228     public @Nullable Sink getSink(String name) {
229         for (AbstractAudioDeviceConfig item : items) {
230             if (item.getPaName().equalsIgnoreCase(name) && item instanceof Sink) {
231                 return (Sink) item;
232             }
233         }
234         return null;
235     }
236
237     /**
238      * retrieves a {@link Sink} by its id
239      *
240      * @return the corresponding {@link Sink} to the given <code>id</code>
241      */
242     public @Nullable Sink getSink(int id) {
243         for (AbstractAudioDeviceConfig item : items) {
244             if (item.getId() == id && item instanceof Sink) {
245                 return (Sink) item;
246             }
247         }
248         return null;
249     }
250
251     /**
252      * retrieves a {@link SinkInput} by its name
253      *
254      * @return the corresponding {@link SinkInput} to the given <code>name</code>
255      */
256     public @Nullable SinkInput getSinkInput(String name) {
257         for (AbstractAudioDeviceConfig item : items) {
258             if (item.getPaName().equalsIgnoreCase(name) && item instanceof SinkInput) {
259                 return (SinkInput) item;
260             }
261         }
262         return null;
263     }
264
265     /**
266      * retrieves a {@link SinkInput} by its id
267      *
268      * @return the corresponding {@link SinkInput} to the given <code>id</code>
269      */
270     public @Nullable SinkInput getSinkInput(int id) {
271         for (AbstractAudioDeviceConfig item : items) {
272             if (item.getId() == id && item instanceof SinkInput) {
273                 return (SinkInput) item;
274             }
275         }
276         return null;
277     }
278
279     /**
280      * retrieves a {@link Source} by its name
281      *
282      * @return the corresponding {@link Source} to the given <code>name</code>
283      */
284     public @Nullable Source getSource(String name) {
285         for (AbstractAudioDeviceConfig item : items) {
286             if (item.getPaName().equalsIgnoreCase(name) && item instanceof Source) {
287                 return (Source) item;
288             }
289         }
290         return null;
291     }
292
293     /**
294      * retrieves a {@link Source} by its id
295      *
296      * @return the corresponding {@link Source} to the given <code>id</code>
297      */
298     public @Nullable Source getSource(int id) {
299         for (AbstractAudioDeviceConfig item : items) {
300             if (item.getId() == id && item instanceof Source) {
301                 return (Source) item;
302             }
303         }
304         return null;
305     }
306
307     /**
308      * retrieves a {@link SourceOutput} by its name
309      *
310      * @return the corresponding {@link SourceOutput} to the given <code>name</code>
311      */
312     public @Nullable SourceOutput getSourceOutput(String name) {
313         for (AbstractAudioDeviceConfig item : items) {
314             if (item.getPaName().equalsIgnoreCase(name) && item instanceof SourceOutput) {
315                 return (SourceOutput) item;
316             }
317         }
318         return null;
319     }
320
321     /**
322      * retrieves a {@link SourceOutput} by its id
323      *
324      * @return the corresponding {@link SourceOutput} to the given <code>id</code>
325      */
326     public @Nullable SourceOutput getSourceOutput(int id) {
327         for (AbstractAudioDeviceConfig item : items) {
328             if (item.getId() == id && item instanceof SourceOutput) {
329                 return (SourceOutput) item;
330             }
331         }
332         return null;
333     }
334
335     /**
336      * retrieves a {@link AbstractAudioDeviceConfig} by its name
337      *
338      * @return the corresponding {@link AbstractAudioDeviceConfig} to the given <code>name</code>
339      */
340     public @Nullable AbstractAudioDeviceConfig getGenericAudioItem(String name) {
341         for (AbstractAudioDeviceConfig item : items) {
342             if (item.getPaName().equalsIgnoreCase(name)) {
343                 return item;
344             }
345         }
346         return null;
347     }
348
349     public List<AbstractAudioDeviceConfig> getItems() {
350         return items;
351     }
352
353     /**
354      * changes the <code>mute</code> state of the corresponding {@link Sink}
355      *
356      * @param item the {@link Sink} to handle
357      * @param mute mutes the sink if true, unmutes if false
358      */
359     public void setMute(@Nullable AbstractAudioDeviceConfig item, boolean mute) {
360         if (item == null) {
361             return;
362         }
363         String itemCommandName = getItemCommandName(item);
364         if (itemCommandName == null) {
365             return;
366         }
367         String muteString = mute ? "1" : "0";
368         sendRawCommand("set-" + itemCommandName + "-mute " + item.getId() + " " + muteString);
369         // update internal data
370         item.setMuted(mute);
371     }
372
373     /**
374      * change the volume of a {@link AbstractAudioDeviceConfig}
375      *
376      * @param item the {@link AbstractAudioDeviceConfig} to handle
377      * @param vol the new volume value the {@link AbstractAudioDeviceConfig} should be changed to (possible values from
378      *            0 - 65536)
379      */
380     public void setVolume(AbstractAudioDeviceConfig item, int vol) {
381         if (item == null) {
382             return;
383         }
384         String itemCommandName = getItemCommandName(item);
385         if (itemCommandName == null) {
386             return;
387         }
388         sendRawCommand("set-" + itemCommandName + "-volume " + item.getId() + " " + vol);
389         item.setVolume(Math.round(100f / 65536f * vol));
390     }
391
392     /**
393      * Locate or load (if needed) the simple protocol tcp module for the given sink
394      * and returns the port.
395      * The module loading (if needed) will be tried several times, on a new random port each time.
396      *
397      * @param item the sink we are searching for
398      * @param simpleTcpPortPref the port to use if we have to load the module
399      * @return the port on which the module is listening
400      * @throws InterruptedException
401      */
402     public Optional<Integer> loadModuleSimpleProtocolTcpIfNeeded(AbstractAudioDeviceConfig item,
403             Integer simpleTcpPortPref, @Nullable String format, @Nullable BigDecimal rate,
404             @Nullable BigDecimal channels) throws InterruptedException {
405         int currentTry = 0;
406         int simpleTcpPortToTry = simpleTcpPortPref;
407         String itemType = getItemCommandName(item);
408         do {
409             Optional<Integer> simplePort = findSimpleProtocolTcpModule(item, format, rate, channels);
410
411             if (simplePort.isPresent()) {
412                 return simplePort;
413             } else {
414                 String moduleOptions = itemType + "=" + item.getPaName() + " port=" + simpleTcpPortToTry;
415                 if (item instanceof Source && format != null && rate != null && channels != null) {
416                     moduleOptions = moduleOptions + String.format(" record=true format=%s rate=%d channels=%d", format,
417                             rate.longValue(), channels.intValue());
418                 }
419                 sendRawCommand("load-module module-simple-protocol-tcp " + moduleOptions);
420                 simpleTcpPortToTry = new Random().nextInt(64512) + 1024; // a random port above 1024
421             }
422             Thread.sleep(100);
423             update();
424             currentTry++;
425         } while (currentTry < 3);
426
427         logger.warn("The pulseaudio binding tried 3 times to load the module-simple-protocol-tcp"
428                 + " on random port on the pulseaudio server and give up trying");
429         return Optional.empty();
430     }
431
432     /**
433      * Find a simple protocol module corresponding to the given sink in argument
434      * and returns the port it listens to
435      *
436      * @param item
437      * @return
438      */
439     private Optional<Integer> findSimpleProtocolTcpModule(AbstractAudioDeviceConfig item, @Nullable String format,
440             @Nullable BigDecimal rate, @Nullable BigDecimal channels) {
441         String itemType = getItemCommandName(item);
442         if (itemType == null) {
443             return Optional.empty();
444         }
445         List<Module> modulesCopy = new ArrayList<Module>(modules);
446         var isSource = item instanceof Source;
447         return modulesCopy.stream() // iteration on modules
448                 .filter(module -> MODULE_SIMPLE_PROTOCOL_TCP_NAME.equals(module.getPaName())) // filter on module name
449                 .filter(module -> {
450                     boolean nameMatch = extractArgumentFromLine(itemType, module.getArgument()) // extract sick|source
451                             .map(name -> name.equals(item.getPaName())).orElse(false);
452                     if (isSource && nameMatch) {
453                         boolean recordStream = extractArgumentFromLine("record", module.getArgument())
454                                 .map("true"::equals).orElse(false);
455                         if (!recordStream) {
456                             return false;
457                         }
458                         if (format != null) {
459                             boolean rateMatch = extractArgumentFromLine("format", module.getArgument())
460                                     .map(format::equals).orElse(false);
461                             if (!rateMatch) {
462                                 return false;
463                             }
464                         }
465                         if (rate != null) {
466                             boolean rateMatch = extractArgumentFromLine("rate", module.getArgument())
467                                     .map(value -> Long.parseLong(value) == rate.longValue()).orElse(false);
468                             if (!rateMatch) {
469                                 return false;
470                             }
471                         }
472                         if (channels != null) {
473                             boolean channelsMatch = extractArgumentFromLine("channels", module.getArgument())
474                                     .map(value -> Integer.parseInt(value) == channels.intValue()).orElse(false);
475                             if (!channelsMatch) {
476                                 return false;
477                             }
478                         }
479                     }
480                     return nameMatch;
481                 }) // filter on sink name
482                 .findAny() // get a corresponding module
483                 .map(module -> extractArgumentFromLine("port", module.getArgument())
484                         .orElse(Integer.toString(MODULE_SIMPLE_PROTOCOL_TCP_DEFAULT_PORT))) // get port
485                 .map(portS -> Integer.parseInt(portS));
486     }
487
488     private @NonNull Optional<@NonNull String> extractArgumentFromLine(String argumentWanted, String argumentLine) {
489         String argument = null;
490         int startPortIndex = argumentLine.indexOf(argumentWanted + "=");
491         if (startPortIndex != -1) {
492             startPortIndex = startPortIndex + argumentWanted.length() + 1;
493             int endPortIndex = argumentLine.indexOf(" ", startPortIndex);
494             if (endPortIndex == -1) {
495                 endPortIndex = argumentLine.length();
496             }
497             argument = argumentLine.substring(startPortIndex, endPortIndex);
498         }
499         return Optional.ofNullable(argument);
500     }
501
502     /**
503      * returns the item names that can be used in commands
504      *
505      * @param item
506      * @return
507      */
508     private @Nullable String getItemCommandName(AbstractAudioDeviceConfig item) {
509         if (item instanceof Sink) {
510             return ITEM_SINK;
511         } else if (item instanceof Source) {
512             return ITEM_SOURCE;
513         } else if (item instanceof SinkInput) {
514             return ITEM_SINK_INPUT;
515         } else if (item instanceof SourceOutput) {
516             return ITEM_SOURCE_OUTPUT;
517         }
518         return null;
519     }
520
521     /**
522      * change the volume of a {@link AbstractAudioDeviceConfig}
523      *
524      * @param item the {@link AbstractAudioDeviceConfig} to handle
525      * @param vol the new volume percent value the {@link AbstractAudioDeviceConfig} should be changed to (possible
526      *            values from 0 - 100)
527      */
528     public void setVolumePercent(@Nullable AbstractAudioDeviceConfig item, int vol) {
529         int volumeToSet = vol;
530         if (item == null) {
531             return;
532         }
533         if (volumeToSet <= 100) {
534             volumeToSet = toAbsoluteVolume(volumeToSet);
535         }
536         setVolume(item, volumeToSet);
537     }
538
539     /**
540      * transform a percent volume to a value that can be send to the pulseaudio server (0-65536)
541      *
542      * @param percent
543      * @return
544      */
545     private int toAbsoluteVolume(int percent) {
546         return (int) Math.round(65536f / 100f * Double.valueOf(percent));
547     }
548
549     /**
550      * changes the combined sinks slaves to the given <code>sinks</code>
551      *
552      * @param combinedSink the combined sink which slaves should be changed
553      * @param sinks the list of new slaves
554      */
555     public void setCombinedSinkSlaves(@Nullable Sink combinedSink, List<Sink> sinks) {
556         if (combinedSink == null || !combinedSink.isCombinedSink()) {
557             return;
558         }
559         List<String> slaves = new ArrayList<>();
560         for (Sink sink : sinks) {
561             slaves.add(sink.getPaName());
562         }
563         // 1. delete old combined-sink
564         sendRawCommand(CMD_UNLOAD_MODULE + " " + combinedSink.getModule().getId());
565         // 2. add new combined-sink with same name and all slaves
566         sendRawCommand(CMD_LOAD_MODULE + " " + MODULE_COMBINE_SINK + " sink_name=" + combinedSink.getPaName()
567                 + " slaves=" + String.join(",", slaves));
568         // 3. update internal data structure because the combined sink has a new number + other slaves
569         update();
570     }
571
572     /**
573      * sets the sink a sink-input should be routed to
574      *
575      * @param sinkInput the sink-input to be rerouted
576      * @param sink the new sink the sink-input should be routed to
577      */
578     public void moveSinkInput(@Nullable SinkInput sinkInput, @Nullable Sink sink) {
579         if (sinkInput == null || sink == null) {
580             return;
581         }
582         sendRawCommand("move-sink-input " + sinkInput.getId() + " " + sink.getId());
583         sinkInput.setSink(sink);
584     }
585
586     /**
587      * sets the sink a source-output should be routed to
588      *
589      * @param sourceOutput the source-output to be rerouted
590      * @param source the new source the source-output should be routed to
591      */
592     public void moveSourceOutput(@Nullable SourceOutput sourceOutput, @Nullable Source source) {
593         if (sourceOutput == null || source == null) {
594             return;
595         }
596         sendRawCommand("move-sink-input " + sourceOutput.getId() + " " + source.getId());
597         sourceOutput.setSource(source);
598     }
599
600     /**
601      * suspend a source
602      *
603      * @param source the source which state should be changed
604      * @param suspend suspend it or not
605      */
606     public void suspendSource(@Nullable Source source, boolean suspend) {
607         if (source == null) {
608             return;
609         }
610         if (suspend) {
611             sendRawCommand("suspend-source " + source.getId() + " 1");
612             source.setState(State.SUSPENDED);
613         } else {
614             sendRawCommand("suspend-source " + source.getId() + " 0");
615             // unsuspending the source could result in different states (RUNNING,IDLE,...)
616             // update to get the new state
617             update();
618         }
619     }
620
621     /**
622      * suspend a sink
623      *
624      * @param sink the sink which state should be changed
625      * @param suspend suspend it or not
626      */
627     public void suspendSink(@Nullable Sink sink, boolean suspend) {
628         if (sink == null) {
629             return;
630         }
631         if (suspend) {
632             sendRawCommand("suspend-sink " + sink.getId() + " 1");
633             sink.setState(State.SUSPENDED);
634         } else {
635             sendRawCommand("suspend-sink " + sink.getId() + " 0");
636             // unsuspending the sink could result in different states (RUNNING,IDLE,...)
637             // update to get the new state
638             update();
639         }
640     }
641
642     /**
643      * changes the combined sinks slaves to the given <code>sinks</code>
644      *
645      * @param combinedSinkName the combined sink which slaves should be changed
646      * @param sinks the list of new slaves
647      */
648     public void setCombinedSinkSlaves(String combinedSinkName, List<Sink> sinks) {
649         if (getSink(combinedSinkName) != null) {
650             return;
651         }
652         List<String> slaves = new ArrayList<>();
653         for (Sink sink : sinks) {
654             slaves.add(sink.getPaName());
655         }
656         // add new combined-sink with same name and all slaves
657         sendRawCommand(CMD_LOAD_MODULE + " " + MODULE_COMBINE_SINK + " sink_name=" + combinedSinkName + " slaves="
658                 + String.join(",", slaves));
659         // update internal data structure because the combined sink is new
660         update();
661     }
662
663     private synchronized void sendRawCommand(String command) {
664         checkConnection();
665         if (client != null && client.isConnected()) {
666             try {
667                 PrintStream out = new PrintStream(client.getOutputStream(), true);
668                 logger.trace("sending command {} to pa-server {}", command, host);
669                 out.print(command + "\r\n");
670                 out.close();
671                 client.close();
672             } catch (IOException e) {
673                 logger.error("{}", e.getLocalizedMessage(), e);
674             }
675         }
676     }
677
678     private String sendRawRequest(String command) {
679         logger.trace("_sendRawRequest({})", command);
680         checkConnection();
681         String result = "";
682         if (client != null && client.isConnected()) {
683             try {
684                 PrintStream out = new PrintStream(client.getOutputStream(), true);
685                 out.print(command + "\r\n");
686
687                 InputStream instr = client.getInputStream();
688
689                 try {
690                     byte[] buff = new byte[1024];
691                     int retRead = 0;
692                     int lc = 0;
693                     do {
694                         retRead = instr.read(buff);
695                         lc++;
696                         if (retRead > 0) {
697                             String line = new String(buff, 0, retRead);
698                             // System.out.println("'"+line+"'");
699                             if (line.endsWith(">>> ") && lc > 1) {
700                                 result += line.substring(0, line.length() - 4);
701                                 break;
702                             }
703                             result += line.trim();
704                         }
705                     } while (retRead > 0);
706                 } catch (SocketTimeoutException e) {
707                     // Timeout -> as newer PA versions (>=5.0) do not send the >>> we have no chance
708                     // to detect the end of the answer, except by this timeout
709                 } catch (SocketException e) {
710                     logger.warn("Socket exception while sending pulseaudio command: {}", e.getMessage());
711                 } catch (IOException e) {
712                     logger.error("Exception while reading socket: {}", e.getMessage());
713                 }
714                 instr.close();
715                 out.close();
716                 client.close();
717                 return result;
718             } catch (IOException e) {
719                 logger.error("{}", e.getLocalizedMessage(), e);
720             }
721         }
722         return result;
723     }
724
725     private void checkConnection() {
726         if (client == null || client.isClosed() || !client.isConnected()) {
727             try {
728                 connect();
729             } catch (IOException e) {
730                 logger.error("{}", e.getLocalizedMessage(), e);
731             }
732         }
733     }
734
735     /**
736      * Connects to the pulseaudio server (timeout 500ms)
737      */
738     private void connect() throws IOException {
739         try {
740             client = new Socket(host, port);
741             client.setSoTimeout(500);
742         } catch (UnknownHostException e) {
743             logger.error("unknown socket host {}", host);
744         } catch (NoRouteToHostException e) {
745             logger.error("no route to host {}", host);
746         } catch (SocketException e) {
747             logger.error("cannot connect to host {} : {}", host, e.getMessage());
748         }
749     }
750
751     /**
752      * Disconnects from the pulseaudio server
753      */
754     public void disconnect() {
755         if (client != null) {
756             try {
757                 client.close();
758             } catch (IOException e) {
759                 logger.error("{}", e.getLocalizedMessage(), e);
760             }
761         }
762     }
763 }