Android NDK 直播推流与引流

2022-12-21,,,

本篇介绍一下直播技术中推流与引流的简单实现。

1.流媒体服务器测试

首先利用快直播 app (其他支持 RTMP 推流与引流的 app 亦可)和 ffplay.exe 对流媒体服务器进行测试。

快直播 app 下载地址:

https://apkpure.biz/cn.nodemedia.qlive/快直播

快直播的推流界面和引流界面:

Windows 下利用 ffplay 进行引流,命令行执行:

ffplay rtmp://192.168.0.0/live/test
# ip 地址换成流媒体服务器的地址, test 表示直播房间号

测试结果:

2.推流

本文直播推流步骤:

使用 AudioRecord 采集音频,使用 Camera API 采集视频数据
分别使用 faac 和 xh264 第三方库在 Native 层对音频和视频进行编码
利用 rtmp-dump 第三方库进行打包和推流

工程目录:

主要的 JNI 方法:

public class NativePush {

    public native void startPush(String url);

    public native void stopPush();

    public native void release();

    /**
* 设置视频参数
* @param width
* @param height
* @param bitrate
* @param fps
*/
public native void setVideoOptions(int width, int height, int bitrate, int fps); /**
* 设置音频参数
* @param sampleRateInHz
* @param channel
*/
public native void setAudioOptions(int sampleRateInHz, int channel); /**
* 发送视频数据
* @param data
*/
public native void fireVideo(byte[] data); /**
* 发送音频数据
* @param data
* @param len
*/
public native void fireAudio(byte[] data, int len); }

视频采集

视频采集主要基于 Camera 相关 API ,利用 SurfaceView 进行预览,通过 PreviewCallback 获取相机预览数据。

视频预览主要代码实现:

  public void startPreview(){
try {
mCamera = Camera.open(mVideoParams.getCameraId());
Camera.Parameters param = mCamera.getParameters(); List<Camera.Size> previewSizes = param.getSupportedPreviewSizes();
int length = previewSizes.size();
for (int i = 0; i < length; i++) {
Log.i(TAG, "SupportedPreviewSizes : " + previewSizes.get(i).width + "x" + previewSizes.get(i).height);
} mVideoParams.setWidth(previewSizes.get(0).width);
mVideoParams.setHeight(previewSizes.get(0).height); param.setPreviewFormat(ImageFormat.NV21);
param.setPreviewSize(mVideoParams.getWidth(), mVideoParams.getHeight()); mCamera.setParameters(param);
//mCamera.setDisplayOrientation(90); // 竖屏
mCamera.setPreviewDisplay(mSurfaceHolder); buffer = new byte[mVideoParams.getWidth() * mVideoParams.getHeight() * 4];
mCamera.addCallbackBuffer(buffer);
mCamera.setPreviewCallbackWithBuffer(this); mCamera.startPreview();
} catch (IOException e) {
e.printStackTrace();
}
}

利用 FrameCallback 获取预览数据传入 Native 层,然后进行编码:


@Override
public void onPreviewFrame(byte[] bytes, Camera camera) {
if (mCamera != null) {
mCamera.addCallbackBuffer(buffer);
} if (mIsPushing) {
mNativePush.fireVideo(bytes);
} } </pre> **音频采集** 音频采集基于 AudioRecord 实现,在一个子线程采集音频 PCM 数据,并将数据不断传入 Native 层进行编码。 <pre style="margin: 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; word-wrap: break-word !important; font-size: inherit; color: inherit; line-height: inherit;"> private class AudioRecordRunnable implements Runnable { @Override
public void run() {
mAudioRecord.startRecording();
while (mIsPushing) {
//通过AudioRecord不断读取音频数据
byte[] buffer = new byte[mMinBufferSize];
int length = mAudioRecord.read(buffer, 0, buffer.length);
if (length > 0) {
//传递给 Native 代码,进行音频编码
mNativePush.fireAudio(buffer, length);
}
}
}
}

