码迷,mamicode.com
首页 > 移动开发 > 详细

Android直播推流学习

时间:2016-07-10 18:29:50      阅读:469      评论:0      收藏:0      [点我收藏+]

标签:

Android直播推流学习


第一部

本文也主要是一步步分析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打包完全可以照搬!

Android直播推流学习

标签:

原文地址:http://blog.csdn.net/qq_15807167/article/details/51870876

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!