From 070de816f3822a0cfb4229343b507541a02ff535 Mon Sep 17 00:00:00 2001 From: lolodomo Date: Sun, 2 Jul 2023 11:22:31 +0200 Subject: [PATCH] [sonos] Support for more audio streams through the HTTP audio servlet (#15116) * [sonos] Audio sink supporting more audio streams Related to #15113 Signed-off-by: Laurent Garnier --- .../sonos/internal/SonosAudioSink.java | 120 ++++++++++++------ 1 file changed, 79 insertions(+), 41 deletions(-) diff --git a/bundles/org.openhab.binding.sonos/src/main/java/org/openhab/binding/sonos/internal/SonosAudioSink.java b/bundles/org.openhab.binding.sonos/src/main/java/org/openhab/binding/sonos/internal/SonosAudioSink.java index 369c4af660b16..afd9e223c57b5 100644 --- a/bundles/org.openhab.binding.sonos/src/main/java/org/openhab/binding/sonos/internal/SonosAudioSink.java +++ b/bundles/org.openhab.binding.sonos/src/main/java/org/openhab/binding/sonos/internal/SonosAudioSink.java @@ -13,11 +13,9 @@ package org.openhab.binding.sonos.internal; import java.io.IOException; -import java.util.Collections; 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.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -25,9 +23,10 @@ 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.FileAudioStream; -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; @@ -44,17 +43,16 @@ * * @author Kai Kreuzer - Initial contribution and API * @author Christoph Weitkamp - Added getSupportedStreams() and UnsupportedAudioStreamException + * @author Laurent Garnier - Support for more audio streams through the HTTP audio servlet * */ @NonNullByDefault -public class SonosAudioSink implements AudioSink { +public class SonosAudioSink extends AudioSinkSync { private final Logger logger = LoggerFactory.getLogger(SonosAudioSink.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); private AudioHTTPServer audioHTTPServer; private ZonePlayerHandler handler; @@ -76,56 +74,96 @@ public String getId() { return handler.getThing().getLabel(); } + @Override + public CompletableFuture<@Nullable Void> processAndComplete(@Nullable AudioStream audioStream) { + if (audioStream instanceof URLAudioStream) { + // Asynchronous handling for URLAudioStream + CompletableFuture<@Nullable Void> completableFuture = new CompletableFuture<@Nullable Void>(); + try { + processAsynchronously(audioStream); + } catch (UnsupportedAudioFormatException | UnsupportedAudioStreamException e) { + completableFuture.completeExceptionally(e); + } + return completableFuture; + } else { + return super.processAndComplete(audioStream); + } + } + @Override public void process(@Nullable AudioStream audioStream) throws UnsupportedAudioFormatException, UnsupportedAudioStreamException { + if (audioStream instanceof URLAudioStream) { + processAsynchronously(audioStream); + } else { + processSynchronously(audioStream); + } + } + + private void processAsynchronously(@Nullable AudioStream audioStream) + throws UnsupportedAudioFormatException, UnsupportedAudioStreamException { + if (audioStream instanceof URLAudioStream urlAudioStream) { + // it is an external URL, the speaker can access it itself and play it. + handler.playURI(new StringType(urlAudioStream.getURL())); + try { + audioStream.close(); + } catch (IOException e) { + } + } + } + + @Override + protected void processSynchronously(@Nullable 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. logger.trace("Stop currently playing stream."); handler.stopPlaying(OnOffType.ON); - } else if (audioStream instanceof URLAudioStream) { - // it is an external URL, the speaker can access it itself and play it. - URLAudioStream urlAudioStream = (URLAudioStream) audioStream; - handler.playURI(new StringType(urlAudioStream.getURL())); + return; + } + + // we serve it on our own HTTP server and treat it as a notification + // Note that Sonos does multiple concurrent requests to the AudioServlet, + // so a one time serving won't work. + if (callbackUrl != null) { + StreamServed streamServed; try { - audioStream.close(); + streamServed = audioHTTPServer.serve(audioStream, 10, true); } catch (IOException e) { - } - } else if (audioStream instanceof FixedLengthAudioStream) { - // we serve it on our own HTTP server and treat it as a notification - // Note that we have to pass a FixedLengthAudioStream, since Sonos does multiple concurrent requests to - // the AudioServlet, so a one time serving won't work. - if (callbackUrl != null) { - String relativeUrl = audioHTTPServer.serve((FixedLengthAudioStream) audioStream, 10).toString(); - String url = callbackUrl + relativeUrl; - - AudioFormat format = audioStream.getFormat(); - if (!ThingHandlerHelper.isHandlerInitialized(handler)) { - logger.warn("Sonos speaker '{}' is not initialized - status is {}", handler.getThing().getUID(), - handler.getThing().getStatus()); - } else if (AudioFormat.WAV.isCompatible(format)) { - handler.playNotificationSoundURI( - new StringType(url + AudioStreamUtils.EXTENSION_SEPARATOR + FileAudioStream.WAV_EXTENSION)); - } else if (AudioFormat.MP3.isCompatible(format)) { - handler.playNotificationSoundURI( - new StringType(url + AudioStreamUtils.EXTENSION_SEPARATOR + FileAudioStream.MP3_EXTENSION)); - } else { - throw new UnsupportedAudioFormatException("Sonos only supports MP3 or WAV.", format); + try { + audioStream.close(); + } catch (IOException ex) { } + throw new UnsupportedAudioStreamException( + "Sonos was not able to handle the audio stream (cache on disk failed).", audioStream.getClass(), + e); + } + String url = callbackUrl + streamServed.url(); + + AudioFormat format = audioStream.getFormat(); + if (!ThingHandlerHelper.isHandlerInitialized(handler)) { + logger.warn("Sonos speaker '{}' is not initialized - status is {}", handler.getThing().getUID(), + handler.getThing().getStatus()); + } else if (AudioFormat.WAV.isCompatible(format)) { + handler.playNotificationSoundURI( + new StringType(url + AudioStreamUtils.EXTENSION_SEPARATOR + FileAudioStream.WAV_EXTENSION)); + } else if (AudioFormat.MP3.isCompatible(format)) { + handler.playNotificationSoundURI( + new StringType(url + AudioStreamUtils.EXTENSION_SEPARATOR + FileAudioStream.MP3_EXTENSION)); } else { - logger.warn("We do not have any callback url, so Sonos cannot play the audio stream!"); + throw new UnsupportedAudioFormatException("Sonos only supports MP3 or WAV.", format); } } else { + logger.warn("We do not have any callback url, so Sonos cannot play the audio stream!"); try { audioStream.close(); } catch (IOException e) { } - throw new UnsupportedAudioStreamException( - "Sonos can only handle FixedLengthAudioStreams and URLAudioStreams.", audioStream.getClass()); - // Instead of throwing an exception, we could ourselves try to wrap it into a - // FixedLengthAudioStream, but this might be dangerous as we have no clue, how much data to expect from - // the stream. } }