0%

Android 音视频任务3

任务3. 在 Android 平台使用 Camera API 进行视频的采集,分别使用 SurfaceView、TextureView 来预览 Camera 数据,取到 NV21 的数据回调

由于这次的任务牵涉面十分广,所以用了很久才搞懂了一些知识,以后的任务也应该会做的越来越慢,不过十分合理,慢工出细活,上来一会儿就完成的东西,要不就是含金量不高,要不就是没有深究,对于学习阶段,虽然说有时候要管中窥豹不要太深究,但有些事情不搞清楚就不能算掌握知识了对吧?

下面是使用CameraAPI进行拍照和录视频,当然还有预览的全过程。虽然CameraAPI在5.0后就被弃用了(原因之一是它不能同时预览和拍摄),取而代之的是camera2,但由于众所周知的原因,仍然要好好学习。

添加权限后,如果是android6以上的,必须在设置的应用里面手动打开申请的权限

1
2
3
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" android:required="true"/>
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>

相机的角度,如果不加调整的打开相机,默认的角度是0°,(这一点很奇怪。。)一般竖屏状态下是90度,横屏则为0或180度(应该是取决于传感器的方向)

拍摄照片

使用Camera拍摄照片的步骤:
要使用此类拍摄照片,请使用以下步骤:

  1. 从open(int)获取Camera的实例。(首先使用以下方法获得设备摄像头数目)
    int cameraNum = Camera.getNumberOfCameras();
    //上面open的有效取值是 0 ~ cameraNum-1 代表了摄像头的设备id
  2. 使用getParameters()获取现有(默认)设置。如有必要,修改返回的Camera.Parameters对象并调setParameters(Camera.Parameters)
  3. 调用setDisplayOrientation(int)以确保正确的预览方向。
    重要提示:将完全初始化的SurfaceHolder传递给 setPreviewDisplay(SurfaceHolder)。没有surface,相机将无法启动预览。
    重要说明:调用startPreview()开始更新预览曲面。必须先开始预览才能拍照。
  4. 如果需要,可以调用takePicture(Camera.ShutterCallback,Camera.PictureCallback,Camera.PictureCallback,Camera.PictureCallback)来捕获照片。等待回调提供实际的图像数据。
  5. 拍照后,预览显示将停止。要拍摄更多照片,请先再次调用startPreview()。
  6. 调用stopPreview()以停止更新预览surface。
    重要提示:调用release()以释放相机以供其他应用程序使用。应用程序应立即在Activity.onPause()中释放相机(并在Activity.onResume()中重新打开它)。

一般自定义的来说,surface显示的图像都会被拉伸,或者压扁,反正就是图像比例不对,这是由于surfaceView和Camera.Size不匹配导致的,所以要根据surfaceView的宽高,在Camera所支持的Size里面找一个最合适的

在启用startPreview后,就可以通过Camera.takePicture()方法拍摄一张照片,返回的照片数据通过Callback接口获取。takePicture()接口可以获取三个类型的照片:

  • 第一个,ShutterCallback接口,在快门瞬间被回调,通常用于播放“咔嚓”这样的音效;
  • 第二个,PictureCallback接口,返回未经压缩的RAW类型照片(字节数组);
  • 第三个,PictureCallback接口,返回经过压缩的JPEG类型照片(字节数组);

这三个都可以不实现,第一个一般没啥影响,但第二个第三个不实现则相当于拍的东西就没了。(第二第三个可以选一个实现)调用此方法后,在返回JPEG回调之前,不得调用startPreview()或拍摄另一张照片。

camera.setDisplayOrientation(90); //设定的是预览的方向,预览和数据是独立的,如果只设置了DisplayOrientation,则在surfaceView中显示正常,但保存的数据仍然是0°的

修改拍摄数据角度的两种方法(以下均是竖屏情况,横屏情况不用额外处理,因为默认是横屏情况):

  1. 直接在设置camera参数的时候设置 parameters.setRotation(90); //这样拍摄出的data就是修正过后的数据,直接写到文件里即可
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @Override
    public void onPictureTaken(byte[] data, Camera camera) {
    File file = new File(fileName);
    try {
    FileOutputStream os = new FileOutputStream(file);
    os.write(data);
    os.flush();
    os.close();
    } catch (FileNotFoundException e) {
    e.printStackTrace();
    } catch (IOException e) {
    e.printStackTrace();
    }

    if (mCamera != null) {
    mCamera.startPreview();
    }
    }
  2. 不设置相机的Rotation,这样得到的数据是横屏情况下的,要使用Matrix和Bitmap进行修正
    parameters.setPreviewFormat(ImageFormat.NV21); //默认预览帧格式为NV21,注意很多格式设备可能不支持,程序就会崩,如nexus6p不支持YUY2