编码和推流

音视频数据编码和推流在 Native 层实现,首先添加 faac , x264 , librtmp 第三方库到 AS 工程,然后初始化相关设置,基于生产者与消费者模式,将编码后的音视频数据,在生产者线程中打包 RTMPPacket 放入双向链表,在消费者线程中从链表中取 RTMPPacket ,通过 RTMP_SendPacket 方法发送给服务器。

x264 初始化:


JNIEXPORT void JNICALL
Java_com_haohao_live_jni_NativePush_setVideoOptions(JNIEnv *env, jobject instance, jint width,
jint height, jint bitRate, jint fps) { x264_param_t param;
//x264_param_default_preset 设置
x264_param_default_preset(&param, "ultrafast", "zerolatency");
//编码输入的像素格式YUV420P
param.i_csp = X264_CSP_I420;
param.i_width = width;
param.i_height = height; y_len = width * height;
u_len = y_len / 4;
v_len = u_len; //参数i_rc_method表示码率控制,CQP(恒定质量),CRF(恒定码率),ABR(平均码率)
//恒定码率,会尽量控制在固定码率
param.rc.i_rc_method = X264_RC_CRF;
param.rc.i_bitrate = bitRate / 1000; //* 码率(比特率,单位Kbps)
param.rc.i_vbv_max_bitrate = bitRate / 1000 * 1.2; //瞬时最大码率 //码率控制不通过timebase和timestamp,而是fps
param.b_vfr_input = 0;
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;
param.i_threads = 1;//并行编码线程数量,0默认为多线程 //是否把SPS和PPS放入每一个关键帧
//SPS Sequence Parameter Set 序列参数集,PPS Picture Parameter Set 图像参数集
//为了提高图像的纠错能力
param.b_repeat_headers = 1;
//设置Level级别
param.i_level_idc = 51;
//设置Profile档次
//baseline级别,没有B帧,只有 I 帧和 P 帧
x264_param_apply_profile(&param, "baseline"); //x264_picture_t(输入图像)初始化
x264_picture_alloc(&pic_in, param.i_csp, param.i_width, param.i_height);
pic_in.i_pts = 0;
//打开编码器
video_encode_handle = x264_encoder_open(&param);
if (video_encode_handle) {
LOGI("打开视频编码器成功");
} else {
throwNativeError(env, INIT_FAILED);
}
}

faac 初始化:


JNIEXPORT void JNICALL
Java_com_byteflow_live_jni_NativePush_setAudioOptions(JNIEnv *env, jobject instance,
jint sampleRateInHz, jint channel) {
audio_encode_handle = faacEncOpen(sampleRateInHz, channel, &nInputSamples,
&nMaxOutputBytes);
if (!audio_encode_handle) {
LOGE("音频编码器打开失败");
return;
}
//设置音频编码参数
faacEncConfigurationPtr p_config = faacEncGetCurrentConfiguration(audio_encode_handle);
p_config->mpegVersion = MPEG4;
p_config->allowMidside = 1;
p_config->aacObjectType = LOW;
p_config->outputFormat = 0; //输出是否包含ADTS头
p_config->useTns = 1; //时域噪音控制,大概就是消爆音
p_config->useLfe = 0;
// p_config->inputFormat = FAAC_INPUT_16BIT;
p_config->quantqual = 100;
p_config->bandWidth = 0; //频宽
p_config->shortctl = SHORTCTL_NORMAL; if (!faacEncSetConfiguration(audio_encode_handle, p_config)) {
LOGE("%s", "音频编码器配置失败..");
throwNativeError(env, INIT_FAILED);
return;
} LOGI("%s", "音频编码器配置成功");
}

对视频数据进行编码打包,通过 add_rtmp_packet 放入链表:


JNIEXPORT void JNICALL
Java_com_byteflow_live_jni_NativePush_fireVideo(JNIEnv *env, jobject instance, jbyteArray buffer_) {
//视频数据转为YUV420P
//NV21->YUV420P
jbyte *nv21_buffer = (*env)->GetByteArrayElements(env, buffer_, NULL);
jbyte *u = pic_in.img.plane[1];
jbyte *v = pic_in.img.plane[2];
//nv21 4:2:0 Formats, 12 Bits per Pixel
//nv21与yuv420p,y个数一致,uv位置对调
//nv21转yuv420p y = w*h,u/v=w*h/4
//nv21 = yvu yuv420p=yuv y=y u=y+1+1 v=y+1
//如果要进行图像处理(美颜),可以再转换为RGB
//还可以结合OpenCV识别人脸等等
memcpy(pic_in.img.plane[0], nv21_buffer, y_len);
int i;
for (i = 0; i < u_len; i++) {
*(u + i) = *(nv21_buffer + y_len + i * 2 + 1);
*(v + i) = *(nv21_buffer + y_len + i * 2);
} //h264编码得到NALU数组
x264_nal_t *nal = NULL; //NAL
int n_nal = -1; //NALU的个数
//进行h264编码
if (x264_encoder_encode(video_encode_handle, &nal, &n_nal, &pic_in, &pic_out) < 0) {
LOGE("%s", "编码失败");
return;
}
//使用rtmp协议将h264编码的视频数据发送给流媒体服务器
//帧分为关键帧和普通帧,为了提高画面的纠错率,关键帧应包含SPS和PPS数据
int sps_len, pps_len;
unsigned char sps[100];
unsigned char pps[100];
memset(sps, 0, 100);
memset(pps, 0, 100);
pic_in.i_pts += 1; //顺序累加
//遍历NALU数组,根据NALU的类型判断
for (i = 0; i < n_nal; i++) {
if (nal[i].i_type == NAL_SPS) {
//复制SPS数据,序列参数集(Sequence parameter set)
sps_len = nal[i].i_payload - 4;
memcpy(sps, nal[i].p_payload + 4, sps_len); //不复制四字节起始码
} else if (nal[i].i_type == NAL_PPS) {
//复制PPS数据,图像参数集(Picture parameter set)
pps_len = nal[i].i_payload - 4;
memcpy(pps, nal[i].p_payload + 4, pps_len); //不复制四字节起始码 //发送序列信息
//h264关键帧会包含SPS和PPS数据
add_264_sequence_header(pps, sps, pps_len, sps_len); } else {
//发送帧信息
add_264_body(nal[i].p_payload, nal[i].i_payload);
} } (*env)->ReleaseByteArrayElements(env, buffer_, nv21_buffer, 0);
}

同样,对音频数据进行编码打包放入链表:


JNIEXPORT void JNICALL
Java_com_byteflow_live_jni_NativePush_fireAudio(JNIEnv *env, jobject instance, jbyteArray buffer_,
jint length) {
int *pcmbuf;
unsigned char *bitbuf;
jbyte *b_buffer = (*env)->GetByteArrayElements(env, buffer_, 0);
pcmbuf = (short *) malloc(nInputSamples * sizeof(int));
bitbuf = (unsigned char *) malloc(nMaxOutputBytes * sizeof(unsigned char));
int nByteCount = 0;
unsigned int nBufferSize = (unsigned int) length / 2;
unsigned short *buf = (unsigned short *) b_buffer;
while (nByteCount < nBufferSize) {
int audioLength = nInputSamples;
if ((nByteCount + nInputSamples) >= nBufferSize) {
audioLength = nBufferSize - nByteCount;
}
int i;
for (i = 0; i < audioLength; i++) {//每次从实时的pcm音频队列中读出量化位数为8的pcm数据。
int s = ((int16_t *) buf + nByteCount)[i];
pcmbuf[i] = s << 8;//用8个二进制位来表示一个采样量化点(模数转换)
}
nByteCount += nInputSamples;
//利用FAAC进行编码,pcmbuf为转换后的pcm流数据,audioLength为调用faacEncOpen时得到的输入采样数,bitbuf为编码后的数据buff,nMaxOutputBytes为调用faacEncOpen时得到的最大输出字节数
int byteslen = faacEncEncode(audio_encode_handle, pcmbuf, audioLength,
bitbuf, nMaxOutputBytes);
if (byteslen < 1) {
continue;
}
add_aac_body(bitbuf, byteslen);//从bitbuf中得到编码后的aac数据流,放到数据队列
}
if (bitbuf)
free(bitbuf);
if (pcmbuf)
free(pcmbuf); (*env)->ReleaseByteArrayElements(env, buffer_, b_buffer, 0);
}

