Android视频推流直播学习【三】

时间:2021-10-03 15:50:50

前面提到了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是如将音视频数据发送带服务器的。