Skip to content
This repository has been archived by the owner on May 7, 2020. It is now read-only.

Introduction of an AudioServlet to provide audio streams through HTTP #2287

Merged
merged 2 commits into from
Oct 10, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ Bundle-Version: 0.9.0.qualifier
Bundle-Vendor: Eclipse.org/SmartHome
Bundle-RequiredExecutionEnvironment: JavaSE-1.7
Bundle-ClassPath: .
Import-Package: org.apache.commons.io,
Import-Package: javax.servlet,
javax.servlet.http,
org.apache.commons.io,
org.apache.commons.lang,
org.eclipse.smarthome.config.core,
org.eclipse.smarthome.core.audio,
Expand All @@ -20,6 +22,7 @@ Import-Package: org.apache.commons.io,
org.eclipse.smarthome.io.console,
org.eclipse.smarthome.io.console.extensions,
org.osgi.framework,
org.osgi.service.http,
org.slf4j
Export-Package: org.eclipse.smarthome.core.audio
Service-Component: OSGI-INF/*.xml
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--

Copyright (c) 2014-2016 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

-->
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0"
name="org.eclipse.smarthome.core.audio.audioservlet">
<implementation
class="org.eclipse.smarthome.core.audio.internal.AudioServlet" />
<reference interface="org.osgi.service.http.HttpService"
cardinality="1..1" policy="static" name="HttpService" bind="setHttpService"
unbind="unsetHttpService" />
<service>
<provide interface="org.eclipse.smarthome.core.audio.AudioHTTPServer"/>
</service>
</scr:component>
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* Copyright (c) 2014-2016 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;

import java.net.URL;

import org.eclipse.smarthome.core.audio.internal.AudioServlet;

/**
* This is an interface that is implemented by {@link AudioServlet} and which allows exposing audio streams through
* HTTP.
* Streams are only served a single time and then discarded.
*
* @author Kai Kreuzer - Initial contribution and API
*
*/
public interface AudioHTTPServer {

/**
* Creates a url for a given {@link AudioStream} where it can be requested a single time.
*
* @param stream the stream to serve on HTTP
* @return the absolute URL to access the stream (using the primary network interface)
*/
URL serve(AudioStream stream);

/**
* Creates a url for a given {@link AudioStream} where it can be requested a single time.
* This method makes sure that the HTTP response contains the "Content-Length" header as some clients require this.
* Note that this should only be used if really needed, since it might mean that the whole stream has to be read
* locally first in order to determine its length.
*
* @param stream the stream to serve on HTTP
* @return the absolute URL to access the stream (using the primary network interface)
*/
URL serveWithSize(AudioStream stream);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
/**
* Copyright (c) 2014-2016 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.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.Hashtable;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
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.AudioFormat;
import org.eclipse.smarthome.core.audio.AudioHTTPServer;
import org.eclipse.smarthome.core.audio.AudioStream;
import org.osgi.framework.BundleContext;
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> streams = new ConcurrentHashMap<>();

protected HttpService httpService;
private BundleContext bundleContext;

protected void activate(BundleContext bundleContext) {
this.bundleContext = bundleContext;
}

protected void deactivate(BundleContext bundleContext) {
this.bundleContext = null;
}

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;
}

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String streamId = StringUtils.substringAfterLast(req.getRequestURI(), "/");
if (!streams.containsKey(streamId)) {
logger.debug("Received request for invalid stream id at {}", req.getRequestURI());
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
} else {
logger.debug("Stream to serve is {}", streamId);

AudioStream stream = streams.remove(streamId);

// try to set the content-type, if possible
String mimeType = null;
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";
}
if (mimeType != null) {
resp.setContentType(mimeType);
}

// try to set the content-length, if possible
if (stream instanceof DiscreteAudioStream) {
Integer size = ((DiscreteAudioStream) stream).size();
if (size != null) {
resp.setContentLength(size);
}
}

ServletOutputStream os = resp.getOutputStream();
IOUtils.copy(stream, os);
resp.flushBuffer();
IOUtils.closeQuietly(stream);
}
}

@Override
public URL serve(AudioStream stream) {
String streamId = UUID.randomUUID().toString();
streams.put(streamId, stream);
return getURL(streamId);
}

@Override
public URL serveWithSize(AudioStream stream) {
if (stream.markSupported()) {
DiscreteAudioStream streamWithSize = new DiscreteAudioStream(stream);
return serve(streamWithSize);
} else {
// TODO: We should also support streams without mark support, but this will mean that we have to read it
// first to memory or file system and create a new AudioStream from there.
logger.warn("Stream cannot be reset, so it is served without size through HTTP");
return serve(stream);
}
}

private URL getURL(String streamId) {
try {
String ipAddress = InetAddress.getLocalHost().getHostAddress(); // we use the primary interface; if a client
// knows it any better, he can himself
// change the url according to his needs.
String port = bundleContext.getProperty("org.osgi.service.http.port"); // we do not use SSL as it can cause
// certificate validation issues.
return new URL("http://" + ipAddress + ":" + port + SERVLET_NAME + "/" + streamId);
} catch (UnknownHostException | MalformedURLException e) {
logger.error("Failed to construct audio stream URL: {}", e.getMessage(), e);
return null;
}
}

/**
* This is a wrapper class around {@link AudioStream}, which additionally provides information about its size.
* Currently, it only support mark-supporting AudioStreams, which are read and reset in order to dertermine the
* stream size.
*/
class DiscreteAudioStream extends AudioStream {

private Integer size;
private AudioStream stream;

public DiscreteAudioStream(AudioStream stream) {
this.stream = stream;
}

public Integer size() {
if (size == null) {
size = calculateSize();
}
return size;
}

private Integer calculateSize() {
if (stream.markSupported()) {
return readStreamAndReset(stream);
}
return null;
}

private Integer readStreamAndReset(AudioStream s) {
int bytes = 0;
int avail = 0;
try {
while ((avail = s.available()) > 0) {
bytes += avail;
s.skip(avail);
}
s.reset();
} catch (IOException e) {
logger.warn("Cannot determine size of audio stream!", e);
return null;
}
return bytes;
}

@Override
public AudioFormat getFormat() {
return stream.getFormat();
}

@Override
public int read() throws IOException {
return stream.read();
}

@Override
public void close() throws IOException {
stream.close();
}

}
}