消费者线程不断从链表中取 RTMPPacket 发送给服务器:


void *push_thread(void *arg) {
JNIEnv *env;//获取当前线程JNIEnv
(*javaVM)->AttachCurrentThread(javaVM, &env, NULL); //建立RTMP连接
RTMP *rtmp = RTMP_Alloc();
if (!rtmp) {
LOGE("rtmp初始化失败");
goto end;
}
RTMP_Init(rtmp);
rtmp->Link.timeout = 5; //连接超时的时间
//设置流媒体地址
RTMP_SetupURL(rtmp, rtmp_path);
//发布rtmp数据流
RTMP_EnableWrite(rtmp);
//建立连接
if (!RTMP_Connect(rtmp, NULL)) {
LOGE("%s", "RTMP 连接失败");
throwNativeError(env, CONNECT_FAILED);
goto end;
}
//计时
start_time = RTMP_GetTime();
if (!RTMP_ConnectStream(rtmp, 0)) { //连接流
LOGE("%s", "RTMP ConnectStream failed");
throwNativeError(env, CONNECT_FAILED);
goto end;
}
is_pushing = TRUE;
//发送AAC头信息
add_aac_sequence_header(); while (is_pushing) {
//发送
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond, &mutex);
//取出队列中的RTMPPacket
RTMPPacket *packet = queue_get_first();
if (packet) {
queue_delete_first(); //移除
packet->m_nInfoField2 = rtmp->m_stream_id; //RTMP协议,stream_id数据
int i = RTMP_SendPacket(rtmp, packet, TRUE); //TRUE放入librtmp队列中,并不是立即发送
if (!i) {
LOGE("RTMP 断开");
RTMPPacket_Free(packet);
pthread_mutex_unlock(&mutex);
goto end;
} else {
LOGI("%s", "rtmp send packet");
}
RTMPPacket_Free(packet);
} pthread_mutex_unlock(&mutex);
}
end:
LOGI("%s", "释放资源");
free(rtmp_path);
RTMP_Close(rtmp);
RTMP_Free(rtmp);
(*javaVM)->DetachCurrentThread(javaVM);
return 0;
}

3.引流

这里引流就不做展开讲,可以通过 QLive 的 SDK 或者 vitamio 等第三方库实现。

基于 vitamio 实现引流:


private void init(){
mVideoView = (VideoView) findViewById(R.id.live_player_view);
mVideoView.setVideoPath(SPUtils.getInstance(this).getString(SPUtils.KEY_NGINX_SER_URI));
mVideoView.setMediaController(new MediaController(this));
mVideoView.requestFocus(); mVideoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
mp.setPlaybackSpeed(1.0f);
}
}); }

下面,高能的地方来了!有幸从一位字节跳动大神那里得到他本人吐血整理的“582页Android NDK七大模块学习宝典”,从原理到实战,一应俱全!

秉承好东西的当然要共享的原则,今天就来秀一把,试试这“582页Android NDK七大模块学习宝典”是否也能让你事半功倍!这份宝典主要涉及以下几个方面:

