NDK RTMP直播客户端三

news2025/1/17 1:06:35

在之前完成的实战项目【FFmpeg音视频播放器】属于拉流范畴,接下来将完成推流工作,通过RTMP实现推流,即直播客户端。简单的说,就是将手机采集的音频数据和视频数据,推到服务器端。

接下来的RTMP直播客户端系列,主要实现红框和紫色部分:

 本节主要内容:

1.Java层音频编码工作。

2.FAAC库导入。

3.Native层音频编码器工作。

4.Native层音频推流编码工作。

源码:

NdkPush: 通过RTMP实现推流,直播客户端。

一、Java层音频编码

在上一节Java层视频编码工作中,MainActivity已经把用户操作页面相关功能分发给NdkPusher.java,现在只需要通过NdkPusher,把音频相关的事件分发给AudioChannel.java处理;

1)NdkPusher:

中转站,分发MainActivity事件和和Native层打交道;

NdkPusher初始化时,主要是的三件事,

①:初始化native层需要的加载,
②:实例化视频通道并传递基本参数(宽高,fps,码率等),
③:实例化音频通道

上节已完成①、②;本节只要是未完成③:实例化音频通道;

/**
 * 此中转站的构造,主要是的三件事,
 * ①:初始化native层需要的加载,
 * ②:实例化视频通道并传递基本参数(宽高,fps,码率等),
 * ③:实例化音频通道
 */
public NdkPusher(Activity activity, int cameraId, int width, int height, int fps, int bitrate) {
	native_init();
	// 将this传递给VideoChannel,方便VideoChannel操控native层
	mVideoChannel = new VideoChannel(this, activity, cameraId, width, height, fps, bitrate);
	mAudioChannel = new AudioChannel(this);
}

开始直播,调用native层开始直播工作,分发给视频通道AudioChannel开始直播

public void startLive(String path) {
	native_start(path);
	mVideoChannel.startLive();
	mAudioChannel.startLive();
}

停止直播,调用native层停止直播工作,分发给视频通道AudioChannel停止直播

public void stopLive() {
	mVideoChannel.stopLive();
	mAudioChannel.stopLive();
	native_stop();
}

释放工作,释放native层数据和视频通道AudioChannel

public void release() {
	mVideoChannel.release();
	mAudioChannel.release(); // audioRecord释放工作
	native_release();
}

获取音频通道需要样本数(faac的编码器,输出样本 的样本数,才是标准)

public int getInputSamples() {
	return native_getInputSamples(); // native层-->从faacEncOpen中获取到的样本数
}

与native层通讯音频函数

// 下面是音频独有
public native void native_initAudioEncoder(int sampleRate, int numChannels); // 初始化faac音频编码器
public native int native_getInputSamples(); // 获取faac编码器 样本数
public native void native_pushAudio(byte[] bytes); // 把audioRecord采集的原始数据,给native层 编码-->入队---> 发给流媒体服务器

2)AudioChannel

音频通道,处理NdkPusher分发下来的事件和将AudioRecord采集录制音频数据推送到native层。

调用构造函数时,初始化Native层faac音频编码器,初始化AudioRecord麦克风;

public AudioChannel(NdkPusher ndkPusher) {
	this.mNdkPusher = ndkPusher;
	executorService = Executors.newSingleThreadExecutor(); // 单例线程池
	int channelConfig;
	if (channels == 2) {
		channelConfig = AudioFormat.CHANNEL_IN_STEREO; // 双声道
	} else {
		channelConfig = AudioFormat.CHANNEL_IN_MONO; // 单声道
	}
	// 初始化faac音频编码器
	mNdkPusher.native_initAudioEncoder(44100, channels);

	// (getInputSamples单通道样本数1024 * 通道数2)=2048 * 2(一个样本16bit,2字节) = 4096
	inputSamples = mNdkPusher.getInputSamples() * 2;

	// AudioRecord.getMinBufferSize 得到的minBufferSize 能大不能小,最好是 * 2
	int minBufferSize = AudioRecord.getMinBufferSize(44100, channelConfig, AudioFormat.ENCODING_PCM_16BIT) * 2;

	audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, // 安卓手机的麦克风
			44100,  // 采样率
			channelConfig, // 声道数 双声道
			AudioFormat.ENCODING_PCM_16BIT, // 位深 16位 2字节
			Math.max(inputSamples, minBufferSize)); // 缓冲区大小(以字节为单位):max在两者中取最大的,内置缓冲buffsize大一些 没关系的,能大 但是不能小
}

