音视频7——安卓软编音视频数据推送到rtmp服务器

音视频开发路线:

Android 音视频开发入门指南_Jhuster的专栏的技术博客_51CTO博客_android 音视频开发入门

demo地址:

videoPath/Demo8Activity.java at master · wygsqsj/videoPath · GitHub

前期的代码我们都是通过MediaCodec来实现音视频数据的编码,使用MedieaCodec其实底层使用的还是DSP芯片进行编码,这种方式耗电量低,效率高,但是最大的问题是版本兼容问题,5.0以下基本不支持,dsp芯片是后期厂商才加入的cpu里面去的,cpu的厂商对不同的dsp芯片的实现也不同,所以各种各样的问题下我们必须掌握软编,软编其实就是通过CPU来对我们的音视频数据进行编码,cpu相对dsp芯片来说,他不是专门为做音视频编码设计的,所以他的效率和耗电量都比dsp硬编要逊色一点,但是也是必不可少的一部分。

将视频数据转换H264的软编框架主要是X264,ffmpeg底层也是使用的X264框架,此处单独拿来使用;音频的编码框架是使用的faac,也是市面上常用的一款音频编码框架,这两个框架都是C编写的,由于太过于庞大,我们要先通过交叉编译的方式来编译成SO库,供我们使用。

项目结构

x264和faac框架可以通过Linux编译成对应的so库,我在代码里编译成了arm64-v8a和armeabi-v7a两种so库,如果自己想编译实现,可参考编译x264视频编码库_程序课代表的博客-CSDN博客

具体的项目结构如下:

x264是c编写,我们使用时要通过JNI来调用,LivePush类是连接java层和Native层的通道,我们在java层通过VideHelper获取摄像头输入数据,通过AudioHelper来获取音频数据,再将采集到的音视频数据通过传输层LivePush发送给底层调用;x264_codec是Native层的入口,rtmp的初始化和发送数据功能在此完成,VideoChannel是x264库的编码,AudioChannel是faac用于音频编码,将这些编码数据放到一个队列中,rtmp从队列中不断取出数据后推送到rtmp服务器

X264使用

VideoHelper通过Camera2获取数据,当获取到宽高数据后,先发送给X264进行设置;有个注意的点是时间戳的存储不是直接以时间单位存储的,而是以帧率为单位,例如当前是1s20帧,那当前单位就是1/20,获取时间戳通过当前帧数*1/20得到时间,这时候我们传输时指需要记录当前时多少帧即可。

//初始化x264框架
void VideoChannel::createX264Encode(int width, int height, int fps, int bitrate) {
    // 加锁, 设置视频编码参数 与 编码互斥
    pthread_mutex_lock(&mMutex);

    mWidth = width;
    mHeight = height;
    mFps = fps;
    mBitrate = bitrate;
    mYSize = width * height;
    mUVSize = mYSize / 4;
    //初始化
    if (mVideoCodec) {
        x264_encoder_close(mVideoCodec);
        mVideoCodec = nullptr;
    }
    //类比与MedeaFormat
    x264_param_t param;
    x264_param_default_preset(&param,
                              "ultrafast",//编码器速度,越快质量越低,适合直播
                              "zerolatency"//编码质量
    );
    //编码等级
    param.i_level_idc = 32;
    param.i_csp = X264_CSP_I420;    //nv12
    param.i_width = width;
    param.i_height = height;
    //设置没有B帧
    param.i_bframe = 0;
    /*
     * 码率控制方式
     * X264_RC_CBR:恒定码率 cpu紧张时画面质量差,以网络传输稳定为先
     * X264_RC_VBR:动态码率,cpu紧张时花费更多时间,画面质量比较均衡,适合本地播放
     * X264_RC_ABR:平均码率,是一种折中方式,也是网络传输中最常用的方式
     *
     */
    param.rc.i_rc_method = X264_RC_ABR;
    //码率,k为单位,所以字节数除以1024
    param.rc.i_bitrate = bitrate / 1024;
    /*
     * 帧率
     * 代表1秒有多少帧
     * 帧率时间
     * 当前帧率为25,那么帧率时间我们一般理解成1/25=40ms
     * 但是帧率的单位不是时间,而是一个我们设定的值 i_fps_den/i_timebase_den
     * 例如当前是1000帧了,他对应的时间戳计算方式为:1000(1/25)
     *
     * 如果你的i_fps_den/i_timebase_den 设置的不是 1/fps,那么最终是以这两个参数为单位计算间隔的,一般我们都会
     * 设置成1/fps
    */
    param.i_fps_num = fps;
    param.i_fps_den = 1;
    param.i_timebase_den = param.i_fps_num;
    param.i_timebase_num = param.i_fps_den;
    //使用fps计算帧间距
    param.b_vfr_input = 0;
    //25帧一个I帧
    param.i_keyint_max = fps * 2;
    //sps和pps自动放到I帧前面
    param.b_repeat_headers = 1;
    //开启多线程
    param.i_threads = 1;
    //编码质量
    x264_param_apply_profile(&param, "baseline");
    //打开编码器
    mVideoCodec = x264_encoder_open(&param);
    //输入缓冲区
    pic_in = new x264_picture_t;
    //初始化缓冲区大小
    x264_picture_alloc(pic_in, X264_CSP_I420, width, height);

    // 解锁, 设置视频编码参数 与 编码互斥
    pthread_mutex_unlock(&mMutex);
}

