Android直播推流学习

  • 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);//设置SessionmClient.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打包完全可以照搬!

更多相关文章

  1. Android 数据存储(三) 数据库存储
  2. Android中EventBus(事件总线)传递数据
  3. 如何在android中调用数据库资源
  4. Android如何连接和操作SQLite数据库
  5. android 通过http访问服务器数据
  6. Android创建和使用数据库

随机推荐

  1. [Android学习笔记五] Android View和Widg
  2. Android(OPhone) 学习笔记 - SharedPrefere
  3. What Is Bootloader And How To Unlock B
  4. 基于 Android 的英文电子词典
  5. 2020.8.12 京东Android开发工程师一面面
  6. Android - API Levels-
  7. Android ScrollView中的组件设置android:
  8. Vue实现简书导航栏效果
  9. App测试中Android和IOS测试区别
  10. PC上安装android market软件并提取apk文