技术背景

在 Android 中录制摄像头采集的数据到 MP4 文件,我们可以用系统自带的MediaRecorder,也可以用第三方成熟的摄像头采集录制库,本文就两种方案,做个大概的梳理。

技术比较

我们先说MediaRecorder的技术实现,再探讨下SmartPublisher的录制模块。

MediaRecorder

一、准备工作

权限申请,在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);

设置预览界面,创建一个SurfaceViewTextureView来显示摄像头预览,并将其设置为摄像头的预览目标:

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);

这里设置了音频和视频源、输出格式、编码器、视频尺寸、帧率和输出文件路径等参数。

开始录制,调用preparestart方法开始录制:

try {
    mediaRecorder.prepare();
    mediaRecorder.start();
} catch (IOException e) {
    e.printStackTrace();
}

四、停止录制

在适当的时候(比如用户点击停止按钮),停止录制并释放资源:

mediaRecorder.stop();
mediaRecorder.reset();
mediaRecorder.release();
camera.lock();
camera.stopPreview();
camera.release();

SmartPublisher

SmartPublisher,是大牛直播SDK的StreamMediaKit生态圈下的录制库。可作为单独功能模块使用(如同时多路录像存档),也可以和其他推送、播放模块组合使用:

  1. 不同于普通录像接口,更智能,和RTMP推送、RTSP|RTMP播放、转发、内置轻量级RTSP服务、GB28181设备接入模块完全分离,支持随时录像;
  2. 在录像过程中,支持切换不同URL,如两个URL配置一致,则可以录制到同一个MP4文件,如不一致,可自动分割到下一个文件;
  3. 支持设置单个录像文件大小、录像路径等,并支持纯音频、纯视频、音视频录制模式;
  4. 支持音频(PCMU/PCMA,Speex等)转AAC后再录像;
  5. 支持RTSP/RTMP H.265(hevc)录制到MP4文件;
  6.  支持采集端(推送端)录像过程中,暂停录像、恢复录像;
  7. 从开始录像,到录像结束均有event callback上来,网络堵塞、音视频同步均做了非常友好的处理。

功能支持

  •  [拉流]支持拉取RTSP流录像;
  •  [拉流]支持拉取RTMP流录像;
  •  [推流端录像]支持RTMP|RTSP推送端同步录像;
  •  [轻量级RTSP服务录像]支持轻量级RTSP服务SDK同步录像;
  •  [推流端录像实时暂停/恢复]支持推送端录像过程中实时暂停录像、恢复录像;
  •  [逻辑分离]大牛直播录像SDK不同于普通录像接口,更智能,和推送、播放、转发、内置轻量级RTSP服务SDK功能完全分离,支持随时录像;
  •  [url切换]在录像过程中,支持切换不同URL,如两个URL配置一致,则可以录制到同一个MP4文件,如不一致,可自动分割到下一个文件;
  •  [参数设置]支持设置单个录像文件大小、录像路径等,并支持纯音频、纯视频、音视频录制模式;
  •  [音频转码]支持音频(PCMU/PCMA,Speex等)转AAC后再录像;
  •  [265支持]支持RTSP/RTMP H.265录制到MP4文件;
  •  [推送端265录像]推送端SDK支持H265录像;
  •  [推送端外部编码数据对接录像]支持推送端外部编码后数据(H.264/AAC)对接录像;
  •  [事件回调]从开始录像,到录像结束均有event callback上来,网络堵塞、音视频同步均做了非常友好的处理。

本文以大牛直播SDK的Camera2的采集demo为例,获取到视音频数据,回调到上层,分别调用投递接口投递到底层模块:

Android平台摄像头麦克风视音频采集录像之MediaRecorder还是SmartPublisher_android

先说摄像头数据采集处理:

@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
 */
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
 */
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
 */
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
 */
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
 */
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的录制模块,更全更强大一些。以上是二者大概的调用实现,感兴趣的开发者可以单独跟我交流。