通过libx246 libfaac转换推送RTMP音视频直播流

news2024/12/24 8:21:44

一、RTMP简介及rtmplib库:

        RTMP协议是Real Time Message Protocol(实时信息传输协议)的缩写,它是由Adobe公司提出的一种应用层的协议,用来解决多媒体数据传输流的多路复用(Multiplexing)和分包(packetizing)的问题。

简介:

  1. RTMP是应用层协议,采用(通常)TCP来保证可靠传输;
  2. 在TCP完成握手链接建立后,还会进行RTMP的一些自己的握手动作,在建立RTMP Connection链接;
  3. 在Connection链接上会传输一些控制信息,例如SetChunkSize,SetACKWindowSize。CreateStream命令会创建一个Stream链接,用于传输具体的音视频数据和控制这些信息传输的命令信息;
  4. RTMP在发送的时候会将数据格式化为Message,然后再将一个个Messsage分为带有Message ID 的Chunk(块),每个Chuck可能是一个完整的Message,可能是一个Message的一部分。在接收端会根据Message ID,长度与data将chunk还原为Message。

        雷霄骅的关于rtmp及rtmplib源码解析的地址。

rtmplib:

        关于外网的RTMPDump文档介绍了rtmp。

RTMPLIB库中重要方法:

  • RTMP_Alloc ():创建会话句柄;
  • RTMP_Init ():初始化rtmp;
  • RTMP_SetupURL():设置URL;
  • RTMP_Connect():建立网络连接;
  • RTMP_ConnectStream():建立连接rtmp流会话;
  • RTMP_Read():读取rtmp流,当返回0字节时,流已经完成。
  • RTMP_Write():发布流,客户端可以在RTMP_Connect () 调用之前 调用RTMP_EnableWrite () 发布流,然后在建立会话后使用 RTMP_Write ();
  • RTMP_Pause():在播放流时,暂停和取消暂停;
  • RTMP_Seek():移动流播放位置;
  • RTMP_Close():关闭流;
  • RTMP_Free():释放会话句柄;
  • RTMPPacket_Alloc():初始化RTMPPacket的内存空间;
  • RTMPPacket:RTMP协议封包;
  • RTMPPacket_Free():释放RTMPPacket的内存空间;

二、X264库简介:

        x264库中重要方法:

  • x264_param_default():给各个参数设置默认值;
  • x264_param_default_preset():设置默认的preset,内部调用了x264_param_apply_preset()和x264_param_apply_tune(),在它们之中即可找到各个preset和tune的详细参数区别;
  • x264_param_apply_profile():给定的文件配置,流的速率;
  • x264_encoder_open():用于打开编码器,其中初始化了libx264编码所需要的各种变量,必须使用x264_encoder_close()进行释放;
  • x264_picture_alloc():为图片开辟数据空间,必须使用x264_picture_clean()进行释放;
  • x264_encoder_encode():编码一帧YUV为H.264码流

        雷霄骅关于X264的源码分析。

        

三、FAAC库简介:

        FAAC是一个MPEG-4和MPEG-2的AAC编码器,其特性是:可移植性好,快速,支持LC/Main/LTP,通过Dream支持DRM,代码小相对于FFMPEG的AAC转码。

        libFaac库中重要方法:

  •  faacEncOpen():打开编码器;
  • faacEncGetCurrentConfiguration():获取配置;
  • faacEncSetConfiguration() :设置配置;
  • faacEncGetDecoderSpecificInfo():得到解码信息;
  • faacEncEnccode():对帧进行编码,并返回编码后的长度。

四、代码框架图及总览:

        Android层获取对应的视频(YUV)及音频(PCM)数据,传入C++层分别通过libx264把YUV转成h264数据、通过libfaac把PCM转成AAC数据。然后包装h264/pcm成RTMPPacket封装包,回调push到PacketQueue队列中,调到子线程RTMP_SendPacket     

五、代码实现细节解析:

        1、LiveManger.java类中获取CamerX视频YUV数据:

private void updateVideoCodecInfo(int degree) {
        camera2Helper.updatePreviewDegree(degree);
        if (mRtmpLivePusher != null) {
            int width = previewSize.getWidth();
            int height = previewSize.getHeight();
            if (degree == 90 || degree == 270) {
                int temp = width;
                width = height;
                height = temp;
            }
            mRtmpLivePusher.setVideoCodecInfo(width, height, videoFrameRate, videoBitRate);
        }
    }


   @Override
    public void onPreviewFrame(byte[] yuvData) {
//        Log.e(TAG, "onPreviewFrame:"+yuvData.length);
        if (yuvData != null && isLiving && mRtmpLivePusher != null) {
           
            mRtmpLivePusher.pushVideoData(yuvData);
        }
    }

        2、VideoStreamPacket.cpp中设置libx264的设置属性及把YUV封装成H264:

        

void VideoStreamPacket::encodeVideo(int8_t *data) {
//    callbackStatusMsg("VideoStreamPacket encodeVideo",0);
    lock_guard<mutex> lock(m_mutex);
    if (!pic_in)
        return;
    //YUV420解析分离
    int offset = 0;
    memcpy(pic_in->img.plane[0], data, (size_t) m_frameLen); // y
    offset += m_frameLen;
    memcpy(pic_in->img.plane[1], data + offset, (size_t) m_frameLen / 4); // u
    offset += m_frameLen / 4;
    memcpy(pic_in->img.plane[2], data + offset, (size_t) m_frameLen / 4);  //v

    //YUV封装成H264
    x264_nal_t *pp_nal;
    int pi_nal;
    x264_picture_t pic_out;
    x264_encoder_encode(videoCodec, &pp_nal, &pi_nal, pic_in, &pic_out);
    int pps_len, sps_len = 0;
    uint8_t sps[100] = {0};
    uint8_t pps[100] = {0};
    //H264包装成RTMP流格式
    for (int i = 0; i < pi_nal; ++i) {
        x264_nal_t nal = pp_nal[i];
        if (nal.i_type == NAL_SPS) {
            sps_len = nal.i_payload - 4;
            memcpy(sps, nal.p_payload + 4, static_cast<size_t>(sps_len));
        } else if (nal.i_type == NAL_PPS) {
            pps_len = nal.i_payload - 4;
            memcpy(pps, nal.p_payload + 4, static_cast<size_t>(pps_len));
            sendSpsPps(sps, pps, sps_len, pps_len);
        } else {
            sendFrame(nal.i_type, nal.p_payload, nal.i_payload);
        }
    }

}

