Android Open Source - spydroid-ipcamera Rtsp Client






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.  java  2  s .  c om*/
 * This file is part of Spydroid (http://code.google.com/p/spydroid-ipcamera/)
 * 
 * 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.io.UnsupportedEncodingException;
import java.net.Socket;
import java.net.SocketException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Locale;
import java.util.concurrent.Semaphore;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import net.majorkernelpanic.streaming.Session;
import net.majorkernelpanic.streaming.Stream;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.util.Log;

/**
 * RFC 2326.
 * A basic and asynchronous RTSP client.
 * The original purpose of this class was to implement a small RTSP client compatible with Wowza.
 * It implements Digest Access Authentication according to RFC 2069. 
 */
public class RtspClient {

  public final static String TAG = "RtspClient";

  /** Message sent when the connection to the RTSP server failed. */
  public final static int ERROR_CONNECTION_FAILED = 0x01;
  
  /** Message sent when the credentials are wrong. */
  public final static int ERROR_WRONG_CREDENTIALS = 0x03;
  
  /** 
   * Message sent when the connection with the RTSP server has been lost for 
   * some reason (for example, the user is going under a bridge).
   * When the connection with the server is lost, the client will automatically try to
   * reconnect as long as {@link #stopStream()} is not called. 
   **/
  public final static int ERROR_CONNECTION_LOST = 0x04;
  
  /**
   * Message sent when the connection with the RTSP server has been reestablished.
   * When the connection with the server is lost, the client will automatically try to
   * reconnect as long as {@link #stopStream()} is not called.
   */
  public final static int MESSAGE_CONNECTION_RECOVERED = 0x05;

  private final static int STATE_STARTED = 0x00;
  private final static int STATE_STARTING = 0x01;
  private final static int STATE_STOPPING = 0x02;
  private final static int STATE_STOPPED = 0x03;
  private int mState = 0;

  private class Parameters {
    public String host; 
    public String username;
    public String password;
    public String path;
    public Session session;
    public int port;
    
    public Parameters clone() {
      Parameters params = new Parameters();
      params.host = host;
      params.username = username;
      params.password = password;
      params.path = path;
      params.session = session;
      params.port = port;
      return params;
    }
  }
  
  
  private Parameters mTmpParameters;
  private Parameters mParameters;
  
  private Socket mSocket;
  private String mSessionID;
  private String mAuthorization;
  private BufferedReader mBufferedReader;
  private OutputStream mOutputStream;
  private int mCSeq;
  private Callback mCallback;
  private Handler mMainHandler;
  private Handler mHandler;

  /**
   * The callback interface you need to implement to know what's going on with the 
   * RTSP server (for example your Wowza Media Server).
   */
  public interface Callback {
    public void onRtspUpdate(int message, Exception exception);
  }

  public RtspClient() {
    mCSeq = 0;
    mTmpParameters = new Parameters();
    mTmpParameters.port = 1935;
    mTmpParameters.path = "/";
    mAuthorization = null;
    mCallback = null;
    mMainHandler = new Handler(Looper.getMainLooper());
    mState = STATE_STOPPED;

    final Semaphore signal = new Semaphore(0);
    new HandlerThread("net.majorkernelpanic.streaming.RtspClient"){
      @Override
      protected void onLooperPrepared() {
        mHandler = new Handler();
        signal.release();
      }
    }.start();
    signal.acquireUninterruptibly();
    
  }

  /**
   * Sets the callback interface that will be called on status updates of the connection
   * with the RTSP server.
   * @param cb The implementation of the {@link Callback} interface
   */
  public void setCallback(Callback cb) {
    mCallback = cb;
  }

  /**
   * The {@link Session} that will be used to stream to the server.
   * If not called before {@link #startStream()}, a it will be created.
   */
  public void setSession(Session session) {
    mTmpParameters.session = session;
  }

  public Session getSession() {
    return mTmpParameters.session;
  }  

  /**
   * Sets the destination address of the RTSP server.
   * @param host The destination address
   * @param port The destination port
   */
  public void setServerAddress(String host, int port) {
    mTmpParameters.port = port;
    mTmpParameters.host = host;
  }

  /**
   * If authentication is enabled on the server, you need to call this with a valid username/password pair.
   * Only implements Digest Access Authentication according to RFC 2069.
   * @param username The username
   * @param password The password
   */
  public void setCredentials(String username, String password) {
    mTmpParameters.username = username;
    mTmpParameters.password = password;
  }

  /**
   * The path to which the stream will be sent to. 
   * @param path The path
   */
  public void setStreamPath(String path) {
    mTmpParameters.path = path;
  }

  public boolean isStreaming() {
    return mState==STATE_STARTED|mState==STATE_STARTING;
  }

  /**
   * Connects to the RTSP server to publish the stream, and the effectively starts streaming.
   * You need to call {@link #setServerAddress(String, int)} and optionnally {@link #setSession(Session)} 
   * and {@link #setCredentials(String, String)} before calling this.
   * Should be called of the main thread !
   */
  public void startStream() {
    if (mTmpParameters.host == null) throw new IllegalStateException("setServerAddress(String,int) has not been called !");
    if (mTmpParameters.session == null) throw new IllegalStateException("setSession() has not been called !");
    mHandler.post(new Runnable () {
      @Override
      public void run() {
        if (mState != STATE_STOPPED) return;
        mState = STATE_STARTING;
        
        Log.d(TAG,"Connecting to RTSP server...");
        
        // If the user calls some methods to configure the client, it won't modify its behavior until the stream is restarted
        mParameters = mTmpParameters.clone();
        mParameters.session.setDestination(mTmpParameters.host);
        
        try {
          mParameters.session.syncConfigure();
        } catch (Exception e) {
          mParameters.session = null;
          mState = STATE_STOPPED;
          return;
        }        
        
        try {
          tryConnection();
        } catch (Exception e) {
          postError(ERROR_CONNECTION_FAILED, e);
          abord();
          return;
        }

        try {
          mParameters.session.syncStart();
          mState = STATE_STARTED;
          mHandler.post(mConnectionMonitor);
        } catch (Exception e) {
          abord();
        }

      }
    });

  }

  /**
   * Stops the stream, and informs the RTSP server.
   */
  public void stopStream() {
    mHandler.post(new Runnable () {
      @Override
      public void run() {
        if (mParameters != null && mParameters.session != null) {
          mParameters.session.stop();
        }
        if (mState != STATE_STOPPED) {
          mState = STATE_STOPPING;
          abord();
        }
      }
    });
  }

  public void release() {
    stopStream();
    mHandler.getLooper().quit();
  }
  
  private void abord() {
    try {
      sendRequestTeardown();
    } catch (Exception ignore) {}
    try {
      mSocket.close();
    } catch (Exception ignore) {}
    mHandler.removeCallbacks(mConnectionMonitor);
    mHandler.removeCallbacks(mRetryConnection);
    mState = STATE_STOPPED;
  }
  
  private void tryConnection() throws IOException {
    mCSeq = 0;
    mSocket = new Socket(mParameters.host, mParameters.port);
    mBufferedReader = new BufferedReader(new InputStreamReader(mSocket.getInputStream()));
    mOutputStream = mSocket.getOutputStream();
    sendRequestAnnounce();
    sendRequestSetup();
    sendRequestRecord();
  }
  
  /**
   * Forges and sends the ANNOUNCE request 
   */
  private void sendRequestAnnounce() throws IllegalStateException, SocketException, IOException {

    String body = mParameters.session.getSessionDescription();
    String request = "ANNOUNCE rtsp://"+mParameters.host+":"+mParameters.port+mParameters.path+" RTSP/1.0\r\n" +
        "CSeq: " + (++mCSeq) + "\r\n" +
        "Content-Length: " + body.length() + "\r\n" +
        "Content-Type: application/sdp \r\n\r\n" +
        body;
    Log.i(TAG,request.substring(0, request.indexOf("\r\n")));

    mOutputStream.write(request.getBytes("UTF-8"));
    Response response = Response.parseResponse(mBufferedReader);

    if (response.headers.containsKey("server")) {
      Log.v(TAG,"RTSP server name:" + response.headers.get("server"));
    } else {
      Log.v(TAG,"RTSP server name unknown");
    }

    try {
      Matcher m = Response.rexegSession.matcher(response.headers.get("session"));
      m.find();
      mSessionID = m.group(1);
    } catch (Exception e) {
      throw new IOException("Invalid response from server. Session id: "+mSessionID);
    }

    if (response.status == 401) {
      String nonce, realm;
      Matcher m;

      if (mParameters.username == null || mParameters.password == null) throw new IllegalStateException("Authentication is enabled and setCredentials(String,String) was not called !");

      try {
        m = Response.rexegAuthenticate.matcher(response.headers.get("www-authenticate")); m.find();
        nonce = m.group(2);
        realm = m.group(1);
      } catch (Exception e) {
        throw new IOException("Invalid response from server");
      }

      String uri = "rtsp://"+mParameters.host+":"+mParameters.port+mParameters.path;
      String hash1 = computeMd5Hash(mParameters.username+":"+m.group(1)+":"+mParameters.password);
      String hash2 = computeMd5Hash("ANNOUNCE"+":"+uri);
      String hash3 = computeMd5Hash(hash1+":"+m.group(2)+":"+hash2);

      mAuthorization = "Digest username=\""+mParameters.username+"\",realm=\""+realm+"\",nonce=\""+nonce+"\",uri=\""+uri+"\",response=\""+hash3+"\"\r\n";

      request = "ANNOUNCE rtsp://"+mParameters.host+":"+mParameters.port+mParameters.path+" RTSP/1.0\r\n" +
          "CSeq: " + (++mCSeq) + "\r\n" +
          "Content-Length: " + body.length() + "\r\n" +
          "Authorization: " + mAuthorization +
          "Session: " + mSessionID + "\r\n" +
          "Content-Type: application/sdp \r\n\r\n" +
          body;

      Log.i(TAG,request.substring(0, request.indexOf("\r\n")));

      mOutputStream.write(request.getBytes("UTF-8"));
      response = Response.parseResponse(mBufferedReader);

      if (response.status == 401) throw new RuntimeException("Bad credentials !");

    } else if (response.status == 403) {
      throw new RuntimeException("Access forbidden !");
    }

  }

  /**
   * Forges and sends the SETUP request 
   */
  private void sendRequestSetup() throws IllegalStateException, SocketException, IOException {
    for (int i=0;i<2;i++) {
      Stream stream = mParameters.session.getTrack(i);
      if (stream != null) {
        String request = "SETUP rtsp://"+mParameters.host+":"+mParameters.port+mParameters.path+"/trackID="+i+" RTSP/1.0\r\n" +
            "Transport: RTP/AVP/UDP;unicast;client_port="+(5000+2*i)+"-"+(5000+2*i+1)+";mode=receive\r\n" +
            addHeaders();

        Log.i(TAG,request.substring(0, request.indexOf("\r\n")));

        mOutputStream.write(request.getBytes("UTF-8"));
        Response response = Response.parseResponse(mBufferedReader);
        Matcher m;
        try {
          m = Response.rexegTransport.matcher(response.headers.get("transport")); m.find();
          stream.setDestinationPorts(Integer.parseInt(m.group(3)), Integer.parseInt(m.group(4)));
          Log.d(TAG, "Setting destination ports: "+Integer.parseInt(m.group(3))+", "+Integer.parseInt(m.group(4)));
        } catch (Exception e) {
          e.printStackTrace();
          int[] ports = stream.getDestinationPorts();
          Log.d(TAG,"Server did not specify ports, using default ports: "+ports[0]+"-"+ports[1]);
        }
      }
    }
  }

  /**
   * Forges and sends the RECORD request 
   */
  private void sendRequestRecord() throws IllegalStateException, SocketException, IOException {
    String request = "RECORD rtsp://"+mParameters.host+":"+mParameters.port+mParameters.path+" RTSP/1.0\r\n" +
        "Range: npt=0.000-" +
        addHeaders();
    Log.i(TAG,request.substring(0, request.indexOf("\r\n")));
    mOutputStream.write(request.getBytes("UTF-8"));
    Response.parseResponse(mBufferedReader);
  }

  /**
   * Forges and sends the TEARDOWN request 
   */
  private void sendRequestTeardown() throws IOException {
    String request = "TEARDOWN rtsp://"+mParameters.host+":"+mParameters.port+mParameters.path+" RTSP/1.0\r\n" + addHeaders();
    Log.i(TAG,request.substring(0, request.indexOf("\r\n")));
    mOutputStream.write(request.getBytes("UTF-8"));
  }
  
  /**
   * Forges and sends the OPTIONS request 
   */
  private void sendRequestOption() throws IOException {
    String request = "OPTIONS rtsp://"+mParameters.host+":"+mParameters.port+mParameters.path+" RTSP/1.0\r\n" + addHeaders();
    Log.i(TAG,request.substring(0, request.indexOf("\r\n")));
    mOutputStream.write(request.getBytes("UTF-8"));
    Response.parseResponse(mBufferedReader);
  }  

  private String addHeaders() {
    return "CSeq: " + (++mCSeq) + "\r\n" +
        "Content-Length: 0\r\n" +
        "Session: " + mSessionID + "\r\n" +
        (mAuthorization != null ? "Authorization: " + mAuthorization + "\r\n":"");
  }  

  /**
   * If the connection with the RTSP server is lost, we try to reconnect to it as
   * long as {@link #stopStream()} is not called.
   */
  private Runnable mConnectionMonitor = new Runnable() {
    @Override
    public void run() {
      if (mState == STATE_STARTED) {
        try {
          // We poll the RTSP server with OPTION requests
          sendRequestOption();
          mHandler.postDelayed(mConnectionMonitor, 6000);
        } catch (IOException e) {
          // Happens if the OPTION request fails
          postMessage(ERROR_CONNECTION_LOST);
          Log.e(TAG, "Connection lost with the server...");
          mParameters.session.stop();
          mHandler.post(mRetryConnection);
        }
      }
    }
  };

  /** Here, we try to reconnect to the RTSP. */
  private Runnable mRetryConnection = new Runnable() {
    @Override
    public void run() {
      if (mState == STATE_STARTED) {
        try {
          Log.e(TAG, "Trying to reconnect...");
          tryConnection();
          try {
            mParameters.session.start();
            mHandler.post(mConnectionMonitor);
            postMessage(MESSAGE_CONNECTION_RECOVERED);
          } catch (Exception e) {
            abord();
          }
        } catch (IOException e) {
          mHandler.postDelayed(mRetryConnection,1000);
        }
      }
    }
  };
  
  final protected static char[] hexArray = {'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'};

  private static String bytesToHex(byte[] bytes) {
    char[] hexChars = new char[bytes.length * 2];
    int v;
    for ( int j = 0; j < bytes.length; j++ ) {
      v = bytes[j] & 0xFF;
      hexChars[j * 2] = hexArray[v >>> 4];
      hexChars[j * 2 + 1] = hexArray[v & 0x0F];
    }
    return new String(hexChars);
  }

  /** Needed for the Digest Access Authentication. */
  private String computeMd5Hash(String buffer) {
    MessageDigest md;
    try {
      md = MessageDigest.getInstance("MD5");
      return bytesToHex(md.digest(buffer.getBytes("UTF-8")));
    } catch (NoSuchAlgorithmException ignore) {
    } catch (UnsupportedEncodingException e) {}
    return "";
  }

  private void postMessage(final int message) {
    mMainHandler.post(new Runnable() {
      @Override
      public void run() {
        if (mCallback != null) {
          mCallback.onRtspUpdate(message, null); 
        }
      }
    });
  }

  private void postError(final int message, final Exception e) {
    mMainHandler.post(new Runnable() {
      @Override
      public void run() {
        if (mCallback != null) {
          mCallback.onRtspUpdate(message, e); 
        }
      }
    });
  }  

  static class Response {

    // Parses method & uri
    public static final Pattern regexStatus = Pattern.compile("RTSP/\\d.\\d (\\d+) (\\w+)",Pattern.CASE_INSENSITIVE);
    // Parses a request header
    public static final Pattern rexegHeader = Pattern.compile("(\\S+):(.+)",Pattern.CASE_INSENSITIVE);
    // Parses a WWW-Authenticate header
    public static final Pattern rexegAuthenticate = Pattern.compile("realm=\"(.+)\",\\s+nonce=\"(\\w+)\"",Pattern.CASE_INSENSITIVE);
    // Parses a Session header
    public static final Pattern rexegSession = Pattern.compile("(\\d+)",Pattern.CASE_INSENSITIVE);
    // Parses a Transport header
    public static final Pattern rexegTransport = Pattern.compile("client_port=(\\d+)-(\\d+).+server_port=(\\d+)-(\\d+)",Pattern.CASE_INSENSITIVE);


    public int status;
    public HashMap<String,String> headers = new HashMap<String,String>();

    /** Parse the method, uri & headers of a RTSP request */
    public static Response parseResponse(BufferedReader input) throws IOException, IllegalStateException, SocketException {
      Response response = new Response();
      String line;
      Matcher matcher;
      // Parsing request method & uri
      if ((line = input.readLine())==null) throw new SocketException("Connection lost");
      matcher = regexStatus.matcher(line);
      matcher.find();
      response.status = Integer.parseInt(matcher.group(1));

      // Parsing headers of the request
      while ( (line = input.readLine()) != null) {
        //Log.e(TAG,"l: "+line.length()+"c: "+line);
        if (line.length()>3) {
          matcher = rexegHeader.matcher(line);
          matcher.find();
          response.headers.put(matcher.group(1).toLowerCase(Locale.US),matcher.group(2));
        } else {
          break;
        }
      }
      if (line==null) throw new SocketException("Connection lost");

      Log.d(TAG, "Response from server: "+response.status);

      return response;
    }
  }

}




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