[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 <lg.hc@free.fr>
This commit is contained in:
lolodomo 2023-07-02 11:22:31 +02:00 committed by GitHub
parent 1f78f9ac7b
commit 070de816f3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -13,11 +13,9 @@
package org.openhab.binding.sonos.internal; package org.openhab.binding.sonos.internal;
import java.io.IOException; import java.io.IOException;
import java.util.Collections;
import java.util.Locale; import java.util.Locale;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.concurrent.CompletableFuture;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; 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.AudioFormat;
import org.openhab.core.audio.AudioHTTPServer; import org.openhab.core.audio.AudioHTTPServer;
import org.openhab.core.audio.AudioSink; import org.openhab.core.audio.AudioSink;
import org.openhab.core.audio.AudioSinkSync;
import org.openhab.core.audio.AudioStream; import org.openhab.core.audio.AudioStream;
import org.openhab.core.audio.FileAudioStream; 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.URLAudioStream;
import org.openhab.core.audio.UnsupportedAudioFormatException; import org.openhab.core.audio.UnsupportedAudioFormatException;
import org.openhab.core.audio.UnsupportedAudioStreamException; import org.openhab.core.audio.UnsupportedAudioStreamException;
@ -44,17 +43,16 @@ import org.slf4j.LoggerFactory;
* *
* @author Kai Kreuzer - Initial contribution and API * @author Kai Kreuzer - Initial contribution and API
* @author Christoph Weitkamp - Added getSupportedStreams() and UnsupportedAudioStreamException * @author Christoph Weitkamp - Added getSupportedStreams() and UnsupportedAudioStreamException
* @author Laurent Garnier - Support for more audio streams through the HTTP audio servlet
* *
*/ */
@NonNullByDefault @NonNullByDefault
public class SonosAudioSink implements AudioSink { public class SonosAudioSink extends AudioSinkSync {
private final Logger logger = LoggerFactory.getLogger(SonosAudioSink.class); private final Logger logger = LoggerFactory.getLogger(SonosAudioSink.class);
private static final Set<AudioFormat> SUPPORTED_AUDIO_FORMATS = Collections private static final Set<AudioFormat> SUPPORTED_AUDIO_FORMATS = Set.of(AudioFormat.MP3, AudioFormat.WAV);
.unmodifiableSet(Stream.of(AudioFormat.MP3, AudioFormat.WAV).collect(Collectors.toSet())); private static final Set<Class<? extends AudioStream>> SUPPORTED_AUDIO_STREAMS = Set.of(AudioStream.class);
private static final Set<Class<? extends AudioStream>> SUPPORTED_AUDIO_STREAMS = Collections
.unmodifiableSet(Stream.of(FixedLengthAudioStream.class, URLAudioStream.class).collect(Collectors.toSet()));
private AudioHTTPServer audioHTTPServer; private AudioHTTPServer audioHTTPServer;
private ZonePlayerHandler handler; private ZonePlayerHandler handler;
@ -76,29 +74,76 @@ public class SonosAudioSink implements AudioSink {
return handler.getThing().getLabel(); 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 @Override
public void process(@Nullable AudioStream audioStream) public void process(@Nullable AudioStream audioStream)
throws UnsupportedAudioFormatException, UnsupportedAudioStreamException { throws UnsupportedAudioFormatException, UnsupportedAudioStreamException {
if (audioStream == null) { if (audioStream instanceof URLAudioStream) {
// in case the audioStream is null, this should be interpreted as a request to end any currently playing processAsynchronously(audioStream);
// stream. } else {
logger.trace("Stop currently playing stream."); processSynchronously(audioStream);
handler.stopPlaying(OnOffType.ON); }
} else if (audioStream instanceof URLAudioStream) { }
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. // it is an external URL, the speaker can access it itself and play it.
URLAudioStream urlAudioStream = (URLAudioStream) audioStream;
handler.playURI(new StringType(urlAudioStream.getURL())); handler.playURI(new StringType(urlAudioStream.getURL()));
try { try {
audioStream.close(); audioStream.close();
} catch (IOException e) { } catch (IOException e) {
} }
} else if (audioStream instanceof FixedLengthAudioStream) { }
}
@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 // 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 // Note that Sonos does multiple concurrent requests to the AudioServlet,
// the AudioServlet, so a one time serving won't work. // so a one time serving won't work.
if (callbackUrl != null) { if (callbackUrl != null) {
String relativeUrl = audioHTTPServer.serve((FixedLengthAudioStream) audioStream, 10).toString(); StreamServed streamServed;
String url = callbackUrl + relativeUrl; 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(); AudioFormat format = audioStream.getFormat();
if (!ThingHandlerHelper.isHandlerInitialized(handler)) { if (!ThingHandlerHelper.isHandlerInitialized(handler)) {
@ -115,17 +160,10 @@ public class SonosAudioSink implements AudioSink {
} }
} else { } else {
logger.warn("We do not have any callback url, so Sonos cannot play the audio stream!"); logger.warn("We do not have any callback url, so Sonos cannot play the audio stream!");
}
} else {
try { try {
audioStream.close(); audioStream.close();
} catch (IOException e) { } 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.
} }
} }