接上一篇,已经清楚RTSP推流需要发送的RTSP请求消息,所以我们实现一个RTSP的客户端,完成这些请求消息,并将H264的RTP包推送到指定的RTSP服务器即可,这个客户端同时推送过Darwin和Crtmp-server两个RTSP的服务端,来验证推流客户端的功能实现。
下面是我们实现的RtspPushStreamClient推流客户端类:
package net.majorkernelpanic.streaming.rtmp;
import static net.majorkernelpanic.streaming.SessionBuilder.AUDIO_NONE;
import static net.majorkernelpanic.streaming.SessionBuilder.VIDEO_NONE;
import android.hardware.Camera.CameraInfo;
import android.os.Handler;
import android.util.Log;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Random;
import net.majorkernelpanic.streaming.Session;
import net.majorkernelpanic.streaming.SessionBuilder;
import net.majorkernelpanic.streaming.rtsp.RtspClient;
import net.majorkernelpanic.streaming.rtsp.RtspClient.Callback;
/**
* Created by aaa on 2015/11/25.
*/
public class RtspPushStreamClient implements Callback{
private static final String TAG = "RtspPushStreamClient";
// private static int localPort = 8100;
RtspPushClient client;
String mSdp;
Handler mUserHandler;//
public RtspPushStreamClient(Handler handler){
client = new RtspPushClient(this);
mUserHandler= handler;
}
public void startRtspClient(String ip, String rtmp_path){
client.start_connect(ip, 554, rtmp_path);
}
public void switchCamera(){
client.switchCamera();
}
public void setFlash(boolean mode){
client.setFlash(mode);
}
public void stopRtspClient(){
client.stop_connect();
}
public static class RtspPushClient {
private RtspClient client;
private String request_uri;
String mSdp;
public RtspPushClient(RtspPushStreamClient streamClient){
client = new RtspClient();
client.setCallback(streamClient);
}
public void start_connect(String ip, int port, String path){
//"rtsp://192.168.0.5:9010/"
client.setServerAddress(ip, port);
client.setStreamPath("/"+path);//"/live/rtsp_test"
//setCamera --CAMERA_FACING_FRONT
SessionBuilder builder = SessionBuilder.getInstance().clone();
// builder.setCamera(CameraInfo.CAMERA_FACING_FRONT);
// builder.setAudioEncoder(SessionBuilder.AUDIO_AAC).setVideoEncoder(SessionBuilder.VIDEO_H264);
builder.setAudioEncoder(SessionBuilder.AUDIO_AAC).setVideoEncoder(SessionBuilder.VIDEO_H264);
SessionBuilder b = SessionBuilder.getInstance();
Random rand = new Random();
int localPort = rand.nextInt(1000)+ 8100; //8100-9100
localPort = (localPort & 0xFFFE); /* turn to even number */
Session session = builder.build(localPort);
client.setSession(session);
client.startStream(localPort);
}
public void switchCamera(){
client.switchCamera();
}
public void setFlash(boolean mode){
client.setFlash(mode);
}
public void stop_connect(){
client.stopStream();
}
}
@Override
public void onRtspUpdate(int message, Exception exception) {
// TODO Auto-generated method stub
Log.d(TAG, "message:"+message);
mUserHandler.sendEmptyMessage(message);
}
}
RTSPClient的tryConnection方法完成了和服务器的RTSP请求交互:
/*
* Copyright (C) 2011-2014 GUIGUI Simon, [email protected]
*
* 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 android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.util.Log;
import net.majorkernelpanic.streaming.Session;
import net.majorkernelpanic.streaming.Stream;
import net.majorkernelpanic.streaming.video.VideoStream;
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;
/**
* 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 int mPort = 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 = "/";
setCredentials("1102", "123456");
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(int port) {
mPort = port;
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.i(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);
Log.i(TAG,"Exception failed:"+e.getMessage());
// abord();
return;
}
try {
mParameters.session.syncStart();
mState = STATE_STARTED;
// mHandler.post(mConnectionMonitor);
} catch (Exception e) {
Log.i(TAG,"ii Exception failed:"+e.getMessage());
// abord();
}
//notify user success.
postMessage(0);
}
});
}
public void switchCamera(){
Stream stream = mParameters.session.getTrack(1);//video
if (stream instanceof VideoStream){
VideoStream videoStream = (VideoStream)stream;
try {
videoStream.switchCamera();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public void setFlash(boolean bFlag){
Stream stream = mParameters.session.getTrack(1);//video
if (stream instanceof VideoStream){
VideoStream videoStream = (VideoStream)stream;
try {
videoStream.setFlashState(bFlag);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
/**
* 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() {
Log.i(TAG, "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();
sendRequestOption();
sendRequestAnnounce();
if (sendRequestSetup()){
if (!sendRequestRecord()){
Log.e(TAG, "Record failed.");
postError(ERROR_CONNECTION_FAILED);
}
}else{
postError(ERROR_CONNECTION_FAILED);
}
}
/**
* 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" +
"Content-Type: application/sdp\r\n" +
"CSeq: " + (++mCSeq) + "\r\n" +
"User-Agent: " + "XdjaClient" + "\r\n" +
"Content-Length: " + body.length() + "\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.i(TAG,"RTSP server name:" + response.headers.get("server"));
} else {
Log.i(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-Type: application/sdp"+ "\r\n" +
"Content-Length: " + body.length() + "\r\n" +
"Authorization: " + mAuthorization +
"Session: " + mSessionID + "\r\n" +
"User-Agent: " + "xdja-xa" + "\r\n\r\n" +
body+ "\r\n\r\n";
Log.i(TAG,request);
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 boolean sendRequestSetup() throws IllegalStateException, SocketException, IOException {
boolean bHaveAudio = false;
for (int i=0;i<2;i++) {
Stream stream = mParameters.session.getTrack(i);
if (stream != null) {
if (i == 0){
bHaveAudio = true;
}
/* 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=record\r\n" +
addHeaders();*/
int trackId = i;
if (!bHaveAudio){
trackId = 0;
}
String request = "SETUP rtsp://"+mParameters.host+":"+mParameters.port+mParameters.path+"/streamid="+trackId+" RTSP/1.0\r\n" +
"Transport: RTP/AVP/UDP;unicast;client_port="+(mPort+2*i)+"-"+(mPort+2*i+1)+";mode=record\r\n" +
addHeaders();
Log.i(TAG,request.substring(0, request.indexOf("\r\n")));
mOutputStream.write(request.getBytes("UTF-8"));
Response response = Response.parseResponse(mBufferedReader);//
if (i == 0){
try {
mSessionID = response.headers.get("session").trim();
Log.i(TAG,"mSessionID: "+ mSessionID+ "response.status:"+response.status);
} catch (Exception e) {
throw new IOException("Invalid response from server. Session id: "+mSessionID);
}
}
if (response.status != 200){
Log.i(TAG,"return for resp :" +response.status);
return false;
}
Matcher m;
try {
if (response.headers.get("transport") == null){
Log.i(TAG,"return for not transport");
return false;
}
m = Response.rexegTransport.matcher(response.headers.get("transport")); m.find();
stream.setDestinationPorts(Integer.parseInt(m.group(3)), Integer.parseInt(m.group(4)));
// mParameters.session.syncStart(i);
Log.i(TAG, "Setting destination ports: "+Integer.parseInt(m.group(3))+", "+Integer.parseInt(m.group(4)));
} catch (Exception e) {
e.printStackTrace();
int[] ports = stream.getDestinationPorts();
Log.i(TAG,"Server did not specify ports, using default ports: "+ports[0]+"-"+ports[1]);
}
}
}
return true;
}
/**
* Forges and sends the RECORD request
*/
private boolean sendRequestRecord() throws IllegalStateException, SocketException, IOException {
String request = "RECORD rtsp://"+mParameters.host+":"+mParameters.port+mParameters.path+" RTSP/1.0\r\n" +
"Range: npt=0.000-\r\n" +
addHeaders();
Log.i(TAG,request.substring(0, request.indexOf("\r\n")));
mOutputStream.write(request.getBytes("UTF-8"));
Response resp = Response.parseResponse(mBufferedReader);
if (resp.status != 200){
return false;
}
return true;
}
/**
* 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" +
(mSessionID != null ? "Session: " + mSessionID + "\r\n" :"") +
"User-Agent: " + "xdja-xa" + "\r\n" +
(mAuthorization != null ? "Authorization: " + mAuthorization + "\r\n\r\n":"\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) {
mMainHandler.post(new Runnable() {
@Override
public void run() {
if (mCallback != null) {
mCallback.onRtspUpdate(message, null);
}
}
});
}
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 headers = new HashMap();
/** 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();
Log.i(TAG, "response.headers: "+matcher.group(1).toLowerCase(Locale.US)+": "+ matcher.group(2));
response.headers.put(matcher.group(1).toLowerCase(Locale.US), matcher.group(2));
} else {
break;
}
}
if (line==null) throw new SocketException("Connection lost");
Log.i(TAG, "Response from server: "+response.status);
return response;
}
}
}