音视频学习 (十一) Android 端实现 rtmp 推流

4.6 设置缩放

当检测到手势缩放的时候,我们往往希望摄像头也能进行相应的缩放,其实这个实现还是比较简单的。首先需要加入缩放的手势识别,当识别到缩放的手势的时候,根据缩放的大小来对摄像头进行缩放。代码如下所示:

/**

  • Handles the pinch-to-zoom gesture
    */
    private class ZoomGestureListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
    @Override
    public boolean onScale(ScaleGestureDetector detector) {
    if (!mIsFocusing) {
    float progress = 0;
    if (detector.getScaleFactor() > 1.0f) {
    progress = CameraHolder.instance().cameraZoom(true);
    } else if (detector.getScaleFactor() < 1.0f) {
    progress = CameraHolder.instance().cameraZoom(false);
    } else {
    return false;
    }
    if(mZoomListener != null) {
    mZoomListener.onZoomProgress(progress);
    }
    }
    return true;
    }
    }

public float cameraZoom(boolean isBig) {
if(mState != State.PREVIEW || mCameraDevice == null || mCameraData == null) {
return -1;
}
Camera.Parameters params = mCameraDevice.getParameters();
if(isBig) {
params.setZoom(Math.min(params.getZoom() + 1, params.getMaxZoom()));
} else {
params.setZoom(Math.max(params.getZoom() - 1, 0));
}
mCameraDevice.setParameters(params);
return (float) params.getZoom()/params.getMaxZoom();
}

4.7 闪光灯操作

一个摄像头可能有相应的闪光灯,也可能没有,因此在使用闪光灯功能的时候先要确认是否有相应的闪光灯。检测摄像头是否有闪光灯的代码如下:

public static boolean supportFlash(Camera camera){
Camera.Parameters params = camera.getParameters();
List flashModes = params.getSupportedFlashModes();
if(flashModes == null) {
return false;
}
for(String flashMode : flashModes) {
if(Camera.Parameters.FLASH_MODE_TORCH.equals(flashMode)) {
return true;
}
}
return false;
}

切换闪光灯的代码如下:

public static void switchLight(Camera camera, Camera.Parameters cameraParameters) {
if (cameraParameters.getFlashMode().equals(Camera.Parameters.FLASH_MODE_OFF)) {
cameraParameters.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH);
} else {
cameraParameters.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);
}
try {
camera.setParameters(cameraParameters);
}catch (Exception e) {
e.printStackTrace();
}
}

4.8 开始预览

当打开了摄像头,并且设置好了摄像头相关的参数后,便可以通过调用 Camera 的 startPreview() 方法开始预览。有一个需要说明,无论是 SurfaceView 还是 GLSurfaceView ,都可以设置 SurfaceHolder.Callback ,当界面开始显示的时候打开摄像头并且开始预览,当界面销毁的时候停止预览并且关闭摄像头,这样的话当程序退到后台,其他应用也能调用摄像头。

private SurfaceHolder.Callback mSurfaceHolderCallback = new SurfaceHolder.Callback() {
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
Log.d(SopCastConstant.TAG, “SurfaceView destroy”);
CameraHolder.instance().stopPreview();
CameraHolder.instance().releaseCamera();
}

@TargetApi(Build.VERSION_CODES.GINGERBREAD)
@Override
public void surfaceCreated(SurfaceHolder holder) {
Log.d(SopCastConstant.TAG, “SurfaceView created”);
}

@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
Log.d(SopCastConstant.TAG, “SurfaceView width:” + width + " height:" + height);
CameraHolder.instance().openCamera();
CameraHolder.instance().startPreview();
}
};

5. 停止预览

停止预览只需要释放掉相机资源即可:

public synchronized void releaseCamera() {
if (mState == State.PREVIEW) {
stopPreview();
}
if (mState != State.OPENED) {
return;
}
if (mCameraDevice == null) {
return;
}
mCameraDevice.release();
mCameraDevice = null;
mCameraData = null;
mState = State.INIT;
}

音频编码

AudioRecord 采集完之后需要对 PCM 数据进行实时的编码 (软编利用 libfaac 通过 NDK 交叉编译静态库、硬编使用 Android SDK MediaCodec 进行编码)。

软编

语音软编这里们用主流的编码库 libfaac 进行编码 AAC 语音格式数据。

1. 编译 libfaac
1.1 下载 libfaac

wget https://sourceforge.net/projects/faac/files/faac-src/faac-1.29/faac-1.29.9.2.tar.gz

1.2 编写交叉编译脚本

#!/bin/bash
#打包地址
PREFIX=pwd/android/armeabi-v7a
#配置NDK 环境变量
NDK_ROOT=KaTeX parse error: Expected 'EOF', got '#' at position 10: NDK_HOME #̲指定 CPU CPU=arm-…NDK_ROOT/toolchains/$CPU-4.9/prebuilt/linux-x86_64

FLAGS=“-isysroot $NDK_ROOT/sysroot -isystem KaTeX parse error: Expected group after '_' at position 54: …-androideabi -D_̲_ANDROID_API__=ANDROID_API -U_FILE_OFFSET_BITS -DANDROID -ffunction-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes -march=armv7-a -mfloat-abi=softfp -mfpu=vfpv3-d16 -mthumb -Wa,–noexecstack -Wformat -Werror=format-security -O0 -fPIC”