开始直播,修改标记 让其可以进入while 完成音频数据推送, 并开启子线程,子线程:AudioRecord采集录制音频数据,再把此数据传递给 --> native层(进行编码) --> 封包(RTMPPacket) --> 发送

public void startLive() {
	isLive = true;
	executorService.submit(new AudioTask()); // 子线程启动 Runnable(AudioTask)
}

private class AudioTask implements Runnable {
	@Override
	public void run() {
		audioRecord.startRecording(); // 开始录音(调用Android的API录制手机麦克风的声音)

		// 单通道样本数:1024
		// 位深: 16bit位 2字节
		// 声道数:双声道
		// 以上规格:之前说过多遍了,经验值是4096
		// 1024单通道样本数 * 2 * 2 = 4096
		byte[] bytes = new byte[inputSamples]; // 接收录制声音数据的 byte[]
		// 读取数据
		while (isLive) {
			// 每次读多少数据要根据编码器来定!
			int len = audioRecord.read(bytes, 0, bytes.length);
			if (len > 0) {
				// 成功采集到音频数据了
				// 对音频数据进行编码并发送(将编码后的数据push到安全队列中)
				mNdkPusher.native_pushAudio(bytes);
			}
		}
		audioRecord.stop(); // 停止录音
	}
}

停止直播,只修改标记 让其可以不要进入while 就不会再数据推送了

public void stopLive() {
	isLive = false;
}

AudioRecord的释放工作

public void release() {
	if (audioRecord != null) {
		audioRecord.release();
		audioRecord = null;
	}
}

二、FAAC库导入

高级音频编码(Advanced Audio Coding),出现于1997年,基于MPEG-2的音频编码技术,目的是取代MP3格式。2000年,MPEG-4标准出现后,AAC重新集成了其特性,为了区别于传统的MPEG-2 AAC又称为MPEG-4 AAC。相对于mp3,AAC格式的音质更佳,文件更小。

1)复制交叉编译后的faac库/头文件到cpp目录下

2)在CMakeLists导入faac库路径

三、Native层音频编码器

1)初始化faac编码器

在Java层AudioChannel构造函数,通过中转站NdkPusher调用到Native层;

native-lib.cpp:

extern "C"
JNIEXPORT void JNICALL
Java_com_ndk_push_NdkPusher_native_1initAudioEncoder(JNIEnv *env, jobject thiz, jint sample_rate,
                                                     jint num_channels) {
    if (audioChannel) {
        audioChannel->initAudioEncoder(sample_rate, num_channels);
    }
}

AudioChannel.cpp:

void AudioChannel::initAudioEncoder(int sample_rate, int channels) {
    this->mChannels = channels; // 通道数量 2
    /**
     * 44100 采样率
     * 两个声道
     * 16bit 2个字节
     *
     * 上面的规格:
     * 单通道样本数:1024 * 2 = 2048
     *
     * inputSamples = 1024 如果没有 channels 的设计,应该是这个值
     *
     * inputSamples = 2048
     */

    /**
     * 第一步:打开faac编码器
     */
    audioEncoder = faacEncOpen(sample_rate, channels, &inputSamples, &maxOutputBytes);
    if (!audioEncoder) {
        LOGE("打开音频编码器失败");
        return;
    }

    /**
     * 第二步:配置编码器参数
     */
    faacEncConfigurationPtr config = faacEncGetCurrentConfiguration(audioEncoder);

    config->mpegVersion = MPEG4; // mpeg4标准 acc音频标准

    config->aacObjectType = LOW; // LC标准: https://zhidao.baidu.com/question/1948794313899470708.html

    config->inputFormat = FAAC_INPUT_16BIT; // 16bit

    // 比特流输出格式为:Raw
    config->outputFormat = 0;

    // 1发送的时候,就消除 最好的,   2结束后消除回音(复杂)
    // 工作中:最麻烦的就是,(开启降噪, 噪声控制)
    config->useTns = 1;
    config->useLfe = 0;

    /**
     * 第三步:把三面的配置参数,传入进去给faac编码器,  audioEncoder==faac编码器 真正的编码器,可以用的
     */
    int ret = faacEncSetConfiguration(audioEncoder, config);
    if (!ret) { // ret == 0 失败 和 x264 设计 一样
        LOGE("音频编码器参数配置失败");
        return;
    }

    LOGE("FAAC编码器初始化成功...");
    // 输出缓冲区定义
    buffer = new u_char(maxOutputBytes);
}

2)获取 faac的样本数给Java层

native-lib.cpp:

extern "C"
JNIEXPORT jint JNICALL
Java_com_ndk_push_NdkPusher_native_1getInputSamples(JNIEnv *env, jobject thiz) {
    if (audioChannel) {
        return audioChannel->getInputSamples();
    }
    return 0;
}

