1. 程式人生 > >Android視訊直播的實現

Android視訊直播的實現

最近一段時間,視訊直播可謂大火。在視訊直播領域,有不同的商家提供各種的商業解決方案,包括軟硬體裝置,攝像機,編碼器,流媒體伺服器等。本文要講解的是如何使用一系列免費工具,打造一套視訊直播方案。

視訊直播流程

視訊直播的流程可以分為如下幾步:
採集 —>處理—>編碼和封裝—>推流到伺服器—>伺服器流分發—>播放器流播放

這裡寫圖片描述

1.採集

採集是整個視訊推流過程中的第一個環節,它從系統的採集裝置中獲取原始視訊資料,將其輸出到下一個環節。視訊的採集涉及兩方面資料的採集:音訊採集和影象採集,它們分別對應兩種完全不同的輸入源和資料格式。

這裡寫圖片描述

  • 音訊採集
    音訊資料既能與影象結合組合成視訊資料,也能以純音訊的方式採集播放,後者在很多成熟的應用場景如線上電臺和語音電臺等起著非常重要的作用。音訊的採集過程主要通過裝置將環境中的模擬訊號採集成 PCM 編碼的原始資料,然後編碼壓縮成 MP3 等格式的資料分發出去。常見的音訊壓縮格式有:MP3,AAC,HE-AAC,Opus,FLAC,Vorbis (Ogg),Speex 和 AMR等。
    音訊採集和編碼主要面臨的挑戰在於:延時敏感、卡頓敏感、噪聲消除(Denoise)、回聲消除(AEC)、靜音檢測(VAD)和各種混音演算法等。

  • 影象採集
    將影象採集的圖片結果組合成一組連續播放的動畫,即構成視訊中可肉眼觀看的內容。影象的採集過程主要由攝像頭等裝置拍攝成 YUV 編碼的原始資料,然後經過編碼壓縮成 H.264 等格式的資料分發出去。常見的視訊封裝格式有:MP4、3GP、AVI、MKV、WMV、MPG、VOB、FLV、SWF、MOV、RMVB 和 WebM 等。
    影象由於其直觀感受最強並且體積也比較大,構成了一個視訊內容的主要部分。影象採集和編碼面臨的主要挑戰在於:裝置相容性差、延時敏感、卡頓敏感以及各種對影象的處理操作如美顏和水印等。

視訊採集的採集源主要有 攝像頭採集、螢幕錄製和從視訊檔案推流。

2.處理

視訊或者音訊完成採集之後得到原始資料,為了增強一些現場效果或者加上一些額外的效果,我們一般會在將其編碼壓縮前進行處理,比如打上時間戳或者公司 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)知識冗餘:規律性的結構可由先驗知識和背景知識得到

  • 編碼器的選擇
    視訊編碼器經歷了數十年的發展,已經從開始的只支援幀內編碼演進到現如今的 H.265 和 VP9 為代表的新一代編碼器,下面是一些常見的視訊編碼器:
    1)H.264/AVC
    2)HEVC/H.265
    3)VP8
    4)VP9
    5)FFmpeg
    注:音訊編碼器有Mp3, AAC等。

(2)封裝
沿用前面的比喻,封裝可以理解為採用哪種貨車去運輸,也就是媒體的容器。
所謂容器,就是把編碼器生成的多媒體內容(視訊,音訊,字幕,章節資訊等)混合封裝在一起的標準。容器使得不同多媒體內容同步播放變得很簡單,而容器的另一個作用就是為多媒體內容提供索引,也就是說如果沒有容器存在的話一部影片你只能從一開始看到最後,不能拖動進度條,而且如果你不自己去手動另外載入音訊就沒有聲音。下面是幾種常見的封裝格式:
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 協議。

4.推流到伺服器

推流是直播的第一公里,直播的推流對這個直播鏈路影響非常大,如果推流的網路不穩定,無論我們如何做優化,觀眾的體驗都會很糟糕。所以也是我們排查問題的第一步,如何系統地解決這類問題需要我們對相關理論有基礎的認識。
推送協議主要有三種:

  • RTSP(Real Time Streaming Protocol):實時流傳送協議,是用來控制聲音或影像的多媒體串流協議, 由Real Networks和Netscape共同提出的;
  • RTMP(Real Time Messaging Protocol):實時訊息傳送協議,是Adobe公司為Flash播放器和伺服器之間音訊、視訊和資料傳輸 開發的開放協議;
  • HLS(HTTP Live Streaming):是蘋果公司(Apple Inc.)實現的基於HTTP的流媒體傳輸協議;

