]> git.basschouten.com Git - openhab-addons.git/blob
dee60aaf9a134d191e98d6bf459035a75a35206f
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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 java.io.IOException;
16 import java.net.Socket;
17 import java.time.Duration;
18 import java.time.Instant;
19 import java.util.HashSet;
20 import java.util.Locale;
21 import java.util.Set;
22 import java.util.concurrent.ScheduledExecutorService;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
25
26 import javax.sound.sampled.UnsupportedAudioFileException;
27
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.openhab.binding.pulseaudio.internal.handler.PulseaudioHandler;
31 import org.openhab.core.audio.AudioFormat;
32 import org.openhab.core.audio.AudioSink;
33 import org.openhab.core.audio.AudioStream;
34 import org.openhab.core.audio.FixedLengthAudioStream;
35 import org.openhab.core.audio.UnsupportedAudioFormatException;
36 import org.openhab.core.audio.UnsupportedAudioStreamException;
37 import org.openhab.core.library.types.PercentType;
38 import org.slf4j.Logger;
39 import org.slf4j.LoggerFactory;
40
41 /**
42  * The audio sink for openhab, implemented by a connection to a pulseaudio sink
43  *
44  * @author Gwendal Roulleau - Initial contribution
45  *
46  */
47 @NonNullByDefault
48 public class PulseAudioAudioSink implements AudioSink {
49
50     private final Logger logger = LoggerFactory.getLogger(PulseAudioAudioSink.class);
51
52     private static final HashSet<AudioFormat> SUPPORTED_FORMATS = new HashSet<>();
53     private static final HashSet<Class<? extends AudioStream>> SUPPORTED_STREAMS = new HashSet<>();
54
55     private PulseaudioHandler pulseaudioHandler;
56     private ScheduledExecutorService scheduler;
57
58     private @Nullable Socket clientSocket;
59
60     private boolean isIdle = true;
61
62     private @Nullable ScheduledFuture<?> scheduledDisconnection;
63
64     static {
65         SUPPORTED_FORMATS.add(AudioFormat.WAV);
66         SUPPORTED_FORMATS.add(AudioFormat.MP3);
67         SUPPORTED_STREAMS.add(FixedLengthAudioStream.class);
68     }
69
70     public PulseAudioAudioSink(PulseaudioHandler pulseaudioHandler, ScheduledExecutorService scheduler) {
71         this.pulseaudioHandler = pulseaudioHandler;
72         this.scheduler = scheduler;
73     }
74
75     @Override
76     public String getId() {
77         return pulseaudioHandler.getThing().getUID().toString();
78     }
79
80     @Override
81     public @Nullable String getLabel(@Nullable Locale locale) {
82         return pulseaudioHandler.getThing().getLabel();
83     }
84
85     /**
86      * Connect to pulseaudio with the simple protocol
87      *
88      * @throws IOException
89      * @throws InterruptedException when interrupted during the loading module wait
90      */
91     public void connectIfNeeded() throws IOException, InterruptedException {
92         Socket clientSocketLocal = clientSocket;
93         if (clientSocketLocal == null || !clientSocketLocal.isConnected() || clientSocketLocal.isClosed()) {
94             String host = pulseaudioHandler.getHost();
95             int port = pulseaudioHandler.getSimpleTcpPort();
96             clientSocket = new Socket(host, port);
97             clientSocket.setSoTimeout(500);
98         }
99     }
100
101     /**
102      * Disconnect the socket to pulseaudio simple protocol
103      */
104     public void disconnect() {
105         final Socket clientSocketLocal = clientSocket;
106         if (clientSocketLocal != null && isIdle) {
107             logger.debug("Disconnecting");
108             try {
109                 clientSocketLocal.close();
110             } catch (IOException e) {
111             }
112         } else {
113             logger.debug("Stream still running or socket not open");
114         }
115     }
116
117     @Override
118     public void process(@Nullable AudioStream audioStream)
119             throws UnsupportedAudioFormatException, UnsupportedAudioStreamException {
120
121         if (audioStream == null) {
122             return;
123         }
124
125         try (ConvertedInputStream normalizedPCMStream = new ConvertedInputStream(audioStream)) {
126             for (int countAttempt = 1; countAttempt <= 2; countAttempt++) { // two attempts allowed
127                 try {
128                     connectIfNeeded();
129                     final Socket clientSocketLocal = clientSocket;
130                     if (clientSocketLocal != null) {
131                         // send raw audio to the socket and to pulse audio
132                         isIdle = false;
133                         Instant start = Instant.now();
134                         normalizedPCMStream.transferTo(clientSocketLocal.getOutputStream());
135                         if (normalizedPCMStream.getDuration() != -1) { // ensure, if the sound has a duration
136                             // that we let at least this time for the system to play
137                             Instant end = Instant.now();
138                             long millisSecondTimedToSendAudioData = Duration.between(start, end).toMillis();
139                             if (millisSecondTimedToSendAudioData < normalizedPCMStream.getDuration()) {
140                                 long timeToSleep = normalizedPCMStream.getDuration() - millisSecondTimedToSendAudioData;
141                                 logger.debug("Sleep time to let the system play sound : {}", timeToSleep);
142                                 Thread.sleep(timeToSleep);
143                             }
144                         }
145                         break;
146                     }
147                 } catch (IOException e) {
148                     disconnect(); // disconnect force to clear connection in case of socket not cleanly shutdown
149                     if (countAttempt == 2) { // we won't retry : log and quit
150                         if (logger.isWarnEnabled()) {
151                             String port = clientSocket != null ? Integer.toString(clientSocket.getPort()) : "unknown";
152                             logger.warn(
153                                     "Error while trying to send audio to pulseaudio audio sink. Cannot connect to {}:{}, error: {}",
154                                     pulseaudioHandler.getHost(), port, e.getMessage());
155                         }
156                         break;
157                     }
158                 } catch (InterruptedException ie) {
159                     logger.info("Interrupted during sink audio connection: {}", ie.getMessage());
160                     break;
161                 }
162             }
163         } catch (UnsupportedAudioFileException | IOException e) {
164             throw new UnsupportedAudioFormatException("Cannot send sound to the pulseaudio sink",
165                     audioStream.getFormat(), e);
166         } finally {
167             scheduleDisconnect();
168         }
169         isIdle = true;
170     }
171
172     public void scheduleDisconnect() {
173         if (scheduledDisconnection != null) {
174             scheduledDisconnection.cancel(true);
175         }
176         int idleTimeout = pulseaudioHandler.getIdleTimeout();
177         if (idleTimeout > -1) {
178             logger.debug("Scheduling disconnect");
179             scheduledDisconnection = scheduler.schedule(this::disconnect, idleTimeout, TimeUnit.MILLISECONDS);
180         }
181     }
182
183     @Override
184     public Set<AudioFormat> getSupportedFormats() {
185         return SUPPORTED_FORMATS;
186     }
187
188     @Override
189     public Set<Class<? extends AudioStream>> getSupportedStreams() {
190         return SUPPORTED_STREAMS;
191     }
192
193     @Override
194     public PercentType getVolume() {
195         return new PercentType(pulseaudioHandler.getLastVolume());
196     }
197
198     @Override
199     public void setVolume(PercentType volume) {
200         pulseaudioHandler.setVolume(volume.intValue());
201     }
202 }