
在 Android 中录制摄像头采集的数据到 MP4 文件,我们可以用系统自带的MediaRecorder,也可以用第三方成熟的摄像头采集录制库,本文就两种方案,做个大概的梳理。
我们先说MediaRecorder的技术实现,再探讨下SmartPublisher的录制模块。
一、准备工作
权限申请,在AndroidManifest.xml文件中添加以下权限,这些权限分别用于访问摄像头、录制音频和写入外部存储:
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />二、初始化摄像头
获取摄像头实例,在 Java 代码中,可以使用以下方式获取摄像头实例:
Camera camera = Camera.open();设置摄像头参数,设置预览尺寸、方向等参数:
Camera.Parameters parameters = camera.getParameters();
parameters.setPreviewSize(width, height);
camera.setParameters(parameters);设置预览界面,创建一个SurfaceView或TextureView来显示摄像头预览,并将其设置为摄像头的预览目标:
SurfaceView surfaceView = findViewById(R.id.surfaceView);
SurfaceHolder surfaceHolder = surfaceView.getHolder();
surfaceHolder.addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
try {
camera.setPreviewDisplay(holder);
camera.startPreview();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
// 在预览界面销毁时停止摄像头预览
if (camera!= null) {
camera.stopPreview();
}
}
});三、创建视频录制器
使用MediaRecorder,如果使用 Android 系统自带的MediaRecorder,可以按照以下步骤进行配置:
MediaRecorder mediaRecorder = new MediaRecorder();
camera.unlock();
mediaRecorder.setCamera(camera);
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
mediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
mediaRecorder.setVideoSize(width, height);
mediaRecorder.setVideoFrameRate(frameRate);
mediaRecorder.setOutputFile(outputFilePath);这里设置了音频和视频源、输出格式、编码器、视频尺寸、帧率和输出文件路径等参数。
开始录制,调用prepare和start方法开始录制:
try {
mediaRecorder.prepare();
mediaRecorder.start();
} catch (IOException e) {
e.printStackTrace();
}四、停止录制
在适当的时候(比如用户点击停止按钮),停止录制并释放资源:
mediaRecorder.stop();
mediaRecorder.reset();
mediaRecorder.release();
camera.lock();
camera.stopPreview();
camera.release();SmartPublisher,是大牛直播SDK的StreamMediaKit生态圈下的录制库。可作为单独功能模块使用(如同时多路录像存档),也可以和其他推送、播放模块组合使用:
功能支持
本文以大牛直播SDK的Camera2的采集demo为例,获取到视音频数据,回调到上层,分别调用投递接口投递到底层模块:

