在之前完成的实战项目【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字节) = 4096inputSamples = mNdkPusher.getInputSamples() * 2;// AudioRecord.getMinBufferSize 得到的minBufferSize 能大不能小,最好是 * 2int 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 {@Overridepublic void run() {audioRecord.startRecording(); // 开始录音(调用Android的API录制手机麦克风的声音)// 单通道样本数:1024// 位深: 16bit位 2字节// 声道数:双声道// 以上规格:之前说过多遍了,经验值是4096// 1024单通道样本数 * 2 * 2 = 4096byte[] 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.htmlconfig->inputFormat = FAAC_INPUT_16BIT; // 16bit// 比特流输出格式为:Rawconfig->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直播客户端项目已完成。

NDK RTMP直播客户端三相关推荐

  1. NDK RTMP直播客户端二

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

  2. 直播时代:让IOS普通开发者一天内做出一个RTMP直播客户端,并且带有美艳直播功能。(文章最下面有github源码地址)...

    2019独角兽企业重金招聘Python工程师标准>>> 包含一下功能: 1, 提供IOS苹果手机的RTMP推流: 填写RTMP服务地址,直接就可以进行推流. 2,美颜直播 美不美都能 ...

  3. 直播软件开发IOS直播客户端SDK,视频直播APP源码【开源】

    当前视频直播非常火爆,手机端的视频直播也非常火爆,PGC.UGC的视频直播门槛都降低了很多. 本文介绍一个:IOS 客户端直播的SDK,代码完全开源. 直播时代:让IOS普通开发者一天内做出一个RTM ...

  4. 直播时代--IOS直播客户端SDK,美颜直播【开源】

    当前视频直播非常火爆,手机端的视频直播也非常火爆,PGC.UGC的视频直播门槛都降低了很多. 本文介绍一个:IOS 客户端直播的SDK,代码完全开源. 直播时代:让IOS普通开发者一天内做出一个RTM ...

  5. Android流媒体开发之路二:NDK C++开发Android端RTMP直播推流程序

    经过一番折腾,成功把RTMP直播推流代码,通过NDK交叉编译的方式,移植到了Android下,从而实现了Android端采集摄像头和麦克缝数据,然后进行h264视频编码和aac音频编码,并发送到RTM ...

  6. 【Android RTMP】RTMP 直播推流阶段总结 ( 服务器端搭建 | Android 手机端编码推流 | 电脑端观看直播 | 服务器状态查看 )

    文章目录 安卓直播推流专栏博客总结 一. 服务器搭建 二. 手机端推流 三. 电脑端观看直播 四. RTMP 服务器端状态 安卓直播推流专栏博客总结 Android RTMP 直播推流技术专栏 : 0 ...

  7. 【Android RTMP】RTMP 直播推流服务器搭建 ( Ubuntu 18.04.4 虚拟机 )

    文章目录 安卓直播推流专栏博客总结 一. Android RTMP 直播推流简介 二. Nginx.RTMP Module 编译环境源码准备 三. pcre.OpenSSL.zlib 函数库安装 四. ...

  8. 实时视频直播客户端技术盘点:Native、HTML5、WebRTC、微信小程序

    1.视频直播客户端技术之Native APP 原生 APP 终端音视频引擎的结构框图如下,基本包括了音频引擎.视频引擎和网络传输,合称实时语音视频终端引擎.这里还包含底层的音视频采集和渲染,还有网络的 ...

  9. C++ RTMP直播流播放器

    抛开flash,自己开发实现C++ RTMP直播流播放器 众所周知,RTMP是以flash为客户端播放器的直播协议,主要应用在B/S形式的场景中.本人研究并用C++开发实现了RTMP直播流协议的播放器 ...

最新文章

  1. python找出图中所有闭合环_求图中的所有闭合环
  2. 为什么我那么努力,模电还是学不懂?
  3. 深度图压缩之-高低8位拆分保存
  4. Mybatis多参数封装到map中,多条件查询
  5. mysql本身主从_Mysql主从复制
  6. k8s容器内的东西复制出来_容器 | Docker 如此之好,你为什么还要用k8s
  7. 拓端tecdat|R语言互联网金融下的中国保险业数据分析
  8. 软件测试——文档测试
  9. vue使用 Tinymce富文本编辑器
  10. 夏昕ibatisiBATIS 2.0 开发指南配置文件说明
  11. MYSQL的随机函数
  12. 如何用人工智能自动玩游戏
  13. 服务器开机硬盘raid连接错误,服务器磁盘阵列常见问题及解决方法
  14. 屏幕和摄像头中的视频分辨率P,I,K,MP表示的含义,720p,1080p,2k,5MP
  15. Navigation Controller 的常用操作
  16. 报错:The server time zone value ‘�й���׼ʱ��‘ is unrecognied
  17. JAVA -- NPOI在excel中画直线
  18. 牛客网——求最小公倍数
  19. 使用Jedis模糊删除redis集群key
  20. 学校教师计算机培训总结,2019学校教师培训工作总结范文

热门文章

  1. win7ue4崩溃问题汇总
  2. Oracle Study学习之--Flashback Archive
  3. 数据库Mysql自增锁问题原来可以这么解决
  4. fiddler查看console
  5. 人性的弱点的自我窥视
  6. 龙尚 U9300C wvdial 拨号上网
  7. mysql 二次方函数_MySQL函数集锦
  8. 2023爱分析·商业智能应用解决方案市场厂商评估报告:数聚股份
  9. 电子工程师也有“鄙视链”!软件硬件无一幸免!
  10. Menlow上网本可畅享雷神之锤