2 * Copyright (c) 2010-2022 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.Collections;
17 import java.util.Locale;
19 import java.util.stream.Collectors;
20 import java.util.stream.Stream;
22 import org.eclipse.jdt.annotation.NonNullByDefault;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.openhab.binding.sonos.internal.handler.ZonePlayerHandler;
25 import org.openhab.core.audio.AudioFormat;
26 import org.openhab.core.audio.AudioHTTPServer;
27 import org.openhab.core.audio.AudioSink;
28 import org.openhab.core.audio.AudioStream;
29 import org.openhab.core.audio.FileAudioStream;
30 import org.openhab.core.audio.FixedLengthAudioStream;
31 import org.openhab.core.audio.URLAudioStream;
32 import org.openhab.core.audio.UnsupportedAudioFormatException;
33 import org.openhab.core.audio.UnsupportedAudioStreamException;
34 import org.openhab.core.audio.utils.AudioStreamUtils;
35 import org.openhab.core.library.types.OnOffType;
36 import org.openhab.core.library.types.PercentType;
37 import org.openhab.core.library.types.StringType;
38 import org.openhab.core.thing.util.ThingHandlerHelper;
39 import org.slf4j.Logger;
40 import org.slf4j.LoggerFactory;
43 * This makes a Sonos speaker to serve as an {@link AudioSink}-
45 * @author Kai Kreuzer - Initial contribution and API
46 * @author Christoph Weitkamp - Added getSupportedStreams() and UnsupportedAudioStreamException
50 public class SonosAudioSink implements AudioSink {
52 private final Logger logger = LoggerFactory.getLogger(SonosAudioSink.class);
54 private static final Set<AudioFormat> SUPPORTED_AUDIO_FORMATS = Collections
55 .unmodifiableSet(Stream.of(AudioFormat.MP3, AudioFormat.WAV).collect(Collectors.toSet()));
56 private static final Set<Class<? extends AudioStream>> SUPPORTED_AUDIO_STREAMS = Collections
57 .unmodifiableSet(Stream.of(FixedLengthAudioStream.class, URLAudioStream.class).collect(Collectors.toSet()));
59 private AudioHTTPServer audioHTTPServer;
60 private ZonePlayerHandler handler;
61 private @Nullable String callbackUrl;
63 public SonosAudioSink(ZonePlayerHandler handler, AudioHTTPServer audioHTTPServer, @Nullable String callbackUrl) {
64 this.handler = handler;
65 this.audioHTTPServer = audioHTTPServer;
66 this.callbackUrl = callbackUrl;
70 public String getId() {
71 return handler.getThing().getUID().toString();
75 public @Nullable String getLabel(@Nullable Locale locale) {
76 return handler.getThing().getLabel();
80 public void process(@Nullable AudioStream audioStream)
81 throws UnsupportedAudioFormatException, UnsupportedAudioStreamException {
82 if (audioStream == null) {
83 // in case the audioStream is null, this should be interpreted as a request to end any currently playing
85 logger.trace("Stop currently playing stream.");
86 handler.stopPlaying(OnOffType.ON);
87 } else if (audioStream instanceof URLAudioStream) {
88 // it is an external URL, the speaker can access it itself and play it.
89 URLAudioStream urlAudioStream = (URLAudioStream) audioStream;
90 handler.playURI(new StringType(urlAudioStream.getURL()));
93 } catch (IOException e) {
95 } else if (audioStream instanceof FixedLengthAudioStream) {
96 // we serve it on our own HTTP server and treat it as a notification
97 // Note that we have to pass a FixedLengthAudioStream, since Sonos does multiple concurrent requests to
98 // the AudioServlet, so a one time serving won't work.
99 if (callbackUrl != null) {
100 String relativeUrl = audioHTTPServer.serve((FixedLengthAudioStream) audioStream, 10).toString();
101 String url = callbackUrl + relativeUrl;
103 AudioFormat format = audioStream.getFormat();
104 if (!ThingHandlerHelper.isHandlerInitialized(handler)) {
105 logger.warn("Sonos speaker '{}' is not initialized - status is {}", handler.getThing().getUID(),
106 handler.getThing().getStatus());
107 } else if (AudioFormat.WAV.isCompatible(format)) {
108 handler.playNotificationSoundURI(
109 new StringType(url + AudioStreamUtils.EXTENSION_SEPARATOR + FileAudioStream.WAV_EXTENSION));
110 } else if (AudioFormat.MP3.isCompatible(format)) {
111 handler.playNotificationSoundURI(
112 new StringType(url + AudioStreamUtils.EXTENSION_SEPARATOR + FileAudioStream.MP3_EXTENSION));
114 throw new UnsupportedAudioFormatException("Sonos only supports MP3 or WAV.", format);
117 logger.warn("We do not have any callback url, so Sonos cannot play the audio stream!");
122 } catch (IOException e) {
124 throw new UnsupportedAudioStreamException(
125 "Sonos can only handle FixedLengthAudioStreams and URLAudioStreams.", audioStream.getClass());
126 // Instead of throwing an exception, we could ourselves try to wrap it into a
127 // FixedLengthAudioStream, but this might be dangerous as we have no clue, how much data to expect from
133 public Set<AudioFormat> getSupportedFormats() {
134 return SUPPORTED_AUDIO_FORMATS;
138 public Set<Class<? extends AudioStream>> getSupportedStreams() {
139 return SUPPORTED_AUDIO_STREAMS;
143 public PercentType getVolume() {
144 String volume = handler.getVolume();
145 return volume != null ? new PercentType(volume) : PercentType.ZERO;
149 public void setVolume(PercentType volume) {
150 handler.setVolume(volume);