]> git.basschouten.com Git - openhab-addons.git/blob
b9bd298caaf8c0dcaa0be88a805f1957d223d222
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.Set;
20 import java.util.concurrent.CompletableFuture;
21 import java.util.concurrent.ScheduledExecutorService;
22 import java.util.concurrent.TimeUnit;
23
24 import javax.sound.sampled.UnsupportedAudioFileException;
25
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.pulseaudio.internal.handler.PulseaudioHandler;
29 import org.openhab.core.audio.AudioFormat;
30 import org.openhab.core.audio.AudioSink;
31 import org.openhab.core.audio.AudioStream;
32 import org.openhab.core.audio.FileAudioStream;
33 import org.openhab.core.audio.UnsupportedAudioFormatException;
34 import org.openhab.core.audio.UnsupportedAudioStreamException;
35 import org.openhab.core.audio.utils.AudioSinkUtils;
36 import org.openhab.core.common.Disposable;
37 import org.slf4j.Logger;
38 import org.slf4j.LoggerFactory;
39
40 /**
41  * The audio sink for openhab, implemented by a connection to a pulseaudio sink
42  *
43  * @author Gwendal Roulleau - Initial contribution
44  * @author Miguel Álvarez - move some code to the PulseaudioSimpleProtocolStream class so sink and source can extend
45  *         from it.
46  *
47  */
48 @NonNullByDefault
49 public class PulseAudioAudioSink extends PulseaudioSimpleProtocolStream implements AudioSink {
50
51     private final Logger logger = LoggerFactory.getLogger(PulseAudioAudioSink.class);
52
53     private AudioSinkUtils audioSinkUtils;
54
55     private static final Set<AudioFormat> SUPPORTED_FORMATS = Set.of(AudioFormat.WAV, AudioFormat.MP3);
56     private static final Set<Class<? extends AudioStream>> SUPPORTED_STREAMS = Set.of(AudioStream.class);
57     private static final AudioFormat TARGET_FORMAT = new AudioFormat(AudioFormat.CONTAINER_WAVE,
58             AudioFormat.CODEC_PCM_SIGNED, false, 16, 4 * 44100, 44100L, 2);
59
60     public PulseAudioAudioSink(PulseaudioHandler pulseaudioHandler, ScheduledExecutorService scheduler,
61             AudioSinkUtils audioSinkUtils) {
62         super(pulseaudioHandler, scheduler);
63         this.audioSinkUtils = audioSinkUtils;
64     }
65
66     @Override
67     public void process(@Nullable AudioStream audioStream)
68             throws UnsupportedAudioFormatException, UnsupportedAudioStreamException {
69         processAndComplete(audioStream);
70     }
71
72     @Override
73     public CompletableFuture<@Nullable Void> processAndComplete(@Nullable AudioStream audioStream) {
74         if (audioStream == null) {
75             return CompletableFuture.completedFuture(null);
76         }
77         addClientCount();
78         try (ConvertedInputStream normalizedPCMStream = new ConvertedInputStream(audioStream)) {
79             for (int countAttempt = 1; countAttempt <= 2; countAttempt++) { // two attempts allowed
80                 try {
81                     connectIfNeeded();
82                     final Socket clientSocketLocal = clientSocket;
83                     if (clientSocketLocal != null) {
84                         // send raw audio to the socket and to pulse audio
85                         Instant start = Instant.now();
86                         if (normalizedPCMStream.getDuration() != -1) {
87                             // ensure, if the sound has a duration
88                             // that we let at least this time for the system to play
89                             normalizedPCMStream.transferTo(clientSocketLocal.getOutputStream());
90                             Instant end = Instant.now();
91                             long millisSecondTimedToSendAudioData = Duration.between(start, end).toMillis();
92                             if (millisSecondTimedToSendAudioData < normalizedPCMStream.getDuration()) {
93                                 CompletableFuture<@Nullable Void> soundPlayed = new CompletableFuture<>();
94                                 long timeToWait = normalizedPCMStream.getDuration() - millisSecondTimedToSendAudioData;
95                                 logger.debug("Some time to let the system play sound : {}", timeToWait);
96                                 scheduler.schedule(() -> soundPlayed.complete(null), timeToWait, TimeUnit.MILLISECONDS);
97                                 return soundPlayed;
98                             } else {
99                                 return CompletableFuture.completedFuture(null);
100                             }
101                         } else {
102                             // We have a second method available to guess the duration, and it is during transfer
103                             Long timeStampEnd = audioSinkUtils.transferAndAnalyzeLength(normalizedPCMStream,
104                                     clientSocketLocal.getOutputStream(), TARGET_FORMAT);
105                             CompletableFuture<@Nullable Void> soundPlayed = new CompletableFuture<>();
106                             if (timeStampEnd != null) {
107                                 long now = System.nanoTime();
108                                 long timeToWait = timeStampEnd - now;
109                                 if (timeToWait > 0) {
110                                     scheduler.schedule(() -> soundPlayed.complete(null), timeToWait,
111                                             TimeUnit.NANOSECONDS);
112                                 }
113                                 return soundPlayed;
114                             } else {
115                                 return CompletableFuture.completedFuture(null);
116                             }
117                         }
118                     }
119                 } catch (IOException e) {
120                     disconnect(); // disconnect force to clear connection in case of socket not cleanly shutdown
121                     if (countAttempt == 2) { // we won't retry : log and quit
122                         final Socket clientSocketLocal = clientSocket;
123                         String port = clientSocketLocal != null ? Integer.toString(clientSocketLocal.getPort())
124                                 : "unknown";
125                         logger.warn(
126                                 "Error while trying to send audio to pulseaudio audio sink. Cannot connect to {}:{}, error: {}",
127                                 pulseaudioHandler.getHost(), port, e.getMessage());
128                         return CompletableFuture.completedFuture(null);
129                     }
130                 } catch (InterruptedException ie) {
131                     logger.info("Interrupted during sink audio connection: {}", ie.getMessage());
132                     return CompletableFuture.completedFuture(null);
133                 }
134             }
135         } catch (UnsupportedAudioFileException | UnsupportedAudioFormatException | IOException e) {
136             return CompletableFuture.failedFuture(new UnsupportedAudioFormatException(
137                     "Cannot send sound to the pulseaudio sink", audioStream.getFormat(), e));
138         } finally {
139             minusClientCount();
140             // if the stream is not needed anymore, then we should call back the AudioStream to let it a chance
141             // to auto dispose.
142             if (audioStream instanceof Disposable disposableAudioStream) {
143                 try {
144                     disposableAudioStream.dispose();
145                 } catch (IOException e) {
146                     String fileName = audioStream instanceof FileAudioStream file ? file.toString() : "unknown";
147                     if (logger.isDebugEnabled()) {
148                         logger.debug("Cannot dispose of stream {}", fileName, e);
149                     } else {
150                         logger.warn("Cannot dispose of stream {}, reason {}", fileName, e.getMessage());
151                     }
152                 }
153             }
154         }
155         return CompletableFuture.completedFuture(null);
156     }
157
158     @Override
159     public Set<AudioFormat> getSupportedFormats() {
160         return SUPPORTED_FORMATS;
161     }
162
163     @Override
164     public Set<Class<? extends AudioStream>> getSupportedStreams() {
165         return SUPPORTED_STREAMS;
166     }
167 }