Nginx-RTMP推流(video)

栏目: 服务器 · Nginx · 发布时间: 5年前

内容简介:Camera负责采集数据,把采集来的数据交给 X264进行编码打包给RTMP进行推流,Camera采集来的数据是NV21, 而X264编码的输入数据格式为I420格式。NV21和I420都是属于YUV420格式。而NV21是一种two-plane模式,即Y和UV分为两个Plane(平面),但是UV(CbCr)交错存储,2个平面,而不是分为三个。这种排列方式被称之为YUV420SP,而I420则称之为YUV420P。(Y:明亮度、灰度,UV:色度、饱和度)

Camera负责采集数据,把采集来的数据交给 X264进行编码打包给RTMP进行推流,

Camera采集来的数据是NV21, 而X264编码的输入数据格式为I420格式。

NV21和I420都是属于YUV420格式。而NV21是一种two-plane模式,即Y和UV分为两个Plane(平面),但是UV(CbCr)交错存储,2个平面,而不是分为三个。这种排列方式被称之为YUV420SP,而I420则称之为YUV420P。(Y:明亮度、灰度,UV:色度、饱和度)

下图是大小为4x4的NV21数据:Y1、Y2、Y5、Y6共用V1与U1,......

Nginx-RTMP推流(video)

而I420则是

Nginx-RTMP推流(video)

可以看出无论是哪种排列方式,YUV420的数据量都为: w*h+w/2*h/2+w/2*h/2 即为w*h*3/2

将NV21转位I420则为:

​ Y数据按顺序完整复制,U数据则是从整个Y数据之后加一个字节再每隔一个字节取一次。

传感器与屏幕自然方向不一致,将图像传感器的坐标系逆时针旋转90度,才能显示到屏幕的坐标系上。所以看到的画面是逆时针旋转了90度的,因此我们需要将图像顺时针旋转90度才能看到正常的画面。而Camera对象提供一个 setDisplayOrientation 接口能够设置预览显示的角度:

Nginx-RTMP推流(video)

根据文档,配置完Camera之后预览确实正常了,但是在onPreviewFrame中回调获得的数据依然是逆时针旋转了90度的。所以如果需要使用预览回调的数据,还需要对onPreviewFrame回调的byte[] 进行旋转。

即对NV21数据顺时针旋转90度。

初始化 编码器、队列SafeQueue

Camera 通过PreviewCallBack把 数据 byte[] data传给 native 中。native在init时准备一个编码器编码,一个队列用来存储数据,编码器 x264_t *videoCodec = 0; 存放在 VideoChannel.cpp中

//native-lib.cpp 文件
//队列
SafeQueue<RTMPPacket *> packets;
VideoChannel *videoChannel = 0;

extern "C"
JNIEXPORT void JNICALL
Java_com_tina_pushstream_live_LivePusher_native_1init(JNIEnv *env, jobject instance) {
    //准备一个Video编码器的 工具 类 :进行编码
    videoChannel = new VideoChannel;
    videoChannel->setVideoCallback(callback);
    //准备一个队列,打包好的数据 放入队列,在线程中统一的取出数据再发送给服务器
    packets.setReleaseCallback(releasePackets);
}
复制代码

在 VideoChannel中创建编码器,并且设置参数:

//  VideoChannel.h/VideoChannel.cpp
x264_t *videoCodec = 0;

//设置编码器参数
void VideoChannel::setVideoEncInfo(int width, int height, int fps, int bitrate) {
    pthread_mutex_lock(&mutex);
    mWidth = width;
    mHeight = height;
    mFps = fps;
    mBitrate = bitrate;
    ySize = width * height;
    uvSize = ySize / 4;
    if (videoCodec) {
        x264_encoder_close(videoCodec);
        videoCodec = 0;
    }
    if (pic_in) {
        x264_picture_clean(pic_in);
        delete pic_in;
        pic_in = 0;
    }

    //打开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;
    //设置了i_vbv_max_bitrate必须设置此参数,码率控制区大小,单位kbps
    param.rc.i_vbv_buffer_size = bitrate / 1000;
  
    //帧率
    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.pf_log = x264_log_default2;
    //用fps而不是时间戳来计算帧间距离
    param.b_vfr_input = 0;
    //帧距离(关键帧)  2s一个关键帧
    param.i_keyint_max = fps * 2;
    // 是否复制sps和pps放在每个关键帧的前面 该参数设置是让每个关键帧(I帧)都附带sps/pps。
    param.b_repeat_headers = 1;
    //多线程
    param.i_threads = 1;

    x264_param_apply_profile(&param, "baseline");
    //打开编码器 videoCodec
    videoCodec = x264_encoder_open(&param);
    pic_in = new x264_picture_t;
    x264_picture_alloc(pic_in, X264_CSP_I420, width, height);
    pthread_mutex_unlock(&mutex);
}
复制代码