RTMP協議基於 TCP,是一種設計用來進行實時資料通訊的網路協議,主要用來在 flash/AIR 平臺和支援 RTMP 協議的流媒體/互動伺服器之間進行音視訊和資料通訊。支援該協議的軟體包括 Adobe Media Server/Ultrant Media Server/red5 等。
它有三種變種:

  • RTMP工作在TCP之上的明文協議,使用埠1935;
  • RTMPT封裝在HTTP請求之中,可穿越防火牆;
  • RTMPS類似RTMPT,但使用的是HTTPS連線;

RTMP 是目前主流的流媒體傳輸協議,廣泛用於直播領域,可以說市面上絕大多數的直播產品都採用了這個協議。
RTMP協議就像一個用來裝資料包的容器,這些資料可以是AMF格式的資料,也可以是FLV中的視/音訊資料。一個單一的連線可以通過不同的通道傳輸多路網路流。這些通道中的包都是按照固定大小的包傳輸的。
這裡寫圖片描述

5.伺服器流分發

流媒體伺服器的作用是負責直播流的釋出和轉播分發功能。
流媒體伺服器有諸多選擇,如商業版的Wowza。但我選擇的是Nginx,它是一款優秀的免費Web伺服器,後面我會詳細介紹如何搭建Nginx伺服器。

6.播放器流播放

主要是實現直播節目在終端上的展現。因為我這裡使用的傳輸協議是RTMP, 所以只要支援 RTMP 流協議的播放器都可以使用,譬如:

  • 電腦端:VLC等
  • 手機端:Vitamio以及ijkplayer等

一般情況下我們把上面流程的前四步稱為第一部分,即視訊主播端的操作。視訊採集處理後推流到流媒體伺服器,第一部分功能完成。第二部分就是流媒體伺服器,負責把從第一部分接收到的流進行處理並分發給觀眾。第三部分就是觀眾啦,只需要擁有支援流傳輸協議的播放器即可。
這裡寫圖片描述

第一部分:採集推流SDK

目前市面上集視訊採集、編碼、封裝和推流於一體的SDK已經有很多了,例如商業版的NodeMedia,但NodeMedia SDK按包名授權,未授權包名應用使用有版權提示資訊。
我這裡使用的是別人分享在github上的一個免費SDK,下載地址

下面我就程式碼分析一下直播推流的過程吧:
先看入口介面:
這裡寫圖片描述
很簡單,一個輸入框讓你填寫伺服器的推流地址,另外一個按鈕開啟推流。

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裡面,同樣,先看介面:
這裡寫圖片描述
佈局檔案:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/cameraRelative"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:theme="@android:style/Theme.NoTitleBar.Fullscreen">

<SurfaceView
        android:id="@+id/surfaceViewEx"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
    <Button
        android:id="@+id/SwitchCamerabutton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBottom="@+id/surfaceViewEx"
        android:text="@string/SwitchCamera" />
</RelativeLayout>

其實就是用一個SurfaceView顯示攝像頭拍攝畫面,並提供了一個按鈕切換前置和後置攝像頭。從入口函式看起:

    @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");
    }

首先設定全屏顯示,常亮,豎屏,獲取伺服器的推流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();//開始推流
    }

首先設定螢幕比例3:4顯示,給SurfaceView設定一些引數並添加回調,再初始化AudioRecord,最後執行開始推流。音訊在這裡初始化了,那麼相機在哪裡初始化呢?其實在SurfaceView的回撥函式裡。

        @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);

        List<Size> PreviewSizeList = p.getSupportedPreviewSizes();
        List<Integer> 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();
    }

還記得之前初始化完成之後開始推流函式嗎?

    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;
                }
            }
        }
    };

真正的推流實現原來在這裡:

    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
    }

裡面主要的函式有四個,我分別標出來了,現在我們逐一看一下。首先是point 1,這已經走到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;
    }

其實就是啟動了一個執行緒,這個執行緒稍微有點複雜

    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迴圈,每隔一段時間去_audioDataQueue和_videoDataQueue兩個緩衝陣列中取資料傳送給伺服器,傳送方法_rtmpSession.RtmpSendAudioData和_rtmpSession.RtmpSendVideoData都是Native方法,通過jni呼叫so庫檔案的內容,每隔一段時間,這個時間是多少呢?看第4行,原來是5秒鐘,也就是說我們的視訊資料會在緩衝中存放5秒才被取出來發給伺服器,所有直播會有5秒的延時,我們可以修改這塊來控制直播延時。
上面說了我們會從_audioDataQueue和_videoDataQueue兩個Buffer裡面取資料,那麼資料是何時放進去的呢?看上面的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呼叫。point 3,4其實就是開啟兩個執行緒,那我們看看執行緒中具體實現吧。

    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();
        }
    };

也是一個迴圈執行緒,第9行,從_YUVQueue中取出攝像頭獲取的資料,然後進行視訊旋轉,第24行,對資料進行編碼,然後執行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。這裡插入的是視訊資料,那麼音訊資料呢?在另外一個執行緒,內容大致相同

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;