CROSS_COMPILE= T O O L C H A I N / b i n / a r m − l i n u x − a n d r o i d e a b i e x p o r t C C = " TOOLCHAIN/bin/arm-linux-androideabi export CC=" TOOLCHAIN/bin/armlinuxandroideabiexportCC="CROSS_COMPILE-gcc --sysroot= N D K R O O T / p l a t f o r m s / a n d r o i d − 17 / a r c h − a r m " e x p o r t C F L A G S = " NDK_ROOT/platforms/android-17/arch-arm" export CFLAGS=" NDKROOT/platforms/android17/archarm"exportCFLAGS="FLAGS"

./configure
–prefix=$PREFIX
–host=arm-linux
–with-pic
–enable-shared=no

make clean
make install

2. CMakeLists.txt 配置

cmake_minimum_required(VERSION 3.4.1)
#语音编码器
set(faac KaTeX parse error: Expected 'EOF', got '#' at position 26: …RCE_DIR}/faac) #̲加载 faac 头文件目录 i…{faac}/include)
#指定 faac 静态库文件目录
set(CMAKE_CXX_FLAGS “ C M A K E C X X F L A G S − L {CMAKE_CXX_FLAGS} -L CMAKECXXFLAGSL{faac}/libs/${CMAKE_ANDROID_ARCH_ABI}”)
#批量添加自己编写的 cpp 文件,不要把 .h 加入进来了
file(GLOB Push_CPP ${ykpusher}/
.cpp)
#添加自己编写 cpp 源文件生成动态库
add_library(ykpusher SHARED ${Push_CPP})
#找系统中 NDK log库
find_library(log_lib
log)
#推流 so
target_link_libraries(
#播放 so
ykpusher

# 写了此命令不用在乎添加 ffmpeg lib 顺序问题导致应用崩溃

-Wl,–start-group

avcodec avfilter avformat avutil swresample swscale

-Wl,–end-group

z

#推流库
rtmp
#视频编码
x264
#语音编码
faac
#本地库
android
${log_lib}
)

3. 配置 faac 编码参数

//设置语音软编码参数
void AudioEncoderChannel::setAudioEncoderInfo(int samplesHZ, int channel) {
//如果已经初始化,需要释放
release();
//通道 默认单声道
mChannels = channel;
//打开编码器
//3、一次最大能输入编码器的样本数量 也编码的数据的个数 (一个样本是16位 2字节)
//4、最大可能的输出数据 编码后的最大字节数
mAudioCodec = faacEncOpen(samplesHZ, channel, &mInputSamples, &mMaxOutputBytes);
if (!mAudioCodec) {
if (mIPushCallback) {
mIPushCallback->onError(THREAD_MAIN, FAAC_ENC_OPEN_ERROR);
}
return;
}

//设置编码器参数
faacEncConfigurationPtr config = faacEncGetCurrentConfiguration(mAudioCodec);
//指定为 mpeg4 标准
config->mpegVersion = MPEG4;
//lc 标准
config->aacObjectType = LOW;
//16位
config->inputFormat = FAAC_INPUT_16BIT;
// 编码出原始数据 既不是adts也不是adif
config->outputFormat = 0;
faacEncSetConfiguration(mAudioCodec, config);
//输出缓冲区 编码后的数据 用这个缓冲区来保存
mBuffer = new u_char[mMaxOutputBytes];
//设置一个标志,用于开启编码
isStart = true;
}

4. 配置 AAC 包头

在发送 rtmp 音视频包的时候需要将语音包头第一个发送

/**

  • 音频头包数据
  • @return
    */
    RTMPPacket *AudioEncoderChannel::getAudioTag() {
    if (!mAudioCodec) {
    setAudioEncoderInfo(FAAC_DEFAUTE_SAMPLE_RATE, FAAC_DEFAUTE_SAMPLE_CHANNEL);
    if (!mAudioCodec)return 0;
    }
    u_char *buf;
    u_long len;
    faacEncGetDecoderSpecificInfo(mAudioCodec, &buf, &len);
    int bodySize = 2 + len;
    RTMPPacket *packet = new RTMPPacket;
    RTMPPacket_Alloc(packet, bodySize);
    //双声道
    packet->m_body[0] = 0xAF;
    if (mChannels == 1) { //单身道
    packet->m_body[0] = 0xAE;
    }
    packet->m_body[1] = 0x00;
    //将包头数据 copy 到RTMPPacket 中
    memcpy(&packet->m_body[2], buf, len);
    //是否使用绝对时间戳
    packet->m_hasAbsTimestamp = FALSE;
    //包大小
    packet->m_nBodySize = bodySize;
    //包类型
    packet->m_packetType = RTMP_PACKET_TYPE_AUDIO;
    //语音通道
    packet->m_nChannel = 0x11;
    packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
    return packet;
    }
5. 开始实时编码

