在之前完成的实战项目【FFmpeg音视频播放器】属于拉流范畴,接下来将完成推流工作,通过RTMP实现推流,即直播客户端。简单的说,就是将手机采集的音频数据和视频数据,推到服务器端。
接下来的RTMP直播客户端系列,主要实现红框和紫色部分:
本节主要内容:
1.RTMP理论;
2.RtmpDump集成;
3.X264集成;
4.Camera画面预览。
源码:
https://gitee.com/sziitjim/ndk-push
一:RTMP理论
RTMP(Real Time Messaging Protocol)实时消息传送协议,是为播放器和服务器之间音频、视频和数据传输 开发的开放协议。类似:HTTP,HTTP一样,都属于TCP/IP四层模型的应用层。
RTMP直播实现流程:
推流:视频+音频
视频:Camera采集-->封装(压缩)--->rtmp包--->发送服务器
音频:AudioRecord-->封装(压缩)--->rtmp包--->发送服务器
二、RtmpDump集成
RtmpDump是一个用来处理RTMP流媒体的开源工具包。它能够单独使用进行RTMP的通信,也可以集成到FFmpeg中通过FFmpeg接口来使用RTMPDump。
类比HTTP中的OkHttp库,RtmpDump在RTMP协议中,类似OkHttp的角色。在Android中可以直接借助NDK在JNI层调用RtmpDump来完成RTMP通信。
将RTMP纯C源码拷贝到cpp.librtmp目录下,借助CMakeLists.txt来进行编译,生成librtmp.a 文件。
三、X264集成
x264是一个C语言编写的目前对H.264标准支持最完善的编解码库。与RTMPDump一样,可以在Android中直接使用,也可以集成进入FFMpeg。
将交叉编译后的x264静态库文件,导入项目。
四、Camera画面预览
简单实现前置摄像和后置摄像头画面预览和布局实现,为后续直播客户端做准备。
1)初始化Camera
SurfaceView surfaceView = findViewById(R.id.surfaceView);
mSurfaceHolder = surfaceView.getHolder();
cameraHelper = new CameraHelper(this, Camera.CameraInfo.CAMERA_FACING_BACK, 640, 480);
2)切换摄像头并开始预览
/**
* 切换摄像头
*
* @param view
*/
public void switchCamera(View view) {
if (initPermission()) {
if (!isBind) {
cameraHelper.setPreviewDisplay(mSurfaceHolder);
isBind = true;
}
cameraHelper.switchCamera();
}
}
3)CameraHelper.java
package com.ndk.push;
import android.app.Activity;
import android.graphics.ImageFormat;
import android.hardware.Camera;
import android.util.Log;
import android.view.Surface;
import android.view.SurfaceHolder;
import com.ndk.push.util.LogUtil;
import java.io.IOException;
import java.util.Iterator;
import java.util.List;
import androidx.annotation.NonNull;
public class CameraHelper implements SurfaceHolder.Callback, Camera.PreviewCallback {
private static final String TAG = "CameraHelper";
private Activity mActivity;
private int mHeight; // 高
private int mWidth; // 宽
private int mCameraId; // 后摄 前摄像头
private Camera mCamera; // Camera1 预览采集图像数据
private byte[] buffer; // 数据
private SurfaceHolder mSurfaceHolder; // Surface画面的帮助
private Camera.PreviewCallback mPreviewCallback; // 后面预览的画面,把此预览的画面 的数据回调出现 --->DerryPush ---> C++层
private int mRotation; // 旋转画面相关的标识
private OnChangedSizeListener mOnChangedSizeListener; // 你的宽和高发生改变,就会回调此接口
public CameraHelper(Activity activity, int cameraId, int width, int height) {
mActivity = activity;
mCameraId = cameraId;
mWidth = width;
mHeight = height;
}
/**
* 切换摄像头
*/
public void switchCamera() {
if (mCameraId == Camera.CameraInfo.CAMERA_FACING_BACK) {
mCameraId = Camera.CameraInfo.CAMERA_FACING_FRONT;
} else {
mCameraId = Camera.CameraInfo.CAMERA_FACING_BACK;
}
stopPreview(); // 先停止预览
startPreview(); // 在开启预览
}
private void startPreview() {
LogUtil.i(TAG, "startPreview");
try {
// 获得camera对象
mCamera = Camera.open(mCameraId);
// 配置camera的属性
Camera.Parameters parameters = mCamera.getParameters();
// 设置预览数据格式为nv21
parameters.setPreviewFormat(ImageFormat.NV21); // yuv420类型的子集
// 这是摄像头宽、高
setPreviewSize(parameters);
// 设置摄像头 图像传感器的角度、方向
setPreviewOrientation(parameters);
mCamera.setParameters(parameters);
buffer = new byte[mWidth * mHeight * 3 / 2]; // 请看什么的细节
// 数据缓存区
mCamera.addCallbackBuffer(buffer);
mCamera.setPreviewCallbackWithBuffer(this);
// 设置预览画面
mCamera.setPreviewDisplay(mSurfaceHolder); // SurfaceView 和 Camera绑定
if (mOnChangedSizeListener != null) { // 你的宽和高发生改变,就会回调此接口
mOnChangedSizeListener.onChanged(mWidth, mHeight);
}
// 开启预览
mCamera.startPreview();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 旋转画面角度(因为默认预览是歪的,所以就需要旋转画面角度)
* 这个只是画面的旋转,但是数据不会旋转,你还需要额外处理
*
* @param parameters
*/
private void setPreviewOrientation(Camera.Parameters parameters) {
Camera.CameraInfo info = new Camera.CameraInfo();
Camera.getCameraInfo(mCameraId, info);
mRotation = mActivity.getWindowManager().getDefaultDisplay().getRotation();
int degrees = 0;
switch (mRotation) {
case Surface.ROTATION_0:
degrees = 0;
break;
case Surface.ROTATION_90: // 横屏 左边是头部(home键在右边)
degrees = 90;
break;
case Surface.ROTATION_180:
degrees = 180;
break;
case Surface.ROTATION_270:// 横屏 头部在右边
degrees = 270;
break;
}
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;
}
// 设置角度, 参考源码注释,从源码里面copy出来的,Google给出旋转的解释
mCamera.setDisplayOrientation(result);
}
/**
* 在设置宽和高的同时,能够打印 支持的分辨率
*
* @param parameters
*/
private void setPreviewSize(Camera.Parameters parameters) {
// 获取摄像头支持的宽、高
List supportedPreviewSizes = parameters.getSupportedPreviewSizes();
Camera.Size size = supportedPreviewSizes.get(0);
Log.d(TAG, "Camera支持: " + size.width + "x" + size.height);
// 选择一个与设置的差距最小的支持分辨率
int m = Math.abs(size.height * size.width - mWidth * mHeight);
supportedPreviewSizes.remove(0);
Iterator iterator = supportedPreviewSizes.iterator();
// 遍历
while (iterator.hasNext()) {
Camera.Size next = iterator.next();
Log.d(TAG, "支持 " + next.width + "x" + next.height);
int n = Math.abs(next.height * next.width - mWidth * mHeight);
if (n < m) {
m = n;
size = next;
}
}
mWidth = size.width;
mHeight = size.height;
parameters.setPreviewSize(mWidth, mHeight);
Log.d(TAG, "预览分辨率 width:" + size.width + " height:" + size.height);
}
/**
* 停止预览
*/
private void stopPreview() {
if (mCamera != null) {
// 预览数据回调接口
mCamera.setPreviewCallback(null);
// 停止预览
mCamera.stopPreview();
// 释放摄像头
mCamera.release();
mCamera = null;
}
}
/**
* 与Surface绑定 == surfaceView.getHolder()
*
* @param surfaceHolder
*/
public void setPreviewDisplay(SurfaceHolder surfaceHolder) {
mSurfaceHolder = surfaceHolder;
mSurfaceHolder.addCallback(this);
}
@Override
public void surfaceCreated(@NonNull SurfaceHolder holder) {
}
@Override
public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {
LogUtil.i(TAG, "surfaceChanged");
// 释放摄像头
stopPreview();
// 开启摄像头
startPreview();
}
@Override
public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
stopPreview(); // 只要画面不可见,就必须释放,因为预览耗电 耗资源
}
/**
* @param data 子集nv21 == YUV420类型的数据
* @param camera C++层 nv21不能用 必须换成 i420
*/
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
// 这个只是画面的旋转,但是数据不会旋转,你还需要额外处理
if (mPreviewCallback != null) {
mPreviewCallback.onPreviewFrame(data, camera); // byte[] data == nv21 ===> C++层 ---> 流媒体服务器
}
camera.addCallbackBuffer(buffer);
}
public void setPreviewCallback(Camera.PreviewCallback previewCallback) {
mPreviewCallback = previewCallback;
}
public void setOnChangedSizeListener(OnChangedSizeListener listener) {
mOnChangedSizeListener = listener;
}
public interface OnChangedSizeListener {
void onChanged(int width, int height);
}
}
源码:
https://gitee.com/sziitjim/ndk-push
准备工作完成,下一节开始推流工作。。。