Nginx-RTMP推流(video)

yxc發表於2019-03-02

Camera 採集資料

Camera負責採集資料,把採集來的資料交給 X264進行編碼打包給RTMP進行推流,

Camera採集來的資料是NV21, 而X264編碼的輸入資料格式為I420格式。

NV21和I420都是屬於YUV420格式。而NV21是一種two-plane模式,即Y和UV分為兩個Plane(平面),但是UV(CbCr)交錯儲存,2個平面,而不是分為三個。這種排列方式被稱之為YUV420SP,而I420則稱之為YUV420P。(Y:明亮度、灰度,UV:色度、飽和度)

下圖是大小為4×4的NV21資料:Y1、Y2、Y5、Y6共用V1與U1,……

Nginx-RTMP推流(video)

而I420則是

Nginx-RTMP推流(video)

可以看出無論是哪種排列方式,YUV420的資料量都為: w*h+w/2*h/2+w/2*h/2 即為w*h*3/2

將NV21轉位I420則為:

​ Y資料按順序完整複製,U資料則是從整個Y資料之後加一個位元組再每隔一個位元組取一次。

感測器與螢幕自然方向不一致,將影像感測器的座標系逆時針旋轉90度,才能顯示到螢幕的座標系上。所以看到的畫面是逆時針旋轉了90度的,因此我們需要將影像順時針旋轉90度才能看到正常的畫面。而Camera物件提供一個setDisplayOrientation介面能夠設定預覽顯示的角度:

Nginx-RTMP推流(video)

根據文件,配置完Camera之後預覽確實正常了,但是在onPreviewFrame中回撥獲得的資料依然是逆時針旋轉了90度的。所以如果需要使用預覽回撥的資料,還需要對onPreviewFrame回撥的byte[] 進行旋轉。

即對NV21資料順時針旋轉90度。

初始化 編碼器、佇列SafeQueue

Camera 通過PreviewCallBack把 資料 byte[] data傳給 native 中。native在init時準備一個編碼器編碼,一個佇列用來儲存資料,編碼器 x264_t *videoCodec = 0; 存放在 VideoChannel.cpp中

//native-lib.cpp 檔案
//佇列
SafeQueue<RTMPPacket *> packets;
VideoChannel *videoChannel = 0;

extern "C"
JNIEXPORT void JNICALL
Java_com_tina_pushstream_live_LivePusher_native_1init(JNIEnv *env, jobject instance) {
    //準備一個Video編碼器的工具類 :進行編碼
    videoChannel = new VideoChannel;
    videoChannel->setVideoCallback(callback);
    //準備一個佇列,打包好的資料 放入佇列,線上程中統一的取出資料再傳送給伺服器
    packets.setReleaseCallback(releasePackets);
}
複製程式碼

在 VideoChannel中建立編碼器,並且設定引數:

//  VideoChannel.h/VideoChannel.cpp
x264_t *videoCodec = 0;

//設定編碼器引數
void VideoChannel::setVideoEncInfo(int width, int height, int fps, int bitrate) {
    pthread_mutex_lock(&mutex);
    mWidth = width;
    mHeight = height;
    mFps = fps;
    mBitrate = bitrate;
    ySize = width * height;
    uvSize = ySize / 4;
    if (videoCodec) {
        x264_encoder_close(videoCodec);
        videoCodec = 0;
    }
    if (pic_in) {
        x264_picture_clean(pic_in);
        delete pic_in;
        pic_in = 0;
    }

    //開啟x264編碼器
    //x264編碼器的屬性
    x264_param_t param;
    //2: 最快
    //3:  無延遲編碼
    x264_param_default_preset(&param, "ultrafast", "zerolatency");
    //base_line 3.2 編碼規格
    param.i_level_idc = 32;
    //輸入資料格式
    param.i_csp = X264_CSP_I420;
    param.i_width = width;
    param.i_height = height;
    //無b幀
    param.i_bframe = 0;
    //引數i_rc_method表示位元速率控制,CQP(恆定質量),CRF(恆定位元速率),ABR(平均位元速率)
    param.rc.i_rc_method = X264_RC_ABR;
    //位元速率(位元率,單位Kbps)
    param.rc.i_bitrate = bitrate / 1000;
    //瞬時最大位元速率
    param.rc.i_vbv_max_bitrate = bitrate / 1000 * 1.2;
    //設定了i_vbv_max_bitrate必須設定此引數,位元速率控制區大小,單位kbps
    param.rc.i_vbv_buffer_size = bitrate / 1000;
  
    //幀率
    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.pf_log = x264_log_default2;
    //用fps而不是時間戳來計算幀間距離
    param.b_vfr_input = 0;
    //幀距離(關鍵幀)  2s一個關鍵幀
    param.i_keyint_max = fps * 2;
    // 是否複製sps和pps放在每個關鍵幀的前面 該引數設定是讓每個關鍵幀(I幀)都附帶sps/pps。
    param.b_repeat_headers = 1;
    //多執行緒
    param.i_threads = 1;

    x264_param_apply_profile(&param, "baseline");
    //開啟編碼器 videoCodec
    videoCodec = x264_encoder_open(&param);
    pic_in = new x264_picture_t;
    x264_picture_alloc(pic_in, X264_CSP_I420, width, height);
    pthread_mutex_unlock(&mutex);
}
複製程式碼

