content: /*
   * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com
   * 
   * This file is part of libstreaming (https://github.com/fyhertz/libstreaming)
   * 
   * Spydroid is free software; you can redistribute it and/or modify
   * it under the terms of the GNU General Public License as published by
   * the Free Software Foundation; either version 3 of the License, or
   * (at your option) any later version.
   * 
   * This source code is distributed in the hope that it will be useful,
   * but WITHOUT ANY WARRANTY; without even the implied warranty of
   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   * GNU General Public License for more details.
   * 
   * You should have received a copy of the GNU General Public License
   * along with this source code; if not, write to the Free Software
   * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
   */
  
  package net.majorkernelpanic.streaming.rtsp;
  
  import java.io.BufferedReader;
  import java.io.IOException;
  import java.io.InputStreamReader;
  import java.io.OutputStream;
  import java.net.BindException;
  import java.net.InetAddress;
  import java.net.ServerSocket;
  import java.net.Socket;
  import java.net.SocketException;
  import java.util.HashMap;
  import java.util.LinkedList;
  import java.util.Locale;
  import java.util.WeakHashMap;
  import java.util.regex.Matcher;
  import java.util.regex.Pattern;
  
  import net.majorkernelpanic.streaming.Session;
  import net.majorkernelpanic.streaming.SessionBuilder;
  import android.app.Service;
  import android.content.Intent;
  import android.content.SharedPreferences;
  import android.content.SharedPreferences.Editor;
  import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
  import android.os.Binder;
  import android.os.IBinder;
  import android.preference.PreferenceManager;
  import android.util.Log;
  
  /**
   * Implementation of a subset of the RTSP protocol (RFC 2326).
   * 
   * It allows remote control of an android device cameras & microphone.
   * For each connected client, a Session is instantiated.
   * The Session will start or stop streams according to what the client wants.
   * 
   */
  public class RtspServer extends Service {
  
  	public final static String TAG = "RtspServer";
  
  	/** The server name that will appear in responses. */
  	public static String SERVER_NAME = "MajorKernelPanic RTSP Server";
  
  	/** Port used by default. */
  	public static final int DEFAULT_RTSP_PORT = 8086;
  
  	/** Port already in use. */
  	public final static int ERROR_BIND_FAILED = 0x00;
  
  	/** A stream could not be started. */
  	public final static int ERROR_START_FAILED = 0x01;
  
  	/** Streaming started. */
  	public final static int MESSAGE_STREAMING_STARTED = 0X00;
  	
  	/** Streaming stopped. */
  	public final static int MESSAGE_STREAMING_STOPPED = 0X01;
  	
  	/** Key used in the SharedPreferences to store whether the RTSP server is enabled or not. */
  	public final static String KEY_ENABLED = "rtsp_enabled";
  
  	/** Key used in the SharedPreferences for the port used by the RTSP server. */
  	public final static String KEY_PORT = "rtsp_port";
  
  	protected SessionBuilder mSessionBuilder;
  	protected SharedPreferences mSharedPreferences;
  	protected boolean mEnabled = true;	
  	protected int mPort = DEFAULT_RTSP_PORT;
  	protected WeakHashMap<Session,Object> mSessions = new WeakHashMap<Session,Object>(2);
  	
  	private RequestListener mListenerThread;
  	private final IBinder mBinder = new LocalBinder();
  	private boolean mRestart = false;
  	private final LinkedList<CallbackListener> mListeners = new LinkedList<CallbackListener>();
  	
  
  	public RtspServer() {
  	}
  
  	/** Be careful: those callbacks won't necessarily be called from the ui thread ! */
  	public interface CallbackListener {
  
  		/** Called when an error occurs. */
  		void onError(RtspServer server, Exception e, int error);
  
  		/** Called when streaming starts/stops. */
  		void onMessage(RtspServer server, int message);
  		
  	}
  
  	/**
  	 * See {@link RtspServer.CallbackListener} to check out what events will be fired once you set up a listener.
  	 * @param listener The listener
  	 */
  	public void addCallbackListener(CallbackListener listener) {
  		synchronized (mListeners) {
  			if (mListeners.size() > 0) {
  				for (CallbackListener cl : mListeners) {
  					if (cl == listener) return;
  				}
  			}
  			mListeners.add(listener);			
  		}
  	}
  
  	/**
  	 * Removes the listener.
  	 * @param listener The listener
  	 */
  	public void removeCallbackListener(CallbackListener listener) {
  		synchronized (mListeners) {
  			mListeners.remove(listener);				
  		}
  	}
  
  	/** Returns the port used by the RTSP server. */	
  	public int getPort() {
  		return mPort;
  	}
  
  	/**
  	 * Sets the port for the RTSP server to use.
  	 * @param port The port
  	 */
  	public void setPort(int port) {
  		Editor editor = mSharedPreferences.edit();
  		editor.putString(KEY_PORT, String.valueOf(port));
  		editor.commit();
  	}	
  
  	/** 
  	 * Starts (or restart if needed, if for example the configuration 
  	 * of the server has been modified) the RTSP server. 
  	 */
  	public void start() {
  		if (!mEnabled || mRestart) stop();
  		if (mEnabled && mListenerThread == null) {
  			try {
  				mListenerThread = new RequestListener();
  			} catch (Exception e) {
  				mListenerThread = null;
  			}
  		}
  		mRestart = false;
  	}
  
  	/** 
  	 * Stops the RTSP server but not the Android Service. 
  	 * To stop the Android Service you need to call {@link android.content.Context#stopService(Intent)}; 
  	 */
  	public void stop() {
  		if (mListenerThread != null) {
  			try {
  				mListenerThread.kill();
  				for ( Session session : mSessions.keySet() ) {
  				    if ( session != null ) {
  				    	if (session.isStreaming()) session.stop();
  				    } 
  				}
  			} catch (Exception e) {
  			} finally {
  				mListenerThread = null;
  			}
  		}
  	}
  
  	/** Returns whether or not the RTSP server is streaming to some client(s). */
  	public boolean isStreaming() {
  		for ( Session session : mSessions.keySet() ) {
  		    if ( session != null ) {
  		    	if (session.isStreaming()) return true;
  		    } 
  		}
  		return false;
  	}
  	
  	public boolean isEnabled() {
  		return mEnabled;
  	}
  
  	/** Returns the bandwidth consumed by the RTSP server in bits per second. */
  	public long getBitrate() {
  		long bitrate = 0;
  		for ( Session session : mSessions.keySet() ) {
  		    if ( session != null ) {
  		    	if (session.isStreaming()) bitrate += session.getBitrate();
  		    } 
  		}
  		return bitrate;
  	}
  	
  	@Override
  	public int onStartCommand(Intent intent, int flags, int startId) {
  		return START_STICKY;
  	}
  
  	@Override
  	public void onCreate() {
  
  		// Let's restore the state of the service 
  		mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
  		mPort = Integer.parseInt(mSharedPreferences.getString(KEY_PORT, String.valueOf(mPort)));
  		mEnabled = mSharedPreferences.getBoolean(KEY_ENABLED, mEnabled);
  
  		// If the configuration is modified, the server will adjust
  		mSharedPreferences.registerOnSharedPreferenceChangeListener(mOnSharedPreferenceChangeListener);
  
  		start();
  	}
  
  	@Override
  	public void onDestroy() {
  		stop();
  		mSharedPreferences.unregisterOnSharedPreferenceChangeListener(mOnSharedPreferenceChangeListener);
  	}
  
  	private OnSharedPreferenceChangeListener mOnSharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
  		@Override
  		public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
  
  			if (key.equals(KEY_PORT)) {
  				int port = Integer.parseInt(sharedPreferences.getString(KEY_PORT, String.valueOf(mPort)));
  				if (port != mPort) {
  					mPort = port;
  					mRestart = true;
  					start();
  				}
  			}		
  			else if (key.equals(KEY_ENABLED)) {
  				mEnabled = sharedPreferences.getBoolean(KEY_ENABLED, mEnabled);
  				start();
  			}
  		}
  	};
  
  	/** The Binder you obtain when a connection with the Service is established. */
  	public class LocalBinder extends Binder {
  		public RtspServer getService() {
  			return RtspServer.this;
  		}
  	}
  
  	@Override
  	public IBinder onBind(Intent intent) {
  		return mBinder;
  	}
  
  	protected void postMessage(int id) {
  		synchronized (mListeners) {
  			if (mListeners.size() > 0) {
  				for (CallbackListener cl : mListeners) {
  					cl.onMessage(this, id);
  				}
  			}			
  		}
  	}	
  	
  	protected void postError(Exception exception, int id) {
  		synchronized (mListeners) {
  			if (mListeners.size() > 0) {
  				for (CallbackListener cl : mListeners) {
  					cl.onError(this, exception, id);
  				}
  			}			
  		}
  	}
  
  	/** 
  	 * By default the RTSP uses {@link UriParser} to parse the URI requested by the client
  	 * but you can change that behavior by override this method.
  	 * @param uri The uri that the client has requested
  	 * @param client The socket associated to the client
  	 * @return A proper session
  	 */
  	protected Session handleRequest(String uri, Socket client) throws IllegalStateException, IOException {
  		Session session = UriParser.parse(uri);
  		session.setOrigin(client.getLocalAddress().getHostAddress());
  		if (session.getDestination()==null) {
  			session.setDestination(client.getInetAddress().getHostAddress());
  		}
  		return session;
  	}
  	
  	class RequestListener extends Thread implements Runnable {
  
  		private final ServerSocket mServer;
  
  		public RequestListener() throws IOException {
  			try {
  				mServer = new ServerSocket(mPort);
  				start();
  			} catch (BindException e) {
  				Log.e(TAG,"Port already in use !");
  				postError(e, ERROR_BIND_FAILED);
  				throw e;
  			}
  		}
  
  		public void run() {
  			Log.i(TAG,"RTSP server listening on port "+mServer.getLocalPort());
  			while (!Thread.interrupted()) {
  				try {
  					new WorkerThread(mServer.accept()).start();
  				} catch (SocketException e) {
  					break;
  				} catch (IOException e) {
  					Log.e(TAG,e.getMessage());
  					continue;
  				}
  			}
  			Log.i(TAG,"RTSP server stopped !");
  		}
  
  		public void kill() {
  			try {
  				mServer.close();
  			} catch (IOException e) {}
  			try {
  				this.join();
  			} catch (InterruptedException ignore) {}
  		}
  
  	}
  
  	// One thread per client
  	class WorkerThread extends Thread implements Runnable {
  
  		private final Socket mClient;
  		private final OutputStream mOutput;
  		private final BufferedReader mInput;
  
  		// Each client has an associated session
  		private Session mSession;
  
  		public WorkerThread(final Socket client) throws IOException {
  			mInput = new BufferedReader(new InputStreamReader(client.getInputStream()));
  			mOutput = client.getOutputStream();
  			mClient = client;
  			mSession = new Session();
  		}
  
  		public void run() {
  			Request request;
  			Response response;
  
  			Log.i(TAG, "Connection from "+mClient.getInetAddress().getHostAddress());
  
  			while (!Thread.interrupted()) {
  
  				request = null;
  				response = null;
  
  				// Parse the request
  				try {
  					request = Request.parseRequest(mInput);
  				} catch (SocketException e) {
  					// Client has left
  					break;
  				} catch (Exception e) {
  					// We don't understand the request :/
  					response = new Response();
  					response.status = Response.STATUS_BAD_REQUEST;
  				}
  
  				// Do something accordingly like starting the streams, sending a session description
  				if (request != null) {
  					try {
  						response = processRequest(request);
  					}
  					catch (Exception e) {
  						// This alerts the main thread that something has gone wrong in this thread
  						postError(e, ERROR_START_FAILED);
  						Log.e(TAG,e.getMessage()!=null?e.getMessage():"An error occurred");
  						e.printStackTrace();
  						response = new Response(request);
  					}
  				}
  
  				// We always send a response
  				// The client will receive an "INTERNAL SERVER ERROR" if an exception has been thrown at some point
  				try {
  					response.send(mOutput);
  				} catch (IOException e) {
  					Log.e(TAG,"Response was not sent properly");
  					break;
  				}
  
  			}
  
  			// Streaming stops when client disconnects
  			boolean streaming = isStreaming();
  			mSession.syncStop();
  			if (streaming && !isStreaming()) {
  				postMessage(MESSAGE_STREAMING_STOPPED);
  			}
  			mSession.release();
  
  			try {
  				mClient.close();
  			} catch (IOException ignore) {}
  
  			Log.i(TAG, "Client disconnected");
  
  		}
  
  		public Response processRequest(Request request) throws IllegalStateException, IOException {
  			Response response = new Response(request);
  
  			/* ********************************************************************************** */
  			/* ********************************* Method DESCRIBE ******************************** */
  			/* ********************************************************************************** */
  			if (request.method.equalsIgnoreCase("DESCRIBE")) {
  
  				// Parse the requested URI and configure the session
  				mSession = handleRequest(request.uri, mClient);
  				mSessions.put(mSession, null);
  				mSession.syncConfigure();
  				
  				String requestContent = mSession.getSessionDescription();
  				String requestAttributes = 
  						"Content-Base: "+mClient.getLocalAddress().getHostAddress()+":"+mClient.getLocalPort()+"/\r\n" +
  								"Content-Type: application/sdp\r\n";
  
  				response.attributes = requestAttributes;
  				response.content = requestContent;
  
  				// If no exception has been thrown, we reply with OK
  				response.status = Response.STATUS_OK;
  
  			}
  
  			/* ********************************************************************************** */
  			/* ********************************* Method OPTIONS ********************************* */
  			/* ********************************************************************************** */
  			else if (request.method.equalsIgnoreCase("OPTIONS")) {
  				response.status = Response.STATUS_OK;
  				response.attributes = "Public: DESCRIBE,SETUP,TEARDOWN,PLAY,PAUSE\r\n";
  				response.status = Response.STATUS_OK;
  			}
  
  			/* ********************************************************************************** */
  			/* ********************************** Method SETUP ********************************** */
  			/* ********************************************************************************** */
  			else if (request.method.equalsIgnoreCase("SETUP")) {
  				Pattern p; Matcher m;
  				int p2, p1, ssrc, trackId, src[];
  				String destination;
  
  				p = Pattern.compile("trackID=(\\w+)",Pattern.CASE_INSENSITIVE);
  				m = p.matcher(request.uri);
  
  				if (!m.find()) {
  					response.status = Response.STATUS_BAD_REQUEST;
  					return response;
  				} 
  
  				trackId = Integer.parseInt(m.group(1));
  
  				if (!mSession.trackExists(trackId)) {
  					response.status = Response.STATUS_NOT_FOUND;
  					return response;
  				}
  
  				p = Pattern.compile("client_port=(\\d+)-(\\d+)",Pattern.CASE_INSENSITIVE);
  				m = p.matcher(request.headers.get("transport"));
  
  				if (!m.find()) {
  					int[] ports = mSession.getTrack(trackId).getDestinationPorts();
  					p1 = ports[0];
  					p2 = ports[1];
  				}
  				else {
  					p1 = Integer.parseInt(m.group(1)); 
  					p2 = Integer.parseInt(m.group(2));
  				}
  
  				ssrc = mSession.getTrack(trackId).getSSRC();
  				src = mSession.getTrack(trackId).getLocalPorts();
  				destination = mSession.getDestination();
  
  				mSession.getTrack(trackId).setDestinationPorts(p1, p2);
  				
  				boolean streaming = isStreaming();
  				mSession.syncStart(trackId);
  				if (!streaming && isStreaming()) {
  					postMessage(MESSAGE_STREAMING_STARTED);
  				}
  
  				response.attributes = "Transport: RTP/AVP/UDP;"+(InetAddress.getByName(destination).isMulticastAddress()?"multicast":"unicast")+
  						";destination="+mSession.getDestination()+
  						";client_port="+p1+"-"+p2+
  						";server_port="+src[0]+"-"+src[1]+
  						";ssrc="+Integer.toHexString(ssrc)+
  						";mode=play\r\n" +
  						"Session: "+ "1185d20035702ca" + "\r\n" +
  						"Cache-Control: no-cache\r\n";
  				response.status = Response.STATUS_OK;
  
  				// If no exception has been thrown, we reply with OK
  				response.status = Response.STATUS_OK;
  
  			}
  
  			/* ********************************************************************************** */
  			/* ********************************** Method PLAY *********************************** */
  			/* ********************************************************************************** */
  			else if (request.method.equalsIgnoreCase("PLAY")) {
  				String requestAttributes = "RTP-Info: ";
  				if (mSession.trackExists(0)) requestAttributes += "url=rtsp://"+mClient.getLocalAddress().getHostAddress()+":"+mClient.getLocalPort()+"/trackID="+0+";seq=0,";
  				if (mSession.trackExists(1)) requestAttributes += "url=rtsp://"+mClient.getLocalAddress().getHostAddress()+":"+mClient.getLocalPort()+"/trackID="+1+";seq=0,";
  				requestAttributes = requestAttributes.substring(0, requestAttributes.length()-1) + "\r\nSession: 1185d20035702ca\r\n";
  
  				response.attributes = requestAttributes;
  
  				// If no exception has been thrown, we reply with OK
  				response.status = Response.STATUS_OK;
  
  			}
  
  			/* ********************************************************************************** */
  			/* ********************************** Method PAUSE ********************************** */
  			/* ********************************************************************************** */
  			else if (request.method.equalsIgnoreCase("PAUSE")) {
  				response.status = Response.STATUS_OK;
  			}
  
  			/* ********************************************************************************** */
  			/* ********************************* Method TEARDOWN ******************************** */
  			/* ********************************************************************************** */
  			else if (request.method.equalsIgnoreCase("TEARDOWN")) {
  				response.status = Response.STATUS_OK;
  			}
  
  			/* ********************************************************************************** */
  			/* ********************************* Unknown method ? ******************************* */
  			/* ********************************************************************************** */
  			else {
  				Log.e(TAG,"Command unknown: "+request);
  				response.status = Response.STATUS_BAD_REQUEST;
  			}
  
  			return response;
  
  		}
  
  	}
  
  	static class Request {
  
  		// Parse method & uri
  		public static final Pattern regexMethod = Pattern.compile("(\\w+) (\\S+) RTSP",Pattern.CASE_INSENSITIVE);
  		// Parse a request header
  		public static final Pattern rexegHeader = Pattern.compile("(\\S+):(.+)",Pattern.CASE_INSENSITIVE);
  
  		public String method;
  		public String uri;
  		public HashMap<String,String> headers = new HashMap<String,String>();
  
  		/** Parse the method, uri & headers of a RTSP request */
  		public static Request parseRequest(BufferedReader input) throws IOException, IllegalStateException, SocketException {
  			Request request = new Request();
  			String line;
  			Matcher matcher;
  
  			// Parsing request method & uri
  			if ((line = input.readLine())==null) throw new SocketException("Client disconnected");
  			matcher = regexMethod.matcher(line);
  			matcher.find();
  			request.method = matcher.group(1);
  			request.uri = matcher.group(2);
  
  			// Parsing headers of the request
  			while ( (line = input.readLine()) != null && line.length()>3 ) {
  				matcher = rexegHeader.matcher(line);
  				matcher.find();
  				request.headers.put(matcher.group(1).toLowerCase(Locale.US),matcher.group(2));
  			}
  			if (line==null) throw new SocketException("Client disconnected");
  
  			// It's not an error, it's just easier to follow what's happening in logcat with the request in red
  			Log.e(TAG,request.method+" "+request.uri);
  
  			return request;
  		}
  	}
  
  	static class Response {
  
  		// Status code definitions
  		public static final String STATUS_OK = "200 OK";
  		public static final String STATUS_BAD_REQUEST = "400 Bad Request";
  		public static final String STATUS_NOT_FOUND = "404 Not Found";
  		public static final String STATUS_INTERNAL_SERVER_ERROR = "500 Internal Server Error";
  
  		public String status = STATUS_INTERNAL_SERVER_ERROR;
  		public String content = "";
  		public String attributes = "";
  
  		private final Request mRequest;
  
  		public Response(Request request) {
  			this.mRequest = request;
  		}
  
  		public Response() {
  			// Be carefull if you modify the send() method because request might be null !
  			mRequest = null;
  		}
  
  		public void send(OutputStream output) throws IOException {
  			int seqid = -1;
  
  			try {
  				seqid = Integer.parseInt(mRequest.headers.get("cseq").replace(" ",""));
  			} catch (Exception e) {
  				Log.e(TAG,"Error parsing CSeq: "+(e.getMessage()!=null?e.getMessage():""));
  			}
  
  			String response = 	"RTSP/1.0 "+status+"\r\n" +
  					"Server: "+SERVER_NAME+"\r\n" +
  					(seqid>=0?("Cseq: " + seqid + "\r\n"):"") +
  					"Content-Length: " + content.length() + "\r\n" +
  					attributes +
  					"\r\n" + 
  					content;
  
  			Log.d(TAG,response.replace("\r", ""));
  
  			output.write(response.getBytes());
  		}
  	}
  
  }
  