java层每获取到摄像头捕获的数据都发送给x264进行编码,先将java层byte数组转换成jni可使用的数据,记得释放:

extern "C"
JNIEXPORT void JNICALL
Java_com_wish_videopath_demo8_x264_LivePush_native_1pushVideo(JNIEnv *env, jobject thiz,
                                                          jbyteArray data_) {

    //没有实例化编码或者rtmp没连接成功时退出
    if (!videoChannel || !readyPushing) {
        return;
    }
    //转换成数组进行编码
    jbyte *data = env->GetByteArrayElements(data_, NULL);
    videoChannel->encodeData(data);
    env->ReleaseByteArrayElements(data_, data, 0);
}

再将yuv数据放到x264的通道中进行编码,x264与硬编不同,他可以将好几帧的数据放到通道中,例如I P P三帧的Y数据一股脑都扔进Y通道,也就是说x264可以一次性输入好几帧一次性输出好几帧,编码后的数据我们可以得到他的类型,当编码出sps和pps,与MediaCodec不同的是,X264会将sps、pps和I帧一块编码出来,而硬编会先将sps和pps输出出来,x264第一次编码会一次性输出四项:sps/pps/补充信息/I帧,我们根据编码类型,解析出sps和pps添加到发送队列,再把普通数据添加到队列中。

/**
 * 将java层传递的yuv(NV21)数据编码成h264码流
 * @param data 输入的yuv数据
 * 将 y u v分别放到单个通道中,X264可以将多个帧的通道同时存入,例如I P P三帧的Y数据放如x264的y通道
 * 所以x264框架可以一次性输出好几个NAL单元
 *
 */
void VideoChannel::encodeData(int8_t *data) {
    // 加锁, 设置视频编码参数 与 编码互斥
    pthread_mutex_lock(&mMutex);

    //将Y放入x264的y通道
    memcpy(pic_in->img.plane[0], data, mYSize);
    // 取出u v数据放入通道
    for (int i = 0; i < mUVSize; i++) {
        //img.plane[1]里面放的是u数据;我们的yuv格式是NV21,data[1]是U数据
        *(pic_in->img.plane[1] + i) = *(data + mYSize + i * 2 + 1);
        //img.plane[2]里面放的是V数据;data[0]是V数据
        *(pic_in->img.plane[2] + i) = *(data + mYSize + i * 2);
    }

    //编码后的NAL个数,可以理解成编码出了几帧
    int pi_nal;
    //编码后的数据存储区,这里面放了pi_nal个帧的数据
    x264_nal_t *pp_nal;
    //输出的编码数据参数,类似于MedeaCodec编码EncodeInfo
    x264_picture_t pic_out;
    //开始编码
    x264_encoder_encode(mVideoCodec, &pp_nal, &pi_nal, pic_in, &pic_out);

    //缓存sps和pps
    uint8_t sps[100];
    uint8_t pps[100];
    int spsLen;
    int ppsLen;
    //编码后的数据
    if (pi_nal > 0) {
        for (int i = 0; i < pi_nal; i++) {
            LOGI("当前帧数:%d,当前帧大小:%d", i, pp_nal[i].i_payload);
            //rtmp 是将sps和pps一起打包发送出去的
            if (pp_nal[i].i_type == NAL_SPS) {
                //减去00000001分隔符的长度
                spsLen = pp_nal[i].i_payload - 4;
                memcpy(sps, pp_nal[i].p_payload + 4, spsLen);
            } else if (pp_nal[i].i_type == NAL_PPS) {
                //减去00000001分隔符的长度
                ppsLen = pp_nal[i].i_payload - 4;
                memcpy(pps, pp_nal[i].p_payload + 4, ppsLen);
                //发送到rtmp服务器
                sendSPSPPS(sps, pps, spsLen, ppsLen);
            } else {
                sendVideo(pp_nal[i].i_type, pp_nal[i].i_payload, pp_nal[i].p_payload);
            }

        }
    }

    // 解锁, 设置视频编码参数 与 编码互斥
    pthread_mutex_unlock(&mMutex);
}