void AudioEncoderChannel::encodeData(int8_t *data) {
if (!mAudioCodec || !isStart)//不符合编码要求,退出
return;
//返回编码后的数据字节长度
int bytelen = faacEncEncode(mAudioCodec, reinterpret_cast<int32_t *>(data), mInputSamples,mBuffer, mMaxOutputBytes);
if (bytelen > 0) {
//开始打包 rtmp
int bodySize = 2 + bytelen;
RTMPPacket *packet = new RTMPPacket;
RTMPPacket_Alloc(packet, bodySize);
//双声道
packet->m_body[0] = 0xAF;
if (mChannels == 1) {
packet->m_body[0] = 0xAE;
}
//编码出的音频 都是 0x01
packet->m_body[1] = 0x01;
memcpy(&packet->m_body[2], mBuffer, bytelen);

packet->m_hasAbsTimestamp = FALSE;
packet->m_nBodySize = bodySize;
packet->m_packetType = RTMP_PACKET_TYPE_AUDIO;
packet->m_nChannel = 0x11;
packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
//发送 rtmp packet,回调给 RTMP send 模块
mAudioCallback(packet);
}
}

6. 释放编码器

在不需要编码或者退出编码的时候需要主动释放编码器,释放 native 内存,可以通过如下函数来实现释放编码器的操作:

void AudioEncoderChannel::release() {
//退出编码的标志
isStart = false;
//释放编码器
if (mAudioCodec) {
//关闭编码器
faacEncClose(mAudioCodec);
//释放缓冲区
DELETE(mBuffer);
mAudioCodec = 0;
}
}

硬编

软编码介绍完了下面利用 Android SDK 自带的 MediaCodec 函数进行对 PCM 编码为 AAC 的格式音频数据。使用 MediaCodec 编码 AAC 对 Android 系统是有要求的,必须是 4.1系统以上,即要求 Android 的版本代号在 Build.VERSION_CODES.JELLY_BEAN (16) 以上。MediaCodec 是 Android 系统提供的硬件编码器,它可以利用设备的硬件来完成编码,从而大大提高编码的效率,还可以降低电量的使用,但是其在兼容性方面不如软编号,因为 Android 设备的锁片化太严重,所以读者可以自己衡量在应用中是否使用 Android 平台的硬件编码特性。

1. 创建 "audio/mp4a-latm" 类型的硬编码器

mediaCodec = MediaCodec.createEncoderByType(configuration.mime);

2. 配置音频硬编码器

public static MediaCodec getAudioMediaCodec(AudioConfiguration configuration){
MediaFormat format = MediaFormat.createAudioFormat(configuration.mime, configuration.frequency, configuration.channelCount);
if(configuration.mime.equals(AudioConfiguration.DEFAULT_MIME)) {
format.setInteger(MediaFormat.KEY_AAC_PROFILE, configuration.aacProfile);
}
//语音码率
format.setInteger(MediaFormat.KEY_BIT_RATE, configuration.maxBps * 1024);
//语音采样率 44100
format.setInteger(MediaFormat.KEY_SAMPLE_RATE, configuration.frequency);
int maxInputSize = AudioUtils.getRecordBufferSize(configuration);
format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, maxInputSize);
format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, configuration.channelCount);

MediaCodec mediaCodec = null;
try {
mediaCodec = MediaCodec.createEncoderByType(configuration.mime);
//MediaCodec.CONFIGURE_FLAG_ENCODE 代表编码器,解码传 0 即可
mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
} catch (Exception e) {
e.printStackTrace();
if (mediaCodec != null) {
mediaCodec.stop();
mediaCodec.release();
mediaCodec = null;
}
}
return mediaCodec;
}

3. 开启音频硬编码器

void prepareEncoder() {
mMediaCodec = AudioMediaCodec.getAudioMediaCodec(mAudioConfiguration);
mMediaCodec.start();
}

4. 拿到硬编码输入(PCM)输出(AAC) ByteBufferer

到了这一步说明,音频编码器配置完成并且也成功开启了,现在就可以从 MediaCodec 实例中获取两个 buffer ,一个是输入 buffer 一个是输出 buffer , 输入 buffer 类似于 FFmpeg 中的 AVFrame 存放待编码的 PCM 数据,输出 buffer 类似于 FFmpeg 的 AVPacket 编码之后的 AAC 数据, 其代码如下:

//存放的是 PCM 数据
ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers();
//存放的是编码之后的 AAC 数据
ByteBuffer[] outputBuffers = mMediaCodec.getOutputBuffers();

5. 开始 PCM 硬编码为 AAC

到此,所有初始化方法已实现完毕,下面来看一下 MediaCodec 的工作原理如下图所示,左边 Client 元素代表要将 PCM 放到 inputBuffer 中的某个具体的 buffer 中去,右边的 Client 元素代表将编码之后的原始 AAC 数据从 outputBuffer 中的某个具体 buffer 中取出来,👈 左边的小方块代表各个 inputBuffer 元素,右边的小方块则代表各个 outputBuffer 元素。详细介绍可以看 MediaCodec 类介绍

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

代码具体实现如下:

//input:PCM
synchronized void offerEncoder(byte[] input) {
if(mMediaCodec == null) {
return;
}
ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers();
ByteBuffer[] outputBuffers = mMediaCodec.getOutputBuffers();
int inputBufferIndex = mMediaCodec.dequeueInputBuffer(12000);
if (inputBufferIndex >= 0) {
ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
inputBuffer.clear();
inputBuffer.put(input);
mMediaCodec.queueInputBuffer(inputBufferIndex, 0, input.length, 0, 0);
}

int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(mBufferInfo, 12000);
while (outputBufferIndex >= 0) {
ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];
if(mListener != null) {
//将 AAC 数据回调出去
mListener.onAudioEncode(outputBuffer, mBufferInfo);
}
//释放当前内部编码内存
mMediaCodec.releaseOutputBuffer(outputBufferIndex, false);
outputBufferIndex = mMediaCodec.dequeueOutputBuffer(mBufferInfo, 0);
}
}

6. AAC 打包为 flv

@Override
public void onAudioData(ByteBuffer bb, MediaCodec.BufferInfo bi) {
if (packetListener == null || !isHeaderWrite || !isKeyFrameWrite) {
return;
}
bb.position(bi.offset);
bb.limit(bi.offset + bi.size);

byte[] audio = new byte[bi.size];
bb.get(audio);
int size = AUDIO_HEADER_SIZE + audio.length;
ByteBuffer buffer = ByteBuffer.allocate(size);
FlvPackerHelper.writeAudioTag(buffer, audio, false, mAudioSampleSize);
packetListener.onPacket(buffer.array(), AUDIO);
}

public static void writeAudioTag(ByteBuffer buffer, byte[] audioInfo, boolean isFirst, int audioSize) {
//写入音频头信息
writeAudioHeader(buffer, isFirst, audioSize);

//写入音频信息
buffer.put(audioInfo);
}

7. 释放编码器

在使用完 MediaCodec 编码器之后,就需要停止运行并释放编码器,代码如下:

synchronized public void stop() {
if (mMediaCodec != null) {
mMediaCodec.stop();
mMediaCodec.release();
mMediaCodec = null;
}
}

视频编码

Camera 采集完之后需要对 YUV 数据进行实时的编码 (软编利用 x264 通过 NDK 交叉编译静态库、硬编使用 Android SDK MediaCodec 进行编码)。

软编

视频软编这里们用主流的编码库 x264 进行编码 H264 视频格式数据。

1. 交叉编译 x264
1.1 下载 x264

//方式 一
git clone https://code.videolan.org/videolan/x264.git
//方式 二
wget ftp://ftp.videolan.org/pub/x264/snapshots/last_x264.tar.bz2

1.2 编写编译脚本

在编写脚本之前需要在 configure 中添加一处代码 -Werror=implicit-function-declaration,如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

交叉编译脚本如下:

#!/bin/bash
#打包地址
PREFIX=./android/armeabi-v7a
#配置NDK 环境变量
NDK_ROOT=$NDK_HOME
#指定 CPU
CPU=arm-linux-androideabi
#指定 Android API
ANDROID_API=17

TOOLCHAIN= N D K R O O T / t o o l c h a i n s / NDK_ROOT/toolchains/ NDKROOT/toolchains/CPU-4.9/prebuilt/linux-x86_64

FLAGS=“-isysroot $NDK_ROOT/sysroot -isystem KaTeX parse error: Expected group after '_' at position 54: …-androideabi -D_̲_ANDROID_API__=ANDROID_API -U_FILE_OFFSET_BITS -DANDROID -ffunction-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes -march=armv7-a -mfloat-abi=softfp -mfpu=vfpv3-d16 -mthumb -Wa,–noexecstack -Wformat -Werror=format-security -O0 -fPIC”
#–disable-cli 不需要命令行工具
#–enable-static 静态库

./configure
–prefix= P R E F I X   − − d i s a b l e − c l i   − − e n a b l e − s t a t i c   − − e n a b l e − p i c   − − h o s t = a r m − l i n u x   − − c r o s s − p r e f i x = PREFIX \ --disable-cli \ --enable-static \ --enable-pic \ --host=arm-linux \ --cross-prefix= PREFIX disablecli enablestatic enablepic host=armlinux crossprefix=TOOLCHAIN/bin/arm-linux-androideabi-
–sysroot= N D K R O O T / p l a t f o r m s / a n d r o i d − 17 / a r c h − a r m   − − e x t r a − c f l a g s = " NDK_ROOT/platforms/android-17/arch-arm \ --extra-cflags=" NDKROOT/platforms/android17/archarm extracflags="FLAGS"

make clean
make install

2. CMakeList.txt 配置

cmake_minimum_required(VERSION 3.4.1)

#视频编码器
set(x264 ${CMAKE_SOURCE_DIR}/x264)

#加载 x264 头文件目录
include_directories(${x264}/include)

#指定 x264 静态库文件目录
set(CMAKE_CXX_FLAGS “ C M A K E C X X F L A G S − L {CMAKE_CXX_FLAGS} -L CMAKECXXFLAGSL{x264}/libs/${CMAKE_ANDROID_ARCH_ABI}”)

