NDK RTMP直播客户端一
在之前完成的实战项目【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();// 设置预览数据格式为nv21parameters.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-facingresult = (info.orientation - degrees + 360) % 360;}// 设置角度, 参考源码注释,从源码里面copy出来的,Google给出旋转的解释mCamera.setDisplayOrientation(result);}/* 在设置宽和高的同时,能够打印 支持的分辨率 @param parameters*/private void setPreviewSize(Camera.Parameters parameters) {// 获取摄像头支持的宽、高List<Camera.Size> 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<Camera.Size> 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);}@Overridepublic void surfaceCreated(@NonNull SurfaceHolder holder) {}@Overridepublic void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {LogUtil.i(TAG, "surfaceChanged");// 释放摄像头stopPreview();// 开启摄像头startPreview();}@Overridepublic void surfaceDestroyed(@NonNull SurfaceHolder holder) {stopPreview(); // 只要画面不可见,就必须释放,因为预览耗电 耗资源}/* @param data 子集nv21 == YUV420类型的数据* @param camera C++层 nv21不能用 必须换成 i420*/@Overridepublic 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
准备工作完成,下一节开始推流工作。。。