构建sps、pps、帧数据包在以前博客中也写过,可以去github了解一下

faac使用

同样faac通过交叉编译的方式得到so库,具体也是在java中通过AudioRecord来获取音频,然后发送到底层进行编码,注意点就是AudioRecord的缓冲区大小,要根据faac实例化得到他的输入大小

   int inputByteNum = livePush.native_initAudioCodec(SAMPLE_RATE_HZ, channelCount);
        /*
         * 缓冲区,此处的最小buffer只能作为参考值,不同于MedeaCodec我们可以直接使用此缓冲区大小,当设备不支持硬编时
         * getMinBufferSize会返回-1,所以还要根据faac返回给我们的输入区大小来确定
         * faac会返回给我们一个缓冲区大小,将他和缓冲区大小比较之后采用最大值
         */
        int minBufferSize = Math.max(inputByteNum, AudioRecord.getMinBufferSize(SAMPLE_RATE_HZ, CHANNEL_CONFIG, AUDIO_FORMAT));

        //初始化录音数据缓冲区,要根据faac返回的采样数据大小构建,否则传输给faac编码的音频数据大小不一致时编码出来的数据会出现杂音
        buffer = new byte[minBufferSize];
        try {
            //初始化录音器,使用的是对比得到的数据大小
            audioRecord = new AudioRecord(
                    MediaRecorder.AudioSource.MIC,
                    SAMPLE_RATE_HZ,
                    CHANNEL_CONFIG,
                    AUDIO_FORMAT,
                    minBufferSize);
        } catch (Exception e) {
            e.printStackTrace();
        }

实例化faac,得到输入容器大小inputByteNum,并返回出去

//初始化音频编码器faac
extern "C"
JNIEXPORT jint JNICALL
Java_com_wish_videopath_demo8_x264_LivePush_native_1initAudioCodec(JNIEnv *env, jobject thiz,
                                                               jint sample_rate,
                                                               jint channel_count) {
    audioChannel = new AudioChannel;
    audioChannel->setCallBack(callBack);
    audioChannel->initCodec(sample_rate, channel_count);
    return audioChannel->getInputByteNum();
}
//实例化音频编码器
void AudioChannel::initCodec(int sampleRate, int channels) {
    chanelCount = channels;
    //输入的容量大小,要与java层AudioRecord获取的缓冲区大小比较得到缓冲区大小
    unsigned long inputSamples;
    //获取编码器,参数解释:采样率、通道数、输入的音频样本数量(返回给我们)、输出的字节容量(返回给我们)
    codec = faacEncOpen(sampleRate, channels, &inputSamples, &maxOutputBytes);

    //输入的容器的大小,我们的采样位数是16,也就是2个字节,用样本数*2得到输入大小
    inputByteNum = inputSamples * 2;
    //配置参数
    faacEncConfigurationPtr configurationPtr = faacEncGetCurrentConfiguration(codec);

    // 设置编码格式标准, 使用 MPEG4 新标准
    configurationPtr->mpegVersion = MPEG4;
    configurationPtr->aacObjectType = LOW;
    //采样位数
    configurationPtr->inputFormat = FAAC_INPUT_16BIT;
    //0 输出aac原始数据 1 添加ADTS头之后的数据
    configurationPtr->outputFormat = 0;
    //使配置生效
    faacEncSetConfiguration(codec, configurationPtr);
    LOGI("编码后的音频缓冲区大小:%d", maxOutputBytes);
    //输出的容器
    outputBuffer = new unsigned char[maxOutputBytes];
}

我们音频在rtmp传输时要先发送一个音频头,这个操作我们把他放在初始化rtmp连接时,如果连接成功,我们就先往传输队列里面扔一个音频头,这样rtmp会先把我们的音频头发送出去

