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四层模型的应用层。

NDK RTMP直播客户端一_第1张图片

 RTMP直播实现流程:

NDK RTMP直播客户端一_第2张图片

 推流:视频+音频

视频: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 文件。

NDK RTMP直播客户端一_第3张图片

 三、X264集成

x264是一个C语言编写的目前对H.264标准支持最完善的编解码库。与RTMPDump一样,可以在Android中直接使用,也可以集成进入FFMpeg。

将交叉编译后的x264静态库文件,导入项目。

NDK RTMP直播客户端一_第4张图片

 四、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

准备工作完成,下一节开始推流工作。。。

你可能感兴趣的:(NDK,NDK,rtmp,直播客户端,推流)