package org.deviceconnect.android.deviceplugin.sonycamera.utils;
import android.net.Uri;
import org.deviceconnect.android.deviceplugin.sonycamera.BuildConfig;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Logger;
/**
* Mixed Replace Media Server.
* @author NTT DOCOMO, INC.
*/
public class MixedReplaceMediaServer {
/** Logger. */
private Logger mLogger = Logger.getLogger("sonycamera.dplugin");
/**
* Max value of cache of media.
*/
private static final int MAX_MEDIA_CACHE = 2;
/**
* Max value of client.
*/
private static final int MAX_CLIENT_SIZE = 8;
/**
* Port of the Socket.
*/
private int mPort = -1;
/**
* The boundary of a multipart.
*/
private String mBoundary = UUID.randomUUID().toString();
/**
* path.
*/
private String mPath;
/**
* Content type.
* Default is "image/jpg".
*/
private String mContentType = "image/jpg";
/**
* Stop flag.
*/
private boolean mStopFlag;
/**
* Name of web server.
*/
private String mServerName = "DevicePlugin Server";
/**
* Server Socket.
*/
private ServerSocket mServerSocket;
/**
* Manage a thread.
*/
private final ExecutorService mExecutor = Executors.newFixedThreadPool(MAX_CLIENT_SIZE);
/**
* List a Server Runnable.
*/
private final List<ServerRunnable> mRunnables = Collections.synchronizedList(
new ArrayList<ServerRunnable>());
/**
* Sever event listener.
*/
private ServerEventListener mListener;
/**
* スリープ時間.
*/
private int mTimeSlice = 60;
/**
* Set a ServerEventListener.
* @param listener server event listener
*/
public void setServerEventListener(final ServerEventListener listener) {
mListener = listener;
}
/**
* Set a boundary.
* @param boundary boundary of a multipart
*/
public void setBoundary(final String boundary) {
if (boundary == null) {
throw new IllegalArgumentException("boundary is null.");
}
if (boundary.isEmpty()) {
throw new IllegalArgumentException("boundary is empty.");
}
mBoundary = boundary;
}
/**
* Get a boundary.
* @return boundary
*/
public String getBoundary() {
return mBoundary;
}
/**
* Set a content type.
* <p>
* Default is "image/jpg".
* </p>
* @param contentType content type
*/
public void setContentType(final String contentType) {
mContentType = contentType;
}
/**
* Get a content type.
* @return content type
*/
public String getContentType() {
return mContentType;
}
/**
* Set a port of web server.
* @param port port of a web server
*/
public void setPort(final int port) {
if (port < 1000) {
throw new IllegalArgumentException("Port is smaller than 1000.");
}
mPort = port;
}
/**
* Get a port of web server.
* @return port
*/
public int getPort() {
return mPort;
}
/**
* Set a name of server.
* @param name name of server
*/
public void setServerName(final String name) {
if (name == null) {
throw new IllegalArgumentException("name is null.");
}
mServerName = name;
}
/**
* Get a name of server.
* @return name of server
*/
public String getServerName() {
return mServerName;
}
/**
* Get a url of server.
* @return url
*/
public String getUrl() {
if (mServerSocket == null || mPath == null) {
return null;
}
return "http://localhost:" + mServerSocket.getLocalPort() + "/" + mPath;
}
/**
* Get a server running status.
* @return server status
*/
public synchronized boolean isRunning() {
return !mStopFlag;
}
/**
* Inserts the media data into queue.
* @param media media data
*/
public synchronized void offerMedia(final byte[] media) {
if (media == null) {
return;
}
if (!mStopFlag) {
synchronized (mRunnables) {
for (ServerRunnable run : mRunnables) {
run.offerMedia(media);
}
}
}
}
/**
* Set a fps.
* @param fps fps
*/
public void setFPS(final int fps) {
mTimeSlice = 1000 / fps;
}
/**
* Set a time slice.
* @param timeSlice time slice
*/
public void setTimeSlice(final int timeSlice) {
mTimeSlice = timeSlice;
}
/**
* Start a mixed replace media server.
* <p>
* If a port is not set, looking for a port that is not used between 9000 to 10000, set to server.
* </p>
* @return the local IP address of this server or {@code null} if this server cannot start.
*/
public synchronized String start() {
try {
mServerSocket = openServerSocket();
mLogger.fine("Open a server socket.");
} catch (IOException e) {
// Failed to open server socket
mStopFlag = true;
if (mListener != null) {
mListener.onError();
}
return null;
}
mPath = UUID.randomUUID().toString();
mStopFlag = false;
new Thread(new Runnable() {
@Override
public void run() {
try {
if (mListener != null) {
mListener.onStart();
}
while (!mStopFlag) {
ServerRunnable run = new ServerRunnable(mServerSocket.accept());
synchronized (MixedReplaceMediaServer.this) {
mExecutor.execute(run);
}
}
} catch (IOException e) {
mLogger.warning("Error server socket[" + mServerName + "]");
} finally {
stop();
if (mListener != null) {
mListener.onStop();
}
}
}
}).start();
return getUrl();
}
/**
* Open a server socket that looking for a port that can be used.
* @return ServerSocket
* @throws IOException if an error occurs while open socket.
*/
private ServerSocket openServerSocket() throws IOException {
if (mPort != -1) {
return new ServerSocket(mPort);
} else {
for (int i = 9000; i < 10000; i++) {
try {
return new ServerSocket(i);
} catch (IOException e) {
continue;
}
}
throw new IOException("Cannot open server socket.");
}
}
/**
* Stop a mixed replace media server.
*/
public synchronized void stop() {
if (mStopFlag) {
return;
}
mStopFlag = true;
mExecutor.shutdown();
synchronized (mRunnables) {
for (ServerRunnable run : mRunnables) {
run.offerMedia(new byte[0]);
}
}
if (mServerSocket != null) {
try {
mServerSocket.close();
mServerSocket = null;
} catch (IOException e) {
if (BuildConfig.DEBUG) {
e.printStackTrace();
}
}
}
mPath = null;
mLogger.fine("MixedReplaceMediaServer is stop.");
}
/**
* Interface of Sever event.
*/
public interface ServerEventListener {
/**
* Event that started a server.
*/
void onStart();
/**
* Event that stopped a server.
*/
void onStop();
/**
* Event that occurred a error on server.
*/
void onError();
}
/**
* Class of Server.
*/
private class ServerRunnable implements Runnable {
/**
* Defined buffer size.
*/
private static final int BUF_SIZE = 8192;
/**
* Socket.
*/
private Socket mSocket;
/**
* Stream for writing.
*/
private OutputStream mStream;
/**
* Queue that holds the media.
*/
private final BlockingQueue<byte[]> mMediaQueue = new ArrayBlockingQueue<>(MAX_MEDIA_CACHE);
/**
* Constructor.
* @param socket socket
*/
ServerRunnable(final Socket socket) {
mSocket = socket;
}
@Override
public void run() {
mLogger.fine("accept client.");
mRunnables.add(this);
try {
mStream = mSocket.getOutputStream();
byte[] buf = new byte[BUF_SIZE];
InputStream in = mSocket.getInputStream();
int len = in.read(buf, 0, BUF_SIZE);
if (len == -1) {
// error
return;
}
decodeHeader(buf, len);
if (mRunnables.size() > MAX_CLIENT_SIZE) {
mStream.write(generateServiceUnavailable().getBytes());
mStream.flush();
} else {
mStream.write(generateHttpHeader().getBytes());
mStream.flush();
while (!mStopFlag) {
long oldTime = System.currentTimeMillis();
byte[] media = mMediaQueue.take();
if (mSocket.isClosed()) {
break;
}
if (media.length > 0) {
sendMedia(media);
}
long newTime = System.currentTimeMillis();
long sleepTime = mTimeSlice - (newTime - oldTime);
if (sleepTime < 2) {
sleepTime = 2;
}
Thread.sleep(sleepTime);
}
}
} catch (InterruptedException e) {
if (mStream != null) {
try {
mStream.write(generateInternalServerError().getBytes());
mStream.flush();
} catch (IOException e1) {
mLogger.warning("Error server socket[" + mServerName + "]");
}
}
Thread.currentThread().interrupt();
} catch (Exception e) {
if (mStream != null) {
try {
mStream.write(generateBadRequest().getBytes());
mStream.flush();
} catch (IOException e1) {
mLogger.warning("Error server socket[" + mServerName + "]");
}
}
} finally {
mLogger.fine("socket close.");
if (mStream != null) {
try {
mStream.close();
} catch (IOException e) {
if (BuildConfig.DEBUG) {
e.printStackTrace();
}
}
}
try {
mSocket.close();
} catch (IOException e) {
if (BuildConfig.DEBUG) {
e.printStackTrace();
}
}
mRunnables.remove(this);
if (mRunnables.isEmpty()) {
stop();
}
}
}
/**
* Inserts the media data into queue.
* @param media the media to add
* @return true if the media data was added to this queue, else false
*/
private boolean offerMedia(final byte[] media) {
if (mMediaQueue.size() == MAX_MEDIA_CACHE) {
mMediaQueue.remove();
}
return mMediaQueue.offer(media);
}
/**
* Send a media data.
* @param media media data
* @throws IOException if an error occurs while sending media data.
*/
private void sendMedia(final byte[] media) throws IOException {
mStream.write("--".getBytes());
mStream.write(mBoundary.getBytes());
mStream.write("\r\n".getBytes());
mStream.write("Content-Type: ".getBytes());
mStream.write(mContentType.getBytes());
mStream.write("\r\n".getBytes());
mStream.write("Content-Length: ".getBytes());
mStream.write(String.valueOf(media.length).getBytes());
mStream.write("\r\n".getBytes());
mStream.write("\r\n".getBytes());
mStream.write(media);
mStream.write("\r\n\r\n".getBytes());
mStream.flush();
}
/**
* Decode a Http header.
* @param buf buffer of http header
* @param len buffer size
* @throws IOException if this http header is invalid.
*/
private void decodeHeader(final byte[] buf, final int len) throws IOException {
HashMap<String, String> pre = new HashMap<String, String>();
HashMap<String, String> headers = new HashMap<String, String>();
HashMap<String, String> params = new HashMap<String, String>();
BufferedReader in = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buf, 0, len)));
// Read the request line
String inLine = in.readLine();
if (inLine == null) {
throw new IOException("no headers.");
}
StringTokenizer st = new StringTokenizer(inLine);
if (!st.hasMoreTokens()) {
throw new IOException("Header is invalid format.");
}
String method = st.nextToken();
if (!method.toLowerCase(Locale.getDefault()).equals("get")) {
throw new IOException("Method is invalid.");
}
pre.put("method", method);
if (!st.hasMoreTokens()) {
throw new IOException("Header is invalid format.");
}
String uri = st.nextToken();
// Decode parameters from the URI
int qmi = uri.indexOf('?');
if (qmi >= 0) {
decodeParms(uri.substring(qmi + 1), params);
uri = decodePercent(uri.substring(0, qmi));
} else {
decodeParms(null, params);
uri = decodePercent(uri);
}
pre.put("uri", uri);
// If there's another token, it's protocol version,
// followed by HTTP headers. Ignore version but parse headers.
// NOTE: this now forces header names lowercase since they are
// case insensitive and vary by client.
if (st.hasMoreTokens()) {
String line = in.readLine();
while (line != null && line.trim().length() > 0) {
int p = line.indexOf(':');
if (p >= 0) {
headers.put(line.substring(0, p).trim().toLowerCase(Locale.US),
line.substring(p + 1).trim());
}
line = in.readLine();
}
}
String segment = Uri.parse(uri).getLastPathSegment();
if (segment == null || !segment.equals(mPath)) {
throw new IOException("Header is invalid format.");
}
}
/**
* Decode of uri param.
* @param parms uri
* @param p
*/
private void decodeParms(final String parms, final Map<String, String> p) {
if (parms == null) {
return;
}
StringTokenizer st = new StringTokenizer(parms, "&");
while (st.hasMoreTokens()) {
String e = st.nextToken();
int sep = e.indexOf('=');
if (sep >= 0) {
p.put(decodePercent(e.substring(0, sep)).trim(), decodePercent(e.substring(sep + 1)));
} else {
p.put(decodePercent(e).trim(), "");
}
}
}
/**
* Decode of uri.
* @param str uri
* @return The decoded URI
*/
private String decodePercent(final String str) {
try {
return URLDecoder.decode(str, "UTF8");
} catch (UnsupportedEncodingException ignored) {
return null;
}
}
}
/**
* Generate a http header.
* @return http header
*/
private String generateHttpHeader() {
return "HTTP/1.0 200 OK\r\n" +
"Server: " + mServerName + "\r\n" +
"Connection: close\r\n" +
"Max-Age: 0\r\n" +
"Expires: 0\r\n" +
"Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" +
"Pragma: no-cache\r\n" +
"Content-Type: multipart/x-mixed-replace; " +
"boundary=" + mBoundary + "\r\n" +
"\r\n" +
"--" + mBoundary + "\r\n";
}
/**
* Generate a Bad Request.
* @return Bad Request
*/
private String generateBadRequest() {
return generateErrorHeader("400");
}
/**
* Generate a Internal Serve rError.
* @return Internal Server Error
*/
private String generateInternalServerError() {
return generateErrorHeader("500");
}
/**
* Generate a Service Unavailable.
* @return Service Unavailable
*/
private String generateServiceUnavailable() {
return generateErrorHeader("503");
}
/**
* Generate a error http header.
* @param status status
* @return http header
*/
private String generateErrorHeader(final String status) {
return ("HTTP/1.0 " + status + " OK\r\n") +
"Server: " + mServerName + "\r\n" +
"Connection: close\r\n" +
"\r\n";
}
}