:视频直播实现之推送视频篇
Posted 薛萌
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了:视频直播实现之推送视频篇相关的知识,希望对你有一定的参考价值。
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平台音视频RTMP推送|GB28181对接之动态水印设计
JavaCV开发详解之26补充篇1:简单视频截图,视频文件和rtsp/rtmp/flv/hls等直播流截取一张图片
JavaCV开发详解之26补充篇1:简单视频截图,视频文件和rtsp/rtmp/flv/hls等直播流截取一张图片
Python案例篇:某短视频采集(不用等着大数据给你推送了)