X264实现H264编码以及MediaMuxer的另类用法「第八章,Android音视频编码那点破事」

1,585 阅读9分钟

  本章仅对部分代码进行讲解,以帮助读者更好的理解章节内容。 本系列文章涉及的项目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,我们首先需要为其编码器申请内存空间,这里先定义一个编码器相关的结构体。

typedef struct {
    x264_param_t *param;
    x264_t *handle;
    x264_picture_t *picture;
    x264_nal_t *nal;
} Encoder;
static Encoder *encoder = NULL;

  然后为其申请内存空间。

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帧格式fpsprofilelevel。由于我这里主要用于直播,所以使用zerolatency的配置来把延迟降到最低。需要特别注意的是,设置encoder->param->b_sliced_threads = 0encoder->param->i_threads = X264_THREADS_AUTO能大幅度提高编码效率,不知道为什么,部分资料说是开启了多帧并行编码。   另外x264还有非常非常多的可配置参数,但如果要开始使用,简单配置上面的几个参数就可以了。更多的可配置参数在文章末尾提供的源码中有注释,但不一定准确,因为我目前也没完全弄懂这些参数的作用,以及该怎么配合使用,泪目。如果有人知道的话,请你一定要告诉我,感谢。

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申请内存空间,并指定帧格式,用于储存待编码帧数据。

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编译的教程。

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
  这个特殊之处在于codec-specific data。查看官方文档可以发现,MediaMuxer对h264进行封装的时候需要spspps,这两块数据分别对应MediaMuxer中的csd-1csd-2,这些数据可以通过MediaFormat.setByteBuffer(String name, ByteBuffer bytes)来设置,划重点!比如

mediaFormat.setByteBuffer("csd-0", sps);
mediaFormat.setByteBuffer("csd-1", pps);

  h264没有使用到csd-2,所以不需要设置。至此,我们可以像打开MediaCodec时构造MediaFormat那样设置对应的参数,然后在此基础上再给MediaFormat设置上对应的csd就可以使用MediaMuxer对x264编码出来的数据进行封装了。   还有一个关键就是,spspps从哪里来呢。其实spspps是h264的标准头数据,保存了视频的分辨率和帧格式等数据,用来告诉解码器如何解码帧数据。而这个头数据也是可以从x264获取到的。   在打开x264编码器之后,我们可以通过x264_encoder_headers来获取spspps

/**
 * 
 * @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;
}

  拿到spspps之后便可以构造出MediaMuxer所需要的特殊MediaFormat了,之后参考第六章正常使用MediaMuxer即可。如果没有spspps,最终出来的视频会绿屏或黑屏。

  至此,「Android音视频编码那点破事」系列的坑终于填完了,断断续续花了四个多月,说到底还是太懒了。感谢大家的支持,如果这个系列对你有帮助,欢迎star开源项目,也可以点赞、评论和收藏。

本章知识点:

  1. x264的使用。
  2. MediaMuxer的另类用法。

本章相关源码·HardwareVideoCodec项目


欢迎关注微信公众,第一时间获取一手多媒体技术资讯