#批量添加自己编写的 cpp 文件,不要把 .h 加入进来了
file(GLOB Player_CPP ${ykplayer}/
.cpp)
file(GLOB Push_CPP ${ykpusher}/*.cpp)
#添加自己编写 cpp 源文件生成动态库
add_library(ykpusher SHARED ${Push_CPP})

#找系统中 NDK log库
find_library(log_lib
log)

#推流 so
target_link_libraries(
#播放 so
ykpusher

# 写了此命令不用在乎添加 ffmpeg lib 顺序问题导致应用崩溃

-Wl,–start-group

avcodec avfilter avformat avutil swresample swscale

-Wl,–end-group

z

#推流库
rtmp
#视频编码
x264
#语音编码
faac
#本地库
android
${log_lib}
)

3. 配置并打开 x264 编码器

void VideoEncoderChannel::setVideoEncoderInfo(int width, int height, int fps, int bit) {
pthread_mutex_lock(&mMutex);
this->mWidth = width;
this->mHeight = height;
this->mFps = fps;
this->mBit = bit;
this->mY_Size = width * height;
this->mUV_Size = mY_Size / 4;

//如果编码器已经存在,需要释放
if (mVideoCodec || pic_in) {
release();
}
//打开x264编码器
//x264编码器的属性
x264_param_t param;
//2: 最快
//3: 无延迟编码
x264_param_default_preset(&param, x264_preset_names[0], x264_tune_names[7]);
//base_line 3.2 编码规格
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 = mBit;
//瞬时最大码率
param.rc.i_vbv_max_bitrate = mBit * 1.2;
//设置了i_vbv_max_bitrate必须设置此参数,码率控制区大小,单位kbps
param.rc.i_vbv_buffer_size = mBit;

//帧率
param.i_fps_num = fps;
param.i_fps_den = 1;
param.i_timebase_den = param.i_fps_num;
param.i_timebase_num = param.i_fps_den;
// param.pf_log = x264_log_default2;
//用fps而不是时间戳来计算帧间距离
param.b_vfr_input = 0;
//帧距离(关键帧) 2s一个关键帧
param.i_keyint_max = fps * 2;
// 是否复制sps和pps放在每个关键帧的前面 该参数设置是让每个关键帧(I帧)都附带sps/pps。
param.b_repeat_headers = 1;
//多线程
param.i_threads = 1;

x264_param_apply_profile(&param, “baseline”);
//打开编码器
mVideoCodec = x264_encoder_open(&param);
pic_in = new x264_picture_t;
x264_picture_alloc(pic_in, X264_CSP_I420, width, height);
//相当于重启编码器
isStart = true;
pthread_mutex_unlock(&mMutex);
}

4. 开始编码

void VideoEncoderChannel::onEncoder() {
while (isStart) {
if (!mVideoCodec) {
continue;
}
int8_t *data = 0;
mVideoPackets.pop(data);
if (!data) {
LOGE(“获取 YUV 数据错误”);
continue;
}
//copy Y 数据
memcpy(this->pic_in->img.plane[0], data, mY_Size);
//拿到 UV 数据
for (int i = 0; i < mUV_Size; ++i) {
//拿到 u 数据
*(pic_in->img.plane[1] + i) = *(data + mY_Size + i * 2 + 1);
//拿到 v 数据
*(pic_in->img.plane[2] + i) = *(data + mY_Size + i * 2);
}
//编码出来的数据
x264_nal_t *pp_nal;
//编码出来的帧数量
int pi_nal = 0;
x264_picture_t pic_out;
//开始编码
int ret = x264_encoder_encode(mVideoCodec, &pp_nal, &pi_nal, pic_in, &pic_out);
if (!ret) {
LOGE(“编码失败”);
continue;
}
//如果是关键帧
int sps_len = 0;
int pps_len = 0;
uint8_t sps[100];
uint8_t pps[100];
for (int i = 0; i < pi_nal; ++i) {
if (pp_nal[i].i_type == NAL_SPS) {
//排除掉 h264的间隔 00 00 00 01
sps_len = pp_nal[i].i_payload - 4;
memcpy(sps, pp_nal[i].p_payload + 4, sps_len);
} else if (pp_nal[i].i_type == NAL_PPS) {
pps_len = pp_nal[i].i_payload - 4;
memcpy(pps, pp_nal[i].p_payload + 4, pps_len);
//pps肯定是跟着sps的
sendSpsPps(sps, pps, sps_len, pps_len);
} else {
//编码之后的 H264 数据
sendFrame(pp_nal[i].i_type, pp_nal[i].p_payload, pp_nal[i].i_payload, 0);
}
}
}
}

/**

  • 发送 sps pps
  • @param sps 编码第一帧数据
  • @param pps 编码第二帧数据
  • @param sps_len 编码第一帧数据的长度
  • @param pps_len 编码第二帧数据的长度
    */
    void VideoEncoderChannel::sendSpsPps(uint8_t *sps, uint8_t *pps, int sps_len, int pps_len) {
    int bodySize = 13 + sps_len + 3 + pps_len;
    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++] = (sps_len >> 8) & 0xff;
packet->m_body[i++] = sps_len & 0xff;
memcpy(&packet->m_body[i], sps, sps_len);
i += sps_len;

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

//视频
packet->m_packetType = RTMP_PACKET_TYPE_VIDEO;
packet->m_nBodySize = bodySize;
//随意分配一个管道(尽量避开rtmp.c中使用的)
packet->m_nChannel = 0x10;
//sps pps没有时间戳
packet->m_nTimeStamp = 0;
//不使用绝对时间
packet->m_hasAbsTimestamp = 0;
packet->m_headerType = RTMP_PACKET_SIZE_MEDIUM;
if (mVideoCallback && isStart)
mVideoCallback(packet);
}