void VideoStreamPacket::encodeVideo(int8_t *data) {
//    callbackStatusMsg("VideoStreamPacket encodeVideo",0);
    lock_guard<mutex> lock(m_mutex);
    if (!pic_in)
        return;
    //YUV420解析分离
    int offset = 0;
    memcpy(pic_in->img.plane[0], data, (size_t) m_frameLen); // y
    offset += m_frameLen;
    memcpy(pic_in->img.plane[1], data + offset, (size_t) m_frameLen / 4); // u
    offset += m_frameLen / 4;
    memcpy(pic_in->img.plane[2], data + offset, (size_t) m_frameLen / 4);  //v

    //YUV封装成H264
    x264_nal_t *pp_nal;
    int pi_nal;
    x264_picture_t pic_out;
    x264_encoder_encode(videoCodec, &pp_nal, &pi_nal, pic_in, &pic_out);
    int pps_len, sps_len = 0;
    uint8_t sps[100] = {0};
    uint8_t pps[100] = {0};
    //H264包装成RTMP流格式
    for (int i = 0; i < pi_nal; ++i) {
        x264_nal_t nal = pp_nal[i];
        if (nal.i_type == NAL_SPS) {
            sps_len = nal.i_payload - 4;
            memcpy(sps, nal.p_payload + 4, static_cast<size_t>(sps_len));
        } else if (nal.i_type == NAL_PPS) {
            pps_len = nal.i_payload - 4;
            memcpy(pps, nal.p_payload + 4, static_cast<size_t>(pps_len));
            sendSpsPps(sps, pps, sps_len, pps_len);
        } else {
            sendFrame(nal.i_type, nal.p_payload, nal.i_payload);
        }
    }

}

        3、LiveManger.java中获取AudioRecord的PCM数据:

 private void initAudio() {
        int minBufferSize = AudioRecord.getMinBufferSize(sampleRate,
                channelConfig, audioFormat) * 2;

        int bufferSizeInBytes = Math.max(minBufferSize, mRtmpLivePusher.getInputSamplesFromNative());
        if (ActivityCompat.checkSelfPermission(mContext
                , Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
            // TODO: Consider calling
            //    ActivityCompat#requestPermissions
            // here to request the missing permissions, and then overriding
            //   public void onRequestPermissionsResult(int requestCode, String[] permissions,
            //                                          int[] grantResults)
            // to handle the case where the user grants the permission. See the documentation
            // for ActivityCompat#requestPermissions for more details.
            return;
        }
        mRtmpLivePusher.setAudioCodecInfo(sampleRate, channelConfig);
        inputSamples = mRtmpLivePusher.getInputSamplesFromNative();
        mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, sampleRate,
                channelConfig, audioFormat, bufferSizeInBytes);
    }


 class AudioTask implements Runnable {

        @Override
        public void run() {
            mAudioRecord.startRecording();
            byte[] bytes = new byte[inputSamples];
            while (isLiving) {
                int len = mAudioRecord.read(bytes, 0, bytes.length);
                if (len > 0) {
                    mRtmpLivePusher.pushAudioData(bytes);
                }
            }
            mAudioRecord.stop();
        }
    }

        4、AudioStreamPacket.cpp中设置libfaac的属性及把PCM封装成AAC:

int AudioStreamPacket::setAudioEncInfo(int samplesInHZ, int channels) {
    callbackStatusMsg("AudioStreamPacket setAudioEncInfo", 0);
    m_channels = channels;
    //open faac encoder
    m_audioCodec = faacEncOpen(static_cast<unsigned long>(samplesInHZ),
                               static_cast<unsigned int>(channels),
                               &m_inputSamples,
                               &m_maxOutputBytes);
    m_buffer = new u_char[m_maxOutputBytes];

    //set encoder params
    faacEncConfigurationPtr config = faacEncGetCurrentConfiguration(m_audioCodec);
    config->mpegVersion = MPEG4;    //设置版本,录制MP4文件时要用MPEG4
    config->aacObjectType = LOW;     //编码类型
    config->inputFormat = FAAC_INPUT_16BIT;     //输入数据类型
    config->outputFormat = 0;
    return faacEncSetConfiguration(m_audioCodec, config);
}

void AudioStreamPacket::encodeData(int8_t *data) {
//    callbackStatusMsg("AudioStreamPacket encodeData", 0);

    //encode a frame, and return encoded len
    int byteLen = faacEncEncode(m_audioCodec, reinterpret_cast<int32_t *>(data),
                                static_cast<unsigned int>(m_inputSamples),
                                m_buffer,
                                static_cast<unsigned int>(m_maxOutputBytes));
    if (byteLen > 0) {
        int bodySize = 2 + byteLen;
        auto *packet = new RTMPPacket();
        RTMPPacket_Alloc(packet, bodySize);
        //stereo
        packet->m_body[0] = 0xAF;
        if (m_channels == 1) {
            packet->m_body[0] = 0xAE;
        }

        packet->m_body[1] = 0x01;
        memcpy(&packet->m_body[2], m_buffer, static_cast<size_t>(byteLen));

        packet->m_hasAbsTimestamp = 0;
        packet->m_nBodySize = bodySize;
        packet->m_packetType = RTMP_PACKET_TYPE_AUDIO;
        packet->m_nChannel = 0x11;
        packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
        if (mContext != nullptr && mAudioCallback != nullptr) {
            mAudioCallback(mContext, packet);
        }
    }
}

        5、RtmpPusherManger.cpp分别把AudioStreamPacket.cpp和VideoStreamPacket.cpp回调的RTMPPacket包,然后传入到RtmpInit.cpp。包装的RTMPPacket包push到队列PacketQueue中,

