2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.pulseaudio.internal;
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;
22 import java.util.concurrent.ScheduledExecutorService;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
26 import javax.sound.sampled.UnsupportedAudioFileException;
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;
42 * The audio sink for openhab, implemented by a connection to a pulseaudio sink
44 * @author Gwendal Roulleau - Initial contribution
48 public class PulseAudioAudioSink implements AudioSink {
50 private final Logger logger = LoggerFactory.getLogger(PulseAudioAudioSink.class);
52 private static final HashSet<AudioFormat> SUPPORTED_FORMATS = new HashSet<>();
53 private static final HashSet<Class<? extends AudioStream>> SUPPORTED_STREAMS = new HashSet<>();
55 private PulseaudioHandler pulseaudioHandler;
56 private ScheduledExecutorService scheduler;
58 private @Nullable Socket clientSocket;
60 private boolean isIdle = true;
62 private @Nullable ScheduledFuture<?> scheduledDisconnection;
65 SUPPORTED_FORMATS.add(AudioFormat.WAV);
66 SUPPORTED_FORMATS.add(AudioFormat.MP3);
67 SUPPORTED_STREAMS.add(FixedLengthAudioStream.class);
70 public PulseAudioAudioSink(PulseaudioHandler pulseaudioHandler, ScheduledExecutorService scheduler) {
71 this.pulseaudioHandler = pulseaudioHandler;
72 this.scheduler = scheduler;
76 public String getId() {
77 return pulseaudioHandler.getThing().getUID().toString();
81 public @Nullable String getLabel(@Nullable Locale locale) {
82 return pulseaudioHandler.getThing().getLabel();
86 * Connect to pulseaudio with the simple protocol
89 * @throws InterruptedException when interrupted during the loading module wait
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);
102 * Disconnect the socket to pulseaudio simple protocol
104 public void disconnect() {
105 final Socket clientSocketLocal = clientSocket;
106 if (clientSocketLocal != null && isIdle) {
107 logger.debug("Disconnecting");
109 clientSocketLocal.close();
110 } catch (IOException e) {
113 logger.debug("Stream still running or socket not open");
118 public void process(@Nullable AudioStream audioStream)
119 throws UnsupportedAudioFormatException, UnsupportedAudioStreamException {
121 if (audioStream == null) {
125 try (ConvertedInputStream normalizedPCMStream = new ConvertedInputStream(audioStream)) {
126 for (int countAttempt = 1; countAttempt <= 2; countAttempt++) { // two attempts allowed
129 final Socket clientSocketLocal = clientSocket;
130 if (clientSocketLocal != null) {
131 // send raw audio to the socket and to pulse audio
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);
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";
153 "Error while trying to send audio to pulseaudio audio sink. Cannot connect to {}:{}, error: {}",
154 pulseaudioHandler.getHost(), port, e.getMessage());
158 } catch (InterruptedException ie) {
159 logger.info("Interrupted during sink audio connection: {}", ie.getMessage());
163 } catch (UnsupportedAudioFileException | IOException e) {
164 throw new UnsupportedAudioFormatException("Cannot send sound to the pulseaudio sink",
165 audioStream.getFormat(), e);
167 scheduleDisconnect();
172 public void scheduleDisconnect() {
173 if (scheduledDisconnection != null) {
174 scheduledDisconnection.cancel(true);
176 int idleTimeout = pulseaudioHandler.getIdleTimeout();
177 if (idleTimeout > -1) {
178 logger.debug("Scheduling disconnect");
179 scheduledDisconnection = scheduler.schedule(this::disconnect, idleTimeout, TimeUnit.MILLISECONDS);
184 public Set<AudioFormat> getSupportedFormats() {
185 return SUPPORTED_FORMATS;
189 public Set<Class<? extends AudioStream>> getSupportedStreams() {
190 return SUPPORTED_STREAMS;
194 public PercentType getVolume() {
195 return new PercentType(pulseaudioHandler.getLastVolume());
199 public void setVolume(PercentType volume) {
200 pulseaudioHandler.setVolume(volume.intValue());