这两年视频直播大火。在视频直播领域,有不同的业务提供各种商业解决方案,包括软硬件设备、摄像机、编码器、流媒体服务器等。本文将介绍如何使用一系列免费工具创建一个视频直播节目。
视频直播过程
视频直播的过程可以分为以下几个步骤:
采集->处理->编码封装->推送到服务器->服务器流分发->播放器流
一般我们把流程的前四步称为第一部分,也就是视频主播的操作。视频采集处理后,推送到流媒体服务器,完成第一部分功能。第二部分是流媒体服务器,负责处理从第一部分接收到的流,分发给观众。第三部分是观众。你只需要有一个支持流媒体协议的播放器。
一.收购
采集是整个视频流过程的第一步。它从系统的采集设备中获取原始视频数据,并将其输出到下一步。视频采集涉及数据采集的两个方面:音频采集和图像采集,分别对应两种完全不同的输入源和数据格式。
1.1-音频采集
1.2-图像采集
视频捕获的主要来源是摄像机捕获、屏幕录制和视频文件流。
二。处理
或者是视频采集后得到的原始数据。为了增强一些现场效果或者增加一些额外的效果,我们通常会在编码压缩之前对其进行处理,比如公司Logo的时间戳或者水印,祛斑,美颜,语音混淆等。在主播和观众用小麦连接的场景中,主播需要和一个或多个观众进行对话,并将对话结果实时分享给其他所有观众,小麦的部分处理是在流媒体端完成的。
如上图所示,处理环节分为音频和视频处理。音频处理具体包括混音、降噪和音效,视频处理包括美颜、水印和各种自定义滤镜。
三。编码和打包
3.1代码
⑴。基本原则
2。编码器的选择
注:音频编码器包括Mp3、AAC等。
3.2-套装
目前我们在流媒体传输尤其是直播中主要采用FLV和MPEG2-TS格式,分别用于RTMP/HTTP-FLV和HLS协议。
四。将流推送到服务器
推流是直播的第一公里,直播的推流对这个直播环节影响很大。如果推流网络不稳定,无论我们怎么优化,观众的体验都会很糟糕。所以也是我们调查问题的第一步。如何系统地解决这类问题,需要我们对相关理论有一个基本的了解。
有三种主要的推送协议:
RTMP协议基于TCP,TCP是一种为实时数据通信而设计的网络协议。主要用于flash/AIR平台与支持RTMP协议的流媒体/交互服务器之间的音频、视频和数据通信。支持该协议的软件包括Adobe Media Server/Ultrant Media Server/red 5等。
它有三个变种:
RTMP是目前主流的流媒体传输协议,广泛应用于直播领域。可以说市面上大部分直播产品都采用了这个协议。
RTMP协议就像数据包的容器,数据包可以是AMF格式的数据,也可以是FLV的视频/音频数据。一个连接可以通过不同的通道传输多个网络流。这些信道中的分组以固定大小的分组传输。
V .服务器流分发
流媒体服务器负责直播流的分发和中继分发。
流媒体服务器有很多选择,比如Wowza的商业版。但是我选择了Nginx,这是一个优秀的免费Web服务器。后面我会详细介绍如何搭建Nginx服务器。
六。播放器流
主要是实现直播节目在终端上的显示。因为我这里使用的传输协议是RTMP,所以任何支持RTMP流协议的播放器都可以使用,比如:
目前市面上有很多集视频采集、编码、封装、流媒体于一体的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,
2。启动nginx服务
双击nginx文件或在dos窗口中运行nginx来启动nginx服务:
1)启动任务管理器,你可以看到nginx.exe进程。
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/]