void RtmpPusherManger::initVideoPacket() {
    RtmpStatusMessage(this, "initVideoPacket", 0);
    if (videoStreamPacket == nullptr) {
        videoStreamPacket = new VideoStreamPacket();
    }
    videoStreamPacket->setRtmpStatusCallback(this, RtmpStatusMessage);
    videoStreamPacket->setVideoCallback(this, callbackRtmpPacket);
}


void RtmpPusherManger::initAudioPacket() {
    RtmpStatusMessage(this, "initAudioPacket", 0);
    if (audioStreamPacket == nullptr) {
        audioStreamPacket = new AudioStreamPacket();
    }
    audioStreamPacket->setRtmpStatusCallback(this, RtmpStatusMessage);
    audioStreamPacket->setAudioCallback(this, callbackRtmpPacket);
}

void RtmpPusherManger::callbackRtmpPacket(void *context, RTMPPacket *packet) {
    if (context != nullptr && packet != nullptr) {
        RtmpPusherManger *pFmpegManger = static_cast<RtmpPusherManger *>(context);
        pFmpegManger->addRtmpPacket(packet);
    }

}

void RtmpPusherManger::addRtmpPacket(RTMPPacket *packet) {
    if (rtmpInit == nullptr) return;
    rtmpInit->addRtmpPacket(packet);
}

        6、RtmpInit.cpp中从RtmpPusherManger.cpp传入添加的RTMPPacket包,push到队列PacketQueue中。在子线程中初始化RTMP相关操作成功后不断从PacketQueue中pop出RTMPPacket,并最终把RTMPPacket在RTMP_SendPacket发送出去。

void RtmpInit::addRtmpPacket(RTMPPacket *packet) {
//    callbackStatusMsg("Rtmp addRtmpPacket", 0);
    packet->m_nTimeStamp = RTMP_GetTime() - start_time;
    packetQueue.push(packet);
}


void RtmpInit::startThread() {
    callbackStatusMsg("Rtmp startThread", 0);
    LOGE("murl:%s", mUrl);

    char *url = const_cast<char *>(mUrl);
    RTMP *rtmp = 0;
    do {
        rtmp = RTMP_Alloc();
        if (!rtmp) {
            callbackStatusMsg("Rtmp create fail", -1);
            break;
        }
        RTMP_Init(rtmp);
        rtmp->Link.timeout = 5;
        int ret = RTMP_SetupURL(rtmp, url);
        if (!ret) {
            callbackStatusMsg("Rtmp SetupURL fail", ret);
            break;
        }
        //开启输出模式
        RTMP_EnableWrite(rtmp);
        ret = RTMP_Connect(rtmp, 0);
        if (!ret) {
            callbackStatusMsg("rtmp连接地址失败", ret);
            break;
        }
        ret = RTMP_ConnectStream(rtmp, 0);
        if (!ret) {
            callbackStatusMsg("rtmp连接流失败", ret);
            break;
        }
        //start pushing
        isPushing = true;
        packetQueue.setRunning(true);
        //获取音频的首帧值
        if (mContext != nullptr) {
            mGetAudioTagCallback(mContext);
        }
        RTMPPacket *packet = nullptr;
        while (isPushing) {
            packetQueue.pop(packet);
            if (!isPushing) {
                break;
            }
            if (!packet) {
                continue;
            }

            packet->m_nInfoField2 = rtmp->m_stream_id;
            ret = RTMP_SendPacket(rtmp, packet, 1);
            releasePackets(packet);
            if (!ret) {
                LOGE("RTMP_SendPacket fail...");
                callbackStatusMsg("RTMP_SendPacket fail...", -2);
                break;
            }
        }
        releasePackets(packet);

    } while (0);
    isPushing = false;
    packetQueue.setRunning(false);
    packetQueue.clear();
    //释放rtmp
    if (rtmp) {
        RTMP_Close(rtmp);
        RTMP_Free(rtmp);
    }
    delete url;
}

      7、 Github的项目地址:https://github.com/wangyongyao1989/WyFFmpeg/tree/main/rtmplive

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1936771.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C++ :友元类