這就是通過迴圈將音訊資料插入_audioDataQueue這個Buffer。
以上就是視訊採集和推流的程式碼分析,Demo中並沒有對視訊進行任何處理,只是攝像頭採集,編碼後推流到伺服器端。

第二部分:Nginx伺服器搭建

流媒體伺服器有諸多選擇,如商業版的Wowza。但我選擇的是免費的Nginx(nginx-rtmp-module)。Nginx本身是一個非常出色的HTTP伺服器,它通過nginx的模組nginx-rtmp-module可以搭建一個功能相對比較完善的流媒體伺服器。這個流媒體伺服器可以支援RTMPHLS
Nginx配合SDK做流媒體伺服器的原理是: Nginx通過rtmp模組提供rtmp服務, SDK推送一個rtmp流到Nginx, 然後客戶端通過訪問Nginx來收看實時視訊流。 HLS也是差不多的原理,只是最終客戶端是通過HTTP協議來訪問的,但是SDK推送流仍然是rtmp的。
下面是一款已經整合rtmp模組的windows版本的Nginx。下載後,即可直接使用
下載連結:https://github.com/illuspas/nginx-rtmp-win32

1、rtmp埠配置
配置檔案在/conf/nginx.conf
RTMP監聽 1935 埠,啟用live 和hls 兩個application
這裡寫圖片描述
所以你的流媒體伺服器url可以寫成:rtmp://(伺服器IP地址):1935/live/xxxrtmp://(伺服器IP地址):1935/hls/xxx
例如我們上面寫的 rtmp://192.168.1.104:1935/live/12345

HTTP監聽 8080 埠,

  • :8080/stat 檢視stream狀態
  • :8080/index.html 為一個直播播放與直播發布測試器
  • :8080/vod.html 為一個支援RTMP和HLS點播的測試器

2、啟動nginx服務
雙擊nginx.exe檔案或者在dos視窗下執行nginx.exe,即可啟動nginx服務:
這裡寫圖片描述

1)啟動工作管理員,可以看到nginx.exe程序
這裡寫圖片描述

第三部分:直播流的播放

主播介面:
這裡寫圖片描述

上面說過了只要支援RTMP流傳輸協議的播放器都可以收看到我們的直播。下面舉兩個例子吧:
(1)window端播放器VLC
這裡寫圖片描述

這裡寫圖片描述

    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");
    }

這裡寫圖片描述

相關推薦

Android視訊直播實現(包括伺服器搭建)

http://blog.csdn.net/huaxun66/article/details/53427771 最近一段時間,視訊直播可謂大火。在視訊直播領域,有不同的商家提供各種的商業解決方案,包括軟硬體裝置,攝像機,編碼器,流媒體伺服器等。本文要講解的是如

Android視訊直播實現

最近一段時間,視訊直播可謂大火。在視訊直播領域,有不同的商家提供各種的商業解決方案,包括軟硬體裝置,攝像機,編碼器,流媒體伺服器等。本文要講解的是如何使用一系列免費工具,打造一套視訊直播方案。 視訊直播流程 視訊直播的流程可以分為如下幾步: 採集 —>處理—>

Android視訊學習第4章:視訊直播實現之推送視訊

H.264標準學習 1.H264編碼框架 H264碼流檔案分為兩層: (1) VCL(Video Coding Layer)視訊編碼層: 負責高效的視訊內容表示,VCL 資料即編碼處理的輸出,它表示被壓縮編碼後的視訊資料序列。 (2)

開源SDK實現Android視訊直播

最近一段時間,視訊直播可謂大火。在視訊直播領域,有不同的商家提供各種的商業解決方案,包括軟硬體裝置,攝像機,編碼器,流媒體伺服器等。本文要講解的是如何使用一系列免費工具,打造一套視訊直播方案。 視訊直播流程 視訊直播的流程可以分為如下幾步: 採集 —&

Android視訊直播實現(推流完整實現001)

最近一段時間,視訊直播可謂大火。在視訊直播領域,有不同的商家提供各種的商業解決方案,包括軟硬體裝置,攝像機,編碼器,流媒體伺服器等。本文要講解的是如何使用一系列免費工具,打造一套視訊直播方案。 視訊直播流程 視訊直播的流程可以分為如下幾步:  採集 —>處理—>編碼和封裝—>

Android直播開發之旅(7):Android視訊直播核心技術(架構)詳解

