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

Commit

Permalink
Introduction of an AudioServlet to provide audio streams through HTTP (
Browse files Browse the repository at this point in the history
…#2287)

* Introduction of an AudioServlet to provide audio streams through HTTP

Signed-off-by: Kai Kreuzer <kai@openhab.org>

* fixed missing stream removal

Signed-off-by: Kai Kreuzer <kai@openhab.org>
  • Loading branch information
kaikreuzer authored and maggu2810 committed Oct 10, 2016
1 parent eba2c09 commit 52ebd12
Show file tree
Hide file tree
Showing 4 changed files with 297 additions and 1 deletion.
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();
}

}
}

0 comments on commit 52ebd12

Please sign in to comment.