Android CameraX 摄像头RTMP推流实现

代码传送门GItHub

CameraX摄像头数据获取

def camerax_version = "1.0.0-alpha05"
implementation "androidx.camera:camera-camera2:$camerax_version"

权限申请

    
    
    
    
    

动态权限获取

public boolean checkPermission() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && checkSelfPermission(
                Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            requestPermissions(new String[]{
                    Manifest.permission.READ_EXTERNAL_STORAGE,
                    Manifest.permission.WRITE_EXTERNAL_STORAGE,
                    Manifest.permission.CAMERA,
                    Manifest.permission.RECORD_AUDIO
            }, 1);

        }
        return false;
    }

获取摄像头数据

//子线程中回调
handlerThread = new HandlerThread("Analyze-thread");
handlerThread.start();
CameraX.bindToLifecycle(lifecycleOwner, getPreView(), getAnalysis());
private Preview getPreView() {
    // 分辨率并不是最终的分辨率,CameraX会自动根据设备的支持情况,结合你的参数,设置一个最为接近的分辨率
    PreviewConfig previewConfig = new PreviewConfig.Builder().setTargetResolution(new Size(width, height)).setLensFacing(currentFacing).build();
    Preview preview = new Preview(previewConfig);
    preview.setOnPreviewOutputUpdateListener(this);
    return preview;
}
private ImageAnalysis getAnalysis() {
    ImageAnalysisConfig imageAnalysisConfig = new ImageAnalysisConfig.Builder()
            .setCallbackHandler(new Handler(handlerThread.getLooper()))
            .setLensFacing(currentFacing)
            .setImageReaderMode(ImageAnalysis.ImageReaderMode.ACQUIRE_LATEST_IMAGE)
            .setTargetResolution(new Size(width, height))
            .build();

    ImageAnalysis imageAnalysis = new ImageAnalysis(imageAnalysisConfig);
    imageAnalysis.setAnalyzer(this);
    return imageAnalysis;
}

数据接收回调方法

@Override
public void analyze(ImageProxy image, int rotationDegrees) {
        //图像格式
        int format = image.getFormat();
        if (format != ImageFormat.YUV_420_888) {
            throw new IllegalStateException("根据文档,Camerax图像分析返回的就是YUV420!");
        }
    
}

回调数据处理Android CameraX 摄像头数据ImageProxy数据分析

LIBRTMP

C语言开源RTMP库,封装 Socket 建立TCP通信,并实现了RTMP数据的收发。

RTMPDump

rtmpdump is a toolkit for RTMP streams. All forms of RTMP are supported, including rtmp://, rtmpt://, rtmpe://, rtmpte://, and rtmps://.

License: GPLv2
Copyright (C) 2009 Andrej Stepanchuk
Copyright (C) 2010-2011 Howard Chu

Download the source:

git clone git://git.ffmpeg.org/rtmpdump

The latest release is 2.4 which you can check out from git. Aside from various minor bugfixes since 2.3, RTMPE type 9 handshakes are now supported.

使用第三方库 Rtmpdump 来实现推流到直播服务器,由于 Rtmpdump 的代码量不是很多,我们直接拷贝源代码到 Android 的 cpp 文件

#定义宏  如果代码中定义了 #defind NO_CRYPTO
#就表示不适用ssl,不支持rtmps。我们这里不支持ssl
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DNO_CRYPTO")

# 把当前目录下所有得文件 变成一个 SOURCE变量表示
aux_source_directory(. SOURCE)
# 编译成librtmp.a 静态库,编译源文件引用${SOURCE}获取
add_library(rtmp STATIC ${SOURCE})

工程cmake引入编译好的rtmp静态库

# 加入子文件夹
add_subdirectory(librtmp)
# 链接引入
target_link_libraries( # Specifies the target library.
                       native-lib
                       # Links the target library to the log library
                       # included in the NDK.
                       ${log-lib}
                         rtmp)

连接直播服务器

这一步中,需要预先准备直播推流地址,然后实现 native 方法

