在Android RTMP协议直播之C/C++下POSIX多线程编程中对POSIX多线程进行了讲解,相信读者对POSIX有一定了解。本文将通过POSIX进行音视频编码,在博客开始的时候就对H264编码进行了讲述,本次将使用H264标准下x264进行视频编码,使用AAC标准下FAAC进行音频编码。
直播工程结构
再次贴出本次直播项目的结构图
使用X264进行视频编码
在Android RTMP协议直播之音视频采集中实现了JAVA视频采集,回顾之前的视频采集流程,通过camera进行摄像头预览并将预览数据通过NativePush发送至Native层进行编码推流。1
2
3
4
5
6
7
8
9
10
11
12
public void onPreviewFrame(byte[] data, Camera camera) {
if (mCamera != null) {
mCamera.addCallbackBuffer(callbackBuffer);
}
if (isPushing) {
//调用NativePush.sendVideo将视频数据发送到Native进行处理
builder.getNativePush().sendVideo(data);
}
}
NativePush.sendVideo方法是一个native方法1
public native void sendVideo(byte[] data);
该native方法在Native中的实现如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
/**
* @param env
* @param instance
* @param data_
*/
JNIEXPORT void JNICALL
Java_com_ben_android_live_NativePush_sendVideo(JNIEnv *env, jobject instance, jbyteArray data_) {
jbyte *data = (*env)->GetByteArrayElements(env, data_, NULL);
//将NV21格式数据转换为YUV420
//NV21转YUV420p的公式:(Y不变)Y=Y,U=Y+1+1,V=Y+1
jbyte *y = pic.img.plane[0];
jbyte *u = pic.img.plane[1];
jbyte *v = pic.img.plane[2];
//设置y
memcpy(y, data, y_len);
//设置u,v
for (int i = 0; i < u_len; ++i) {
*(u + i) = *(data + y_len + i * 2 + 1);
*(v + i) = *(data + y_len + i * 2);
}
//使用x264编码
x264_nal_t *nal = NULL;
int n_nal = -1;
if (x264_encoder_encode(x264_encoder, &nal, &n_nal, &pic, &pic_out) < 0) {
LOGE("%s", "x264 encode error");
return;
}
//设置SPS PPS
unsigned char sps[SPS_OUT_BUFFER_SIZE];
unsigned char pps[PPS_OUT_BUFFER_SIZE];
int sps_length, pps_length;
//reset
memset(sps, 0, SPS_OUT_BUFFER_SIZE);
memset(pps, 0, PPS_OUT_BUFFER_SIZE);
pic.i_pts += 1; //顺序累加
for (int i = 0; i < n_nal; ++i) {
if (nal[i].i_type == NAL_SPS) {
//00 00 00 01;07;payload
//不复制四字节起始码,设置sps_length的长度为总长度-四字节起始码长度
sps_length = nal[i].i_payload - 4;
//复制sps数据
memcpy(sps, nal[i].p_payload + 4, sps_length);
} else if (nal[i].i_type == NAL_PPS) {
pps_length = nal[i].i_payload - 4;
memcpy(pps, nal[i].p_payload + 4, pps_length);
//发送视频序列消息
add_squence_header_to_rtmppacket(sps, pps, sps_length, pps_length);
} else {
//发送帧信息
add_frame_body_to_rtmppacket(nal[i].p_payload, nal[i].i_payload);
}
}
(*env)->ReleaseByteArrayElements(env, data_, data, 0);
}
代码不多主要做了三件事
- 将NV21格式转换为YUV420格式
- 使用X264进行编码
- 将编码后的数据进行包装添加到队列中
在进行编解码之前先对Android RTMP协议直播之音视频采集文中的x264相关配置初始化作出补充,贴出之前的配置代码。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51JNIEXPORT void JNICALL
Java_com_ben_android_live_NativePush_setNativeVideoOptions(JNIEnv *env, jobject instance,
jint width, jint height, jint bitrate,
jint fps) {
LOGI("%s", "setNativeVideoOptions...");
//x264提供了多种配置,这里选择0延迟
x264_param_default_preset(¶m, "ultrafast", "zerolatency");
//YUV420
param.i_csp = X264_CSP_I420;
//这里的宽高需要同JAVA Camera中预览的宽高同步,否则会出现分屏的情况
param.i_width = width;
param.i_height = height;
//设置yuv长度
y_len = width * height;
u_len = y_len / 4;
v_len = u_len;
//码率控制,CQP(恒定质量),CRF(恒定码率),ABR(平均码率)
param.rc.i_rc_method = X264_RC_CRF;
//码率 单位(Kbps)
param.rc.i_bitrate = bitrate / 1000;
//瞬时最大码率
param.rc.i_vbv_max_bitrate = bitrate / 1000 * 1.2;
//通过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;
//是否把SPS PPS放入每个关键帧,提高纠错能力
param.b_repeat_headers = 1;
//设置level级别,5.1
param.i_level_idc = 51;
//设置档次
x264_param_apply_profile(¶m, "baseline");
//@pic x264操作句柄
x264_picture_alloc(&pic, param.i_csp, param.i_width, param.i_height);
x264_encoder = x264_encoder_open(¶m);
if (x264_encoder) {
LOGI("initVideoOptions:%s", "success");
} else {
LOGE("initVideoOptions:%s", "failed");
}
}
在本系列文章开始之初就对H264标准做出了详解,上边代码出现了如下代码注意这些配置对直播相关具有很重要的影响1
param.b_repeat_headers = 1;
在Android RTMP协议直播之H264标准(一)文中对SPS以及PPS进行了简述,这里配置x264的时候选择将SPS和PPS放入每个关键帧,其目的在于利用了SPS以及PPS的特性(具体含义这里不做解释,请参考之前的文章)来提高纠错能力。1
2param.i_level_idc = 51;
x264_param_apply_profile(¶m, "baseline");
对level以及profile的配置会直接影响到直播清晰度等,具体含义参考Android RTMP协议直播之H264标准(二)
将NV21格式转换为YUV420格式
YUV介绍
YUV是一种颜色编码方法。常使用在各个影像处理组件中。 YUV在对照片或影片编码时,考虑到人类的感知能力,允许降低色度的带宽(可以脑补小时候的黑白电视机)。其中Y’代表明亮度(luma; brightness)而U与V存储色度(色讯; chrominance; color)部分;
YUV Formats分成两个格式:
- 紧缩格式(packed formats):将Y、U、V值存储成Macro Pixels数组,和RGB的存放方式类似。
- 平面格式(planar formats):将Y、U、V的三个分量分别存放在不同的矩阵中。
常见的YUV格式
为节省带宽起见,大多数YUV格式平均使用的每像素位数都少于24位。主要的抽样(subsample)格式有YCbCr 4:2:0、YCbCr 4:2:2、YCbCr 4:1:1和YCbCr 4:4:4。YUV的表示法称为A:B:C表示法
- 4:4:4表示完全取样。
- 4:2:2表示2:1的水平取样,垂直完全采样。
- 4:2:0表示2:1的水平取样,垂直2:1采样。
- 4:1:1表示4:1的水平取样,垂直完全采样。
最常用Y:UV记录的比重通常1:1或2:1,DVD-Video是以YUV 4:2:0的方式记录,也就是我们俗称的I420,YUV4:2:0并不是说只有U(即Cb), V(即Cr)一定为0,而是指U:V互相援引,时见时隐,也就是说对于每一个行,只有一个U或者V分量,如果一行是4:2:0的话,下一行就是4:0:2,再下一行是4:2:0…以此类推。至于其他常见的YUV格式有YUY2、YUYV、YVYU、UYVY、AYUV、Y41P、Y411、Y211、IF09、IYUV、YV12、YVU9、YUV411、YUV420等。
NV21
NV21与YUV420p都属于YUV420格式,每四个Y共用一组UV分量。区别是UV分量的空间排列不同。
NV21的颜色空间排列 :YYYYYYYY VUVU
YUV420p的颜色空间排列:YYYYYYYY UVUV
NV21转YUV420p的公式:(Y不变)Y=Y,U=Y+1+1,V=Y+1
NV21 -> YUV420
1 | //将NV21格式数据转换为YUV420 |
使用X264进行编码
对X264进行初始化配置以后就可以使用X264进行视频编码了1
2
3
4
5
6
7
8//...
x264_nal_t *nal = NULL;
int n_nal = -1; //nal单元数
if (x264_encoder_encode(x264_encoder, &nal, &n_nal, &pic, &pic_out) < 0) {
LOGE("%s", "x264 encode error");
return;
}
//...
使用x264_encoder_encode函数将刚刚处理的YUV420数据进行编码处理
将编码后的数据进行包装添加到队列中
在对YUV420进行编码后就可以解析NAL单元数据并添加到队列中了1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 //设置SPS PPS
unsigned char sps[SPS_OUT_BUFFER_SIZE];
unsigned char pps[PPS_OUT_BUFFER_SIZE];
int sps_length, pps_length;
//reset
memset(sps, 0, SPS_OUT_BUFFER_SIZE);
memset(pps, 0, PPS_OUT_BUFFER_SIZE);
pic.i_pts += 1;//逐帧
for (int i = 0; i < n_nal; ++i) {
if (nal[i].i_type == NAL_SPS) {
//00 00 00 01;07;payload
//不复制四字节起始码,设置sps_length的长度为总长度-四字节起始码长度
sps_length = nal[i].i_payload - 4;
//复制sps数据
memcpy(sps, nal[i].p_payload + 4, sps_length);
} else if (nal[i].i_type == NAL_PPS) {
pps_length = nal[i].i_payload - 4;
memcpy(pps, nal[i].p_payload + 4, pps_length);
//发送视频序列消息
add_squence_header_to_rtmppacket(sps, pps, sps_length, pps_length);
} else {
//发送帧信息
add_frame_body_to_rtmppacket(nal[i].p_payload, nal[i].i_payload);
}
}
关注代码行9这一行代码,回顾我们初始化X264的时候设置的profile为baseline,当profile为baseline的时候是不存在B帧的,所以在没有B帧的时候DTS和PTS是一致的,这里每次累计加一就是表示直播解码的时候逐帧解码渲染。关于profile和IBP帧以及DTS和PTS请参阅Android RTMP协议直播之H264标准(二)
使用FAAC进行音频编码
和视频编码类似,当开启直播的时候会初始化AAC相关配置然后在JAVA端通过AudioRecord采集音频数据然后发送给Native进行音频编码。
在AudioPusher中1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24//...
public void startPush() {
isPushing = true;
builder.getNativePush().setNativeAudioOptions(builder.getSampleRateInHz(), builder.getChannelConfig());
mAudioThread = new Thread(new AudioPushTask());
mAudioThread.start();
}
//...
private class AudioPushTask implements Runnable {
public void run() {
audioRecord.startRecording();
while (isPushing && audioRecord != null) {
byte[] buffer = new byte[bufferSize];
int len = audioRecord.read(buffer, 0, bufferSize);
if (len > 0) {
builder.getNativePush().sendAudio(buffer, 0, len);
}
}
}
}
当开始直播时,将会通过setNativeAudioOptions初始化AAC音频编码相关配置,该native函数在C中的实现1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29JNIEXPORT void JNICALL
Java_com_ben_android_live_NativePush_setNativeAudioOptions(JNIEnv *env, jobject instance,
jint sampleRateInHz, jint channel) {
faacEncodeHandle = faacEncOpen(sampleRateInHz, channel, &inputSamples, &maxOutputBytes);
if (!faacEncodeHandle) {
LOGE("%s", "FAAC encode open failed!");
return;
}
faacEncConfigurationPtr faacEncodeConfigurationPtr = faacEncGetCurrentConfiguration(
faacEncodeHandle);
//指定MPEG版本
faacEncodeConfigurationPtr->mpegVersion = MPEG4;
faacEncodeConfigurationPtr->allowMidside = 1;
faacEncodeConfigurationPtr->aacObjectType = LOW;
faacEncodeConfigurationPtr->outputFormat = 0; //输出是否包含ADTS头
faacEncodeConfigurationPtr->useTns = 1; //时域噪音控制,大概就是消爆音
faacEncodeConfigurationPtr->useLfe = 0;
faacEncodeConfigurationPtr->quantqual = 100;
faacEncodeConfigurationPtr->bandWidth = 0; //频宽
faacEncodeConfigurationPtr->shortctl = SHORTCTL_NORMAL;
//call faacEncSetConfiguration
if (!faacEncSetConfiguration(faacEncodeHandle, faacEncodeConfigurationPtr)) {
LOGE("%s", "faacEncSetConfiguration failed!");
return;
}
LOGI("%s", "faac initialization successful");
}
相对于X264的配置,FAAC的配置少了不少。这里主要的点是在初始化配置FAAC的时候必须制定MPEG版本。FAAC配置完成后就可以进行音频编码了,在AudioPusher中调用了native函数sendAudio,以下是该函数的实现。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38JNIEXPORT void JNICALL
Java_com_ben_android_live_NativePush_sendAudio(JNIEnv *env, jobject instance, jbyteArray audioData_,
jint offsetInBytes, jint sizeInBytes) {
jbyte *audioData = (*env)->GetByteArrayElements(env, audioData_, NULL);
int *pcmbuf;
unsigned char *bitbuf;
pcmbuf = (short *) malloc(inputSamples * sizeof(int));
bitbuf = (unsigned char *) malloc(maxOutputBytes * sizeof(unsigned char));
int nByteCount = 0;
unsigned int nBufferSize = (unsigned int) sizeInBytes / 2;
unsigned short *buf = (unsigned short *) audioData;
while (nByteCount < nBufferSize) {
int audioLength = inputSamples;
if ((nByteCount + inputSamples) >= 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 += inputSamples;
//利用FAAC进行编码,pcmbuf为转换后的pcm流数据,audioLength为调用faacEncOpen时得到的输入采样数,bitbuf为编码后的数据buff,nMaxOutputBytes为调用faacEncOpen时得到的最大输出字节数
int byteslen = faacEncEncode(faacEncodeHandle, pcmbuf, audioLength,
bitbuf, maxOutputBytes);
if (byteslen < 1) {
continue;
}
add_audio_body_to_rtmppacket(bitbuf, byteslen);//从bitbuf中得到编码后的aac数据流,放到数据队列
}
(*env)->ReleaseByteArrayElements(env, audioData_, audioData, 0);
if (bitbuf)
free(bitbuf);
if (pcmbuf)
free(pcmbuf);
}
使用RTMP推送编码后的视频数据
在上边我们已经对Camera预览的数据进行了编码,现在需要将这些编码后的数据包装在RTMP Packet中进行发送。现在回到视频编码代码块有以下两个函数的调用。1
2
3
4
5
6//发送视频序列消息
add_squence_header_to_rtmppacket(sps, pps, sps_length, pps_length);
//...
//发送帧信息
add_frame_body_to_rtmppacket(nal[i].p_payload, nal[i].i_payload);
可能读者在这有疑问了,W**TF为什么要分两个函数进行封装?,稍安勿躁我慢慢道来。再本文开始对RTMP协议进行了讲解其中RTMP规定了Message中的结构类型有MessageType来进行指定,并且Message在网络传输的过程中会被拆分成Chunk Message,其中Chunk也有特定的格式进行约束。再加之H264标准中的SPS以及PPS的特殊性我们这对NAL类型进行判断,如果该NAL单元类型是SPS或者PPS则按添加头信息的方式进行封装,其他内容方式封装。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77/**
* 添加视频序列消息头至rtmppacket中
* @param sps
* @param pps
* @param sps_length
* @param pps_length
*/
void add_squence_header_to_rtmppacket(unsigned char *sps, unsigned char *pps, int sps_length,
int pps_length) {
//packet内容大小
int size = sps_length + pps_length + 16;
RTMPPacket *packet = malloc(sizeof(RTMPPacket));
RTMPPacket_Alloc(packet, size);
RTMPPacket_Reset(packet);
//设置packet中的body信息
char *body = packet->m_body;
int i = 0;
/**
* (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
*/
//body 第一位
body[i++] = 0x17; //(1)-(2)4bit*2关键帧,帧内压缩
body[i++] = 0x00; //(3)8bit
body[i++] = 0x00; //(4)8bit
body[i++] = 0x00; //(5)8bit
body[i++] = 0x00; //(6)8bit
/*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_length >> 8) & 0xff;//sequenceParameterSetLength:SPS的长度
body[i++] = sps_length & 0xff;//sequenceParameterSetNALUnits
memcpy(&body[i], sps, sps_length);
i += sps_length;
/*pps*/
body[i++] = 0x01;//numOfPictureParameterSets:PPS 的个数,计算方法是 numOfPictureParameterSets & 0x1F,实际测试时发现总为E1,计算结果为1.
body[i++] = (pps_length >> 8) & 0xff;//pictureParameterSetLength:PPS的长度
body[i++] = (pps_length) & 0xff;//PPS
memcpy(&body[i], pps, pps_length);
i += pps_length;
//设置packet头信息
packet->m_packetType = RTMP_PACKET_TYPE_VIDEO;
packet->m_nBodySize = size;
packet->m_nTimeStamp = 0;
packet->m_hasAbsTimestamp = 0;
packet->m_nChannel = 0x04;//Audio和Video通道
packet->m_headerType = RTMP_PACKET_SIZE_MEDIUM;
add_rtmp_packet_queue(packet);
}
1 | /** |
使用RTMP推送编码后的音频数据
完整Native代码
1 | //@author zhangchuan622@gmail.com |