[kodi] Support for more audio streams through the HTTP audio servlet (#15192)

* [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>
This commit is contained in:
Gwendal Roulleau 2023-07-08 11:18:44 +02:00 committed by GitHub
parent 365e900a1f
commit 081bf3a9d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 76 additions and 38 deletions

View File

@ -12,18 +12,21 @@
*/ */
package org.openhab.binding.kodi.internal; 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.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.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.kodi.internal.handler.KodiHandler; import org.openhab.binding.kodi.internal.handler.KodiHandler;
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.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;
@ -39,16 +42,14 @@ import org.slf4j.LoggerFactory;
* @author Paul Frank - Adapted for Kodi * @author Paul Frank - Adapted for Kodi
* @author Christoph Weitkamp - Improvements for playing audio notifications * @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 final Logger logger = LoggerFactory.getLogger(KodiAudioSink.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()));
// Needed because Kodi does multiple requests for the stream // 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 KodiHandler handler;
private final AudioHTTPServer audioHTTPServer; private final AudioHTTPServer audioHTTPServer;
@ -71,8 +72,30 @@ public class KodiAudioSink implements AudioSink {
} }
@Override @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 { throws UnsupportedAudioFormatException, UnsupportedAudioStreamException {
if (audioStream instanceof URLAudioStream) {
return;
}
if (audioStream == null) { if (audioStream == null) {
// in case the audioStream is null, this should be interpreted as a request to end any currently playing // in case the audioStream is null, this should be interpreted as a request to end any currently playing
// stream. // stream.
@ -81,28 +104,36 @@ public class KodiAudioSink implements AudioSink {
} else { } else {
AudioFormat format = audioStream.getFormat(); AudioFormat format = audioStream.getFormat();
if (!AudioFormat.MP3.isCompatible(format) && !AudioFormat.WAV.isCompatible(format)) { if (!AudioFormat.MP3.isCompatible(format) && !AudioFormat.WAV.isCompatible(format)) {
tryClose(audioStream);
throw new UnsupportedAudioFormatException("Currently only MP3 and WAV formats are supported.", format); throw new UnsupportedAudioFormatException("Currently only MP3 and WAV formats are supported.", format);
} }
if (audioStream instanceof URLAudioStream) { if (callbackUrl != null) {
// it is an external URL, the speaker can access it itself and play it // we serve it on our own HTTP server for 10 seconds as Kodi requests the stream several times
String url = ((URLAudioStream) audioStream).getURL(); // Form the URL for streaming the notification from the OH web server
logger.trace("Processing audioStream URL {} of format {}.", url, format); try {
handler.playURI(new StringType(url)); StreamServed streamServed = audioHTTPServer.serve(audioStream, STREAM_TIMEOUT, true);
} else if (audioStream instanceof FixedLengthAudioStream) { String url = callbackUrl + streamServed.url();
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);
logger.trace("Processing audioStream URL {} of format {}.", url, format); logger.trace("Processing audioStream URL {} of format {}.", url, format);
handler.playNotificationSoundURI(new StringType(url)); handler.playNotificationSoundURI(new StringType(url), false);
} else { } catch (IOException e) {
logger.warn("We do not have any callback url, so Kodi cannot play the audio stream!"); tryClose(audioStream);
throw new UnsupportedAudioStreamException(
"Kodi binding was not able to handle the audio stream (cache on disk failed)",
audioStream.getClass(), e);
} }
} else { } else {
throw new UnsupportedAudioStreamException( tryClose(audioStream);
"Kodi can only handle URLAudioStream or FixedLengthAudioStreams.", audioStream.getClass()); 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) {
} }
} }
} }

View File

@ -214,7 +214,7 @@ public class KodiHandler extends BaseThingHandler implements KodiEventListener {
break; break;
case CHANNEL_PLAYNOTIFICATION: case CHANNEL_PLAYNOTIFICATION:
if (command instanceof StringType) { if (command instanceof StringType) {
playNotificationSoundURI((StringType) command); playNotificationSoundURI((StringType) command, true);
updateState(CHANNEL_PLAYNOTIFICATION, UnDefType.UNDEF); updateState(CHANNEL_PLAYNOTIFICATION, UnDefType.UNDEF);
} else if (command.equals(RefreshType.REFRESH)) { } else if (command.equals(RefreshType.REFRESH)) {
updateState(CHANNEL_PLAYNOTIFICATION, UnDefType.UNDEF); 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 * 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, 3) adding the notification as a new playlist item, 4) playing the new
* playlist item, and 5) restoring the player to its previous state. * 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 // save the current state of the player
logger.trace("Saving current player state"); logger.trace("Saving current player state");
KodiPlayerState playerState = new KodiPlayerState(); KodiPlayerState playerState = new KodiPlayerState();
playerState.setSavedVolume(connection.getVolume()); if (manageVolume) {
playerState.setSavedVolume(connection.getVolume());
}
playerState.setPlaylistID(connection.getActivePlaylist()); playerState.setPlaylistID(connection.getActivePlaylist());
playerState.setSavedState(connection.getState()); playerState.setSavedState(connection.getState());
@ -482,10 +485,12 @@ public class KodiHandler extends BaseThingHandler implements KodiEventListener {
} }
// set notification sound volume // set notification sound volume
logger.trace("Setting up player for notification"); if (manageVolume) {
int notificationVolume = getNotificationSoundVolume().intValue(); logger.trace("Setting up player for notification");
connection.setVolume(notificationVolume); int notificationVolume = getNotificationSoundVolume().intValue();
waitForVolume(notificationVolume); connection.setVolume(notificationVolume);
waitForVolume(notificationVolume);
}
// add the notification uri to the playlist and play it // add the notification uri to the playlist and play it
logger.trace("Playing notification"); logger.trace("Playing notification");
@ -504,8 +509,10 @@ public class KodiHandler extends BaseThingHandler implements KodiEventListener {
waitForPlaylistState(KodiPlaylistState.REMOVED); waitForPlaylistState(KodiPlaylistState.REMOVED);
// restore previous volume // restore previous volume
connection.setVolume(playerState.getSavedVolume()); if (manageVolume) {
waitForVolume(playerState.getSavedVolume()); connection.setVolume(playerState.getSavedVolume());
waitForVolume(playerState.getSavedVolume());
}
// resume playing save playlist item if player wasn't stopped // resume playing save playlist item if player wasn't stopped
logger.trace("Restoring player state"); logger.trace("Restoring player state");
@ -551,10 +558,10 @@ public class KodiHandler extends BaseThingHandler implements KodiEventListener {
*/ */
private boolean waitForState(KodiState state) { private boolean waitForState(KodiState state) {
int timeoutMaxCount = getConfigAs(KodiConfig.class).getNotificationTimeout().intValue(), timeoutCount = 0; 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) { while (!state.equals(connection.getState()) && timeoutCount < timeoutMaxCount) {
try { try {
Thread.sleep(100); Thread.sleep(1000);
} catch (InterruptedException e) { } catch (InterruptedException e) {
break; break;
} }