]> git.basschouten.com Git - openhab-addons.git/blob
7050291643834c25b13f26f40f89faffbf6a6902
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.freebox.internal;
14
15 import static org.openhab.core.audio.AudioFormat.*;
16
17 import java.io.IOException;
18 import java.io.InputStream;
19 import java.util.HashSet;
20 import java.util.Locale;
21 import java.util.Set;
22
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.binding.freebox.internal.api.FreeboxException;
26 import org.openhab.binding.freebox.internal.config.FreeboxAirPlayDeviceConfiguration;
27 import org.openhab.binding.freebox.internal.handler.FreeboxThingHandler;
28 import org.openhab.core.audio.AudioFormat;
29 import org.openhab.core.audio.AudioHTTPServer;
30 import org.openhab.core.audio.AudioSink;
31 import org.openhab.core.audio.AudioSinkAsync;
32 import org.openhab.core.audio.AudioStream;
33 import org.openhab.core.audio.StreamServed;
34 import org.openhab.core.audio.URLAudioStream;
35 import org.openhab.core.audio.UnsupportedAudioFormatException;
36 import org.openhab.core.audio.UnsupportedAudioStreamException;
37 import org.openhab.core.library.types.PercentType;
38 import org.openhab.core.thing.ThingStatus;
39 import org.openhab.core.thing.ThingStatusDetail;
40 import org.openhab.core.thing.util.ThingHandlerHelper;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
43
44 /**
45  * This makes an AirPlay device to serve as an {@link AudioSink}-
46  *
47  * @author Laurent Garnier - Initial contribution for AudioSink and notifications
48  * @author Laurent Garnier - Support for more audio streams through the HTTP audio servlet
49  */
50 @NonNullByDefault
51 public class FreeboxAirPlayAudioSink extends AudioSinkAsync {
52
53     private final Logger logger = LoggerFactory.getLogger(FreeboxAirPlayAudioSink.class);
54
55     private static final AudioFormat MP3_96 = new AudioFormat(CONTAINER_NONE, CODEC_MP3, null, null, 96000, null);
56     private static final AudioFormat MP3_112 = new AudioFormat(CONTAINER_NONE, CODEC_MP3, null, null, 112000, null);
57     private static final AudioFormat MP3_128 = new AudioFormat(CONTAINER_NONE, CODEC_MP3, null, null, 128000, null);
58     private static final AudioFormat MP3_160 = new AudioFormat(CONTAINER_NONE, CODEC_MP3, null, null, 160000, null);
59     private static final AudioFormat MP3_192 = new AudioFormat(CONTAINER_NONE, CODEC_MP3, null, null, 192000, null);
60     private static final AudioFormat MP3_224 = new AudioFormat(CONTAINER_NONE, CODEC_MP3, null, null, 224000, null);
61     private static final AudioFormat MP3_256 = new AudioFormat(CONTAINER_NONE, CODEC_MP3, null, null, 256000, null);
62     private static final AudioFormat MP3_320 = new AudioFormat(CONTAINER_NONE, CODEC_MP3, null, null, 320000, null);
63
64     private static final Set<AudioFormat> SUPPORTED_FORMATS = new HashSet<>();
65     private static final Set<Class<? extends AudioStream>> SUPPORTED_STREAMS = Set.of(AudioStream.class);
66     private AudioHTTPServer audioHTTPServer;
67     private FreeboxThingHandler handler;
68     private @Nullable String callbackUrl;
69
70     public FreeboxAirPlayAudioSink(FreeboxThingHandler handler, AudioHTTPServer audioHTTPServer,
71             @Nullable String callbackUrl) {
72         this.handler = handler;
73         this.audioHTTPServer = audioHTTPServer;
74         this.callbackUrl = callbackUrl;
75         Boolean acceptLowBitrate = (Boolean) handler.getThing().getConfiguration()
76                 .get(FreeboxAirPlayDeviceConfiguration.ACCEPT_ALL_MP3);
77         SUPPORTED_FORMATS.add(WAV);
78         if (acceptLowBitrate) {
79             SUPPORTED_FORMATS.add(MP3);
80         } else {
81             // Only accept MP3 bitrates >= 96 kbps
82             SUPPORTED_FORMATS.add(MP3_96);
83             SUPPORTED_FORMATS.add(MP3_112);
84             SUPPORTED_FORMATS.add(MP3_128);
85             SUPPORTED_FORMATS.add(MP3_160);
86             SUPPORTED_FORMATS.add(MP3_192);
87             SUPPORTED_FORMATS.add(MP3_224);
88             SUPPORTED_FORMATS.add(MP3_256);
89             SUPPORTED_FORMATS.add(MP3_320);
90         }
91         // OGG seems to not be properly supported (tested with a file produced by VoiceRSS)
92         // SUPPORTED_FORMATS.add(OGG);
93     }
94
95     @Override
96     public String getId() {
97         return handler.getThing().getUID().toString();
98     }
99
100     @Override
101     public @Nullable String getLabel(@Nullable Locale locale) {
102         return handler.getThing().getLabel();
103     }
104
105     @Override
106     protected void processAsynchronously(@Nullable AudioStream audioStream)
107             throws UnsupportedAudioFormatException, UnsupportedAudioStreamException {
108         if (!ThingHandlerHelper.isHandlerInitialized(handler)
109                 || ((handler.getThing().getStatus() == ThingStatus.OFFLINE)
110                         && ((handler.getThing().getStatusInfo().getStatusDetail() == ThingStatusDetail.BRIDGE_OFFLINE)
111                                 || (handler.getThing().getStatusInfo()
112                                         .getStatusDetail() == ThingStatusDetail.CONFIGURATION_ERROR)))) {
113             tryClose(audioStream);
114             return;
115         }
116
117         if (audioStream == null) {
118             try {
119                 handler.stopMedia();
120             } catch (FreeboxException e) {
121                 logger.warn("Exception while stopping audio stream playback: {}", e.getMessage());
122             }
123             return;
124         }
125
126         String url;
127         if (audioStream instanceof URLAudioStream urlAudioStream) {
128             // it is an external URL, we can access it directly
129             url = urlAudioStream.getURL();
130             tryClose(audioStream);
131         } else if (callbackUrl != null) {
132             // we serve it on our own HTTP server
133             logger.debug("audioStream {} {}", audioStream.getClass().getSimpleName(), audioStream.getFormat());
134             StreamServed streamServed;
135             try {
136                 streamServed = audioHTTPServer.serve(audioStream, 5, true);
137             } catch (IOException e) {
138                 tryClose(audioStream);
139                 throw new UnsupportedAudioStreamException(
140                         "AirPlay device was not able to handle the audio stream (cache on disk failed).",
141                         audioStream.getClass(), e);
142             }
143             url = callbackUrl + streamServed.url();
144             streamServed.playEnd().thenRun(() -> {
145                 try {
146                     handler.stopMedia();
147                 } catch (FreeboxException e) {
148                     logger.warn("Exception while stopping audio stream playback: {}", e.getMessage());
149                 }
150                 this.playbackFinished(audioStream);
151             });
152         } else {
153             logger.warn("We do not have any callback url, so AirPlay device cannot play the audio stream!");
154             tryClose(audioStream);
155             return;
156         }
157         try {
158             logger.debug("AirPlay audio sink: process url {}", url);
159             handler.playMedia(url);
160         } catch (FreeboxException e) {
161             logger.warn("Audio stream playback failed: {}", e.getMessage());
162         }
163     }
164
165     private void tryClose(@Nullable InputStream is) {
166         if (is != null) {
167             try {
168                 is.close();
169             } catch (IOException ignored) {
170             }
171         }
172     }
173
174     @Override
175     public Set<AudioFormat> getSupportedFormats() {
176         return SUPPORTED_FORMATS;
177     }
178
179     @Override
180     public Set<Class<? extends AudioStream>> getSupportedStreams() {
181         return SUPPORTED_STREAMS;
182     }
183
184     @Override
185     public PercentType getVolume() {
186         throw new UnsupportedOperationException("Volume can not be determined");
187     }
188
189     @Override
190     public void setVolume(PercentType volume) {
191         throw new UnsupportedOperationException("Volume can not be set");
192     }
193 }