Android直播推流学习

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

时间: 2024-10-25 03:14:40

Android直播推流学习的相关文章

安卓直播推流SDK

最近整理了Android直播推流SDK,在github上开源出来. 1,支持市面上绝大部分的rtmp服务器 nginx-rtmp,SRS,RED5等 2,视频用软编,兼容性好 市面上的一些android?rtmp推流sdk用的是android中mediacodec来进行,但是有两个缺点: 1,mediacodec这个类在android4.1以后才支持,之前的版本就没法用: 2,mediacodec这个类是硬件编码,需要手机厂家支持,很多厂家支持的情况都不一样,手机的失败是个大的问题. 这里采用软

Android直播实现 Android端推流、播放

最近想实现一个Android直播,但是对于这方面的资料都比较零碎,一开始是打算用ffmpeg来实现编码推流,在搜集资料期间,找到了几个强大的开源库,直接避免了jni的代码,集成后只用少量的java代码就可实现编码.推流和取流播放,整理了一下做了一个demo,在这里记录一下 效果图:  编码和推流,有两个方案选择: 一: 使用javacv来实现,最终也是用过ffmpeg来进行编码和推流,javacv实现到可以直接接收摄像头的帧数据 需要自己实现的代码只是打开摄像头,写一个SurfaceView进行

Android流媒体开发之路二:NDK开发Android端RTMP直播推流程序

NDK开发Android端RTMP直播推流程序 经过一番折腾,成功把RTMP直播推流代码,通过NDK交叉编译的方式,移植到了Android下,从而实现了Android端采集摄像头和麦克缝数据,然后进行h264视频编码和aac音频编码,并发送到RTMP服务器,从而实现Android摄像头直播.程序名为NdkRtmpEncoder,在这里把整个过程,和大体框架介绍一下,算是给需要的人引路. 开发思路 首先,为什么要用NDK来做,因为自己之前就已经实现过RTMP推流.RTMP播放.RTSP转码等等各种

不用任何第三方,写一个RTMP直播推流器

2016年是移动直播爆发年,不到半年的时间内无数移动直播App掀起了全民直播的热潮.然而个人觉得直播的门槛相对较高,从推流端到服务端器到播放端,无不需要专业的技术来支撑,仅仅推流端就有不少需要学习的知识.目前大部分直播采用的都是RTMP协议,我这里写一个简单的Demo,帮助大家更好的理解直播推流的过程,主要包括:音视频采集, 音视频编码, 数据打包, RTMP协议等相关的知识等.项目结构分的很清楚,各个模块也用协议进行了分离,方便大家学习不同的模块. 先阐述下推流的整体流程: 建立tcp连接 建

EasyRTMP获取H.264实时流并转化成为RTMP直播推流之EasyRTMP-iOS如何处理H264关键帧和SPS、PPS数据的

EasyRTMP是结合了多种音视频缓存及网络技术的一个rtmp直播推流端,包括:圆形缓冲区(circular buffer).智能丢帧.自动重连.rtmp协议等等多种技术,能够非常有效地适应各种平台(Windows.Linux.ARM.Android.iOS),各种网络环境(有线.wifi.4G),以及各种情况下的直播恢复(服务器重启.网络重启.硬件设备重启). ? 提出问题: EasyRTMP-iOS如何处理H264关键帧和SPS.PPS数据? 分析问题: 对于编码后的H264数据的处理在H2

基于 Android NDK 的学习之旅----- C调用Java

http://www.cnblogs.com/luxiaofeng54/archive/2011/08/17/2142000.html 基于 Android NDK 的学习之旅----- C调用Java许多成熟的C引擎要移植到Android 平台上使用 , 一般都会 提供 一些接口, 让Android sdk 和 jdk 实现. 下文将会介绍 C 如何 通过 JNI 层调用 Java 的静态和非静态方法. 1.主要流程 1.  新建一个测试类TestProvider.java a)       

【转】基于 Android NDK 的学习之旅-----数据传输(引用数据类型)

原文网址:http://www.cnblogs.com/luxiaofeng54/archive/2011/08/20/2147086.html 基于 Android NDK 的学习之旅-----数据传输二(引用数据类型)(附源码) 基于 Android NDK 的学习之旅-----数据传输(引用数据类型) 接着上篇文章继续讲.主要关于引用类型的数据传输,本文将介绍字符串传输和自定义对象的传输. 1.主要流程 1.  String 字符串传输 a)         上层定义一个native的方法

Android Web Service学习总结(一)

最近学习android平台调用webWebService,学习了一篇不错的博客(http://blog.csdn.net/lyq8479/article/details/6428288),可惜是2011年时的方法,而不适合现在android4.0之后的android版本,所以通过一番学习和研究,总结如下. web Service简介 通俗的理解:通过使用WebService,我们能够像调用本地方法一样去调用远程服务器上的方法.我们并不需要关心远程的那个方法是Java写的,还是PHP或C#写的:我

Android ARM指令学习

在逆向分析Android APK的时候,往往需要分析它的.so文件.这个.so文件就是Linux的动态链接库,只不过是在ARM-cpu下编译的.所以学习Android下的ARM指令很重要.目前,市面上的ARM-cpu基本都支持一种叫做THUMB的指令集模式.这个THUMB指令集可以看作是ARM指令集的子集,只不过ARM指令集为32bit,THUMB指令集为16bit.之所以要使用这个THUMB指令集,主要是为了提升代码密度.具体信息大家可以google. 下面介绍如何简单修改.so文件. 首先,