Nginx-RTMP推流(video)

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

内容简介: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);
复制代码

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

查看所有标签

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

Sass and Compass in Action

Sass and Compass in Action

Wynn Netherland、Nathan Weizenbaum、Chris Eppstein、Brandon Mathis / Manning Publications / 2013-8-2 / USD 44.99

Written by Sass and Compass creators * Complete Sass language reference * Covers prominent Compass community plug-ins * Innovative approach to creating stylesheets Cascading Style Sheets paint the we......一起来看看 《Sass and Compass in Action》 这本书的介绍吧!

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具

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

HEX CMYK 互转工具