NDK 模块开发
JNI 模块
Native 开发工具
Linux 编程
底层图片处理
音视频开发
机器学习

笔记内容全部免费分享,有需要完整版笔记的小伙伴【点击我】免费获取哦!

一、NDK 模块开发

主要内容:

C++与 C#数据类型总结
C 与 C++之内存结构与管理
C 与 C++之预处理命令与用 typedef 命名已有类型
C 与 C++之结构体、共用体
C 与 C++之指针
C/C++ 之多线程机制
C/C++ 之函数与初始化列表

二、JNI 模块

主要内容:

JNI 开发之 静态注册与动态注册

静态注册、动态注册、JNINativeMethod、数据类型映射、jni 函数默认参数

JNI 开发之方法签名与 Java 通信

Android NDK 开发 JNI 类型签名和方法签名、JNI 实现 java 与 c/c++相互通讯

JNI 开发之局部引用、全局引用和弱全局引用

三、Native 开发工具

主要内容:

编译器、打包工具与分析器

十大最受欢迎的 React Native 应用开发编辑器、react-native 打包流程

静态库与动态库

CPU 架构与注意事项

ABI 管理、处理 CPU 功能、NEON 支持

构建脚本与构建工具

环境搭建、NDK 项目、Cmake、Makefile

交叉编译移植

FFmpeg 编译、FFmpeg+LIBX264+FACC 交叉编译 实现 264 流录制、移植 FFmpeg 在 arm 交叉编译时遇到的问题、FFmpeg 交叉编译、X264 FAAC 交叉编译、解决所有移植问题

AS 构建 NDK 项目

配置 NDK 环境、建立 app 项目、生成.h 头文件、创建 C 文件,实现 native 方法、jni.h 文件

四、Linux 编程

Linux 环境搭建,系统管理,权限系统和工具使用(vim 等)

Linux 环境的搭建、Linux 系统管理操作(25 个命令)

Shell 脚本编程

Shell 脚本、编写简单 Shell 脚本、流程控制语句、计划任务服务程序

五、底层图片处理

PNG/JPEG/WEBP 图像处理与压缩

四种图片格式、推荐几种图片处理网站、squoosh 在线无损图片压缩工具,JPG/webP/PNG/ 互转

微信图片压缩

计算原始宽高、计算近似宽高、第一次采样获取目标图片、循环逼近目标大小

GIF 合成原理与实现

GIF 图片的解析、GIF 图片的合成(序列图像合成 GIF 图像)

六、音视频开发

多媒体系统

Camera 与手机屏幕采集、图像原始数据格式 YUV420(NV21 与 YV12 等)、音频采集与播放系统、编解码器 MediaCodec、MediaMuxer 复用与 MediaExtractor

FFmpeg

ffmpeg 模块介绍、音视频解码,音视频同步、I 帧,B 帧,P 帧解码原理、x264 视频编码与 faac 音频编码、OpenGL 绘制与 NativeWindow 绘制

流媒体协议

RTMP 协议、、音视频通话 P2P WebRtc

OpenGL ES 滤镜开发之美颜效果

高斯模糊、高反差保留、强光处理、融合

抖音视频效果分析与实现

流程列表、视频拍摄、视频编辑、视频导出

音视频变速原理

变速入口分析、音频变速实现、视频变速实现

七、机器学习

Opencv

图像预处理

灰度化和二值化、腐蚀与膨胀、人脸检测、身份证识别

最后

由于篇幅限制,文档的详解资料太全面,细节内容太多,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!

除了上面的之外我还自己整理了以下一系列的学习进阶资料:

《Android开发七大模块核心知识笔记》

《2246页最新Android大厂高频面试题解析大全》

笔记内容全部免费分享,有需要完整版笔记的小伙伴【点击我】免费获取哦!

Android NDK 直播推流与引流的相关教程结束。

《Android NDK 直播推流与引流.doc》

下载本文的Word格式文档,以方便收藏与打印。