Android-音视频学习系列-(九)Android-端实现-rtmp-推流(2)

这篇具有很好参考价值的文章主要介绍了Android-音视频学习系列-(九)Android-端实现-rtmp-推流(2)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

配置好之后,检查一下 AudioRecord 当前的状态是否可以进行录制,可以通过 AudioRecord##getState 来获取当前的状态:

  • STATE_UNINITIALIZED 还没有初始化,或者初始化失败了
  • STATE_INITIALIZED 已经初始化成功了。

2. 开启采集

创建好 AudioRecord 之后,就可以开启音频数据的采集了,可以通过调用下面的函数进行控制麦克风的采集:

mAudioRecord.startRecording();

3. 提取数据

执行完上一步之后,需要开启一个子线程用于不断的从 AudioRecord 缓冲区读取 PCM 数据,调用如下函数进行读取数据:

int read(@NonNull byte[] audioData, int offsetInBytes, int sizeInBytes);

4. 停止采集

如果想要停止采集,那么只需要调用 AudioRecord 的 stop 方法来实现,最后可以通过一个变量先控制子线程停止读取数据,然后在调用 stop 停止最后释放 AudioRecord 实例。

public void stopEncode() {
//停止的变量标记
mStopFlag = true;
if(mAudioEncoder != null) {
//停止采集
mAudioEncoder.stop();
//释放内存
mAudioEncoder = null;
}
}

视频采集

视频画面的采集主要是使用各个平台提供的摄像头 API 来实现的,在为摄像头设置了合适的参数之后,将摄像头实时采集的视频帧渲染到屏幕上提供给用户预览,然后将该视频帧传递给编码通道,进行编码。

1. 权限配置

2. 打开摄像头

2.1 检查摄像头

public static void checkCameraService(Context context)
throws CameraDisabledException {
// Check if device policy has disabled the camera.
DevicePolicyManager dpm = (DevicePolicyManager) context.getSystemService(
Context.DEVICE_POLICY_SERVICE);
if (dpm.getCameraDisabled(null)) {
throw new CameraDisabledException();
}
}

2.2 检查摄像头的个数

检查完摄像头服务后,还需要检查手机上摄像头的个数,如果个数为 0,则说明手机上没有摄像头,这样的话也是不能进行后续操作的。

public static List getAllCamerasData(boolean isBackFirst) {
ArrayList cameraDatas = new ArrayList<>();
Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
int numberOfCameras = Camera.getNumberOfCameras();
for (int i = 0; i < numberOfCameras; i++) {
Camera.getCameraInfo(i, cameraInfo);
if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
CameraData cameraData = new CameraData(i, CameraData.FACING_FRONT);
if(isBackFirst) {
cameraDatas.add(cameraData);
} else {
cameraDatas.add(0, cameraData);
}
} else if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
CameraData cameraData = new CameraData(i, CameraData.FACING_BACK);
if(isBackFirst) {
cameraDatas.add(0, cameraData);
} else {
cameraDatas.add(cameraData);
}
}
}
return cameraDatas;
}

在上面的方法中,需要传入一个是否先开启背面摄像头的 boolean 变量,如果变量为 true,则把背面摄像头放在列表第一个,之后打开摄像头的时候,直接获取列表中第一个摄像头相关参数,然后进行打开。这样的设计使得切换摄像头也变得十分简单,切换摄像头时,先关闭当前摄像头,然后变化摄像头列表中的顺序,然后再打开摄像头即可,也就是每次打开摄像头都打开摄像头列表中第一个摄像头参数所指向的摄像头。

2.3 打开摄像头

打开摄像头之前,先从摄像头列表中获取第一个摄像头参数,之后根据参数中的 CameraId 来打开摄像头,打开成功后改变相关状态。相关代码如下:

public synchronized Camera openCamera()
throws CameraHardwareException, CameraNotSupportException {
CameraData cameraData = mCameraDatas.get(0);
if(mCameraDevice != null && mCameraData == cameraData) {
return mCameraDevice;
}
if (mCameraDevice != null) {
releaseCamera();
}
try {
Log.d(TAG, "open camera " + cameraData.cameraID);
mCameraDevice = Camera.open(cameraData.cameraID);
} catch (RuntimeException e) {
Log.e(TAG, “fail to connect Camera”);
throw new CameraHardwareException(e);
}
if(mCameraDevice == null) {
throw new CameraNotSupportException();
}
mCameraData = cameraData;
mState = State.OPENED;
return mCameraDevice;
}

