《Android音视频系列-7》直播推流

这篇文章将介绍在Android平台使用RTMPDump来进行直播推流。
一、推流核心思想
《Android音视频系列-7》直播推流
文章图片
推流流程图:来自文末参考链接
推流,可以推H264裸流,也可以封装成FLV格式再推送,
为什么不直接推H264裸流,而是要封装成FLV格式再推,多此一举?
其实是为了兼容多种编码格式的流。

如果直接推H264裸流,服务端就对应一套H264裸流的逻辑。
假如后面要推H265的流或者其它封装格式的流,那么无论是推流端还是服务端,都要改逻辑。
而封装成FLV格式再推流,后面如果要推H265流,只需要将H265流封装成FLV格式即可,服务端不需要任何更改,拉流端格式也没变。
RTMP协议采用的封装格式是FLV
二、集成RTMPDump
RTMP(Real Time Messaging Protocol):实时消息协议,目前主流的流媒体协议。
RTMPDump是一个用来处理RTMP流媒体的工具包,是一个C++的开源工程,我们只需要将音视频流封装成RTMPDump所需要的格式,然后调用推流方法RTMP_SendPacket即可。
RTMPDump源码下载
下载最新的就行

《Android音视频系列-7》直播推流
文章图片
解压之后把源码拷贝到Android工程
《Android音视频系列-7》直播推流
文章图片
这里我创建一个文件夹 push_rtmp,然后将librtmp整个拷过去
《Android音视频系列-7》直播推流
文章图片
配置cmake,主要添加的配置如下,生成一个新的so叫 push_rtmp_handle ,其它跟之前一样。
# 添加 define-DNO_CRYPTO,不然rtmp里面会报错找不到 openssl set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DNO_CRYPTO")AUX_SOURCE_DIRECTORY(${CMAKE_SOURCE_DIR}/src/main/cpp/push_rtmp PUSH_RTMP_SRC_LIST) AUX_SOURCE_DIRECTORY(${CMAKE_SOURCE_DIR}/src/main/cpp/push_rtmp/librtmp RTMP_LIB_LIST)add_library( # 编译生成的库的名称叫 push_handle,对应System.loadLibrary("push_handle"); target_link_libraries( push_rtmp_handle # 编解码(最重要的库) avcodec-57 # 设备信息 avdevice-57 # 滤镜特效处理库 avfilter-6 # 封装格式处理库 avformat-57 # 工具库(大部分库都需要这个库的支持) avutil-55 # 后期处理 postproc-54 # 音频采样数据格式转换库 swresample-2 # 视频像素数据格式转换 swscale-4 # 链接 android ndk 自带的一些库 android # Links the target library to the log library # included in the NDK. # 链接 OpenSLES OpenSLES log) # Sets the library as a shared library. SHARED # Provides a relative path to your source file(s). ${PUSH_RTMP_SRC_LIST} ${RTMP_LIB_LIST} )