#连接服务

native_start启动一个线程连接服务器,RTMP跟Http一样是基于TCP的上层协议,所以在start方法里连接。

//LivePusher 调用native_start()
public void startLive(String path) {
        native_start(path);
        videoChannel.startLive();
        audioChannel.startLive();
 }
复制代码

native层RTMP连接服务器,首先启动线程,在线程回调中开启连接:

//native-lib.cpp

extern "C"
JNIEXPORT void JNICALL
Java_com_dongnao_pusher_live_LivePusher_native_1start(JNIEnv *env, jobject instance,
                                                      jstring path_) {
    if (isStart) {
        return;
    }
    const char *path = env->GetStringUTFChars(path_, 0);
    char *url = new char[strlen(path) + 1];
    strcpy(url, path);
    isStart = 1;
    //启动线程
    pthread_create(&pid, 0, start, url);
    env->ReleaseStringUTFChars(path_, path);
}

//线程启动 RTMP connect 服务器
void *start(void *args) {
    char *url = static_cast<char *>(args);
    RTMP *rtmp = 0;
    do {
        rtmp = RTMP_Alloc();
        if (!rtmp) {
            LOGE("rtmp创建失败");
            break;
        }
        RTMP_Init(rtmp);
        //设置超时时间 5s
        rtmp->Link.timeout = 5;
        int ret = RTMP_SetupURL(rtmp, url);
        if (!ret) {
            LOGE("rtmp设置地址失败:%s", url);
            break;
        }
        //开启输出模式
        RTMP_EnableWrite(rtmp);
        ret = RTMP_Connect(rtmp, 0);
        if (!ret) {
            LOGE("rtmp连接地址失败:%s", url);
            break;
        }
        ret = RTMP_ConnectStream(rtmp, 0);
        if (!ret) {
            LOGE("rtmp连接流失败:%s", url);
            break;
        }

        //准备好了 可以开始推流了
        readyPushing = 1;
        //记录一个开始推流的时间
        start_time = RTMP_GetTime();
        packets.setWork(1);
        RTMPPacket *packet = 0;
        //循环从队列取包 然后发送
        while (isStart) {
            packets.pop(packet);
            if (!isStart) {
                break;
            }
            if (!packet) {
                continue;
            }
            // 给rtmp的流id
            packet->m_nInfoField2 = rtmp->m_stream_id;
            //发送包 1:加入队列发送
            ret = RTMP_SendPacket(rtmp, packet, 1);
            releasePackets(packet);
            if (!ret) {
                LOGE("发送数据失败");
                break;
            }
        }
        releasePackets(packet);
    } while (0);
    if (rtmp) {
        RTMP_Close(rtmp);
        RTMP_Free(rtmp);
    }
    delete url;
    return 0;
}
复制代码

以上start函数中的整个流程:

Nginx-RTMP推流(video)

数据传输

start连接好后,就开始pushVideo数据了:

//VideoChannel,  在LivePusher中start时调用 videoChannel.startLive()
public void startLive() {
    isLiving = true;
}

//在 PreviewCallback中的回调里,此时isLiving为true,调用native_pushVideo.
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
  if (isLiving) {
    mLivePusher.native_pushVideo(data);
  }
}
复制代码

从Camera采集的NV21到 X264的I420需要转码:

extern "C"
JNIEXPORT void JNICALL
Java_com_tina_pushstream_live_LivePusher_native_1pushVideo(JNIEnv *env, jobject instance,jbyteArray data_) {
    if (!videoChannel || !readyPushing) {
        return;
    }
    jbyte *data = env->GetByteArrayElements(data_, NULL);
    videoChannel->encodeData(data);
    env->ReleaseByteArrayElements(data_, data, 0);
}
复制代码

根据NV21、I420的yuv格式的不同,转化后存储到x264_picture_t *pic_in = 0;

//图片
x264_picture_t *pic_in = 0;