上面需要注意的是,在 Android 提供的 Camera 源码中,Camera.open(cameraData.cameraID) 抛出异常则说明Camera 不可用,否则说明 Camera 可用,但是在一些手机上 Camera.open(cameraData.cameraID) 不是抛出异常,而是返回 null。

3. 配置摄像头参数

在给摄像头设置参数后,需要记录这些参数,以方便其他地方使用。比如记录当前摄像头是否有闪光点,从而可以决定 UI 界面上是否显示打开闪光灯按钮。在直播项目中使用 CameraData 来记录这些参数,CameraData 类如下所示:

public class CameraData {
public static final int FACING_FRONT = 1;
public static final int FACING_BACK = 2;

public int cameraID; //camera的id
public int cameraFacing; //区分前后摄像头
public int cameraWidth; //camera的采集宽度
public int cameraHeight; //camera的采集高度
public boolean hasLight; //camera是否有闪光灯
public int orientation; //camera旋转角度
public boolean supportTouchFocus; //camera是否支持手动对焦
public boolean touchFocusMode; //camera是否处在自动对焦模式

public CameraData(int id, int facing, int width, int height){
cameraID = id;
cameraFacing = facing;
cameraWidth = width;
cameraHeight = height;
}

public CameraData(int id, int facing) {
cameraID = id;
cameraFacing = facing;
}
}

给摄像头设置参数的时候,有一点需要注意:设置的参数不生效会抛出异常,因此需要每个参数单独设置,这样就避免一个参数不生效后抛出异常,导致之后所有的参数都没有设置。

4. 摄像头开启预览

设置预览界面有两种方式:1、通过 SurfaceView 显示;2、通过 GLSurfaceView 显示。当为 SurfaceView 显示时,需要传给 Camera 这个 SurfaceView 的 SurfaceHolder。当使用 GLSurfaceView 显示时,需要使用Renderer 进行渲染,先通过 OpenGL 生成纹理,通过生成纹理的纹理 id 生成 SurfaceTexture ,将SurfaceTexture 交给 Camera ,那么在 Render 中便可以使用这个纹理进行相应的渲染,最后通过GLSurfaceView 显示。

4.1 设置预览回调

public static void setPreviewFormat(Camera camera, Camera.Parameters parameters) {
//设置预览回调的图片格式
try {
parameters.setPreviewFormat(ImageFormat.NV21);
camera.setParameters(parameters);
} catch (Exception e) {
e.printStackTrace();
}
}

当设置预览好预览回调的图片格式后,需要设置预览回调的 Callback。

Camera.PreviewCallback myCallback = new Camera.PreviewCallback() {
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
//得到相应的图片数据
//Do something
}
};
public static void setPreviewCallback(Camera camera, Camera.PreviewCallback callback) {
camera.setPreviewCallback(callback);
}

Android 推荐的 PreViewFormat 是 NV21,在 PreviewCallback 中会返回 Preview 的 N21 图片。如果是软编的话,由于 H264 支持 I420 的图片格式,因此需要将 N21格式转为 I420 格式,然后交给 x264 编码库。如果是硬编的话,由于 Android 硬编编码器支持 I420(COLOR_FormatYUV420Planar) 和NV12(COLOR_FormatYUV420SemiPlanar),因此可以将 N21 的图片转为 I420 或者 NV12 ,然后交给硬编编码器。

4.2 设置预览图像大小

在摄像头相关处理中,一个比较重要的是 屏幕显示大小和摄像头预览大小比例不一致 的处理。在 Android 中,摄像头有一系列的 PreviewSize,我们需要从中选出适合的 PreviewSize 。选择合适的摄像头 PreviewSize 的代码如下所示:

public static Camera.Size getOptimalPreviewSize(Camera camera, int width, int height) {
Camera.Size optimalSize = null;
double minHeightDiff = Double.MAX_VALUE;
double minWidthDiff = Double.MAX_VALUE;
List<Camera.Size> sizes = camera.getParameters().getSupportedPreviewSizes();
if (sizes == null) return null;
//找到宽度差距最小的
for(Camera.Size size:sizes){
if (Math.abs(size.width - width) < minWidthDiff) {
minWidthDiff = Math.abs(size.width - width);
}
}
//在宽度差距最小的里面,找到高度差距最小的
for(Camera.Size size:sizes){
if(Math.abs(size.width - width) == minWidthDiff) {
if(Math.abs(size.height - height) < minHeightDiff) {
optimalSize = size;
minHeightDiff = Math.abs(size.height - height);
}
}
}
return optimalSize;
}