void *connect(void *args) {
    int ret;
    rtmp = RTMP_Alloc();
    RTMP_Init(rtmp);
    do {
        // 解析url地址,可能失败(地址不合法)
        ret = RTMP_SetupURL(rtmp, path);
        if (!ret) {
            //todo 通知Java 地址传的有问题。
            break;
        }
        //开启输出模式, 播放拉流不需要推流,就可以不开
        RTMP_EnableWrite(rtmp);
        ret = RTMP_Connect(rtmp, 0);
        if (!ret) {
            //todo 通知Java 服务器连接失败。
            break;
        }
        ret = RTMP_ConnectStream(rtmp, 0);
        if (!ret) {
            //todo 通知Java 未连接到流。
            break;
        }
        // 发送audio specific config(告诉播放器怎么解码我推流的音频)
//        RTMPPacket *packet = audioChannel->getAudioConfig();
//        callback(packet);
    } while (false);

    if (!ret) {
        RTMP_Close(rtmp);
        RTMP_Free(rtmp);
        rtmp = 0;
    }

    delete (path);
    path = 0;

    // 通知Java层可以开始推流了
    helper->onParpare(ret);
    startTime = RTMP_GetTime();
    return 0;
}

交叉编译X264

摄像头直播采集的数据,采用x264软编码。

x264是一个开源的H.264/MPEG-4 AVC视频编码函数库,是最好的有损视频编码器之一。 它将作为我们直播数据的视频编码库。

FFmpeg中同样实现了H.264的编码,同时FFmpeg也能够集成X264。本次我们将直接使用X264来进行视 频编码而不是FFmpeg

macos NDK 交叉编译X264

引入x264lib库

image-20210408213156751

导入armeabi-v7a在Cpp目录下,app/build.gradle 配置一下abiFilters

externalNativeBuild {
    cmake {
        cppFlags ""
        abiFilters 'armeabi-v7a'
    }
}

ndk{
    abiFilters 'armeabi-v7a'
}

cmake编译引入x264

#x264 lib
include_directories(${CMAKE_SOURCE_DIR}/x264/${ANDROID_ABI}/include)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${CMAKE_SOURCE_DIR}/x264/${ANDROID_ABI}/lib")

target_link_libraries(
        native-lib
        rtmp
        x264
        log)

根据参数配置x264编码器

void VideoChannel::openCodec(int width, int height, int fps, int bitrate) {
    // 编码器参数
    x264_param_t param;

    // ultrafast: 编码速度与质量的控制 ,使用最快的模式编码
    // zerolatency: 无延迟编码 , 实时通信方面
    x264_param_default_preset(¶m, "ultrafast", "zerolatency");
    // main base_line high
    //base_line 3.2 编码规格 无B帧(数据量最小,但是解码速度最慢)
    param.i_level_idc = 32;
    //输入数据格式
    param.i_csp = X264_CSP_I420;
    param.i_width = width;
    param.i_height = height;
    //无b帧
    param.i_bframe = 0;
    //参数i_rc_method表示码率控制,CQP(恒定质量),CRF(恒定码率),ABR(平均码率)
    param.rc.i_rc_method = X264_RC_ABR;
    //码率(比特率,单位Kbps)
    param.rc.i_bitrate = bitrate / 1000;
    //瞬时最大码率
    param.rc.i_vbv_max_bitrate = bitrate / 1000 * 1.2;

    //帧率
    param.i_fps_num = fps;
    param.i_fps_den = 1;
    param.pf_log = x264_log_default2;
    //帧距离(关键帧)  2s一个关键帧
    param.i_keyint_max = fps * 2;
    // 是否复制sps和pps放在每个关键帧的前面 该参数设置是让每个关键帧(I帧)都附带sps/pps。
    param.b_repeat_headers = 1;
    //不使用并行编码。zerolatency场景下设置param.rc.i_lookahead=0;
    // 那么编码器来一帧编码一帧,无并行、无延时
    param.i_threads = 1;
    param.rc.i_lookahead = 0;
    x264_param_apply_profile(¶m, "baseline");

    codec = x264_encoder_open(¶m);
    ySize = width * height;
    uSize = (width >> 1) * (height >> 1);
    this->width = width;
    this->height = height;
}

根据RTMP协议在I帧前发SPS和PPS数据包

