代码传送门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库
导入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);
}
测试一下视频数据的推流:实现效果如下图:
获取音频数据
- 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实现音频编码,主要有以下步骤:
主要函数
-
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);
}
}
代码传送门GitHub