Back to project page spydroid-ipcamera.
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.
/* * 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; } } }