|
@@ -0,0 +1,891 @@
|
|
|
+package com.easygbs.device.push;
|
|
|
+
|
|
|
+import android.content.Context;
|
|
|
+import android.content.Intent;
|
|
|
+import android.graphics.ImageFormat;
|
|
|
+import android.graphics.SurfaceTexture;
|
|
|
+import android.hardware.Camera;
|
|
|
+import android.media.MediaCodecInfo;
|
|
|
+import android.media.MediaCodecList;
|
|
|
+import android.media.MediaFormat;
|
|
|
+import android.os.Build;
|
|
|
+import android.os.Handler;
|
|
|
+import android.os.HandlerThread;
|
|
|
+import android.os.Message;
|
|
|
+import android.os.Process;
|
|
|
+import android.util.Log;
|
|
|
+
|
|
|
+import com.easygbs.Device;
|
|
|
+import com.easygbs.device.BackgroundCameraService;
|
|
|
+import com.easygbs.device.util.DataUtil;
|
|
|
+import com.easygbs.device.util.SPUtil;
|
|
|
+import com.serenegiant.usb.IFrameCallback;
|
|
|
+import com.serenegiant.usb.UVCCamera;
|
|
|
+
|
|
|
+import org.easydarwin.bus.SupportResolution;
|
|
|
+
|
|
|
+import com.easygbs.device.UVCCameraService;
|
|
|
+
|
|
|
+import org.easydarwin.encode.AudioStream;
|
|
|
+import org.easydarwin.encode.ClippableVideoConsumer;
|
|
|
+import org.easydarwin.encode.HWConsumer;
|
|
|
+import org.easydarwin.encode.SWConsumer;
|
|
|
+import org.easydarwin.encode.VideoConsumer;
|
|
|
+import org.easydarwin.muxer.EasyMuxer;
|
|
|
+import org.easydarwin.muxer.RecordVideoConsumer;
|
|
|
+import org.easydarwin.push.Pusher;
|
|
|
+import org.easydarwin.sw.JNIUtil;
|
|
|
+import org.easydarwin.util.BUSUtil;
|
|
|
+import org.easydarwin.util.Util;
|
|
|
+
|
|
|
+import java.io.File;
|
|
|
+import java.io.IOException;
|
|
|
+import java.io.PrintWriter;
|
|
|
+import java.io.StringWriter;
|
|
|
+import java.lang.ref.WeakReference;
|
|
|
+import java.nio.ByteBuffer;
|
|
|
+import java.text.SimpleDateFormat;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.Date;
|
|
|
+import java.util.Iterator;
|
|
|
+import java.util.List;
|
|
|
+import java.util.concurrent.ArrayBlockingQueue;
|
|
|
+import java.util.concurrent.BlockingQueue;
|
|
|
+
|
|
|
+import static android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedPlanar;
|
|
|
+import static android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar;
|
|
|
+import static android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar;
|
|
|
+import static android.media.MediaCodecInfo.CodecCapabilities.COLOR_TI_FormatYUV420PackedSemiPlanar;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 摄像头实时数据采集,并调用相关编码器
|
|
|
+ * */
|
|
|
+public class MediaStream {
|
|
|
+ private static final String TAG = MediaStream.class.getSimpleName();
|
|
|
+ private static final int SWITCH_CAMERA = 11;
|
|
|
+
|
|
|
+ private final boolean enableVideo;
|
|
|
+ private boolean mSWCodec, mHevc; // mSWCodec是否软编码, mHevc是否H265
|
|
|
+
|
|
|
+ private String recordPath; // 录像地址
|
|
|
+ boolean isPushStream = false; // 是否要推送数据
|
|
|
+ private int displayRotationDegree; // 旋转角度
|
|
|
+
|
|
|
+ private Context context;
|
|
|
+ WeakReference<SurfaceTexture> mSurfaceHolderRef;
|
|
|
+
|
|
|
+ private VideoConsumer mVC, mRecordVC;
|
|
|
+ private AudioStream audioStream;
|
|
|
+ private EasyMuxer mMuxer;
|
|
|
+ private Pusher mEasyPusher;
|
|
|
+
|
|
|
+ private final HandlerThread mCameraThread;
|
|
|
+ private final Handler mCameraHandler;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 初始化MediaStream
|
|
|
+ * */
|
|
|
+ public MediaStream(Context context, SurfaceTexture texture, boolean enableVideo) {
|
|
|
+ this.context = context;
|
|
|
+ audioStream = AudioStream.getInstance(context);
|
|
|
+ mSurfaceHolderRef = new WeakReference(texture);
|
|
|
+
|
|
|
+ mCameraThread = new HandlerThread("CAMERA") {
|
|
|
+ public void run() {
|
|
|
+ try {
|
|
|
+ super.run();
|
|
|
+ } catch (Throwable e) {
|
|
|
+ e.printStackTrace();
|
|
|
+
|
|
|
+ Intent intent = new Intent(context, BackgroundCameraService.class);
|
|
|
+ context.stopService(intent);
|
|
|
+ } finally {
|
|
|
+ stopStream();
|
|
|
+ stopPreview();
|
|
|
+ destroyCamera();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ mCameraThread.start();
|
|
|
+
|
|
|
+ mCameraHandler = new Handler(mCameraThread.getLooper()) {
|
|
|
+ @Override
|
|
|
+ public void handleMessage(Message msg) {
|
|
|
+ super.handleMessage(msg);
|
|
|
+
|
|
|
+ if (msg.what == SWITCH_CAMERA) {
|
|
|
+ switchCameraTask.run();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ this.enableVideo = enableVideo;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 初始化摄像头
|
|
|
+ public void createCamera() {
|
|
|
+ if (Thread.currentThread() != mCameraThread) {
|
|
|
+ mCameraHandler.post(() -> {
|
|
|
+ Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT);
|
|
|
+ createCamera();
|
|
|
+ });
|
|
|
+
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ mSWCodec = SPUtil.getswCodec(context);
|
|
|
+ mHevc = SPUtil.getHevcCodec(context);
|
|
|
+
|
|
|
+ mEasyPusher = new Device(context);
|
|
|
+ ((Device) mEasyPusher).setCallback(new Device.OnInitPusherCallback() {
|
|
|
+ @Override
|
|
|
+ public void onCallback(int code, String name) {
|
|
|
+ BUSUtil.BUS.post(new PushCallback(code, name));
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!enableVideo) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (mCameraId == CAMERA_FACING_BACK_UVC) {
|
|
|
+ createUvcCamera();
|
|
|
+ } else {
|
|
|
+ createNativeCamera();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void createNativeCamera() {
|
|
|
+ try {
|
|
|
+ mCamera = Camera.open(mCameraId);// 初始化创建Camera实例对象
|
|
|
+ mCamera.setErrorCallback((i, camera) -> {
|
|
|
+ throw new IllegalStateException("Camera Error:" + i);
|
|
|
+ });
|
|
|
+ Log.i(TAG, "open Camera");
|
|
|
+
|
|
|
+ parameters = mCamera.getParameters();
|
|
|
+
|
|
|
+ if (Util.getSupportResolution(context).size() == 0) {
|
|
|
+ StringBuilder stringBuilder = new StringBuilder();
|
|
|
+
|
|
|
+ // 查看支持的预览尺寸
|
|
|
+ List<Camera.Size> supportedPreviewSizes = parameters.getSupportedPreviewSizes();
|
|
|
+
|
|
|
+ for (Camera.Size str : supportedPreviewSizes) {
|
|
|
+ stringBuilder.append(str.width + "x" + str.height).append(";");
|
|
|
+ }
|
|
|
+
|
|
|
+ Util.saveSupportResolution(context, stringBuilder.toString());
|
|
|
+ }
|
|
|
+
|
|
|
+ BUSUtil.BUS.post(new SupportResolution());
|
|
|
+
|
|
|
+ camInfo = new Camera.CameraInfo();
|
|
|
+ Camera.getCameraInfo(mCameraId, camInfo);
|
|
|
+ int cameraRotationOffset = camInfo.orientation;
|
|
|
+
|
|
|
+ if (mCameraId == Camera.CameraInfo.CAMERA_FACING_FRONT)
|
|
|
+ cameraRotationOffset += 180;
|
|
|
+
|
|
|
+ int rotate = (360 + cameraRotationOffset - displayRotationDegree) % 360;
|
|
|
+ parameters.setRotation(rotate); // 设置Camera预览方向
|
|
|
+// parameters.setRecordingHint(true);
|
|
|
+
|
|
|
+ ArrayList<CodecInfo> infos = listEncoders(mHevc ? MediaFormat.MIMETYPE_VIDEO_HEVC : MediaFormat.MIMETYPE_VIDEO_AVC);
|
|
|
+
|
|
|
+ if (!infos.isEmpty()) {
|
|
|
+ CodecInfo ci = infos.get(0);
|
|
|
+ info.mName = ci.mName;
|
|
|
+ info.mColorFormat = ci.mColorFormat;
|
|
|
+ } else {
|
|
|
+ mSWCodec = true;
|
|
|
+ }
|
|
|
+
|
|
|
+// List<Camera.Size> sizes = parameters.getSupportedPreviewSizes();
|
|
|
+ parameters.setPreviewSize(defaultWidth, defaultHeight);// 设置预览尺寸
|
|
|
+
|
|
|
+ int[] ints = determineMaximumSupportedFramerate(parameters);
|
|
|
+ parameters.setPreviewFpsRange(ints[0], ints[1]);
|
|
|
+
|
|
|
+ List<String> supportedFocusModes = parameters.getSupportedFocusModes();
|
|
|
+
|
|
|
+ if (supportedFocusModes == null)
|
|
|
+ supportedFocusModes = new ArrayList<>();
|
|
|
+
|
|
|
+ // 自动对焦
|
|
|
+ if (supportedFocusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
|
|
|
+ parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
|
|
|
+ } else if (supportedFocusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) {
|
|
|
+ parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
|
|
|
+ }
|
|
|
+
|
|
|
+// int maxExposureCompensation = parameters.getMaxExposureCompensation();
|
|
|
+// parameters.setExposureCompensation(3);
|
|
|
+//
|
|
|
+// if(parameters.isAutoExposureLockSupported()) {
|
|
|
+// parameters.setAutoExposureLock(false);
|
|
|
+// }
|
|
|
+
|
|
|
+// parameters.setWhiteBalance(Camera.Parameters.WHITE_BALANCE_AUTO);
|
|
|
+// parameters.setFlashMode(Camera.Parameters.FLASH_MODE_AUTO);
|
|
|
+// parameters.setSceneMode(Camera.Parameters.SCENE_MODE_AUTO);
|
|
|
+// parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
|
|
|
+// mCamera.setFaceDetectionListener(new );
|
|
|
+
|
|
|
+// if (parameters.isAutoWhiteBalanceLockSupported()){
|
|
|
+// parameters.setAutoExposureLock(false);
|
|
|
+// }
|
|
|
+
|
|
|
+ mCamera.setParameters(parameters);
|
|
|
+ Log.i(TAG, "setParameters");
|
|
|
+
|
|
|
+ int displayRotation;
|
|
|
+ displayRotation = (cameraRotationOffset - displayRotationDegree + 360) % 360;
|
|
|
+ mCamera.setDisplayOrientation(displayRotation);
|
|
|
+
|
|
|
+ Log.i(TAG, "setDisplayOrientation");
|
|
|
+ } catch (Exception e) {
|
|
|
+ StringWriter sw = new StringWriter();
|
|
|
+ PrintWriter pw = new PrintWriter(sw);
|
|
|
+ e.printStackTrace(pw);
|
|
|
+
|
|
|
+// String stack = sw.toString();
|
|
|
+ destroyCamera();
|
|
|
+ e.printStackTrace();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void createUvcCamera() {
|
|
|
+// frameWidth = frameRotate ? height : width;
|
|
|
+// frameHeight = frameRotate ? width : height;
|
|
|
+
|
|
|
+ frameWidth = defaultWidth;
|
|
|
+ frameHeight = defaultHeight;
|
|
|
+
|
|
|
+ uvcCamera = UVCCameraService.liveData.getValue();
|
|
|
+ if (uvcCamera != null) {
|
|
|
+ uvcCamera.setPreviewSize(frameWidth,
|
|
|
+ frameHeight,
|
|
|
+ 1,
|
|
|
+ 30,
|
|
|
+ UVCCamera.PIXEL_FORMAT_YUV420SP,1.0f);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (uvcCamera == null) {
|
|
|
+ mCameraId = Camera.CameraInfo.CAMERA_FACING_BACK;
|
|
|
+ createNativeCamera();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 销毁Camera
|
|
|
+ public synchronized void destroyCamera() {
|
|
|
+ if (Thread.currentThread() != mCameraThread) {
|
|
|
+ mCameraHandler.post(() -> destroyCamera());
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (uvcCamera != null) {
|
|
|
+ uvcCamera.destroy();
|
|
|
+ uvcCamera = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (mCamera != null) {
|
|
|
+ mCamera.stopPreview();
|
|
|
+
|
|
|
+ try {
|
|
|
+ mCamera.release();
|
|
|
+ } catch (Exception e) {
|
|
|
+ e.printStackTrace();
|
|
|
+ }
|
|
|
+
|
|
|
+ Log.i(TAG, "release Camera");
|
|
|
+
|
|
|
+ mCamera = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (mMuxer != null) {
|
|
|
+ mMuxer.release();
|
|
|
+ mMuxer = null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 回收线程
|
|
|
+ public void release() {
|
|
|
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
|
|
+ mCameraThread.quitSafely();
|
|
|
+ } else {
|
|
|
+ if (!mCameraHandler.post(() -> mCameraThread.quit())) {
|
|
|
+ mCameraThread.quit();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ mCameraThread.join();
|
|
|
+ } catch (InterruptedException e) {
|
|
|
+ e.printStackTrace();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 开启预览
|
|
|
+ public synchronized void startPreview() {
|
|
|
+ if (Thread.currentThread() != mCameraThread) {
|
|
|
+ mCameraHandler.post(() -> startPreview());
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (uvcCamera != null) {
|
|
|
+ startUvcPreview();
|
|
|
+ } else if (mCamera != null) {
|
|
|
+ startCameraPreview();
|
|
|
+ }
|
|
|
+
|
|
|
+ if (mSWCodec) {
|
|
|
+ SWConsumer sw = new SWConsumer(context, mEasyPusher, SPUtil.getBitrateKbps(context));
|
|
|
+ mVC = new ClippableVideoConsumer(context,
|
|
|
+ sw,
|
|
|
+ frameWidth,
|
|
|
+ frameHeight,
|
|
|
+ SPUtil.getEnableVideoOverlay(context));
|
|
|
+ } else {
|
|
|
+ HWConsumer hw = new HWConsumer(context,
|
|
|
+ mHevc ? MediaFormat.MIMETYPE_VIDEO_HEVC : MediaFormat.MIMETYPE_VIDEO_AVC,
|
|
|
+ mEasyPusher,
|
|
|
+ SPUtil.getBitrateKbps(context),
|
|
|
+ info.mName,
|
|
|
+ info.mColorFormat);
|
|
|
+ mVC = new ClippableVideoConsumer(context,
|
|
|
+ hw,
|
|
|
+ frameWidth,
|
|
|
+ frameHeight,
|
|
|
+ SPUtil.getEnableVideoOverlay(context));
|
|
|
+ }
|
|
|
+
|
|
|
+ if (uvcCamera != null || mCamera != null) {
|
|
|
+ mVC.onVideoStart(frameWidth, frameHeight);
|
|
|
+ }
|
|
|
+
|
|
|
+ audioStream.setEnableAudio(SPUtil.getEnableAudio(context));
|
|
|
+ audioStream.addPusher(mEasyPusher);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void startUvcPreview() {
|
|
|
+ SurfaceTexture holder = mSurfaceHolderRef.get();
|
|
|
+ if (holder != null) {
|
|
|
+ uvcCamera.setPreviewTexture(holder);
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ uvcCamera.setFrameCallback(uvcFrameCallback, UVCCamera.PIXEL_FORMAT_YUV420SP/*UVCCamera.PIXEL_FORMAT_NV21*/);
|
|
|
+ uvcCamera.startPreview();
|
|
|
+ } catch (Throwable e){
|
|
|
+ e.printStackTrace();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void startCameraPreview() {
|
|
|
+ int previewFormat = parameters.getPreviewFormat();
|
|
|
+
|
|
|
+ Camera.Size previewSize = parameters.getPreviewSize();
|
|
|
+ int size = previewSize.width * previewSize.height * ImageFormat.getBitsPerPixel(previewFormat) / 8;
|
|
|
+
|
|
|
+ defaultWidth = previewSize.width;
|
|
|
+ defaultHeight = previewSize.height;
|
|
|
+
|
|
|
+ mCamera.addCallbackBuffer(new byte[size]);
|
|
|
+ mCamera.addCallbackBuffer(new byte[size]);
|
|
|
+ mCamera.setPreviewCallbackWithBuffer(previewCallback);
|
|
|
+
|
|
|
+ Log.i(TAG, "setPreviewCallbackWithBuffer");
|
|
|
+
|
|
|
+ try {
|
|
|
+ // TextureView的
|
|
|
+ SurfaceTexture holder = mSurfaceHolderRef.get();
|
|
|
+
|
|
|
+ // SurfaceView传入上面创建的Camera对象
|
|
|
+ if (holder != null) {
|
|
|
+ mCamera.setPreviewTexture(holder);
|
|
|
+ Log.i(TAG, "setPreviewTexture");
|
|
|
+ }
|
|
|
+ } catch (IOException e) {
|
|
|
+ e.printStackTrace();
|
|
|
+ }
|
|
|
+
|
|
|
+ mCamera.startPreview();
|
|
|
+
|
|
|
+ boolean frameRotate;
|
|
|
+ int result;
|
|
|
+
|
|
|
+ if (camInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
|
|
|
+ result = (camInfo.orientation + displayRotationDegree) % 360;
|
|
|
+ } else { // back-facing
|
|
|
+ result = (camInfo.orientation - displayRotationDegree + 360) % 360;
|
|
|
+ }
|
|
|
+
|
|
|
+ frameRotate = result % 180 != 0;
|
|
|
+
|
|
|
+ frameWidth = frameRotate ? defaultHeight : defaultWidth;
|
|
|
+ frameHeight = frameRotate ? defaultWidth : defaultHeight;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 停止预览
|
|
|
+ public synchronized void stopPreview() {
|
|
|
+ if (Thread.currentThread() != mCameraThread) {
|
|
|
+ mCameraHandler.post(() -> stopPreview());
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (uvcCamera != null) {
|
|
|
+ uvcCamera.stopPreview();
|
|
|
+ }
|
|
|
+
|
|
|
+// mCameraHandler.removeCallbacks(dequeueRunnable);
|
|
|
+
|
|
|
+ // 关闭摄像头
|
|
|
+ if (mCamera != null) {
|
|
|
+ mCamera.stopPreview();
|
|
|
+ mCamera.setPreviewCallbackWithBuffer(null);
|
|
|
+ Log.i(TAG, "StopPreview");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 关闭音频采集和音频编码器
|
|
|
+ if (audioStream != null) {
|
|
|
+ audioStream.removePusher(mEasyPusher);
|
|
|
+ audioStream.setMuxer(null);
|
|
|
+ Log.i(TAG, "Stop AudioStream");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 关闭视频编码器
|
|
|
+ if (mVC != null) {
|
|
|
+ mVC.onVideoStop();
|
|
|
+ Log.i(TAG, "Stop VC");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 关闭录像的编码器
|
|
|
+ if (mRecordVC != null) {
|
|
|
+ mRecordVC.onVideoStop();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 关闭音视频合成器
|
|
|
+ if (mMuxer != null) {
|
|
|
+ mMuxer.release();
|
|
|
+ mMuxer = null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 开始推流
|
|
|
+ public void startStream() throws IOException {
|
|
|
+ try {
|
|
|
+ mEasyPusher.initPush(DataUtil.getSIP());
|
|
|
+
|
|
|
+ isPushStream = true;
|
|
|
+ } catch (Exception ex) {
|
|
|
+ ex.printStackTrace();
|
|
|
+ throw new IOException(ex.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 停止推流
|
|
|
+ public void stopStream() {
|
|
|
+ if (mEasyPusher != null) {
|
|
|
+ mEasyPusher.stop();
|
|
|
+ }
|
|
|
+
|
|
|
+ isPushStream = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 开始录像
|
|
|
+ public synchronized void startRecord() {
|
|
|
+ if (Thread.currentThread() != mCameraThread) {
|
|
|
+ mCameraHandler.post(() -> startRecord());
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (mCamera == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 默认录像时间300000毫秒
|
|
|
+ mMuxer = new EasyMuxer(new File(recordPath, new SimpleDateFormat("yyyy_MM_dd_HH_mm_ss").format(new Date())).toString(), 300000);
|
|
|
+
|
|
|
+ mRecordVC = new RecordVideoConsumer(context,
|
|
|
+ mHevc ? MediaFormat.MIMETYPE_VIDEO_HEVC : MediaFormat.MIMETYPE_VIDEO_AVC,
|
|
|
+ mMuxer,
|
|
|
+ SPUtil.getEnableVideoOverlay(context),
|
|
|
+ SPUtil.getBitrateKbps(context),
|
|
|
+ info.mName,
|
|
|
+ info.mColorFormat);
|
|
|
+ mRecordVC.onVideoStart(frameWidth, frameHeight);
|
|
|
+
|
|
|
+ if (audioStream != null) {
|
|
|
+ audioStream.setMuxer(mMuxer);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 停止录像
|
|
|
+ public synchronized void stopRecord() {
|
|
|
+ if (Thread.currentThread() != mCameraThread) {
|
|
|
+ mCameraHandler.post(() -> stopRecord());
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (mRecordVC == null || audioStream == null) {
|
|
|
+// nothing
|
|
|
+ } else {
|
|
|
+ audioStream.setMuxer(null);
|
|
|
+ mRecordVC.onVideoStop();
|
|
|
+ mRecordVC = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (mMuxer != null)
|
|
|
+ mMuxer.release();
|
|
|
+
|
|
|
+ mMuxer = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 更新分辨率
|
|
|
+ public void updateResolution(final int w, final int h) {
|
|
|
+ if (mCamera == null)
|
|
|
+ return;
|
|
|
+
|
|
|
+ stopPreview();
|
|
|
+ destroyCamera();
|
|
|
+
|
|
|
+ mCameraHandler.post(() -> {
|
|
|
+ defaultWidth = w;
|
|
|
+ defaultHeight = h;
|
|
|
+ });
|
|
|
+
|
|
|
+ createCamera();
|
|
|
+ startPreview();
|
|
|
+ }
|
|
|
+
|
|
|
+ /* ============================== Camera ============================== */
|
|
|
+
|
|
|
+ /*
|
|
|
+ * 默认后置摄像头
|
|
|
+ * Camera.CameraInfo.CAMERA_FACING_BACK
|
|
|
+ * Camera.CameraInfo.CAMERA_FACING_FRONT
|
|
|
+ * CAMERA_FACING_BACK_UVC
|
|
|
+ * */
|
|
|
+ int mCameraId = Camera.CameraInfo.CAMERA_FACING_BACK;
|
|
|
+ public static final int CAMERA_FACING_BACK_UVC = 2;
|
|
|
+ public static final int CAMERA_FACING_BACK_LOOP = -1;
|
|
|
+
|
|
|
+ private int frameWidth;
|
|
|
+ private int frameHeight;
|
|
|
+ int defaultWidth = 1280, defaultHeight = 720;
|
|
|
+ private int mTargetCameraId;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 切换前后摄像头
|
|
|
+ * CAMERA_FACING_BACK_LOOP 循环切换摄像头
|
|
|
+ * Camera.CameraInfo.CAMERA_FACING_BACK 后置摄像头
|
|
|
+ * Camera.CameraInfo.CAMERA_FACING_FRONT 前置摄像头
|
|
|
+ * CAMERA_FACING_BACK_UVC UVC摄像头
|
|
|
+ * */
|
|
|
+ public void switchCamera(int cameraId) {
|
|
|
+ this.mTargetCameraId = cameraId;
|
|
|
+
|
|
|
+ if (mCameraHandler.hasMessages(SWITCH_CAMERA)) {
|
|
|
+ return;
|
|
|
+ } else {
|
|
|
+ mCameraHandler.sendEmptyMessage(SWITCH_CAMERA);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public void switchCamera() {
|
|
|
+ switchCamera(CAMERA_FACING_BACK_LOOP);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 切换摄像头的线程
|
|
|
+ private Runnable switchCameraTask = new Runnable() {
|
|
|
+ @Override
|
|
|
+ public void run() {
|
|
|
+ if (!enableVideo)
|
|
|
+ return;
|
|
|
+
|
|
|
+ try {
|
|
|
+ if (mTargetCameraId != CAMERA_FACING_BACK_LOOP && mCameraId == mTargetCameraId) {
|
|
|
+ if (uvcCamera != null || mCamera != null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (mTargetCameraId == CAMERA_FACING_BACK_LOOP) {
|
|
|
+ if (mCameraId == Camera.CameraInfo.CAMERA_FACING_BACK) {
|
|
|
+ mCameraId = Camera.CameraInfo.CAMERA_FACING_FRONT;
|
|
|
+ } else if (mCameraId == Camera.CameraInfo.CAMERA_FACING_FRONT) {
|
|
|
+ mCameraId = CAMERA_FACING_BACK_UVC;// 尝试切换到外置摄像头
|
|
|
+ } else {
|
|
|
+ mCameraId = Camera.CameraInfo.CAMERA_FACING_BACK;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ mCameraId = mTargetCameraId;
|
|
|
+ }
|
|
|
+
|
|
|
+ stopPreview();
|
|
|
+ destroyCamera();
|
|
|
+ createCamera();
|
|
|
+ startPreview();
|
|
|
+ } catch (Exception e) {
|
|
|
+ e.printStackTrace();
|
|
|
+ } finally {
|
|
|
+
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ /* ============================== Native Camera ============================== */
|
|
|
+
|
|
|
+ Camera mCamera;
|
|
|
+ private Camera.CameraInfo camInfo;
|
|
|
+ private Camera.Parameters parameters;
|
|
|
+ private byte[] i420_buffer;
|
|
|
+
|
|
|
+ // 摄像头预览的视频流数据
|
|
|
+ Camera.PreviewCallback previewCallback = (data, camera) -> {
|
|
|
+ if (data == null)
|
|
|
+ return;
|
|
|
+
|
|
|
+ int result;
|
|
|
+ if (camInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
|
|
|
+ result = (camInfo.orientation + displayRotationDegree) % 360;
|
|
|
+ } else { // back-facing
|
|
|
+ result = (camInfo.orientation - displayRotationDegree + 360) % 360;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (i420_buffer == null || i420_buffer.length != data.length) {
|
|
|
+ i420_buffer = new byte[data.length];
|
|
|
+ }
|
|
|
+
|
|
|
+ JNIUtil.ConvertToI420(data, i420_buffer, defaultWidth, defaultHeight, 0, 0, defaultWidth, defaultHeight, result % 360, 2);
|
|
|
+ System.arraycopy(i420_buffer, 0, data, 0, data.length);
|
|
|
+
|
|
|
+ if (mRecordVC != null) {
|
|
|
+ mRecordVC.onVideo(i420_buffer, 0);
|
|
|
+ }
|
|
|
+
|
|
|
+ mVC.onVideo(data, 0);
|
|
|
+ mCamera.addCallbackBuffer(data);
|
|
|
+ };
|
|
|
+
|
|
|
+ /* ============================== UVC Camera ============================== */
|
|
|
+
|
|
|
+ private UVCCamera uvcCamera;
|
|
|
+
|
|
|
+ BlockingQueue<byte[]> cache = new ArrayBlockingQueue<byte[]>(100);
|
|
|
+// BlockingQueue<byte[]>
|
|
|
+// = new ArrayBlockingQueue<byte[]>(10);
|
|
|
+// final Runnable dequeueRunnable = new Runnable() {
|
|
|
+// @Override
|
|
|
+// public void run() {
|
|
|
+// try {
|
|
|
+// byte[] data = bufferQueue.poll(10, TimeUnit.MICROSECONDS);
|
|
|
+//
|
|
|
+// if (data != null) {
|
|
|
+// onPreviewFrame2(data, uvcCamera);
|
|
|
+// cache.offer(data);
|
|
|
+// }
|
|
|
+//
|
|
|
+// if (uvcCamera == null)
|
|
|
+// return;
|
|
|
+//
|
|
|
+// mCameraHandler.post(this);
|
|
|
+// } catch (InterruptedException ex) {
|
|
|
+// ex.printStackTrace();
|
|
|
+// }
|
|
|
+// }
|
|
|
+// };
|
|
|
+
|
|
|
+ final IFrameCallback uvcFrameCallback = new IFrameCallback() {
|
|
|
+ @Override
|
|
|
+ public void onFrame(ByteBuffer frame) {
|
|
|
+ if (uvcCamera == null)
|
|
|
+ return;
|
|
|
+
|
|
|
+ Thread.currentThread().setName("UVCCamera");
|
|
|
+ frame.clear();
|
|
|
+
|
|
|
+ byte[] data = cache.poll();
|
|
|
+ if (data == null) {
|
|
|
+ data = new byte[frame.capacity()];
|
|
|
+ }
|
|
|
+
|
|
|
+ frame.get(data);
|
|
|
+
|
|
|
+// bufferQueue.offer(data);
|
|
|
+// mCameraHandler.post(dequeueRunnable);
|
|
|
+
|
|
|
+ onPreviewFrame2(data, uvcCamera);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ public void onPreviewFrame2(byte[] data, Object camera) {
|
|
|
+ if (data == null)
|
|
|
+ return;
|
|
|
+
|
|
|
+ if (i420_buffer == null || i420_buffer.length != data.length) {
|
|
|
+ i420_buffer = new byte[data.length];
|
|
|
+ }
|
|
|
+
|
|
|
+ JNIUtil.ConvertToI420(data, i420_buffer,
|
|
|
+ defaultWidth, defaultHeight,
|
|
|
+ 0, 0,
|
|
|
+ defaultWidth, defaultHeight,
|
|
|
+ 0, 2);
|
|
|
+ System.arraycopy(i420_buffer, 0, data, 0, data.length);
|
|
|
+
|
|
|
+ if (mRecordVC != null) {
|
|
|
+ mRecordVC.onVideo(i420_buffer, 0);
|
|
|
+ }
|
|
|
+
|
|
|
+ mVC.onVideo(data, 0);
|
|
|
+ }
|
|
|
+
|
|
|
+ /* ============================== CodecInfo ============================== */
|
|
|
+
|
|
|
+ public static CodecInfo info = new CodecInfo();
|
|
|
+
|
|
|
+ public static class CodecInfo {
|
|
|
+ public String mName;
|
|
|
+ public int mColorFormat;
|
|
|
+ }
|
|
|
+
|
|
|
+ public static ArrayList<CodecInfo> listEncoders(String mime) {
|
|
|
+ // 可能有多个编码库,都获取一下
|
|
|
+ ArrayList<CodecInfo> codecInfoList = new ArrayList<>();
|
|
|
+ int numCodecs = MediaCodecList.getCodecCount();
|
|
|
+
|
|
|
+ // int colorFormat = 0;
|
|
|
+ // String name = null;
|
|
|
+ for (int i1 = 0; i1 < numCodecs; i1++) {
|
|
|
+ MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i1);
|
|
|
+
|
|
|
+ if (!codecInfo.isEncoder()) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (codecMatch(mime, codecInfo)) {
|
|
|
+ String name = codecInfo.getName();
|
|
|
+ int colorFormat = getColorFormat(codecInfo, mime);
|
|
|
+
|
|
|
+ if (colorFormat != 0) {
|
|
|
+ CodecInfo ci = new CodecInfo();
|
|
|
+ ci.mName = name;
|
|
|
+ ci.mColorFormat = colorFormat;
|
|
|
+ codecInfoList.add(ci);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return codecInfoList;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* ============================== private method ============================== */
|
|
|
+
|
|
|
+ private static boolean codecMatch(String mimeType, MediaCodecInfo codecInfo) {
|
|
|
+ String[] types = codecInfo.getSupportedTypes();
|
|
|
+
|
|
|
+ for (String type : types) {
|
|
|
+ if (type.equalsIgnoreCase(mimeType)) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static int getColorFormat(MediaCodecInfo codecInfo, String mimeType) {
|
|
|
+ // 在ByteBuffer模式下,视频缓冲区根据其颜色格式进行布局。
|
|
|
+ MediaCodecInfo.CodecCapabilities capabilities = codecInfo.getCapabilitiesForType(mimeType);
|
|
|
+ int[] cf = new int[capabilities.colorFormats.length];
|
|
|
+ System.arraycopy(capabilities.colorFormats, 0, cf, 0, cf.length);
|
|
|
+ List<Integer> sets = new ArrayList<>();
|
|
|
+
|
|
|
+ for (int i = 0; i < cf.length; i++) {
|
|
|
+ sets.add(cf[i]);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (sets.contains(COLOR_FormatYUV420SemiPlanar)) {
|
|
|
+ return COLOR_FormatYUV420SemiPlanar;
|
|
|
+ } else if (sets.contains(COLOR_FormatYUV420Planar)) {
|
|
|
+ return COLOR_FormatYUV420Planar;
|
|
|
+ } else if (sets.contains(COLOR_FormatYUV420PackedPlanar)) {
|
|
|
+ return COLOR_FormatYUV420PackedPlanar;
|
|
|
+ } else if (sets.contains(COLOR_TI_FormatYUV420PackedSemiPlanar)) {
|
|
|
+ return COLOR_TI_FormatYUV420PackedSemiPlanar;
|
|
|
+ }
|
|
|
+
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static int[] determineMaximumSupportedFramerate(Camera.Parameters parameters) {
|
|
|
+ int[] maxFps = new int[]{0, 0};
|
|
|
+ List<int[]> supportedFpsRanges = parameters.getSupportedPreviewFpsRange();
|
|
|
+
|
|
|
+ for (Iterator<int[]> it = supportedFpsRanges.iterator(); it.hasNext(); ) {
|
|
|
+ int[] interval = it.next();
|
|
|
+
|
|
|
+ if (interval[1] > maxFps[1] || (interval[0] > maxFps[0] && interval[1] == maxFps[1])) {
|
|
|
+ maxFps = interval;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return maxFps;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* ============================== get/set ============================== */
|
|
|
+
|
|
|
+ public void setRecordPath(String recordPath) {
|
|
|
+ this.recordPath = recordPath;
|
|
|
+ }
|
|
|
+
|
|
|
+ public boolean isRecording() {
|
|
|
+ return mMuxer != null;
|
|
|
+ }
|
|
|
+
|
|
|
+ public void setSurfaceTexture(SurfaceTexture texture) {
|
|
|
+ mSurfaceHolderRef = new WeakReference<SurfaceTexture>(texture);
|
|
|
+ }
|
|
|
+
|
|
|
+ public boolean isStreaming() {
|
|
|
+ return isPushStream;
|
|
|
+ }
|
|
|
+
|
|
|
+ public Camera getCamera() {
|
|
|
+ return mCamera;
|
|
|
+ }
|
|
|
+
|
|
|
+ public int getDisplayRotationDegree() {
|
|
|
+ return displayRotationDegree;
|
|
|
+ }
|
|
|
+
|
|
|
+ public void setDisplayRotationDegree(int degree) {
|
|
|
+ displayRotationDegree = degree;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 旋转YUV格式数据
|
|
|
+ *
|
|
|
+ * @param src YUV数据
|
|
|
+ * @param format 0,420P;1,420SP
|
|
|
+ * @param width 宽度
|
|
|
+ * @param height 高度
|
|
|
+ * @param degree 旋转度数
|
|
|
+ */
|
|
|
+ private static void yuvRotate(byte[] src, int format, int width, int height, int degree) {
|
|
|
+ int offset = 0;
|
|
|
+ if (format == 0) {
|
|
|
+ JNIUtil.rotateMatrix(src, offset, width, height, degree);
|
|
|
+ offset += (width * height);
|
|
|
+ JNIUtil.rotateMatrix(src, offset, width / 2, height / 2, degree);
|
|
|
+ offset += width * height / 4;
|
|
|
+ JNIUtil.rotateMatrix(src, offset, width / 2, height / 2, degree);
|
|
|
+ } else if (format == 1) {
|
|
|
+ JNIUtil.rotateMatrix(src, offset, width, height, degree);
|
|
|
+ offset += width * height;
|
|
|
+ JNIUtil.rotateShortMatrix(src, offset, width / 2, height / 2, degree);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|