public static void setPreviewSize(Camera camera, Camera.Size size, Camera.Parameters parameters) {
try {
parameters.setPreviewSize(size.width, size.height);
camera.setParameters(parameters);
}
catch (Exception e) {
e.printStackTrace();
}
}

在设置好最适合的 PreviewSize 之后,将 size 信息存储在 CameraData 中。当选择了 SurfaceView 显示的方式,可以将 SurfaceView 放置在一个 LinearLayout 中,然后根据摄像头 PreviewSize 的比例改变 SurfaceView 的大小,从而使得两者比例一致,确保图像正常。当选择了GLSurfaceView 显示的时候,可以通过裁剪纹理,使得纹理的大小比例和 GLSurfaceView 的大小比例保持一致,从而确保图像显示正常。

4.3 图像旋转

在 Android 中摄像头出来的图像需要进行一定的旋转,然后才能交给屏幕显示,而且如果应用支持屏幕旋转的话,也需要根据旋转的状况实时调整摄像头的角度。在 Android 中旋转摄像头图像同样有两种方法,一是通过摄像头的 setDisplayOrientation(result) 方法,一是通过 OpenGL 的矩阵进行旋转。下面是通过setDisplayOrientation(result) 方法进行旋转的代码:

public static int getDisplayRotation(Activity activity) {
int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
switch (rotation) {
case Surface.ROTATION_0: return 0;
case Surface.ROTATION_90: return 90;
case Surface.ROTATION_180: return 180;
case Surface.ROTATION_270: return 270;
}
return 0;
}

public static void setCameraDisplayOrientation(Activity activity, int cameraId, Camera camera) {
// See android.hardware.Camera.setCameraDisplayOrientation for
// documentation.
Camera.CameraInfo info = new Camera.CameraInfo();
Camera.getCameraInfo(cameraId, info);
int degrees = getDisplayRotation(activity);
int result;
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
result = (info.orientation + degrees) % 360;
result = (360 - result) % 360; // compensate the mirror
} else { // back-facing
result = (info.orientation - degrees + 360) % 360;
}
camera.setDisplayOrientation(result);
}

4.4 设置预览帧率

通过 Camera.Parameters 中 getSupportedPreviewFpsRange() 可以获得摄像头支持的帧率变化范围,从中选取合适的设置给摄像头即可。相关的代码如下:

public static void setCameraFps(Camera camera, int fps) {
Camera.Parameters params = camera.getParameters();
int[] range = adaptPreviewFps(fps, params.getSupportedPreviewFpsRange());
params.setPreviewFpsRange(range[0], range[1]);
camera.setParameters(params);
}

private static int[] adaptPreviewFps(int expectedFps, List<int[]> fpsRanges) {
expectedFps *= 1000;
int[] closestRange = fpsRanges.get(0);
int measure = Math.abs(closestRange[0] - expectedFps) + Math.abs(closestRange[1] - expectedFps);
for (int[] range : fpsRanges) {
if (range[0] <= expectedFps && range[1] >= expectedFps) {
int curMeasure = Math.abs(range[0] - expectedFps) + Math.abs(range[1] - expectedFps);
if (curMeasure < measure) {
closestRange = range;
measure = curMeasure;
}
}
}
return closestRange;
}

4.5 设置相机对焦

一般摄像头对焦的方式有两种:手动对焦和触摸对焦。下面的代码分别是设置自动对焦和触摸对焦的模式:

public static void setAutoFocusMode(Camera camera) {
try {
Camera.Parameters parameters = camera.getParameters();
List focusModes = parameters.getSupportedFocusModes();
if (focusModes.size() > 0 && focusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
camera.setParameters(parameters);
} else if (focusModes.size() > 0) {
parameters.setFocusMode(focusModes.get(0));
camera.setParameters(parameters);
}
} catch (Exception e) {
e.printStackTrace();
}
}

public static void setTouchFocusMode(Camera camera) {
try {
Camera.Parameters parameters = camera.getParameters();
List focusModes = parameters.getSupportedFocusModes();
if (focusModes.size() > 0 && focusModes.contains(Camera.Parameters.FOCUS_MODE_AUTO)) {
parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
camera.setParameters(parameters);
} else if (focusModes.size() > 0) {
parameters.setFocusMode(focusModes.get(0));
camera.setParameters(parameters);
}
} catch (Exception e) {
e.printStackTrace();
}
}

对于自动对焦这样设置后就完成了工作,但是对于触摸对焦则需要设置对应的对焦区域。要准确地设置对焦区域,有三个步骤:一、得到当前点击的坐标位置;二、通过点击的坐标位置转换到摄像头预览界面坐标系统上的坐标;三、根据坐标生成对焦区域并且设置给摄像头。整个摄像头预览界面定义了如下的坐标系统,对焦区域也需要对应到这个坐标系统中。

