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 369c4af660b..afd9e223c57 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.binding.sonos.internal.handler.ZonePlayerHandler; 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 @@ import org.slf4j.LoggerFactory; * * @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 class SonosAudioSink implements AudioSink { 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 == 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) { + 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. - URLAudioStream urlAudioStream = (URLAudioStream) audioStream; handler.playURI(new StringType(urlAudioStream.getURL())); try { audioStream.close(); } 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); + @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); + 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 { + streamServed = audioHTTPServer.serve(audioStream, 10, true); + } catch (IOException e) { + 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. } }