欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

音视频基础(网络传输):视频编码推流

程序员文章站 2022-07-14 20:44:22
...

一. 前言

  一个 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) {
// 开启直播并且已经成功连接服务器才获取i420数据 if (rtmpClient.isConnectd()) {
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 来进行通信。

  再来复习下 librtmp 的使用 音视频基础(网络传输):视频编码推流

所以 startLive 方法对应的 JNI 实现为:

//编码结果
x264_nal_t *pp_nal;
int pi_nal; //编码出多少个nalu
x264_picture_t pic_out;
int ret = x264_encoder_encode(codec, &pp_nal, &pi_nal, &pic_in, &pic_out); if (ret < 0) {
//编码失败 return; } int spslen, ppslen; uint8_t sps[100]; uint8_t pps[100]; for (int i = 0; i < pi_nal; ++i) { if (pp_nal[i].i_type == NAL_SPS) { //记录sps 长度需要去掉 0 0 0 1帧分割数据 spslen = pp_nal[i].i_payload - 4; memcpy(sps, pp_nal[i].p_payload + 4, spslen); } else if (pp_nal[i].i_type == NAL_PPS) { ppslen = pp_nal[i].i_payload - 4; memcpy(pps, pp_nal[i].p_payload + 4, ppslen); //发送sps与pps  sendVideoConfig(sps, pps, spslen, ppslen); } else { //发送其他 sendFrame(pp_nal[i].i_type, pp_nal[i].i_payload, pp_nal[i].p_payload); }  }} 

  startLive 方法会在 JNI 中启动线程与服务器建立 Socket 连接,并随时能够让客户端发送音视频数据至服务器。

  在 完成连接后 rtmpClient.isConnectd() 为真,那么就会开始获取 I420 数据,并执行 rtmpClient.sendVideo 方法。

  那么 sendVideo 就会将图像交给 JNI 完成编码与封包并最终发送。

三. x264 编码

https://www.jianshu.com/p/9522c4a7818d 链接的文中普及了 H.264 的各种基础概念

  在 RTMPClient 的构造方法中会根据使用者传递的参数,进行视频编码器 x264 的初始化。

void openCodec(int width, int height, int fps, int bitrate) {
        if (equals(width, height, fps, bitrate)) {
return; }
        closeCodec();
//打开x264编码器 //x264编码器的属性 x264_param_t param; //2: 最快
//3: 无延迟编码 x264_param_default_preset(&param, "ultrafast", "zerolatency"); 

接下来当有数据需要编码时,就可以使用 codec 完成编码。

void openCodec(int width, int height, int fps, int bitrate) {
        if (equals(width, height, fps, bitrate)) {
return; }
        closeCodec();
//打开x264编码器 //x264编码器的属性 x264_param_t param; //2: 最快
//3: 无延迟编码 x264_param_default_preset(&param, "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; //帧率 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(&param, "baseline"); codec = x264_encoder_open(&param); this->width = width; this->height = height; this->fps = fps; this->bitrate = bitrate; } 

  是否还记得在发送 I 帧之前我们需要发送 sps 与 pps,所以我们在编码器初始化设置中配置了:

// 是否复制 sps 和 pps 放在每个关键帧的前面 该参数设置是让每个 I 帧都附带 sps/pps。param.b_repeat_headers = 1;

  这样只需要我们得到 sps 与 pps 之后直接发送给服务器,因为下一帧必然是 I 帧。在 sendVideoConfig 与 sendFrame 方法中,我们会将编码数组包装为: 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 ? 后期再和大家细讲。

改不完的 Bug,写不完的矫情。公众号现在专注于Android移动开发,涵盖音视频,APM等各个知识领域。
只做全网最geek的公众号,欢迎您的关注!

音视频基础(网络传输):视频编码推流
相关标签: 学习