软编码ffmpeg可以解决众多Android机型适配的问题,但由于软编码会大量消耗CPU资源,反而得不偿失!所以一般会考虑使用Android自身硬编码,不足时用软编码补充!硬编码中的一些特性有时会难以理解,这里记录一下我在编码时的心得总结!
一般根据使用场景实际需求来设定,帧率表示每秒输出多少帧画面!一般来说在30帧左右的情况下,人眼是看不出画面有拖影的现象,所以没有特殊要求可以设置30帧;但是帧率也和我们画面的宽高尺寸有关,如果宽高尺寸太大有可能导致编码器无法及时编码输出,所以我们可以使用Android提供的api测试我们的帧率是否符合
MediaCodecList list = new MediaCodecList(MediaCodecList.ALL_CODECS);
MediaCodecInfo[] infos = list.getCodecInfos();
for(MediaCodecInfo info : infos){
if(!info.isEncoder()){
continue;
}
Log.i("j_tag", "codec name " + info.getName());
if(info.getSupportedTypes()[0].equals("video/avc")){
MediaCodecInfo.CodecCapabilities cap = info.getCapabilitiesForType("video/avc");
Range<Integer> r = cap.getVideoCapabilities().getSupportedFrameRatesFor(w, h);
Log.i("j_tag", "fps range" + r.getLower() + " -- " + r.getUpper());
}
}
如上,会返回这个尺寸对应的帧率范围!
码率表示每秒输出的bit数据量大小,码率越高画面越真实,码率越低画面越模糊,尽可能在低码率的情况下输出高画质的图像!这是一个互相矛盾的问题;码率的设置和画面内容、帧率以及视频宽高有关,一般来说码率可以有个公司来算出:
B i t e r a t e = W i d t h ( p x ) ∗ H e i g h t ( p x ) ∗ F r a m e R a t e ∗ F a c t o r Biterate = Width(px) * Height (px)* FrameRate * Factor Biterate=Width(px)∗Height(px)∗FrameRate∗Factor
px = 像素单位
Factor是因子,通常情况下,在网络流媒体使用场景中,可以将Factor设置为0.1~0.2,这样能在保证画面损失不严重的情况下生成出来的视频文件大小较小;
在普通本地浏览的使用场景中,可以将Factor设置为0.25~0.5,这样可以保证画面清晰度不会因为编码而造成过多肉眼可见的损失,这样生成出来的视频文件也相对较大;
在高清视频处理的使用场景中,可以将Factor设置为0.5以上。
有时候按照以上算法计算出来有可能超出硬编码的能力范围了,同上也可以获得硬编码的码率支持范围,如下:
MediaCodecInfo.CodecCapabilities cap = info.getCapabilitiesForType("video/avc");
Range<Integer> r = cap.getVideoCapabilities().getBitrateRange();
Log.i("j_tag", "bitrate range" + r.getLower() + " -- " + r.getUpper());
设置码率后,编码过程不一定按照我们设置的码率输出,还和mode有关系,硬编码提供的mode有三种:
cap.getEncoderCapabilities().isBitrateModeSupported(MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR);
硬编码用法大家都知道,获取输入缓冲区 – 往里面添加数据 – 编码 – 获取输出缓冲区 – 获取编码后的数据;
获取输入缓冲区
ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers();
获取输入缓冲区空队列
int inputBufferIndex = mMediaCodec.dequeueInputBuffer(-1);
if (inputBufferIndex >= 0) {
ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
inputBuffer.clear();
inputBuffer.put(数据);
输入缓冲区入队
mMediaCodec.queueInputBuffer(inputBufferIndex, 0, input.length, pts, 0);
}
获取输出缓冲区
ByteBuffer[] outputBuffers = mMediaCodec.getOutputBuffers();
int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_USEC);
从输出缓冲区队列中拿到编码好的内容,对内容进行相应处理后在释放
while (outputBufferIndex >= 0) {
拿到编码好的数据
ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];
byte[] outData = newbyte[bufferInfo.size];
outputBuffer.get(outData);
flags 利用位操作,定义的 flag 都是 2 的倍数
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { 配置相关的内容,也就是 SPS,PPS
mOutputStream.write(outData, 0, outData.length);
} elseif ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) { 关键帧
mOutputStream.write(outData, 0, outData.length);
} else {
非关键帧和SPS、PPS,直接写入文件,可能是B帧或者P帧
mOutputStream.write(outData, 0, outData.length);
}
释放数据
mMediaCodec.releaseOutputBuffer(outputBufferIndex, false);
输出缓冲区入队
outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_USEC);
}
以上这种模式就是同步模式,每往输入缓冲区添加一次数据,就要等待这次数据解析完成,才能添加下一次的数据!
mMediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_USEC)的第二个参数是此次取数据的超时时间,可以实际测试确定取值!越小响应速度越快!
而异步模式,则无需添加后等待,只管往里面添加,编码后数据会回调通知,如下:
private MediaCodec.Callback mCallback = new MediaCodec.Callback() {
@Override
public void onInputBufferAvailable(@NonNull MediaCodec mediaCodec, int id) {
添加数据
}
@Override
public void onOutputBufferAvailable(@NonNull MediaCodec mediaCodec, int id, @NonNull MediaCodec.BufferInfo bufferInfo) {
取出数据
}
@Override
public void onError(@NonNull MediaCodec mediaCodec, @NonNull MediaCodec.CodecException e) { }
@Override
public void onOutputFormatChanged(@NonNull MediaCodec mediaCodec, @NonNull MediaFormat mediaFormat) { }
};
mMediaCodec.setCallback(mCallback, mVideoEncoderHandler);
mMediaCodec.configure(mMediaFormat, null, null, CONFIGURE_FLAG_ENCODE);
mMediaCodec.start();
java层的video encoder效率没有NDK效率高,如果需要NDK的编码,使用方法和java步骤差不多,创建codec,配置编码能力format,设置异步回调或者同步编码等;
mediacodec = AMediaCodec_createEncoderByType(mine);
videoFormat = AMediaFormat_new();
AMediaFormat_setString(videoFormat, "mime", mine);
AMediaFormat_setInt32(videoFormat, AMEDIAFORMAT_KEY_WIDTH, encoder->m_n_width); // 视频宽度
AMediaFormat_setInt32(videoFormat, AMEDIAFORMAT_KEY_HEIGHT, encoder->m_n_height); // 视频高度
AMediaFormat_setInt32(videoFormat, AMEDIAFORMAT_KEY_COLOR_FORMAT, encoder->m_e_color_format);
AMediaFormat_setInt32(videoFormat, AMEDIAFORMAT_KEY_FRAME_RATE, 25);
AMediaFormat_setInt32(videoFormat, AMEDIAFORMAT_KEY_I_FRAME_INTERVAL, 1);//关键帧间隔时间 单位s
AMediaFormat_setInt32(videoFormat, AMEDIAFORMAT_KEY_BIT_RATE, videoBps);
media_status_t rc = AMediaCodec_configure(mediacodec, videoFormat, NULL, NULL, MediaCodec.CONFIGURE_FLAG_ENCODE);
if (AMEDIA_OK == rc) {
AMediaCodec_start(mediacodec);
}
同步和异步上面的配置是一样的,同步需要自己去取缓冲区队列,读写数据等,其API如下:
输入缓存区操作
int inputBufferIndex = AMediaCodec_dequeueInputBuffer(mediacodec, 5*1000);
uint8_t *input_buf = AMediaCodec_getInputBuffer(mediacodec, inputBufferIndex, &input_buf_size);
往input_buf写数据
入队
AMediaCodec_queueInputBuffer(mediacodec, inputBufferIndex, 0, yuv_data_len, pts, 0);
===============================================================
输出缓冲区操作
int outputBufferIndex = AMediaCodec_dequeueOutputBuffer(mediacodec, &info, 5 * 1000);
uint8_t *output_buf = AMediaCodec_getOutputBuffer(mediacodec, outputBufferIndex, &output_buf_size);
取出output_buf数据
释放数据
AMediaCodec_releaseOutputBuffer(mediacodec, outputBufferIndex, false);
归还队列
AMediaCodec_dequeueOutputBuffer(mediacodec, &info, 5 * 1000);
异步和java层方式差不多,需要创建回调和设置回调,API如下:
创建回调:
struct AMediaCodecOnAsyncNotifyCallback {
AMediaCodecOnAsyncInputAvailable onAsyncInputAvailable;
AMediaCodecOnAsyncOutputAvailable onAsyncOutputAvailable;
AMediaCodecOnAsyncFormatChanged onAsyncFormatChanged;
AMediaCodecOnAsyncError onAsyncError;
};
设置回调:
media_status_t AMediaCodec_setAsyncNotifyCallback(
AMediaCodec*,
AMediaCodecOnAsyncNotifyCallback callback,
void *userdata) __INTRODUCED_IN(28);
注意:这些新功能对版本有要求的,使用时注意Android系统版本情况
附录:Android支持的编码器截图: