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.freebox.internal;
15 import static org.openhab.core.audio.AudioFormat.*;
17 import java.io.IOException;
18 import java.util.HashSet;
19 import java.util.Locale;
22 import org.eclipse.jdt.annotation.NonNullByDefault;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.openhab.binding.freebox.internal.api.FreeboxException;
25 import org.openhab.binding.freebox.internal.config.FreeboxAirPlayDeviceConfiguration;
26 import org.openhab.binding.freebox.internal.handler.FreeboxThingHandler;
27 import org.openhab.core.audio.AudioFormat;
28 import org.openhab.core.audio.AudioHTTPServer;
29 import org.openhab.core.audio.AudioSink;
30 import org.openhab.core.audio.AudioStream;
31 import org.openhab.core.audio.FixedLengthAudioStream;
32 import org.openhab.core.audio.URLAudioStream;
33 import org.openhab.core.audio.UnsupportedAudioFormatException;
34 import org.openhab.core.audio.UnsupportedAudioStreamException;
35 import org.openhab.core.library.types.PercentType;
36 import org.openhab.core.thing.ThingStatus;
37 import org.openhab.core.thing.ThingStatusDetail;
38 import org.openhab.core.thing.util.ThingHandlerHelper;
39 import org.slf4j.Logger;
40 import org.slf4j.LoggerFactory;
43 * This makes an AirPlay device to serve as an {@link AudioSink}-
45 * @author Laurent Garnier - Initial contribution for AudioSink and notifications
48 public class FreeboxAirPlayAudioSink implements AudioSink {
50 private final Logger logger = LoggerFactory.getLogger(FreeboxAirPlayAudioSink.class);
52 private static final AudioFormat MP3_96 = new AudioFormat(CONTAINER_NONE, CODEC_MP3, null, null, 96000, null);
53 private static final AudioFormat MP3_112 = new AudioFormat(CONTAINER_NONE, CODEC_MP3, null, null, 112000, null);
54 private static final AudioFormat MP3_128 = new AudioFormat(CONTAINER_NONE, CODEC_MP3, null, null, 128000, null);
55 private static final AudioFormat MP3_160 = new AudioFormat(CONTAINER_NONE, CODEC_MP3, null, null, 160000, null);
56 private static final AudioFormat MP3_192 = new AudioFormat(CONTAINER_NONE, CODEC_MP3, null, null, 192000, null);
57 private static final AudioFormat MP3_224 = new AudioFormat(CONTAINER_NONE, CODEC_MP3, null, null, 224000, null);
58 private static final AudioFormat MP3_256 = new AudioFormat(CONTAINER_NONE, CODEC_MP3, null, null, 256000, null);
59 private static final AudioFormat MP3_320 = new AudioFormat(CONTAINER_NONE, CODEC_MP3, null, null, 320000, null);
61 private static final Set<AudioFormat> SUPPORTED_FORMATS = new HashSet<>();
62 private static final HashSet<Class<? extends AudioStream>> SUPPORTED_STREAMS = new HashSet<>();
63 private AudioHTTPServer audioHTTPServer;
64 private FreeboxThingHandler handler;
65 private @Nullable String callbackUrl;
68 SUPPORTED_STREAMS.add(AudioStream.class);
71 public FreeboxAirPlayAudioSink(FreeboxThingHandler handler, AudioHTTPServer audioHTTPServer,
72 @Nullable String callbackUrl) {
73 this.handler = handler;
74 this.audioHTTPServer = audioHTTPServer;
75 this.callbackUrl = callbackUrl;
76 Boolean acceptLowBitrate = (Boolean) handler.getThing().getConfiguration()
77 .get(FreeboxAirPlayDeviceConfiguration.ACCEPT_ALL_MP3);
78 SUPPORTED_FORMATS.add(WAV);
79 if (acceptLowBitrate) {
80 SUPPORTED_FORMATS.add(MP3);
82 // Only accept MP3 bitrates >= 96 kbps
83 SUPPORTED_FORMATS.add(MP3_96);
84 SUPPORTED_FORMATS.add(MP3_112);
85 SUPPORTED_FORMATS.add(MP3_128);
86 SUPPORTED_FORMATS.add(MP3_160);
87 SUPPORTED_FORMATS.add(MP3_192);
88 SUPPORTED_FORMATS.add(MP3_224);
89 SUPPORTED_FORMATS.add(MP3_256);
90 SUPPORTED_FORMATS.add(MP3_320);
92 SUPPORTED_FORMATS.add(OGG);
96 public String getId() {
97 return handler.getThing().getUID().toString();
101 public @Nullable String getLabel(@Nullable Locale locale) {
102 return handler.getThing().getLabel();
106 public void process(@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)))) {
116 if (audioStream == null) {
119 } catch (FreeboxException e) {
120 logger.warn("Exception while stopping audio stream playback: {}", e.getMessage());
126 if (audioStream instanceof URLAudioStream) {
127 // it is an external URL, we can access it directly
128 URLAudioStream urlAudioStream = (URLAudioStream) audioStream;
129 url = urlAudioStream.getURL();
131 if (callbackUrl != null) {
132 // we serve it on our own HTTP server
134 if (audioStream instanceof FixedLengthAudioStream) {
135 relativeUrl = audioHTTPServer.serve((FixedLengthAudioStream) audioStream, 20);
137 relativeUrl = audioHTTPServer.serve(audioStream);
139 url = callbackUrl + relativeUrl;
141 logger.warn("We do not have any callback url, so AirPlay device cannot play the audio stream!");
146 } catch (IOException e) {
147 logger.debug("Exception while closing audioStream", e);
150 logger.debug("AirPlay audio sink: process url {}", url);
151 handler.playMedia(url);
152 } catch (FreeboxException e) {
153 logger.warn("Audio stream playback failed: {}", e.getMessage());
158 public Set<AudioFormat> getSupportedFormats() {
159 return SUPPORTED_FORMATS;
163 public Set<Class<? extends AudioStream>> getSupportedStreams() {
164 return SUPPORTED_STREAMS;
168 public PercentType getVolume() {
169 throw new UnsupportedOperationException("Volume can not be determined");
173 public void setVolume(PercentType volume) {
174 throw new UnsupportedOperationException("Volume can not be set");