(轉載請宣告出處:http://blog.csdn.net/andrexpert/article/details/76919535) 一、直播架構解析      目前主流的直播架構中主要有兩種方案,即流媒體轉發、P2P。流媒體轉發,是一種在視訊直播中以流的方式將連續的音、

Android視訊播放實現的三種辦法(MediaPlayer+SurfaceView,VideoView,Vitamio框架)

今天來說一下Android中怎麼實現視訊播放,我主要說三種:1.MediaPlayer+SurfaceView;2.VideoView;3.Vitamio框架。1.MediaPlayer+SurfaceView這種方法是基礎,後面的兩種方法其實就是把這種方法封裝了一下,使用起來

Android視訊直播原理詳解

最近一段時間,視訊直播可謂大火。在視訊直播領域,有不同的商家提供各種的商業解決方案,包括軟硬體裝置,攝像機,編碼器,流媒體伺服器等。本文要講解的是如何使用一系列免費工具,打造一套視訊直播方案。 視訊直播流程 視訊直播的流程可以分為如下幾步:  採集 —&

【 專欄 】- Android視訊直播

Android視訊直播 本欄目主要記錄了基於Android平臺流媒體直播開發的所有過程,包括各種音視訊編解碼格式剖析、AAC/MP3/MP4檔案封裝、ffmpeg編譯移植、rtsp/rtmp流媒體協議研究、其他多媒體第三方框架的使

android視訊直播直播流程概述

最近都在忙著做視訊直播的專案,有一個月沒有寫部落格了,現在直播流程終於通了,寫個部落格總結一下。 首先,如果你要實現的是攝像頭直播,那其實很簡單,因為已經有很多第三方支援攝像頭直播了,你所要做的,無非就是接入他們的sdk。 或者你也可以去github上找一套

Android視訊播放實現的三種辦法

今天來說一下Android中怎麼實現視訊播放,我主要說三種: 1.MediaPlayer+SurfaceView; 2.VideoView; 3.Vitamio框架。 1.MediaPlayer+SurfaceView這種方法是基礎,後面的兩種方法其實就是把這種方法封裝了

Android 特效直播實現原理解析

  作者簡介:  2010年開始從事Android開發工作,網易資深開發工程師,主要負責視訊雲Android端的直播SDK開發與維護工作。  這篇文章主要講解下目前市面上比較新穎的特效直播,比如Faceu激萌等軟體的具體實現原理。  如上圖所示,要實現特效直播至少需要實現這

Android+Web視訊直播裝逼實現

根據上圖,我們註冊並開通LSS服務後,第一步要做的是建立直播會話。我們先來建立一個指定設定的直播會話,如圖所示: 直播會話建立選項 這裡大家可以看到我用一個紅框標識了重點部分。此處建議大家選擇一個合適的轉碼模板,因為預設的模板設定會導致最終視訊直播時,伺服器只做轉發,保持輸入解析度與位元速率不變。這樣有兩

Android直播實現 Android端推流、播放

size input 準備 預覽 不必要 targe height 不出 oar 最近想實現一個Android直播,但是對於這方面的資料都比較零碎,一開始是打算用ffmpeg來實現編碼推流,在搜集資料期間,找到了幾個強大的開源庫,直接避免了jni的代碼,集成後只用少量的ja

Android SurfaceView+MediaPlayer實現幾個不同的視訊輪流播放

MediaPlayer 1)如何獲得MediaPlayer例項: 可以使用直接new的方式: MediaPlayer mp = new MediaPlayer(); 也可以使用create的方式,如: MediaPlayer mp = MediaPlayer.create(t

android,Exoplayer實現視訊播放器

bundle配置: implementation 'com.google.android.exoplayer:exoplayer-core:2.8.1'implementation 'com.google.android.exoplayer:exoplayer-dash:2.8.1'implementati

C++實現RTMP協議傳送H.264編碼及AAC編碼的音視訊直播

  RTMP(Real Time Messaging Protocol)是專門用來傳輸音視訊資料的流媒體協議,最初由Macromedia 公司建立,後來歸Adobe公司所有,是一種私有協議,主要用來聯絡Flash Player和RtmpServer,如FMS, Red5, 

簡單實現Android視訊播放器倍速、清晰度切換、m3u8下載

簡單的前提是使用開源庫 在庫的基礎上新增相對應的功能, 以 JiaoZiVideoPlayer 為例 ,本身自帶的播放引擎是MediaPlayer,也就是Android自帶的播放器,有很多不完善的地方,倍速切換隻支援5.0以上,否則報 NoClassDefFoundError 錯誤

Android直播視訊技術探究之---採集攝像頭Camera視訊源資料進行推流(採用金山雲SDK)

一、前言在之前已經詳細介紹了Android中的一種視訊資料來源:Camera,不瞭解的同學可以點選進入:Android中Camera使用詳解 ,在這篇文章中我們介紹瞭如何採集攝像頭的每一幀資料,然後進行

釋出宅男神器:視訊直播app for Android ----- 萬紫千紅

點此下載(27MB):同樣只打包了arm版,所以不支援x86http://oltag.com:8080/yaolixing/18/11/00/android/OHUI_V56-wzqh-release_arm.apk使用他山跨平臺混合應用開發框架,提升傳統開發效率十幾,甚至