|
网站内容均来自网络,本站只提供信息平台,如有侵权请联系删除,谢谢!
音视频开发路线:
Android 音视频开发入门指南_Jhuster的专栏的技术博客_51CTO博客_android 音视频开发入门
demo地址:
videoPath/Demo8Activity.java at master · wygsqsj/videoPath · GitHub
前期的代码我们都是通过MediaCodec来实现音视频数据的编码,使用MedieaCodec其实底层使用的还是DSP芯片进行编码,这种方式耗电量低,效率高,但是最大的问题是版本兼容问题,5.0以下基本不支持,dsp芯片是后期厂商才加入的cpu里面去的,cpu的厂商对不同的dsp芯片的实现也不同,所以各种各样的问题下我们必须掌握软编,软编其实就是通过CPU来对我们的音视频数据进行编码,cpu相对dsp芯片来说,他不是专门为做音视频编码设计的,所以他的效率和耗电量都比dsp硬编要逊色一点,但是也是必不可少的一部分。
将视频数据转换H264的软编框架主要是X264,ffmpeg底层也是使用的X264框架,此处单独拿来使用;音频的编码框架是使用的faac,也是市面上常用的一款音频编码框架,这两个框架都是C编写的,由于太过于庞大,我们要先通过交叉编译的方式来编译成SO库,供我们使用。
项目结构
x264和faac框架可以通过Linux编译成对应的so库,我在代码里编译成了arm64-v8a和armeabi-v7a两种so库,如果自己想编译实现,可参考编译x264视频编码库_程序课代表的博客-CSDN博客
具体的项目结构如下:
x264是c编写,我们使用时要通过JNI来调用,LivePush类是连接java层和Native层的通道,我们在java层通过VideHelper获取摄像头输入数据,通过AudioHelper来获取音频数据,再将采集到的音视频数据通过传输层LivePush发送给底层调用;x264_codec是Native层的入口,rtmp的初始化和发送数据功能在此完成,VideoChannel是x264库的编码,AudioChannel是faac用于音频编码,将这些编码数据放到一个队列中,rtmp从队列中不断取出数据后推送到rtmp服务器
X264使用
VideoHelper通过Camera2获取数据,当获取到宽高数据后,先发送给X264进行设置;有个注意的点是时间戳的存储不是直接以时间单位存储的,而是以帧率为单位,例如当前是1s20帧,那当前单位就是1/20,获取时间戳通过当前帧数*1/20得到时间,这时候我们传输时指需要记录当前时多少帧即可。
- //初始化x264框架void VideoChannel::createX264Encode(int width, int height, int fps, int bitrate) { // 加锁, 设置视频编码参数 与 编码互斥 pthread_mutex_lock(&mMutex); mWidth = width; mHeight = height; mFps = fps; mBitrate = bitrate; mYSize = width * height; mUVSize = mYSize / 4; //初始化 if (mVideoCodec) { x264_encoder_close(mVideoCodec); mVideoCodec = nullptr; } //类比与MedeaFormat x264_param_t param; x264_param_default_preset(¶m, "ultrafast",//编码器速度,越快质量越低,适合直播 "zerolatency"//编码质量 ); //编码等级 param.i_level_idc = 32; param.i_csp = X264_CSP_I420; //nv12 param.i_width = width; param.i_height = height; //设置没有B帧 param.i_bframe = 0; /* * 码率控制方式 * X264_RC_CBR:恒定码率 cpu紧张时画面质量差,以网络传输稳定为先 * X264_RC_VBR:动态码率,cpu紧张时花费更多时间,画面质量比较均衡,适合本地播放 * X264_RC_ABR:平均码率,是一种折中方式,也是网络传输中最常用的方式 * */ param.rc.i_rc_method = X264_RC_ABR; //码率,k为单位,所以字节数除以1024 param.rc.i_bitrate = bitrate / 1024; /* * 帧率 * 代表1秒有多少帧 * 帧率时间 * 当前帧率为25,那么帧率时间我们一般理解成1/25=40ms * 但是帧率的单位不是时间,而是一个我们设定的值 i_fps_den/i_timebase_den * 例如当前是1000帧了,他对应的时间戳计算方式为:1000(1/25) * * 如果你的i_fps_den/i_timebase_den 设置的不是 1/fps,那么最终是以这两个参数为单位计算间隔的,一般我们都会 * 设置成1/fps */ 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; //使用fps计算帧间距 param.b_vfr_input = 0; //25帧一个I帧 param.i_keyint_max = fps * 2; //sps和pps自动放到I帧前面 param.b_repeat_headers = 1; //开启多线程 param.i_threads = 1; //编码质量 x264_param_apply_profile(¶m, "baseline"); //打开编码器 mVideoCodec = x264_encoder_open(¶m); //输入缓冲区 pic_in = new x264_picture_t; //初始化缓冲区大小 x264_picture_alloc(pic_in, X264_CSP_I420, width, height); // 解锁, 设置视频编码参数 与 编码互斥 pthread_mutex_unlock(&mMutex);}
复制代码 java层每获取到摄像头捕获的数据都发送给x264进行编码,先将java层byte数组转换成jni可使用的数据,记得释放:
- extern "C"JNIEXPORT void JNICALLJava_com_wish_videopath_demo8_x264_LivePush_native_1pushVideo(JNIEnv *env, jobject thiz, jbyteArray data_) { //没有实例化编码或者rtmp没连接成功时退出 if (!videoChannel || !readyPushing) { return; } //转换成数组进行编码 jbyte *data = env->GetByteArrayElements(data_, NULL); videoChannel->encodeData(data); env->ReleaseByteArrayElements(data_, data, 0);}
复制代码 再将yuv数据放到x264的通道中进行编码,x264与硬编不同,他可以将好几帧的数据放到通道中,例如I P P三帧的Y数据一股脑都扔进Y通道,也就是说x264可以一次性输入好几帧一次性输出好几帧,编码后的数据我们可以得到他的类型,当编码出sps和pps,与MediaCodec不同的是,X264会将sps、pps和I帧一块编码出来,而硬编会先将sps和pps输出出来,x264第一次编码会一次性输出四项:sps/pps/补充信息/I帧,我们根据编码类型,解析出sps和pps添加到发送队列,再把普通数据添加到队列中。
- /** * 将java层传递的yuv(NV21)数据编码成h264码流 * @param data 输入的yuv数据 * 将 y u v分别放到单个通道中,X264可以将多个帧的通道同时存入,例如I P P三帧的Y数据放如x264的y通道 * 所以x264框架可以一次性输出好几个NAL单元 * */void VideoChannel::encodeData(int8_t *data) { // 加锁, 设置视频编码参数 与 编码互斥 pthread_mutex_lock(&mMutex); //将Y放入x264的y通道 memcpy(pic_in->img.plane[0], data, mYSize); // 取出u v数据放入通道 for (int i = 0; i < mUVSize; i++) { //img.plane[1]里面放的是u数据;我们的yuv格式是NV21,data[1]是U数据 *(pic_in->img.plane[1] + i) = *(data + mYSize + i * 2 + 1); //img.plane[2]里面放的是V数据;data[0]是V数据 *(pic_in->img.plane[2] + i) = *(data + mYSize + i * 2); } //编码后的NAL个数,可以理解成编码出了几帧 int pi_nal; //编码后的数据存储区,这里面放了pi_nal个帧的数据 x264_nal_t *pp_nal; //输出的编码数据参数,类似于MedeaCodec编码EncodeInfo x264_picture_t pic_out; //开始编码 x264_encoder_encode(mVideoCodec, &pp_nal, &pi_nal, pic_in, &pic_out); //缓存sps和pps uint8_t sps[100]; uint8_t pps[100]; int spsLen; int ppsLen; //编码后的数据 if (pi_nal > 0) { for (int i = 0; i < pi_nal; i++) { LOGI("当前帧数:%d,当前帧大小:%d", i, pp_nal[i].i_payload); //rtmp 是将sps和pps一起打包发送出去的 if (pp_nal[i].i_type == NAL_SPS) { //减去00000001分隔符的长度 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) { //减去00000001分隔符的长度 ppsLen = pp_nal[i].i_payload - 4; memcpy(pps, pp_nal[i].p_payload + 4, ppsLen); //发送到rtmp服务器 sendSPSPPS(sps, pps, spsLen, ppsLen); } else { sendVideo(pp_nal[i].i_type, pp_nal[i].i_payload, pp_nal[i].p_payload); } } } // 解锁, 设置视频编码参数 与 编码互斥 pthread_mutex_unlock(&mMutex);}
复制代码 构建sps、pps、帧数据包在以前博客中也写过,可以去github了解一下
faac使用
同样faac通过交叉编译的方式得到so库,具体也是在java中通过AudioRecord来获取音频,然后发送到底层进行编码,注意点就是AudioRecord的缓冲区大小,要根据faac实例化得到他的输入大小
- int inputByteNum = livePush.native_initAudioCodec(SAMPLE_RATE_HZ, channelCount); /* * 缓冲区,此处的最小buffer只能作为参考值,不同于MedeaCodec我们可以直接使用此缓冲区大小,当设备不支持硬编时 * getMinBufferSize会返回-1,所以还要根据faac返回给我们的输入区大小来确定 * faac会返回给我们一个缓冲区大小,将他和缓冲区大小比较之后采用最大值 */ int minBufferSize = Math.max(inputByteNum, AudioRecord.getMinBufferSize(SAMPLE_RATE_HZ, CHANNEL_CONFIG, AUDIO_FORMAT)); //初始化录音数据缓冲区,要根据faac返回的采样数据大小构建,否则传输给faac编码的音频数据大小不一致时编码出来的数据会出现杂音 buffer = new byte[minBufferSize]; try { //初始化录音器,使用的是对比得到的数据大小 audioRecord = new AudioRecord( MediaRecorder.AudioSource.MIC, SAMPLE_RATE_HZ, CHANNEL_CONFIG, AUDIO_FORMAT, minBufferSize); } catch (Exception e) { e.printStackTrace(); }
复制代码 实例化faac,得到输入容器大小inputByteNum,并返回出去
- //初始化音频编码器faacextern "C"JNIEXPORT jint JNICALLJava_com_wish_videopath_demo8_x264_LivePush_native_1initAudioCodec(JNIEnv *env, jobject thiz, jint sample_rate, jint channel_count) { audioChannel = new AudioChannel; audioChannel->setCallBack(callBack); audioChannel->initCodec(sample_rate, channel_count); return audioChannel->getInputByteNum();}
复制代码- //实例化音频编码器void AudioChannel::initCodec(int sampleRate, int channels) { chanelCount = channels; //输入的容量大小,要与java层AudioRecord获取的缓冲区大小比较得到缓冲区大小 unsigned long inputSamples; //获取编码器,参数解释:采样率、通道数、输入的音频样本数量(返回给我们)、输出的字节容量(返回给我们) codec = faacEncOpen(sampleRate, channels, &inputSamples, &maxOutputBytes); //输入的容器的大小,我们的采样位数是16,也就是2个字节,用样本数*2得到输入大小 inputByteNum = inputSamples * 2; //配置参数 faacEncConfigurationPtr configurationPtr = faacEncGetCurrentConfiguration(codec); // 设置编码格式标准, 使用 MPEG4 新标准 configurationPtr->mpegVersion = MPEG4; configurationPtr->aacObjectType = LOW; //采样位数 configurationPtr->inputFormat = FAAC_INPUT_16BIT; //0 输出aac原始数据 1 添加ADTS头之后的数据 configurationPtr->outputFormat = 0; //使配置生效 faacEncSetConfiguration(codec, configurationPtr); LOGI("编码后的音频缓冲区大小:%d", maxOutputBytes); //输出的容器 outputBuffer = new unsigned char[maxOutputBytes];}
复制代码 我们音频在rtmp传输时要先发送一个音频头,这个操作我们把他放在初始化rtmp连接时,如果连接成功,我们就先往传输队列里面扔一个音频头,这样rtmp会先把我们的音频头发送出去
- void *start(void *args) { char *url = static_cast(args); //不断重试,链接服务器 do { //初始化RTMP,申请内存 rtmp = RTMP_Alloc(); if (!rtmp) { LOGI("RTMP 创建失败"); break; } RTMP_Init(rtmp); //设置超时时间 rtmp->Link.timeout = 10; //设置地址 int ret = RTMP_SetupURL(rtmp, (char *) url); if (!ret) { LOGI("RTMP 创建失败"); break; } LOGI("connect %s", url); //设置输出模式 RTMP_EnableWrite(rtmp); LOGI("connect Connect"); //连接 if (!(ret = RTMP_Connect(rtmp, 0))) break; LOGI("connect ConnectStream"); //连接流 if (!(ret = RTMP_ConnectStream(rtmp, 0))) break; LOGI("connect 成功"); start_time = RTMP_GetTime(); packets.setWork(1); RTMPPacket *packet = 0; //添加音频头到队列中 if (audioChannel) { callBack(audioChannel->getAudioHead()); LOGI("添加音频头到队列中"); } //从队列中取出数据发送 readyPushing = 1; while (isStart) { packets.pop(packet); if (!isStart) { break; } if (!packet) { continue; } packet->m_nInfoField2 = rtmp->m_stream_id; //发送 ret = RTMP_SendPacket(rtmp, packet, 1); releasePackets(packet); if (!ret) { LOGI("发送数据失败!"); break; } } releasePackets(packet); } while (false); delete url; return nullptr;}
复制代码 音频头的封装在AudioChannal中:
- /发送音频头RTMPPacket *AudioChannel::getAudioHead() { if (!codec) { return nullptr; } unsigned char *buf; unsigned long len; //音频头 faacEncGetDecoderSpecificInfo(codec, &buf, &len); RTMPPacket *packet = new RTMPPacket; RTMPPacket_Alloc(packet, len + 2); packet->m_body[0] = 0xAF; if (chanelCount == 1) { // 如果是单声道, 将该值修改成 AE packet->m_body[0] = 0xAE; } packet->m_body[1] = 0x00; memcpy(&packet->m_body[2], buf, len); //设置音频类型 packet->m_packetType = RTMP_PACKET_TYPE_AUDIO; packet->m_nBodySize = len + 2; //通道值,音视频不能相同 packet->m_nChannel = 0x05; packet->m_nTimeStamp = 0; packet->m_hasAbsTimestamp = 0; packet->m_headerType = RTMP_PACKET_SIZE_LARGE; return packet;}
复制代码 然后就是faac框架对音频的编码逻辑:
- //编码音频void AudioChannel::encode(int8_t *data) { //进行编码 参数:编码器 编码数据,编码数据大小 ,输入容器,输入容器大小 int encodeLen = faacEncEncode(codec, reinterpret_cast(data), inputByteNum / 2,//样本数量 outputBuffer, maxOutputBytes); LOGI("编码后的音频数据大小:%d", encodeLen); if (encodeLen > 0) { RTMPPacket *packet = new RTMPPacket; int body_size = encodeLen + 2; RTMPPacket_Alloc(packet, body_size); packet->m_body[0] = 0xAF; if (chanelCount == 1) { // 如果是单声道, 将该值修改成 AE packet->m_body[0] = 0xAE; } packet->m_body[1] = 0x01; memcpy(&packet->m_body[2], outputBuffer, encodeLen); //设置音频类型 packet->m_packetType = RTMP_PACKET_TYPE_AUDIO; packet->m_nBodySize = body_size; //通道值,音视频不能相同 packet->m_nChannel = 0x05; packet->m_nTimeStamp = 0; packet->m_hasAbsTimestamp = 0; packet->m_headerType = RTMP_PACKET_SIZE_LARGE; if (callbackAudio) { callbackAudio(packet); } }}
复制代码 这样x264和faac的基本使用就完成了,其实逻辑很简单,对应的初始化代码和编码api也是固定的,ok,这样我们就完成了软编推流的功能了。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
x
免责声明
1. 本论坛所提供的信息均来自网络,本网站只提供平台服务,所有账号发表的言论与本网站无关。
2. 其他单位或个人在使用、转载或引用本文时,必须事先获得该帖子作者和本人的同意。
3. 本帖部分内容转载自其他媒体,但并不代表本人赞同其观点和对其真实性负责。
4. 如有侵权,请立即联系,本网站将及时删除相关内容。
|