友元类的概念和使用 (1)将类A声明为B中的friend class后&#xff0c;则A中所有成员函数都成为类B的友元函数了 (2)代码实战&#xff1a;友元类的定义和使用友元类是单向的 (3)友元类是单向的&#xff0c;代码实战验证 互为友元类 (1)2个类可以互为友元类&#xff0c;代码实战…

相同IP地址仿真测试

相同IP地址仿真测试 背景与挑战解决方案技术优势功能特点 背景与挑战 在汽车电子领域&#xff0c;电子控制单元&#xff08;ECU&#xff09;的测试是确保其功能性和可靠性的关键步骤。然而&#xff0c;当测试场景涉及多个配置相同IP地址的ECU时&#xff0c;传统的测试方法面临…

GooglePlay 金融品类政策更新(7月17号)

距离上次政策大更新&#xff08;4月5号&#xff09;才过去了3个月&#xff0c;Google Play又迎来了一次大更新&#xff0c;不得不说Google Play的要求越来越高了。 我们来梳理一下这次GooglePlay针对金融品类更新了哪些政策: 1.要求提供金融产品和服务的开发者必须注册为组织…

IDEA的常见代码模板的使用

《IDEA破解、配置、使用技巧与实战教程》系列文章目录 第一章 IDEA破解与HelloWorld的实战编写 第二章 IDEA的详细设置 第三章 IDEA的工程与模块管理 第四章 IDEA的常见代码模板的使用 第五章 IDEA中常用的快捷键 第六章 IDEA的断点调试&#xff08;Debug&#xff09; 第七章 …

STM32使用SPI向W25Q64存储信息(HAL库)

SPI全双工通信&#xff1a;全双工在时钟脉冲周期的每一个周期内&#xff0c;每当主设备同时发送一个字节的同时&#xff0c;会接受从设备接受一个字节数据&#xff0c;SPI全双工最大的特点就是发送和接受数据同步进行&#xff0c;发送多少数据就要接受多少数据。使用全双工通信…

vst 算法R语言手工实现 | Seurat4 筛选高变基因的算法

1. vst算法描述 &#xff08;1&#xff09;为什么需要矫正 image source: https://ouyanglab.com/singlecell/basic.html In this panel, we observe that there is a very strong positive relationship between a gene’s average expression and its observed variance. I…

【iOS】static、extern、const、auto关键字以及联合使用

目录 前言extern关键字static关键字const关键字 联合使用static和externstatic和constextern和const auto关键字 先了解一下静态变量所在的全局/静态区的特点&#xff1a;【iOS】内存五大分区 前言 上面提到的全局/静态区中存放的是全局变量或静态变量&#xff1a; 全局变量…

逻辑回归(Logistic Regression,LR)

分类和回归是机器学习的两个主要问题。 分类处理的是离散数据回归处理的是连续数据 线性回归&#xff1a;回归 拟合一条线预测函数&#xff1a; 逻辑回归&#xff1a;分类——找到一条线可以将不同类别区分开 虽然称为逻辑回归&#xff0c;但是实际是一种分…

Chromium CI/CD 之Jenkins实用指南2024-在Windows节点上创建任务(九)

1. 引言 在现代软件开发流程中&#xff0c;持续集成&#xff08;CI&#xff09;和持续交付&#xff08;CD&#xff09;已成为确保代码质量和加速发布周期的关键实践。Jenkins作为一款广泛应用的开源自动化服务器&#xff0c;通过其强大的插件生态系统和灵活的配置选项&#xf…

【第4章】Spring Cloud之Nacos单机模式支持mysql

文章目录 前言一、初始化1. 初始化数据库2. 修改配置文件 二、效果1. 重新启动2. 新增用户 总结 前言 在0.7版本之前&#xff0c;在单机模式时nacos使用嵌入式数据库实现数据的存储&#xff0c;不方便观察数据存储的基本情况。0.7版本增加了支持mysql数据源能力&#xff0c;具…

