2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.sonos.internal;
15 import java.io.IOException;
16 import java.util.Locale;
18 import java.util.concurrent.CompletableFuture;
20 import org.eclipse.jdt.annotation.NonNullByDefault;
21 import org.eclipse.jdt.annotation.Nullable;
22 import org.openhab.binding.sonos.internal.handler.ZonePlayerHandler;
23 import org.openhab.core.audio.AudioFormat;
24 import org.openhab.core.audio.AudioHTTPServer;
25 import org.openhab.core.audio.AudioSink;
26 import org.openhab.core.audio.AudioSinkSync;
27 import org.openhab.core.audio.AudioStream;
28 import org.openhab.core.audio.FileAudioStream;
29 import org.openhab.core.audio.StreamServed;
30 import org.openhab.core.audio.URLAudioStream;
31 import org.openhab.core.audio.UnsupportedAudioFormatException;
32 import org.openhab.core.audio.UnsupportedAudioStreamException;
33 import org.openhab.core.audio.utils.AudioStreamUtils;
34 import org.openhab.core.library.types.OnOffType;
35 import org.openhab.core.library.types.PercentType;
36 import org.openhab.core.library.types.StringType;
37 import org.openhab.core.thing.util.ThingHandlerHelper;
38 import org.slf4j.Logger;
39 import org.slf4j.LoggerFactory;
42 * This makes a Sonos speaker to serve as an {@link AudioSink}-
44 * @author Kai Kreuzer - Initial contribution and API
45 * @author Christoph Weitkamp - Added getSupportedStreams() and UnsupportedAudioStreamException
46 * @author Laurent Garnier - Support for more audio streams through the HTTP audio servlet
50 public class SonosAudioSink extends AudioSinkSync {
52 private final Logger logger = LoggerFactory.getLogger(SonosAudioSink.class);
54 private static final Set<AudioFormat> SUPPORTED_AUDIO_FORMATS = Set.of(AudioFormat.MP3, AudioFormat.WAV);
55 private static final Set<Class<? extends AudioStream>> SUPPORTED_AUDIO_STREAMS = Set.of(AudioStream.class);
57 private AudioHTTPServer audioHTTPServer;
58 private ZonePlayerHandler handler;
59 private @Nullable String callbackUrl;
61 public SonosAudioSink(ZonePlayerHandler handler, AudioHTTPServer audioHTTPServer, @Nullable String callbackUrl) {
62 this.handler = handler;
63 this.audioHTTPServer = audioHTTPServer;
64 this.callbackUrl = callbackUrl;
68 public String getId() {
69 return handler.getThing().getUID().toString();
73 public @Nullable String getLabel(@Nullable Locale locale) {
74 return handler.getThing().getLabel();
78 public CompletableFuture<@Nullable Void> processAndComplete(@Nullable AudioStream audioStream) {
79 if (audioStream instanceof URLAudioStream) {
80 // Asynchronous handling for URLAudioStream
81 CompletableFuture<@Nullable Void> completableFuture = new CompletableFuture<@Nullable Void>();
83 processAsynchronously(audioStream);
84 } catch (UnsupportedAudioFormatException | UnsupportedAudioStreamException e) {
85 completableFuture.completeExceptionally(e);
87 return completableFuture;
89 return super.processAndComplete(audioStream);
94 public void process(@Nullable AudioStream audioStream)
95 throws UnsupportedAudioFormatException, UnsupportedAudioStreamException {
96 if (audioStream instanceof URLAudioStream) {
97 processAsynchronously(audioStream);
99 processSynchronously(audioStream);
103 private void processAsynchronously(@Nullable AudioStream audioStream)
104 throws UnsupportedAudioFormatException, UnsupportedAudioStreamException {
105 if (audioStream instanceof URLAudioStream urlAudioStream) {
106 // it is an external URL, the speaker can access it itself and play it.
107 handler.playURI(new StringType(urlAudioStream.getURL()));
110 } catch (IOException e) {
116 protected void processSynchronously(@Nullable AudioStream audioStream)
117 throws UnsupportedAudioFormatException, UnsupportedAudioStreamException {
118 if (audioStream instanceof URLAudioStream) {
122 if (audioStream == null) {
123 // in case the audioStream is null, this should be interpreted as a request to end any currently playing
125 logger.trace("Stop currently playing stream.");
126 handler.stopPlaying(OnOffType.ON);
130 // we serve it on our own HTTP server and treat it as a notification
131 // Note that Sonos does multiple concurrent requests to the AudioServlet,
132 // so a one time serving won't work.
133 if (callbackUrl != null) {
134 StreamServed streamServed;
136 streamServed = audioHTTPServer.serve(audioStream, 10, true);
137 } catch (IOException e) {
140 } catch (IOException ex) {
142 throw new UnsupportedAudioStreamException(
143 "Sonos was not able to handle the audio stream (cache on disk failed).", audioStream.getClass(),
146 String url = callbackUrl + streamServed.url();
148 AudioFormat format = audioStream.getFormat();
149 if (!ThingHandlerHelper.isHandlerInitialized(handler)) {
150 logger.warn("Sonos speaker '{}' is not initialized - status is {}", handler.getThing().getUID(),
151 handler.getThing().getStatus());
152 } else if (AudioFormat.WAV.isCompatible(format)) {
153 handler.playNotificationSoundURI(
154 new StringType(url + AudioStreamUtils.EXTENSION_SEPARATOR + FileAudioStream.WAV_EXTENSION));
155 } else if (AudioFormat.MP3.isCompatible(format)) {
156 handler.playNotificationSoundURI(
157 new StringType(url + AudioStreamUtils.EXTENSION_SEPARATOR + FileAudioStream.MP3_EXTENSION));
159 throw new UnsupportedAudioFormatException("Sonos only supports MP3 or WAV.", format);
162 logger.warn("We do not have any callback url, so Sonos cannot play the audio stream!");
165 } catch (IOException e) {
171 public Set<AudioFormat> getSupportedFormats() {
172 return SUPPORTED_AUDIO_FORMATS;
176 public Set<Class<? extends AudioStream>> getSupportedStreams() {
177 return SUPPORTED_AUDIO_STREAMS;
181 public PercentType getVolume() {
182 String volume = handler.getVolume();
183 return volume != null ? new PercentType(volume) : PercentType.ZERO;
187 public void setVolume(PercentType volume) {
188 handler.setVolume(volume);