diff --git a/bundles/org.openhab.binding.kodi/src/main/java/org/openhab/binding/kodi/internal/KodiAudioSink.java b/bundles/org.openhab.binding.kodi/src/main/java/org/openhab/binding/kodi/internal/KodiAudioSink.java index 74c83744f..996926275 100644 --- a/bundles/org.openhab.binding.kodi/src/main/java/org/openhab/binding/kodi/internal/KodiAudioSink.java +++ b/bundles/org.openhab.binding.kodi/src/main/java/org/openhab/binding/kodi/internal/KodiAudioSink.java @@ -12,18 +12,21 @@ */ 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; @@ -39,16 +42,14 @@ import org.slf4j.LoggerFactory; * @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 SUPPORTED_AUDIO_FORMATS = Collections - .unmodifiableSet(Stream.of(AudioFormat.MP3, AudioFormat.WAV).collect(Collectors.toSet())); - private static final Set> SUPPORTED_AUDIO_STREAMS = Collections - .unmodifiableSet(Stream.of(FixedLengthAudioStream.class, URLAudioStream.class).collect(Collectors.toSet())); + private static final Set SUPPORTED_AUDIO_FORMATS = Set.of(AudioFormat.MP3, AudioFormat.WAV); + private static final Set> 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; @@ -71,8 +72,30 @@ public class KodiAudioSink implements AudioSink { } @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. @@ -81,28 +104,36 @@ public class KodiAudioSink implements AudioSink { } 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) { } } } diff --git a/bundles/org.openhab.binding.kodi/src/main/java/org/openhab/binding/kodi/internal/handler/KodiHandler.java b/bundles/org.openhab.binding.kodi/src/main/java/org/openhab/binding/kodi/internal/handler/KodiHandler.java index d27e4a37a..494904127 100644 --- a/bundles/org.openhab.binding.kodi/src/main/java/org/openhab/binding/kodi/internal/handler/KodiHandler.java +++ b/bundles/org.openhab.binding.kodi/src/main/java/org/openhab/binding/kodi/internal/handler/KodiHandler.java @@ -214,7 +214,7 @@ public class KodiHandler extends BaseThingHandler implements KodiEventListener { 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); @@ -456,12 +456,15 @@ public class KodiHandler extends BaseThingHandler implements KodiEventListener { * 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()); @@ -482,10 +485,12 @@ public class KodiHandler extends BaseThingHandler implements KodiEventListener { } // 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"); @@ -504,8 +509,10 @@ public class KodiHandler extends BaseThingHandler implements KodiEventListener { 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"); @@ -551,10 +558,10 @@ public class KodiHandler extends BaseThingHandler implements KodiEventListener { */ 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; }