录制视频

使用surfaceView

  1. 像上面一样获得camera的实例并初始化它,并开启preview
  2. 调用unlock()允许mediaRecorder的工作线程使用设定好参数的camera
  3. 把camera实例传给MediaRecorder.setCamera(Camera)
  4. 设置用于预览的surface:MediaRecorder.setPreviewDisplay(如果camera没绑定surface,则此步骤会替换camera的surface为本参数)
    //以下为设置录制的参数
    设置视频源setVideoSource: 一般为摄像头
    设置音频源setAudioSource:一般是CAMCORDER(单纯录视频是没声音的) 如果是用mediaRecorder单录音频的话,一般用MIC
    设置输出文件格式:setOutputFormat
    设置视频的角度:setOrientationHint //预览角度在camera的参数里设置
    //下面的必须要放在输出格式确定后
    设置视频编码:setVideoEncoder
    设置音频编码:setAudioEncoder
    //下面的必须要放在编码确定后
    设置视频分辨率:setVideoSize; 这里的分辨率必须要选择设备摄像头支持的分辨率,否则报错(Camera.getParameters().getSupportedPreviewSizes()的值之一)
    设置录制视频的捕获帧速率:setVideoFrameRate(); //必须要选择摄像头支持的帧率,否则报错(mCamera.getParameters().getSupportedPreviewFrameRates()的值之一)
    设置所录制视频的编码位率:setVideoEncodingBitRate(3 * 1024 * 1024);
    设置记录会话的最大持续时间(毫秒)setMaxDuration(30 * 1000);
    设置输出文件的路径:setOutputFile
  5. 当完成录制后,调用camera.reconnect把camera的使用权从mediaRecorder的工作线程转回到设置了它的本线程
  6. 调用mediaRecorder调用stop和release 释放资源
    一些总结性的概述:
    surfaceView 只是一个用来占位置的控件,是用来显示surface内容的,它会显示它绑定的SurfaceHolder所持有的surface, 真正用于显示的是surface(它是具体的要显示的数据),surface的持有者是SurfaceHolder,每个surfaceView生来就有一个默认与其绑定的SurfaceHolder,通过getHolder来获取,SurfaceHolder会决定如何去显示surface,每个surface有且只有一个SurfaceHolder,
    surfaceView:画布
    surface:画的内容
    surfaceHolder,画内容的持有者
    surfaceView、surfaceHolder、surface三位一体,是一个不可分的整体

一个camera设置好参数,它本身是被本线程持有的,持有的话是被lock的,mediaRecorder的工作是在单独的线程中完成的
使用setCamera给mediarecorder设置一个被设置好相机。而如果不调用setCamera设置相机的话,分配给的是一个默认情况下的相机,也就是说什么参数都没有被设置,这样一般不可能满足拍摄需求,因此一般要给mediaRecorder设置camera

在mediaRecorder.start之前,它的camera必须要调用unlock(如果设置了的话),因为mediaRecorder的工作是在单独的线程中完成的,而camera默认是被创建它的线程所持有的,这样mediaRecorder的工作线程无法使用它),如果start顺利结束,则会自动调用lock归还camera,如果start调用失败,则要手动lock来回收camera的所有权

mediaRecorder.setPreviewDisplay(Surface s)
给mediaRecorder设置surface,surface是用来显示mediaRecorder的相机的预览。如果s已经被一个设置给了camera(只是设置给了,还没有被持有资源),而且这个camera已经通过mediaRecorder.setCamera被设置给了mediaRecorder,那么这个方法不需要调用。如果s已经被设置给了一个另一个camera,只是设置给了,还没有被持有资源),但这个camera没有被设置给mediaRecorder,那么这个s将会被设置给给mediaRecorder自己的camera(如果没设置,则是默认的camera)。此时mediaRecorder自己的camera和另一个camera被设置了同一个surface.
如果s为空,则完全不会发生任何事


camera.setHolder
给camera设置一个holder,即surface;但此时holder的surface资源并不为camera所持有,只有当开启预览(即startPreview)时,surface资源才会被camera所持有(下面所说的surface和相应的surfaceHolder都是绑定的,彼此一体,surface资源也就相当于holder的资源)

