目录:
RTMP(一)录屏直播理论入门
RTMP(二)搭建推流服务
RTMP (三)音视频采集与数据封包
RTMP(四)交叉编译与CameraX
RTMP (五)摄像头数据处理
RTMP (六)音视频编码推流
我们已经成功通过CameraX获得了摄像头所捕获的I420数据,下面我们需要进入直播过程的后续 的编码阶段。一个I420格式的图像大小为: 宽x高x3/2
,这意味着640x480的分辨率,10 fps的视频,我们1s也会 产生 4M 左右的数据。
因此我们需要使用编码算法对这个图像数据进行编码,让其数据量变小。还记得我们交叉编 译的x264库吗?接下来我们将使用x264对摄像头采集的图像进行编码。当然这里考虑到程序性能等问题,我们首 先需要进行一些设计。
还是围绕这个流程
RTMPClient
在从Image获取到I420数据的过程中,我们会执行一系列的方法。那么在直播未开启阶段,我们在分析图像接口回调中进行一个前置判断。
//宽、高、帧率、码率
rtmpClient = new RtmpClient(480, 640, 10, 640_000);
@Override
public void analyze(ImageProxy image, int rotationDegrees) {
if (rtmpClient.isConnected()) {
byte[] bytes = ImageUtils.getBytes(image, rotationDegrees,
rtmpClient.getWidth(), rtmpClient.getHeight());
rtmpClient.sendVideo(bytes);
}
}
这里的 rtmpClient 是我们封装的一个处理与rtmp服务器连接与发送音视频数据的类。它需要负责打开/关闭 编解 码器,并且将Java传输的音视频数据送于JNI层进行编码,最终再封包并发送给服务器。它这里的设计为不关心数据 来源,使用者必须保证提供的图像数据为480x640分辨率。
rtmpClient 可以在 onCreate
中创建,在创建时需要对编码器初始化,确定编码器处理的图像宽与高。然而我们 获得的Image中图像的宽与高,可能与 rtmpClient 中设置的宽高不匹配。因此可以借助上节课中使用的 libYUV 在对图像旋转之后进行缩放至需要的宽高。
使用者首先执行rtmpClient.startLive("rtmp://xxxx");
会与RTMP服务器建立连接。我们仍然使用 录屏直播,之前的文章
时 所使用的 librtmp 来进行通信。
所以 startLive 方法对应的JNI实现为:
void *connect(void *args) {
int ret;
rtmp = RTMP_Alloc();// 申请堆内存
RTMP_Init(rtmp);
do {
ret = RTMP_SetupURL(rtmp, path);
if (!ret) {
//TODO: 通知java地址传入的有问题
// __android_log_print(ANDROID_LOG_ERROR, "X264", "%s,%s", "RTMP_SetupURL", path);
break;
}
// 打开输出模式,这里推流的时候.(拉流的时候可以不用开启)
RTMP_EnableWrite(rtmp);
ret = RTMP_Connect(rtmp, 0);
if (!ret) {
//TODO: 通知java服务器链接失败
__android_log_print(ANDROID_LOG_ERROR, "X264", "%s", "RTMP_Connect");
break;
}
ret = RTMP_ConnectStream(rtmp, 0);
if (!ret) {
//TODO: 通知java未连接到流(相当于握手失败)
__android_log_print(ANDROID_LOG_ERROR, "X264", "%s", "RTMP_ConnectStream");
break;
}
} while (false);
if (!ret) {
if (rtmp) {
RTMP_Close(rtmp);
RTMP_Free(rtmp);
rtmp = 0;
}
}
delete (path);
path = 0;
// 通知java可以开始推流了,(在子线程通知Java)
helper->onPrepare(ret);
startTime = RTMP_GetTime();
return 0;
}
extern "C"
JNIEXPORT void JNICALL
Java_top_zcwfeng_pusher_RtmpClient_connect(JNIEnv *env, jobject thiz, jstring url_) {
const char *url = env->GetStringUTFChars(url_, 0);
// 因为你在这里并不知道调用的是java的主线程还是子线程,所以需要JavaVM ,env 绑定线程
path = new char[strlen(url) + 1];
strcpy(path, url);
// 启动子线程url
pthread_create(&pid, 0, connect, 0);
env->ReleaseStringUTFChars(url_, url);
}
x264编码
参考h264 基础概念
在RTMPClient的构造方法中会根据使用者传递的参数,进行视频编码器x264的初始化。
void VideoChannel::openCodec(int width, int height, int fps, int bitrate) {
// 编码器参数配置
x264_param_t param;
// ultrafast: 编码速度与质量的控制 ,使用最快的模式编码
// zerolatency: 无延迟编码 , 实时通信方面
x264_param_default_preset(¶m, "ultrafast", "zerolatency");
// main base_line high
//base_line 3.2 编码规格 无B帧(数据量最小,但是解码速度最慢)
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;
//帧率
param.i_fps_num = fps;
param.i_fps_den = 1;
param.pf_log = x264_log_default2;
//帧距离(关键帧) 2s一个关键帧(影像观看者第一帧出现)
param.i_keyint_max = fps * 2;
// 是否复制sps和pps放在每个关键帧的前面 该参数设置是让每个关键帧(I帧)都附带sps/pps。
param.b_repeat_headers = 1;
//不使用并行编码。zerolatency场景下设置param.rc.i_lookahead=0;
// 那么编码器来一帧编码一帧,无并行、无延时
param.i_threads = 1;
param.rc.i_lookahead = 0;
x264_param_apply_profile(¶m, "baseline");
codec = x264_encoder_open(¶m);
ySize = width * height;
uSize = (width >> 1) * (height >> 1);
this->width = width;
this->height = height;
}
接下来当有数据需要编码时,就可以使用 codec 完成编码。
void VideoChannel::encode(uint8_t *data) {
//输出的待编码数据
x264_picture_t pic_in;
x264_picture_alloc(&pic_in, X264_CSP_I420, width, height);
pic_in.img.plane[0] = data;
pic_in.img.plane[1] = data + ySize;
pic_in.img.plane[2] = data + ySize + uSize;
//todo 编码的i_pts,每次需要增长
pic_in.i_pts = i_pts++;
x264_picture_t pic_out;
x264_nal_t *pp_nal;
int pi_nal;
//pi_nal: 输出了多少nal
int error = x264_encoder_encode(codec, &pp_nal, &pi_nal, &pic_in, &pic_out);
if (error <= 0) {
return;
}
int spslen, ppslen;
uint8_t *sps;
uint8_t *pps;
for (int i = 0; i < pi_nal; ++i) {
int type = pp_nal[i].i_type;
//数据
uint8_t *p_payload = pp_nal[i].p_payload;
//数据长度
int i_payload = pp_nal[i].i_payload;
if (type == NAL_SPS) {
//sps后面肯定跟着pps
spslen = i_payload - 4; //去掉间隔 00 00 00 01
sps = (uint8_t *) alloca(spslen); //栈中申请,不需要释放
memcpy(sps, p_payload + 4, spslen);
} else if (type == NAL_PPS) {
ppslen = i_payload - 4; //去掉间隔 00 00 00 01
pps = (uint8_t *) alloca(ppslen);
memcpy(pps, p_payload + 4, ppslen);
//pps 后面肯定有I帧 ,发I帧之前要发一个sps与pps
sendVideoConfig(sps, pps, spslen, ppslen);
} else {
sendFrame(type, p_payload, i_payload);
}
}
}
fps 后面一定跟着pps,他们的间隔都是 00 00 00 01
在发送I帧之前我们需要发送sps与pps,所以我们在编码器初始化设置中配置了:
// 是否复制sps和pps放在每个关键帧的前面 该参数设置是让每个I帧都附带sps/pps。
param.b_repeat_headers = 1;
这样只需要我们得到sps与pps之后直接发送给服务器,因为下一帧必然是I帧。在 sendVideoConfig 与 sendFrame 方法中,我们会将编码数组包装为: RTMPPacket packet 。
而发送代码就比较简单了:
/**
* 回调,类似java
* @param packet
*/
void callback(RTMPPacket *packet) {
if (rtmp) {
packet->m_nInfoField2 = rtmp->m_stream_id;
packet->m_nTimeStamp = RTMP_GetTime() - startTime;
RTMP_SendPacket(rtmp, packet, 1);
}
RTMPPacket_Free(packet);
delete (packet);
}
RTMPPacket 参看我之前文章封装
关于 封包参照,我的
[RTMP (三)音视频采集与数据封包](https://www.jianshu.com/p/952295c4fdfc)