NDK学习笔记:RtmpPusher之认识x264编码库

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/a360940265a/article/details/87784225

NDK学习笔记:RtmpPusher之认识x264编码库

在家过了个懒散年,为自己定了些小目标(譬如一个亿什么的,人要有梦想是吧)哈哈。祝大家诸事顺景,代码没bug,服务器不宕机。距离上一篇文章一个多月了,之前的文章主要是学习ffmpeg关于视音频解码同步的部分内容(native_decode),接下来的几篇文章,我们来学习关于x264/faac的视音频编码内容(native_encode),通过rtmpdump推流到流媒体服务器,系统学习直播服务的理论基础知识。

然后还想说的是,之前有同学问,水印录制是否支持音频?所以CameraRecordEncoder核心编码模块CameraRecordEncoderCore已升级CameraRecordEncoderCore2,支持了音频录制。

认识编码库 x264

本篇主要内容是学习  获取Android原生摄像头nv21码流,通过ndk层的x264编码库,输出h264的数据流。本章内容在BlogApp -> NativeAVEncodeActivity,详情请follow https://github.com/MrZhaozhirong/BlogApp

好了废话不说,直接正题。

第一步,我们需要打开原生摄像头并获取其预览帧数据,在此推荐一下自己封装整理好的模块,能做到完美支持的视图/预览尺寸的最佳吻合,同时支持SurfaceView/TextureView,支持nv21的数据回调,二次扩展自由度高,使用也非常方便...不吹了,再吹就爆了。。。代码如下:

    private SurfaceView cameraView;


        cameraView = findViewById(R.id.camera_view);
        cameraView.getHolder().setFormat(PixelFormat.RGBA_8888);

        CameraListener cameraListener = new CameraListener() {
            @Override
            public void onCameraOpened(Camera camera, int cameraId, int displayOrientation, boolean isMirror) {

            }

            @Override
            public void onPreview(byte[] data, Camera camera) {
                if(rtmpPusher!=null){
                    rtmpPusher.feedVideoData(data);
                }
            }

            @Override
            public void onCameraClosed() {

            }

            @Override
            public void onCameraError(Exception e) {

            }

            @Override
            public void onCameraConfigurationChanged(int cameraID, int displayOrientation) {

            }
        };

        cameraHelper = new CameraHelper.Builder()
                .previewViewSize(new Point(cameraView.getMeasuredWidth(),cameraView.getMeasuredHeight()))
                .rotation(getWindowManager().getDefaultDisplay().getRotation())
                .specificCameraId(cameraID != null ? cameraID : Camera.CameraInfo.CAMERA_FACING_FRONT)
                .isMirror(true)
                .previewOn(cameraView)
                .cameraListener(cameraListener)
                .build();
        cameraHelper.init();

        
        if(rtmpPusher == null) {
            rtmpPusher = new RtmpPusher();
        }
        Point previewSize = cameraHelper.getPreviewSize();
        rtmpPusher.prepareVideoEncoder(previewSize.x, previewSize.y, 150*1000, 25);

然后第二步,通过RtmpPusher初始化视频编码器,通过接口喂养nv21格式的yuv预览数据进行编码,方法接口如下:

public class RtmpPusher {
    static {
        System.loadLibrary("rtmp-push");
    }
    /**
     * 设置视频参数
     * @param width
     * @param height
     * @param bitrate
     * @param fps
     */
    public native void prepareVideoEncoder(int width, int height, int bitrate, int fps);
    /**
     * 发送视频数据
     * @param data nv21数据
     */
    public native void feedVideoData(byte[] data);

    ... ...
}

