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.freeboxos.internal.handler;
15 import static org.openhab.core.audio.AudioFormat.*;
17 import java.io.IOException;
18 import java.io.InputStream;
19 import java.util.HashSet;
20 import java.util.Locale;
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.binding.freeboxos.internal.api.FreeboxException;
26 import org.openhab.binding.freeboxos.internal.api.rest.MediaReceiverManager;
27 import org.openhab.binding.freeboxos.internal.api.rest.MediaReceiverManager.Action;
28 import org.openhab.binding.freeboxos.internal.api.rest.MediaReceiverManager.MediaType;
29 import org.openhab.core.audio.AudioFormat;
30 import org.openhab.core.audio.AudioHTTPServer;
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.slf4j.Logger;
40 import org.slf4j.LoggerFactory;
43 * The {@link AirMediaSink} is holding AudioSink capabilities for various
46 * @author Gaƫl L'hopital - Initial contribution
49 public class AirMediaSink extends AudioSinkAsync {
50 private static final Set<Class<? extends AudioStream>> SUPPORTED_STREAMS = Set.of(AudioStream.class);
51 // OGG seems to not be properly supported (tested with a file produced by VoiceRSS)
52 private static final Set<AudioFormat> BASIC_FORMATS = Set.of(WAV/* , OGG */);
53 private static final Set<AudioFormat> ALL_MP3_FORMATS = Set.of(
54 new AudioFormat(CONTAINER_NONE, CODEC_MP3, null, null, 96000, null),
55 new AudioFormat(CONTAINER_NONE, CODEC_MP3, null, null, 112000, null),
56 new AudioFormat(CONTAINER_NONE, CODEC_MP3, null, null, 128000, null),
57 new AudioFormat(CONTAINER_NONE, CODEC_MP3, null, null, 160000, null),
58 new AudioFormat(CONTAINER_NONE, CODEC_MP3, null, null, 192000, null),
59 new AudioFormat(CONTAINER_NONE, CODEC_MP3, null, null, 224000, null),
60 new AudioFormat(CONTAINER_NONE, CODEC_MP3, null, null, 256000, null),
61 new AudioFormat(CONTAINER_NONE, CODEC_MP3, null, null, 320000, null));
63 private final Logger logger = LoggerFactory.getLogger(AirMediaSink.class);
64 private final ApiConsumerHandler thingHandler;
65 private final Set<AudioFormat> supportedFormats = new HashSet<>();
66 private final AudioHTTPServer audioHTTPServer;
67 private final String callbackUrl;
68 private final String playerName;
69 private final String password;
71 public AirMediaSink(ApiConsumerHandler thingHandler, AudioHTTPServer audioHTTPServer, String callbackUrl,
72 String playerName, String password, boolean acceptAllMp3) {
73 this.thingHandler = thingHandler;
74 this.audioHTTPServer = audioHTTPServer;
75 this.playerName = playerName;
76 this.callbackUrl = callbackUrl;
77 this.password = password;
79 supportedFormats.addAll(BASIC_FORMATS);
81 supportedFormats.addAll(ALL_MP3_FORMATS);
82 } else { // Only accept MP3 bitrates >= 96 kbps
83 supportedFormats.add(MP3);
88 public Set<Class<? extends AudioStream>> getSupportedStreams() {
89 return SUPPORTED_STREAMS;
93 public PercentType getVolume() throws IOException {
94 logger.debug("getVolume received but AirMedia does not have the capability - returning 100%.");
95 return PercentType.HUNDRED;
99 public void setVolume(PercentType volume) throws IOException {
100 logger.debug("setVolume received but AirMedia does not have the capability - ignoring it.");
104 public String getId() {
105 return thingHandler.getThing().getUID().toString();
109 public @Nullable String getLabel(@Nullable Locale locale) {
110 return thingHandler.getThing().getLabel();
114 protected void processAsynchronously(@Nullable AudioStream audioStream)
115 throws UnsupportedAudioFormatException, UnsupportedAudioStreamException {
116 if (thingHandler.getThing().getStatus() != ThingStatus.ONLINE) {
117 tryClose(audioStream);
121 if (audioStream == null) {
127 if (audioStream instanceof URLAudioStream urlAudioStream) {
128 // it is an external URL, we can access it directly
129 url = urlAudioStream.getURL();
130 tryClose(audioStream);
132 // we serve it on our own HTTP server
133 logger.debug("audioStream {} {}", audioStream.getClass().getSimpleName(), audioStream.getFormat());
134 StreamServed streamServed;
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);
143 url = callbackUrl + streamServed.url();
144 streamServed.playEnd().thenRun(() -> {
146 this.playbackFinished(audioStream);
149 logger.debug("AirPlay audio sink: process url {}", url);
153 private void tryClose(@Nullable InputStream is) {
157 } catch (IOException ignored) {
162 private void playMedia(String url) {
164 MediaReceiverManager manager = thingHandler.getManager(MediaReceiverManager.class);
165 manager.sendToReceiver(playerName, password, Action.STOP, MediaType.VIDEO);
166 manager.sendToReceiver(playerName, password, Action.START, MediaType.VIDEO, url);
167 } catch (FreeboxException e) {
168 logger.warn("Playing media failed: {}", e.getMessage());
172 private void stopMedia() {
174 MediaReceiverManager manager = thingHandler.getManager(MediaReceiverManager.class);
175 manager.sendToReceiver(playerName, password, Action.STOP, MediaType.VIDEO);
176 } catch (FreeboxException e) {
177 logger.warn("Stopping media failed: {}", e.getMessage());
182 public Set<AudioFormat> getSupportedFormats() {
183 return supportedFormats;