标签:
本文也主要是一步步分析spydroid源码。 首先spydroid的采用的协议是RTSP,目前我知道支持RTSP协议的服务器是Darwin,但是Darwin比较复杂,所以大家可以选择EasyDarwin,大家可以去搜搜看看。还是继续说spydroid吧,spydroid这个项目大家可以在github上搜到的,不过作者也是很久没有更新了,如果大家只做推流的话可以看看原作者的另外一个项目Spydroid。
项目包结构
从这个包结构可以看出作者大概的设计,首先是rtsp这个包,这个包里有一个RtspClient,这里主要是和服务器建立RTSP会话连接使用的。接着是Session SessionBuilder MediaStream三个类。首先是Session,这个对象保存了本次推流连接所有的音视频相关信息和资源,包括各种参数等等,SessionBuilder主要用于创建Session。MediaStream是一个父类,它下面有两个子类VideoStream和AudioStream,如果大家想要扩展音视频的编码支持,可以继承这两个子类进行改造。具体参照可以查看H264Stream和AACStream两个类。video和audio两个包就是具体的音视频编码和采集相关的东西;rtp和rtcp则是音视频打包发送相关的东西;gl包是作者封装了SurfaceView,这样可以不用通过摄像头来直接采集数据,而是从SurfaceView的预览里面采集视频数据;hw包则是处理硬编码相关的;mp4包是提取视频的sps和pps信息的。
现在已经对spydroid的项目有了大致的了解,接着我会分析一些重要的类。
首先是Session类,这个类主要有两个重要成员:AudioStream和VideoStream,通过该类可以初始化音视频流,停止音视频推流,以及获取相关流媒体信息等。在Spydroid的设计中,Session一般不是直接创建的,而是通过SessionBuilder进行创建的。SessionBuilder是一个单例模式的类,通过SessionBuilder我们创建Session对象,AudioStream和VideoStream对象,并且对AudioStream和VideoStream参数进行了初始化设置。代码如下:
Session mSession = SessionBuilder.getInstance()
.setContext(getApplicationContext())
.setAudioEncoder(SessionBuilder.AUDIO_AAC)//音频编码格式
.setAudioQuality(new AudioQuality(8000,16000))//音频参数 采样率
.setVideoEncoder(SessionBuilder.VIDEO_H264)//视频编码格式
//视频参数 分辨率1280*720 帧率15 码率1000*1000
.setVideoQuality(new VideoQuality(1280, 720, 15, 1000*1000))
.setSurfaceView(mSurfaceView)//用于进行预览展示的SurfaceView
.setPreviewOrientation(0)urfaceView//Camera方向
.setCallback(this)//一些监听回调
.build();
接下来是RtspClient这个类,这个类主要是负责与流媒体服务器进行RTSP协议会话连接,还是首先来看看相关初始化设置吧,这里我们首先设定我们推送的地址为:rtsp://192.168.1.115:554/live.sdp。代码如下:
RtspClient mClient = new RtspClient();
mClient.setSession(mSession);//设置Session
mClient.setCallback(this); //回调监听
mClient.setServerAddress("192.168.1.115", 554);//服务器的ip和端口号
//这里算是一个标识符,服务器会在连接后创建一个名为live.sdp的文件,所以这里的名字一定要唯一。
mClient.setStreamPath("/live.sdp");
mClient.startStream();//开始推流
暂时就这样吧,下一节具体分析RTSP的会话过程。
前面提到了Spydroid两个关键的类:Session和RtspClient。Session是负责维护流媒体资源的,而RtspClient则是建立RTSP链接的。接下来我们就详细的分析RtspClient类。
首先RtspClient有一个Parameter的内部类,这个内部类保存了服务器ip、端口号、Session对象等信息。在RtspClient对象创建的时候,首先是创建了一个HandlerThread和Handler对象,Spydroid整个项目用到了很多HandlerThread。大家可以把这个理解成一个线程就好了,Handler可以和HandlerThread对象绑定到一起,然后就可以像平时用Handler给主线程发送消息一样给这个HandlerThread对象发消息。实际上,Android应用的主线程就是一个HandlerThread。这样做的好处是方便线程之间进行通信,也方便管理。
创建好RtspClient并且设置好相关参数之后,就开始调用startStream()方法进行推流了。我们看到Spydroid是在一个子线程中进行的推流的。
第一步是获取流媒体的sdp信息,这里调用了syncConfigure()方法。继续跟踪下去会发现其实是分别调用了AudioStream和VideoStream的configure()方法。这里就暂时不深入分析,这些方法具体做了什么。这里调用这个的主要目的是提取编码器的相关信息,并组成sdp信息,用于后面RTSP会话阶段使用。
第二步是开始和服务器进行交互。这里分为了Announce、Setup、Record三个阶段。Announce阶段主要是向服务器发送客户端的。
//Announce阶段
private void sendRequestAnnounce() throws IllegalStateException, SocketException, IOException {
//body就是sdp信息
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"));
mOutputStream.flush();
//解析服务器返回的信息
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");
}
//获取服务器返回的SessionID
if (response.headers.containsKey("session")) {
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);
}
}
//如果服务器的返回码是401 说明服务器需要进行帐号登录授权才可以进行使用
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+"\"";
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 + "\r\n" +
"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"));
mOutputStream.flush();
response = Response.parseResponse(mBufferedReader);
if (response.status == 401) throw new RuntimeException("Bad credentials !");
} else if (response.status == 403) {
throw new RuntimeException("Access forbidden !");
}
}
Setup阶段,主要就是告诉服务器音视频数据是通过udp还是tcp方式进行发送,如果是udp方式,服务器会返回udp接收的端口号,tcp的话则是直接使用当前的socket进行数据发送。这里需要注意的是,某些RTSP服务器在Announce阶段并不会返回SessionID,可能会在Setup阶段返回。所以两个地方我们都要尝试获取服务器的SessionID,并且下一次向服务器发送消息的时候带上SessionID。
//Setup阶段
private void sendRequestSetup() throws IllegalStateException, SocketException, IOException {
//通过循环 分别为音视频进行setup操作
for (int i=0;i<2;i++) {
Stream stream = mParameters.session.getTrack(i);
if (stream != null) {
String params = mParameters.transport==TRANSPORT_TCP ?
("TCP;interleaved="+2*i+"-"+(2*i+1)) : ("UDP;unicast;client_port="+(5000+2*i)+"-"+(5000+2*i+1)+";mode=receive");
String request = "SETUP rtsp://"+mParameters.host+":"+mParameters.port+mParameters.path+"/trackID="+i+" RTSP/1.0\r\n" +
"Transport: RTP/AVP/"+params+"\r\n" +
addHeaders();
//addHeaders()方法主要是在会话里添加SessionID
Log.i(TAG,request.substring(0, request.indexOf("\r\n")));
mOutputStream.write(request.getBytes("UTF-8"));
mOutputStream.flush();
Response response = Response.parseResponse(mBufferedReader);
Matcher m;
if (response.headers.containsKey("session")) {
try {
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);
}
}
//如果是UDP方式发送音视频数据包,那么则要获取服务器返回的UDP端口号
if (mParameters.transport == TRANSPORT_UDP) {
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]);
}
} else {
//如果是TCP方式发送音视频数据包,那么则直接使用当前的socket。
stream.setOutputStream(mOutputStream, (byte)(2*i));
}
}
}
}
Record阶段没什么需要分析的,这个阶段我个人理解是通知服务器准备接收音视频数据了。
Record阶段结束后,客户端和服务器的rtsp会话已经建立,接下来就是开始发送音视频数据了,后面主要分析视频数据,音频数据就暂时不分析了,基本上也是大同小异。
这里我们注意到在RTSP连接完成后,还有一些代码:
if (mParameters.transport == TRANSPORT_UDP) {
mHandler.post(mConnectionMonitor);
}
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);
}
}
}
};
这里,如果音视频数据包是以UDP方式进行发送的话,那么为了维护和服务器的RTSP会话链接,那么客户端必须要隔一段时间向服务器发送Option信息。上面的代码主要工作就是这个。
后面,我们会通过ViedeoStream来分析,spydroid是如将音视频数据发送带服务器的。
前面已经分析完客户端和服务器的RTSP会话连接,下面就进入推流阶段,也就是客户端向服务器发送音视频数据。这里就暂时只分析视频了,音频也是差不多的。
首先是VideoStream类,这个类和AudioStream一样继承了MediaStream,然后MediaStream实现了Stream接口。VideoStream也有子类:H264Stream和H263Stream,当然我们如果有其他编码方式也可以按照这个进行扩展。这里主要讲H264Stream的软编码。
发送数据的流程是,首先调用了H264Strem的start方法,在这个方法里首先执行了config()方法,这个方法主要是获取视频的sps和pps信息,并且以分辨率,帧率和码率为键值存储在sharepreference中,如果下一次参数一样则直接从sharepreference中取。
接着把sps和pps传递给了H264Packetizer对象,这个H264Packetizer是一个用来进行RTP打包的类,暂时就不分析了。接着调用了父类的start方法,然后根据判断系统能否使用硬编码来决定视频的编码器,这里我们先分析软编码。
在VideoStream的encodeWithMediaRecorder方法中我们看到,首先是创建了Localsocket,这是一个本地的Socket,主要用于系统的MediaRecoder服务接收数据;然后打开了Camera,并设置了视频采集编码参数。最后通过H264Packetizer对象进行编码。
注意:Spydroid的作者使用了很多子线程,很多地方的try catch并没有做任何处理,所以如果推流失败的时候,请检查这些try catch。
本次分析就到此为止了,Spydroid的RTP打包完全可以照搬!
标签:
原文地址:http://blog.csdn.net/qq_15807167/article/details/51870876