void *start(void *args) {
    char *url = static_cast<char *>(args);
    //不断重试,链接服务器
    do {
        //初始化RTMP,申请内存
        rtmp = RTMP_Alloc();
        if (!rtmp) {
            LOGI("RTMP 创建失败");
            break;
        }
        RTMP_Init(rtmp);
        //设置超时时间
        rtmp->Link.timeout = 10;
        //设置地址
        int ret = RTMP_SetupURL(rtmp, (char *) url);
        if (!ret) {
            LOGI("RTMP 创建失败");
            break;
        }
        LOGI("connect %s", url);
        //设置输出模式
        RTMP_EnableWrite(rtmp);
        LOGI("connect Connect");
        //连接
        if (!(ret = RTMP_Connect(rtmp, 0))) break;
        LOGI("connect ConnectStream");
        //连接流
        if (!(ret = RTMP_ConnectStream(rtmp, 0))) break;
        LOGI("connect 成功");
        start_time = RTMP_GetTime();
        packets.setWork(1);
        RTMPPacket *packet = 0;

        //添加音频头到队列中
        if (audioChannel) {
            callBack(audioChannel->getAudioHead());
            LOGI("添加音频头到队列中");
        }
        //从队列中取出数据发送
        readyPushing = 1;
        while (isStart) {
            packets.pop(packet);
            if (!isStart) {
                break;
            }
            if (!packet) {
                continue;
            }
            packet->m_nInfoField2 = rtmp->m_stream_id;
            //发送
            ret = RTMP_SendPacket(rtmp, packet, 1);
            releasePackets(packet);
            if (!ret) {
                LOGI("发送数据失败!");
                break;
            }
        }
        releasePackets(packet);
    } while (false);

    delete url;
    return nullptr;
}

音频头的封装在AudioChannal中:

/发送音频头
RTMPPacket *AudioChannel::getAudioHead() {
    if (!codec) {
        return nullptr;
    }
    unsigned char *buf;
    unsigned long len;
    //音频头
    faacEncGetDecoderSpecificInfo(codec, &buf, &len);

    RTMPPacket *packet = new RTMPPacket;
    RTMPPacket_Alloc(packet, len + 2);
    packet->m_body[0] = 0xAF;
    if (chanelCount == 1) {
        // 如果是单声道, 将该值修改成 AE
        packet->m_body[0] = 0xAE;
    }
    packet->m_body[1] = 0x00;
    memcpy(&packet->m_body[2], buf, len);
    //设置音频类型
    packet->m_packetType = RTMP_PACKET_TYPE_AUDIO;
    packet->m_nBodySize = len + 2;
    //通道值,音视频不能相同
    packet->m_nChannel = 0x05;
    packet->m_nTimeStamp = 0;
    packet->m_hasAbsTimestamp = 0;
    packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
    return packet;
}

然后就是faac框架对音频的编码逻辑:

//编码音频
void AudioChannel::encode(int8_t *data) {
    //进行编码 参数:编码器 编码数据,编码数据大小 ,输入容器,输入容器大小
    int encodeLen = faacEncEncode(codec,
                                  reinterpret_cast<int32_t *>(data),
                                  inputByteNum / 2,//样本数量
                                  outputBuffer,
                                  maxOutputBytes);
    LOGI("编码后的音频数据大小:%d", encodeLen);
    if (encodeLen > 0) {
        RTMPPacket *packet = new RTMPPacket;
        int body_size = encodeLen + 2;
        RTMPPacket_Alloc(packet, body_size);
        packet->m_body[0] = 0xAF;
        if (chanelCount == 1) {
            // 如果是单声道, 将该值修改成 AE
            packet->m_body[0] = 0xAE;
        }
        packet->m_body[1] = 0x01;
        memcpy(&packet->m_body[2], outputBuffer, encodeLen);
        //设置音频类型
        packet->m_packetType = RTMP_PACKET_TYPE_AUDIO;
        packet->m_nBodySize = body_size;
        //通道值,音视频不能相同
        packet->m_nChannel = 0x05;
        packet->m_nTimeStamp = 0;
        packet->m_hasAbsTimestamp = 0;
        packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
        if (callbackAudio) {
            callbackAudio(packet);
        }
    }
}

这样x264和faac的基本使用就完成了,其实逻辑很简单,对应的初始化代码和编码api也是固定的,ok,这样我们就完成了软编推流的功能了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值