设为首页收藏本站

安徽论坛

 找回密码
 立即注册

QQ登录

只需一步,快速开始

查看: 77502|回复: 0

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

[复制链接]

68

主题

0

回帖

216

积分

中级会员

Rank: 3Rank: 3

积分
216
发表于 2022-3-26 10:28:36 | 显示全部楼层 |阅读模式
网站内容均来自网络,本站只提供信息平台,如有侵权请联系删除,谢谢!
音视频开发路线:
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得到时间,这时候我们传输时指需要记录当前时多少帧即可。
  1. //初始化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可使用的数据,记得释放:
  1. extern "C"JNIEXPORT void JNICALLJava_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添加到发送队列,再把普通数据添加到队列中。
  1. /** * 将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实例化得到他的输入大小
  1.    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,并返回出去
  1. //初始化音频编码器faacextern "C"JNIEXPORT jint JNICALLJava_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();}
复制代码
  1. //实例化音频编码器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会先把我们的音频头发送出去
  1. void *start(void *args) {    char *url = static_cast(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中:
  1. /发送音频头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框架对音频的编码逻辑:
  1. //编码音频void AudioChannel::encode(int8_t *data) {    //进行编码 参数:编码器 编码数据,编码数据大小 ,输入容器,输入容器大小    int encodeLen = faacEncEncode(codec,                                  reinterpret_cast(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,这样我们就完成了软编推流的功能了。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
免责声明
1. 本论坛所提供的信息均来自网络,本网站只提供平台服务,所有账号发表的言论与本网站无关。
2. 其他单位或个人在使用、转载或引用本文时,必须事先获得该帖子作者和本人的同意。
3. 本帖部分内容转载自其他媒体,但并不代表本人赞同其观点和对其真实性负责。
4. 如有侵权,请立即联系,本网站将及时删除相关内容。
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表