AudioChannel.cpp:

// 获取faac的样本数
int AudioChannel::getInputSamples() {
    return inputSamples;
}

四、Native层音频推流编码

1)初始化AudioChannel音频通道,并设置 AudioRecord采集录制音频数据推送到native层,audioChannel编码后数据,通过callback回调到native-lib.cpp,加入队列;

native-lib.cpp:

extern "C"
JNIEXPORT void JNICALL
Java_com_ndk_push_NdkPusher_native_1init(JNIEnv *env, jobject thiz) {
    // 初始化 VideoChannel 视频通道
    videoChannel = new VideoChannel();
    // 设置 Camera预览画面的数据推送到native层,videoChannel编码后数据,通过callback回调到native-lib.cpp,加入队列
    videoChannel->setVideoCallback(callback);
    // 初始化 AudioChannel 音频通道
    audioChannel = new AudioChannel();
    // 设置 AudioRecord采集录制音频数据推送到native层,audioChannel编码后数据,通过callback回调到native-lib.cpp,加入队列
    audioChannel->setAudioCallback(callback);
    // 设置 队列的释放工作 回调
    packets.setReleaseCallback(releasePackets);
}

2)使用faac编码器,编码,封包,入队,使用start线程发送给流媒体服务器

在Java层AudioChannel子线程:AudioRecord采集录制音频数据,调用至此;

native-lib.cpp:

extern "C"
JNIEXPORT void JNICALL
Java_com_ndk_push_NdkPusher_native_1pushAudio(JNIEnv *env, jobject thiz, jbyteArray data_) {
    if (!audioChannel || !readyPushing) {
        return;
    }
    jbyte *data = env->GetByteArrayElements(data_, nullptr); // 此data数据就是AudioRecord采集到的原始数据
    audioChannel->encodeData(data); // 核心函数:对音频数据 【进行faac的编码工作】
    env->ReleaseByteArrayElements(data_, data, 0); // 释放byte[]
}

AudioChannel.cpp:

// 使用faac去编码,你必须把上面的告诉人家,  signed char 有符号(在所有音视频里面,最好用 无符号  uint8_t)
void AudioChannel::encodeData(int8_t *data) {
    LOGE("faac编码");
    /**
     * 开始编码
     * 参数1,初始化好的faac编码器
     * 参数2,音频原始数据(无符号的事情)
     * 参数3,初始化好的样本数
     * 参数4,接收成果的 输出 缓冲区
     * 参数5,接收成果的 输出 缓冲区 大小
     * @return:返回编码后数据字节长度
     */
    int byteLen = faacEncEncode(audioEncoder, reinterpret_cast<int32_t *>(data), inputSamples,
                                buffer, maxOutputBytes);
    if (byteLen > 0) {
        LOGE("faac编码 byteLen");
        RTMPPacket *packet = new RTMPPacket;
        // 根据协议设置压缩包数据长度
        int body_size = 2 + byteLen; // 后面的byteLen:我们实际数据编码后的长度
        RTMPPacket_Alloc(packet, body_size); // 堆区实例化里面的成员 packet

        // AF == AAC编码器,44100采样率,位深16bit,双声道
        // AE == AAC编码器,44100采样率,位深16bit,单声道
        packet->m_body[0] = 0xAF; // 双声道
        if (mChannels == 1) {
            packet->m_body[0] = 0xAE; // 单声道
        }

        // 这里是编码出来的音频数据,所以都是 01,  非序列/非头参数
        packet->m_body[1] = 0x01;

        // 音频数据 Copy进去
        memcpy(&packet->m_body[2], buffer, byteLen);

        // 封包处理
        packet->m_packetType = RTMP_PACKET_TYPE_AUDIO; // 包类型,音频
        packet->m_nBodySize = body_size;
        packet->m_nChannel = 11; // 通道ID,随便写一个,注意:不要写的和rtmp.c(里面的m_nChannel有冲突 4301行)
        packet->m_nTimeStamp = -1; // 帧数据有时间戳
        packet->m_hasAbsTimestamp = 0; // 一般都不用
        packet->m_headerType = RTMP_PACKET_SIZE_LARGE; // 大包的类型,如果是头信息,可以给一个小包

        // 把数据包放入队列
        audioCallback(packet);
    }
}

音频编码数据(压缩数据)加入队列后,上一节实现的循环从队列中获取压缩包数据推送到服务端;