/**

  • 发送视频帧 – 关键帧
  • @param type
  • @param payload
  • @param i_playload
    */
    void VideoEncoderChannel::sendFrame(int type, uint8_t *payload, int i_payload, long timestamp) {
    if (payload[2] == 0x00) {
    i_payload -= 4;
    payload += 4;
    } else {
    i_payload -= 3;
    payload += 3;
    }
    //看表
    int bodySize = 9 + i_payload;
    RTMPPacket *packet = new RTMPPacket;
    //
    RTMPPacket_Alloc(packet, bodySize);

packet->m_body[0] = 0x27;
if (type == NAL_SLICE_IDR) {
packet->m_body[0] = 0x17;
LOGE(“关键帧”);
}
//类型
packet->m_body[1] = 0x01;
//时间戳
packet->m_body[2] = 0x00;
packet->m_body[3] = 0x00;
packet->m_body[4] = 0x00;
//数据长度 int 4个字节
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], 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;
if (mVideoCallback && isStart)
mVideoCallback(packet);//回调给 RTMP 模块
}

5. 释放编码器

当我们不需要编码的时候需要释放编码器,代码如下:

x264_encoder_close(mVideoCodec);

硬编

在 Android 4.3 系统以后,用 MediaCodec 编码视频成为了主流的使用场景,尽管 Android 的碎片化很严重,会导致一些兼容性问题,但是硬件编码器的性能以及速度是非常可观的,并且在 4.3 系统之后可以通过 Surface 来配置编码器的输入,大大降低了显存到内存的交换过程所使用的时间,从而使得整个应用的体验得到大大提升。由于输入和输出已经确定,因此接下来将直接编写 MediaCodec 编码视频帧的过程。

1. 创建 video/avc 类型的硬编码器

mediaCodec = MediaCodec.createEncoderByType(videoConfiguration.mime);

2. 配置视频编码器

public static MediaCodec getVideoMediaCodec(VideoConfiguration videoConfiguration) {
int videoWidth = getVideoSize(videoConfiguration.width);
int videoHeight = getVideoSize(videoConfiguration.height);
MediaFormat format = MediaFormat.createVideoFormat(videoConfiguration.mime, videoWidth, videoHeight);
format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
format.setInteger(MediaFormat.KEY_BIT_RATE, videoConfiguration.maxBps* 1024);
int fps = videoConfiguration.fps;
//设置摄像头预览帧率
if(BlackListHelper.deviceInFpsBlacklisted()) {
SopCastLog.d(SopCastConstant.TAG, “Device in fps setting black list, so set mediacodec fps 15”);
fps = 15;
}
format.setInteger(MediaFormat.KEY_FRAME_RATE, fps);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, videoConfiguration.ifi);
format.setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR);
format.setInteger(MediaFormat.KEY_COMPLEXITY, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR);
MediaCodec mediaCodec = null;

try {
mediaCodec = MediaCodec.createEncoderByType(videoConfiguration.mime);
mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
}catch (Exception e) {
e.printStackTrace();
if (mediaCodec != null) {
mediaCodec.stop();
mediaCodec.release();
mediaCodec = null;
}
}
return mediaCodec;
}

3. 开启视频编码器

mMediaCodec.start();

4. 拿到编码之后的数据

