Nginx-RTMP推流(video)
Camera負責採集資料,把採集來的資料交給 X264進行編碼打包給RTMP進行推流,
Camera採集來的資料是NV21, 而X264編碼的輸入資料格式為I420格式。
NV21和I420都是屬於YUV420格式。而NV21是一種two-plane模式,即Y和UV分為兩個Plane(平面),但是UV(CbCr)交錯儲存,2個平面,而不是分為三個。這種排列方式被稱之為YUV420SP,而I420則稱之為YUV420P。(Y:明亮度、灰度,UV:色度、飽和度)
下圖是大小為4x4的NV21資料:Y1、Y2、Y5、Y6共用V1與U1,......

而I420則是

可以看出無論是哪種排列方式,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
介面能夠設定預覽顯示的角度:

根據文件,配置完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(¶m, "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(¶m, "baseline"); //開啟編碼器 videoCodec videoCodec = x264_encoder_open(¶m); 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函式中的整個流程:

資料傳輸
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, π_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); 複製程式碼