From 5254dac2441d0523383e2f2d1fa12815c11f824f Mon Sep 17 00:00:00 2001
From: lolodomo <lg.hc@free.fr>
Date: Wed, 12 Jul 2023 21:50:20 +0200
Subject: [PATCH] [freebox] Support for more audio streams through the HTTP
 audio servlet (#15121)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Related to #15113

Signed-off-by: Laurent Garnier <lg.hc@free.fr>
Signed-off-by: Jørgen Austvik <jaustvik@acm.org>
---
 .../internal/FreeboxAirPlayAudioSink.java     | 75 ++++++++++++-------
 1 file changed, 46 insertions(+), 29 deletions(-)

diff --git a/bundles/org.openhab.binding.freebox/src/main/java/org/openhab/binding/freebox/internal/FreeboxAirPlayAudioSink.java b/bundles/org.openhab.binding.freebox/src/main/java/org/openhab/binding/freebox/internal/FreeboxAirPlayAudioSink.java
index aff314d448684..7050291643834 100644
--- a/bundles/org.openhab.binding.freebox/src/main/java/org/openhab/binding/freebox/internal/FreeboxAirPlayAudioSink.java
+++ b/bundles/org.openhab.binding.freebox/src/main/java/org/openhab/binding/freebox/internal/FreeboxAirPlayAudioSink.java
@@ -15,6 +15,7 @@
 import static org.openhab.core.audio.AudioFormat.*;
 
 import java.io.IOException;
+import java.io.InputStream;
 import java.util.HashSet;
 import java.util.Locale;
 import java.util.Set;
@@ -27,8 +28,9 @@
 import org.openhab.core.audio.AudioFormat;
 import org.openhab.core.audio.AudioHTTPServer;
 import org.openhab.core.audio.AudioSink;
+import org.openhab.core.audio.AudioSinkAsync;
 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;
@@ -43,9 +45,10 @@
  * This makes an AirPlay device to serve as an {@link AudioSink}-
  *
  * @author Laurent Garnier - Initial contribution for AudioSink and notifications
+ * @author Laurent Garnier - Support for more audio streams through the HTTP audio servlet
  */
 @NonNullByDefault
-public class FreeboxAirPlayAudioSink implements AudioSink {
+public class FreeboxAirPlayAudioSink extends AudioSinkAsync {
 
     private final Logger logger = LoggerFactory.getLogger(FreeboxAirPlayAudioSink.class);
 
@@ -59,15 +62,11 @@ public class FreeboxAirPlayAudioSink implements AudioSink {
     private static final AudioFormat MP3_320 = new AudioFormat(CONTAINER_NONE, CODEC_MP3, null, null, 320000, null);
 
     private static final Set<AudioFormat> SUPPORTED_FORMATS = new HashSet<>();
-    private static final HashSet<Class<? extends AudioStream>> SUPPORTED_STREAMS = new HashSet<>();
+    private static final Set<Class<? extends AudioStream>> SUPPORTED_STREAMS = Set.of(AudioStream.class);
     private AudioHTTPServer audioHTTPServer;
     private FreeboxThingHandler handler;
     private @Nullable String callbackUrl;
 
-    static {
-        SUPPORTED_STREAMS.add(AudioStream.class);
-    }
-
     public FreeboxAirPlayAudioSink(FreeboxThingHandler handler, AudioHTTPServer audioHTTPServer,
             @Nullable String callbackUrl) {
         this.handler = handler;
@@ -89,7 +88,8 @@ public FreeboxAirPlayAudioSink(FreeboxThingHandler handler, AudioHTTPServer audi
             SUPPORTED_FORMATS.add(MP3_256);
             SUPPORTED_FORMATS.add(MP3_320);
         }
-        SUPPORTED_FORMATS.add(OGG);
+        // OGG seems to not be properly supported (tested with a file produced by VoiceRSS)
+        // SUPPORTED_FORMATS.add(OGG);
     }
 
     @Override
@@ -103,13 +103,14 @@ public String getId() {
     }
 
     @Override
-    public void process(@Nullable AudioStream audioStream)
+    protected void processAsynchronously(@Nullable AudioStream audioStream)
             throws UnsupportedAudioFormatException, UnsupportedAudioStreamException {
         if (!ThingHandlerHelper.isHandlerInitialized(handler)
                 || ((handler.getThing().getStatus() == ThingStatus.OFFLINE)
                         && ((handler.getThing().getStatusInfo().getStatusDetail() == ThingStatusDetail.BRIDGE_OFFLINE)
                                 || (handler.getThing().getStatusInfo()
                                         .getStatusDetail() == ThingStatusDetail.CONFIGURATION_ERROR)))) {
+            tryClose(audioStream);
             return;
         }
 
@@ -122,29 +123,36 @@ public void process(@Nullable AudioStream audioStream)
             return;
         }
 
-        String url = null;
-        if (audioStream instanceof URLAudioStream) {
+        String url;
+        if (audioStream instanceof URLAudioStream urlAudioStream) {
             // it is an external URL, we can access it directly
-            URLAudioStream urlAudioStream = (URLAudioStream) audioStream;
             url = urlAudioStream.getURL();
-        } else {
-            if (callbackUrl != null) {
-                // we serve it on our own HTTP server
-                String relativeUrl;
-                if (audioStream instanceof FixedLengthAudioStream) {
-                    relativeUrl = audioHTTPServer.serve((FixedLengthAudioStream) audioStream, 20);
-                } else {
-                    relativeUrl = audioHTTPServer.serve(audioStream);
-                }
-                url = callbackUrl + relativeUrl;
-            } else {
-                logger.warn("We do not have any callback url, so AirPlay device cannot play the audio stream!");
+            tryClose(audioStream);
+        } else if (callbackUrl != null) {
+            // we serve it on our own HTTP server
+            logger.debug("audioStream {} {}", audioStream.getClass().getSimpleName(), audioStream.getFormat());
+            StreamServed streamServed;
+            try {
+                streamServed = audioHTTPServer.serve(audioStream, 5, true);
+            } catch (IOException e) {
+                tryClose(audioStream);
+                throw new UnsupportedAudioStreamException(
+                        "AirPlay device was not able to handle the audio stream (cache on disk failed).",
+                        audioStream.getClass(), e);
             }
-        }
-        try {
-            audioStream.close();
-        } catch (IOException e) {
-            logger.debug("Exception while closing audioStream", e);
+            url = callbackUrl + streamServed.url();
+            streamServed.playEnd().thenRun(() -> {
+                try {
+                    handler.stopMedia();
+                } catch (FreeboxException e) {
+                    logger.warn("Exception while stopping audio stream playback: {}", e.getMessage());
+                }
+                this.playbackFinished(audioStream);
+            });
+        } else {
+            logger.warn("We do not have any callback url, so AirPlay device cannot play the audio stream!");
+            tryClose(audioStream);
+            return;
         }
         try {
             logger.debug("AirPlay audio sink: process url {}", url);
@@ -154,6 +162,15 @@ public void process(@Nullable AudioStream audioStream)
         }
     }
 
+    private void tryClose(@Nullable InputStream is) {
+        if (is != null) {
+            try {
+                is.close();
+            } catch (IOException ignored) {
+            }
+        }
+    }
+
     @Override
     public Set<AudioFormat> getSupportedFormats() {
         return SUPPORTED_FORMATS;