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

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

一、推流核心思想

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

推流,可以推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工程

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

配置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 <jni.h>
#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 <iostream>

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 <malloc.h>
#include <string.h>

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


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
只贴出需要改动的地方,不保证代码的简洁。

需要改动的地方如下


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

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);


总结

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

  1. 连接流媒体服务器,不断从队列读取封装好的数据,推流。
  2. 视频流来源:通过采集摄像头数据-编码成H264格式(avc),然后调用通过RTMPDump开源工具,将每一帧数据封装成FLV格式,放到队列中去。
  3. 音频流来源:通过AudioTrack采集音频PCM数据-编码成aac格式,然后通过通过RTMPDump,封装成FLV格式放到队列去。

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


参考:
Android客户端音视频推流
FFmpeg - Android 直播推拉流
RTMPdump源码分析

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容