视频直播系列(二):音视频数据采集

nginx服务器搭建完成以后,就可以着手客户端的开发了,首先是直播端,这一节主要是说明音频和视频数据的采集

视频数据采集思路

调用摄像头,前置和后置都需要,因此需要摄像头切换
视频开始直播和结束直播,需要在本地获得预览,选择surfaceview进行绘制
将获取到的视频信息传递给native,在底层进行视频推流
因此,视频数据采集只需要调用Android自带方法就可完成

音频数据采集思路

调用麦克风,采集收到的数据
当开始直播的时候将数据传递给native层进行推流
因此,音频推流也只需要调用Android自带方法即可

布局实现

从分析来看,需要两个按钮,一个发送开始结束信号,一个切换摄像头
还有一个SurfaceView布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >

<android.view.SurfaceView
android:id="@+id/surface_view"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_gravity="center" />

<Button
android:id="@+id/btn_camera_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:gravity="center"
android:text="切换摄像头"
android:onClick="mSwitchCamera"/>

<Button
android:id="@+id/btn_push"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:gravity="center"
android:text="开始直播"
android:onClick="mStartLive" />

</RelativeLayout>

Java文件

由于要传递音视频参数,这实现两个GetSet类
AudioParam.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class AudioParam {

private int sampleRateInHz = 44100;
private int channel = 1;

public AudioParam(int sampleRateInHz, int channel) {
super();
this.sampleRateInHz = sampleRateInHz;
this.channel = channel;
}

public int getSampleRateInHz() {
return sampleRateInHz;
}

public void setSampleRateInHz(int sampleRateInHz) {
this.sampleRateInHz = sampleRateInHz;
}

public int getChannel() {
return channel;
}

public void setChannel(int channel) {
this.channel = channel;
}
}

VideoParam.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class VideoParam {

private int width;
private int height;
private int cameraId;

public VideoParam(int width, int height, int cameraId) {
super();
this.width = width;
this.height = height;
this.cameraId = cameraId;
}

public int getWidth() {
return width;
}

public void setWidth(int width) {
this.width = width;
}

public int getHeight() {
return height;
}

public void setHeight(int height) {
this.height = height;
}

public int getCameraId() {
return cameraId;
}

public void setCameraId(int cameraId) {
this.cameraId = cameraId;
}
}

定义抽象类Pusher,让Video和Audio去实现其方法,其方法包括开始,结束,释放资源
Pusher.java

1
2
3
4
5
6
7
8
9
public abstract class Pusher {

public abstract void startPush();

public abstract void stopPush();

public abstract void release();

}

Video实现类
VideoPusher.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
import java.io.IOException;

import com.cj5785.livegetvideoaudio.params.VideoParam;

import android.hardware.Camera;
import android.hardware.Camera.CameraInfo;
import android.hardware.Camera.PreviewCallback;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceHolder.Callback;

public class VideoPusher extends Pusher implements Callback, PreviewCallback{

private Camera mCamera;
private boolean isPushing = false;
private SurfaceHolder surfaceHolder;
private VideoParam videoParam;
private byte[] buffers;

public VideoPusher(SurfaceHolder surfaceHolder, VideoParam videoParam) {
this.surfaceHolder = surfaceHolder;
this.videoParam = videoParam;
surfaceHolder.addCallback(this);
}

@Override
public void startPush() {
isPushing = true;
}

@Override
public void stopPush() {
isPushing = false;
}

@Override
public void release() {
stopPreview();
}

public void switchCamera() {
if(videoParam.getCameraId() == CameraInfo.CAMERA_FACING_BACK){
videoParam.setCameraId(CameraInfo.CAMERA_FACING_FRONT);
}else{
videoParam.setCameraId(CameraInfo.CAMERA_FACING_BACK);
}
//重新预览
stopPreview();
startPreview();
}

private void startPreview() {
try {
//SurfaceView初始化完成,开始相机预览
mCamera = Camera.open(videoParam.getCameraId());
mCamera.setPreviewDisplay(surfaceHolder);
//获取预览图像数据
buffers = new byte[videoParam.getWidth() * videoParam.getHeight() * 5 * 10];
mCamera.addCallbackBuffer(buffers);
mCamera.setPreviewCallbackWithBuffer(this);

mCamera.startPreview();
} catch (IOException e) {
e.printStackTrace();
}
}

private void stopPreview() {
if(mCamera != null){
mCamera.stopPreview();
mCamera.release();
mCamera = null;
}
}

@Override
public void onPreviewFrame(byte[] data, Camera camera) {
if(mCamera != null){
mCamera.addCallbackBuffer(buffers);
}
if(isPushing){
//回调函数中获取图像数据,然后给Native代码编码
Log.d("cj5785","start video");
}
}

@Override
public void surfaceCreated(SurfaceHolder holder) {
startPreview();
}

@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {}

@Override
public void surfaceDestroyed(SurfaceHolder holder) {}

}