#連線服務

native_start啟動一個執行緒連線伺服器,RTMP跟Http一樣是基於TCP的上層協議,所以在start方法裡連線。

//LivePusher 呼叫native_start()
public void startLive(String path) {
        native_start(path);
        videoChannel.startLive();
        audioChannel.startLive();
 }
複製程式碼

native層RTMP連線伺服器,首先啟動執行緒,線上程回撥中開啟連線:

//native-lib.cpp

extern "C"
JNIEXPORT void JNICALL
Java_com_dongnao_pusher_live_LivePusher_native_1start(JNIEnv *env, jobject instance,
                                                      jstring path_) {
    if (isStart) {
        return;
    }
    const char *path = env->GetStringUTFChars(path_, 0);
    char *url = new char[strlen(path) + 1];
    strcpy(url, path);
    isStart = 1;
    //啟動執行緒
    pthread_create(&pid, 0, start, url);
    env->ReleaseStringUTFChars(path_, path);
}

//執行緒啟動 RTMP connect 伺服器
void *start(void *args) {
    char *url = static_cast<char *>(args);
    RTMP *rtmp = 0;
    do {
        rtmp = RTMP_Alloc();
        if (!rtmp) {
            LOGE("rtmp建立失敗");
            break;
        }
        RTMP_Init(rtmp);
        //設定超時時間 5s
        rtmp->Link.timeout = 5;
        int ret = RTMP_SetupURL(rtmp, url);
        if (!ret) {
            LOGE("rtmp設定地址失敗:%s", url);
            break;
        }
        //開啟輸出模式
        RTMP_EnableWrite(rtmp);
        ret = RTMP_Connect(rtmp, 0);
        if (!ret) {
            LOGE("rtmp連線地址失敗:%s", url);
            break;
        }
        ret = RTMP_ConnectStream(rtmp, 0);
        if (!ret) {
            LOGE("rtmp連線流失敗:%s", url);
            break;
        }

        //準備好了 可以開始推流了
        readyPushing = 1;
        //記錄一個開始推流的時間
        start_time = RTMP_GetTime();
        packets.setWork(1);
        RTMPPacket *packet = 0;
        //迴圈從佇列取包 然後傳送
        while (isStart) {
            packets.pop(packet);
            if (!isStart) {
                break;
            }
            if (!packet) {
                continue;
            }
            // 給rtmp的流id
            packet->m_nInfoField2 = rtmp->m_stream_id;
            //傳送包 1:加入佇列傳送
            ret = RTMP_SendPacket(rtmp, packet, 1);
            releasePackets(packet);
            if (!ret) {
                LOGE("傳送資料失敗");
                break;
            }
        }
        releasePackets(packet);
    } while (0);
    if (rtmp) {
        RTMP_Close(rtmp);
        RTMP_Free(rtmp);
    }
    delete url;
    return 0;
}
複製程式碼

以上start函式中的整個流程:

Nginx-RTMP推流(video)

資料傳輸

start連線好後,就開始pushVideo資料了:

//VideoChannel,  在LivePusher中start時呼叫 videoChannel.startLive()
public void startLive() {
    isLiving = true;
}

//在 PreviewCallback中的回撥裡,此時isLiving為true,呼叫native_pushVideo.
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
  if (isLiving) {
    mLivePusher.native_pushVideo(data);
  }
}
複製程式碼

從Camera採集的NV21到 X264的I420需要轉碼:

extern "C"
JNIEXPORT void JNICALL
Java_com_tina_pushstream_live_LivePusher_native_1pushVideo(JNIEnv *env, jobject instance,jbyteArray data_) {
    if (!videoChannel || !readyPushing) {
        return;
    }
    jbyte *data = env->GetByteArrayElements(data_, NULL);
    videoChannel->encodeData(data);
    env->ReleaseByteArrayElements(data_, data, 0);
}
複製程式碼

根據NV21、I420的yuv格式的不同,轉化後儲存到x264_picture_t *pic_in = 0;

