摘要:視頻采集處理后推流到流媒體服務(wù)器,第一部分功能完成。第二部分就是流媒體服務(wù)器,負(fù)責(zé)把從第一部分接收到的流進行處理并分發(fā)給觀眾。五服務(wù)器流分發(fā)流媒體服務(wù)器的作用是負(fù)責(zé)直播流的發(fā)布和轉(zhuǎn)播分發(fā)功能。
前言
近兩年時間,視頻直播可謂大火。在視頻直播領(lǐng)域,有不同的商家提供各種的商業(yè)解決方案,包括軟硬件設(shè)備,攝像機,編碼器,流媒體服務(wù)器等。本文要講解的是如何使用一系列免費工具,打造一套視頻直播方案。
視頻直播流程
視頻直播的流程可以分為如下幾步:
采集 —>處理—>編碼和封裝—>推流到服務(wù)器—>服務(wù)器流分發(fā)—>播放器流播放
一般情況下我們把流程的前四步稱為第一部分,即視頻主播端的操作。視頻采集處理后推流到流媒體服務(wù)器,第一部分功能完成。第二部分就是流媒體服務(wù)器,負(fù)責(zé)把從第一部分接收到的流進行處理并分發(fā)給觀眾。第三部分就是觀眾啦,只需要擁有支持流傳輸協(xié)議的播放器即可。
一、采集
采集是整個視頻推流過程中的第一個環(huán)節(jié),它從系統(tǒng)的采集設(shè)備中獲取原始視頻數(shù)據(jù),將其輸出到下一個環(huán)節(jié)。視頻的采集涉及兩方面數(shù)據(jù)的采集:音頻采集和圖像采集,它們分別對應(yīng)兩種完全不同的輸入源和數(shù)據(jù)格式。
1.1-音頻采集
音頻數(shù)據(jù)既能與圖像結(jié)合組合成視頻數(shù)據(jù),也能以純音頻的方式采集播放,后者在很多成熟的應(yīng)用場景如在線電臺和語音電臺等起著非常重要的作用。音頻的采集過程主要通過設(shè)備將環(huán)境中的模擬信號采集成
PCM 編碼的原始數(shù)據(jù),然后編碼壓縮成 MP3
等格式的數(shù)據(jù)分發(fā)出去。常見的音頻壓縮格式有:MP3,AAC,HE-AAC,Opus,F(xiàn)LAC,Vorbis (Ogg),Speex 和
AMR等。
音頻采集和編碼主要面臨的挑戰(zhàn)在于:延時敏感、卡頓敏感、噪聲消除(Denoise)、回聲消除(AEC)、靜音檢測(VAD)和各種混音算法等。
1.2-圖像采集
將圖像采集的圖片結(jié)果組合成一組連續(xù)播放的動畫,即構(gòu)成視頻中可肉眼觀看的內(nèi)容。圖像的采集過程主要由攝像頭等設(shè)備拍攝成 YUV
編碼的原始數(shù)據(jù),然后經(jīng)過編碼壓縮成 H.264
等格式的數(shù)據(jù)分發(fā)出去。常見的視頻封裝格式有:MP4、3GP、AVI、MKV、WMV、MPG、VOB、FLV、SWF、MOV、RMVB 和
WebM 等。
圖像由于其直觀感受最強并且體積也比較大,構(gòu)成了一個視頻內(nèi)容的主要部分。圖像采集和編碼面臨的主要挑戰(zhàn)在于:設(shè)備兼容性差、延時敏感、卡頓敏感以及各種對圖像的處理操作如美顏和水印等。
視頻采集的采集源主要有 攝像頭采集、屏幕錄制和從視頻文件推流。
二、處理
視頻或者音頻完成采集之后得到原始數(shù)據(jù),為了增強一些現(xiàn)場效果或者加上一些額外的效果,我們一般會在將其編碼壓縮前進行處理,比如打上時間戳或者公司 Logo 的水印,祛斑美顏和聲音混淆等處理。在主播和觀眾連麥場景中,主播需要和某個或者多個觀眾進行對話,并將對話結(jié)果實時分享給其他所有觀眾,連麥的處理也有部分工作在推流端完成。
如上圖所示,處理環(huán)節(jié)中分為音頻和視頻處理,音頻處理中具體包含混音、降噪和聲音特效等處理,視頻處理中包含美顏、水印、以及各種自定義濾鏡等處理。
三、編碼和封裝
3.1-編碼
如果把整個流媒體比喻成一個物流系統(tǒng),那么編解碼就是其中配貨和裝貨的過程,這個過程非常重要,它的速度和壓縮比對物流系統(tǒng)的意義非常大,影響物流系統(tǒng)的整體速度和成本。同樣,對流媒體傳輸來說,編碼也非常重要,它的編碼性能、編碼速度和編碼壓縮比會直接影響整個流媒體傳輸?shù)挠脩趔w驗和傳輸成本。
視頻編碼的意義
原始視頻數(shù)據(jù)存儲空間大,一個 1080P 的 7 s 視頻需要 817 MB
原始視頻數(shù)據(jù)傳輸占用帶寬大,10 Mbps 的帶寬傳輸上述 7 s 視頻需要 11 分鐘
而經(jīng)過 H.264 編碼壓縮之后,視頻大小只有 708 k ,10 Mbps 的帶寬僅僅需要 500 ms
可以滿足實時傳輸?shù)男枨螅詮囊曨l采集傳感器采集來的原始視頻勢必要經(jīng)過視頻編碼。
⑴.基本原理
為什么巨大的原始視頻可以編碼成很小的視頻呢?這其中的技術(shù)是什么呢?核心思想就是去除冗余信息:
1、空間冗余:圖像相鄰像素之間有較強的相關(guān)性
2、時間冗余:視頻序列的相鄰圖像之間內(nèi)容相似
3、編碼冗余:不同像素值出現(xiàn)的概率不同
4、視覺冗余:人的視覺系統(tǒng)對某些細(xì)節(jié)不敏感
5、知識冗余:規(guī)律性的結(jié)構(gòu)可由先驗知識和背景知識得到
⑵.編碼器的選擇
視頻編碼器經(jīng)歷了數(shù)十年的發(fā)展,已經(jīng)從開始的只支持幀內(nèi)編碼演進到現(xiàn)如今的 H.265和 VP9 為代表的新一代編碼器,下面是一些常見的視頻編碼器:
1.H.264/AVC
2.HEVC/H.265
3.VP8
4.VP9
5.FFmpeg
注:音頻編碼器有Mp3, AAC等。
3.2-封裝
沿用前面的比喻,封裝可以理解為采用哪種貨車去運輸,也就是媒體的容器。
所謂容器,就是把編碼器生成的多媒體內(nèi)容(視頻,音頻,字幕,章節(jié)信息等)混合封裝在一起的標(biāo)準(zhǔn)。容器使得不同多媒體內(nèi)容同步播放變得很簡單,而容器的另一個作用就是為多媒體內(nèi)容提供索引,也就是說如果沒有容器存在的話一部影片你只能從一開始看到最后,不能拖動進度條,而且如果你不自己去手動另外載入音頻就沒有聲音。下面是幾種常見的封裝格式:
1.AVI 格式(后綴為 .avi)
2.DV-AVI 格式(后綴為 .avi)
3.QuickTime File Format 格式(后綴為 .mov)
4.MPEG 格式(文件后綴可以是 .mpg .mpeg .mpe .dat .vob .asf .3gp .mp4等)
5.WMV 格式(后綴為.wmv .asf)
6.Real Video 格式(后綴為 .rm .rmvb)
7.Flash Video 格式(后綴為 .flv)
8.Matroska 格式(后綴為 .mkv)
9.MPEG2-TS 格式 (后綴為 .ts)
目前,我們在流媒體傳輸,尤其是直播中主要采用的就是 FLV 和 MPEG2-TS 格式,分別用于 RTMP/HTTP-FLV 和 HLS協(xié)議。
四、推流到服務(wù)器
推流是直播的第一公里,直播的推流對這個直播鏈路影響非常大,如果推流的網(wǎng)絡(luò)不穩(wěn)定,無論我們?nèi)绾巫鰞?yōu)化,觀眾的體驗都會很糟糕。所以也是我們排查問題的第一步,如何系統(tǒng)地解決這類問題需要我們對相關(guān)理論有基礎(chǔ)的認(rèn)識。
推送協(xié)議主要有三種:
RTSP(Real Time Streaming Protocol):實時流傳送協(xié)議,是用來控制聲音或影像的多媒體串流協(xié)議, 由Real Networks和Netscape共同提出的;
RTMP(Real Time Messaging Protocol):實時消息傳送協(xié)議,是Adobe公司為Flash播放器和服務(wù)器之間音頻、視頻和數(shù)據(jù)傳輸 開發(fā)的開放協(xié)議;
HLS(HTTP Live Streaming):是蘋果公司(Apple Inc.)實現(xiàn)的基于HTTP的流媒體傳輸協(xié)議;
RTMP協(xié)議基于 TCP,是一種設(shè)計用來進行實時數(shù)據(jù)通信的網(wǎng)絡(luò)協(xié)議,主要用來在 flash/AIR 平臺和支持 RTMP 協(xié)議的流媒體/交互服務(wù)器之間進行音視頻和數(shù)據(jù)通信。支持該協(xié)議的軟件包括 Adobe Media Server/Ultrant Media Server/red5 等。
它有三種變種:
RTMP工作在TCP之上的明文協(xié)議,使用端口1935;
RTMPT封裝在HTTP請求之中,可穿越防火墻;
RTMPS類似RTMPT,但使用的是HTTPS連接;
RTMP 是目前主流的流媒體傳輸協(xié)議,廣泛用于直播領(lǐng)域,可以說市面上絕大多數(shù)的直播產(chǎn)品都采用了這個協(xié)議。
RTMP協(xié)議就像一個用來裝數(shù)據(jù)包的容器,這些數(shù)據(jù)可以是AMF格式的數(shù)據(jù),也可以是FLV中的視/音頻數(shù)據(jù)。一個單一的連接可以通過不同的通道傳輸多路網(wǎng)絡(luò)流。這些通道中的包都是按照固定大小的包傳輸?shù)摹?/p>
五、服務(wù)器流分發(fā)
流媒體服務(wù)器的作用是負(fù)責(zé)直播流的發(fā)布和轉(zhuǎn)播分發(fā)功能。
流媒體服務(wù)器有諸多選擇,如商業(yè)版的Wowza。但我選擇的是Nginx,它是一款優(yōu)秀的免費Web服務(wù)器,后面我會詳細(xì)介紹如何搭建Nginx服務(wù)器。
六、播放器流播放
主要是實現(xiàn)直播節(jié)目在終端上的展現(xiàn)。因為我這里使用的傳輸協(xié)議是RTMP, 所以只要支持 RTMP 流協(xié)議的播放器都可以使用,譬如:
電腦端:VLC等
手機端:Vitamio以及ijkplayer等
第一部分:采集推流SDK
目前市面上集視頻采集、編碼、封裝和推流于一體的SDK已經(jīng)有很多了,例如商業(yè)版的NodeMedia,但NodeMedia SDK按包名授權(quán),未授權(quán)包名應(yīng)用使用有版權(quán)提示信息。
我這里使用的是別人分享在github上的一個免費SDK。文章下點贊+私信我獲取!
下面我就代碼分析一下直播推流的過程吧:
先看入口界面:
很簡單,一個輸入框讓你填寫服務(wù)器的推流地址,另外一個按鈕開啟推流。
public class StartActivity extends Activity { public static final String RTMPURL_MESSAGE = "rtmppush.hx.com.rtmppush.rtmpurl"; private Button _startRtmpPushButton = null; private EditText _rtmpUrlEditText = null; private View.OnClickListener _startRtmpPushOnClickedEvent = new View.OnClickListener() { @Override public void onClick(View arg0) { Intent i = new Intent(StartActivity.this, MainActivity.class); String rtmpUrl = _rtmpUrlEditText.getText().toString(); i.putExtra(StartActivity.RTMPURL_MESSAGE, rtmpUrl); StartActivity.this.startActivity(i); } }; private void InitUI(){ _rtmpUrlEditText = (EditText)findViewById(R.id.rtmpUrleditText); _startRtmpPushButton = (Button)findViewById(R.id.startRtmpButton); _rtmpUrlEditText.setText("rtmp://192.168.1.104:1935/live/12345"); _startRtmpPushButton.setOnClickListener(_startRtmpPushOnClickedEvent); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_start); InitUI(); } }
主要的推流過程在MainActivity里面,同樣,先看界面:
布局文件:
其實就是用一個SurfaceView顯示攝像頭拍攝畫面,并提供了一個按鈕切換前置和后置攝像頭。從入口函數(shù)看起:
@Override protected void onCreate(Bundle savedInstanceState) { requestWindowFeature(Window.FEATURE_NO_TITLE); getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); Intent intent = getIntent(); _rtmpUrl = intent.getStringExtra(StartActivity.RTMPURL_MESSAGE); InitAll(); PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); _wakeLock = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, "My Tag"); }
首先設(shè)置全屏顯示,常亮,豎屏,獲取服務(wù)器的推流url,再初始化所有東西。
private void InitAll() { WindowManager wm = this.getWindowManager(); int width = wm.getDefaultDisplay().getWidth(); int height = wm.getDefaultDisplay().getHeight(); int iNewWidth = (int) (height * 3.0 / 4.0); RelativeLayout rCameraLayout = (RelativeLayout) findViewById(R.id.cameraRelative); RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT); int iPos = width - iNewWidth; layoutParams.setMargins(iPos, 0, 0, 0); _mSurfaceView = (SurfaceView) this.findViewById(R.id.surfaceViewEx); _mSurfaceView.getHolder().setFixedSize(HEIGHT_DEF, WIDTH_DEF); _mSurfaceView.getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); _mSurfaceView.getHolder().setKeepScreenOn(true); _mSurfaceView.getHolder().addCallback(new SurceCallBack()); _mSurfaceView.setLayoutParams(layoutParams); InitAudioRecord(); _SwitchCameraBtn = (Button) findViewById(R.id.SwitchCamerabutton); _SwitchCameraBtn.setOnClickListener(_switchCameraOnClickedEvent); RtmpStartMessage();//開始推流 }
首先設(shè)置屏幕比例3:4顯示,給SurfaceView設(shè)置一些參數(shù)并添加回調(diào),再初始化AudioRecord,最后執(zhí)行開始推流。音頻在這里初始化了,那么相機在哪里初始化呢?其實在SurfaceView的回調(diào)函數(shù)里。
@Override public void surfaceCreated(SurfaceHolder holder) { _iDegrees = getDisplayOritation(getDispalyRotation(), 0); if (_mCamera != null) { InitCamera(); //初始化相機 return; } //華為i7前后共用攝像頭 if (Camera.getNumberOfCameras() == 1) { _bIsFront = false; _mCamera = Camera.open(Camera.CameraInfo.CAMERA_FACING_BACK); } else { _mCamera = Camera.open(Camera.CameraInfo.CAMERA_FACING_FRONT); } InitCamera(); } @Override public void surfaceDestroyed(SurfaceHolder holder) { } }
相機的初始化就在這里啦:
public void InitCamera() { Camera.Parameters p = _mCamera.getParameters(); Size prevewSize = p.getPreviewSize(); showlog("Original Width:" + prevewSize.width + ", height:" + prevewSize.height); ListPreviewSizeList = p.getSupportedPreviewSizes(); List PreviewFormats = p.getSupportedPreviewFormats(); showlog("Listing all supported preview sizes"); for (Camera.Size size : PreviewSizeList) { showlog(" w: " + size.width + ", h: " + size.height); } showlog("Listing all supported preview formats"); Integer iNV21Flag = 0; Integer iYV12Flag = 0; for (Integer yuvFormat : PreviewFormats) { showlog("preview formats:" + yuvFormat); if (yuvFormat == android.graphics.ImageFormat.YV12) { iYV12Flag = android.graphics.ImageFormat.YV12; } if (yuvFormat == android.graphics.ImageFormat.NV21) { iNV21Flag = android.graphics.ImageFormat.NV21; } } if (iNV21Flag != 0) { _iCameraCodecType = iNV21Flag; } else if (iYV12Flag != 0) { _iCameraCodecType = iYV12Flag; } p.setPreviewSize(HEIGHT_DEF, WIDTH_DEF); p.setPreviewFormat(_iCameraCodecType); p.setPreviewFrameRate(FRAMERATE_DEF); showlog("_iDegrees="+_iDegrees); _mCamera.setDisplayOrientation(_iDegrees); p.setRotation(_iDegrees); _mCamera.setPreviewCallback(_previewCallback); _mCamera.setParameters(p); try { _mCamera.setPreviewDisplay(_mSurfaceView.getHolder()); } catch (Exception e) { return; } _mCamera.cancelAutoFocus();//只有加上了這一句,才會自動對焦。 _mCamera.startPreview(); }
還記得之前初始化完成之后開始推流函數(shù)嗎?
private void RtmpStartMessage() { Message msg = new Message(); msg.what = ID_RTMP_PUSH_START; Bundle b = new Bundle(); b.putInt("ret", 0); msg.setData(b); mHandler.sendMessage(msg); }
Handler處理:
public Handler mHandler = new Handler() { public void handleMessage(android.os.Message msg) { Bundle b = msg.getData(); int ret; switch (msg.what) { case ID_RTMP_PUSH_START: { Start(); break; } } } };
真正的推流實現(xiàn)原來在這里:
private void Start() { if (DEBUG_ENABLE) { File saveDir = Environment.getExternalStorageDirectory(); String strFilename = saveDir + "/aaa.h264"; try { if (!new File(strFilename).exists()) { new File(strFilename).createNewFile(); } _outputStream = new DataOutputStream(new FileOutputStream(strFilename)); } catch (Exception e) { e.printStackTrace(); } } //_rtmpSessionMgr.Start("rtmp://192.168.0.110/live/12345678"); _rtmpSessionMgr = new RtmpSessionManager(); _rtmpSessionMgr.Start(_rtmpUrl); //------point 1 int iFormat = _iCameraCodecType; _swEncH264 = new SWVideoEncoder(WIDTH_DEF, HEIGHT_DEF, FRAMERATE_DEF, BITRATE_DEF); _swEncH264.start(iFormat); //------point 2 _bStartFlag = true; _h264EncoderThread = new Thread(_h264Runnable); _h264EncoderThread.setPriority(Thread.MAX_PRIORITY); _h264EncoderThread.start(); //------point 3 _AudioRecorder.startRecording(); _AacEncoderThread = new Thread(_aacEncoderRunnable); _AacEncoderThread.setPriority(Thread.MAX_PRIORITY); _AacEncoderThread.start(); //------point 4 }
里面主要的函數(shù)有四個,我分別標(biāo)出來了,現(xiàn)在我們逐一看一下。首先是point 1,這已經(jīng)走到SDK里面了
public int Start(String rtmpUrl){ int iRet = 0; _rtmpUrl = rtmpUrl; _rtmpSession = new RtmpSession(); _bStartFlag = true; _h264EncoderThread.setPriority(Thread.MAX_PRIORITY); _h264EncoderThread.start(); return iRet; }
其實就是啟動了一個線程,這個線程稍微有點復(fù)雜
private Thread _h264EncoderThread = new Thread(new Runnable() { private Boolean WaitforReConnect(){ for(int i=0; i < 500; i++){ try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } if(_h264EncoderThread.interrupted() || (!_bStartFlag)){ return false; } } return true; } @Override public void run() { while (!_h264EncoderThread.interrupted() && (_bStartFlag)) { if(_rtmpHandle == 0) { _rtmpHandle = _rtmpSession.RtmpConnect(_rtmpUrl); if(_rtmpHandle == 0){ if(!WaitforReConnect()){ break; } continue; } }else{ if(_rtmpSession.RtmpIsConnect(_rtmpHandle) == 0){ _rtmpHandle = _rtmpSession.RtmpConnect(_rtmpUrl); if(_rtmpHandle == 0){ if(!WaitforReConnect()){ break; } continue; } } } if((_videoDataQueue.size() == 0) && (_audioDataQueue.size()==0)){ try { Thread.sleep(30); } catch (InterruptedException e) { e.printStackTrace(); } continue; } //Log.i(TAG, "VideoQueue length="+_videoDataQueue.size()+", AudioQueue length="+_audioDataQueue.size()); for(int i = 0; i < 100; i++){ byte[] audioData = GetAndReleaseAudioQueue(); if(audioData == null){ break; } //Log.i(TAG, "###RtmpSendAudioData:"+audioData.length); _rtmpSession.RtmpSendAudioData(_rtmpHandle, audioData, audioData.length); } byte[] videoData = GetAndReleaseVideoQueue(); if(videoData != null){ //Log.i(TAG, "$$$RtmpSendVideoData:"+videoData.length); _rtmpSession.RtmpSendVideoData(_rtmpHandle, videoData, videoData.length); } try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } _videoDataQueueLock.lock(); _videoDataQueue.clear(); _videoDataQueueLock.unlock(); _audioDataQueueLock.lock(); _audioDataQueue.clear(); _audioDataQueueLock.unlock(); if((_rtmpHandle != 0) && (_rtmpSession != null)){ _rtmpSession.RtmpDisconnect(_rtmpHandle); } _rtmpHandle = 0; _rtmpSession = null; } });
看18行,主要就是一個while循環(huán),每隔一段時間去_audioDataQueue和_videoDataQueue兩個緩沖數(shù)組中取數(shù)據(jù)發(fā)送給服務(wù)器,發(fā)送方法_rtmpSession.RtmpSendAudioData和_rtmpSession.RtmpSendVideoData都是Native方法,通過jni調(diào)用so庫文件的內(nèi)容,每隔一段時間,這個時間是多少呢?看第4行,原來是5秒鐘,也就是說我們的視頻數(shù)據(jù)會在緩沖中存放5秒才被取出來發(fā)給服務(wù)器,所有直播會有5秒的延時,我們可以修改這塊來控制直播延時。
上面說了我們會從_audioDataQueue和_videoDataQueue兩個Buffer里面取數(shù)據(jù),那么數(shù)據(jù)是何時放進去的呢?看上面的point 2,3,4。首先是point 2,同樣走進了SDK:
public boolean start(int iFormateType){ int iType = OpenH264Encoder.YUV420_TYPE; if(iFormateType == android.graphics.ImageFormat.YV12){ iType = OpenH264Encoder.YUV12_TYPE; }else{ iType = OpenH264Encoder.YUV420_TYPE; } _OpenH264Encoder = new OpenH264Encoder(); _iHandle = _OpenH264Encoder.InitEncode(_iWidth, _iHeight, _iBitRate, _iFrameRate, iType); if(_iHandle == 0){ return false; } _iFormatType = iFormateType; return true; }
其實這是初始化編碼器,具體的初始化過程也在so文件,jni調(diào)用。point 3,4其實就是開啟兩個線程,那我們看看線程中具體實現(xiàn)吧。
private Thread _h264EncoderThread = null; private Runnable _h264Runnable = new Runnable() { @Override public void run() { while (!_h264EncoderThread.interrupted() && _bStartFlag) { int iSize = _YUVQueue.size(); if (iSize > 0) { _yuvQueueLock.lock(); byte[] yuvData = _YUVQueue.poll(); if (iSize > 9) { Log.i(LOG_TAG, "###YUV Queue len=" + _YUVQueue.size() + ", YUV length=" + yuvData.length); } _yuvQueueLock.unlock(); if (yuvData == null) { continue; } if (_bIsFront) { _yuvEdit = _swEncH264.YUV420pRotate270(yuvData, HEIGHT_DEF, WIDTH_DEF); } else { _yuvEdit = _swEncH264.YUV420pRotate90(yuvData, HEIGHT_DEF, WIDTH_DEF); } byte[] h264Data = _swEncH264.EncoderH264(_yuvEdit); if (h264Data != null) { _rtmpSessionMgr.InsertVideoData(h264Data); if (DEBUG_ENABLE) { try { _outputStream.write(h264Data); int iH264Len = h264Data.length; //Log.i(LOG_TAG, "Encode H264 len="+iH264Len); } catch (IOException e1) { e1.printStackTrace(); } } } } try { Thread.sleep(1); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } _YUVQueue.clear(); } };
也是一個循環(huán)線程,第9行,從_YUVQueue中取出攝像頭獲取的數(shù)據(jù),然后進行視頻旋轉(zhuǎn),第24行,對數(shù)據(jù)進行編碼,然后執(zhí)行26行,InsertVideoData:
public void InsertVideoData(byte[] videoData){ if(!_bStartFlag){ return; } _videoDataQueueLock.lock(); if(_videoDataQueue.size() > 50){ _videoDataQueue.clear(); } _videoDataQueue.offer(videoData); _videoDataQueueLock.unlock(); }
果然就是插入之前提到的_videoDataQueue的Buffer。這里插入的是視頻數(shù)據(jù),那么音頻數(shù)據(jù)呢?在另外一個線程,內(nèi)容大致相同
private Runnable _aacEncoderRunnable = new Runnable() { @Override public void run() { DataOutputStream outputStream = null; if (DEBUG_ENABLE) { File saveDir = Environment.getExternalStorageDirectory(); String strFilename = saveDir + "/aaa.aac"; try { if (!new File(strFilename).exists()) { new File(strFilename).createNewFile(); } outputStream = new DataOutputStream(new FileOutputStream(strFilename)); } catch (Exception e1) { e1.printStackTrace(); } } long lSleepTime = SAMPLE_RATE_DEF * 16 * 2 / _RecorderBuffer.length; while (!_AacEncoderThread.interrupted() && _bStartFlag) { int iPCMLen = _AudioRecorder.read(_RecorderBuffer, 0, _RecorderBuffer.length); // Fill buffer if ((iPCMLen != _AudioRecorder.ERROR_BAD_VALUE) && (iPCMLen != 0)) { if (_fdkaacHandle != 0) { byte[] aacBuffer = _fdkaacEnc.FdkAacEncode(_fdkaacHandle, _RecorderBuffer); if (aacBuffer != null) { long lLen = aacBuffer.length; _rtmpSessionMgr.InsertAudioData(aacBuffer); //Log.i(LOG_TAG, "fdk aac length="+lLen+" from pcm="+iPCMLen); if (DEBUG_ENABLE) { try { outputStream.write(aacBuffer); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } } else { Log.i(LOG_TAG, "######fail to get PCM data"); } try { Thread.sleep(lSleepTime / 10); } catch (InterruptedException e) { e.printStackTrace(); } } Log.i(LOG_TAG, "AAC Encoder Thread ended ......"); } }; private Thread _AacEncoderThread = null;
這就是通過循環(huán)將音頻數(shù)據(jù)插入_audioDataQueue這個Buffer。
以上就是視頻采集和推流的代碼分析,Demo中并沒有對視頻進行任何處理,只是攝像頭采集,編碼后推流到服務(wù)器端。
第二部分:Nginx服務(wù)器搭建
流媒體服務(wù)器有諸多選擇,如商業(yè)版的Wowza。但我選擇的是免費的Nginx(nginx-rtmp-module)。Nginx本身是一個非常出色的HTTP服務(wù)器,它通過nginx的模塊nginx-rtmp-module可以搭建一個功能相對比較完善的流媒體服務(wù)器。這個流媒體服務(wù)器可以支持RTMP和HLS。
Nginx配合SDK做流媒體服務(wù)器的原理是: Nginx通過rtmp模塊提供rtmp服務(wù), SDK推送一個rtmp流到Nginx, 然后客戶端通過訪問Nginx來收看實時視頻流。 HLS也是差不多的原理,只是最終客戶端是通過HTTP協(xié)議來訪問的,但是SDK推送流仍然是rtmp的。
集成rtmp模塊的windows版本的Nginx。文章下點贊+私信我獲取!
1、rtmp端口配置
配置文件在/conf/nginx.conf
RTMP監(jiān)聽 1935 端口,啟用live 和hls 兩個application
所以你的流媒體服務(wù)器url可以寫成:rtmp://(服務(wù)器IP地址):1935/live/xxx 或 rtmp://(服務(wù)器IP地址):1935/hls/xxx
例如我們上面寫的 rtmp://192.168.1.104:1935/live/12345
HTTP監(jiān)聽 8080 端口,
:8080/stat 查看stream狀態(tài)
:8080/index.html 為一個直播播放與直播發(fā)布測試器
:8080/vod.html 為一個支持RTMP和HLS點播的測試器
2、啟動nginx服務(wù)
雙擊nginx.exe文件或者在dos窗口下運行nginx.exe,即可啟動nginx服務(wù):
1)啟動任務(wù)管理器,可以看到nginx.exe進程
2)打開網(wǎng)頁輸入http://localhot:8080,出現(xiàn)如下畫面:
顯示以上界面說明啟動成功。
第三部分:直播流的播放
主播界面:
上面說過了只要支持RTMP流傳輸協(xié)議的播放器都可以收看到我們的直播。下面舉兩個例子吧:
(1)window端播放器VLC
(2)android端播放器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:1935/live/12345"); }
總結(jié)
到這里整個基于RTMP推流實現(xiàn)Android視頻直播的項目已經(jīng)完成了,如有你有更好的想法可以在文章下方評論留言或私信我!另外前文中第二部分提到的推流SDK和Android實現(xiàn)的Demo以及第三部分提到的已經(jīng)集成rtmp模塊的windows版本的Nginx下載地址由于發(fā)文規(guī)則不允許插入外部鏈接,如有需要的可以再文章下點贊+評論后,下載地址我會私信發(fā)給你如回復(fù)不及時歡迎加入Android開發(fā)技術(shù)交流群:150923287!
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/73012.html
摘要:視頻采集處理后推流到流媒體服務(wù)器,第一部分功能完成。第二部分就是流媒體服務(wù)器,負(fù)責(zé)把從第一部分接收到的流進行處理并分發(fā)給觀眾。五服務(wù)器流分發(fā)流媒體服務(wù)器的作用是負(fù)責(zé)直播流的發(fā)布和轉(zhuǎn)播分發(fā)功能。 前言 近兩年時間,視頻直播可謂大火。在視頻直播領(lǐng)域,有不同的商家提供各種的商業(yè)解決方案,包括軟硬件設(shè)備,攝像機,編碼器,流媒體服務(wù)器等。本文要講解的是如何使用一系列免費工具,打造一套視頻直播方案...
閱讀 3427·2021-11-12 10:36
閱讀 2734·2021-11-11 16:55
閱讀 2958·2021-09-27 13:36
閱讀 1615·2021-08-05 10:01
閱讀 3556·2019-08-30 15:55
閱讀 765·2019-08-30 13:01
閱讀 1906·2019-08-29 17:16
閱讀 2376·2019-08-29 16:40