该工程目录是Android客户端推流环境搭建的工程基础下创建的(音频相关的类AudioChannel先不做):
视频推流:
视频推流的工作主要是这几个部分:
-
获取摄像头原始数据
这里要注意的是拿到后置摄像头原始数据后要进行旋转,原因如图: - 数据转码(NV21转I420)
Android摄像头拿到的数据是NV21,为了支持更多终端,需要转为I420 -
数据进行H264编码
为什么要编码?
视频是由一帧帧图像组成,就如常见的gif图片,如果打开一张gif图片,可以发现里面是由很多张图片组成。一般视频为了不让观众感觉到卡顿,一秒钟至少需要16帧画面(一般是30帧),假如该视频是一个1280×720分辨率的视频,那么不经过编码一秒钟的大小:结果:1280x720x60≈843.75M所以不经过编码的视频根本没法保存和传输。
H264编码
H264的数据中分为I帧、B帧、P帧,I帧是关键帧,与I帧相似程度极高达到95%以上编码成B帧,相似程度70%编码成P帧,所以不是每一帧都要传输完整的数据,只需要有与关键帧(I帧)的差异数据,就可以通过关键帧算出该帧的显示数据。如何编码不需要程序员来实现,已经由x264这个工具帮我们做了。除了I/P/B帧外,还有图像序列GOP,可以理解成一个场景,场景的物体都是相似的。图示:NALU单元:
为了方便传输(传输指 文件传输,网络流传输) 我们并不能把一整帧传输过去,一帧的内容太大了,还需要细分才能更方便的传输。如果通过传递一完整帧传过去,对方等的花都谢了。所以我们需要更小的传输单元以保证 更好的压缩性,容错性和实时观看性。这种更小的单元成为NALU单元,所以H264 原始码流(又称为裸流),是有一个接一个的 NALU 组成的,关于NALU的组成(组成可以不去了解,知道传输数据是以NALU为单位就可以了):
NALU = NALU头 + RBSP(切片)
RBSP = 片头 + 片数据
片数据 = n * 宏块
//把一张图片划分成若干个小的区域,这些小的区域称之为宏块
//H264默认是使用 16X16 大小的区域作为一个宏块,也可以划分成 8X8 大小。
所以:NALU = NALU头 + (片头 + n宏块)
- 组装RTMPPacket并发送
这里是把NALU的数据按照RTMP协议进行封装,然后传输里是把NALU的数据按照RTMP协议进行封装,然后传输
开撸之前还有个事,编码使用到的x264的参数配置超多,相关帖子也很多,需要先了解一下。
开撸
布局不贴了,一个SurfaceView, 三个按钮:开始直播、停止直播、切换摄像头,直接先用后置摄像头实现推流再说。局不贴了,一个SurfaceView, 三个按钮:开始直播、停止直播、切换摄像头,直接先用后置摄像头实现推流再说。
类的说明看上面的目录结构。的说明看上面的目录结构。
MainActivity:对LivePusher初始化(初始化参数中包括图像宽、高、传输码率、传输帧率、摄像头id,这些都是VideoChannel推流需要的)。通过LivePusher设置摄像头预览的界面、控制推流开关
public class MainActivity extends AppCompatActivity {
private LivePusher livePusher;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
SurfaceView surfaceView = findViewById(R.id.surfaceView);
livePusher = new LivePusher(this, 1920, 1080, 800_000, 10, Camera.CameraInfo.CAMERA_FACING_BACK);
// 设置摄像头预览的界面
livePusher.setPreviewDisplay(surfaceView.getHolder());
}
public void switchCamera(View view) {
livePusher.switchCamera();
}
public void startLive(View view) {
livePusher.startLive("rtmp://47.96.117.157/myapp");
}
public void stopLive(View view) {
livePusher.stopLive();
}
}
LivePusher:调度音、视频推流类,目前只实现了视频相关,所以目前任务是初始化VideoChannel,命令VideoChannel设置摄像头预览、开关视频推流。因为这些功能都是视频相关,所以具体实现都在VideoChannel中。native_init()其实也是对的native层的VideoChannel初始化。
public class LivePusher {
private AudioChannel audioChannel;
private VideoChannel videoChannel;
static {
System.loadLibrary("native-lib");
}
public LivePusher(Activity activity, int width, int height, int bitrate,
int fps, int cameraId) {
//对native层的VideoChannel进行初始化
native_init();
videoChannel = new VideoChannel(this, activity, width, height, bitrate, fps, cameraId);
audioChannel = new AudioChannel(this);
}
/**
* 设置摄像头预览
* @param surfaceHolder
*/
public void setPreviewDisplay(SurfaceHolder surfaceHolder) {
videoChannel.setPreviewDisplay(surfaceHolder);
}
/**
* 切换摄像头
*/
public void switchCamera() {
videoChannel.switchCamera();
}
/**
* 开始直播
* @param path
*/
public void startLive(String path) {
native_start(path);
videoChannel.startLive();
}
/**
* 停止直播
*/
public void stopLive() {
videoChannel.stopLive();
}
public native void native_init();
public native void native_setVideoEncInfo(int w, int h, int mFps, int mBitrate);
public native void native_start(String path);
public native void native_pushVideo(byte[] data);
}
先不看LivePusher中的native函数怎么实现,假设native是已经实现了希望的功能。先看java层的VideoChannel,VideoChannel的主要工作是同过CameraHelper打开摄像头预览,监听尺寸改变和摄像头数据回调。这里会发现java层的VideoChannel并没有调用native层的VideoChannel,而是都把数据传给了Java层的LivePusher,因为为了保证数据清晰,LivePusher才是唯一和native层连接的:
public class VideoChannel implements Camera.PreviewCallback, CameraHelper.OnChangedSizeListener {
private static final String TAG = "VideoChannel";
private CameraHelper cameraHelper;
private int mBitrate;
private int mFps;
private boolean isLiving;
LivePusher livePusher;
public VideoChannel(LivePusher livePusher, Activity activity, int width, int height, int bitrate, int fps, int cameraId) {
mBitrate = bitrate;
mFps = fps;
this.livePusher = livePusher;
cameraHelper = new CameraHelper(activity, cameraId, width, height);
cameraHelper.setPreviewCallback(this);
cameraHelper.setOnChangedSizeListener(this);
}
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
Log.i(TAG, "onPreviewFrame: ");
if (isLiving) {
Log.i(TAG, "push");
livePusher.native_pushVideo(data);
}
}
@Override
public void onChanged(int w, int h) {
livePusher.native_setVideoEncInfo(w, h, mFps, mBitrate);
}
public void switchCamera() {
cameraHelper.switchCamera();
}
public void setPreviewDisplay(SurfaceHolder surfaceHolder) {
cameraHelper.setPreviewDisplay(surfaceHolder);
}
public void startLive() {
isLiving = true;
}
public void stopLive() {
isLiving = false;
}
}
CameraHelper就是个摄像头预览工具类,除去正常的开启摄像头和预览外,还有两项工作:
- 在摄像头的onPreviewFrame和setPreviewOrientation回调里,把数据通过监听器传出去,也就是传给VideoChannel.
- 对摄像头数据进行旋转,注意这里是对数据进行旋转,不仅仅是预览画面旋转.
代码:
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;
private byte[] buffer;
private SurfaceHolder mSurfaceHolder;
private Camera.PreviewCallback mPreviewCallback;
private int mRotation;
private OnChangedSizeListener mOnChangedSizeListener;
byte[] bytes;
public CameraHelper(Activity activity, int cameraId, int width, int height) {
mActivity = activity;
mCameraId = cameraId;
mWidth = width;
mHeight = height;
}
/**
* 设置surfaceHolder
* @param surfaceHolder
*/
public void setPreviewDisplay(SurfaceHolder surfaceHolder) {
mSurfaceHolder = surfaceHolder;
mSurfaceHolder.addCallback(this);
}
/**
* SurfaceHolder.Callback
*/
@Override
public void surfaceCreated(SurfaceHolder holder) {
}
/**
* SurfaceHolder.Callback
*/
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
stopPreview();
startPreview();
}
/**
* SurfaceHolder.Callback
*/
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
stopPreview();
}
/**
* 停止预览
*/
private void stopPreview() {
if (mCamera != null) {
//预览数据回调接口
mCamera.setPreviewCallback(null);
//停止预览
mCamera.stopPreview();
//释放摄像头
mCamera.release();
mCamera = null;
}
}
/**
* 开始预览
*/
private void startPreview() {
try {
//获得camera对象
mCamera = Camera.open(mCameraId);
//配置camera的属性
Camera.Parameters parameters = mCamera.getParameters();
//设置预览数据格式为nv21
parameters.setPreviewFormat(ImageFormat.NV21);
//设置摄像头宽、高
setPreviewSize(parameters);
// 设置摄像头 图像传感器的角度、方向
setPreviewOrientation(parameters);
// 设置自动对焦
setFocusMode(parameters);
mCamera.setParameters(parameters);
// 大小由YUV格式决定
buffer = new byte[mWidth * mHeight * 3 / 2