void *task_start(void *args) {
    //...
    do {
        //...
        // 从队列里面获取压缩包(视频或音频),直接发给服务器
        while (readyPushing) {
            packets.pop(packet); // 阻塞式
            if (!readyPushing) {
                break;
            }
            // 取不到数据,重新取,可能还没生产出来
            if (!packet) {
                continue;
            }
            // 到这里就是成功的获取队列的ptk了,可以发送给流媒体服务器
            packet->m_nInfoField2 = rtmp->m_stream_id;// 给rtmp的流id
            // 成功取出数据包,发送
            result = RTMP_SendPacket(rtmp, packet, 1); // 1==true 开启内部缓冲
            // packet 你都发给服务器了,可以大胆释放
            releasePackets(&packet);
            if (!result) { // result == 0 和 ffmpeg不同,0代表失败
                LOGE("rtmp 失败 自动断开服务器");
                break;
            }
        }
        releasePackets(&packet); // 只要跳出循环,就释放
    } while (false);
    // =...
    return nullptr;
}

 源码:

NdkPush: 通过RTMP实现推流,直播客户端。

至此,RTMP直播客户端项目已完成。

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

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

相关文章

Openssh 版本升级至8.4

目录 安装包下载地址 zlib包 openssl包 openssh 1、为了防止升级失败登陆不了&#xff0c;所以需要安装telnet 2、检查环境 2.1安装所需的相关组件 2.2备份原来的数据 2.3删除现有的安装sshd的相关软件包 3、下载所需的源码包 3.1编译安装sshd 3.2查看ssh命令的执…

XML文件检索技术:Xpath

纠正&#xff1a;上图中是通过根元素、父元素、子元素… Xpath检索方法及路径&#xff1a; 绝对路径代码示例&#xff1a; 47行&#xff1a;Xpath解析技术也是基于Dom4J的技术&#xff1b; 52行&#xff1a;List<Node> 创建Node类型的集合nameNodes&#xff0c;selec…

大数据应用开发--概述

大数据应用开发–概述 1. 大数据应用开发简介 1.1 数据分析的概念 数据分析就是利用数学、统计学理论相结合科学统计分析方法对数据库中的数据、Excel数据、收集的大量数据、网页抓取的数据进行分析&#xff0c;从中提取有价值的信息形成结论并进行展示的过程。 数据分析的目…

没想到大厂Adobe还有这些“猫腻”!

北京时间周四晚间&#xff0c;图像及视频生产力工具大厂Adobe发布公告&#xff0c;宣布旗下的视频创作应用Premiere Pro将喜提一系列新的AI功能。这也是Adobe上个月发布AIGC创作功能“萤火虫”后的最新动作。综合Adobe的官方公告和演示视频&#xff0c;最大亮点就是基于文字的视…

生存函数(Survival function)

文章目录1. 定义2. 生存函数的例子3. 参数生存函数3.1 指数生存函数&#xff08;Exponential survival function&#xff09;3.2 威布尔生存函数&#xff08;Weibull survival function&#xff09;3.3 其他参数生存函数4. 非参数生存函数5. 性质6. Kaplan–Meier estimator6.1…

总结824

学习目标&#xff1a; 4月&#xff08;复习完高数18讲内容&#xff0c;背诵21篇短文&#xff0c;熟词僻义300词基础词&#xff09; 学习内容&#xff1a; 英语&#xff1a;早上 读了《nasty place》&#xff0c;单词150个 高数&#xff1a;看了12讲二重积分的内容&#xff0…

算法设计与智能计算 || 专题六: 不可导凸函数的最优解搜索问题

不可导凸函数的最优解搜索问题 文章目录不可导凸函数的最优解搜索问题1. 次梯度下降方法1.1 基于次梯度的 Lasso 回归求解1.2 次梯度求解 Lasso 算法1.3 编程实现2. 软阈值方法2.1 软阈值求解Lasso回归1. 次梯度下降方法 如目标函数包含不可微分的部分&#xff0c;形如 E(w)1N…

计组2.3——浮点数的表示和运算

计组2.3 浮点数 #mermaid-svg-hwjyO2bt7hFXy1eD {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-hwjyO2bt7hFXy1eD .error-icon{fill:#552222;}#mermaid-svg-hwjyO2bt7hFXy1eD .error-text{fill:#552222;stroke:#552…

视频美颜sdk的开发流程与注意事项

目前&#xff0c;视频美颜技术逐渐成为了人们关注的焦点。而视频美颜sdk作为实现视频美颜的重要工具&#xff0c;也因此备受关注。本文将从视频美颜sdk的开发流程和注意事项两个方面进行探讨。 一、视频美颜sdk的开发流程 1、确定需求 在进行视频美颜sdk的开发之前&#xff0…

Solon v2.2.10 发布,助力信创国产化

