Android音視訊學習第4章:視訊直播實現之推送視訊篇
H.264標準學習
1.H264編碼框架
H264碼流檔案分為兩層:
(1) VCL(Video Coding Layer)視訊編碼層:
負責高效的視訊內容表示,VCL 資料即編碼處理的輸出,它表示被壓縮編碼後的視訊資料序列。
(2) NAL(Network Abstraction Layer)網路提取層:
負責以網路所要求的恰當的方式對資料進行打包和傳送,是傳輸層,不管是在本地播放還是在網路播放的傳輸,都要通過這一層來傳輸。
2.H264編碼原理
在H264協議裡定義了三種幀,完整編碼的幀叫I幀,參考之前的I幀生成的只包含差異部分編碼的幀叫P幀,還有一種參考前後的幀編碼的幀叫B幀
(1) I幀
幀內編碼幀 ,I幀表示關鍵幀,你可以理解為這一幀畫面的完整保留;解碼時只需要本幀資料就可以完成(因為包含完整畫面)
(2) P幀
前向預測編碼幀。P幀表示的是這一幀跟之前的一個關鍵幀(或P幀)的差別,解碼時需要用之前快取的畫面疊加上本幀定義的差別,生成最終畫面。(也就是差別幀,P幀沒有完整畫面資料,只有與前一幀的畫面差別的資料)
P幀的預測與重構:
P幀是以I幀為參考幀,在I幀中找出P幀“某點”的預測值和運動向量,取預測差值和運動向量一起傳送。在接收端根據運動向量從I幀中找出P幀“某點”的預測值並與差值相加以得到P幀“某點”樣值,從而可得到完整的P幀。
(3) B幀
雙向預測內插編碼幀。B幀是雙向差別幀,也就是B幀記錄的是本幀與前後幀的差別(具體比較複雜,有4種情況,但我這樣說簡單些),換言之,要解碼B幀,不僅要取得之前的快取畫面,還要解碼之後的畫面,通過前後畫面的與本幀資料的疊加取得最終的畫面。B幀壓縮率高,但是解碼時CPU會比較累。
B幀的預測與重構:
B幀以前面的I或P幀和後面的P幀為參考幀,“找出”B幀“某點”的預測值和兩個運動向量,並取預測差值和運動向量傳送。接收端根據運動向量在兩個參考幀中“找出(算出)”預測值並與差值求和,得到B幀“某點”樣值,從而可得到完整的B幀。
3. H264碼流分析
NALU型別:
1 ~ 23表示單個NAL包,24 ~ 31需要分包或者組合傳送
nal_unit_type取值的含義如下:
0 沒有定義
1 不分割槽,非IDR影象的片
2 片分割槽A
3 片分割槽B
4 片分割槽C
5 IDR影象中的片
6 補充增強資訊單元(SEI)
7 SPS(Sequence Parameter Set序列引數集,作用於一串連續的視訊影象,即視訊序列)
8 PPS(Picture Parameter Set影象引數集,作用於視訊序列中的一個或多個影象)
9 序列結束
10 序列結束
11 碼流結束
12 填充
13-23 保留
24 STAP-A 單一時間的組合包
25 STAP-B 單一時間的組合包
26 MTAP16 多個時間的組合包
27 MTAP24 多個時間的組合包
28 FU-A分片的單元
29 FU-B分片的單元
30-31 沒有定義
流程及程式碼片段
編碼與推流時序圖
推視訊流:
x264流程:
x264_param_default_preset 設定
x264_param_apply_profile 設定檔次
x264_picture_alloc(x264_picture_t輸入影象)初始化
x264_encoder_open 開啟編碼器
x264_encoder_encode 編碼
x264_encoder_close( h ) 關閉編碼器,釋放資源
/**
* 設定視訊引數
*/
setVideoOptions{
//x264_param_default_preset 設定
x264_param_t param;
//x264_param_default_preset 設定
x264_param_default_preset(¶m,"ultrafast","zerolatency");
//x264_param_apply_profile 設定檔次
//編碼輸入的畫素格式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幀
x264_param_apply_profile(¶m,"baseline");
//x264_picture_alloc(x264_picture_t輸入影象)初始化
//x264_picture_t(輸入影象)初始化
x264_picture_alloc(&pic_in, param.i_csp, param.i_width, param.i_height);
pic_in.i_pts = 0;
//x264_encoder_open 開啟編碼器
video_encode_handle = x264_encoder_open(¶m);
if(video_encode_handle){
LOGI("開啟視訊編碼器成功");
}
}
/**
* 將採集到視訊資料進行編碼
*/
fireVideo{
//將相機採集到的視訊資料使用x264編碼,並加入到佇列中
首先將安卓Camera採集到的視訊NV21資料轉碼成YUV420給到H264進行編碼。
NV21與YUV420p都屬於YUV420格式,每四個Y共用一組UV分量。區別是UV分量的空間排列不同。
NV21的顏色空間排列 :YYYYYYYY VUVU
YUV420p的顏色空間排列:YYYYYYYY UVUV
NV21轉YUV420p的公式:(Y不變)Y=Y,U=Y+1+1,V=Y+1
//視訊資料轉為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
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的個數
//使用x264_encoder_encode函式對視訊進行H264編碼
if(x264_encoder_encode(video_encode_handle,&nal, &n_nal,&pic_in,&pic_out) < 0){
LOGE("%s","編碼失敗");
return;
}
//編碼成功後,複製SPS和PPS放在每個關鍵幀的前面,讓每個關鍵幀(I幀)都附帶SPS或PPS
//使用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資料
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資料
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);
}
}
}
視訊RTMPPacket的構建
/**
* 傳送h264 SPS與PPS引數集
*/
add_264_sequence_header{
//構建RTMPPacket
int body_size = 16 + sps_len + pps_len; //按照H264標準配置SPS和PPS,共使用了16位元組
RTMPPacket *packet = malloc(sizeof(RTMPPacket));
//RTMPPacket初始化
RTMPPacket_Alloc(packet,body_size);
RTMPPacket_Reset(packet);
//Message Type,Payload Length,Time Stamp等的設定
//Message Type,RTMP_PACKET_TYPE_VIDEO:0x09
packet->m_packetType = RTMP_PACKET_TYPE_VIDEO;
//Payload Length
packet->m_nBodySize = body_size;
//Time Stamp:4位元組
//記錄了每一個tag相對於第一個tag(File Header)的相對時間。
//以毫秒為單位。而File Header的time stamp永遠為0。
packet->m_nTimeStamp = 0;
packet->m_hasAbsTimestamp = 0;
packet->m_nChannel = 0x04; //Channel ID,Audio和Vidio通道
packet->m_headerType = RTMP_PACKET_SIZE_MEDIUM;
H264 header資訊的引數解釋
(1) FrameType,4bit,幀型別
1 = key frame (for AVC, a seekable frame)
2 = inter frame (for AVC, a non-seekable frame)
3 = disposable inter frame (H.263 only)
4 = generated key frame (reserved for server use only)
5 = video info/command frame
H264的一般為1或者2.
(2)CodecID ,4bit,編碼型別
1 = JPEG(currently unused)
2 = Sorenson H.263
3 = Screen video
4 = On2 VP6
5 = On2 VP6 with alpha channel
6 = Screen video version 2
7 = AVC
VideoTagHeader之後跟著的就是VIDEODATA資料了,也就是video payload。如果視訊的格式是AVC(H.264)的話,VideoTagHeader會多出4個位元組的資訊,全部置為0。
接下來設定AVCDecoderConfigurationRecord,它包含的是H.264解碼相關比較重要的SPS和PPS資訊,在給AVC解碼器送資料流之前一定要把SPS和PPS資訊送出,否則的話解碼器不能正常解碼。而且在解碼器stop之後再次start之前,如seek、快進快退狀態切換等,都需要重新送一遍SPS和PPS的資訊。AVCDecoderConfigurationRecord在FLV檔案中一般情況也是出現1次,也就是第一個 video tag。
(3) AVCPacketType 8bit
IF AVCPacketType == 0 AVCDecoderConfigurationRecord(AVC sequence header)(此時FrameType必為1)
IF AVCPacketType == 1 One or more NALUs (Full frames are required)
IF AVCPacketType == 2 AVC end of sequence (lower level NALU sequence ender is not required or supported)
Composition Time,24bit
(4) AVCDecoderConfigurationRecord(AVCPacketType == 0,FrameType==1)
1.configurationVersion,8bit
2.AVCProfileIndication,8bit
3.profile_compatibility,8bit
4.AVCLevelIndication,8bit
5.lengthSizeMinusOne,8bit
H.264 視訊中 NALU 的長度,計算方法是 1 + (lengthSizeMinusOne & 3),實際測試時發現總為ff,計算結果為4.
6.numOfSequenceParameterSets,8bit
SPS 的個數,計算方法是 numOfSequenceParameterSets & 0x1F,實際測試時發現總為E1,計算結果為1
7.sequenceParameterSetLength,16bit
SPS 的長度
8.sequenceParameterSetNALUnits ,sps。
長度為sequenceParameterSetLength。
9.numOfPictureParameterSets,8bit
PPS 的個數,計算方法是 numOfPictureParameterSets & 0x1F,實際測試時發現總為E1,計算結果為1。
10.pictureParameterSetLength,16bit。
PPS的長度。
11.PPS
長度為pictureParameterSetLength。
unsigned char * body = packet->m_body;
int i = 0;
//二進位制表示:00010111
body[i++] = 0x17;//VideoHeaderTag:FrameType(1=key frame)+CodecID(7=AVC)
body[i++] = 0x00;//AVCPacketType = 0表示設定AVCDecoderConfigurationRecord
//composition time 0x000000 24bit ?
body[i++] = 0x00;
body[i++] = 0x00;
body[i++] = 0x00;
/*AVCDecoderConfigurationRecord*/
body[i++] = 0x01;//configurationVersion,版本為1
body[i++] = sps[1];//AVCProfileIndication
body[i++] = sps[2];//profile_compatibility
body[i++] = sps[3];//AVCLevelIndication
//?
body[i++] = 0xFF;//lengthSizeMinusOne,H264 視訊中 NALU的長度,計算方法是 1 + (lengthSizeMinusOne & 3),實際測試時發現總為FF,計算結果為4.
/*sps*/
body[i++] = 0xE1;//numOfSequenceParameterSets:SPS的個數,計算方法是 numOfSequenceParameterSets & 0x1F,實際測試時發現總為E1,計算結果為1.
body[i++] = (sps_len >> 8) & 0xff;//sequenceParameterSetLength:SPS的長度
body[i++] = sps_len & 0xff;//sequenceParameterSetNALUnits
memcpy(&body[i], sps, sps_len);
i += sps_len;
/*pps*/
body[i++] = 0x01;//numOfPictureParameterSets:PPS 的個數,計算方法是 numOfPictureParameterSets & 0x1F,實際測試時發現總為E1,計算結果為1.
body[i++] = (pps_len >> 8) & 0xff;//pictureParameterSetLength:PPS的長度
body[i++] = (pps_len) & 0xff;//PPS
memcpy(&body[i], pps, pps_len);
i += pps_len;
//將RTMPPacket加入佇列
add_rtmp_packet(packet);
}
/**
* 傳送h264幀資訊
*/
add_264_body{
//去掉起始碼(界定符)
if(buf[2] == 0x00){ //00 00 00 01
buf += 4;
len -= 4;
}else if(buf[2] == 0x01){ // 00 00 01
buf += 3;
len -= 3;
}
int body_size = len + 9;
RTMPPacket *packet = malloc(sizeof(RTMPPacket));
RTMPPacket_Alloc(packet,body_size);
unsigned char * body = packet->m_body;
//當NAL頭資訊中,type(5位)等於5,說明這是關鍵幀NAL單元
//buf[0] NAL Header與運算,獲取type,根據type判斷關鍵幀和普通幀
//00000101 & 00011111(0x1f) = 00000101
int type = buf[0] & 0x1f;
//Inter Frame 幀間壓縮
body[0] = 0x27;//VideoHeaderTag:FrameType(2=Inter Frame)+CodecID(7=AVC)
//IDR I幀影象
if (type == NAL_SLICE_IDR) {
body[0] = 0x17;//VideoHeaderTag:FrameType(1=key frame)+CodecID(7=AVC)
}
//AVCPacketType = 1
body[1] = 0x01; /*nal unit,NALUs(AVCPacketType == 1)*/
body[2] = 0x00; //composition time 0x000000 24bit
body[3] = 0x00;
body[4] = 0x00;
//寫入NALU資訊,右移8位,一個位元組的讀取?
body[5] = (len >> 24) & 0xff;
body[6] = (len >> 16) & 0xff;
body[7] = (len >> 8) & 0xff;
body[8] = (len) & 0xff;
/*copy data*/
memcpy(&body[9], buf, len);
packet->m_hasAbsTimestamp = 0;
packet->m_nBodySize = body_size;
packet->m_packetType = RTMP_PACKET_TYPE_VIDEO;//當前packet的型別:Video
packet->m_nChannel = 0x04;
packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
// packet->m_nTimeStamp = -1;
packet->m_nTimeStamp = RTMP_GetTime() - start_time;//記錄了每一個tag相對於第一個tag(File Header)的相對時間
add_rtmp_packet(packet);
}
現在將佇列裡面的RTMPPacket推送給流媒體
/**
* 從佇列中不斷拉取RTMPPacket傳送給流媒體伺服器)
*/
void *push_thread(void * arg){
//建立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 連線失敗");
goto end;
}
//計時
start_time = RTMP_GetTime();
if(!RTMP_ConnectStream(rtmp,0)){ //連線流
LOGE("%s","RTMP ConnectStream failed");
goto end;
}
is_pushing = TRUE;
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);
return 0;
}
完整程式碼
#include "com_dongnaoedu_live_jni_PushNative.h"
#include <android/log.h>
#include <android/native_window_jni.h>
#include <android/native_window.h>
#define LOGI(FORMAT,...) __android_log_print(ANDROID_LOG_INFO,"jason",FORMAT,##__VA_ARGS__)
#define LOGE(FORMAT,...) __android_log_print(ANDROID_LOG_ERROR,"jason",FORMAT,##__VA_ARGS__)
#include <pthread.h>
#include "queue.h"
#include "x264.h"
#include "rtmp.h"
#ifndef TRUE
#define TRUE 1
#define FALSE 0
#endif
//x264編碼輸入影象YUV420P
x264_picture_t pic_in;
x264_picture_t pic_out;
//YUV個數
int y_len, u_len, v_len;
//x264編碼處理器
x264_t *video_encode_handle;
unsigned int start_time;
//執行緒處理
pthread_mutex_t mutex;
pthread_cond_t cond;
//rtmp流媒體地址
char *rtmp_path;
//是否直播
int is_pushing = FALSE;
/**
* 從佇列中不斷拉取RTMPPacket傳送給流媒體伺服器)
*/
void *push_thread(void * arg){
//建立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 連線失敗");
goto end;
}
//計時
start_time = RTMP_GetTime();
if(!RTMP_ConnectStream(rtmp,0)){ //連線流
LOGE("%s","RTMP ConnectStream failed");
goto end;
}
is_pushing = TRUE;
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);
return 0;
}
JNIEXPORT void JNICALL Java_com_dongnaoedu_live_jni_PushNative_startPush
(JNIEnv *env, jobject jobj, jstring url_jstr){
//初始化的操作
const char* url_cstr = (*env)->GetStringUTFChars(env,url_jstr,NULL);
//複製url_cstr內容到rtmp_path
rtmp_path = malloc(strlen(url_cstr) + 1);
memset(rtmp_path,0,strlen(url_cstr) + 1);
memcpy(rtmp_path,url_cstr,strlen(url_cstr));
//初始化互斥鎖與條件變數
pthread_mutex_init(&mutex,NULL);
pthread_cond_init(&cond,NULL);
//建立佇列
create_queue();
//啟動消費者執行緒(從佇列中不斷拉取RTMPPacket傳送給流媒體伺服器)
pthread_t push_thread_id;
pthread_create(&push_thread_id, NULL,push_thread, NULL);
(*env)->ReleaseStringUTFChars(env,url_jstr,url_cstr);
}
JNIEXPORT void JNICALL Java_com_dongnaoedu_live_jni_PushNative_stopPush
(JNIEnv *env, jobject jobj){
is_pushing = FALSE;
}
JNIEXPORT void JNICALL Java_com_dongnaoedu_live_jni_PushNative_release
(JNIEnv *env, jobject jobj){
}
/**
* 設定視訊引數
*/
JNIEXPORT void JNICALL Java_com_dongnaoedu_live_jni_PushNative_setVideoOptions
(JNIEnv *env, jobject jobj, jint width, jint height, jint bitrate, jint fps){
x264_param_t param;
//x264_param_default_preset 設定
x264_param_default_preset(¶m,"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幀
x264_param_apply_profile(¶m,"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(¶m);
if(video_encode_handle){
LOGI("開啟編碼器成功...");
}
}
JNIEXPORT void JNICALL Java_com_dongnaoedu_live_jni_PushNative_setAudioOptions
(JNIEnv *env, jobject jobj, jint sampleRateInHz, jint channel){
}
/**
* 加入RTMPPacket佇列,等待發送執行緒傳送
*/
void add_rtmp_packet(RTMPPacket *packet){
pthread_mutex_lock(&mutex);
if(is_pushing){
queue_append_last(packet);
}
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
}
/**
* 傳送h264 SPS與PPS引數集
*/
void add_264_sequence_header(unsigned char* pps,unsigned char* sps,int pps_len,int sps_len){
int body_size = 16 + sps_len + pps_len; //按照H264標準配置SPS和PPS,共使用了16位元組
RTMPPacket *packet = malloc(sizeof(RTMPPacket));
//RTMPPacket初始化
RTMPPacket_Alloc(packet,body_size);
RTMPPacket_Reset(packet);
unsigned char * body = packet->m_body;
int i = 0;
//二進位制表示:00010111
body[i++] = 0x17;//VideoHeaderTag:FrameType(1=key frame)+CodecID(7=AVC)
body[i++] = 0x00;//AVCPacketType = 0表示設定AVCDecoderConfigurationRecord
//composition time 0x000000 24bit ?
body[i++] = 0x00;
body[i++] = 0x00;
body[i++] = 0x00;
/*AVCDecoderConfigurationRecord*/
body[i++] = 0x01;//configurationVersion,版本為1
body[i++] = sps[1];//AVCProfileIndication
body[i++] = sps[2];//profile_compatibility
body[i++] = sps[3];//AVCLevelIndication
//?
body[i++] = 0xFF;//lengthSizeMinusOne,H264 視訊中 NALU的長度,計算方法是 1 + (lengthSizeMinusOne & 3),實際測試時發現總為FF,計算結果為4.
/*sps*/
body[i++] = 0xE1;//numOfSequenceParameterSets:SPS的個數,計算方法是 numOfSequenceParameterSets & 0x1F,實際測試時發現總為E1,計算結果為1.
body[i++] = (sps_len >> 8) & 0xff;//sequenceParameterSetLength:SPS的長度
body[i++] = sps_len & 0xff;//sequenceParameterSetNALUnits
memcpy(&body[i], sps, sps_len);
i += sps_len;
/*pps*/
body[i++] = 0x01;//numOfPictureParameterSets:PPS 的個數,計算方法是 numOfPictureParameterSets & 0x1F,實際測試時發現總為E1,計算結果為1.
body[i++] = (pps_len >> 8) & 0xff;//pictureParameterSetLength:PPS的長度
body[i++] = (pps_len) & 0xff;//PPS
memcpy(&body[i], pps, pps_len);
i += pps_len;
//Message Type,RTMP_PACKET_TYPE_VIDEO:0x09
packet->m_packetType = RTMP_PACKET_TYPE_VIDEO;
//Payload Length
packet->m_nBodySize = body_size;
//Time Stamp:4位元組
//記錄了每一個tag相對於第一個tag(File Header)的相對時間。
//以毫秒為單位。而File Header的time stamp永遠為0。
packet->m_nTimeStamp = 0;
packet->m_hasAbsTimestamp = 0;
packet->m_nChannel = 0x04; //Channel ID,Audio和Vidio通道
packet->m_headerType = RTMP_PACKET_SIZE_MEDIUM; //?
//將RTMPPacket加入佇列
add_rtmp_packet(packet);
}
/**
* 傳送h264幀資訊
*/
void add_264_body(unsigned char *buf ,int len){
//去掉起始碼(界定符)
if(buf[2] == 0x00){ //00 00 00 01
buf += 4;
len -= 4;
}else if(buf[2] == 0x01){ // 00 00 01
buf += 3;
len -= 3;
}
int body_size = len + 9;
RTMPPacket *packet = malloc(sizeof(RTMPPacket));
RTMPPacket_Alloc(packet,body_size);
unsigned char * body = packet->m_body;
//當NAL頭資訊中,type(5位)等於5,說明這是關鍵幀NAL單元
//buf[0] NAL Header與運算,獲取type,根據type判斷關鍵幀和普通幀
//00000101 & 00011111(0x1f) = 00000101
int type = buf[0] & 0x1f;
//Inter Frame 幀間壓縮
body[0] = 0x27;//VideoHeaderTag:FrameType(2=Inter Frame)+CodecID(7=AVC)
//IDR I幀影象
if (type == NAL_SLICE_IDR) {
body[0] = 0x17;//VideoHeaderTag:FrameType(1=key frame)+CodecID(7=AVC)
}
//AVCPacketType = 1
body[1] = 0x01; /*nal unit,NALUs(AVCPacketType == 1)*/
body[2] = 0x00; //composition time 0x000000 24bit
body[3] = 0x00;
body[4] = 0x00;
//寫入NALU資訊,右移8位,一個位元組的讀取?
body[5] = (len >> 24) & 0xff;
body[6] = (len >> 16) & 0xff;
body[7] = (len >> 8) & 0xff;
body[8] = (len) & 0xff;
/*copy data*/
memcpy(&body[9], buf, len);
packet->m_hasAbsTimestamp = 0;
packet->m_nBodySize = body_size;
packet->m_packetType = RTMP_PACKET_TYPE_VIDEO;//當前packet的型別:Video
packet->m_nChannel = 0x04;
packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
// packet->m_nTimeStamp = -1;
packet->m_nTimeStamp = RTMP_GetTime() - start_time;//記錄了每一個tag相對於第一個tag(File Header)的相對時間
add_rtmp_packet(packet);
}
/**
* 將採集到視訊資料進行編碼
*/
JNIEXPORT void JNICALL Java_com_dongnaoedu_live_jni_PushNative_fireVideo
(JNIEnv *env, jobject jobj, 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
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資料
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資料
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);
}
}
}
JNIEXPORT void JNICALL Java_com_dongnaoedu_live_jni_PushNative_fireAudio
(JNIEnv *env, jobject jobj, jbyteArray array, jint len){
}
相關推薦
Android音視訊學習第4章:視訊直播實現之推送視訊篇
H.264標準學習 1.H264編碼框架 H264碼流檔案分為兩層: (1) VCL(Video Coding Layer)視訊編碼層: 負責高效的視訊內容表示,VCL 資料即編碼處理的輸出,它表示被壓縮編碼後的視訊資料序列。 (2)
【機器學習實戰—第4章:基於概率論的分類方法:樸素貝葉斯】程式碼報錯(python3)
1、報錯:UnicodeDecodeError: ‘gbk’ codec can’t decode byte 0xae in position 199: illegal multibyte sequence 原因:這是檔案編碼的問題,檔案中有非法的多位元組字元。 解決辦法:開啟Ch04\
《.NET 設計規範》第 4 章:類型設計規範
這樣的 方法 開放 flags 包含 困難 自由 權限 center 第 4 章:類型設計規範 4.1 類型和命名空間 要用命名空間把類型組織成一個由相關的功能區所構成的層次結構中。 避免非常深的命名空間層次。因為用戶需要經常回找,所以這樣的層次瀏覽起來很困難
第4章:資料和連結串列結構
資料結構是表示一個集合中包含的資料的一個物件 陣列資料結構 陣列是一個數據結構 支援按照位置對某一項的隨機訪問,且這種訪問的時間是常數 在建立陣列時,給定了用於儲存資料的位置的一個數目,並且陣列的長度保
讀書筆記--《程式設計師的自我修養》第4章:靜態連結(1)
本章以 如何將a.c檔案與b.c檔案連結成一個可執行檔案 來探討如何進行靜態連結 其中a.c和b.c檔案如下: a.c檔案 extern int shared; int main() { int a = 100; swap(&a,&shared);
第4章:作為Servlet:請求和響應/4.5 重定向和請求分派
重定向 在瀏覽器端進行重定向:可以是HTML,也可以是JSP 程式碼: package web; import java.io.IOException; import java.io.PrintWriter; import javax.servlet.S
第4章:作為Servlet:請求和響應/4.4 資源下載例項
package com.example.web; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import jav
第4章:作為Servlet:請求和響應/4.3 響應
響應內容型別 為什麼要設定內容型別?這個瀏覽器要根據這個型別進行相關操作,比如如果是視訊型別,要呼叫視訊播放軟體;如果是位元組流要啟動下載程式; 伺服器為什麼不能根據資源型別或者檔案型別自動設定內容型別呢?因為是在servlet中的doGet或者doPost方法中向響應
第4章:作為Servlet:請求和響應/4.2 請求
HTTP所有方法彙總 GET:獲取伺服器上的資源,如果HTML中沒有指定方法則預設是GET方法 POST:修改伺服器上的資源 HEAD:只要求獲取首部部分,有點像GET,但是沒有響應體 TRACE:要求服務端返回請求訊息 PUT:訊息體放在URL資源上
第4章:作為Servlet:請求和響應/4.1 Servlet載入過程
Servlet載入過程 容器tomcat啟動讀取web.xml檔案,載入相關資源 讀取全域性servlet資源,這部分資源所有的servlet物件共用(比如 資料庫連線資訊 ),形成ServletContext物件 讀取<servlet>配
第4章:介紹python物件型別/4.1 python的核心資料型別/4.4 字典
建立字典 格式:用大括號括起來,每一對以:隔開 內容:key和value的值隨便,數字、字元、列表、集合、元組混合都可以 注意:如果key相同,會用新的value覆蓋舊的value;這裡的key相同指的是要麼都是數字,要麼都是字元才算相同,比如1和“1”不算相同的
第4章:介紹python物件型別/4.1 python的核心資料型別/4.3 列表
獲取操作 >>> L = [123,'abc',1.23] >>> L[0] 從左邊開始獲取 123 >>> L[-1] 從右邊開始獲取 1.23 >>>
第4章:介紹python物件型別/4.1 python的核心資料型別/4.2 字串/4.2.4 字串格式化、字串編輯HTML或者XML語法、使用正則表示式
字串格式化 %s 方式格式化: >>> "%s,abc,%s" %('123','456') '123,abc,456' {數字}方式格式化: >>> "{0},abc,{1}".format('123','456') '123,a
第4章:介紹python物件型別/4.1 python的核心資料型別/4.2 字串/4.2.3 字串查詢、替換、分解、轉大小寫
字串查詢/替換/分解/轉大小寫 字串查詢 >>> S 'abcd' >>> S.find("bc") 1 替換 >>> S.replace("bc","xyz") 'axyzd' 分解 >>>
第4章:介紹python物件型別/4.1 python的核心資料型別/4.2.1 字串獲取操作、字串合併和重複操作
字串獲取操作 概念:用雙引號或者單引號括起來的一串字元 字串按下標獲取操作 定義字串 >>> S="abcd" 給字串求長度 >>> len(S) 4
易學筆記-Linux命令-第4章:研究作業系統
研究作業系統: 第4章:研究作業系統 · The Linux Command Line 中文版 · 看雲 命令列的通用格式:command -options arguments command:命令程式,比如 cd ls ll
易學筆記-go語言-第4章:基本結構和基本資料型別/4.4 變數/4.4.3 函式體內最簡單的變數初始化
函式體內最簡單的變數賦值 格式: 變數名 := 值 舉例: var goos string = os.Getenv("GOOS") fmt.Printf("The operating system is: %s\n", goos) //函式體內最
易學筆記-go語言-第4章:基本結構和基本資料型別/4.4 變數/4.4.2 宣告和賦值語句結合
宣告和賦值語句結合 格式:var identifier [type] = value 這裡的type是可選的,具體的型別參照: 第4章:基本結構和基本資料型別/4.2 Go 程式的基本結構和要素/4.2.8 型別 顯式型別舉例: //整型 var a&nbs
易學筆記-go語言-第4章:基本結構和基本資料型別/4.4 變數/4.4.4 函式體內並行初始化
函式體內並行賦值 在 第4章:基本結構和基本資料型別/4.4 變數/4.4.3 函式體內最簡單的變數賦值基礎上,多個變數同時賦值 舉例: 程式碼: a, b, c := 5, 10, "易學筆記" fmt.Printf("a&n
易學筆記-Go語言-第4章:基本結構和基本資料型別/4.5 基本型別/4.5.2 整形
整形 固定位元組數整形:與作業系統無關 int 和 uint 在 32 位作業系統上,它們均使用 32 位(4 個位元組),在 64 位作業系統上,它們均使用 64 位(8 個位元組)。 uintptr 存放指標 指定位元組