先说摄像头数据采集处理:
@Override
public void onCameraImageData(Image image) {
Image.Plane[] planes = image.getPlanes();
int w = image.getWidth(), h = image.getHeight();
int y_offset = 0, u_offset = 0, v_offset = 0;
int scale_w = 0, scale_h = 0, scale_filter_mode = 0;
scale_filter_mode = 3;
int rotation_degree = cameraImageRotationDegree_;
if (rotation_degree < 0) {
Log.i(TAG, "onCameraImageData rotation_degree < 0, may need to set orientation_ to 0, 90, 180 or 270");
return;
}
for (LibPublisherWrapper i : publisher_array_)
i.PostLayerImageYUV420888ByteBuffer(0, 0, 0,
planes[0].getBuffer(), y_offset, planes[0].getRowStride(),
planes[1].getBuffer(), u_offset, planes[1].getRowStride(),
planes[2].getBuffer(), v_offset, planes[2].getRowStride(), planes[1].getPixelStride(),
w, h, 0, 0,
scale_w, scale_h, scale_filter_mode, rotation_degree);
}调用到的PostLayerImageYUV420888ByteBuffer()封装实现如下:
/*
* LibPublisherWrapper.java
* Created by daniusdk.com
* WeChat: xinsheng120
*/
public boolean PostLayerImageYUV420888ByteBuffer(int index, int left, int top,
ByteBuffer y_plane, int y_offset, int y_row_stride,
ByteBuffer u_plane, int u_offset, int u_row_stride,
ByteBuffer v_plane, int v_offset, int v_row_stride, int uv_pixel_stride,
int width, int height, int is_vertical_flip, int is_horizontal_flip,
int scale_width, int scale_height, int scale_filter_mode,
int rotation_degree) {
if (!check_native_handle())
return false;
if (!read_lock_.tryLock())
return false;
try {
if (!check_native_handle())
return false;
return OK == lib_publisher_.PostLayerImageYUV420888ByteBuffer(get(), index, left, top, y_plane, y_offset, y_row_stride,
u_plane, u_offset, u_row_stride, v_plane, v_offset, v_row_stride, uv_pixel_stride,
width, height, is_vertical_flip, is_horizontal_flip, scale_width, scale_height, scale_filter_mode, rotation_degree);
} catch (Exception e) {
Log.e(TAG, "PostLayerImageYUV420888ByteBuffer Exception:", e);
return false;
} finally {
read_lock_.unlock();
}
}再说麦克风采集,麦克风采集,通过AudioRecorder获取到audio数据,然后回调上来,再传到SDK即可。
/*
* MainActivity.java
* Created by daniusdk.com
* WeChat: xinsheng120
*/
void startAudioRecorder() {
if (audio_recorder_ != null)
return;
audio_recorder_ = new NTAudioRecordV2(this);
Log.i(TAG, "startAudioRecorder call audio_recorder_.start()+++...");
audio_recorder_callback_ = new NTAudioRecordV2CallbackImpl(stream_publisher_, null);
audio_recorder_.AddCallback(audio_recorder_callback_);
if (!audio_recorder_.Start(is_pcma_ ? 8000 : 44100, 1) ) {
audio_recorder_.RemoveCallback(audio_recorder_callback_);
audio_recorder_callback_ = null;
audio_recorder_ = null;
Log.e(TAG, "startAudioRecorder start failed.");
}
else {
Log.i(TAG, "startAudioRecorder call audio_recorder_.start() OK---...");
}
}
void stopAudioRecorder() {
if (null == audio_recorder_)
return;
Log.i(TAG, "stopAudioRecorder+++");
audio_recorder_.Stop();
if (audio_recorder_callback_ != null) {
audio_recorder_.RemoveCallback(audio_recorder_callback_);
audio_recorder_callback_ = null;
}
audio_recorder_ = null;
Log.i(TAG, "stopAudioRecorder---");
}audio数据回调上来,投递设计如下:
/*
* MainActivity.java
* Created by daniusdk.com
* WeChat: xinsheng120
*/
private static class NTAudioRecordV2CallbackImpl implements NTAudioRecordV2Callback {
private WeakReference<LibPublisherWrapper> publisher_0_;
private WeakReference<LibPublisherWrapper> publisher_1_;
public NTAudioRecordV2CallbackImpl(LibPublisherWrapper publisher_0, LibPublisherWrapper publisher_1) {
if (publisher_0 != null)
publisher_0_ = new WeakReference<>(publisher_0);
if (publisher_1 != null)
publisher_1_ = new WeakReference<>(publisher_1);
}
private final LibPublisherWrapper get_publisher_0() {
if (publisher_0_ !=null)
return publisher_0_.get();
return null;
}
private final LibPublisherWrapper get_publisher_1() {
if (publisher_1_ != null)
return publisher_1_.get();
return null;
}
@Override
public void onNTAudioRecordV2Frame(ByteBuffer data, int size, int sampleRate, int channel, int per_channel_sample_number) {
LibPublisherWrapper publisher_0 = get_publisher_0();
if (publisher_0 != null)
publisher_0.OnPCMData(data, size, sampleRate, channel, per_channel_sample_number);
LibPublisherWrapper publisher_1 = get_publisher_1();
if (publisher_1 != null)
publisher_1.OnPCMData(data, size, sampleRate, channel, per_channel_sample_number);
}
}开始录像、停止录像设计:
/*
* MainActivity.java
* Created by daniusdk.com
* WeChat: xinsheng120
*/
class ButtonStartRecorderListener implements View.OnClickListener {
public void onClick(View v) {
if (layer_post_thread_ != null)
layer_post_thread_.update_layers();
if (stream_publisher_.is_recording()) {
stopRecorder();
if (stream_publisher_.empty())
ConfigControlEnable(true);
btnStartRecorder.setText("实时录像");
btnPauseRecorder.setText("暂停录像");
btnPauseRecorder.setEnabled(false);
isPauseRecording = true;
return;
}
Log.i(TAG, "onClick start recorder..");
InitAndSetConfig();
ConfigRecorderParam();
boolean start_ret = stream_publisher_.StartRecorder();
if (!start_ret) {
stream_publisher_.try_release();
Log.e(TAG, "Failed to start recorder.");
return;
}
startAudioRecorder();
ConfigControlEnable(false);
startLayerPostThread();
btnStartRecorder.setText("停止录像");
btnPauseRecorder.setEnabled(true);
isPauseRecording = true;
}
}调用的初始化编码参数接口实现如下:
/*
* MainActivity.java
* Created by daniusdk.com
* WeChat: xinsheng120
*/
private boolean initialize_publisher(SmartPublisherJniV2 lib_publisher, long handle, int width, int height, int fps, int gop) {
if (null == lib_publisher) {
Log.e(TAG, "initialize_publisher lib_publisher is null");
return false;
}
if (0 == handle) {
Log.e(TAG, "initialize_publisher handle is 0");
return false;
}
if (videoEncodeType == 1) {
int kbps = LibPublisherWrapper.estimate_video_hardware_kbps(width, height, fps, true);
Log.i(TAG, "h264HWKbps: " + kbps);
int isSupportH264HWEncoder = lib_publisher.SetSmartPublisherVideoHWEncoder(handle, kbps);
if (isSupportH264HWEncoder == 0) {
lib_publisher.SetNativeMediaNDK(handle, 0);
lib_publisher.SetVideoHWEncoderBitrateMode(handle, 1); // 0:CQ, 1:VBR, 2:CBR
lib_publisher.SetVideoHWEncoderQuality(handle, 39);
lib_publisher.SetAVCHWEncoderProfile(handle, 0x08); // 0x01: Baseline, 0x02: Main, 0x08: High
lib_publisher.SetAVCHWEncoderLevel(handle, 0x1000); // Level 4.1 多数情况下,这个够用了
Log.i(TAG, "Great, it supports h.264 hardware encoder!");
}
} else if (videoEncodeType == 2) {
int kbps = LibPublisherWrapper.estimate_video_hardware_kbps(width, height, fps, false);
Log.i(TAG, "hevcHWKbps: " + kbps);
int isSupportHevcHWEncoder = lib_publisher.SetSmartPublisherVideoHevcHWEncoder(handle, kbps);
if (isSupportHevcHWEncoder == 0) {
lib_publisher.SetNativeMediaNDK(handle, 0);
lib_publisher.SetVideoHWEncoderBitrateMode(handle, 1); // 0:CQ, 1:VBR, 2:CBR
lib_publisher.SetVideoHWEncoderQuality(handle, 39);
Log.i(TAG, "Great, it supports hevc hardware encoder!");
}
}
boolean is_sw_vbr_mode = true;
//H.264 software encoder
if (is_sw_vbr_mode) {
int is_enable_vbr = 1;
int video_quality = LibPublisherWrapper.estimate_video_software_quality(width, height, true);
int vbr_max_kbps = LibPublisherWrapper.estimate_video_vbr_max_kbps(width, height, fps);
lib_publisher.SmartPublisherSetSwVBRMode(handle, is_enable_vbr, video_quality, vbr_max_kbps);
}
if (is_pcma_) {
lib_publisher.SmartPublisherSetAudioCodecType(handle, 3);
} else {
lib_publisher.SmartPublisherSetAudioCodecType(handle, 1);
}
lib_publisher.SetSmartPublisherEventCallbackV2(handle, new EventHandlerPublisherV2().set(handler_, record_executor_));
lib_publisher.SmartPublisherSetSWVideoEncoderProfile(handle, 3);
lib_publisher.SmartPublisherSetSWVideoEncoderSpeed(handle, 2);
lib_publisher.SmartPublisherSetGopInterval(handle, gop);
lib_publisher.SmartPublisherSetFPS(handle, fps);
// lib_publisher.SmartPublisherSetSWVideoBitRate(handle, 600, 1200);
boolean is_noise_suppression = true;
lib_publisher.SmartPublisherSetNoiseSuppression(handle, is_noise_suppression ? 1 : 0);
boolean is_agc = false;
lib_publisher.SmartPublisherSetAGC(handle, is_agc ? 1 : 0);
int echo_cancel_delay = 0;
lib_publisher.SmartPublisherSetEchoCancellation(handle, 1, echo_cancel_delay);
return true;
}
private void InitAndSetConfig() {
if (null == libPublisher)
return;
if (!stream_publisher_.empty())
return;
Log.i(TAG, "InitAndSetConfig video width: " + video_width_ + ", height" + video_height_ + " imageRotationDegree:" + cameraImageRotationDegree_);
int audio_opt = 1;
long handle = libPublisher.SmartPublisherOpen(context_, audio_opt, 3, video_width_, video_height_);
if (0==handle) {
Log.e(TAG, "sdk open failed!");
return;
}
Log.i(TAG, "publisherHandle=" + handle);
int fps = 25;
int gop = fps * 3;
initialize_publisher(libPublisher, handle, video_width_, video_height_, fps, gop);
stream_publisher_.set(libPublisher, handle);
}录像参数配置:
void ConfigRecorderParam() {
if (null == libPublisher)
return;
if (null == recDir || recDir.isEmpty())
return;
int ret = libPublisher.SmartPublisherCreateFileDirectory(recDir);
if (ret != 0) {
Log.e(TAG, "Create record dir failed, path:" + recDir);
return;
}
if (!stream_publisher_.SetRecorderDirectory(recDir)) {
Log.e(TAG, "Set record dir failed , path:" + recDir);
return;
}
// 更细粒度控制录像的, 一般情况无需调用
//libPublisher.SmartPublisherSetRecorderAudio(publisherHandle, 0);
//libPublisher.SmartPublisherSetRecorderVideo(publisherHandle, 0);
if (!stream_publisher_.SetRecorderFileMaxSize(200)) {
Log.e(TAG, "SmartPublisherSetRecorderFileMaxSize failed.");
return;
}
}暂停录像、恢复录像设计:
class ButtonPauseRecorderListener implements View.OnClickListener {
public void onClick(View v) {
if (stream_publisher_.is_recording()) {
if (isPauseRecording) {
boolean ret = stream_publisher_.PauseRecorder(true);
if (ret) {
isPauseRecording = false;
btnPauseRecorder.setText("恢复录像");
} else {
Log.e(TAG, "Pause recorder failed..");
}
} else {
boolean ret = stream_publisher_.PauseRecorder(false);
if (ret) {
isPauseRecording = true;
btnPauseRecorder.setText("暂停录像");
} else {
Log.e(TAG, "Resume recorder failed..");
}
}
}
}
}Android平台采集摄像头麦克风编码录制MP4文件保存,到底是用MediaRecorder还是SmartPublisher?如果只是最基础的数据保存,其实用MediaRecorder也可以,如果对录像功能要求比较高的话,比如需要自定义目录、需要设置单个录像文件大小、需要可以添加动态水印、可以支持录像暂停等,可以考虑用SmartPublisher的录制模块,更全更强大一些。以上是二者大概的调用实现,感兴趣的开发者可以单独跟我交流。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。