private void drainEncoder() {
ByteBuffer[] outBuffers = mMediaCodec.getOutputBuffers();
while (isStarted) {
encodeLock.lock();
if(mMediaCodec != null) {
int outBufferIndex = mMediaCodec.dequeueOutputBuffer(mBufferInfo, 12000);
if (outBufferIndex >= 0) {
ByteBuffer bb = outBuffers[outBufferIndex];
if (mListener != null) { //将编码好的 H264 数据回调出去
mListener.onVideoEncode(bb, mBufferInfo);
}
mMediaCodec.releaseOutputBuffer(outBufferIndex, false);
} else {
try {
// wait 10ms
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
encodeLock.unlock();
} else {
encodeLock.unlock();
break;
}
}
}

5. H264 打包为 flv

//接收 H264 数据
@Override
public void onVideoData(ByteBuffer bb, MediaCodec.BufferInfo bi) {
mAnnexbHelper.analyseVideoData(bb, bi);
}
/**

  • 将硬编得到的视频数据进行处理生成每一帧视频数据,然后传给flv打包器
  • @param bb 硬编后的数据buffer
  • @param bi 硬编的BufferInfo
    */
    public void analyseVideoData(ByteBuffer bb, MediaCodec.BufferInfo bi) {
    bb.position(bi.offset);
    bb.limit(bi.offset + bi.size);

ArrayList<byte[]> frames = new ArrayList<>();
boolean isKeyFrame = false;

while(bb.position() < bi.offset + bi.size) {
byte[] frame = annexbDemux(bb, bi);
if(frame == null) {
LogUtils.e(“annexb not match.”);
break;
}
// ignore the nalu type aud(9)
if (isAccessUnitDelimiter(frame)) {
continue;
}
// for pps
if(isPps(frame)) {
mPps = frame;
continue;
}
// for sps
if(isSps(frame)) {
mSps = frame;
continue;
}
// for IDR frame
if(isKeyFrame(frame)) {
isKeyFrame = true;
} else {
isKeyFrame = false;
}
byte[] naluHeader = buildNaluHeader(frame.length);
frames.add(naluHeader);
frames.add(frame);
}
if (mPps != null && mSps != null && mListener != null && mUploadPpsSps) {
if(mListener != null) {
mListener.onSpsPps(mSps, mPps);
}
mUploadPpsSps = false;
}
if(frames.size() == 0 || mListener == null) {
return;
}
int size = 0;
for (int i = 0; i < frames.size(); i++) {
byte[] frame = frames.get(i);
size += frame.length;
}
byte[] data = new byte[size];
int currentSize = 0;
for (int i = 0; i < frames.size(); i++) {
byte[] frame = frames.get(i);
System.arraycopy(frame, 0, data, currentSize, frame.length);
currentSize += frame.length;
}
if(mListener != null) {
mListener.onVideo(data, isKeyFrame);
}
}

这个方法主要是从编码后的数据中解析得到NALU,然后判断NALU的类型,最后再把数据回调给 FlvPacker 去处理。

处理 spsPps:

@Override
public void onSpsPps(byte[] sps, byte[] pps) {
if (packetListener == null) {
return;
}
//写入第一个视频信息
writeFirstVideoTag(sps, pps);
//写入第一个音频信息
writeFirstAudioTag();
isHeaderWrite = true;
}

处理视频帧:

@Override
public void onVideo(byte[] video, boolean isKeyFrame) {
if (packetListener == null || !isHeaderWrite) {
return;
}
int packetType = INTER_FRAME;
if (isKeyFrame) {
isKeyFrameWrite = true;
packetType = KEY_FRAME;
}
//确保第一帧是关键帧,避免一开始出现灰色模糊界面
if (!isKeyFrameWrite) {
return;
}
int size = VIDEO_HEADER_SIZE + video.length;
ByteBuffer buffer = ByteBuffer.allocate(size);
FlvPackerHelper.writeH264Packet(buffer, video, isKeyFrame);
packetListener.onPacket(buffer.array(), packetType);
}

6. 释放编码器,并释放 Surface

//释放编码器
private void releaseEncoder() {
if (mMediaCodec != null) {
mMediaCodec.signalEndOfInputStream();
mMediaCodec.stop();
mMediaCodec.release();
mMediaCodec = null;
}
if (mInputSurface != null) {
mInputSurface.release();
mInputSurface = null;
}
}

//释放 OpenGL ES 渲染,Surface
public void release() {
EGL14.eglDestroySurface(mEGLDisplay, mEGLSurface);
EGL14.eglDestroyContext(mEGLDisplay, mEGLContext);
EGL14.eglReleaseThread();
EGL14.eglTerminate(mEGLDisplay);

mSurface.release();

mSurface = null;
mEGLDisplay = null;
mEGLContext = null;
mEGLSurface = null;
}

rtmp 推流

注: 实际项目 rtmp 需要先连接上才有后续操作。

rtmp 模块我们已在开发 播放器 的时候,将它和 ffmpeg 一并编译了。所以我们直接使用上次的静态库和头文件就可以了,如果对 rtmp 协议不了解的可以参考上一篇文章,里面也有介绍 搭建 RTMP 直播服务器

到这里软编码和硬编码数据都已准备好了现在,需要发送给 rtmp 模块,也就是在 native 中,先看 java 发送出口:

/**

  • 打包之后的数据,和裸流数据
  • @param data
  • @param type
    */
    @Override
    public void onData(byte[] data, int type) {
    if (type == RtmpPacker.FIRST_AUDIO || type == RtmpPacker.AUDIO) {//音频 AAC 数据,已打包
    mPusherManager.pushAACData(data, data.length, type);
    } else if (type == RtmpPacker.FIRST_VIDEO ||
    type == RtmpPacker.INTER_FRAME || type == RtmpPacker.KEY_FRAME) {//H264 视频数据,已打包
    mPusherManager.pushH264(data, type, 0);
    } else if (type == RtmpPacker.PCM) { //PCM 裸流数据
    mPusherManager.pushPCM(data);
    } else if (type == RtmpPacker.YUV) { //YUV 裸流数据
    mPusherManager.pushYUV(data);
    }
    }

/**

  • 发送 H264 数据
  • @param h264
    /
    public native void pushH264(byte[] h264, int type, long timeStamp);
    /
    *
  • @param audio 直接推编码完成之后的音频流
  • @param length
  • @param timestamp
    /
    public native void pushAACData(byte[] audio, int length, int timestamp);
    /
    *
  • 发送 PCM 原始数据
  • @param audioData
    /
    public native void native_pushAudio(byte[] audioData);
    /
    *
  • push 视频原始 nv21
  • @param data
    */
    public native void native_push_video(byte[] data);

1. Rtmp 链接

Rtmp 底层是 TCP 协议,所以你可以使用 Java Socket 进行连接,也可以使用 c++ librtmp 库来进行连接,咱们这里就使用 librtmp 来进行连接。

/**

  • 真正 rtmp 连接的函数
    */
    void RTMPModel::onConnect() {

//1. 初始化
RTMP_Init(rtmp);
//2. 设置rtmp地址
int ret = RTMP_SetupURL(rtmp, this->url)

//3. 确认写入 rtmp
RTMP_EnableWrite(rtmp);
//4. 开始链接
ret = RTMP_Connect(rtmp, 0);
//5. 连接成功之后需要连接一个流
ret = RTMP_ConnectStream(rtmp, 0);

}

2. Native 音频模块接收 AAC Flv 打包数据

/**

  • 直接推送 AAC 硬编码
  • @param data
    */
    void AudioEncoderChannel::pushAAC(u_char *data, int dataLen, long timestamp) {
    RTMPPacket *packet = (RTMPPacket *) malloc(sizeof(RTMPPacket));
    RTMPPacket_Alloc(packet, dataLen);
    RTMPPacket_Reset(packet);
    packet->m_nChannel = 0x05; //音频
    memcpy(packet->m_body, data, dataLen);
    packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
    packet->m_hasAbsTimestamp = FALSE;
    packet->m_packetType = RTMP_PACKET_TYPE_AUDIO;
    packet->m_nBodySize = dataLen;
    if (mAudioCallback)
    mAudioCallback(packet); //发送给 rtmp 模块
    }

3. Native 视频模块接收 H264 Flv 打包数据

学习福利

【Android 详细知识点思维脑图(技能树)】

其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。

虽然 Android 没有前几年火热了,已经过去了会四大组件就能找到高薪职位的时代了。这只能说明 Android 中级以下的岗位饱和了,现在高级工程师还是比较缺少的,很多高级职位给的薪资真的特别高(钱多也不一定能找到合适的),所以努力让自己成为高级工程师才是最重要的。

这里附上上述的面试题相关的几十套字节跳动,京东,小米,腾讯、头条、阿里、美团等公司19年的面试题。把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。

由于篇幅有限,这里以图片的形式给大家展示一小部分。

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!
*data, int dataLen, long timestamp) {
RTMPPacket *packet = (RTMPPacket *) malloc(sizeof(RTMPPacket));
RTMPPacket_Alloc(packet, dataLen);
RTMPPacket_Reset(packet);
packet->m_nChannel = 0x05; //音频
memcpy(packet->m_body, data, dataLen);
packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
packet->m_hasAbsTimestamp = FALSE;
packet->m_packetType = RTMP_PACKET_TYPE_AUDIO;
packet->m_nBodySize = dataLen;
if (mAudioCallback)
mAudioCallback(packet); //发送给 rtmp 模块
}

3. Native 视频模块接收 H264 Flv 打包数据

学习福利

【Android 详细知识点思维脑图(技能树)】

[外链图片转存中…(img-QlaQH362-1715432166834)]

其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。

虽然 Android 没有前几年火热了,已经过去了会四大组件就能找到高薪职位的时代了。这只能说明 Android 中级以下的岗位饱和了,现在高级工程师还是比较缺少的,很多高级职位给的薪资真的特别高(钱多也不一定能找到合适的),所以努力让自己成为高级工程师才是最重要的。

这里附上上述的面试题相关的几十套字节跳动,京东,小米,腾讯、头条、阿里、美团等公司19年的面试题。把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。

由于篇幅有限,这里以图片的形式给大家展示一小部分。

[外链图片转存中…(img-Y72SDjS3-1715432166835)]

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

  • 16
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android平台上实现RTMP推流可以通过使用第三方的库或者自己编写相关代码来实现。下面是一个简单的步骤来实现Android平台上的RTMP推流: 1. 导入第三方库:首先,需要将第三方库添加到Android项目中。目前较为常用的第三方库有librtmp、ffmpeg等。 2. 初始化推流参数:在开始推流之前,需要初始化相关的推流参数,例如RTMP服务器地址、推流地址等。可以通过设置参数为其赋值,确保推流的正确性。 3. 创建推流线程:为了避免在主线程中执行推流操作导致界面卡顿,可以在新的线程中执行推流操作。可以通过创建一个推流线程来实现。 4. 连接RTMP服务器:使用已经设置好的RTMP服务器地址,建立与服务器的连接。连接成功后即可开始推流。 5. 采集视频、音频:通过Android平台提供的相应API,可以采集相机的视频数据和麦克风的音频数据。可以使用Camera和MediaRecorder类来进行视频的采集和编码,使用AudioRecord类来进行音频的采集和编码。 6. 推流:将采集到的视频、音频数据进行编码后,使用RTMP协议将数据发送给服务器。可以使用librtmp库提供的接口或者使用第三方库提供的特定接口来实现推流操作。 7. 结束推流:当推流完成或者需要停止推流时,需要释放相关资源并断开与RTMP服务器的连接。 需要注意的是,实现RTMP推流的过程中需要根据具体需求来设置相应的配置并处理异常情况。同时,还需要对Android相机、音频等操作有一定的了解,并进行适当的错误处理和资源管理。 以上是一种简单的实现RTMP推流的方式,具体实现可能涉及的内容较多,还需根据具体的项目需求进行相应的调整和优化。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值