[*] 优化拍照

This commit is contained in:
acgist
2023-05-22 08:20:59 +08:00
parent a6ced43283
commit 3cf25a1736
3 changed files with 127 additions and 18 deletions

View File

@@ -881,6 +881,7 @@ public final class MediaManager {
@Override @Override
public void onFrameCaptured(VideoFrame videoFrame) { public void onFrameCaptured(VideoFrame videoFrame) {
// TODO验证使用一个source使用cropandscale缩放看看性能能否提升
// 注意VideoFrame必须释放多线程环境需要调用retain和release方法。 // 注意VideoFrame必须释放多线程环境需要调用retain和release方法。
if(MediaManager.this.videoProcesser == null) { if(MediaManager.this.videoProcesser == null) {
this.mainObserver.onFrameCaptured(videoFrame); this.mainObserver.onFrameCaptured(videoFrame);

View File

@@ -44,36 +44,42 @@ public abstract class Client extends CloseableClient {
* 播放音频 * 播放音频
*/ */
public void playAudio() { public void playAudio() {
Log.i(Client.class.getSimpleName(), "播放音频:" + this.clientId);
} }
/** /**
* 暂停音频 * 暂停音频
*/ */
public void pauseAudio() { public void pauseAudio() {
Log.i(Client.class.getSimpleName(), "暂停音频:" + this.clientId);
} }
/** /**
* 恢复音频 * 恢复音频
*/ */
public void resumeAudio() { public void resumeAudio() {
Log.i(Client.class.getSimpleName(), "恢复音频:" + this.clientId);
} }
/** /**
* 播放视频 * 播放视频
*/ */
public void playVideo() { public void playVideo() {
Log.i(Client.class.getSimpleName(), "播放视频:" + this.clientId);
} }
/** /**
* 暂停视频 * 暂停视频
*/ */
public void pauseVideo() { public void pauseVideo() {
Log.i(Client.class.getSimpleName(), "暂停视频:" + this.clientId);
} }
/** /**
* 恢复视频 * 恢复视频
*/ */
public void resumeVideo() { public void resumeVideo() {
Log.i(Client.class.getSimpleName(), "恢复视频:" + this.clientId);
} }
/** /**

View File

@@ -46,31 +46,74 @@ import java.util.List;
/** /**
* 拍照终端 * 拍照终端
* *
* 没有拉流时使用Camera2拍照
* 拉流时使用WebRTC帧数据拍照
*
* @author acgist * @author acgist
*/ */
public class PhotographClient implements VideoSink { public class PhotographClient implements VideoSink {
/**
* 图片质量
*/
private final int quantity; private final int quantity;
/**
* 图片名称
*/
private final String filename; private final String filename;
/**
* 图片路径
*/
private final String filepath; private final String filepath;
private volatile boolean done; /**
* 是否完成
*/
private volatile boolean finish; private volatile boolean finish;
/**
* 是否采集到了图片数据
*/
private volatile boolean hasImage;
/**
* Camera2拍照Surface
*/
private Surface surface; private Surface surface;
/**
* WebRTC VideoTrack
*/
private VideoTrack videoTrack; private VideoTrack videoTrack;
/**
* Camera2拍照图片处理
*/
private ImageReader imageReader; private ImageReader imageReader;
/**
* Camera2设备
*/
private CameraDevice cameraDevice; private CameraDevice cameraDevice;
/**
* 拍照线程
*/
private HandlerThread handlerThread; private HandlerThread handlerThread;
/**
* Camera2图片采集线程
*/
private CameraCaptureSession cameraCaptureSession; private CameraCaptureSession cameraCaptureSession;
/**
* @param quantity 图片质量
* @param path 图片路径
*/
public PhotographClient(int quantity, String path) { public PhotographClient(int quantity, String path) {
this.quantity = quantity; this.quantity = quantity;
this.filename = DateUtils.format(LocalDateTime.now(), DateUtils.DateTimeStyle.YYYYMMDDHH24MMSS) + ".jpg"; this.filename = DateUtils.format(LocalDateTime.now(), DateUtils.DateTimeStyle.YYYYMMDDHH24MMSS) + ".jpg";
this.filepath = Paths.get(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getAbsolutePath(), path, this.filename).toString(); this.filepath = Paths.get(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getAbsolutePath(), path, this.filename).toString();
this.done = false;
this.finish = false; this.finish = false;
this.hasImage = false;
Log.i(RecordClient.class.getSimpleName(), "拍摄照片文件:" + this.filepath); Log.i(RecordClient.class.getSimpleName(), "拍摄照片文件:" + this.filepath);
} }
/**
* 唤醒等待现场
*/
private void notifyWait() { private void notifyWait() {
synchronized (this) { synchronized (this) {
this.finish = true; this.finish = true;
@@ -78,6 +121,11 @@ public class PhotographClient implements VideoSink {
} }
} }
/**
* 等待拍照完成
*
* @return 图片路径
*/
public String waitForPhotograph() { public String waitForPhotograph() {
synchronized (this) { synchronized (this) {
try { try {
@@ -96,6 +144,12 @@ public class PhotographClient implements VideoSink {
return this.filepath; return this.filepath;
} }
/**
* WebRTC拍照
*
* @param videoSource 视频来源
* @param peerConnectionFactory PeerConnectionFactory
*/
public void photograph(VideoSource videoSource, PeerConnectionFactory peerConnectionFactory) { public void photograph(VideoSource videoSource, PeerConnectionFactory peerConnectionFactory) {
this.videoTrack = peerConnectionFactory.createVideoTrack("TaoyaoVP", videoSource); this.videoTrack = peerConnectionFactory.createVideoTrack("TaoyaoVP", videoSource);
this.videoTrack.setEnabled(true); this.videoTrack.setEnabled(true);
@@ -104,10 +158,15 @@ public class PhotographClient implements VideoSink {
@Override @Override
public void onFrame(VideoFrame videoFrame) { public void onFrame(VideoFrame videoFrame) {
if(this.done) { if(this.hasImage) {
// 已经完成忽略 // 已经完成忽略
} else { } else {
this.done = true; synchronized(this) {
if(this.hasImage) {
return;
}
this.hasImage = true;
}
this.handlerThread = new HandlerThread("PhotographThread"); this.handlerThread = new HandlerThread("PhotographThread");
this.handlerThread.start(); this.handlerThread.start();
final Handler handler = new Handler(this.handlerThread.getLooper()); final Handler handler = new Handler(this.handlerThread.getLooper());
@@ -116,22 +175,27 @@ public class PhotographClient implements VideoSink {
} }
} }
/**
* WebRTC拍照
*
* @param videoFrame 视频帧
*/
private void photograph(VideoFrame videoFrame) { private void photograph(VideoFrame videoFrame) {
final VideoFrame.I420Buffer i420 = videoFrame.getBuffer().toI420(); final VideoFrame.I420Buffer i420 = videoFrame.getBuffer().toI420();
videoFrame.release(); videoFrame.release();
final File file = new File(this.filepath); final File file = new File(this.filepath);
final int width = i420.getWidth(); final int width = i420.getWidth();
final int height = i420.getHeight(); final int height = i420.getHeight();
// YuvHelper转换颜色溢出 // YuvHelper转换颜色溢出
final YuvImage image = this.i420ToYuvImage(i420, width, height); final YuvImage image = this.i420ToYuvImage(i420, width, height);
i420.release(); i420.release();
final Rect rect = new Rect(0, 0, width, height); final Rect rect = new Rect(0, 0, width, height);
try ( try (
final OutputStream output = new FileOutputStream(file); final OutputStream output = new FileOutputStream(file);
final ByteArrayOutputStream byteArray = new ByteArrayOutputStream(); final ByteArrayOutputStream byteArray = new ByteArrayOutputStream();
) { ) {
image.compressToJpeg(rect, this.quantity, byteArray); image.compressToJpeg(rect, this.quantity, byteArray);
final byte[] array = byteArray.toByteArray(); final byte[] array = byteArray.toByteArray();
final Bitmap bitmap = BitmapFactory.decodeByteArray(array, 0, array.length); final Bitmap bitmap = BitmapFactory.decodeByteArray(array, 0, array.length);
// final Matrix matrix = new Matrix(); // final Matrix matrix = new Matrix();
// matrix.setRotate(90); // matrix.setRotate(90);
@@ -145,6 +209,13 @@ public class PhotographClient implements VideoSink {
} }
} }
/**
* @param i420 I420帧数据
* @param width 图片宽度
* @param height 图片高度
*
* @return YuvImage
*/
private YuvImage i420ToYuvImage(VideoFrame.I420Buffer i420, int width, int height) { private YuvImage i420ToYuvImage(VideoFrame.I420Buffer i420, int width, int height) {
int index = 0; int index = 0;
final int yy = i420.getStrideY(); final int yy = i420.getStrideY();
@@ -159,7 +230,7 @@ public class PhotographClient implements VideoSink {
nv21[index++] = y.get(col + row * yy); nv21[index++] = y.get(col + row * yy);
} }
} }
final int halfWidth = width / 2; final int halfWidth = width / 2;
final int halfHeight = height / 2; final int halfHeight = height / 2;
for (int row = 0; row < halfHeight; row++) { for (int row = 0; row < halfHeight; row++) {
for (int col = 0; col < halfWidth; col++) { for (int col = 0; col < halfWidth; col++) {
@@ -170,6 +241,9 @@ public class PhotographClient implements VideoSink {
return new YuvImage(nv21, ImageFormat.NV21, width, height, null); return new YuvImage(nv21, ImageFormat.NV21, width, height, null);
} }
/**
* 关闭VideoTrack
*/
private void closeVideoTrack() { private void closeVideoTrack() {
if(this.videoTrack != null) { if(this.videoTrack != null) {
this.videoTrack.removeSink(this); this.videoTrack.removeSink(this);
@@ -178,6 +252,15 @@ public class PhotographClient implements VideoSink {
} }
} }
/**
* Camera2拍照
*
* @param width 图片宽度
* @param height 图片高度
* @param fps 帧率
* @param videoSourceType 图片来源
* @param context 上下文
*/
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
public void photograph(int width, int height, int fps, VideoSourceType videoSourceType, Context context) { public void photograph(int width, int height, int fps, VideoSourceType videoSourceType, Context context) {
if(this.handlerThread != null) { if(this.handlerThread != null) {
@@ -195,14 +278,21 @@ public class PhotographClient implements VideoSink {
final String[] cameraIdList = cameraManager.getCameraIdList(); final String[] cameraIdList = cameraManager.getCameraIdList();
for (String id : cameraIdList) { for (String id : cameraIdList) {
final CameraCharacteristics cameraCharacteristics = cameraManager.getCameraCharacteristics(id); final CameraCharacteristics cameraCharacteristics = cameraManager.getCameraCharacteristics(id);
if(cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_BACK && videoSourceType == VideoSourceType.BACK) { final int lensFacing = cameraCharacteristics.get(CameraCharacteristics.LENS_FACING);
if(
lensFacing == CameraCharacteristics.LENS_FACING_BACK &&
videoSourceType == VideoSourceType.BACK
) {
cameraId = id; cameraId = id;
break; break;
} else if(cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT && videoSourceType == VideoSourceType.FRONT) { } else if(
lensFacing == CameraCharacteristics.LENS_FACING_FRONT &&
videoSourceType == VideoSourceType.FRONT
) {
cameraId = id; cameraId = id;
break; break;
} else { } else {
// TODO截屏 // 其他情况:文件、截屏
} }
} }
if(cameraId == null) { if(cameraId == null) {
@@ -217,6 +307,9 @@ public class PhotographClient implements VideoSink {
}); });
} }
/**
* Camera2设备回调
*/
private CameraDevice.StateCallback cameraDeviceStateCallback = new CameraDevice.StateCallback() { private CameraDevice.StateCallback cameraDeviceStateCallback = new CameraDevice.StateCallback() {
@Override @Override
@@ -244,15 +337,17 @@ public class PhotographClient implements VideoSink {
}; };
/**
* Camera2会话回调
*/
private CameraCaptureSession.StateCallback cameraCaptureSessionStateCallback = new CameraCaptureSession.StateCallback() { private CameraCaptureSession.StateCallback cameraCaptureSessionStateCallback = new CameraCaptureSession.StateCallback() {
@Override @Override
public void onConfigured(CameraCaptureSession cameraCaptureSession) { public void onConfigured(CameraCaptureSession cameraCaptureSession) {
try { try {
PhotographClient.this.cameraCaptureSession = cameraCaptureSession; PhotographClient.this.cameraCaptureSession = cameraCaptureSession;
final CaptureRequest.Builder builder = PhotographClient.this.cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); final CaptureRequest.Builder builder = PhotographClient.this.cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
builder.set(CaptureRequest.JPEG_QUALITY, (byte) PhotographClient.this.quantity); builder.set(CaptureRequest.JPEG_QUALITY, (byte) PhotographClient.this.quantity);
// builder.set(CaptureRequest.JPEG_ORIENTATION, 90);
builder.addTarget(PhotographClient.this.surface); builder.addTarget(PhotographClient.this.surface);
cameraCaptureSession.setRepeatingRequest(builder.build(), PhotographClient.this.cameraCaptureSessionCaptureCallback, null); cameraCaptureSession.setRepeatingRequest(builder.build(), PhotographClient.this.cameraCaptureSessionCaptureCallback, null);
} catch (CameraAccessException e) { } catch (CameraAccessException e) {
@@ -266,6 +361,9 @@ public class PhotographClient implements VideoSink {
}; };
/**
* Camera2捕获回调
*/
private CameraCaptureSession.CaptureCallback cameraCaptureSessionCaptureCallback = new CameraCaptureSession.CaptureCallback() { private CameraCaptureSession.CaptureCallback cameraCaptureSessionCaptureCallback = new CameraCaptureSession.CaptureCallback() {
private volatile int index = 0; private volatile int index = 0;
@@ -276,12 +374,12 @@ public class PhotographClient implements VideoSink {
if(image == null) { if(image == null) {
return; return;
} }
if(this.index++ <= 4 || PhotographClient.this.done) { if(this.index++ <= 4 || PhotographClient.this.hasImage) {
image.close(); image.close();
return; return;
} }
PhotographClient.this.done = true; PhotographClient.this.hasImage = true;
final Image.Plane[] planes = image.getPlanes(); final Image.Plane[] planes = image.getPlanes();
final ByteBuffer byteBuffer = planes[0].getBuffer(); final ByteBuffer byteBuffer = planes[0].getBuffer();
final byte[] bytes = new byte[byteBuffer.remaining()]; final byte[] bytes = new byte[byteBuffer.remaining()];
byteBuffer.get(bytes); byteBuffer.get(bytes);
@@ -291,16 +389,19 @@ public class PhotographClient implements VideoSink {
// bitmap.compress(Bitmap.CompressFormat.JPEG, PhotographClient.this.quantity, output); // bitmap.compress(Bitmap.CompressFormat.JPEG, PhotographClient.this.quantity, output);
output.write(bytes, 0, bytes.length); output.write(bytes, 0, bytes.length);
cameraCaptureSession.stopRepeating(); cameraCaptureSession.stopRepeating();
PhotographClient.this.notifyWait();
} catch (IOException | CameraAccessException e) { } catch (IOException | CameraAccessException e) {
Log.e(PhotographClient.class.getSimpleName(), "拍照异常", e); Log.e(PhotographClient.class.getSimpleName(), "拍照异常", e);
} finally { } finally {
image.close(); image.close();
PhotographClient.this.notifyWait();
} }
} }
}; };
/**
* 关闭Camera2
*/
private void closeCamera() { private void closeCamera() {
if(this.cameraCaptureSession != null) { if(this.cameraCaptureSession != null) {
this.cameraCaptureSession.close(); this.cameraCaptureSession.close();
@@ -311,6 +412,7 @@ public class PhotographClient implements VideoSink {
this.cameraDevice = null; this.cameraDevice = null;
} }
if(this.imageReader != null) { if(this.imageReader != null) {
// 包含释放Surface
this.imageReader.close(); this.imageReader.close();
this.imageReader = null; this.imageReader = null;
} }