封面出自:板栗懒得很
本章仅对部分代码进行讲解,以帮助读者更好的理解章节内容。 本系列文章涉及的项目HardwareVideoCodec已经开源到Github,支持软编和硬编。使用它你可以很容易的实现任何分辨率的视频编码,无需关心摄像头预览大小。一切都如此简单。目前已迭代多个稳定版本,欢迎查阅学习和使用,如有BUG或建议,欢迎Issue。
x264是目前使用最广泛、效率最高的h264编码库,著名的音视频处理库ffmpeg也支持x264的扩展。如果你的项目用于商业用途,建议选用免费的openh264。 相比x264,可能著名的ffmpeg更广为人知。但是我们为什么不使用ffmpeg呢。正如本系列文章的序章所说,如果你只是打算用于h264编码,完全没必要使用庞大复杂ffmpeg,反而选择短小精悍的x264更适合你。不仅可以使用更小的so库(这在移动平台很有必要),而且也不需要再去啃ffmpeg枯燥复杂的代码。我是前前后后看了五遍才勉强看懂,一直处于看了又忘,忘了又看的状态,似会非会的叠加状态。相比之下x264的流程更为短小清晰,使用更为简单。
一、使用x264
在上一章我们详细的讲解了如何编译x264,如果你尚未接触过x264,建议回头翻阅学习。
1. 申请内存空间
x264是一个c库,所以你需要搭建好ndk环境。要使用x264,我们首先需要为其编码器申请内存空间,这里先定义一个编码器相关的结构体。
代码语言:javascript复制typedef struct {
x264_param_t *param;
x264_t *handle;
x264_picture_t *picture;
x264_nal_t *nal;
} Encoder;
static Encoder *encoder = NULL;
然后为其申请内存空间。
代码语言:javascript复制X264Encoder::X264Encoder() {
LOGE("X264Encoder");
encoder = (Encoder *) malloc(sizeof(Encoder));
encoder->param = (x264_param_t *) malloc(sizeof(x264_param_t));
encoder->picture = (x264_picture_t *) malloc(sizeof(x264_picture_t));
}
2. 配置编码器
内存申请完毕之后,还需要对编码器参数进行配置,包括分辨率、bitrate、帧格式、fps、profile和level。由于我这里主要用于直播,所以使用zerolatency的配置来把延迟降到最低。需要特别注意的是,设置encoder->param->b_sliced_threads = 0和encoder->param->i_threads = X264_THREADS_AUTO能大幅度提高编码效率,不知道为什么,部分资料说是开启了多帧并行编码。 另外x264还有非常非常多的可配置参数,但如果要开始使用,简单配置上面的几个参数就可以了。更多的可配置参数在文章末尾提供的源码中有注释,但不一定准确,因为我目前也没完全弄懂这些参数的作用,以及该怎么配合使用,泪目。如果有人知道的话,请你一定要告诉我,感谢。
代码语言:javascript复制static void config() {
x264_param_default_preset(encoder->param, "veryfast", "zerolatency");
//开启多帧并行编码
encoder->param->b_sliced_threads = 0;
encoder->param->i_threads = X264_THREADS_AUTO;
/**
* 是否复制sps和pps放在每个关键帧的前面
*/
encoder->param->b_repeat_headers = 0;
/**
* 恒定质量
* ABR(平均码率)/CQP(恒定质量)/CRF(恒定码率)
* ABR模式下调整i_bitrate
* CQP下调整i_qp_constant调整QP值,太细致了人眼也分辨不出来,为了增加编码速度降低数据量还是设大些好
* CRF下调整f_rf_constant和f_rf_constant_max影响编码速度和图像质量(数据量),码率和图像效果参数失效
*/
encoder->param->rc.i_rc_method = X264_RC_ABR;
/**
* 范围0~51,值越大图像越模糊,默认23
*/
//encoder->param->rc.i_qp_constant = 51;
/**
* inter,取值范围1~32
* 值越大数据量相应越少,占用带宽越低
*/
encoder->param->analyse.i_luma_deadzone[0] = 32;
/**
* intra,取值范围1~32
* 值越大数据量相应越少,占用带宽越低
*/
encoder->param->analyse.i_luma_deadzone[1] = 32;
/**
* 快速P帧跳过检测
*/
encoder->param->analyse.b_fast_pskip = 1;
/**
* 是否允许非确定性时线程优化
*/
encoder->param->b_deterministic = 0;
/**
* 强制采用典型行为,而不是采用独立于cpu的优化算法
*/
encoder->param->b_cpu_independent = 0;
}
void X264Encoder::setVideoSize(int width, int height) {
encoder->param->i_width = width; //set frame width
encoder->param->i_height = height; //set frame height
}
void X264Encoder::setBitrate(int bitrate) {
encoder->param->rc.i_bitrate = bitrate / 1000;
}
void X264Encoder::setFrameFormat(int format) {
encoder->param->i_csp = format; // 设置输入的视频采样的格式
}
void X264Encoder::setFps(int fps) {
encoder->param->i_fps_num = (uint32_t) fps;
encoder->param->i_fps_den = 1;
}
void X264Encoder::setProfile(char *profile) {
x264_param_apply_profile(encoder->param, profile);
}
void X264Encoder::setLevel(int level) {
encoder->param->i_level_idc = level;// 11 12 13 20 for CIF;31 for 720P
}
3. 打开编码器
这里调用x264_encoder_open打开编码器,并为picture申请内存空间,并指定帧格式,用于储存待编码帧数据。
代码语言:javascript复制bool X264Encoder::start() {
if (INVALID != state) {
LOGI("Start failed. Invalid state, encoder is not invalid");
return false;
}
state = START;
if ((encoder->handle = x264_encoder_open(encoder->param)) == NULL) {
reset();
return false;
}
x264_picture_alloc(encoder->picture, encoder->param->i_csp, encoder->param->i_width,
encoder->param->i_height);
int y_size = encoder->param->i_width * encoder->param->i_height;
uint8_t *buff = (uint8_t *) malloc(y_size * 3 / 2);
encoder->picture->img.i_csp = X264_CSP_I420;
encoder->picture->img.i_plane = 3;
encoder->picture->img.plane[0] = buff;//Y
encoder->picture->img.plane[1] = buff y_size;//U
encoder->picture->img.plane[2] = buff y_size * 5 / 4;//V
encoder->picture->img.i_stride[0] = encoder->param->i_width;
encoder->picture->img.i_stride[1] = encoder->param->i_width / 2;
encoder->picture->img.i_stride[2] = encoder->param->i_width / 2;
return true;
}
4. 开始编码
使用x264_encoder_encode可以对数据进行编码,第一个参数是编码器句柄,第二个是编码后数据,第三个是输出数据的nal个数,第四个是输入的原始数据,第五个是编码后的帧信息。 由于我的原始帧数据格式是ARGB,而我们打开编码器的时候设置的输入格式是I420(x264目前只支持这个,虽然可以设置别的格式),所以我们需要把ARGB转成I420。 这里需要注意的是,不要使用除libyuv以外的任何方法进行格式转换,特别是网上一些自己写的java或c的转换算法,这些算法效率极低,基本不可用,千万不要浪费时间尝试这些(过来人),当然学习一下是可以的。libyuv之所以效率高,是因为其使用了arm的neon扩展指令进行加速,直接跟硬件交互,速度不是普通的java和c能比的。 libyuv是google开源的c库,需要自己编译,也可以使用别人编译好的,如果有必要,可以写一篇关于libyuv编译的教程。
代码语言:javascript复制bool X264Encoder::encode(char *src, char *dest, int *s, int *type) {
if (START != state) {
LOGI("Start failed. Invalid state, encoder is not start");
return 0;
}
s[0] = 0;
encoder->picture->i_type = X264_TYPE_AUTO;
int nNal = -1;
x264_picture_t pic_out;
int size = 0, i = 0;
struct timeval start, end;
gettimeofday(&start, NULL);
if (!fillSrc(src)) {
LOGE("Convert failed");
return false;
}
gettimeofday(&end, NULL);
int time = end.tv_usec - start.tv_usec;
gettimeofday(&start, NULL);
if (x264_encoder_encode(encoder->handle, &(encoder->nal), &nNal, encoder->picture, &pic_out) <
0) {
return false;
}
for (i = 0; i < nNal; i ) {
memcpy(dest, encoder->nal[i].p_payload, encoder->nal[i].i_payload);
dest = encoder->nal[i].i_payload;
size = encoder->nal[i].i_payload;
}
s[0] = size;
type[0] = pic_out.i_type;
gettimeofday(&end, NULL);
LOGI("Encode type: %d, Yuv convert time: %d, Encode time: %ld", pic_out.i_type, time,
(end.tv_usec - start.tv_usec));
return true;
}
/**
* 使用libyuv把rgb转为i420,并填充到encoder->picture
* @param argb
* @return
*/
bool X264Encoder::fillSrc(char *argb) {
int width = encoder->param->i_width;
int height = encoder->param->i_height;
int ret = libyuv::ConvertToI420((const uint8 *) argb, width * height,
encoder->picture->img.plane[0], width,
encoder->picture->img.plane[1], width / 2,
encoder->picture->img.plane[2], width / 2,
0, 0,
width, height,
width, height,
libyuv::kRotate0, libyuv::FOURCC_ABGR);
return ret >= 0;
}
到这里我们就可以编码出h264数据了。
二、使用MediaMuxer混合音视频
当我们通过x264编码出h264数据后,我们就可以把视频数据跟音频数据进行混合写入到文件了。但是x264只提供了编码器,不像ffmpeg那样提供一条龙服务。那我编码出数据没法封装成文件有个luan用啊!难道我们还需要使用ffmpeg对编码数据进行封装吗?这样子的话还不如也使用ffmpeg进行编码得了。 回想之前我们使用MediaCodec进行硬编的时候,可以使用MediaMuxer进行文件封装,那么这里我们能不能也使用这个对x264编码后的数据进行封装呢,答案是可以的! 第六章讲MediaMuxer用法的时候我们说到,要使用MediaMuxer就必须先addTrack(MediaFormat)来添加音视频轨道,而这个方法需要一个特殊的MediaFormat,这个参数特殊在哪呢。
这个特殊之处在于codec-specific data。查看官方文档可以发现,MediaMuxer对h264进行封装的时候需要sps和pps,这两块数据分别对应MediaMuxer中的csd-1和csd-2,这些数据可以通过MediaFormat.setByteBuffer(String name, ByteBuffer bytes)来设置,划重点!比如
代码语言:javascript复制mediaFormat.setByteBuffer("csd-0", sps);
mediaFormat.setByteBuffer("csd-1", pps);
h264没有使用到csd-2,所以不需要设置。至此,我们可以像打开MediaCodec时构造MediaFormat那样设置对应的参数,然后在此基础上再给MediaFormat设置上对应的csd就可以使用MediaMuxer对x264编码出来的数据进行封装了。 还有一个关键就是,sps和pps从哪里来呢。其实sps和pps是h264的标准头数据,保存了视频的分辨率和帧格式等数据,用来告诉解码器如何解码帧数据。而这个头数据也是可以从x264获取到的。 在打开x264编码器之后,我们可以通过x264_encoder_headers来获取sps和pps。
代码语言:javascript复制/**
*
* @param dest sps和pps,这里把他们保存在同一块内存,也可以分开保存
* @param s sps和pps总长度
* @param type 用于标记这是sps和pps
* @return
*/
bool X264Encoder::encodeHeader(char *dest, int *s, int *type) {
int nal, size = 0;
x264_nal_t *nals;
x264_encoder_headers(encoder->handle, &nals, &nal);
for (int i = 0; i < nal; i ) {
if (nals[i].i_type == NAL_SPS) {
memcpy(dest, nals[i].p_payload, nals[i].i_payload);
dest = nals[i].i_payload;
size = nals[i].i_payload;
} else if (nals[i].i_type == NAL_PPS) {
memcpy(dest, nals[i].p_payload, nals[i].i_payload);
dest = nals[i].i_payload;
size = nals[i].i_payload;
}
}
s[0] = size;
type[0] = X264_TYPE_HEADER;
return true;
}
拿到sps和pps之后便可以构造出MediaMuxer所需要的特殊MediaFormat了,之后参考第六章正常使用MediaMuxer即可。如果没有sps和pps,最终出来的视频会绿屏或黑屏。
至此,「Android音视频编码那点破事」系列的坑终于填完了,断断续续花了四个多月,说到底还是太懒了。感谢大家的支持,如果这个系列对你有帮助,欢迎star本开源项目,也可以点赞、评论和收藏。
本章知识点:
- x264的使用。
- MediaMuxer的另类用法。
本章相关源码·HardwareVideoCodec项目:
- SoftVideoEncoderImpl
- CacheX264Encoder
- X264Encoder
- Java_com_lmy_codec_x264_X264Encoder.cpp
- X264Encoder.cpp
- MuxerImpl