本文介绍在Android平台下,使用OpenGL同时对两个摄像头同时预览、录像,并且录像文件均带音轨,并将录像文件(带声音)、音频文件保存下来。预览与录像均添加时间水印:
先放一个demo效果图:
1.先初始化两个摄像头预览:
import android.content.Context;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import com.reach.mutilcamerarecord.MainActivity;
import com.reach.mutilcamerarecord.media.base.RecordManageBase;
import com.reach.mutilcamerarecord.media.encode.IAudioListener;
import com.reach.mutilcamerarecord.media.manager.RecordVideoAndAudioManager;
import com.reach.mutilcamerarecord.media.setting.CameraSetting;
import com.reach.mutilcamerarecord.media.setting.RecordSetting;
import com.reach.mutilcamerarecord.media.setting.RenderSetting;
import java.io.File;
import java.io.IOException;
/**
* 本地内置摄像头
*/
public class GLRecordManager {
private MainActivity mActivity;
private Context mContext;
//view
private SurfaceView surfaceView;
private SurfaceHolder holder;
private RecordVideoAndAudioManager recorder;
private final static int TargetLongWidth = 1280;//480;//640;//1920; 这个是录像分辨率
private int TargetShortWidth = 720;//320;//480;//1080;
private int cameraWidth = 1280;//320;//640;//这个是摄像头原始数据
private int cameraHeight = 720;//240;//480;
public GLRecordManager(MainActivity activity, Context context, SurfaceView sv){
mActivity = activity;
mContext = context;
surfaceView = sv;
RecordSetting recordSetting = new RecordSetting();
CameraSetting cameraSetting = new CameraSetting();
cameraSetting.fps = 15;//30;
cameraSetting.cameraW = cameraWidth;
cameraSetting.cameraH = cameraHeight;
cameraSetting.cameraPosition = 0;//1;
RenderSetting renderSetting = new RenderSetting();
/*String s = Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + System.currentTimeMillis() + ".mp4";
File file = new File(s);
try {
file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}*/
recorder = new RecordVideoAndAudioManager(mActivity, null, recordSetting, cameraSetting, renderSetting, surfaceView);
recorder.setCallBackEvent(new RecordManageBase.CallBackEvent() {
@Override
public void startRecordSuccess() {
Log.e("main", "startRecordSuccess");
}
@Override
public void onDuringUpdate(float time) {
Log.e("main", "onDuringUpdate => time = " + time);
}
@Override
public void stopRecordFinish(File file) {
Log.e("main", "stopRecordFinish => path = " + file.getPath());
}
@Override
public void recordError(String errorMsg) {
Log.e("main", "recordError => msg = " + errorMsg);
}
@Override
public void openCameraSuccess(int cameraPosition) {
recorder.getRecordSetting().setVideoSetting(TargetLongWidth,TargetShortWidth,
recorder.getCameraManager().getRealFps() / 1000, RecordSetting.ColorFormatDefault);
recorder.getRecordSetting().setVideoBitRate(3000 * 1024);
recorder.switchOnBeauty(cameraPosition == 1);
Log.e("main", "openCameraSuccess => cameraPosition = " + cameraPosition);
}
@Override
public void openCameraFailure(int cameraPosition) {
Log.e("main", "openCameraFailure => cameraPosition = " + cameraPosition);
}
@Override
public void onVideoSizeChange(int width, int height) {
Log.e("main", "onVideoSizeChange => width = " + width + ", height = " + height);
}
@Override
public void onPhotoSizeChange(int width, int height) {
Log.e("main", "onPhotoSizeChange => width = " + width + ", height = " + height);
}
});
holder = surfaceView.getHolder();
holder.addCallback(new CustomCallBack());
}
public void onDestroy() {
if (recorder != null)
recorder.destroy();
}
public void recordVideo(){
if (recorder.isRecord){
stopRecord();
} else {
startRecord();
}
}
/**
* 录像前先创建个文件先
*/
public void setVideoFileName() {
String path = "/sdcard/test_A.mp4";
String pathMp3 = "/sdcard/test.mp3";
File file = new File(path);
File mp3 = new File(pathMp3);
try {
file.createNewFile();
mp3.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
recorder.setFileName(file, mp3);
}
public void startRecord(){
recorder.startRecord();
}
public void stopRecord(){
recorder.stopRecord();
}
public IAudioListener getGLAudioListener(){
return recorder.getGLAudioListener();
}
private class CustomCallBack implements SurfaceHolder.Callback {
@Override
public void surfaceCreated(SurfaceHolder holder) {
recorder.init();
//recorder.getCameraManager().getEvent().openCameraFailure(0);
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
recorder.getRenderSetting().setRenderSize(width < height ? TargetShortWidth : TargetLongWidth, width < height ? TargetLongWidth : TargetShortWidth);
recorder.getRenderSetting().setDisplaySize(width, height);
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
recorder.destroy();
}
}
}
第二路摄像头预览代码:
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.SurfaceTexture;
import android.hardware.Camera;
import android.os.Environment;
import android.util.Log;
import android.view.TextureView;
import android.view.View;
import androidx.annotation.NonNull;
import com.reach.mutilcamerarecord.MainActivity;
import com.reach.mutilcamerarecord.R;
import com.reach.mutilcamerarecord.mediausb.client.RecorderClient;
import com.reach.mutilcamerarecord.mediausb.code.listener.IVideoChange;
import com.reach.mutilcamerarecord.mediausb.code.listener.UvcPcmDataCallBack;
import com.reach.mutilcamerarecord.mediausb.filter.filter.DrawMultiImageFilter;
import com.reach.mutilcamerarecord.mediausb.model.MediaConfig;
import com.reach.mutilcamerarecord.mediausb.model.RecordConfig;
import com.reach.mutilcamerarecord.mediausb.model.Size;
import com.reach.mutilcamerarecord.mediausb.view.AspectTextureView;
/**
* Author:lzh on 2021/6/10 10:04
*/
public class GLUsbManager {
private MainActivity mActivity;
private Context mContext;
protected RecorderClient mRecorderClient;
protected AspectTextureView mTextureView;
RecordConfig recordConfig;
boolean isStart = false;
public GLUsbManager(MainActivity activity, Context context){
mActivity = activity;
mContext = context;
initView();
}
private void initView(){
mTextureView = mActivity.findViewById(R.id.camera_usb);
mTextureView.setVisibility(View.VISIBLE);
mTextureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
@Override
public void onSurfaceTextureAvailable(@NonNull SurfaceTexture surface, int width, int height) {
if (mRecorderClient != null) {
mRecorderClient.startPreview(surface, width, height);
}
updateOutsideCamState(1);
Log.i("textureAvailable", "usb camera start to preview...");
}
@Override
public void onSurfaceTextureSizeChanged(@NonNull SurfaceTexture surface, int width, int height) {
if (mRecorderClient != null) {
mRecorderClient.updatePreview(width, height);
}
}
@Override
public boolean onSurfaceTextureDestroyed(@NonNull SurfaceTexture surface) {
if (mRecorderClient != null) {
mRecorderClient.stopPreview(true);
}
updateOutsideCamState(0);
return false;
}
@Override
public void onSurfaceTextureUpdated(@NonNull SurfaceTexture surface) {
}
});
prepareStreamingClient();
onSetFilters();
}
private void prepareStreamingClient() {
try {
mRecorderClient = new RecorderClient(this);
recordConfig = RecordConfig.obtain();
recordConfig.setTargetVideoSize(new Size(1280, 720));
int bitRate = 1280 * 720;
recordConfig.setSquare(true);//方正,不圆角
recordConfig.setBitRate(bitRate);
recordConfig.setVideoFPS(15);
recordConfig.setVideoGOP(1);
recordConfig.setRenderingMode(MediaConfig.Rending_Model_OpenGLES);
//camera
recordConfig.setDefaultCamera(Camera.CameraInfo.CAMERA_FACING_FRONT);
int frontDirection, backDirection;
Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
Camera.getCameraInfo(Camera.CameraInfo.CAMERA_FACING_FRONT, cameraInfo);
frontDirection = cameraInfo.orientation;
Camera.getCameraInfo(Camera.CameraInfo.CAMERA_FACING_BACK, cameraInfo);
backDirection = cameraInfo.orientation;
if (mContext.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
recordConfig.setFrontCameraDirectionMode((frontDirection == 90 ? MediaConfig.DirectionMode.FLAG_DIRECTION_ROATATION_270 : MediaConfig.DirectionMode.FLAG_DIRECTION_ROATATION_90) | MediaConfig.DirectionMode.FLAG_DIRECTION_FLIP_HORIZONTAL);
recordConfig.setBackCameraDirectionMode((backDirection == 90 ? MediaConfig.DirectionMode.FLAG_DIRECTION_ROATATION_90 : MediaConfig.DirectionMode.FLAG_DIRECTION_ROATATION_270));
} else {
recordConfig.setBackCameraDirectionMode((backDirection == 90 ? MediaConfig.DirectionMode.FLAG_DIRECTION_ROATATION_0 : MediaConfig.DirectionMode.FLAG_DIRECTION_ROATATION_180));
recordConfig.setFrontCameraDirectionMode((frontDirection == 90 ? MediaConfig.DirectionMode.FLAG_DIRECTION_ROATATION_180 : MediaConfig.DirectionMode.FLAG_DIRECTION_ROATATION_0) | MediaConfig.DirectionMode.FLAG_DIRECTION_FLIP_HORIZONTAL);
}
//save video
String mSaveVideoPath = Environment.getExternalStorageDirectory().getPath() + "/usb_video" + System.currentTimeMillis() + ".mp4";
recordConfig.setSaveVideoPath(mSaveVideoPath);
if (!mRecorderClient.prepare(mContext, recordConfig)) {
mRecorderClient = null;
Log.e("RecordingActivity", "prepare,failed!!");
updateOutsideCamState(0);
return;
}
Size s = mRecorderClient.getVideoSize();
mTextureView.setAspectRatio(AspectTextureView.MODE_FITXY, ((double) s.getWidth()) / s.getHeight());
mRecorderClient.setVideoChangeListener(new IVideoChange() {
@Override
public void onVideoSizeChanged(int width, int height) {
mTextureView.setAspectRatio(AspectTextureView.MODE_FITXY, ((double) width) / height);
}
});
}catch (Exception e){
e.printStackTrace();
}
}
protected void onSetFilters() {
mRecorderClient.setHardVideoFilter(new DrawMultiImageFilter(mContext));
}
public void recordVideo(){
if (!isStart) {
updateOutsideCamState(1);
String path = "/sdcard/test_B.mp4";
mRecorderClient.updatePath(path);
mRecorderClient.startRecording();
} else{
mRecorderClient.stopRecording();
}
isStart = !isStart;
}
public void setUsbAudioCallback(UvcPcmDataCallBack l){
mActivity.setUsbAudioCallback(l);
}
public void updateOutsideCamState(int status){
try {
// mActivity.updateOutsideCamState(status);
}catch (Exception e){
e.printStackTrace();
}
}
}
接下来录像操作:
/**
* 开始/结束录像按钮
*/
private void recordFunc(){
if (isRecording) {
mRecordBtn.setText("开始录像");
}else {
mRecordBtn.setText("结束录像");
}
recordVideo();
isRecording = !isRecording;
}
private void recordVideo(){
mAudioManager.record();
if (glUsbManager != null) {
glUsbManager.recordVideo();
} else {
Log.e("usbCamera", "start usb camera record fail,gl usb manager is null...");
}
mGLRecord.setVideoFileName();
mGLRecord.recordVideo();
getGLAudioListener();
}
另外,由于麦克风在Android上是独占性质,所以拿到pcm音频流后,需要分出两路供两个摄像头录像使用:
/**
* 原始音频来了转发给各线程分享
* @param buffer
* @param readBytes
*/
public void onFramePcm(ByteBuffer buffer, int readBytes) {
if (uvcPcmDataCallBack != null){
uvcPcmDataCallBack.onFramePcm(buffer, readBytes);//UVC
buffer.flip();
//buffer.clear();
}
if (glAudioCallback != null) {
glAudioCallback.onPcm(buffer, readBytes);//本地
buffer.flip();
}
}
最后时间水印重要部分代码如下:
import android.content.Context;
import android.graphics.Rect;
import android.graphics.RectF;
import android.opengl.GLES11Ext;
import android.opengl.GLES20;
import android.util.Log;
import com.reach.mutilcamerarecord.mediausb.filter.hardvideofilter.BaseHardVideoFilter;
import com.reach.mutilcamerarecord.mediausb.tools.GLESTools;
import com.reach.mutilcamerarecord.utils.BitmapUtils;
import java.nio.FloatBuffer;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
/**
* Author:lzh on 2021/7/14 17:34
*/
public class DrawMultiImageFilter extends BaseHardVideoFilter {
protected int glProgram;
protected int glCamTextureLoc;
protected int glCamPostionLoc;
protected int glCamTextureCoordLoc;
protected int glImageTextureLoc;
protected int glImageRectLoc;
protected int glImageAngelLoc;
protected Context mContext;
//private ArrayList<ImageDrawData> mImageInfos = new ArrayList<>();
private ArrayList<ImageTexture> imageTextures = new ArrayList<>();
private int mSize = 12;
int textureId;
int frameBuffer;
Rect rect;
int x;
int y;
String str;
private final static SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public DrawMultiImageFilter(Context context) {
super();
mContext = context;
}
@Override
public void onInit(int videoWidth, int videoHeight) {
super.onInit(videoWidth, videoHeight);
glProgram = GLESTools.createProgram(GLESTools.uRes(mContext.getResources(), "drawimage_vertex.sh"),
GLESTools.uRes(mContext.getResources(), "drawimage_fragment.sh"));
GLES20.glUseProgram(glProgram);
glCamTextureLoc = GLES20.glGetUniformLocation(glProgram, "uCamTexture");
glImageTextureLoc = GLES20.glGetUniformLocation(glProgram, "uImageTexture");
glCamPostionLoc = GLES20.glGetAttribLocation(glProgram, "aCamPosition");
glCamTextureCoordLoc = GLES20.glGetAttribLocation(glProgram, "aCamTextureCoord");
glImageRectLoc = GLES20.glGetUniformLocation(glProgram, "imageRect");
glImageAngelLoc = GLES20.glGetUniformLocation(glProgram, "imageAngel");
x = 20;
y = videoHeight - 50;//时间水印设在左下角
Log.e("water", "usb water title,y pos:" + y + ",h=" + videoHeight);
initImageTexture();
}
protected void initImageTexture() {
imageTextures = new ArrayList<>();
ImageTexture imageTexture;
for (int i = 0; i < 19; i++) {
imageTexture = new ImageTexture(outVideoWidth, outVideoHeight);
if (i == 10) {
imageTexture.loadBitmap(BitmapUtils.textToBitmap("-"));
} else if (i == 11) {
imageTexture.loadBitmap(BitmapUtils.textToBitmap(":"));
} else if (i >=0 && i <= 9){
imageTexture.loadBitmap(BitmapUtils.textToBitmap(i + ""));
} else {
imageTexture.loadBitmap(BitmapUtils.textToBitmap("*"));
}
imageTextures.add(imageTexture);
}
mSize = imageTextures.size();
}
@Override
public void onDraw(int cameraTexture, int targetFrameBuffer, FloatBuffer shapeBuffer, FloatBuffer textureBuffer) {
GLES20.glViewport(0, 0, outVideoWidth, outVideoHeight);
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
ImageTexture preImageTexture = null;
String time = formatter.format(new Date());
if ("".equals(time)) {
return;
}
for (int i = 0; i < mSize; i++) {
if (preImageTexture == null) {
textureId = cameraTexture;
} else {
textureId = preImageTexture.getTextureId();
}
if (i == mSize - 1) {
frameBuffer = targetFrameBuffer;
} else {
frameBuffer = imageTextures.get(i).getFrameBuffer();
}
rect = new Rect(x+15*i, y, x+15*i+220, y+40);
if (rect.left == rect.right || rect.top == rect.bottom) {
continue;
}
str = time.substring(i, i+1);
if (str.equals("-")){
str = "10";
} else if (str.equals(":")){
str = "11";
} else if (str.equals(" ") || str.equals("*")){
continue;
}
drawImage(convertToRectF(rect), imageTextures.get(Integer.parseInt(str)).getImageTextureId(), textureId, frameBuffer, shapeBuffer, textureBuffer);
preImageTexture = imageTextures.get(i);
}
GLES20.glFinish();
}
protected void drawImage(RectF rectF, int imageTextureId, int cameraTexture, int targetFrameBuffer, FloatBuffer shapeBuffer, FloatBuffer textureBuffer) {
GLES20.glEnableVertexAttribArray(glCamPostionLoc);
GLES20.glEnableVertexAttribArray(glCamTextureCoordLoc);
shapeBuffer.position(0);
GLES20.glVertexAttribPointer(glCamPostionLoc, 2,
GLES20.GL_FLOAT, false,
2 * 4, shapeBuffer);
textureBuffer.position(0);
GLES20.glVertexAttribPointer(glCamTextureCoordLoc, 2,
GLES20.GL_FLOAT, false,
2 * 4, textureBuffer);
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, targetFrameBuffer);
GLES20.glUseProgram(glProgram);
GLES20.glUniform4f(glImageRectLoc, rectF.left, rectF.top, rectF.right, rectF.bottom);
// GLES20.glUniform1f(glImageAngelLoc, (float)(30.0f*Math.PI/180));//用来更新旋转角度的
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, cameraTexture);
GLES20.glUniform1i(glCamTextureLoc, 0);
GLES20.glActiveTexture(GLES20.GL_TEXTURE1);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, imageTextureId);
GLES20.glUniform1i(glImageTextureLoc, 1);
GLES20.glDrawElements(GLES20.GL_TRIANGLES, drawIndecesBuffer.limit(), GLES20.GL_UNSIGNED_SHORT, drawIndecesBuffer);
GLES20.glDisableVertexAttribArray(glCamPostionLoc);
GLES20.glDisableVertexAttribArray(glCamTextureCoordLoc);
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0);
GLES20.glUseProgram(0);
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
}
@Override
public void onDestroy() {
super.onDestroy();
GLES20.glDeleteProgram(glProgram);
destroyImageTexture();
}
protected void destroyImageTexture() {
for (ImageTexture imageTexture : imageTextures) {
imageTexture.destroy();
}
}
private RectF convertToRectF(Rect iconRect) {
RectF iconRectF = new RectF();
iconRectF.top = iconRect.top / (float) outVideoHeight;
iconRectF.bottom = iconRect.bottom / (float) outVideoHeight;
iconRectF.left = iconRect.left / (float) outVideoWidth;
iconRectF.right = iconRect.right / (float) outVideoWidth;
return iconRectF;
}
public static class ImageDrawData {
public int resId = 0;
public Rect rect;
}
}
本文最后也附上demo的下载地址,供大家下载学习,有什么问题可以直接在文章下面评论或留言哈~~
CSDN下载地址:https://download.csdn.net/download/toyauko/21861215
github下载地址:https://github.com/Liangzhuhua/MutilCameraRecordDemo