[*] 优化拍照
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user