]> git.basschouten.com Git - openhab-addons.git/commitdiff
[kodi] Support for more audio streams through the HTTP audio servlet (#15192)
authorGwendal Roulleau <dalgwen@users.noreply.github.com>
Sat, 8 Jul 2023 09:18:44 +0000 (11:18 +0200)
committerGitHub <noreply@github.com>
Sat, 8 Jul 2023 09:18:44 +0000 (11:18 +0200)
* [kodi] Support for more audio streams through the HTTP audio servlet

[kodi] Audio sink supporting more audio streams

Related to #15113

---------

Signed-off-by: Gwendal Roulleau <gwendal.roulleau@gmail.com>
bundles/org.openhab.binding.kodi/src/main/java/org/openhab/binding/kodi/internal/KodiAudioSink.java
bundles/org.openhab.binding.kodi/src/main/java/org/openhab/binding/kodi/internal/handler/KodiHandler.java

index 74c83744ff2b3b63d28d76deaac34a55b577e9d1..996926275be6d0b82bc99f1495662a3d24bd142f 100644 (file)
  */
 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<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;
@@ -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) {
             }
         }
     }
index d27e4a37ab2cc6822510c12a201f5cf0e09df294..4949041273964e7daef3087c76f47dd74e7bc7a0 100644 (file)
@@ -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;
             }