Android-音视频学习系列-(九)Android-端实现-rtmp-推流(2),2024年程序员学习,android,音视频,学习

如果摄像机预览界面是通过 SurfaceView 显示的则比较简单,由于要确保不变形,会将 SurfaceView 进行拉伸,从而使得 SurfaceView 和预览图像大小比例一致,因此整个 SurfaceView 相当于预览界面,只需要得到当前点击点在整个 SurfaceView 上对应的坐标,然后转化为相应的对焦区域即可。如果摄像机预览界面是通过GLSurfaceView 显示的则要复杂一些,由于纹理需要进行裁剪,才能使得显示不变形,这样的话,我们要还原出整个预览界面的大小,然后通过当前点击的位置换算成预览界面坐标系统上的坐标,然后得到相应的对焦区域,然后设置给摄像机。当设置好对焦区域后,通过调用 Camera 的 autoFocus() 方法即可完成触摸对焦。 整个过程代码量较多,请自行阅读项目源码。

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 类介绍。

Android-音视频学习系列-(九)Android-端实现-rtmp-推流(2),2024年程序员学习,android,音视频,学习

代码具体实现如下:

//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,如下所示:

Android-音视频学习系列-(九)Android-端实现-rtmp-推流(2),2024年程序员学习,android,音视频,学习

交叉编译脚本如下:

#!/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;

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
Android-音视频学习系列-(九)Android-端实现-rtmp-推流(2),2024年程序员学习,android,音视频,学习
Android-音视频学习系列-(九)Android-端实现-rtmp-推流(2),2024年程序员学习,android,音视频,学习
Android-音视频学习系列-(九)Android-端实现-rtmp-推流(2),2024年程序员学习,android,音视频,学习
Android-音视频学习系列-(九)Android-端实现-rtmp-推流(2),2024年程序员学习,android,音视频,学习
Android-音视频学习系列-(九)Android-端实现-rtmp-推流(2),2024年程序员学习,android,音视频,学习
Android-音视频学习系列-(九)Android-端实现-rtmp-推流(2),2024年程序员学习,android,音视频,学习
Android-音视频学习系列-(九)Android-端实现-rtmp-推流(2),2024年程序员学习,android,音视频,学习

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
Android-音视频学习系列-(九)Android-端实现-rtmp-推流(2),2024年程序员学习,android,音视频,学习

自学编程路线、面试题集合/面经、及系列技术文章等,资源持续更新中…

Android-音视频学习系列-(九)Android-端实现-rtmp-推流(2),2024年程序员学习,android,音视频,学习

ut;
//开始编码
int ret = x264_encoder_encode(mVideoCodec, &pp_nal, &pi_nal, pic_in, &pic_out);
if (!ret) {
LOGE(“编码失败”);
continue;
}
//如果是关键帧
int sps_len = 0;

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-MnPucfT0-1711916466684)]
[外链图片转存中…(img-K5Cp8myo-1711916466685)]
[外链图片转存中…(img-5CJ2bp5N-1711916466686)]
[外链图片转存中…(img-JD4irCbo-1711916466687)]
[外链图片转存中…(img-DzLaX3OR-1711916466687)]
[外链图片转存中…(img-nw2aT6To-1711916466688)]
Android-音视频学习系列-(九)Android-端实现-rtmp-推流(2),2024年程序员学习,android,音视频,学习

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
[外链图片转存中…(img-mniGFlP9-1711916466689)]

自学编程路线、面试题集合/面经、及系列技术文章等,资源持续更新中…

[外链图片转存中…(img-oSEfASTC-1711916466689)]

本文已被CODING开源项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》收录文章来源地址https://www.toymoban.com/news/detail-848334.html

