当前位置:   article > 正文

NDK RTMP直播客户端三_rtmp客户端

rtmp客户端

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

直播客户端系列:

NDK RTMP直播客户端一

NDK RTMP直播客户端二

NDK 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,码率等),
③:实例化音频通道

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

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

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

  1. public void startLive(String path) {
  2. native_start(path);
  3. mVideoChannel.startLive();
  4. mAudioChannel.startLive();
  5. }

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

  1. public void stopLive() {
  2. mVideoChannel.stopLive();
  3. mAudioChannel.stopLive();
  4. native_stop();
  5. }

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

  1. public void release() {
  2. mVideoChannel.release();
  3. mAudioChannel.release(); // audioRecord释放工作
  4. native_release();
  5. }

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

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

与native层通讯音频函数

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

2)AudioChannel

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

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

  1. public AudioChannel(NdkPusher ndkPusher) {
  2. this.mNdkPusher = ndkPusher;
  3. executorService = Executors.newSingleThreadExecutor(); // 单例线程池
  4. int channelConfig;
  5. if (channels == 2) {
  6. channelConfig = AudioFormat.CHANNEL_IN_STEREO; // 双声道
  7. } else {
  8. channelConfig = AudioFormat.CHANNEL_IN_MONO; // 单声道
  9. }
  10. // 初始化faac音频编码器
  11. mNdkPusher.native_initAudioEncoder(44100, channels);
  12. // (getInputSamples单通道样本数1024 * 通道数2)=2048 * 2(一个样本16bit,2字节) = 4096
  13. inputSamples = mNdkPusher.getInputSamples() * 2;
  14. // AudioRecord.getMinBufferSize 得到的minBufferSize 能大不能小,最好是 * 2
  15. int minBufferSize = AudioRecord.getMinBufferSize(44100, channelConfig, AudioFormat.ENCODING_PCM_16BIT) * 2;
  16. audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, // 安卓手机的麦克风
  17. 44100, // 采样率
  18. channelConfig, // 声道数 双声道
  19. AudioFormat.ENCODING_PCM_16BIT, // 位深 16位 2字节
  20. Math.max(inputSamples, minBufferSize)); // 缓冲区大小(以字节为单位):max在两者中取最大的,内置缓冲buffsize大一些 没关系的,能大 但是不能小
  21. }

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

  1. public void startLive() {
  2. isLive = true;
  3. executorService.submit(new AudioTask()); // 子线程启动 Runnable(AudioTask)
  4. }
  5. private class AudioTask implements Runnable {
  6. @Override
  7. public void run() {
  8. audioRecord.startRecording(); // 开始录音(调用Android的API录制手机麦克风的声音)
  9. // 单通道样本数:1024
  10. // 位深: 16bit位 2字节
  11. // 声道数:双声道
  12. // 以上规格:之前说过多遍了,经验值是4096
  13. // 1024单通道样本数 * 2 * 2 = 4096
  14. byte[] bytes = new byte[inputSamples]; // 接收录制声音数据的 byte[]
  15. // 读取数据
  16. while (isLive) {
  17. // 每次读多少数据要根据编码器来定!
  18. int len = audioRecord.read(bytes, 0, bytes.length);
  19. if (len > 0) {
  20. // 成功采集到音频数据了
  21. // 对音频数据进行编码并发送(将编码后的数据push到安全队列中)
  22. mNdkPusher.native_pushAudio(bytes);
  23. }
  24. }
  25. audioRecord.stop(); // 停止录音
  26. }
  27. }

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

  1. public void stopLive() {
  2. isLive = false;
  3. }

AudioRecord的释放工作

  1. public void release() {
  2. if (audioRecord != null) {
  3. audioRecord.release();
  4. audioRecord = null;
  5. }
  6. }

二、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:

  1. extern "C"
  2. JNIEXPORT void JNICALL
  3. Java_com_ndk_push_NdkPusher_native_1initAudioEncoder(JNIEnv *env, jobject thiz, jint sample_rate,
  4. jint num_channels) {
  5. if (audioChannel) {
  6. audioChannel->initAudioEncoder(sample_rate, num_channels);
  7. }
  8. }

