音视频技术杂谈音视频-RTMP 知识总结《简书与写作》

RTMP(六) 音视频编码推流

2020-11-05  本文已影响0人  zcwfeng

目录:

RTMP(一)录屏直播理论入门
RTMP(二)搭建推流服务
RTMP (三)音视频采集与数据封包
RTMP(四)交叉编译与CameraX
RTMP (五)摄像头数据处理
RTMP (六)音视频编码推流

我们已经成功通过CameraX获得了摄像头所捕获的I420数据,下面我们需要进入直播过程的后续 的编码阶段。一个I420格式的图像大小为: 宽x高x3/2,这意味着640x480的分辨率,10 fps的视频,我们1s也会 产生 4M 左右的数据。因此我们需要使用编码算法对这个图像数据进行编码,让其数据量变小。还记得我们交叉编 译的x264库吗?接下来我们将使用x264对摄像头采集的图像进行编码。当然这里考虑到程序性能等问题,我们首 先需要进行一些设计。

还是围绕这个流程

librtmp.png

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(&param, "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(&param, "baseline");

    codec = x264_encoder_open(&param);
    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

上一篇下一篇

猜你喜欢

热点阅读