内容简介: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,......
而I420则是
可以看出无论是哪种排列方式,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
接口能够设置预览显示的角度:
根据文档,配置完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(¶m, "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(¶m, "baseline"); //打开编码器 videoCodec videoCodec = x264_encoder_open(¶m); 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函数中的整个流程:
数据传输
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); 复制代码
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
区块链技术驱动金融
阿尔文德·纳拉亚南、约什·贝努、爱德华·费尔顿、安德鲁·米勒、史蒂文·戈德费德 / 林华、王勇 / 中信出版社,中信出版集团 / 2016-8-25 / CNY 79.00
从数字货币及智能合约技术层面,解读了区块链技术在金融领域的运用。“如果你正在寻找一本在技术层面解释比特币是如何运作的,并且你有一定计算机科学和编程的基本知识,这本书应该很适合你。” 《区块链:技术驱动金融》回答了一系列关于比特币如何运用区块链技术运作的问题,并且着重讲述了各种技术功能,以及未来会形成的网络。比特币是如何运作的?它因何而与众不同?你的比特币安全吗?比特币用户如何匿名?区块链如何......一起来看看 《区块链技术驱动金融》 这本书的介绍吧!