//编码,把NV21 转成I420
void VideoChannel::encodeData(int8_t *data) {
    //编码
    pthread_mutex_lock(&mutex);
    //将data 放入 pic_in
    //y数据
    memcpy(pic_in->img.plane[0], data, ySize);
    for (int i = 0; i < uvSize; ++i) {
        //间隔1个字节取一个数据
        //u数据
        *(pic_in->img.plane[1] + i) = *(data + ySize + i * 2 + 1);
        //v数据
        *(pic_in->img.plane[2] + i) = *(data + ySize + i * 2);
    }
    pic_in->i_pts = index++;
    //编码出的数据
    x264_nal_t *pp_nal;
    //编码出了几个 nalu (暂时理解为帧)
    int pi_nal;
    x264_picture_t pic_out;
    //编码
    int ret = x264_encoder_encode(videoCodec, &pp_nal, π_nal, pic_in, &pic_out);
    if (ret < 0) {
        pthread_mutex_unlock(&mutex);
        return;
    }
    int sps_len, pps_len;
    uint8_t sps[100];
    uint8_t pps[100];
    //
    for (int i = 0; i < pi_nal; ++i) {
        //数据类型
        if (pp_nal[i].i_type == NAL_SPS) {
            // 去掉 00 00 00 01
            sps_len = pp_nal[i].i_payload - 4;
            memcpy(sps, pp_nal[i].p_payload + 4, sps_len);
        } else if (pp_nal[i].i_type == NAL_PPS) {
            pps_len = pp_nal[i].i_payload - 4;
            memcpy(pps, pp_nal[i].p_payload + 4, pps_len);
            //拿到pps 就表示 sps已经拿到了
            sendSpsPps(sps, pps, sps_len, pps_len);
        } else {
            //关键帧、非关键帧
            sendFrame(pp_nal[i].i_type,pp_nal[i].i_payload,pp_nal[i].p_payload);
        }
    }
    pthread_mutex_unlock(&mutex);
}

复制代码

组装spspps帧、Frame帧:

//拼数据,省略了数据拼装的过程
void VideoChannel::sendSpsPps(uint8_t *sps, uint8_t *pps, int sps_len, int pps_len) {
    RTMPPacket *packet = new RTMPPacket;
    int bodysize = 13 + sps_len + 3 + pps_len;
    RTMPPacket_Alloc(packet, bodysize);
    int i = 0;
    //固定头
    packet->m_body[i++] = 0x17;
    ......
    ......
    //sps pps没有时间戳
    packet->m_nTimeStamp = 0;
    //不使用绝对时间
    packet->m_hasAbsTimestamp = 0;
    packet->m_headerType = RTMP_PACKET_SIZE_MEDIUM;

    callback(packet);
}

void VideoChannel::sendFrame(int type, int payload, uint8_t *p_payload) {
    //去掉 00 00 00 01 / 00 00 01
    if (p_payload[2] == 0x00){
        payload -= 4;
        p_payload += 4;
    } else if(p_payload[2] == 0x01){
        payload -= 3;
        p_payload += 3;
    }
    RTMPPacket *packet = new RTMPPacket;
    int bodysize = 9 + payload;
    .........
    .......
    packet->m_packetType = RTMP_PACKET_TYPE_VIDEO;
    packet->m_nChannel = 0x10;
    packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
    //通过函数
    callback(packet);
}
复制代码

最终通过 函数指针讲packet放入队列中:

//native-lib.cpp
void callback(RTMPPacket *packet) {
    if (packet) {
        //设置时间戳
        packet->m_nTimeStamp = RTMP_GetTime() - start_time;
        //这里往队列里 塞数据,在start中 pop取数据然后发出去
        packets.push(packet);
    }
}
复制代码

队列的消耗在 start连接成功时,视频上传的整个流程完成。

//循环从队列取包 然后发送
        while (isStart) {
            packets.pop(packet);
            if (!isStart) {
                break;
            }
            if (!packet) {
                continue;
            }
            // 给rtmp的流id
            packet->m_nInfoField2 = rtmp->m_stream_id;
            //发送包 1:加入队列发送
            ret = RTMP_SendPacket(rtmp, packet, 1);
            releasePackets(packet);
            if (!ret) {
                LOGE("发送数据失败");
                break;
            }
        }
        releasePackets(packet);
复制代码

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

世界是平的(3.0版)

世界是平的(3.0版)

[美] 托马斯·弗里德曼 / 何帆、肖莹莹、郝正非 / 湖南科学技术出版社 / 2008-9 / 58.00元

世界变得平坦,是不是迫使我们跑得更快才能拥有一席之地? 在《世界是平的》中,托马斯·弗里德曼描述了当代世界发生的重大变化。科技和通信领域如闪电般迅速的进步,使全世界的人们可以空前地彼此接近——在印度和中国创造爆炸式增长的财富;挑战我们中的一些人,比他们更快占领地盘。3.0版新增两章,更新了报告和注释方面的内容,这些内容均采自作者考察世界各地特别是整个美国中心地带的见闻,在美国本土,世界的平坦......一起来看看 《世界是平的(3.0版)》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具