/**
* Copyright (c) 2014-2017 by the respective copyright holders.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.smarthome.core.audio.internal;
import java.io.IOException;
import java.io.InputStream;
import java.util.Hashtable;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.eclipse.smarthome.core.audio.AudioException;
import org.eclipse.smarthome.core.audio.AudioFormat;
import org.eclipse.smarthome.core.audio.AudioHTTPServer;
import org.eclipse.smarthome.core.audio.AudioStream;
import org.eclipse.smarthome.core.audio.FixedLengthAudioStream;
import org.osgi.service.http.HttpContext;
import org.osgi.service.http.HttpService;
import org.osgi.service.http.NamespaceException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A servlet that serves audio streams via HTTP.
*
* @author Kai Kreuzer - Initial contribution and API
*
*/
public class AudioServlet extends HttpServlet implements AudioHTTPServer {
private static final long serialVersionUID = -3364664035854567854L;
private static final String SERVLET_NAME = "/audio";
private final Logger logger = LoggerFactory.getLogger(AudioServlet.class);
private Map<String, AudioStream> oneTimeStreams = new ConcurrentHashMap<>();
private Map<String, FixedLengthAudioStream> multiTimeStreams = new ConcurrentHashMap<>();
private Map<String, Long> streamTimeouts = new ConcurrentHashMap<>();
protected HttpService httpService;
protected void setHttpService(HttpService httpService) {
this.httpService = httpService;
try {
logger.debug("Starting up the audio servlet at " + SERVLET_NAME);
Hashtable<String, String> props = new Hashtable<String, String>();
httpService.registerServlet(SERVLET_NAME, this, props, createHttpContext());
} catch (NamespaceException e) {
logger.error("Error during servlet startup", e);
} catch (ServletException e) {
logger.error("Error during servlet startup", e);
}
}
protected void unsetHttpService(HttpService httpService) {
httpService.unregister(SERVLET_NAME);
this.httpService = null;
}
/**
* Creates an {@link HttpContext}.
*
* @return an {@link HttpContext} that grants anonymous access
*/
protected HttpContext createHttpContext() {
// TODO: Once we have a role-based permission system in place, we need to make sure that we create an
// HttpContext here, which allows accessing the servlet without any authentication.
HttpContext httpContext = httpService.createDefaultHttpContext();
return httpContext;
}
private InputStream prepareInputStream(final String streamId, final HttpServletResponse resp)
throws AudioException {
final AudioStream stream;
final boolean multiAccess;
if (oneTimeStreams.containsKey(streamId)) {
stream = oneTimeStreams.remove(streamId);
multiAccess = false;
} else if (multiTimeStreams.containsKey(streamId)) {
stream = multiTimeStreams.get(streamId);
multiAccess = true;
} else {
return null;
}
logger.debug("Stream to serve is {}", streamId);
// try to set the content-type, if possible
final String mimeType;
if (stream.getFormat().getCodec() == AudioFormat.CODEC_MP3) {
mimeType = "audio/mpeg";
} else if (stream.getFormat().getContainer() == AudioFormat.CONTAINER_WAVE) {
mimeType = "audio/wav";
} else if (stream.getFormat().getContainer() == AudioFormat.CONTAINER_OGG) {
mimeType = "audio/ogg";
} else {
mimeType = null;
}
if (mimeType != null) {
resp.setContentType(mimeType);
}
// try to set the content-length, if possible
if (stream instanceof FixedLengthAudioStream) {
final Long size = ((FixedLengthAudioStream) stream).length();
if (size != null) {
resp.setContentLength(size.intValue());
}
}
if (multiAccess) {
// we need to care about concurrent access and have a separate stream for each thread
return ((FixedLengthAudioStream) stream).getClonedStream();
} else {
return stream;
}
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
removeTimedOutStreams();
final String streamId = StringUtils.substringBefore(StringUtils.substringAfterLast(req.getRequestURI(), "/"),
".");
try (final InputStream stream = prepareInputStream(streamId, resp)) {
if (stream == null) {
logger.debug("Received request for invalid stream id at {}", req.getRequestURI());
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
} else {
IOUtils.copy(stream, resp.getOutputStream());
resp.flushBuffer();
}
} catch (final AudioException ex) {
resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex.getMessage());
}
}
private synchronized void removeTimedOutStreams() {
for (String streamId : multiTimeStreams.keySet()) {
if (streamTimeouts.get(streamId) < System.nanoTime()) {
// the stream has expired, we need to remove it!
FixedLengthAudioStream stream = multiTimeStreams.remove(streamId);
streamTimeouts.remove(streamId);
IOUtils.closeQuietly(stream);
stream = null;
logger.debug("Removed timed out stream {}", streamId);
}
}
}
@Override
public String serve(AudioStream stream) {
String streamId = UUID.randomUUID().toString();
oneTimeStreams.put(streamId, stream);
return getRelativeURL(streamId);
}
@Override
public String serve(FixedLengthAudioStream stream, int seconds) {
String streamId = UUID.randomUUID().toString();
multiTimeStreams.put(streamId, stream);
streamTimeouts.put(streamId, System.nanoTime() + TimeUnit.SECONDS.toNanos(seconds));
return getRelativeURL(streamId);
}
private String getRelativeURL(String streamId) {
return SERVLET_NAME + "/" + streamId;
}
}