数据如光,科技助航丨泰迪智能科技2024年暑期数据分析学徒班正式开班

7月16日&#xff0c;泰迪智能科技2024年暑期数据分析学徒班在泰迪智能科技天河培训中心正式开班&#xff0c;泰迪智能科技培训业务部孙学镂、教学代表周津、授课讲师巫兴港、就业指导老师赵欣欣、孟宪同、教学班主任黄晨华出席此次开班仪式。 仪式伊始&#xff0c;孙学镂代表广…

达梦数据库的系统视图v$arch_status

达梦数据库的系统视图v$arch_status 在达梦数据库&#xff08;DM Database&#xff09;中&#xff0c;V$ARCH_STATUS 是一个动态性能视图&#xff08;Dynamic Performance View&#xff09;&#xff0c;用于显示归档日志的状态信息。这个视图可以帮助数据库管理员监控和管理数…

【Linux服务器Java环境搭建】010在linux中安装Redis,以及对Redis的配置与远程连接

系列文章目录 【Linux服务器Java环境搭建】 前言 好久没有更新博客了&#xff0c;今天下了班回到家&#xff0c;看到电脑桌上尘封已久的《Spring Boot应用开发实战》&#xff0c;翻开目录想起来之前写的系列【Linux服务器Java环境搭建】还未完结&#xff0c;那就继续吧&#…

通义千问AI模型对接飞书机器人-模型配置(2-1)

一 背景 根据业务或者使用场景搭建自定义的智能ai模型机器人&#xff0c;可以较少我们人工回答的沟通成本&#xff0c;而且可以更加便捷的了解业务需求给出大家设定的业务范围的回答&#xff0c;目前基于阿里云的通义千问模型研究。 二 模型研究 参考阿里云帮助文档&#xf…

IDEA的断点调试(Debug)

《IDEA破解、配置、使用技巧与实战教程》系列文章目录 第一章 IDEA破解与HelloWorld的实战编写 第二章 IDEA的详细设置 第三章 IDEA的工程与模块管理 第四章 IDEA的常见代码模板的使用 第五章 IDEA中常用的快捷键 第六章 IDEA的断点调试&#xff08;Debug&#xff09; 第七章 …

基于chrome插件的企业应用

一、chrome插件技术介绍 1、chrome插件组件介绍 名称 职责 访问权限 DOM访问情况 popup 弹窗页面。即打开形式是通过点击在浏览器右上方的icon&#xff0c;一个弹窗的形式。 注: 展示维度 browser_action:所有页面 page_action:指定页面 可访问绝大部分api 不可以 bac…

数据分析入门:用Python和Numpy探索音乐流行趋势

一、引言 音乐是文化的重要组成部分&#xff0c;而音乐流行趋势则反映了社会文化的变迁和人们审美的变化。通过分析音乐榜单&#xff0c;我们可以了解哪些歌曲或歌手正在受到大众的欢迎&#xff0c;甚至预测未来的流行趋势。Python作为一种强大的编程语言&#xff0c;结合其丰…

【数据库系列】Parquet 文件介绍

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

记录些Spring+题集(8)

Spring Bean 的生命周期 Spring Bean 的生命周期涉及到 Spring 框架中对象的创建、初始化、使用和销毁等关键过程。 在Spring框架中&#xff0c;Bean的生命周期经历以下几个阶段&#xff1a; 1&#xff09;实例化&#xff1a; Spring 根据配置文件或注解等方式创建 Bean 实例…

《0基础》学习Python——第十八讲__爬虫\<1>

一、什么是爬虫 爬虫是一种网络数据抓取的技术。通过编写程序&#xff08;通常使用Python&#xff09;&#xff0c;爬虫可以自动化地访问网页&#xff0c;解析网页内容并提取出所需的数据。爬虫可以用于各种用途&#xff0c;如搜索引擎的索引&#xff0c;数据分析和挖掘&#x…