AudioChannel.cpp:

  1. void AudioChannel::initAudioEncoder(int sample_rate, int channels) {
  2. this->mChannels = channels; // 通道数量 2
  3. /**
  4. * 44100 采样率
  5. * 两个声道
  6. * 16bit 2个字节
  7. *
  8. * 上面的规格:
  9. * 单通道样本数:1024 * 2 = 2048
  10. *
  11. * inputSamples = 1024 如果没有 channels 的设计,应该是这个值
  12. *
  13. * inputSamples = 2048
  14. */
  15. /**
  16. * 第一步:打开faac编码器
  17. */
  18. audioEncoder = faacEncOpen(sample_rate, channels, &inputSamples, &maxOutputBytes);
  19. if (!audioEncoder) {
  20. LOGE("打开音频编码器失败");
  21. return;
  22. }
  23. /**
  24. * 第二步:配置编码器参数
  25. */
  26. faacEncConfigurationPtr config = faacEncGetCurrentConfiguration(audioEncoder);
  27. config->mpegVersion = MPEG4; // mpeg4标准 acc音频标准
  28. config->aacObjectType = LOW; // LC标准: https://zhidao.baidu.com/question/1948794313899470708.html
  29. config->inputFormat = FAAC_INPUT_16BIT; // 16bit
  30. // 比特流输出格式为:Raw
  31. config->outputFormat = 0;
  32. // 1发送的时候,就消除 最好的, 2结束后消除回音(复杂)
  33. // 工作中:最麻烦的就是,(开启降噪, 噪声控制)
  34. config->useTns = 1;
  35. config->useLfe = 0;
  36. /**
  37. * 第三步:把三面的配置参数,传入进去给faac编码器, audioEncoder==faac编码器 真正的编码器,可以用的
  38. */
  39. int ret = faacEncSetConfiguration(audioEncoder, config);
  40. if (!ret) { // ret == 0 失败 和 x264 设计 一样
  41. LOGE("音频编码器参数配置失败");
  42. return;
  43. }
  44. LOGE("FAAC编码器初始化成功...");
  45. // 输出缓冲区定义
  46. buffer = new u_char(maxOutputBytes);
  47. }

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

native-lib.cpp:

  1. extern "C"
  2. JNIEXPORT jint JNICALL
  3. Java_com_ndk_push_NdkPusher_native_1getInputSamples(JNIEnv *env, jobject thiz) {
  4. if (audioChannel) {
  5. return audioChannel->getInputSamples();
  6. }
  7. return 0;
  8. }

AudioChannel.cpp:

  1. // 获取faac的样本数
  2. int AudioChannel::getInputSamples() {
  3. return inputSamples;
  4. }

四、Native层音频推流编码

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

native-lib.cpp:

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

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

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

native-lib.cpp:

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

AudioChannel.cpp:

  1. // 使用faac去编码,你必须把上面的告诉人家, signed char 有符号(在所有音视频里面,最好用 无符号 uint8_t)
  2. void AudioChannel::encodeData(int8_t *data) {
  3. LOGE("faac编码");
  4. /**
  5. * 开始编码
  6. * 参数1,初始化好的faac编码器
  7. * 参数2,音频原始数据(无符号的事情)
  8. * 参数3,初始化好的样本数
  9. * 参数4,接收成果的 输出 缓冲区
  10. * 参数5,接收成果的 输出 缓冲区 大小
  11. * @return:返回编码后数据字节长度
  12. */
  13. int byteLen = faacEncEncode(audioEncoder, reinterpret_cast<int32_t *>(data), inputSamples,
  14. buffer, maxOutputBytes);
  15. if (byteLen > 0) {
  16. LOGE("faac编码 byteLen");
  17. RTMPPacket *packet = new RTMPPacket;
  18. // 根据协议设置压缩包数据长度
  19. int body_size = 2 + byteLen; // 后面的byteLen:我们实际数据编码后的长度
  20. RTMPPacket_Alloc(packet, body_size); // 堆区实例化里面的成员 packet
  21. // AF == AAC编码器,44100采样率,位深16bit,双声道
  22. // AE == AAC编码器,44100采样率,位深16bit,单声道
  23. packet->m_body[0] = 0xAF; // 双声道
  24. if (mChannels == 1) {
  25. packet->m_body[0] = 0xAE; // 单声道
  26. }
  27. // 这里是编码出来的音频数据,所以都是 01, 非序列/非头参数
  28. packet->m_body[1] = 0x01;
  29. // 音频数据 Copy进去
  30. memcpy(&packet->m_body[2], buffer, byteLen);
  31. // 封包处理
  32. packet->m_packetType = RTMP_PACKET_TYPE_AUDIO; // 包类型,音频
  33. packet->m_nBodySize = body_size;
  34. packet->m_nChannel = 11; // 通道ID,随便写一个,注意:不要写的和rtmp.c(里面的m_nChannel有冲突 4301行)
  35. packet->m_nTimeStamp = -1; // 帧数据有时间戳
  36. packet->m_hasAbsTimestamp = 0; // 一般都不用
  37. packet->m_headerType = RTMP_PACKET_SIZE_LARGE; // 大包的类型,如果是头信息,可以给一个小包
  38. // 把数据包放入队列
  39. audioCallback(packet);
  40. }
  41. }

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

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

 源码:

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

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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小蓝xlanll/article/detail/196003
推荐阅读
相关标签
  

闽ICP备14008679号