//圖片
x264_picture_t *pic_in = 0;

//編碼,把NV21 轉成I420
void VideoChannel::encodeData(int8_t *data) {
    //編碼
    pthread_mutex_lock(&mutex);
    //將data 放入 pic_in
    //y資料
    memcpy(pic_in->img.plane[0], data, ySize);
    for (int i = 0; i < uvSize; ++i) {
        //間隔1個位元組取一個資料
        //u資料
        *(pic_in->img.plane[1] + i) = *(data + ySize + i * 2 + 1);
        //v資料
        *(pic_in->img.plane[2] + i) = *(data + ySize + i * 2);
    }
    pic_in->i_pts = index++;
    //編碼出的資料
    x264_nal_t *pp_nal;
    //編碼出了幾個 nalu (暫時理解為幀)
    int pi_nal;
    x264_picture_t pic_out;
    //編碼
    int ret = x264_encoder_encode(videoCodec, &pp_nal, &pi_nal, pic_in, &pic_out);
    if (ret < 0) {
        pthread_mutex_unlock(&mutex);
        return;
    }
    int sps_len, pps_len;
    uint8_t sps[100];
    uint8_t pps[100];
    //
    for (int i = 0; i < pi_nal; ++i) {
        //資料型別
        if (pp_nal[i].i_type == NAL_SPS) {
            // 去掉 00 00 00 01
            sps_len = pp_nal[i].i_payload - 4;
            memcpy(sps, pp_nal[i].p_payload + 4, sps_len);
        } else if (pp_nal[i].i_type == NAL_PPS) {
            pps_len = pp_nal[i].i_payload - 4;
            memcpy(pps, pp_nal[i].p_payload + 4, pps_len);
            //拿到pps 就表示 sps已經拿到了
            sendSpsPps(sps, pps, sps_len, pps_len);
        } else {
            //關鍵幀、非關鍵幀
            sendFrame(pp_nal[i].i_type,pp_nal[i].i_payload,pp_nal[i].p_payload);
        }
    }
    pthread_mutex_unlock(&mutex);
}

複製程式碼

組裝spspps幀、Frame幀:

//拼資料,省略了資料拼裝的過程
void VideoChannel::sendSpsPps(uint8_t *sps, uint8_t *pps, int sps_len, int pps_len) {
    RTMPPacket *packet = new RTMPPacket;
    int bodysize = 13 + sps_len + 3 + pps_len;
    RTMPPacket_Alloc(packet, bodysize);
    int i = 0;
    //固定頭
    packet->m_body[i++] = 0x17;
    ......
    ......
    //sps pps沒有時間戳
    packet->m_nTimeStamp = 0;
    //不使用絕對時間
    packet->m_hasAbsTimestamp = 0;
    packet->m_headerType = RTMP_PACKET_SIZE_MEDIUM;

    callback(packet);
}

void VideoChannel::sendFrame(int type, int payload, uint8_t *p_payload) {
    //去掉 00 00 00 01 / 00 00 01
    if (p_payload[2] == 0x00){
        payload -= 4;
        p_payload += 4;
    } else if(p_payload[2] == 0x01){
        payload -= 3;
        p_payload += 3;
    }
    RTMPPacket *packet = new RTMPPacket;
    int bodysize = 9 + payload;
    .........
    .......
    packet->m_packetType = RTMP_PACKET_TYPE_VIDEO;
    packet->m_nChannel = 0x10;
    packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
    //通過函式
    callback(packet);
}
複製程式碼

最終通過 函式指標講packet放入佇列中:

//native-lib.cpp
void callback(RTMPPacket *packet) {
    if (packet) {
        //設定時間戳
        packet->m_nTimeStamp = RTMP_GetTime() - start_time;
        //這裡往佇列裡 塞資料,在start中 pop取資料然後發出去
        packets.push(packet);
    }
}
複製程式碼

佇列的消耗在 start連線成功時,視訊上傳的整個流程完成。

 //迴圈從佇列取包 然後傳送
        while (isStart) {
            packets.pop(packet);
            if (!isStart) {
                break;
            }
            if (!packet) {
                continue;
            }
            // 給rtmp的流id
            packet->m_nInfoField2 = rtmp->m_stream_id;
            //傳送包 1:加入佇列傳送
            ret = RTMP_SendPacket(rtmp, packet, 1);
            releasePackets(packet);
            if (!ret) {
                LOGE("傳送資料失敗");
                break;
            }
        }
        releasePackets(packet);
複製程式碼

執行結果

Nginx-RTMP推流(video)

VLC訪問伺服器拉流播放:

Nginx-RTMP推流(video)

相關文章