Audio实现类
AudioPusher.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import com.cj5785.livegetvideoaudio.params.AudioParam;

import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder.AudioSource;
import android.util.Log;

public class AudioPusher extends Pusher{

private AudioRecord audioRecord;
private boolean isPushing = false;
private int minBufferSize;

public AudioPusher(AudioParam audioParam) {
int channelConfig = audioParam.getChannel() == 1 ?
AudioFormat.CHANNEL_IN_MONO : AudioFormat.CHANNEL_IN_STEREO;

//最小缓冲区大小
minBufferSize = AudioRecord.getMinBufferSize(audioParam.getSampleRateInHz(),
channelConfig, AudioFormat.ENCODING_PCM_16BIT);

audioRecord = new AudioRecord(AudioSource.MIC, audioParam.getSampleRateInHz(),
channelConfig, AudioFormat.ENCODING_PCM_16BIT, minBufferSize);
}


@Override
public void startPush() {
isPushing = true;
//启动一个录音子线程
new Thread(new AudioRecordTask()).start();
}

@Override
public void stopPush() {
isPushing = false;
audioRecord.stop();
}

@Override
public void release() {
if(audioRecord != null){
audioRecord.release();
audioRecord = null;
}
}

class AudioRecordTask implements Runnable{

@Override
public void run() {
//开始录音
audioRecord.startRecording();

while(isPushing){
//通过AudioRecord不断读取音频数据
byte[] buffer = new byte[minBufferSize];
int len = audioRecord.read(buffer, 0, buffer.length);
if(len > 0){
//传给Native代码,进行音频编码
Log.d("cj5785","start record");
}
}
}
}
}

另外还需要一个实例化Video和Audio的类
LivePusher.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import com.cj5785.livegetvideoaudio.params.AudioParam;
import com.cj5785.livegetvideoaudio.params.VideoParam;

import android.hardware.Camera.CameraInfo;
import android.view.SurfaceHolder;
import android.view.SurfaceHolder.Callback;

public class LivePusher implements Callback{
private SurfaceHolder surfaceHolder;
private VideoPusher videoPusher;
private AudioPusher audioPusher;

public LivePusher(SurfaceHolder surfaceHolder) {
this.surfaceHolder = surfaceHolder;
surfaceHolder.addCallback(this);
prepare();
}


private void prepare() {
//实例化视频推流器
VideoParam videoParam = new VideoParam(320, 240, CameraInfo.CAMERA_FACING_BACK);
videoPusher = new VideoPusher(surfaceHolder,videoParam);

//实例化音频推流器
AudioParam audioParam = new AudioParam(44100, 1);
audioPusher = new AudioPusher(audioParam);
}

public void switchCamera() {
videoPusher.switchCamera();
}

public void startPush(String url) {
videoPusher.startPush();
audioPusher.startPush();
}

public void stopPush() {
videoPusher.stopPush();
audioPusher.stopPush();
}

private void release() {
videoPusher.release();
audioPusher.release();
}

@Override
public void surfaceCreated(SurfaceHolder holder) {}

@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {}

@Override
public void surfaceDestroyed(SurfaceHolder holder) {
stopPush();
release();
}
}

最后是主活动类
MainActivity.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import com.cj5785.livegetvideoaudio.pusher.LivePusher;

import android.app.Activity;
import android.os.Bundle;
import android.view.SurfaceView;
import android.view.View;
import android.widget.Button;

public class MainActivity extends Activity {

public static final String URL = "";
private LivePusher live;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
SurfaceView surfaceView = (SurfaceView) findViewById(R.id.surface_view);
live = new LivePusher(surfaceView.getHolder());
}

public void mStartLive(View view) {
Button btn = (Button)view;
if(btn.getText().equals("开始直播")){
live.startPush(URL);
btn.setText("停止直播");
}else{
live.stopPush();
btn.setText("开始直播");
}
}

public void mSwitchCamera(View view) {
live.switchCamera();
}
}

效果查看

在发布到手机上以后,摄像头前后切换正常
在点击开始直播按钮的时候,会不断打印出start videostart audio,在数据采集这一块到这里就可以继续接下来的数据编码了

Donate comment here