三、Java层直播推流管理类 LivePushHandle
/** * 直播推流管理类 */ public class LivePushHandle {static { System.loadLibrary("push_rtmp_handle"); }/** * 主线程的 handler */ private static Handler MAIN_HANDLER = new Handler(Looper.getMainLooper()); //默认推流地址 private String mLiveUrl = "rtmp://192.168.43.144:1935/test/live"; public LivePushHandle() { } public LivePushHandle(String liveUrl) { this.mLiveUrl = liveUrl; }/** * 初始化連接 */ public void initConnect(){ nInitConnect(mLiveUrl); }public void stop() { MAIN_HANDLER.post(new Runnable() { @Override public void run() { nStop(); } }); }//1.初始化连接 private native void nInitConnect(String liveUrl); //2.推sps和pps,关键帧中的数据 public native void pushSpsPps(byte[] spsData, int spsLen, byte[] ppsData, int ppsLen); //3.推送每一帧视频 public native void pushVideo(byte[] videoData, int dataLen, boolean keyFrame); //4.推送每一帧音频 public native void pushAudio(byte[] audioData, int dataLen); //5.停止推送 private native void nStop(); /**回调*/ private ConnectListener mConnectListener; public void setOnConnectListener(ConnectListener connectListener) { this.mConnectListener = connectListener; }public interface ConnectListener{ void connectError(int errorCode, String errorMsg); void connectSuccess(); void onInfo(long pts, long dts, long duration, long index); }// 連接的回調 called from jni private void onConnectError(int errorCode, String errorMsg){ stop(); if(mConnectListener != null){ mConnectListener.connectError(errorCode,errorMsg); } } // 連接的回調 called from jni private void onConnectSuccess(){ if(mConnectListener != null){ mConnectListener.connectSuccess(); } }// 推流每一帧信息回调 called from jni private void onInfo(long pts, long dts, long duration, long index) { if (mConnectListener != null) { mConnectListener.onInfo(pts, dts, duration, index); } }}

四、JNI层实现方法
RtmpPushHandle.cpp,主要是做分发,代码比较清晰
#include #include "PushJniCall.h" #include "PushStatus.h" #include "LivePush.h"//ffmpeg 是c写的,要用c的include extern "C" { #include "libavcodec/avcodec.h" #include "libavformat/avformat.h" //引入时间 #include "libavutil/time.h" }; #include using namespace std; //JNI回调处理,跟上一篇差不多,可以自己按需修改 PushJniCall *pJniCall; //推流的几个方法封装 LivePush *pLivePush; //状态处理,跟上一篇一样 PushStatus *pushStatus; JavaVM *pJavaVM = NULL; // 重写 so 被加载时会调用的一个方法,动态注册了解一下 extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *javaVM, void *reserved) { pJavaVM = javaVM; JNIEnv *env; if (javaVM->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) { return -1; } return JNI_VERSION_1_6; }extern "C" JNIEXPORT void JNICALL Java_com_lanshifu_ffmpegdemo_push_1live_LivePushHandle_nInitConnect(JNIEnv *env, jobject instance, jstring liveUrl_) { const char *liveUrl = env->GetStringUTFChars(liveUrl_, 0); LOGD("开始连接..."); pJniCall = new PushJniCall(pJavaVM, env, instance); pLivePush = new LivePush(liveUrl, pJniCall); pLivePush->initConnect(); env->ReleaseStringUTFChars(liveUrl_, liveUrl); }extern "C" JNIEXPORT void JNICALL Java_com_lanshifu_ffmpegdemo_push_1live_LivePushHandle_pushSpsPps(JNIEnv *env, jobject instance, jbyteArray spsData_, jint spsLen, jbyteArray ppsData_, jint ppsLen) { jbyte *spsData = https://www.it610.com/article/env->GetByteArrayElements(spsData_, NULL); jbyte *ppsData = https://www.it610.com/article/env->GetByteArrayElements(ppsData_, NULL); LOGD("推sps和pps"); if (pLivePush != NULL) { pLivePush->pushSpsPps(spsData, spsLen, ppsData, ppsLen); }env->ReleaseByteArrayElements(spsData_, spsData, 0); env->ReleaseByteArrayElements(ppsData_, ppsData, 0); }extern "C" JNIEXPORT void JNICALL Java_com_lanshifu_ffmpegdemo_push_1live_LivePushHandle_pushVideo(JNIEnv *env, jobject instance, jbyteArray videoData_, jint dataLen, jboolean keyFrame) { jbyte *videoData = https://www.it610.com/article/env->GetByteArrayElements(videoData_, NULL); //调用推视频函数 if (pLivePush != NULL) { pLivePush->pushVideo(videoData, dataLen, keyFrame); }env->ReleaseByteArrayElements(videoData_, videoData, 0); }extern "C" JNIEXPORT void JNICALL Java_com_lanshifu_ffmpegdemo_push_1live_LivePushHandle_pushAudio(JNIEnv *env, jobject instance, jbyteArray audioData_, jint dataLen) { jbyte *audioData = https://www.it610.com/article/env->GetByteArrayElements(audioData_, NULL); //调用推音频函数 if (pLivePush != NULL) { pLivePush->pushAudio(audioData, dataLen); }env->ReleaseByteArrayElements(audioData_, audioData, 0); }extern "C" JNIEXPORT void JNICALL Java_com_lanshifu_ffmpegdemo_push_1live_LivePushHandle_nStop(JNIEnv *env, jobject instance) {LOGD("停止推流"); if (pLivePush != NULL) { pLivePush->stop(); delete (pLivePush); pLivePush = NULL; }if (pJniCall != NULL) { delete (pJniCall); pJniCall = NULL; }}

上面并没有真正去推流,推流相关的操作封装在LivePush中
LivePush.h 如下
#ifndef _LIVEPUSH_H #define _LIVEPUSH_H#include "PushJniCall.h" #include "PacketQueue.h" #include #include extern "C" { #include "librtmp/rtmp.h" }class LivePush { public: PushJniCall *pJniCall = NULL; char *liveUrl = NULL; PacketQueue *pPacketQueue; RTMP *pRtmp = NULL; bool isPushing = true; uint32_t startTime; pthread_t initConnectTid; //初始化连接的线程id public: LivePush(const char *liveUrl, PushJniCall *pJniCall); ~LivePush(); void initConnect(); void pushSpsPps(jbyte *spsData, jint spsLen, jbyte *ppsData, jint ppsLen); void pushVideo(jbyte *videoData, jint dataLen, jboolean keyFrame); void pushAudio(jbyte *audioData, jint dataLen); void stop(); }; #endif //_LIVEPUSH_H

PushJniCall :封装了回调Java的方法
PacketQueue :是一个存放RTMPPacket的队列
采用生产者消费者模式
消费者:连接建立之后不断从队列中取出RTMPPacket,然后调用RTMPdump推流函数,队列空就阻塞。
生产者:App传过来的流封装成RTMPPacket,然后放到队列去,唤醒消费者
接下来介绍如何将音视频帧数据封装成RTMPPacket
五、推流步骤
5.1 初始化连接流媒体服务器
void *initConnectFun(void *context) {LivePush *pLivePush = (LivePush *)context; // 1. 创建 RTMP pLivePush->pRtmp = RTMP_Alloc(); // 2. 初始化 RTMP_Init(pLivePush->pRtmp); // 3. 设置参数,连接的超时时间等 pLivePush->pRtmp->Link.timeout = 5; pLivePush->pRtmp->Link.lFlags |= RTMP_LF_LIVE; RTMP_SetupURL(pLivePush->pRtmp, pLivePush->liveUrl); RTMP_EnableWrite(pLivePush->pRtmp); // 开始连接 if (!RTMP_Connect(pLivePush->pRtmp, NULL)) { // 回调到 java 层,这个错误一般是手机没网络,或者服务器没打开 LOGE("rtmp connect error,url = %s",pLivePush->liveUrl); pLivePush->pJniCall->callConnectError(THREAD_CHILD, INIT_RTMP_CONNECT_ERROR_CODE, "rtmp connect error"); return (void *) INIT_RTMP_CONNECT_ERROR_CODE; }if (!RTMP_ConnectStream(pLivePush->pRtmp, 0)) { // 回调到 java 层 LOGE("rtmp connect stream error"); pLivePush->pJniCall->callConnectError(THREAD_CHILD, INIT_RTMP_CONNECT_STREAM_ERROR_CODE, "rtmp connect stream error"); return (void *) INIT_RTMP_CONNECT_STREAM_ERROR_CODE; } LOGW("rtmp 连接成功,回调给java层"); pLivePush->pJniCall->callConnectSuccess(THREAD_CHILD); pLivePush->startTime = RTMP_GetTime(); while (pLivePush->isPushing) { // 从队列读,不断的往流媒体服务器上推(生产者消费者模式) RTMPPacket *pPacket = pLivePush->pPacketQueue->pop(); if (pPacket != NULL) { RTMP_SendPacket(pLivePush->pRtmp, pPacket, 1); RTMPPacket_Free(pPacket); free(pPacket); } }LOGE("推流结束,线程停止了"); return 0; }

集成RTMPDump源码之后,就按照RTMP协议,先连接流媒体服务器,连接失败回调给Java层,连接成功则进入循环,从队列读RTMPPacket,然后往流媒体服务器上推。这里要能理解生产者消费者模式。
生产者消费者模式
消费者线程:连接推流服务器是单独一个线程,连接成功之后不断从队列拿数据进行消费,读不到就等待,需要生产者唤醒才继续。
生产者线程:将编码后的数据放入队列,然后唤醒消费者线程
5.2 推送视频流 视频数据是通过摄像头采集(NV21格式),在通过MediaCodec编码(H264/avc格式),然后传到native层,native层再将数据转换成RTMPDump要求的格式,然后进行推流。
H264 可以分为两层:
1.VCL video codinglayer(视频编码层),
2.NAL network abstraction layer(网络提取层)。
这里我们要关注的是 NAL 层,即网络提取层,这是解码的基础。

《Android音视频系列-7》直播推流
文章图片
NAL H264编码格式涉及到I帧、P帧、B帧、SPS、PPS是什么意思呢?
SPS:序列参数集,作用于一系列连续编码图像
PPS:图像参数集,作用于编码视频序列中一个或多个图像
I帧:帧内编码帧,可独立解码生成完整的图片。
P帧: 前向预测编码帧,需要参考其前面的一个I 或者B 来生成一张完整的图片。
B帧: 双向预测内插编码帧,则要参考其前一个I或者P帧及其后面的一个P帧来生成一张完整的图片
5.2.1 推送SPS和PPS 为了确保直播过程中进来的用户也可以正常的观看直播,我们需要在每个关键帧前先把 SPS 和 PPS 推送到流媒体服务器。
void LivePush::pushSpsPps(jbyte *spsData, jint spsLen, jbyte *ppsData, jint ppsLen) { // frame type : 1关键帧,2 非关键帧 (4bit) // CodecID : 7表示 AVC (4bit), 与 frame type 组合起来刚好是 1 个字节0x17 // fixed : 0x00 0x00 0x00 0x00 (4byte)-固定的 // configurationVersion(1byte)0x01版本-固定的 // AVCProfileIndication(1byte)sps[1] profile // profile_compatibility (1byte)sps[2] compatibility // AVCLevelIndication(1byte)sps[3] Profile level // lengthSizeMinusOne(1byte)0xff包长数据所使用的字节数,传最大// sps + pps 的数据 // sps number(1byte)0xe1sps 个数 // sps data length(2byte)sps 长度 // sps datasps 的内容 // pps number(1byte)0x01pps 个数 // pps data length(2byte)pps 长度 // pps datapps 的内容// 数据的长度(大小) = sps 大小 + pps 大小 + 16字节 int bodySize = spsLen + ppsLen + 16; // 构建 RTMPPacket RTMPPacket *pPacket = (RTMPPacket *) malloc(sizeof(RTMPPacket)); RTMPPacket_Alloc(pPacket, bodySize); RTMPPacket_Reset(pPacket); // 构建 body 按上面的一个一个开始赋值 char *body = pPacket->m_body; int index = 0; // CodecID : 7表示 AVC (4bit), 与 frame type 组合起来刚好是 1 个字节0x17 body[index++] = 0x17; // fixed : 0x00 0x00 0x00 0x00 (4byte) body[index++] = 0x00; body[index++] = 0x00; body[index++] = 0x00; body[index++] = 0x00; // configurationVersion(1byte)0x01版本 body[index++] = 0x01; // AVCProfileIndication(1byte)sps[1] profile body[index++] = spsData[1]; ///sps第1个字节 // profile_compatibility (1byte)sps[2] compatibility body[index++] = spsData[2]; ///sps第2个字节 // AVCLevelIndication(1byte)sps[3] Profile level body[index++] = spsData[3]; /// ///sps第3个字节 // lengthSizeMinusOne(1byte)0xff包长数据所使用的字节数 body[index++] = 0xff; // sps + pps 的数据 // sps number(1byte)0xe1sps 个数 body[index++] = 0xe1; // sps data length(2byte)sps 长度 body[index++] = (spsLen >> 8) & 0xFF; ///sps长度用两个字节表示,第一个字节表示高八位,256 -> 0000 0001 0000 0000 右移8位 -> 0000 0001 body[index++] = spsLen & 0xFF; ///第二个字节放低八位,比如256,如果只放一个字节,前面的1会被干掉,变成 0000 0000 // sps datasps 的内容 memcpy(&body[index], spsData, spsLen); ///拷贝sps到body index += spsLen; // pps number(1byte)0x01pps 个数 body[index++] = 0x01; // pps data length(2byte)pps 长度 body[index++] = (ppsLen >> 8) & 0xFF; body[index++] = ppsLen & 0xFF; // pps datapps 的内容 memcpy(&body[index], ppsData, ppsLen); ///拷贝pps到body// RTMPPacket 设置一些信息 pPacket->m_hasAbsTimestamp = 0; pPacket->m_nTimeStamp = 0; pPacket->m_headerType = RTMP_PACKET_SIZE_MEDIUM; pPacket->m_packetType = RTMP_PACKET_TYPE_VIDEO; pPacket->m_nBodySize = bodySize; pPacket->m_nChannel = 0x04; pPacket->m_nInfoField2 = this->pRtmp->m_stream_id; pPacketQueue->push(pPacket); }

封装 RTMPPacket 数据,一个RTMPPacket对应RTMP协议规范里面的一个块(Chunk),pPacket->m_body 中的每个字节有不同意思,其实就是一种规范,按照规范来就对了,SPS和PPS的封装看起来有点小复杂,慢慢理解即可。
5.2.2 推送视频帧
void LivePush::pushVideo(jbyte *videoData, jint dataLen, jboolean keyFrame) { // frame type : 1关键帧,2 非关键帧 (4bit) // CodecID : 7表示 AVC (4bit), 与 frame type 组合起来刚好是 1 个字节0x17 // fixed : 0x01 0x00 0x00 0x00 (4byte)0x01表示 NALU 单元// video data length(4byte)video 长度 // video data // 数据的长度(大小) =dataLen + 9 int bodySize = dataLen + 9; // 构建 RTMPPacket RTMPPacket *pPacket = (RTMPPacket *) malloc(sizeof(RTMPPacket)); RTMPPacket_Alloc(pPacket, bodySize); RTMPPacket_Reset(pPacket); // 构建 body 按上面的一个一个开始赋值 char *body = pPacket->m_body; int index = 0; // frame type : 1关键帧,2 非关键帧 (4bit) // CodecID : 7表示 AVC (4bit), 与 frame type 组合起来刚好是 1 个字节0x17 if (keyFrame) { body[index++] = 0x17; } else { body[index++] = 0x27; }// fixed : 0x01 0x00 0x00 0x00 (4byte)0x01表示 NALU 单元 body[index++] = 0x01; body[index++] = 0x00; body[index++] = 0x00; body[index++] = 0x00; // video data length(4byte)video 长度 body[index++] = (dataLen >> 24) & 0xFF; body[index++] = (dataLen >> 16) & 0xFF; body[index++] = (dataLen >> 8) & 0xFF; body[index++] = dataLen & 0xFF; // video data memcpy(&body[index], videoData, dataLen); // RTMPPacket 设置一些信息 pPacket->m_headerType = RTMP_PACKET_SIZE_LARGE; pPacket->m_packetType = RTMP_PACKET_TYPE_VIDEO; pPacket->m_hasAbsTimestamp = 0; pPacket->m_nTimeStamp = RTMP_GetTime() - startTime; //时间戳 pPacket->m_nBodySize = bodySize; pPacket->m_nChannel = 0x04; pPacket->m_nInfoField2 = this->pRtmp->m_stream_id; pPacketQueue->push(pPacket); }

推送视频帧(H264编码)比推送SPS和PPS要简单一些。
AVC是H.264编码的mime类型,
在Java层,判断是关键帧,要在关键帧之前先推SPS和PPS
5.2 推送音频数据
void LivePush::pushAudio(jbyte *audioData, jint dataLen) { // 2 字节头信息 // 前四位表示音频数据格式 AAC10->1010->A // 五六位表示采样率 0 = 5.5k1 = 11k2 = 22k3(11) = 44k // 七位表示采样采样的精度 0 = 8bits1 = 16bits // 八位表示音频类型0 = mono1 = stereo // 我们这里算出来第一个字节是 0xAF101011 11// 数据的长度(大小) =dataLen + 2 int bodySize = dataLen + 2; // 构建 RTMPPacket RTMPPacket *pPacket = (RTMPPacket *) malloc(sizeof(RTMPPacket)); RTMPPacket_Alloc(pPacket, bodySize); RTMPPacket_Reset(pPacket); // 构建 body 按上面的一个一个开始赋值 char *body = pPacket->m_body; int index = 0; // 我们这里算出来第一个字节是 0xAF body[index++] = 0xAF; // 0x01 代表 aac 原始数据 body[index++] = 0x01; // audio data memcpy(&body[index], audioData, dataLen); // RTMPPacket 设置一些信息 pPacket->m_headerType = RTMP_PACKET_SIZE_LARGE; pPacket->m_packetType = RTMP_PACKET_TYPE_AUDIO; pPacket->m_hasAbsTimestamp = 0; pPacket->m_nTimeStamp = RTMP_GetTime() - startTime; pPacket->m_nBodySize = bodySize; pPacket->m_nChannel = 0x04; pPacket->m_nInfoField2 = this->pRtmp->m_stream_id; pPacketQueue->push(pPacket); }

音频帧(AAC编码)的推流也是比较简单,m_packetType 不同,其它跟视频的类似。
六、App层调用推流方法
上面基本把RTMPDump的使用介绍了,基础就是这些,实际开发中更多的应该是处理视频流,添加滤镜、美颜效果等,然后再编码成H264格式,然后推流。
这里基于上一篇的基础上添加推流功能。
《Android音视频系列-5》音视频采集,生成mp4
只贴出需要改动的地方,不保证代码的简洁。
需要改动的地方如下

《Android音视频系列-7》直播推流
文章图片
编码管理类、音频编码线程、视频编码线程
1、编码管理类修改 创建 LivePushHandle
public LivePushHandle mLivePush = new LivePushHandle();

添加开始/结束推流方法
public void startPush(){ mLivePush.setOnConnectListener(new LivePushHandle.ConnectListener() { @Override public void connectError(int errorCode, String errorMsg) { Log.d(TAG, "connectError: "); }@Override public void connectSuccess() { Log.d(TAG, "connectSuccess: "); startEncode(); }@Override public void onInfo(long pts, long dts, long duration, long index) {} }); mLivePush.initConnect(); }public void stopPush(){ mLivePush.stop(); }

收到连接成功的回调才去开启编码线程 startEncode();
2. 视频编码线程 创建两个变量,sps和pps
public byte[] mVideoSps, mVideoPps;

if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {这个判断里,获取sps和pps
... mMediaEncodeManager.startMediaMuxer(); // 推流要获取 sps 和 pps。 ”csd-0” (sps) ,”csd-1”(pps) ByteBuffer byteBuffer = videoCodec.getOutputFormat().getByteBuffer("csd-0"); mVideoSps = new byte[byteBuffer.remaining()]; byteBuffer.get(mVideoSps, 0, mVideoSps.length); byteBuffer = videoCodec.getOutputFormat().getByteBuffer("csd-1"); mVideoPps = new byte[byteBuffer.remaining()]; byteBuffer.get(mVideoPps, 0, mVideoPps.length); Log.d(TAG, " 成功获取sps和pps ");

在写入混合器的之后,加入推流逻辑
... mediaMuxer.writeSampleData(mVideoTrackIndex, outputBuffer, bufferInfo); //1、 在关键帧前先把 sps 和 pps 推到流媒体服务器 if (bufferInfo.flags == MediaCodec.BUFFER_FLAG_KEY_FRAME) { mMediaEncodeManager.mLivePush.pushSpsPps(mVideoSps, mVideoSps.length, mVideoPps, mVideoPps.length); Log.d(TAG, "推送关键帧sps和pps"); }//2、推送每一帧 byte[] data = https://www.it610.com/article/new byte[outputBuffer.remaining()]; outputBuffer.get(data, 0, data.length); mMediaEncodeManager.mLivePush.pushVideo(data, data.length, bufferInfo.flags == MediaCodec.BUFFER_FLAG_KEY_FRAME);

视频编码线程添加的代码就这些
3. 音频编码线程 在写入混合器的后面推音频流
... mediaMuxer.writeSampleData(mAudioTrackIndex, outputBuffer, bufferInfo); byte[] data = https://www.it610.com/article/new byte[outputBuffer.remaining()]; outputBuffer.get(data, 0, data.length); mMediaEncodeManager.mLivePush.pushAudio(data,data.length);

总结
到此,这个流程就打通了,效果就不演示了,流程总结如下:
  1. 连接流媒体服务器,不断从队列读取封装好的数据,推流。
  2. 视频流来源:通过采集摄像头数据-编码成H264格式(avc),然后调用通过RTMPDump开源工具,将每一帧数据封装成FLV格式,放到队列中去。
  3. 音频流来源:通过AudioTrack采集音频PCM数据-编码成aac格式,然后通过通过RTMPDump,封装成FLV格式放到队列去。
todo:
视频数据是通过摄像头+OpenGL渲染出来的,所以滤镜、美颜等效果可以通过修改着色器代码来实现,之前OpenGL系列文章有介绍过滤镜的实现,可以拿过来用的。
【《Android音视频系列-7》直播推流】参考:
Android客户端音视频推流
FFmpeg - Android 直播推拉流
RTMPdump源码分析

    推荐阅读