之后就是开始x264的使用学习了,在开始前,我想说一点编译x264的小插曲。很多时候我们都是从网上下载的c源码库进行编译,以x264为例(https://www.videolan.org/developers/x264.html)下载源码一般有两种方式,一种方式就是网络文件下载tar.bz压缩包。另外一种方式就是从官网上提供的git clone通过gitbash下载;在此建议下载压缩包,然后通过xftp传送到ubuntu环境上。如果要使用git clone方式就直接在ubuntu环境上直接git,因为在windows上gitclone下来,源码文件全都windows的文件格式了,不能直接在ubuntu使用,每个文件都需要dos2unix,那是一件让人疯狂的事情。(最好的办法就是在imac上做完全套工作)

顺带说下,编译x264库需要依赖yasm,且有最低版本要求 1.2.0。系统尽量保持最新,直接apt-get install yasm就可以了。NDK交叉编译的脚本也放上来吧,我用的是静态库(lib.a) 注意按照自己的情况enable/disable-shared/static

#!/bin/bash
make clean
export NDK=/usr/ndk/android-ndk-r14b
export SYSROOT=$NDK/platforms/android-9/arch-arm/
export TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.8/prebuilt/linux-x86_64
export CPU=arm
export PREFIX=$(pwd)/android/$CPU

./configure --prefix=$PREFIX \
--disable-shared \
--enable-static \
--disable-asm \
--enable-pic \
--enable-debug \
--host=arm-linux \
--cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \
--sysroot=$SYSROOT

make clean
make
make install

接着我们来分析第一个函数,设置视频参数并初始化编码器prepareVideoEncoder

扫描二维码关注公众号,回复: 5736686 查看本文章

JNIEXPORT void JNICALL
Java_org_zzrblog_ffmp_RtmpPusher_prepareVideoEncoder(JNIEnv *env, jobject jobj,
                                                        jint width, jint height, jint bitrate, jint fps)
{
    if(gRtmpPusher == NULL) {
        gRtmpPusher = (RtmpPusher*)calloc(1, sizeof(RtmpPusher));
    } else if(gRtmpPusher->x264_encoder != NULL){
        return;
    }
    gRtmpPusher->bitrate = bitrate;
    gRtmpPusher->fps = fps;
    gRtmpPusher->width = width;
    gRtmpPusher->height = height;
    // 参照源码example.c 的模板代码
    ////////////////////////////////////////////////////////////////
    x264_param_t param;
    //x264_param_default_preset设置   两个字符串所代表的参数集在源码目录的common/base.c当中
    x264_param_default_preset(&param,"ultrafast","zerolatency");
    //编码输入的像素格式YUV420P
    param.i_csp = X264_CSP_I420; // X264_CSP_NV21
    param.i_width  = width;
    param.i_height = height;
    //参数i_rc_method表示码率控制,CQP(恒定质量),CRF(恒定码率),ABR(平均码率)
    //恒定码率,会尽量控制在固定码率
    param.rc.i_rc_method = X264_RC_CRF;
    param.rc.i_bitrate = bitrate / 1000; //码率(比特率) 单位Kbps
    param.rc.i_vbv_max_bitrate = (int) (bitrate / 1000 * 1.2); //瞬时最大码率
    //码率控制不通过timebase和timestamp,而是fps
    param.b_vfr_input = 0;
    param.i_fps_num = (uint32_t) fps; //* 帧率分子
    param.i_fps_den = 1; //* 帧率分母
    param.i_timebase_den = param.i_fps_num;
    param.i_timebase_num = param.i_fps_den;
    param.i_threads = 1;//并行编码线程数量,0默认为多线程
    //是否把SPS和PPS放入每一个关键帧
    //置为1表示每个I帧都重复带SPS/PPS头,为了提高图像的纠错能力
    param.b_repeat_headers = 1;
    //设置Level级别
    param.i_level_idc = 51;
    //设置Profile档次
    //baseline级别,没有B帧   字符串所代表的参数集在源码目录的common/base.c当中
    x264_param_apply_profile(&param, "baseline");

    //x264_picture_t(输入图像)初始化
    x264_picture_alloc(&(gRtmpPusher->pic_in), param.i_csp, param.i_width, param.i_height);

    gRtmpPusher->pic_in.i_pts = 0;
    //打开编码器
    gRtmpPusher->x264_encoder = x264_encoder_open(&param);
    if(gRtmpPusher->x264_encoder){
        LOGI("打开x264编码器成功...");
    }
}

简单分析以上代码,按照我自己的编码风格,把相关的传入参数放到一个自定义类似于上下文(RtmpPusher)结构体内,然后就是参照源码x264/example.c 当中的main函数。其中有两个方法 x264_param_default_preset / x264_param_apply_profile都是传入根据传入的字符串设置x264_param_t引用参数。那么这些传入的字符串是什么意思呢,又代表怎样的x264_param_t设置呢?   继续跟着x264/example.c,根据头文件的引用,锁定到了x264/common/base.c当中。

int x264_param_default_preset( x264_param_t *param, const char *preset, const char *tune )
{
    // x264_stack_align只是一个宏定义,相当于函数指针,第一个参数就是真正的函数地址。
    return x264_stack_align( param_default_preset, param, preset, tune );
}

static int param_default_preset( x264_param_t *param, const char *preset, const char *tune )
{
    param_default( param );

    if( preset && param_apply_preset( param, preset ) < 0 )
        return -1;
    if( tune && param_apply_tune( param, tune ) < 0 )
        return -1;
    return 0;
}

以x264_param_default_preset为例阅读源码:第一步调用param_default初始化x264_param_t的默认值。然后就是两个apply方法,源码大概是长成这样子的:

static int param_apply_preset( x264_param_t *param, const char *preset )
{
    char *end;
    int i = strtol( preset, &end, 10 );
    if( *end == 0 && i >= 0 && i < sizeof(x264_preset_names)/sizeof(*x264_preset_names)-1 )
        preset = x264_preset_names[i];

    if( !strcasecmp( preset, "ultrafast" ) )
    {
        param->i_frame_reference = 1;
        ... ...
    }
    else if( !strcasecmp( preset, "superfast" ) )
    {
        param->analyse.inter = X264_ANALYSE_I8x8|X264_ANALYSE_I4x4;
        ... ...
    }
    else if( !strcasecmp( preset, "veryfast" ) )
    {
        param->analyse.i_subpel_refine = 2;
        ... ...
    }
    else if( !strcasecmp( preset, "faster" ) )
    {
        param->analyse.b_mixed_references = 0;
        ... ...
    }
    else if( !strcasecmp( preset, "fast" ) )
    {
        param->i_frame_reference = 2;
        ... ...
    }
    else if( !strcasecmp( preset, "medium" ) )
    {
        /* Default is medium */
    }
    else if( !strcasecmp( preset, "slow" ) )
    {
        param->analyse.i_subpel_refine = 8;
        ... ...
    }
    else if( !strcasecmp( preset, "slower" ) )
    {
        param->analyse.i_me_method = X264_ME_UMH;
        ... ...
    }
    else if( !strcasecmp( preset, "veryslow" ) )
    {
        param->analyse.i_me_method = X264_ME_UMH;
        ... ...
    }
    else if( !strcasecmp( preset, "placebo" ) )
    {
        param->analyse.i_me_method = X264_ME_TESA;
        ... ...
    }
    else
    {
        x264_log_internal( X264_LOG_ERROR, "invalid preset '%s'\n", preset );
        return -1;
    }
    return 0;
}

这样就知道了x264_param_default_preset 第一个设置选项有多少种类别的字符串供选择,对应设置怎样的x264_param_t参数。由于编幅的关系,我就不分析param_apply_tune。

回归到我们的prepareVideoEncoder,经过x264_param_default_preset(&param,"ultrafast","zerolatency"); 我们已经有一些默认的设置参数,接下来还需要根据实际进行一些调整。代码就不重发了,上面贴出的代码已经有详尽的注释,应该也能明白个一二,但还是有三个点要拿出来说说的:

  1. 设置Level级别 param.i_level_idc = 51 是什么意思?
  2. x264_param_apply_profile设置Profile档次 ?
  3. 编码输入像素格式param.i_csp 为什么是420P,而不直接使用NV21?毕竟传入进来的摄像头预览数据格式就是NV21啊

带着问题,我们进入下一章的学习 视频编码协议 h264。

猜你喜欢

转载自blog.csdn.net/a360940265a/article/details/87784225