到了这里,关于Android-音视频学习系列-(九)Android-端实现-rtmp-推流(2)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Android-音视频学习系列-(八)基于-Nginx-搭建(rtmp、http)直播服务器

    #!/bin/sh HTTP_FLV_MODULE_PATH=…/nginx-http-flv-module-1.2.7 OpenSSL_PATH=…/openssl-1.1.1d #–prefix=./bin 代表编译完成之后输出的路径地址 #–add-module 将拓展模块添加到当前一起编译 ./configure --prefix=./bin –add-module= H T T P F L V M O D U L E P A T H   − − w i t h − o p e n s s l = HTTP_FLV_MODULE_PATH --with

    2024年04月15日
    浏览(61)
  • 音视频开发---ffmpeg rtmp推流

    推流是将输入视频数据推送至流媒体服务器, 输入视频数据可以是本地视频文件(avi,mp4,flv......),也可以是内存视频数据,或者摄像头等系统设备,也可以是网络流URL。本篇介绍将本地视频文件通过FFmpeg编程以RTMP直播流的形式推送至RTMP流媒体服务器的方法。 推流的网络拓扑

    2024年02月16日
    浏览(78)
  • JavaCV音视频开发宝典:使用javacv读取GB28181、海康大华平台和网络摄像头sdk回调视频码流并转码推流rtmp流媒体服务

    《JavaCV音视频开发宝典》专栏目录导航 《JavaCV音视频开发宝典》专栏介绍和目录 本篇文章用于解决javacv接入h264/hevc裸流或者接入ps/ts流等字节流的非流媒体协议视频源接入并推流到rtmp流媒体服务。 本篇文章适用于gb28181/海康大华网络摄像机设备sdk对接以及海康大华等视频平

    2023年04月09日
    浏览(57)
  • 音视频开发系列(10):基于qt的音频推流

    今天分享一下利用qt录制音频,然后再利用ffmpeg推流到nginx服务器,最后再利用vlc进行拉流的demo。 首先介绍一下如何利用qt来进行音频的录制,qt的音频录制主要利用qt的QAudioFormat先进行音频信息的配置。主要需要配置以下的信息: 然后使用QAudioDeviceInfo来获取是否支持改设置

    2024年02月02日
    浏览(55)
  • 音视频开发系列(7):完成本地摄像头直播推流

    今天把读取本地摄像头将视频流推流到nginx服务器的直播代码学习完了,这里对代码的流程做一下记录,以便以后进行复习。 这边用到了opencv和ffmpeg的开源库(PS:在前面有进行分享),配置环境在之前也有进行分享。 第一步:先用到了opencv的VideoCapture类的open函数打开摄像头,

    2024年02月02日
    浏览(48)
  • Android平台音视频推送选RTMP还是GB28181?

    早在2015年,我们发布了RTMP直播推送模块,那时候音视频直播这块场景需求,还不像现在这么普遍,我们做这块的初衷,主要是为了实现移动单兵应急指挥系统的低延迟音视频数据传输。好多开发者可能会疑惑,走RTMP怎么可能低延迟?网上看到的RTMP推拉流延迟,总归要2-3秒起

    2024年02月10日
    浏览(42)
  • 音视频学习(二十一)——rtmp收流(tcp方式)

    本文主要介绍rtmp协议收流流程,在linux上搭建rtmp服务器,通过自研的rtmp收流库发起取流请求,使用ffmpeg+qt实现视频流的解码与播放。 关于rtmp协议基础介绍可查看:https://blog.csdn.net/www_dong/article/details/131026072 下载nginx 解压,将nginx-rtmp-module拷贝至nginx-1.24.0目录,如下所示:

    2024年02月03日
    浏览(48)
  • Android-音视频学习系列-(二)-交叉编译动态库、静态库的入门学习

    gcc -S test.i -o test.s//-S 的作用是编译结束生成汇编文件。 汇编阶段 汇编阶段把 .S 文件翻译成二进制机器指令文件 .o ,这个阶段接收.c ,.i ,.s 的文件都没有问题。 下面我们通过以下命令生成二进制机器指令文件 .o 文件: gcc -c test.s -o test.o 链接阶段 链接阶段,链接的是函数库。

    2024年04月09日
    浏览(100)
  • 音视频开发 RTMP协议发送H.264编码及AAC编码的音视频(C++实现)

    RTMP(Real Time Messaging Protocol)是专门用来传输音视频数据的流媒体协议,最初由Macromedia 公司创建,后来归Adobe公司所有,是一种私有协议,主要用来联系Flash Player和RtmpServer,如 FMS , Red5 , crtmpserver 等。RTMP协议可用于实现直播、点播应用,通过 FMLE(Flash Media Live Encoder) 推送音

    2023年04月08日
    浏览(74)
  • Android平台一对一音视频通话方案对比:WebRTC VS RTMP VS RTSP

    一对一音视频通话使用场景 一对一音视频通话都需要稳定、清晰和流畅,以确保良好的用户体验,常用的使用场景如下: 社交应用 :社交应用是一种常见的使用场景,用户可以通过音视频通话进行面对面的交流; 在线教育: 老师和学生可以通过音视频通话功能进行实时互

    2024年02月13日
    浏览(48)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包