void VideoChannel::sendVideoConfig(uint8_t *sps, uint8_t *pps, int spslen, int ppslen) {
    int bodySize = 13 + spslen + 3 + ppslen;
    RTMPPacket *packet = new RTMPPacket;
    RTMPPacket_Alloc(packet, bodySize);

    int i = 0;
    //固定头
    packet->m_body[i++] = 0x17;
    //类型
    packet->m_body[i++] = 0x00;
    //composition time 0x000000
    packet->m_body[i++] = 0x00;
    packet->m_body[i++] = 0x00;
    packet->m_body[i++] = 0x00;

    //版本
    packet->m_body[i++] = 0x01;
    //编码规格
    packet->m_body[i++] = sps[1];
    packet->m_body[i++] = sps[2];
    packet->m_body[i++] = sps[3];
    packet->m_body[i++] = 0xFF;

    //整个sps
    packet->m_body[i++] = 0xE1;
    //sps长度
    packet->m_body[i++] = (spslen >> 8) & 0xff;
    packet->m_body[i++] = spslen & 0xff;
    memcpy(&packet->m_body[i], sps, spslen);
    i += spslen;

    //pps
    packet->m_body[i++] = 0x01;
    packet->m_body[i++] = (ppslen >> 8) & 0xff;
    packet->m_body[i++] = (ppslen) & 0xff;
    memcpy(&packet->m_body[i], pps, ppslen);

    packet->m_packetType = RTMP_PACKET_TYPE_VIDEO;
    packet->m_nBodySize = bodySize;
    packet->m_headerType = RTMP_PACKET_SIZE_MEDIUM;
    //时间戳  sps与pps(不是图像) 没有时间戳
    packet->m_nTimeStamp = 0;
    // 使用相对时间
    packet->m_hasAbsTimestamp = 0;
    //随便给一个通道 ,避免rtmp.c中使用的就行
    packet->m_nChannel = 0x10;
    callback(packet);
}

void VideoChannel::sendFrame(int type, uint8_t *p_payload, int i_payload) {
    //去掉 00 00 00 01 / 00 00 01
    if (p_payload[2] == 0x00) {
        i_payload -= 4;
        p_payload += 4;
    } else if (p_payload[2] == 0x01) {
        i_payload -= 3;
        p_payload += 3;
    }
    RTMPPacket *packet = new RTMPPacket;
    int bodysize = 9 + i_payload;
    RTMPPacket_Alloc(packet, bodysize);
    RTMPPacket_Reset(packet);
//    int type = payload[0] & 0x1f;
    packet->m_body[0] = 0x27;
    //关键帧
    if (type == NAL_SLICE_IDR) {
        packet->m_body[0] = 0x17;
    }
    //类型
    packet->m_body[1] = 0x01;
    //时间戳
    packet->m_body[2] = 0x00;
    packet->m_body[3] = 0x00;
    packet->m_body[4] = 0x00;
    //数据长度 int 4个字节 相当于把int转成4个字节的byte数组
    packet->m_body[5] = (i_payload >> 24) & 0xff;
    packet->m_body[6] = (i_payload >> 16) & 0xff;
    packet->m_body[7] = (i_payload >> 8) & 0xff;
    packet->m_body[8] = (i_payload) & 0xff;

    //图片数据
    memcpy(&packet->m_body[9], p_payload, i_payload);

    packet->m_hasAbsTimestamp = 0;
    packet->m_nBodySize = bodysize;
    packet->m_packetType = RTMP_PACKET_TYPE_VIDEO;
    packet->m_nChannel = 0x10;
    packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
    callback(packet);
}

测试一下视频数据的推流:实现效果如下图:

image-20210408212725131.png

获取音频数据

  • AudioRecord 采集
//录音工具类  采样位数 通道数   采样评率   固定了   设备没关系  录音 数据一样的
            minBufferSize = AudioRecord.getMinBufferSize(44100,
                    AudioFormat.CHANNEL_IN_MONO,
                    AudioFormat.ENCODING_PCM_16BIT);
            audioRecord = new AudioRecord(
                    MediaRecorder.AudioSource.MIC, 44100,
                    AudioFormat.CHANNEL_IN_MONO,
                    AudioFormat.ENCODING_PCM_16BIT, minBufferSize);
  • 麦克风的数据读取出来 pcm
 audioRecord.startRecording();