如果对一个camera持有一个surface的资源,再次让另一个camera再次去持有这个surface资源,则会报错(mediaRecorder.start中会调用类似于它自己的camera.startPreview,startPreview会试图去占有surface资源)也就是说,同一个surfaceHolder可以被多个camera设置,但同时只能有一个camera持有它的surface的资源,否则就会报错。可以对同一个camera连续多次试图持有资源(反正资源就是你的,反复持有还是这些).

当用startPreview成功开启预览后,surface的资源就被camera持有,此时单纯调用stopPreview关闭预览并不能释放它所持有的surface资源,必须要用camera.release(完全释放camera),或者直接setDisplayHolder(null)(只释放camera占有的surface资源,不改变camera的其他参数设置)才能释放它占用的surface资源。但不能直接把camera置为null来释放资源,因为这样只能减少它的引用计数,不是真正的释放资源。

mediaRecorder 不会吧自己的surface主动连接到自己设置的camera, 除非是不设置camera而使用默认的camera,因为mediaRecorder会默认自己设置的camera有surface

camera.setDisplayOrientation设置相机预览的角度,mediaRecorder.setOrientationHini设置存储的视频的角度,两者互相独立

使用TextureView

TextureViewView:
TextureView可用于显示内容流。 这样的内容流可以例如是视频或OpenGL场景。 内容流可以来自应用程序的进程以及远程进程。

TextureView只能在硬件加速窗口中使用。 在软件中渲染时,TextureView将不会绘制任何内容。(例如在xml中给textureView设置背景色,就直接会报错)

与SurfaceView不同,TextureView不会创建单独的窗口,而是表现为常规View。 这个关键区别允许它进行移动,转换,动画等。例如,您可以通过调用myView.setAlpha(0.5f)使TextureView半透明。

使用TextureView很简单:您需要做的就是获得它的SurfaceTexture。 然后,把SurfaceTexture用来呈现内容。 以下示例演示如何将相机预览渲染到TextureView中:

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
public class LiveCameraActivity extends Activity implements TextureView.SurfaceTextureListener {
private Camera mCamera;
private TextureView mTextureView;

protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

mTextureView = new TextureView(this);
mTextureView.setSurfaceTextureListener(this);

setContentView(mTextureView);
}

public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
mCamera = Camera.open();

try {
mCamera.setPreviewTexture(surface);
mCamera.startPreview();
} catch (IOException ioe) {
// Something bad happened
}
}

public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
// Ignored, Camera does all the work for us
}

public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
mCamera.stopPreview();
mCamera.release();
return true;
}

public void onSurfaceTextureUpdated(SurfaceTexture surface) {
// Invoked every time there's a new Camera preview frame
}
}

可以通过调用getSurfaceTexture()或使用TextureView.SurfaceTextureListener来获取TextureView的SurfaceTexture。 重要的是要知道只有在将TextureView附加到窗口(并且已调用onAttachedToWindow()之后)才能使用SurfaceTexture。因此,强烈建议您使用listener在SurfaceTexture可用时进行通知。

值得注意的是,只有一个生产者可以使用TextureView。 例如,如果使用TextureView显示相机预览,则无法使用lockCanvas()同时绘制到TextureView。
如同surface一样,TextureView的surfaceTexture资源同一时间也只能被一个对象所持有

———————————————————————————————————————————————————————————————
SurfaceTexture:
从图像流中捕获帧作为OpenGL ES纹理。

图像流可以来自相机预览或视频解码。从SurfaceTexture创建的Surface可以用作android.hardware.camera2,MediaCodec,MediaPlayer和Allocation API的输出目标。调用updateTexImage()时,将更新创建SurfaceTexture时指定的纹理对象的内容,以包含图像流中的最新图像。这可能导致跳过一些流的帧。

在指定旧版Camera API的输出目标时,也可以使用SurfaceTexture代替SurfaceHolder。这样做会导致图像流中的所有帧都被发送到SurfaceTexture对象而不是设备的显示。

从纹理中采样时,应首先使用通过getTransformMatrix(float [])查询的矩阵变换纹理坐标。每次调用updateTexImage()时变换矩阵都会改变,因此每次更新纹理图像时都应该重新查询。该矩阵将形式为(s,t,0,1)的传统2D OpenGL ES纹理坐标列向量转换为包含区间[0,1]上的s和t到流式纹理中的适当采样位置。此变换可补偿图像流源的任何属性,使其看起来与传统的OpenGL ES纹理不同。例如,可以通过使用查询的矩阵变换列向量(0,0,0,1)来完成从图像左下角的采样,而从图像的右上角进行采样可以通过变换来完成( 1,1,0,1)。

