Android Open Source - spydroid-ipcamera Rtsp Server






From Project

Back to project page spydroid-ipcamera.

License

The source code is released under:

GNU General Public License

If you think the Android project spydroid-ipcamera listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.

Java Source Code

/*
 * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com
 * /* w  ww.j a va  2 s.  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());
    }
  }

}




Java Source Code List

net.majorkernelpanic.http.ModAssetServer.java
net.majorkernelpanic.http.ModInternationalization.java
net.majorkernelpanic.http.ModSSL.java
net.majorkernelpanic.http.TinyHttpServer.java
net.majorkernelpanic.spydroid.SpydroidApplication.java
net.majorkernelpanic.spydroid.Utilities.java
net.majorkernelpanic.spydroid.api.CustomHttpServer.java
net.majorkernelpanic.spydroid.api.CustomRtspServer.java
net.majorkernelpanic.spydroid.api.RequestHandler.java
net.majorkernelpanic.spydroid.ui.AboutFragment.java
net.majorkernelpanic.spydroid.ui.HandsetFragment.java
net.majorkernelpanic.spydroid.ui.OptionsActivity.java
net.majorkernelpanic.spydroid.ui.PreviewFragment.java
net.majorkernelpanic.spydroid.ui.SpydroidActivity.java
net.majorkernelpanic.spydroid.ui.TabletFragment.java
net.majorkernelpanic.streaming.MediaStream.java
net.majorkernelpanic.streaming.SessionBuilder.java
net.majorkernelpanic.streaming.Session.java
net.majorkernelpanic.streaming.Stream.java
net.majorkernelpanic.streaming.audio.AACStream.java
net.majorkernelpanic.streaming.audio.AMRNBStream.java
net.majorkernelpanic.streaming.audio.AudioQuality.java
net.majorkernelpanic.streaming.audio.AudioStream.java
net.majorkernelpanic.streaming.exceptions.CameraInUseException.java
net.majorkernelpanic.streaming.exceptions.ConfNotSupportedException.java
net.majorkernelpanic.streaming.exceptions.InvalidSurfaceException.java
net.majorkernelpanic.streaming.exceptions.StorageUnavailableException.java
net.majorkernelpanic.streaming.gl.SurfaceManager.java
net.majorkernelpanic.streaming.gl.SurfaceView.java
net.majorkernelpanic.streaming.gl.TextureManager.java
net.majorkernelpanic.streaming.hw.CodecManager.java
net.majorkernelpanic.streaming.hw.EncoderDebugger.java
net.majorkernelpanic.streaming.hw.NV21Convertor.java
net.majorkernelpanic.streaming.mp4.MP4Config.java
net.majorkernelpanic.streaming.mp4.MP4Parser.java
net.majorkernelpanic.streaming.rtcp.SenderReport.java
net.majorkernelpanic.streaming.rtp.AACADTSPacketizer.java
net.majorkernelpanic.streaming.rtp.AACLATMPacketizer.java
net.majorkernelpanic.streaming.rtp.AMRNBPacketizer.java
net.majorkernelpanic.streaming.rtp.AbstractPacketizer.java
net.majorkernelpanic.streaming.rtp.H263Packetizer.java
net.majorkernelpanic.streaming.rtp.H264Packetizer.java
net.majorkernelpanic.streaming.rtp.MediaCodecInputStream.java
net.majorkernelpanic.streaming.rtp.RtpSocket.java
net.majorkernelpanic.streaming.rtsp.RtspClient.java
net.majorkernelpanic.streaming.rtsp.RtspServer.java
net.majorkernelpanic.streaming.rtsp.UriParser.java
net.majorkernelpanic.streaming.video.CodecManager.java
net.majorkernelpanic.streaming.video.H263Stream.java
net.majorkernelpanic.streaming.video.H264Stream.java
net.majorkernelpanic.streaming.video.VideoQuality.java
net.majorkernelpanic.streaming.video.VideoStream.java