//        容器 固定
        byte[] buffer = new byte[minBufferSize];
//            麦克风的数据读取出来   pcm   buffer  aac
       int len = audioRecord.read(buffer, 0, buffer.length);

使用faac音频软编码,区别于Android RTMP 投屏直播推流实现

下载faac

wget https://nchc.dl.sourceforge.net/project/faac/faac-src/faac-1.29/faac-1.29.9.2.tar.gz

#下载完成后解压

tar xvf faac-1.29.9.2.tar.gz

#进入facc目录

cd faac-1.29.9.2

AAC全称为Advanced Audio Coding,目前比较主流的AAC开源编码器主要有Nero和Faac。接下来我们将使用Faac实现音频PCM至AAC的音频格式转换,并使用Emscripten编译成WebAssembly模块。

run.sh脚本内容

#!/bin/bash
# NDK目录
NDK_ROOT=/Users/zcw/Android/android_SDK/sdk/ndk/21.1.6352462
#编译后安装位置 pwd表示当前目录
PREFIX=`pwd`/android/armeabi-v7a
#目标平台版本,我们将兼容到android-21
API=21
#编译工具链目录
TOOLCHAIN=$NDK_ROOT/toolchains/llvm/prebuilt/darwin-x86_64

#小技巧,创建一个AS的NDK工程,执行编译,
#然后在 app/.cxx/cmake/debug(release)/自己要编译的平台/ 目录下自己观察 build.ninja与 rules.ninja

#虽然x264提供了交叉编译配置:--cross-prefix,如--corss-prefix=/NDK/arm-linux-androideabi-
#那么则会使用 /NDK/arm-linux-androideabi-gcc 来编译
#然而ndk19开始gcc已经被移除,由clang替代。
# 小常识:一般的库都会使用$CC 变量来保存编译器,我们自己设置CC变量的值为clang。

export CC=$TOOLCHAIN/bin/armv7a-linux-androideabi$API-clang
export CXX=$TOOLCHAIN/bin/armv7a-linux-androideabi$API-clang++




#--extra-cflags会附加到CFLAGS 变量之后,作为传递给编译器的参数,所以就算有些库没有--extra-cflags配置,我们也可以自己创建变量cFLAGS传参
# FLAGS="--target=armv7-none-linux-androideabi21 --gcc-toolchain=${TOOLCHAIN}  -g -DANDROID -fdata-sections -ffunction-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes -D_FORTIFY_SOURCE=2 -march=armv7-a -mthumb -Wformat -Werror=format-security   -Oz -DNDEBUG  -fPIC "

# 和 x264 编译不同,需要配置这个环境变量
export PATH=$PATH:$TOOLCHAIN/bin
# echo ${FLAGS}


# prefix: 指定编译结果的保存目录 `pwd`: 当前目录
./configure -prefix=${PREFIX} \
--enable-static=yes \
--enable-shared=no \
--with-pic=yes \
--host=arm-linux-androideabi \
--with-sysroot=${TOOLCHAIN}/sysroot \
# --extra-cflags="${FLAGS}"

make clean
make install

cmake编译连接faac

include_directories(${CMAKE_SOURCE_DIR}/faac/${ANDROID_ABI}/include)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${CMAKE_SOURCE_DIR}/faac/${ANDROID_ABI}/lib")

target_link_libraries(
        native-lib
        rtmp
        x264
        faac
        log)

使用Faac实现音频编码,主要有以下步骤:

FAAC编码示意图

主要函数

  • faacEncOpen

faacEncHandle FAACAPI faacEncOpen(unsigned long sampleRate,
    unsigned int numChannels,
    unsigned long *inputSamples,
    unsigned long *maxOutputBytes
);
变量名 变量含义
sampleRate 输入PCM的采样率。
numChannels 输入PCM的通道数。
inputSamples 编码一帧AAC所需要的字节数,打开编码器后获取,故声明时不需赋值。
maxOutputBytes 编码后的数据输出的最大长度。
  • faacEncEncode