纹理对象使用GL_TEXTURE_EXTERNAL_OES纹理目标,该目标由GL_OES_EGL_image_external OpenGL ES扩展定义。这限制了纹理的使用方式。每次绑定纹理时,它必须绑定到GL_TEXTURE_EXTERNAL_OES目标而不是GL_TEXTURE_2D目标。此外,从纹理中采样的任何OpenGL ES 2.0着色器必须使用例如“#extension GL_OES_EGL_image_external:require”指令声明其对此扩展的使用。此类着色器还必须使用samplerExternalOES GLSL采样器类型访问纹理。

可以在任何线程上创建SurfaceTexture对象。 updateTexImage()只能在包含纹理对象的OpenGL ES上下文的线程上调用。在任意线程上调用可用帧的回调,因此除非特别小心,否则不应直接从回调中调用updateTexImage()。
———————————————————————————————————————————————————————————————

录制流程:
其他步骤均和surfaceView一样,只有第四步有区别,由于此时不能获得surfaceHolder,只要把TextureView的SurfaceTexture绑定到相机即可(给camera设置用于预览的surfaceTexture:camera.setPreviewTexture(代替camera.setPreviewDisplay),同时而且也不需要给mediaRecorder调用setPreviewDisplay)

MediaRecorder:用来录制音频和视频,录制控制基于简单的状态机,如下图:
在这里插入图片描述

使用MediaRecorder录制音频的流程:

1
2
3
4
5
6
7
8
9
10
11
12
A common case of using MediaRecorder to record audio works as follows:
MediaRecorder recorder = new MediaRecorder();
recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
recorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
recorder.setOutputFile(PATH_NAME);
recorder.prepare();
recorder.start(); // Recording is now started
...
recorder.stop();
recorder.reset(); // You can reuse the object by going back to setAudioSource() step
recorder.release(); // Now the object cannot be reused

//这个不需要手动另开线程写数据,体积小使用方便,但很大的一个缺点就是录下的音质不太好。相比之下AudioRecord虽然麻烦一点,但录制的更像是无损音质

