fdka客户是什么意思(Android开发基于RTMP实现视频直播(附Demo))

前言

这两年视频直播大火。在视频直播领域,有不同的业务提供各种商业解决方案,包括软硬件设备、摄像机、编码器、流媒体服务器等。本文将介绍如何使用一系列免费工具创建一个视频直播节目。

视频直播过程

视频直播的过程可以分为以下几个步骤:

采集->处理->编码封装->推送到服务器->服务器流分发->播放器流



一般我们把流程的前四步称为第一部分,也就是视频主播的操作。视频采集处理后,推送到流媒体服务器,完成第一部分功能。第二部分是流媒体服务器,负责处理从第一部分接收到的流,分发给观众。第三部分是观众。你只需要有一个支持流媒体协议的播放器。

一.收购

采集是整个视频流过程的第一步。它从系统的采集设备中获取原始视频数据,并将其输出到下一步。视频采集涉及数据采集的两个方面:音频采集和图像采集,分别对应两种完全不同的输入源和数据格式。



1.1-音频采集

  • 音频数据可以和图像结合形成视频数据,也可以通过纯音频的方式采集和播放,在很多成熟的应用场景如在线电台、语音电台中发挥着非常重要的作用。在音频采集过程中,环境中的模拟信号主要由设备采集成脉码调制的原始数据,然后编码压缩成MP3格式数据进行分发。常见的音频压缩格式有MP3、AAC、HE-AAC、Opus、FLAC、Vorbis (Ogg)、Speex和AMR等。
  • 音频采集和编码的主要挑战是延迟灵敏度、卡顿灵敏度、噪声消除、回声消除(AEC)、静音检测(VAD)和各种混音算法。
  • 1.2-图像采集

  • 将图像采集的画面结果组合成一组可以连续播放的动画,也就是视频中肉眼可以看到的内容。在图像采集过程中,摄像头等设备拍摄YUV编码的原始数据,然后压缩成H.264格式数据并分发。常见的视频打包格式有mp4、3GP、AVI、MKV、WMV、MPG、VOB、FLV、SWF、MOV、RMVB和WebM等。
  • 由于其最强的直观感受和相对较大的体积,图像构成了视频内容的主要部分。图像采集和编码面临的主要挑战有:设备兼容性差、延迟敏感、卡顿敏感以及美颜、水印等各种图像处理操作。
  • 视频捕获的主要来源是摄像机捕获、屏幕录制和视频文件流。

    二。处理

    或者是视频采集后得到的原始数据。为了增强一些现场效果或者增加一些额外的效果,我们通常会在编码压缩之前对其进行处理,比如公司Logo的时间戳或者水印,祛斑,美颜,语音混淆等。在主播和观众用小麦连接的场景中,主播需要和一个或多个观众进行对话,并将对话结果实时分享给其他所有观众,小麦的部分处理是在流媒体端完成的。



    如上图所示,处理环节分为音频和视频处理。音频处理具体包括混音、降噪和音效,视频处理包括美颜、水印和各种自定义滤镜。

    三。编码和打包

    3.1代码

  • 如果把整个流媒体比作一个物流系统,那么编解码就是分发和加载的过程,非常重要,它的速度和压缩比对物流系统意义重大,影响着物流系统的整体速度和成本。同样,对于流媒体传输来说,编码也很重要。它的编码性能、编码速度和编码压缩比将直接影响整个流媒体传输的用户体验和传输成本。
  • 视频编码的意义
  • 原始视频数据存储量空较大,一个1080P的7 s视频需要817 MB。
  • 原始视频数据的传输占用带宽较大,10 Mbps带宽传输上述7 s视频需要11分钟。
  • 但是经过H.264编码压缩后,视频大小只有708 k,10 Mbps的带宽只需要500 ms就能满足实时传输的需求,所以视频采集传感器采集的原始视频必然要经过视频编码。
  • ⑴。基本原则

  • 为什么一个巨大的原始视频可以编码成一个非常小的视频?技术是什么?核心思想是去除冗余信息:
  • 1.空之间的冗余:图像的相邻像素之间有很强的相关性。
  • 2.时间冗余:视频序列的相邻图像在内容上是相似的。
  • 3.编码冗余:不同的像素值有不同的概率。
  • 4.视觉冗余:人类视觉系统对某些细节不敏感。
  • 5.知识冗余:规则结构可以从先验知识和背景知识中获得。
  • 2。编码器的选择

  • 经过几十年的发展,视频编码器已经从最初支持帧内编码发展到以H.265和VP9为代表的新一代编码器。以下是一些常见的视频编码器:
  • 1.H.264/AVC
  • 2.HEVC/H.265
  • 3.VP8
  • 4.VP9
  • 5.FFmpeg
  • 注:音频编码器包括Mp3、AAC等。

    3.2-套装

  • 用前面的比喻,包装可以理解为用什么样的卡车来运输,也就是介质的容器。
  • 所谓容器,就是混合打包多媒体内容(视频、音频、字幕、章节信息等)的标准。)由编码器生成。它便于同步播放容器中不同的多媒体内容,而容器的另一个作用是为多媒体内容提供索引,也就是说,如果没有容器,你只能从头到尾看一部电影,不能拖动进度条,如果不单独手动加载音频,就没有声音。以下是几种常见的打包格式:
  • 1.AVI格式(后缀。avi)
  • 2.DV-AVI格式(后缀。avi)
  • 3.QuickTime文件格式(后缀。mov)
  • 4.MPEG格式(文件后缀可以是。mpg.mpeg.mpe.dat.vob.asf.3gp.mp4等。)
  • 5.WMV格式(后缀。wmv。asf)
  • 6.真实视频格式(后缀。rm。rmvb)
  • 7.Flash视频格式(后缀。flv)
  • 8.Matroska格式(后缀。mkv)
  • 9.MPEG2-ts格式(后缀。ts)
  • 目前我们在流媒体传输尤其是直播中主要采用FLV和MPEG2-TS格式,分别用于RTMP/HTTP-FLV和HLS协议。

    四。将流推送到服务器

    推流是直播的第一公里,直播的推流对这个直播环节影响很大。如果推流网络不稳定,无论我们怎么优化,观众的体验都会很糟糕。所以也是我们调查问题的第一步。如何系统地解决这类问题,需要我们对相关理论有一个基本的了解。

    有三种主要的推送协议:

  • RTSP(实时流协议):实时流协议,一种用于控制声音或视频的多媒体流协议,由Real Networks和Netscape联合提出。
  • RTMP(实时消息协议):实时消息协议是由Adobe开发的一种开放协议,用于Flash player和服务器之间的音频、视频和数据传输。
  • HLS(HTTP Live Streaming):是苹果公司实现的基于HTTP的流媒体传输协议
  • RTMP协议基于TCP,TCP是一种为实时数据通信而设计的网络协议。主要用于flash/AIR平台与支持RTMP协议的流媒体/交互服务器之间的音频、视频和数据通信。支持该协议的软件包括Adobe Media Server/Ultrant Media Server/red 5等。

    它有三个变种:

  • RTMP是基于TCP的明文协议,使用端口1935;
  • RTMPT封装在HTTP请求中,可以穿越防火墙。
  • RTMPS类似于RTMPT,但是使用HTTPS连接;
  • RTMP是目前主流的流媒体传输协议,广泛应用于直播领域。可以说市面上大部分直播产品都采用了这个协议。

    RTMP协议就像数据包的容器,数据包可以是AMF格式的数据,也可以是FLV的视频/音频数据。一个连接可以通过不同的通道传输多个网络流。这些信道中的分组以固定大小的分组传输。



    V .服务器流分发

    流媒体服务器负责直播流的分发和中继分发。

    流媒体服务器有很多选择,比如Wowza的商业版。但是我选择了Nginx,这是一个优秀的免费Web服务器。后面我会详细介绍如何搭建Nginx服务器。

    六。播放器流

    主要是实现直播节目在终端上的显示。因为我这里使用的传输协议是RTMP,所以任何支持RTMP流协议的播放器都可以使用,比如:

  • 计算机:VLC等
  • 手机:Vitamio,ijkplayer等。


  • 第一部分:采集推流SDK

    目前市面上有很多集视频采集、编码、封装、流媒体于一体的SDK,比如商业版的NodeMedia,但是NodeMedia SDK是通过包名授权的,未授权的包名应用使用版权提示信息。

    这里我用的是github上别人分享的一个免费SDK。因为头条发帖规则不允许插入外部链接,文章评论+私信我得到!

    我用下面的代码来分析一下直播的过程:

    先看入口界面:



    很简单,一个输入框让你填写服务器的推送地址,另一个按钮开始推送。

    公共类StartActivity扩展Activity { public static final String RTM purl _ MESSAGE = " RTM push . hx . com . RTM push . RTM purl "; private Button _ startrtmpbutton = null; private EditText _ rtmpUrlEditText = null; 私观。onclick listener _ startRtmpPushOnClickedEvent =新视图。onClick listener(){ @ Override public void onClick(View arg 0){ Intent I = new Intent(start activity . this,main activity . class); String RTM purl = _ rtmpurledittext . gettext()。toString(); i.putExtra(StartActivity。RTMPURL_MESSAGE,RTM purl); start activity . this . start activity(I); } }; private void InitUI(){ _ rtmpurletext =(EditText)findViewById(r . id . rtmpurletext); _ startrtmpbutton =(Button)findViewById(r . id . startrtmpbutton); _ rtmpurledittext . settext(" rtmp://192 . 168 . 1 . 104:1935 ve/12345 "); _ startrtmppush button . setonclicklistener(_ startRtmpPushOnClickedEvent); } @ Override protected void onCreate(Bundle savedInstanceState){ super . onCreate(savedInstanceState); setContentView(r . layout . activity _ start); InitUI(); } } 主推流流程在MainActivity中。同样,先看界面:



    布局文件:

    & ltrelative layout xmlns:Android = " http://schemas . Android . com/apk/RES/Android " xmlns:tools = " http://schemas . Android . com/tools " Android:id = " @+id/camera relative "/h/]Android:layout _ width = " match _ parent " Android:layout _ height = " match _ parent "/h/]Android:padding bottom = " @ dimen/activity _ vertical _ margin " Android:padding全屏" >; & lt;surface view Android:id = " @+id/surface view ex " Android:layout _ width = " match _ parent " Android:layout _ height = " match _ parent "/& gt; & lt;button Android:id = " @+id/SwitchCamerabutton " Android:layout _ width = " wrap _ content " Android:layout _ height = " wrap _ content " Android:layout _ align bottom = " @+id/surface view ex "/h/]Android:text = " @ string/switch camera "/& gt; & lt;/relative layout & gt; 其实是用一个SurfaceView来显示相机拍摄的图片,并提供了一个按钮来切换前后摄像头。从入口功能:

    @ Override protected void onCreate(Bundle savedInstanceState){ request Window feature(Window。特征_编号_标题); getWindow()。setFlags(WindowManager。LayoutParams.FLAG_FULLSCREEN, WindowManager。layout params . FLAG _ full screen); this.getWindow()。setFlags(WindowManager。layout params . FLAG _ KEEP _ SCREEN _ ON,WindowManager。layout params . FLAG _ KEEP _ SCREEN _ ON); super . oncreate(savedInstanceState); setContentView(r . layout . activity _ main); setrequesteorientation(activity info。屏幕_方位_人像); Intent Intent = getIntent(); _ RTM purl = intent . getstring extra(start activity。RTM purl _ MESSAGE); InitAll(); power manager pm =(power manager)getsystem service(Context。POWER _ SERVICE); _ wake lock = pm . new wake lock(power manager。SCREEN_DIM_WAKE_LOCK,“我的标签”); } 首先设置全屏显示、常亮、竖屏,获取服务器的推送url,然后初始化一切。

    private void InitAll(){ window manager WM = this . getwindow manager(); int width = WM . getdefaultdisplay()。getWidth(); int height = WM . getdefaultdisplay()。getHeight(); int iNewWidth =(int)(height * 3.0/4.0); relative layout rCameraLayout =(relative layout)findViewById(r . id . camera relative); RelativeLayout。layout params layout params = new relative layout。LayoutParams(RelativeLayout。LayoutParams.MATCH_PARENT, RelativeLayout。layout params . MATCH _ PARENT); int iPos = width-iNewWidth; layout params . set margins(iPos,0,0,0); _ mSurfaceView =(surface view)this . findviewbyid(r . id . surfaceviewex); _mSurfaceView.getHolder()。setFixedSize(HEIGHT_DEF,WIDTH _ DEF); _mSurfaceView.getHolder()。setType(SurfaceHolder。表面类型推送缓冲区); _mSurfaceView.getHolder()。setKeepScreenOn(true); _mSurfaceView.getHolder()。add callback(new SurceCallBack()); _ msurfaceview . setlayoutparams(layout params); init audio record(); _ SwitchCameraBtn =(Button)findViewById(r . id . switchcamerabutton); _ switchcamerabtn . setonclicklistener(_ switchCameraOnClickedEvent); RtmpStartMessage();//Start streaming } 首先设置3:4显示的屏幕比例,为SurfaceView设置一些参数并添加回调,然后初始化AudioRecord,最后开始流式播放。音频在这里初始化,那么摄像头在哪里初始化呢?其实在SurfaceView的回调函数里。

    @ Override public void surface created(surface holder holder){ _ iDegrees = getDisplayOritation(getdisplayrotation(),0); if (_mCamera!= null){ init camera();//初始化摄像头 返回; } /华为i7共享摄像头 if(摄像头。getnumberofcameras()= = 1){ _ bis front = false; _ MC amera = Camera . open(Camera。CAMERA info . CAMERA _ FACING _ BACK); } else { _ MC amera = Camera . open(Camera。CAMERA info . CAMERA _ FACING _ FRONT); } init camera(); } @ override Public void surface destroyed(surface holder){ } } 相机的初始化在这里:

    public void init Camera(){ Camera。parameters p = _ MC amera . get parameters(); Size preview Size = p . getpreviewsize(); showlog("原始宽度:"+preview size . Width+",高度:"+preview size . height); List & lt;Size & gtPreviewSizeList = p . getsupportedpreviewsizes(); List & lt;整数& gtpreview formats = p . getsupportedpreviewformats(); showlog("列出所有支持的预览大小"); for(相机。size size:PreviewSizeList){ showlog(" w:"+size . width+",h:"+size . height); } showlog("列出所有支持的预览格式"); Integer inv 21 flag = 0; Integer IYV 12 flag = 0; for(Integer YUV format:preview formats){ showlog(" preview format:"+YUV format); if(YUV format = = Android . graphics . image format . yv12){ IYV 12 flag = Android . graphics . image format . yv12; } if(YUV format = = Android . graphics . image format . nv21){ inv 21 flag = Android . graphics . image format . nv21; } } if (iNV21Flag!= 0){ _ iCameraCodecType = inv 21 flag; } else if (iYV12Flag!= 0){ _ iCameraCodecType = IYV 12 flag; } p . setpreviewsize(HEIGHT _ DEF,WIDTH _ DEF); p . setpreviewformat(_ icameracodetype); p . setpreviewframrate(frame rate _ DEF); showlog(" _ iDegrees = "+_ iDegrees); _ MC amera . setdisplayorientation(_ I grees); p . set rotation(_ I grees); _ MC amera . setpreviewcallback(_ preview callback); _ MC amera . set parameters(p); try { _ MC amera . setpreviewdisplay(_ msurfaceview . get holder()); } catch(Exception e){ return; } _ MC amera . cancelautofocus();//只有加上这句话,才会实现自动对焦。 _ MC amera . start preview(); } 初始化后记得启动推送功能?

    private void RtmpStartMessage(){ Message msg = new Message(); msg . what = ID _ RTMP _推送_开始; Bundle b = new Bundle(); b.putInt("ret ",0); msg . setdata(b); mhandler . sendmessage(msg); } 处理程序处理:

    public Handler mHandler = new Handler(){ public void handle message(Android . OS . message msg){ Bundle b = msg . get data(); int ret; switch(msg . what){ case ID _ RTMP _推送_开始:{ START(); break; } } } }; 推流的真正实现在这里:

    private void Start(){ if(DEBUG _ ENABLE){ File saveDir = environment . getexternalseraturedirectory(); String strFilename = saveDir+"/AAA . h264 "; 试试{ if(!新文件(strFilename)。exists()) { 新文件(strFilename)。create new file(); } _ output stream = new data output stream(new file output stream(strFilename)); } catch(Exception e){ e . printstacktrace(); } } //_ rtmpSessionMgr。start(" rtmp://192 . 168 . 0 . 110 ve/12345678 "); _ rtmpSessionMgr = new RtmpSessionManager(); _rtmpSessionMgr。start(_ RTM purl);//-point 1 int iFormat = _ icameracodetype; _ swench 264 = new swvideo encoder(WIDTH _ DEF,HEIGHT_DEF,FRAMERATE_DEF,BITRATE _ DEF); _ swench 264 . start(iFormat);//-point 2 _ bStartFlag = true; _ h 264 encoder Thread = new Thread(_ h 264 runnable); _ h 264 encoder Thread . set priority(线程。MAX _ PRIORITY); _ h 264 encoder thread . start();//-point 3 _ audio recorder . startrecording(); _ AacEncoderThread = new Thread(_ aacEncoderRunnable); _ aacencoterthread . set priority(Thread。MAX _ PRIORITY); _ aacencoderthread . start();//-point 4中有四个主要函数 } 。我已经把它们分开标记了。现在让我们一个一个来看看。首先点1,已经进SDK了。

    public int Start(String RTM purl){ int iRet = 0; _ RTM purl = RTM purl; _ rtmp session = new rtmp session(); _ bStartFlag = true; _ h 264 encoder Thread . set priority(线程。MAX _ PRIORITY); _ h 264 encoder thread . start(); return iRet; } 居然启动了一个线程,有点复杂。

    private Thread _ h 264 encoder Thread = new Thread(new Runnable(){ private Boolean WaitforReConnect(){ for(int I = 0;我& lt500;i++){ try { thread . sleep(10); } catch(interrupted exception e){ e . printstacktrace(); } if(_ h 264 encoder thread . interrupted()| |(!_bStartFlag)){ 返回false } } 返回true } @ Override public void run(){ while(!_ h 264 encoder thread . interrupted()& amp;& amp(_ bStartFlag)){ if(_ RTM handle = = 0){ _ RTM handle = _ rtmp session。rtmp connect(_ RTM purl); if(_ RTM handle = = 0){ if(!WaitforReConnect()){ break; } 继续; } } else { if(_ rtmp session。RtmpIsConnect(_ rtmpHandle)= = 0){ _ rtmpHandle = _ rtmp session。rtmp connect(_ RTM purl); if(_ RTM handle = = 0){ if(!WaitforReConnect()){ break; } 继续; } } } if((_ video data queue . size()= = 0)& amp;& amp(_ audiodataqueue . size()= = 0)){ try { thread . sleep(30); } catch(interrupted exception e){ e . printstacktrace(); } 继续; } //Log.i(TAG," video queue length = "+_ video data queue . size()+",audio queue length = "+_ audio data queue . size()); for(int I = 0;我& lt100;i++){ byte[]audioData = getandreleasaudioqueue(); if(audioData = = null){ break; } //Log.i(TAG," # # # RtmpSendAudioData:"+audiodata . length); _rtmpSession。RtmpSendAudioData(_ RTM handle,AudioData,audioData . length); } byte[]video data = GetAndReleaseVideoQueue(); if(videoData!= null){ //Log.i(TAG," $ $ $ RtmpSendVideoData:"+videodata . length); _rtmpSession。RtmpSendVideoData(_ RTM handle,VideoData,videoData . length); } 试试{ thread . sleep(1); } catch(interrupted exception e){ e . printstacktrace(); } } _ videodataquelock . lock(); _ video data queue . clear(); _ videodataquelock . unlock(); _ audiodataquelock . lock(); _ audiodataqueue . clear(); _ audiodataquelock . unlock(); if((_ RTM handle!= 0)& amp;& amp(_rtmpSession!= null)){ _rtmpSession。rtmp disconnect(_ RTM handle); } _ RTM handle = 0; _ rtmp session = null; } }); 看第18行,主要是while循环。每隔一段时间,从_ audiotaqueue和_videoDataQueue两个缓冲区数组中获取数据并发送给服务器。发送方法_ rtmpsession。rtmpsendeaudiodataand _ rtmp session。rtmpsendevideodata都是本机方法。通过jni调用so库文件的内容。每隔一段时间几点?再看第四行,结果是5秒,也就是说我们的视频数据在取出发送到服务器之前会在缓冲区存储5秒,所有的直播都会有5秒的延迟。我们可以修改这个块来控制直播延迟。

    上面说我们会从两个Buffer _ audiotaqueue和_videoDataQueue中获取数据,那么数据什么时候放进去呢?看上面的点2,3,4 3,4。首先,点2,也进入了SDK:

    public boolean start(int iFormateType){ int iType = openh 264 encoder。YUV420 _ TYPE if(iformate type = = Android . graphics . image format . yv12){ iType = openh 264 encoder。YUV12 _ TYPE } else { iType = openh 264 encoder。YUV420 _ TYPE } _ openh 264 encoder = new openh 264 encoder(); _iHandle = _OpenH264Encoder。InitEncode(_iWidth,_iHeight,_iBitRate,_iFrameRate,iType); if(_iHandle == 0){ 返回false } _ iformat type = iformate type; 返回true } 其实这是初始化编码器,具体的初始化过程也在so文件里,jni调用。第3、4和4点实际上启动了两个线程,所以让我们看看线程中的具体实现。

    private Thread _ h 264 encoder Thread = null; private Runnable _ h 264 Runnable = new Runnable(){ @ Override public void run(){ while(!_ h 264 encoder thread . interrupted()& amp;& amp_ bStartFlag){ int iSize = _ YUV queue . size(); if(iSize & gt;0){ _ yuvqueuelock . lock(); byte[]YUV data = _ YUV queue . poll(); if(iSize & gt;9) { Log.i(LOG_TAG," # # # YUV Queue len = "+_ YUV Queue . size()+",YUV length = "+YUV data . length); } _ yuvqueuelock . unlock(); if(YUV data = = null){ continue; } if(_ bis front){ _ yuvEdit = _ swen ch 264。YUV420pRotate270(yuvData,HEIGHT_DEF,WIDTH _ DEF); } else { _ yuvEdit = _ swen ch 264。YUV420pRotate90(yuvData,HEIGHT_DEF,WIDTH _ DEF); } byte[]h 264 data = _ swench 264。encoder h264(_ yuvEdit); if (h264Data!= null) { _rtmpSessionMgr。InsertVideoData(h 264 data); if(DEBUG _ ENABLE){ try { _ output stream . write(h 264 data); int ih 264 len = h 264 data . length; //Log.i(LOG_TAG," Encode H264 len = "+ih 264 len); } catch(io exception E1){ E1 . printstacktrace(); } } } } try { thread . sleep(1); } catch(interrupted exception e){ //TODO自动生成的catch块 e . printstacktrace(); } } _ YUV queue . clear(); } }; 也是循环线程,第9行,从_YUVQueue中取出摄像头采集的数据,然后旋转视频,第24行,对数据进行编码,然后执行第26行,InsertVideoData:

    public void InsertVideoData(byte[]videoData){ if(!_ bStartFlag){ return; } _ videodataquelock . lock(); if(_ video data queue . size()& gt;50){ _ videodataqueue . clear(); } _ video data queue . offer(video data); _ videodataquelock . unlock(); } 确实是前面提到的_videoDataQueue的缓冲区。视频数据插入此处。音频数据呢?在另一个线程中,内容大致相同。

    private Runnable _ aacEncoderRunnable = new Runnable(){ @ Override public void run(){ data output stream output stream = null; if(DEBUG _ ENABLE){ File saveDir = environment . getexternalstoratedirectory(); String strFilename = saveDir+"/AAA . AAC "; 试试{ if(!新文件(strFilename)。exists()) { 新文件(strFilename)。create new file(); } output stream = new data output stream(new file output stream(strFilename)); } catch(异常E1){ E1 . printstacktrace(); } } long lSleepTime = SAMPLE _ RATE _ DEF * 16 * 2/_ recorder buffer . length; while(!_ aacencoderthread . interrupted()& amp;& amp_ bStartFlag){ int iPCMLen = _ audio recorder . read(_ recorder buffer,0,_ recorder buffer . length);//填充缓冲区 if ((iPCMLen!= _音频记录器。ERROR _ BAD _ VALUE)& amp;& amp(iPCMLen!= 0)) { if (_fdkaacHandle!= 0){ byte[]AAC buffer = _ fdkaacEnc。FdkAacEncode(_fdkaacHandle,_ recorder buffer); if (aacBuffer!= null){ long lLen = AAC buffer . length; _rtmpSessionMgr。insert audiodata(AAC buffer); //Log.i(LOG_TAG," fdk AAC length = "+lLen+" from PCM = "+iPCMLen); if(DEBUG _ ENABLE){ try { output stream . write(AAC buffer); } catch(io exception e){ //TODO自动生成的catch块 e . printstacktrace(); } } } } } } else { LOG . I(LOG _ TAG," # # # # # #获取PCM数据失败"); } 试试{ thread . sleep(lSleepTime/10); } catch(interrupted exception e){ e . printstacktrace(); } } Log.i(LOG_TAG,“AAC编码器线程结束……”); } }; private Thread _ AacEncoderThread = null; 这是通过循环将音频数据插入_ audiotaqueue的缓冲区。

    以上是视频采集和流式传输的代码分析。演示中没有视频处理,只有摄像头捕捉,编码和传输到服务器。

    第二部分:Nginx服务器搭建

    流媒体服务器有很多选择,比如Wowza的商业版。但是我选择了免费的Nginx(nginx-rtmp-module)。Nginx本身就是一个优秀的HTTP服务器,通过nginx-rtmp-module可以搭建一个功能相对完善的流媒体服务器。该流媒体服务器可以支持RTMP和HLS。

    Nginx配合SDK作为流媒体服务器的原理是:Nginx通过rtmp模块提供rtmp服务,SDK向Nginx推送一个rtmp流,然后客户端通过访问Nginx观看实时视频流。HLS也是一样的原理,只是最终客户端是通过HTTP协议访问的,但是SDK推送流还是rtmp。

    集成rtmp模块的windows版Nginx。下载后可以直接使用,因为头条发帖规则不允许插入外部链接,所以文章下评论+私信我懂了!

    1。rtmp端口配置

    配置文件位于/conf/nginx.conf中。

    RTMP监控端口1935并启用实时和hls应用程序。



    所以你的流媒体服务器url可以写成:rtmp://(服务器IP地址):1935/live/xxx或者rtmp://(服务器IP地址):1935/hls/xxx。

    比如上面写的RTMP://192 . 168 . 1 . 104:1935/live/12345。

    HTTP侦听端口8080,

  • :8080/stat查看流的状态
  • :8080/index.html是一个直播和直播测试器。
  • :8080/vod.html是一个按需支持RTMP和HLS的测试仪。
  • 2。启动nginx服务

    双击nginx文件或在dos窗口中运行nginx来启动nginx服务:



    1)启动任务管理器,你可以看到nginx.exe进程。

    ka客户是什么意思



    2)打开网页,输入http://localhot:8080。将出现以下屏幕:



    显示以上界面说明启动成功。第三部分:直播流的播放

    锚接口:



    如上所述,任何支持RTMP流媒体协议的播放器都可以接收我们的直播。这里有两个例子:

    (1)橱窗女郎VLC





    安卓播放器ijkplayer

    private void initPlayer() { player = new PlayerManager(this); player.setFullScreenOnly(true); player.setScaleType(PlayerManager.SCALETYPE_FILLPARENT); player.playInFullScreen(true); player.setPlayerStateListener(this); player.play("rtmp://192.168.1.104:1935ve/12345"); }


    private void init player(){ player = new player manager(this); player . setfullscreenonly(true); player . setscaletype(player manager。scale type _ fill parent); player . playing full screen(true); player . setplayerstatelistener(this); player . play(" rtmp://192 . 168 . 1 . 104:1935 ve/12345 "); }

    总结

    至此,整个基于RTMP推流的Android视频直播项目已经完成。如果你有更好的想法,可以在文章底部留言评论或者私信我!另外,上一篇文章第二部分提到的streaming SDK和第三部分提到的集成rtmp模块的windows版本的Nginx下载地址,由于头条发布规则,不允许插入外部链接。如有需要,可在文章私信评论后回复【下载地址】![/s2/]

    您可以还会对下面的文章感兴趣

    使用微信扫描二维码后

    点击右上角发送给好友