天天看點

NDK 直播推流與引流流媒體伺服器測試推流引流

作者:位元組流動

來源:

https://blog.csdn.net/Kennethdroid/article/details/86418725

流媒體伺服器測試

首先利用

快直播 app

(其他支援 RTMP 推流與引流的 app 亦可)和 ffplay.exe 對流媒體伺服器進行測試。

快直播的推流界面和引流界面

NDK 直播推流與引流流媒體伺服器測試推流引流
NDK 直播推流與引流流媒體伺服器測試推流引流

Windows 下利用 ffplay 進行引流,指令行執行:

ffplay rtmp://192.168.0.0/live/test  
# ip 位址換成流媒體伺服器的位址, test 表示直播房間号      

測試結果:

NDK 直播推流與引流流媒體伺服器測試推流引流

推流

本文直播推流步驟:

  • 使用 AudioRecord 采集音頻,使用 Camera API 采集視訊資料
  • 分别使用 faac 和 xh264 第三方庫在 Native 層對音頻和視訊進行編碼
  • 利用 rtmp-dump 第三方庫進行打包和推流

工程目錄:

NDK 直播推流與引流流媒體伺服器測試推流引流

主要的 JNI 方法:

public class NativePush {

    public native void startPush(String url);

    public native void stopPush();

    public native void release();

    /**
     * 設定視訊參數
     * @param width
     * @param height
     * @param bitrate
     * @param fps
     */
    public native void setVideoOptions(int width, int height, int bitrate, int fps);

    /**
     * 設定音頻參數
     * @param sampleRateInHz
     * @param channel
     */
    public native void setAudioOptions(int sampleRateInHz, int channel);


    /**
     * 發送視訊資料
     * @param data
     */
    public native void fireVideo(byte[] data);

    /**
     * 發送音頻資料
     * @param data
     * @param len
     */
    public native void fireAudio(byte[] data, int len);

}      

視訊采集

視訊采集主要基于 Camera 相關 API ,利用 SurfaceView 進行預覽,通過 PreviewCallback 擷取相機預覽資料。

視訊預覽主要代碼實作:

public void startPreview(){
        try {
            mCamera = Camera.open(mVideoParams.getCameraId());
            Camera.Parameters param = mCamera.getParameters();

            List<Camera.Size> previewSizes = param.getSupportedPreviewSizes();
            int length = previewSizes.size();
            for (int i = 0; i < length; i++) {
                Log.i(TAG, "SupportedPreviewSizes : " + previewSizes.get(i).width + "x" + previewSizes.get(i).height);
            }

            mVideoParams.setWidth(previewSizes.get(0).width);
            mVideoParams.setHeight(previewSizes.get(0).height);

            param.setPreviewFormat(ImageFormat.NV21);
            param.setPreviewSize(mVideoParams.getWidth(), mVideoParams.getHeight());

            mCamera.setParameters(param);
            //mCamera.setDisplayOrientation(90); // 豎屏
            mCamera.setPreviewDisplay(mSurfaceHolder);

            buffer = new byte[mVideoParams.getWidth() * mVideoParams.getHeight() * 4];
            mCamera.addCallbackBuffer(buffer);
            mCamera.setPreviewCallbackWithBuffer(this);

            mCamera.startPreview();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
      

利用 FrameCallback 擷取預覽資料傳入 Native 層,然後進行編碼:

@Override
    public void onPreviewFrame(byte[] bytes, Camera camera) {
        if (mCamera != null) {
            mCamera.addCallbackBuffer(buffer);
        }

        if (mIsPushing) {
            mNativePush.fireVideo(bytes);
        }

    }
      

音頻采集

音頻采集基于 AudioRecord 實作,在一個子線程采集音頻 PCM 資料,并将資料不斷傳入 Native 層進行編碼。

private class AudioRecordRunnable implements Runnable {

        @Override
        public void run() {
            mAudioRecord.startRecording();
            while (mIsPushing) {
                //通過AudioRecord不斷讀取音頻資料
                byte[] buffer = new byte[mMinBufferSize];
                int length = mAudioRecord.read(buffer, 0, buffer.length);
                if (length > 0) {
                    //傳遞給 Native 代碼,進行音頻編碼
                    mNativePush.fireAudio(buffer, length);
                }
            }
        }
    }      

編碼和推流

音視訊資料編碼和推流在 Native 層實作,首先添加 faac , x264 , librtmp 第三方庫到 AS 工程,然後初始化相關設定,基于生産者與消費者模式,将編碼後的音視訊資料,在生産者線程中打包 RTMPPacket 放入雙向連結清單,在消費者線程中從連結清單中取 RTMPPacket ,通過 RTMP_SendPacket 方法發送給伺服器。

x264 初始化:

JNIEXPORT void JNICALL
Java_com_haohao_live_jni_NativePush_setVideoOptions(JNIEnv *env, jobject instance, jint width,
                                                    jint height, jint bitRate, jint fps) {

    x264_param_t param;
    //x264_param_default_preset 設定
    x264_param_default_preset(&param, "ultrafast", "zerolatency");
    //編碼輸入的像素格式YUV420P
    param.i_csp = X264_CSP_I420;
    param.i_width = width;
    param.i_height = height;

    y_len = width * height;
    u_len = y_len / 4;
    v_len = u_len;

    //參數i_rc_method表示碼率控制,CQP(恒定品質),CRF(恒定碼率),ABR(平均碼率)
    //恒定碼率,會盡量控制在固定碼率
    param.rc.i_rc_method = X264_RC_CRF;
    param.rc.i_bitrate = bitRate / 1000; //* 碼率(比特率,機關Kbps)
    param.rc.i_vbv_max_bitrate = bitRate / 1000 * 1.2; //瞬時最大碼率

    //碼率控制不通過timebase和timestamp,而是fps
    param.b_vfr_input = 0;
    param.i_fps_num = fps; //* 幀率分子
    param.i_fps_den = 1; //* 幀率分母
    param.i_timebase_den = param.i_fps_num;
    param.i_timebase_num = param.i_fps_den;
    param.i_threads = 1;//并行編碼線程數量,0預設為多線程

    //是否把SPS和PPS放入每一個關鍵幀
    //SPS Sequence Parameter Set 序列參數集,PPS Picture Parameter Set 圖像參數集
    //為了提高圖像的糾錯能力
    param.b_repeat_headers = 1;
    //設定Level級别
    param.i_level_idc = 51;
    //設定Profile檔次
    //baseline級别,沒有B幀,隻有 I 幀和 P 幀
    x264_param_apply_profile(&param, "baseline");

    //x264_picture_t(輸入圖像)初始化
    x264_picture_alloc(&pic_in, param.i_csp, param.i_width, param.i_height);
    pic_in.i_pts = 0;
    //打開編碼器
    video_encode_handle = x264_encoder_open(&param);
    if (video_encode_handle) {
        LOGI("打開視訊編碼器成功");
    } else {
        throwNativeError(env, INIT_FAILED);
    }
}
      

faac 初始化:

JNIEXPORT void JNICALL
Java_com_haohao_live_jni_NativePush_setAudioOptions(JNIEnv *env, jobject instance,
                                                    jint sampleRateInHz, jint channel) {
    audio_encode_handle = faacEncOpen(sampleRateInHz, channel, &nInputSamples,
                                      &nMaxOutputBytes);
    if (!audio_encode_handle) {
        LOGE("音頻編碼器打開失敗");
        return;
    }
    //設定音頻編碼參數
    faacEncConfigurationPtr p_config = faacEncGetCurrentConfiguration(audio_encode_handle);
    p_config->mpegVersion = MPEG4;
    p_config->allowMidside = 1;
    p_config->aacObjectType = LOW;
    p_config->outputFormat = 0; //輸出是否包含ADTS頭
    p_config->useTns = 1; //時域噪音控制,大概就是消爆音
    p_config->useLfe = 0;
//  p_config->inputFormat = FAAC_INPUT_16BIT;
    p_config->quantqual = 100;
    p_config->bandWidth = 0; //頻寬
    p_config->shortctl = SHORTCTL_NORMAL;

    if (!faacEncSetConfiguration(audio_encode_handle, p_config)) {
        LOGE("%s", "音頻編碼器配置失敗..");
        throwNativeError(env, INIT_FAILED);
        return;
    }

    LOGI("%s", "音頻編碼器配置成功");
}      

對視訊資料進行編碼打包,通過 add_rtmp_packet 放傳入連結表:

JNIEXPORT void JNICALL
Java_com_haohao_live_jni_NativePush_fireVideo(JNIEnv *env, jobject instance, jbyteArray buffer_) {
    //視訊資料轉為YUV420P
    //NV21->YUV420P
    jbyte *nv21_buffer = (*env)->GetByteArrayElements(env, buffer_, NULL);
    jbyte *u = pic_in.img.plane[1];
    jbyte *v = pic_in.img.plane[2];
    //nv21 4:2:0 Formats, 12 Bits per Pixel
    //nv21與yuv420p,y個數一緻,uv位置對調
    //nv21轉yuv420p  y = w*h,u/v=w*h/4
    //nv21 = yvu yuv420p=yuv y=y u=y+1+1 v=y+1
    //如果要進行圖像處理(美顔),可以再轉換為RGB
    //還可以結合OpenCV識别人臉等等
    memcpy(pic_in.img.plane[0], nv21_buffer, y_len);
    int i;
    for (i = 0; i < u_len; i++) {
        *(u + i) = *(nv21_buffer + y_len + i * 2 + 1);
        *(v + i) = *(nv21_buffer + y_len + i * 2);
    }

    //h264編碼得到NALU數組
    x264_nal_t *nal = NULL; //NAL
    int n_nal = -1; //NALU的個數
    //進行h264編碼
    if (x264_encoder_encode(video_encode_handle, &nal, &n_nal, &pic_in, &pic_out) < 0) {
        LOGE("%s", "編碼失敗");
        return;
    }
    //使用rtmp協定将h264編碼的視訊資料發送給流媒體伺服器
    //幀分為關鍵幀和普通幀,為了提高畫面的糾錯率,關鍵幀應包含SPS和PPS資料
    int sps_len, pps_len;
    unsigned char sps[100];
    unsigned char pps[100];
    memset(sps, 0, 100);
    memset(pps, 0, 100);
    pic_in.i_pts += 1; //順序累加
    //周遊NALU數組,根據NALU的類型判斷
    for (i = 0; i < n_nal; i++) {
        if (nal[i].i_type == NAL_SPS) {
            //複制SPS資料,序列參數集(Sequence parameter set)
            sps_len = nal[i].i_payload - 4;
            memcpy(sps, nal[i].p_payload + 4, sps_len); //不複制四位元組起始碼
        } else if (nal[i].i_type == NAL_PPS) {
            //複制PPS資料,圖像參數集(Picture parameter set)
            pps_len = nal[i].i_payload - 4;
            memcpy(pps, nal[i].p_payload + 4, pps_len); //不複制四位元組起始碼

            //發送序列資訊
            //h264關鍵幀會包含SPS和PPS資料
            add_264_sequence_header(pps, sps, pps_len, sps_len);

        } else {
            //發送幀資訊
            add_264_body(nal[i].p_payload, nal[i].i_payload);
        }

    }

    (*env)->ReleaseByteArrayElements(env, buffer_, nv21_buffer, 0);
}      

同樣,對音頻資料進行編碼打包放傳入連結表:

JNIEXPORT void JNICALL
Java_com_haohao_live_jni_NativePush_fireAudio(JNIEnv *env, jobject instance, jbyteArray buffer_,
                                              jint length) {
    int *pcmbuf;
    unsigned char *bitbuf;
    jbyte *b_buffer = (*env)->GetByteArrayElements(env, buffer_, 0);
    pcmbuf = (short *) malloc(nInputSamples * sizeof(int));
    bitbuf = (unsigned char *) malloc(nMaxOutputBytes * sizeof(unsigned char));
    int nByteCount = 0;
    unsigned int nBufferSize = (unsigned int) length / 2;
    unsigned short *buf = (unsigned short *) b_buffer;
    while (nByteCount < nBufferSize) {
        int audioLength = nInputSamples;
        if ((nByteCount + nInputSamples) >= nBufferSize) {
            audioLength = nBufferSize - nByteCount;
        }
        int i;
        for (i = 0; i < audioLength; i++) {//每次從實時的pcm音頻隊列中讀出量化位數為8的pcm資料。
            int s = ((int16_t *) buf + nByteCount)[i];
            pcmbuf[i] = s << 8;//用8個二進制位來表示一個采樣量化點(模數轉換)
        }
        nByteCount += nInputSamples;
        //利用FAAC進行編碼,pcmbuf為轉換後的pcm流資料,audioLength為調用faacEncOpen時得到的輸入采樣數,bitbuf為編碼後的資料buff,nMaxOutputBytes為調用faacEncOpen時得到的最大輸出位元組數
        int byteslen = faacEncEncode(audio_encode_handle, pcmbuf, audioLength,
                                     bitbuf, nMaxOutputBytes);
        if (byteslen < 1) {
            continue;
        }
        add_aac_body(bitbuf, byteslen);//從bitbuf中得到編碼後的aac資料流,放到資料隊列
    }
    if (bitbuf)
        free(bitbuf);
    if (pcmbuf)
        free(pcmbuf);

    (*env)->ReleaseByteArrayElements(env, buffer_, b_buffer, 0);
}      

消費者線程不斷從連結清單中取 RTMPPacket 發送給伺服器:

void *push_thread(void *arg) {
    JNIEnv *env;//擷取目前線程JNIEnv
    (*javaVM)->AttachCurrentThread(javaVM, &env, NULL);

    //建立RTMP連接配接
    RTMP *rtmp = RTMP_Alloc();
    if (!rtmp) {
        LOGE("rtmp初始化失敗");
        goto end;
    }
    RTMP_Init(rtmp);
    rtmp->Link.timeout = 5; //連接配接逾時的時間
    //設定流媒體位址
    RTMP_SetupURL(rtmp, rtmp_path);
    //釋出rtmp資料流
    RTMP_EnableWrite(rtmp);
    //建立連接配接
    if (!RTMP_Connect(rtmp, NULL)) {
        LOGE("%s", "RTMP 連接配接失敗");
        throwNativeError(env, CONNECT_FAILED);
        goto end;
    }
    //計時
    start_time = RTMP_GetTime();
    if (!RTMP_ConnectStream(rtmp, 0)) { //連接配接流
        LOGE("%s", "RTMP ConnectStream failed");
        throwNativeError(env, CONNECT_FAILED);
        goto end;
    }
    is_pushing = TRUE;
    //發送AAC頭資訊
    add_aac_sequence_header();

    while (is_pushing) {
        //發送
        pthread_mutex_lock(&mutex);
        pthread_cond_wait(&cond, &mutex);
        //取出隊列中的RTMPPacket
        RTMPPacket *packet = queue_get_first();
        if (packet) {
            queue_delete_first(); //移除
            packet->m_nInfoField2 = rtmp->m_stream_id; //RTMP協定,stream_id資料
            int i = RTMP_SendPacket(rtmp, packet, TRUE); //TRUE放入librtmp隊列中,并不是立即發送
            if (!i) {
                LOGE("RTMP 斷開");
                RTMPPacket_Free(packet);
                pthread_mutex_unlock(&mutex);
                goto end;
            } else {
                LOGI("%s", "rtmp send packet");
            }
            RTMPPacket_Free(packet);
        }

        pthread_mutex_unlock(&mutex);
    }
    end:
    LOGI("%s", "釋放資源");
    free(rtmp_path);
    RTMP_Close(rtmp);
    RTMP_Free(rtmp);
    (*javaVM)->DetachCurrentThread(javaVM);
    return 0;
}      

引流

這裡引流就不做展開講,可以通過 QLive 的 SDK 或者 vitamio (小楠總)等第三方庫實作。

基于 vitamio 實作引流:

private void init(){
        mVideoView = (VideoView) findViewById(R.id.live_player_view);
        mVideoView.setVideoPath(SPUtils.getInstance(this).getString(SPUtils.KEY_NGINX_SER_URI));
        mVideoView.setMediaController(new MediaController(this));
        mVideoView.requestFocus();

        mVideoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
            @Override
            public void onPrepared(MediaPlayer mp) {
                mp.setPlaybackSpeed(1.0f);
            }
        });

    }      

PS:源碼位址:

https://github.com/githubhaohao/NDKLive

NDK 開發系列文章:

「視訊雲技術」你最值得關注的音視訊技術公衆号,每周推送來自阿裡雲一線的實踐技術文章,在這裡與音視訊領域一流工程師交流切磋。
NDK 直播推流與引流流媒體伺服器測試推流引流

繼續閱讀