application可能希望注册信息和错误事件,以便在录制期间获知某些内部更新和可能的运行时错误。 通过设置适当的监听器(通过调用(到setOnInfoListener(OnInfoListener)setOnInfoListener和/或setOnErrorListener(OnErrorListener)setOnErrorListener)来注册此类事件。为了接收与这些侦听器关联的相应回调,应用程序需要在运行Looper的线程上创建MediaRecorder对象 (默认情况下,主UI线程已经运行了Looper)。

1
2
3
4
5
6
7
8
9
10
11
使用MediaRecorder报错at android.media.MediaRecorder.start(Native Method):很可能是如下原因: 
1.使用下面两个函数但参数不被当前硬件所支持
mediaRecorder.setVideoFrameRate()和mediaRecorder.setVideoSize(videoWidth, videoHeight)
//注释掉后,会使用硬件支持的默认的参数,就不会出错了,
要获得硬件支持的参数用如下的函数:
mCamera.getParameters().getSupportedPreviewSizes()的值之一
mCamera.getParameters().getSupportedPreviewFrameRates()的值之一

2.已经有其他摄像头开启过预览,持有了SurfaceView或SurfaceTexture的surface资源,但停止预览后没有释放资源

另:mediaRecorder.setVideoSize不能设置的太大,否则点击录像后画面会停住
相机预览数据的获取

转自:https://blog.csdn.net/lb377463323/article/details/53338045
首先定义一个类实现Camera.PreviewCallback接口,然后在它的onPreviewFrame(byte[] data, Camera camera)方法中即可接收到每一帧的预览数据,也就是参数data。
然后使用setPreviewCallback()、setOneShotPreviewCallback或setPreviewCallbackWithBuffer()注册回调接口,下面介绍一下这些方法:

1,void setPreviewCallback (Camera.PreviewCallback cb) 

一旦使用此方法注册预览回调接口,onPreviewFrame()方法会一直被调用,直到camera preview销毁

注意,onPreviewFrame()方法跟Camera.open()是运行于同一个线程,所以为了防止onPreviewFrame()会阻塞UI线程,将Camera.open()放置在子线程中运行。

2,void setOneShotPreviewCallback (Camera.PreviewCallback cb) 

使用此方法注册预览回调接口时,会将下一帧数据回调给onPreviewFrame()方法,调用完成后这个回调接口将被销毁。也就是只会回调一次预览帧数据。

3,void setPreviewCallbackWithBuffer (Camera.PreviewCallback cb) 

它跟setPreviewCallback的工作方式一样,但是要求指定一个字节数组作为缓冲区,用于预览帧数据,这样能够更好的管理预览帧数据时使用的内存。它一般搭配addCallbackBuffer方法使用,伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
byte[] mPreBuffer = new byte[size];//首先分配一块内存作为缓冲区,size的计算方式见第四点中
mCamera.addCallbackBuffer(mPreBuffer);
mCamera.setPreviewCallbackWithBuffer(Camera.PreviewCallback cb);
mCamera.startPreview();

@Override
public void onPreviewFrame(byte[] data, Camera camera) {
if (mPreBuffer == null) {
mPreBuffer = new byte[size];
}
mCamera.addCallbackBuffer(mPreBuffer);//将此缓冲区添加到预览回调缓冲区队列中
}

setPreviewCallbackWithBuffer需要在startPreview()之前调用,因为setPreviewCallbackWithBuffer使用时需要指定一个字节数组作为缓冲区,用于预览帧数据,所以我们需要在setPreviewCallbackWithBuffer之前调用addCallbackBuffer,这样onPreviewFrame的data才有值。

总结一下,设置addCallbackBuffer的地方有两个,一个是在startPreview之前,一个是在onPreviewFrame中,这两个都需要调用,如果在onPreviewFrame中不调用,那么预览帧数据就不会回调给onPreviewFrame了

4,void addCallbackBuffer (byte[] callbackBuffer) 

添加一个预分配的缓冲区到预览回调缓冲区队列中。应用程序可一添加一个或多个缓冲器到这个队列中。当预览帧数据到达时并且缓冲区队列仍然有至少一个可用的缓冲区时,这个 缓冲区将会被消耗掉然后从队列中移除,然后这个缓冲区会调用预览回调接口。如果预览帧数据到达时没有剩余的缓冲区,这帧数据将会被丢弃。当缓冲区中的数据处理完成后,应用程序应该将这个缓冲区添加回缓冲区队列中。
对于非YV12的格式,缓冲区的Size是预览图像的宽、高和每个像素的字节数的乘积。宽高可以使用getPreviewSize()方法获取。每个像素的字节数可以使用ImageFormat.getBitsPerPixel(mCameraParameters.getPreviewFormat()) / 8获取。
对于YU12的格式,缓冲区的Size可以使用setPreviewFormat(int)里面的公式计算,具体详见官方文档。
这个方法只有在使用setPreviewCallbackWithBuffer(PreviewCallback)时才有必要使用。当使用setPreviewCallback(PreviewCallback) 或者setOneShotPreviewCallback(PreviewCallback)时,缓冲区会自动分配。当提供的缓冲区如果太小了,不能支持预览帧数据时,预览回调接口将会return null,然后从缓冲区队列中移除此缓冲区。

完整代码:只需在activity里面调用runTask3即可

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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.graphics.SurfaceTexture;
import android.hardware.Camera;
import android.media.MediaRecorder;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.TextureView;
import android.view.View;
import android.widget.Button;

import com.example.lll.va.R;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

public class Task3 implements View.OnClickListener,
TextureView.SurfaceTextureListener,
SurfaceHolder.Callback {
private final static String tag = Task3.class.getName();
private Camera mCamera;
private SurfaceHolder mHolder;
private CameraUtil mCameraUtil;
private Context mContext;
private int height;
private int width;
private SurfaceView surfaceView;
private TextureView textureView;
private static Task3 task3;
private Activity mActivity;
private boolean isSurfaceMode; //切换surfaceview和textureview的演示

static String videoFileName = Environment.getExternalStorageDirectory() + "/test_vedio.mp4";

public static void runTask3(Activity activity) {
task3 = new Task3(activity);
task3.initView();

}

public Task3(Activity activity) {
mActivity = activity;
mContext = activity;
}

private void initView() {
mActivity.setContentView(R.layout.activity_task3);

surfaceView = mActivity.findViewById(R.id.sv_t3);
textureView = mActivity.findViewById(R.id.texture_view_1);

//为了知道surface的生命周期,一般只有surface建好后才开始下一步动作
//surfaceView必须给holder添加callback,TextureView必须给自己添加listener
if (surfaceView.getVisibility() == View.VISIBLE) {
isSurfaceMode = true;
surfaceView.getHolder().addCallback(this);
mHolder = surfaceView.getHolder();
} else {
isSurfaceMode = false;
textureView.setSurfaceTextureListener(this);
}

Button btnTakePic = mActivity.findViewById(R.id.btn_take_pic);
Button btnStartVideo = mActivity.findViewById(R.id.btn_start_video);
Button btnStopVideo = mActivity.findViewById(R.id.btn_stop_video);
btnTakePic.setOnClickListener(this);
btnStartVideo.setOnClickListener(this);
btnStopVideo.setOnClickListener(this);
}

private void getWidthAndHeight() {
width = isSurfaceMode ? surfaceView.getWidth() : textureView.getWidth();
height = isSurfaceMode ? surfaceView.getHeight() : textureView.getHeight();
}

public void initCamera() {
if (mCamera != null) {
mCamera.release();
mCamera = null;
}
Log.d(tag, "is surfaceview = " + isSurfaceMode);
mCameraUtil = CameraUtil.getInstance();
mCamera = mCameraUtil.openCamera(0);
Bundle paramBundle = new Bundle(); //感觉干脆用bundle传参好了
getWidthAndHeight();
paramBundle.putInt("width", width);
paramBundle.putInt("height", height);
mCameraUtil.setCameraParamter(mContext, mCamera, paramBundle);

//根据不同的view,选择不同的预览载体
if (isSurfaceMode) {
try {

mCamera.setPreviewDisplay(surfaceView.getHolder());
} catch (IOException e) {
e.printStackTrace();
}
} else {
try {
mCamera.setPreviewTexture(textureView.getSurfaceTexture());
} catch (IOException e) {
e.printStackTrace();
}
}
//预览监听
mCamera.setPreviewCallback(mCameraCallback);
// mCamera.setPreviewCallbackWithBuffer(mCameraCallback);
//开始预览
mCamera.startPreview();

}


/************************** 录视频部分 *******************************/
MediaRecorder mediaRecorder;

public void startVideoRecord() {

mediaRecorder = new MediaRecorder();// 创建mediarecorder对象
// 设置录制视频源为设置好参数的Camera(相机)
mediaRecorder.setOnInfoListener(mediaCallbackListener);
mediaRecorder.setOnErrorListener(mediaCallbackListener);
mCamera.unlock();

mediaRecorder.setCamera(mCamera);

if (isSurfaceMode)//如果给camera设置了setPreviewDisplay,则这句可以不加
mediaRecorder.setPreviewDisplay(mHolder.getSurface());


Log.d(tag, "camera + " + mCamera);


mediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
// 设置录制完成后视频的封装格式THREE_GPP为3gp.MPEG_4为mp4
mediaRecorder
.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
// 设置录制的视频编码h264
//音频编码为AAC
mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
mediaRecorder.setOrientationHint(90);
// 设置视频录制的分辨率。必须放在设置编码和格式的后面,否则报错
mediaRecorder.setVideoSize(1600, 1200);
mediaRecorder.setVideoEncodingBitRate(1024 * 1024 * 5);// 设置编码位率,图像模糊的设置了这个图像就清晰了

// 设置录制的视频帧率。必须放在设置编码和格式的后面,否则报错
mediaRecorder.setVideoFrameRate(24);
// mediaRecorder.setPreviewDisplay(mHolder.getSurface());
// 设置视频文件输出的路径
mediaRecorder.setOutputFile(Task3.videoFileName);
try {
// 准备录制
mediaRecorder.prepare();
// 开始录制
mediaRecorder.start();
} catch (IllegalStateException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

public void stopVideoRecord() {
if (mediaRecorder == null) return;
mediaRecorder.stop();
mediaRecorder.release();

try {
mCamera.lock();
mCamera.reconnect();

//这或许是camera的bug,预览的时候不能同时进行拍摄,如果拍摄结束后还需要预览
//要再setPreviewCallback和startPreview,貌似在camera2中解决了
mCamera.setPreviewCallback(mCameraCallback);
mCamera.startPreview();
// mCamera.lock();
} catch (IOException e) {
e.printStackTrace();
}

}

private CameraCallback mCameraCallback = new CameraCallback();
class CameraCallback implements Camera.PreviewCallback{

@Override
public void onPreviewFrame(byte[] data, Camera camera) {
int len = data.length;
//当相机的预览分辨率为1600 * 1200,预览图像的编码格式为 NV21 即 12bit/pixel 时
//每一帧图像的大小应该为 1600 * 1200 * 12 / 8 = 2880000 Bytes
Log.d(tag, "onPreviewFrame data.data len=" + len);

}
}


MediaCallbackListener mediaCallbackListener;

/************************ SurfaceTextureListener **********************************/
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
if (!isSurfaceMode)
initCamera();
}

@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {

}

@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
return false;
}

@Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {

}


/************************ SurfaceCallback **********************************/
@Override
public void surfaceCreated(SurfaceHolder holder) {
if (isSurfaceMode)
initCamera();
}

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

}

@Override
public void surfaceDestroyed(SurfaceHolder holder) {

}

/**********************************************************/

class MediaCallbackListener implements MediaRecorder.OnInfoListener, MediaRecorder.OnErrorListener {

@Override
public void onError(MediaRecorder mr, int what, int extra) {
Log.d(tag, "error");
}

@Override
public void onInfo(MediaRecorder mr, int what, int extra) {
Log.d(tag, "info");
}
}


public void onClick(View v) {
// if (mCamera == null) return;
if (v.getId() == R.id.btn_take_pic) //拍照
takePicture();
else if (v.getId() == R.id.btn_start_video)
startVideoRecord();
else if (v.getId() == R.id.btn_stop_video)
stopVideoRecord();
}


/************************** 拍照部分 *******************************/

private void takePicture() {
mCamera.takePicture(null, null, mPictureCallback);
}


String fileName = Environment.getExternalStorageDirectory() + "/t3.jpg";
private Camera.PictureCallback mPictureCallback = new Camera.PictureCallback() {
@Override
public void onPictureTaken(byte[] data, Camera camera) {

SavePicAsyncTask saveBitmapTask = new SavePicAsyncTask();
saveBitmapTask.execute(data);
Thread t = new Thread();


if (mCamera != null) {
mCamera.startPreview();
}
}
};

private Camera.ShutterCallback mShutterCallback = new Camera.ShutterCallback() {
@Override
public void onShutter() {

}
};

//这是设置了rotation时调用保存图片的方法
public void savePic(byte[] data) {
try {
File file = new File(fileName);
FileOutputStream os = new FileOutputStream(file);
os.write(data);
os.flush();
os.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}

if (mCamera != null) {
mCamera.startPreview();
}
}

//使用bitmap保存时,因为compress是耗时方法,放在onPictureTaken中会体验极差,所以放个后台任务来压缩保存图片
private class SavePicAsyncTask extends AsyncTask {
@Override
protected Object doInBackground(Object[] objects) {
byte[] data = (byte[]) objects[0];
Bitmap bm0 = BitmapFactory.decodeByteArray(data, 0, data.length);

//由于camera.
Matrix m = new Matrix();
m.setRotate(90, (float) bm0.getWidth() / 2, (float) bm0.getHeight() / 2); //后面两个是旋转轴心坐标
Bitmap bitmap = Bitmap.createBitmap(bm0, 0, 0, bm0.getWidth(), bm0.getHeight(), m, false);

File file = new File(fileName);
try {
FileOutputStream os = new FileOutputStream(file);
bitmap.compress(Bitmap.CompressFormat.JPEG, 85, os);
os.flush();
os.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
};
}

CameraUtil

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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.ImageFormat;
import android.hardware.Camera;
import android.media.Image;
import android.os.Bundle;
import android.util.Log;

import java.security.PublicKey;
import java.util.List;

public class CameraUtil {
private static final String tag = CameraUtil.class.getName();
private static CameraUtil util;

public static CameraUtil getInstance() {
if (util == null) {
util = new CameraUtil();
}
return util;
}

int CameraNum = 0;
boolean mIsPortrait = true;

private CameraUtil() {
CameraNum = Camera.getNumberOfCameras();
Log.d(tag, "cam_num=" + CameraNum);
}


public Camera openCamera(int id) {
if (CameraNum == 0) return null;
Camera camera = Camera.open(id); //打开第一个摄像头
return camera;
}

public void setCameraParamter(Context context, Camera camera, Bundle paramBundle) {
if (camera == null) return;

Camera.Parameters parameters = camera.getParameters();
parameters.setFlashMode(Camera.Parameters.FLASH_MODE_AUTO); //闪光灯模式
parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);

int width = paramBundle.getInt("width");
int height = paramBundle.getInt("height");
Log.d(tag, "width = " + width + " height =" + height);
List<Camera.Size> sizeList = parameters.getSupportedPreviewSizes();
Camera.Size lastSize = getOptimalPreviewSize(context, sizeList, width, height);
parameters.setPreviewSize(lastSize.width, lastSize.height);
Log.d(tag, "surwidth = " + width + " surheight =" + height
+ " size width=" + lastSize.width + " size height=" + lastSize.height);
parameters.setPreviewFormat(ImageFormat.NV21); //默认预览帧格式
//拍照分辨率和预览分辨率
// parameters.setPictureSize();
// parameters.setPreviewSize(); //预览分辨率只能从上面的sizeList里面选取

followScreenOrientation(context, camera);
camera.setParameters(parameters);
//有的手机这么设置会出错,则使用camera.getParameters().setFlashMode(Camera.Parameters.FLASH_MODE_AUTO);的形式设置


}

public void followScreenOrientation(Context context, Camera camera) {
final int orientation = context.getResources().getConfiguration().orientation;
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
camera.setDisplayOrientation(180);
mIsPortrait = false;
} else if (orientation == Configuration.ORIENTATION_PORTRAIT) {
camera.setDisplayOrientation(90);
mIsPortrait = true;
}
}


//虽然这样画面还是有一点点扁,还是不知道系统相机是如何做到画面不扁而且还像素高的
private Camera.Size getOptimalPreviewSize(Context context, List<Camera.Size> sizes, int w, int h) {
final double ASPECT_TOLERANCE = 0.2; //camera宽高比与surface宽高比的最大误差阈值,越小越精确,但可能没有合适的分辨率,一般为0.1或0.2
final int orientation = context.getResources().getConfiguration().orientation;
int targetHeight = 0;
double targetRatio = 0d;
//由于横竖屏不一样,系统相机的尺寸默认是横屏状态下的,
// 所以竖屏状态下,比例就是高宽比(因此此时高大于宽),而与系统中高度比较时则应该用宽(哪个小用哪个)
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
targetRatio = (double) w / h;
targetHeight = h;
} else if (orientation == Configuration.ORIENTATION_PORTRAIT) {
targetRatio = (double) h / w;
targetHeight = w;
}
//获得surfaceView的宽高比
if (sizes == null) return null;

Camera.Size optimalSize = null;
double minDiff = Double.MAX_VALUE; //在比例合适的情况下,两种高度最低能达到的差值(肯定是越小越符合),一开始设为最大浮点数


Log.d(tag, "targetHeight=" + targetHeight);
// Try to find an size match aspect ratio and size
for (Camera.Size size : sizes) { //遍历当前摄像头支持的分辨率,并计算宽高比
double ratio = (double) size.width / size.height;
Log.d(tag, "width=" + size.width + " height=" + size.height);
Log.d(tag, "target Ratio=" + targetRatio + " ratio=" + ratio);
if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE) continue; //如果两个宽高比之差大于阈值,不符合
if (Math.abs(size.height - targetHeight) < minDiff) { //找到了更小的差值,则替换最合适的比例
optimalSize = size;
minDiff = Math.abs(size.height - targetHeight);
}
}

// Cannot find the one match the aspect ratio, ignore the requirement
if (optimalSize == null) { //当使用阈值找不到时,则忽略比例阈值,直接用最小的高度差的那组分辨率
minDiff = Double.MAX_VALUE;
for (Camera.Size size : sizes) {
if (Math.abs(size.height - targetHeight) < minDiff) {
optimalSize = size;
minDiff = Math.abs(size.height - targetHeight);
}
}
}

return optimalSize;
}
}

布局文件,只让surfaceview和textureview其中之一显示

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
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<!--surfaceView 和 TextureView选一个隐藏 -->
<SurfaceView
android:id="@+id/sv_t3"
android:visibility="visible"
android:layout_width="match_parent"
android:layout_height="500dp" />


<TextureView
android:id="@+id/texture_view_1"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="500dp" />

<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<Button
android:id="@+id/btn_take_pic"
android:text="btn_take_pic"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<Button
android:id="@+id/btn_start_video"
android:text="btn_start_video"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<Button
android:id="@+id/btn_stop_video"
android:text="btn_stop_video"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>


</LinearLayout>