int FAACAPI faacEncEncode(faacEncHandle hEncoder,
    int32_t * inputBuffer,
    unsigned int samplesInput,
    unsigned char *outputBuffer,
    unsigned int bufferSize
);
变量名 变量含义
hEncoder faacEncOpen返回的编码器句柄
inputBuffer PCM缓冲区
samplesInput faacEncOpen编码后的数据长度inputSamples,即PCM缓冲区长度
outputBuffer 编码后输出数据
bufferSize 输出数据的长度,对应faacEncOpen的maxOutputBytes

编码器参数

与Faac编码器相关的配置在faaccfg.h中声明。主要参数的含义如下:

// 生成的mpeg版本,如果需要录制MP4则设置为MPEG4,如果希望得到未封装的AAC裸流,则设置为MPEG2
// 0-MPEG4 1-MPEG2
unsigned int mpegVersion;

// AAC编码类型
// 1-MAIN 2-LOW 3-SSR 4-LTP
unsigned int aacObjectType;

// 是否允许一个通道为低频通道
// 0-NO 1-YES
unsigned int useLfe;

// 是否使用瞬时噪声定形(temporal noise shaping,TNS)滤波器
// 0-NO 1-YES
unsigned int useTns;

// AAC码率,可参考常见AAC码率,单位bps
unsigned long bitRate;

// AAC频宽
unsigned int bandWidth;

// AAC编码质量
// lower<100 default=100 higher>100
unsigned long quantqual;

// 输出的数据类型,RAW不带adts头部
// 0-RAW 1-ADTS
unsigned int outputFormat;

// 输入PCM数据类型
// PCM Sample Input Format
// 0    FAAC_INPUT_NULL         invalid, signifies a misconfigured config
// 1    FAAC_INPUT_16BIT        native endian 16bit
// 2    FAAC_INPUT_24BIT        native endian 24bit in 24 bits      (not implemented)
// 3    FAAC_INPUT_32BIT        native endian 24bit in 32 bits      (DEFAULT)
// 4    FAAC_INPUT_FLOAT        32bit floating point
unsigned int inputFormat;

编码实现

void AudioChannel::openCodec(int sampleRate, int channels) {
    //输入样本: 要送给编码器编码的样本数
    unsigned long inputSamples;
    codec = faacEncOpen(sampleRate, channels, &inputSamples, &maxOutputBytes);
    // 样本是 16位的,那么一个样本就是2个字节
    inputByteNum = inputSamples * 2;
    outputBuffer = static_cast(malloc(maxOutputBytes));
    //得到当前编码器的各种参数配置
    faacEncConfigurationPtr configurationPtr = faacEncGetCurrentConfiguration(codec);
    configurationPtr->mpegVersion = MPEG4;
    configurationPtr->aacObjectType = LOW;
    // 1: 每一帧音频编码的结果数据 都会携带ADTS(包含了采样、声道等信息的一个数据头)
    // 0: 编码出aac裸数据
    configurationPtr->outputFormat = 0;

    configurationPtr->inputFormat = FAAC_INPUT_16BIT;

    faacEncSetConfiguration(codec, configurationPtr);
}
void AudioChannel::encode(int32_t *data, int len) {
    //3、输入的样本数
    //4、输出,编码之后的结果
    //5、编码结果缓存区能接收数据的个数
    int bytelen = faacEncEncode(codec, data, len, outputBuffer, maxOutputBytes);
    if (bytelen > 0) {

        RTMPPacket *packet = new RTMPPacket;
        RTMPPacket_Alloc(packet, bytelen + 2);
        packet->m_body[0] = 0xAF;
        packet->m_body[1] = 0x01;

        memcpy(&packet->m_body[2], outputBuffer, bytelen);

        packet->m_hasAbsTimestamp = 0;
        packet->m_nBodySize = bytelen + 2;
        packet->m_packetType = RTMP_PACKET_TYPE_AUDIO;
        packet->m_nChannel = 0x11;
        packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
        callback(packet);
    }
}
image-20210408220814047

代码传送门GitHub

你可能感兴趣的:(Android CameraX 摄像头RTMP推流实现)