android 音媒体多线程,《Android音视频系列-7》直播推流

这篇文章将介绍在Android平台使用RTMPDump来进行直播推流。

一、推流核心思想

3387ac42d769

推流流程图:来自文末参考链接

推流,可以推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即可。

下载最新的就行

3387ac42d769

解压之后把源码拷贝到Android工程

3387ac42d769

这里我创建一个文件夹 push_rtmp,然后将librtmp整个拷过去

3387ac42d769

配置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 = env->GetByteArrayElements(spsData_, NULL);

jbyte *ppsData = 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 = 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 = 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 层,即网络提取层,这是解码的基础。

3387ac42d769

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) 0xe1 sps 个数

// sps data length (2byte) sps 长度

// sps data sps 的内容

// pps number (1byte) 0x01 pps 个数

// pps data length (2byte) pps 长度

// pps data pps 的内容

// 数据的长度(大小) = 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) 0xe1 sps 个数

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 data sps 的内容

memcpy(&body[index], spsData, spsLen); ///拷贝sps到body

index += spsLen;

// pps number (1byte) 0x01 pps 个数

body[index++] = 0x01;

// pps data length (2byte) pps 长度

body[index++] = (ppsLen >> 8) & 0xFF;

body[index++] = ppsLen & 0xFF;

// pps data pps 的内容

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 字节头信息

// 前四位表示音频数据格式 AAC 10 -> 1010 -> A

// 五六位表示采样率 0 = 5.5k 1 = 11k 2 = 22k 3(11) = 44k

// 七位表示采样采样的精度 0 = 8bits 1 = 16bits

// 八位表示音频类型 0 = mono 1 = stereo

// 我们这里算出来第一个字节是 0xAF 1010 11 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

只贴出需要改动的地方,不保证代码的简洁。

需要改动的地方如下

3387ac42d769

编码管理类、音频编码线程、视频编码线程

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 = 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 = new byte[outputBuffer.remaining()];

outputBuffer.get(data, 0, data.length);

mMediaEncodeManager.mLivePush.pushAudio(data,data.length);

总结

到此,这个流程就打通了,效果就不演示了,流程总结如下:

连接流媒体服务器,不断从队列读取封装好的数据,推流。

视频流来源:通过采集摄像头数据-编码成H264格式(avc),然后调用通过RTMPDump开源工具,将每一帧数据封装成FLV格式,放到队列中去。

音频流来源:通过AudioTrack采集音频PCM数据-编码成aac格式,然后通过通过RTMPDump,封装成FLV格式放到队列去。

todo:

视频数据是通过摄像头+OpenGL渲染出来的,所以滤镜、美颜等效果可以通过修改着色器代码来实现,之前OpenGL系列文章有介绍过滤镜的实现,可以拿过来用的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值