美文网首页Android技术知识Android开发Android开发经验谈
X264实现H264编码以及MediaMuxer的另类用法「第八

X264实现H264编码以及MediaMuxer的另类用法「第八

作者: Alimin利民 | 来源:发表于2018-09-09 17:16 被阅读127次

      本章仅对部分代码进行讲解,以帮助读者更好的理解章节内容。
    本系列文章涉及的项目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项目

    相关文章

      网友评论

      本文标题:X264实现H264编码以及MediaMuxer的另类用法「第八

      本文链接:https://www.haomeiwen.com/subject/znyvlftx.html