Solon 是一个高效的 Java 应用开发框架&#xff1a;更快、更小、更简单。它不是 Spring、没有使用 Servlet、JavaEE 接口&#xff0c;是一个有自己接口标准的开放生态。可以为应用软件国产化提供支持&#xff0c;助力信创建设。 150来个生态插件&#xff0c;覆盖各种不同的应用…

天猫数据分析:饮料市场头部份额下滑,无糖饮料占比40%

如今&#xff0c;全世界减糖、控糖的大趋势已经拉开帷幕。 根据沸点测评数据&#xff0c;今年所有在新加坡销售的饮料&#xff0c;必须在包装上注明A、B、C或D的营养等级标签&#xff0c;列明饮料含糖分和饱和脂肪的百分比&#xff0c;营养等级为D的饮品则会被禁止做广告营销。…

Tinymce富文本编辑器在vue项目中的使用;引入第三方插件和上传视频、图片等

先放张效果图第一步&#xff1a;安装依赖 npm install tinymce5.0.12第二步&#xff1a;在项目中的public文件夹中新建tinymce文件夹&#xff08;因为我的项目是脚手架创建的&#xff0c;所以公共文件夹是public&#xff09;&#xff1b;在node_modules中找到skins文件夹复制到…

插件化换肤原理—— 布局加载过程、View创建流程、Resources 浅析

作者&#xff1a;孙先森Blog 本文主要分析了 Android 布局加载流程 分析 一般的换肤功能大概是这样的&#xff1a;在 App 的皮肤商城内下载“皮肤包”&#xff0c;下载完成后点击更换界面上的 View 相关资源&#xff08;颜色、样式、图片、背景等&#xff09;发生改变&#xf…

栈的实现及相关OJ题

&#x1f389;&#x1f389;&#x1f389;点进来你就是我的人了 博主主页&#xff1a;&#x1f648;&#x1f648;&#x1f648;戳一戳,欢迎大佬指点!人生格言&#xff1a;当你的才华撑不起你的野心的时候,你就应该静下心来学习! 欢迎志同道合的朋友一起加油喔&#x1f9be;&am…

28岁,他是如何成为上市公司测试总监的

现在的大环境下&#xff0c;各行各业都开始内卷起来&#xff0c;测试也不例外&#xff0c;企业要求也越来越高&#xff0c;“会代码”逐渐成为测试工程师的一个标签。你要想拿到一个不错的薪资&#xff0c;必不可少的一个技能—自动化测试&#xff0c;自动化测试难吗&#xff1…

Oracle集合查询详解加练习题

#集合查询 概念&#xff1a;将不同的数据集合&#xff08;SQL查询语句&#xff09;按照集合的规则&#xff0c;拼接一个临时的&#xff0c;新的数据集合&#xff08;表&#xff09; 1.集合&#xff1a;并集、交集、差集 并集 union all 语法&#xff1a;select column1,COLUM…

「MongoDB」时序数据库和MongoDB第二部分-模式设计最佳实践

在上一篇博客文章时间序列数据与MongoDB&#xff1a;第一部分-简介中&#xff0c;我们介绍了时间序列数据的概念&#xff0c;然后介绍了一些可以用于帮助收集时间序列应用程序需求的发现问题。对这些问题的回答有助于指导支持大容量生产应用程序部署所需的模式和MongoDB数据库配…

基于深度学习PaddleOcr身份证识别

之前使用opencv机械学习处理图片&#xff0c;使用Testseract-OCR进行身份证和姓名识别&#xff0c;发现受背景图片的影响比较大&#xff0c;转PaddleOcr&#xff0c;识别成功率能达到使用要求。 PaddleOcr官网地址&#xff1a;飞桨PaddlePaddle-源于产业实践的开源深度学习平台…

Python爬虫-某跨境电商(AM)搜索热词

前言 本文是该专栏的第42篇,后面会持续分享python爬虫干货知识,记得关注。 关于某跨境电商(AM),本专栏前面有单独详细介绍过,获取配送地的cookie信息以及商品库存数据,感兴趣的同学可往前翻阅。 1. python爬虫|爬取某跨境电商AM的商品库存数据(Selenium实战) 2. Seleni…

偶数科技发布实时湖仓数据平台 Skylab 5.0

2023 年 4 月 11 日&#xff0c; 偶数发布了最新的实时湖仓数据平台 Skylab 5.0&#xff0c;平台各个组件进行了全面升级。新版 Skylab 的发布标志着偶数科技具有从数据存储、计算、管理到分析、应用和 AI 能力的完整的数据管理生态闭环&#xff0c;帮助用户实现批流一体、实时…