音视频开发路线:
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 JNICALL
Java_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,并返回出去
//初始化音频编码器faac
extern "C"
JNIEXPORT jint JNICALL
Java_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<char *>(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<int32_t *>(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,这样我们就完成了软编推流的功能了。