*/
package org.openhab.binding.kodi.internal;
-import java.util.Collections;
+import java.io.IOException;
+import java.io.InputStream;
import java.util.Locale;
import java.util.Set;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
+import java.util.concurrent.CompletableFuture;
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.kodi.internal.handler.KodiHandler;
import org.openhab.core.audio.AudioFormat;
import org.openhab.core.audio.AudioHTTPServer;
import org.openhab.core.audio.AudioSink;
+import org.openhab.core.audio.AudioSinkSync;
import org.openhab.core.audio.AudioStream;
-import org.openhab.core.audio.FixedLengthAudioStream;
+import org.openhab.core.audio.StreamServed;
import org.openhab.core.audio.URLAudioStream;
import org.openhab.core.audio.UnsupportedAudioFormatException;
import org.openhab.core.audio.UnsupportedAudioStreamException;
* @author Paul Frank - Adapted for Kodi
* @author Christoph Weitkamp - Improvements for playing audio notifications
*/
-public class KodiAudioSink implements AudioSink {
+public class KodiAudioSink extends AudioSinkSync {
private final Logger logger = LoggerFactory.getLogger(KodiAudioSink.class);
- private static final Set<AudioFormat> SUPPORTED_AUDIO_FORMATS = Collections
- .unmodifiableSet(Stream.of(AudioFormat.MP3, AudioFormat.WAV).collect(Collectors.toSet()));
- private static final Set<Class<? extends AudioStream>> SUPPORTED_AUDIO_STREAMS = Collections
- .unmodifiableSet(Stream.of(FixedLengthAudioStream.class, URLAudioStream.class).collect(Collectors.toSet()));
+ private static final Set<AudioFormat> SUPPORTED_AUDIO_FORMATS = Set.of(AudioFormat.MP3, AudioFormat.WAV);
+ private static final Set<Class<? extends AudioStream>> SUPPORTED_AUDIO_STREAMS = Set.of(AudioStream.class);
// Needed because Kodi does multiple requests for the stream
- private static final int STREAM_TIMEOUT = 30;
+ private static final int STREAM_TIMEOUT = 10;
private final KodiHandler handler;
private final AudioHTTPServer audioHTTPServer;
}
@Override
- public void process(AudioStream audioStream)
+ public @NonNull CompletableFuture<@Nullable Void> processAndComplete(@Nullable AudioStream audioStream) {
+ // we override this method to intercept URLAudioStream and handle it asynchronously. We won't wait for it to
+ // play through the end as it can be very long
+ if (audioStream instanceof URLAudioStream) {
+ // Asynchronous handling for URLAudioStream. Id it is an external URL, the speaker can access it itself and
+ // play it. There will be no volume restoration or call to dispose / complete, but there is no need to.
+ String url = ((URLAudioStream) audioStream).getURL();
+ AudioFormat format = audioStream.getFormat();
+ logger.trace("Processing audioStream URL {} of format {}.", url, format);
+ handler.playURI(new StringType(url));
+ tryClose(audioStream);
+ return new CompletableFuture<@Nullable Void>();
+ } else {
+ return super.processAndComplete(audioStream);
+ }
+ }
+
+ @Override
+ public void processSynchronously(AudioStream audioStream)
throws UnsupportedAudioFormatException, UnsupportedAudioStreamException {
+ if (audioStream instanceof URLAudioStream) {
+ return;
+ }
+
if (audioStream == null) {
// in case the audioStream is null, this should be interpreted as a request to end any currently playing
// stream.
} else {
AudioFormat format = audioStream.getFormat();
if (!AudioFormat.MP3.isCompatible(format) && !AudioFormat.WAV.isCompatible(format)) {
+ tryClose(audioStream);
throw new UnsupportedAudioFormatException("Currently only MP3 and WAV formats are supported.", format);
}
- if (audioStream instanceof URLAudioStream) {
- // it is an external URL, the speaker can access it itself and play it
- String url = ((URLAudioStream) audioStream).getURL();
- logger.trace("Processing audioStream URL {} of format {}.", url, format);
- handler.playURI(new StringType(url));
- } else if (audioStream instanceof FixedLengthAudioStream) {
- if (callbackUrl != null) {
- // we serve it on our own HTTP server for 30 seconds as Kodi requests the stream several times
- // Form the URL for streaming the notification from the OH2 web server
- String url = callbackUrl
- + audioHTTPServer.serve((FixedLengthAudioStream) audioStream, STREAM_TIMEOUT);
+ if (callbackUrl != null) {
+ // we serve it on our own HTTP server for 10 seconds as Kodi requests the stream several times
+ // Form the URL for streaming the notification from the OH web server
+ try {
+ StreamServed streamServed = audioHTTPServer.serve(audioStream, STREAM_TIMEOUT, true);
+ String url = callbackUrl + streamServed.url();
logger.trace("Processing audioStream URL {} of format {}.", url, format);
- handler.playNotificationSoundURI(new StringType(url));
- } else {
- logger.warn("We do not have any callback url, so Kodi cannot play the audio stream!");
+ handler.playNotificationSoundURI(new StringType(url), false);
+ } catch (IOException e) {
+ tryClose(audioStream);
+ throw new UnsupportedAudioStreamException(
+ "Kodi binding was not able to handle the audio stream (cache on disk failed)",
+ audioStream.getClass(), e);
}
} else {
- throw new UnsupportedAudioStreamException(
- "Kodi can only handle URLAudioStream or FixedLengthAudioStreams.", audioStream.getClass());
+ tryClose(audioStream);
+ logger.warn("We do not have any callback url, so Kodi cannot play the audio stream!");
+ }
+ }
+ }
+
+ private void tryClose(@Nullable InputStream is) {
+ if (is != null) {
+ try {
+ is.close();
+ } catch (IOException ignored) {
}
}
}
break;
case CHANNEL_PLAYNOTIFICATION:
if (command instanceof StringType) {
- playNotificationSoundURI((StringType) command);
+ playNotificationSoundURI((StringType) command, true);
updateState(CHANNEL_PLAYNOTIFICATION, UnDefType.UNDEF);
} else if (command.equals(RefreshType.REFRESH)) {
updateState(CHANNEL_PLAYNOTIFICATION, UnDefType.UNDEF);
* Play the notification by 1) saving the state of the player, 2) stopping the current
* playlist item, 3) adding the notification as a new playlist item, 4) playing the new
* playlist item, and 5) restoring the player to its previous state.
+ * set manageVolume to true if the binding must handle volume change by itself
*/
- public void playNotificationSoundURI(StringType uri) {
+ public void playNotificationSoundURI(StringType uri, boolean manageVolume) {
// save the current state of the player
logger.trace("Saving current player state");
KodiPlayerState playerState = new KodiPlayerState();
- playerState.setSavedVolume(connection.getVolume());
+ if (manageVolume) {
+ playerState.setSavedVolume(connection.getVolume());
+ }
playerState.setPlaylistID(connection.getActivePlaylist());
playerState.setSavedState(connection.getState());
}
// set notification sound volume
- logger.trace("Setting up player for notification");
- int notificationVolume = getNotificationSoundVolume().intValue();
- connection.setVolume(notificationVolume);
- waitForVolume(notificationVolume);
+ if (manageVolume) {
+ logger.trace("Setting up player for notification");
+ int notificationVolume = getNotificationSoundVolume().intValue();
+ connection.setVolume(notificationVolume);
+ waitForVolume(notificationVolume);
+ }
// add the notification uri to the playlist and play it
logger.trace("Playing notification");
waitForPlaylistState(KodiPlaylistState.REMOVED);
// restore previous volume
- connection.setVolume(playerState.getSavedVolume());
- waitForVolume(playerState.getSavedVolume());
+ if (manageVolume) {
+ connection.setVolume(playerState.getSavedVolume());
+ waitForVolume(playerState.getSavedVolume());
+ }
// resume playing save playlist item if player wasn't stopped
logger.trace("Restoring player state");
*/
private boolean waitForState(KodiState state) {
int timeoutMaxCount = getConfigAs(KodiConfig.class).getNotificationTimeout().intValue(), timeoutCount = 0;
- logger.trace("Waiting up to {} ms for state '{}' to be set ...", timeoutMaxCount * 100, state);
+ logger.trace("Waiting up to {} ms for state '{}' to be set ...", timeoutMaxCount * 1000, state);
while (!state.equals(connection.getState()) && timeoutCount < timeoutMaxCount) {
try {
- Thread.sleep(100);
+ Thread.sleep(1000);
} catch (InterruptedException e) {
break;
}