
本篇介绍一下直播技术中推流与引流的简单实现。1.流媒体服务器测试首先利用快直播 app 其他支持 RTMP 推流与引流的 app 亦可和 ffplay.exe 对流媒体服务器进行测试。快直播 app 下载地址https://apkpure.biz/cn.nodemedia.qlive/%E5%BF%AB%E7%9B%B4%E6%92%AD快直播的推流界面和引流界面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(); ListCamera.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 stylemargin: 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与yuv420py个数一致uv位置对调 //nv21转yuv420p y w*h,u/vw*h/4 //nv21 yvu yuv420pyuv yy uy11 vy1 //如果要进行图像处理美颜可以再转换为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为编码后的数据buffnMaxOutputBytes为调用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 编程底层图片处理音视频开发机器学习由于篇幅限制这里只能展示部分内容朋友们如果需要这份完整版的PDF资料合集微信扫描下方CSDN官方二维码【免费获取】。一、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 编译、FFmpegLIBX264FACC 交叉编译 实现 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 复用与 MediaExtractorFFmpegffmpeg 模块介绍、音视频解码,音视频同步、I 帧,B 帧,P 帧解码原理、x264 视频编码与 faac 音频编码、OpenGL 绘制与 NativeWindow 绘制流媒体协议RTMP 协议、、音视频通话 P2P WebRtcOpenGL ES 滤镜开发之美颜效果高斯模糊、高反差保留、强光处理、融合抖音视频效果分析与实现流程列表、视频拍摄、视频编辑、视频导出音视频变速原理变速入口分析、音频变速实现、视频变速实现七、机器学习Opencv图像预处理灰度化和二值化、腐蚀与膨胀、人脸检测、身份证识别最后由于篇幅限制文档的详解资料太全面细节内容太多所以只把部分知识点截图出来粗略的介绍每个小节点里面都有更细化的内容除了上面的之外我还自己整理了以下一系列的学习进阶资料《Android开发七大模块核心知识笔记》《2246页最新Android大厂高频面试题解析大全》由于篇幅限制这里只能展示部分内容朋友们如果需要这份完整版的PDF资料合集微信扫描下方CSDN官方二维码【免费获取】。