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