NekoX/TMessagesProj/src/main/java/org/telegram/messenger/camera/CameraController.java

792 lines
32 KiB
Java

/*
* This is the source code of Telegram for Android v. 5.x.x.
* It is licensed under GNU GPL v. 2 or later.
* You should have received a copy of the license in this archive (see LICENSE).
*
* Copyright Nikolai Kudashov, 2013-2018.
*/
package org.telegram.messenger.camera;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.graphics.SurfaceTexture;
import android.graphics.drawable.BitmapDrawable;
import android.hardware.Camera;
import android.media.MediaMetadataRetriever;
import android.media.MediaRecorder;
import android.os.Build;
import android.provider.MediaStore;
import android.util.Base64;
import org.telegram.messenger.AndroidUtilities;
import org.telegram.messenger.ApplicationLoader;
import org.telegram.messenger.Bitmaps;
import org.telegram.messenger.BuildVars;
import org.telegram.messenger.FileLoader;
import org.telegram.messenger.FileLog;
import org.telegram.messenger.ImageLoader;
import org.telegram.messenger.MessagesController;
import org.telegram.messenger.NotificationCenter;
import org.telegram.messenger.SendMessagesHelper;
import org.telegram.messenger.SharedConfig;
import org.telegram.messenger.Utilities;
import org.telegram.tgnet.SerializedData;
import java.io.File;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class CameraController implements MediaRecorder.OnInfoListener {
private static final int CORE_POOL_SIZE = 1;
private static final int MAX_POOL_SIZE = 1;
private static final int KEEP_ALIVE_SECONDS = 60;
private ThreadPoolExecutor threadPool;
protected ArrayList<String> availableFlashModes = new ArrayList<>();
private MediaRecorder recorder;
private String recordedFile;
protected volatile ArrayList<CameraInfo> cameraInfos;
private VideoTakeCallback onVideoTakeCallback;
private boolean cameraInitied;
private boolean loadingCameras;
private ArrayList<Runnable> onFinishCameraInitRunnables = new ArrayList<>();
private static volatile CameraController Instance = null;
public interface VideoTakeCallback {
void onFinishVideoRecording(String thumbPath, long duration);
}
public static CameraController getInstance() {
CameraController localInstance = Instance;
if (localInstance == null) {
synchronized (CameraController.class) {
localInstance = Instance;
if (localInstance == null) {
Instance = localInstance = new CameraController();
}
}
}
return localInstance;
}
public CameraController() {
threadPool = new ThreadPoolExecutor(CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
}
public void cancelOnInitRunnable(final Runnable onInitRunnable) {
onFinishCameraInitRunnables.remove(onInitRunnable);
}
public void initCamera(final Runnable onInitRunnable) {
initCamera(onInitRunnable, false);
}
private void initCamera(final Runnable onInitRunnable, boolean withDelay) {
if (cameraInitied) {
return;
}
if (onInitRunnable != null && !onFinishCameraInitRunnables.contains(onInitRunnable)) {
onFinishCameraInitRunnables.add(onInitRunnable);
}
if (loadingCameras || cameraInitied) {
return;
}
loadingCameras = true;
threadPool.execute(() -> {
try {
if (cameraInfos == null) {
SharedPreferences preferences = MessagesController.getGlobalMainSettings();
String cache = preferences.getString("cameraCache", null);
Comparator<Size> comparator = (o1, o2) -> {
if (o1.mWidth < o2.mWidth) {
return 1;
} else if (o1.mWidth > o2.mWidth) {
return -1;
} else {
if (o1.mHeight < o2.mHeight) {
return 1;
} else if (o1.mHeight > o2.mHeight) {
return -1;
}
return 0;
}
};
ArrayList<CameraInfo> result = new ArrayList<>();
if (cache != null) {
SerializedData serializedData = new SerializedData(Base64.decode(cache, Base64.DEFAULT));
int count = serializedData.readInt32(false);
for (int a = 0; a < count; a++) {
CameraInfo cameraInfo = new CameraInfo(serializedData.readInt32(false), serializedData.readInt32(false));
int pCount = serializedData.readInt32(false);
for (int b = 0; b < pCount; b++) {
cameraInfo.previewSizes.add(new Size(serializedData.readInt32(false), serializedData.readInt32(false)));
}
pCount = serializedData.readInt32(false);
for (int b = 0; b < pCount; b++) {
cameraInfo.pictureSizes.add(new Size(serializedData.readInt32(false), serializedData.readInt32(false)));
}
result.add(cameraInfo);
Collections.sort(cameraInfo.previewSizes, comparator);
Collections.sort(cameraInfo.pictureSizes, comparator);
}
serializedData.cleanup();
} else {
int count = Camera.getNumberOfCameras();
Camera.CameraInfo info = new Camera.CameraInfo();
int bufferSize = 4;
for (int cameraId = 0; cameraId < count; cameraId++) {
Camera.getCameraInfo(cameraId, info);
CameraInfo cameraInfo = new CameraInfo(cameraId, info.facing);
if (ApplicationLoader.mainInterfacePaused && ApplicationLoader.externalInterfacePaused) {
throw new RuntimeException("APP_PAUSED");
}
Camera camera = Camera.open(cameraInfo.getCameraId());
Camera.Parameters params = camera.getParameters();
List<Camera.Size> list = params.getSupportedPreviewSizes();
for (int a = 0; a < list.size(); a++) {
Camera.Size size = list.get(a);
if (size.width == 1280 && size.height != 720) {
continue;
}
if (size.height < 2160 && size.width < 2160) {
cameraInfo.previewSizes.add(new Size(size.width, size.height));
if (BuildVars.LOGS_ENABLED) {
FileLog.d("preview size = " + size.width + " " + size.height);
}
}
}
list = params.getSupportedPictureSizes();
for (int a = 0; a < list.size(); a++) {
Camera.Size size = list.get(a);
if (size.width == 1280 && size.height != 720) {
continue;
}
if (!"samsung".equals(Build.MANUFACTURER) || !"jflteuc".equals(Build.PRODUCT) || size.width < 2048) {
cameraInfo.pictureSizes.add(new Size(size.width, size.height));
if (BuildVars.LOGS_ENABLED) {
FileLog.d("picture size = " + size.width + " " + size.height);
}
}
}
camera.release();
result.add(cameraInfo);
Collections.sort(cameraInfo.previewSizes, comparator);
Collections.sort(cameraInfo.pictureSizes, comparator);
bufferSize += 4 + 4 + 8 * (cameraInfo.previewSizes.size() + cameraInfo.pictureSizes.size());
}
SerializedData serializedData = new SerializedData(bufferSize);
serializedData.writeInt32(result.size());
for (int a = 0; a < count; a++) {
CameraInfo cameraInfo = result.get(a);
serializedData.writeInt32(cameraInfo.cameraId);
serializedData.writeInt32(cameraInfo.frontCamera);
int pCount = cameraInfo.previewSizes.size();
serializedData.writeInt32(pCount);
for (int b = 0; b < pCount; b++) {
Size size = cameraInfo.previewSizes.get(b);
serializedData.writeInt32(size.mWidth);
serializedData.writeInt32(size.mHeight);
}
pCount = cameraInfo.pictureSizes.size();
serializedData.writeInt32(pCount);
for (int b = 0; b < pCount; b++) {
Size size = cameraInfo.pictureSizes.get(b);
serializedData.writeInt32(size.mWidth);
serializedData.writeInt32(size.mHeight);
}
}
preferences.edit().putString("cameraCache", Base64.encodeToString(serializedData.toByteArray(), Base64.DEFAULT)).apply();
serializedData.cleanup();
}
cameraInfos = result;
}
AndroidUtilities.runOnUIThread(() -> {
loadingCameras = false;
cameraInitied = true;
if (!onFinishCameraInitRunnables.isEmpty()) {
for (int a = 0; a < onFinishCameraInitRunnables.size(); a++) {
onFinishCameraInitRunnables.get(a).run();
}
onFinishCameraInitRunnables.clear();
}
NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.cameraInitied);
});
} catch (Exception e) {
AndroidUtilities.runOnUIThread(() -> {
onFinishCameraInitRunnables.clear();
loadingCameras = false;
cameraInitied = false;
if (!withDelay && "APP_PAUSED".equals(e.getMessage())) {
AndroidUtilities.runOnUIThread(() -> initCamera(onInitRunnable, true), 1000);
}
});
}
});
}
public boolean isCameraInitied() {
return cameraInitied && cameraInfos != null && !cameraInfos.isEmpty();
}
public void runOnThreadPool(Runnable runnable) {
threadPool.execute(runnable);
}
public void close(final CameraSession session, final CountDownLatch countDownLatch, final Runnable beforeDestroyRunnable) {
session.destroy();
threadPool.execute(() -> {
if (beforeDestroyRunnable != null) {
beforeDestroyRunnable.run();
}
if (session.cameraInfo.camera != null) {
try {
session.cameraInfo.camera.stopPreview();
session.cameraInfo.camera.setPreviewCallbackWithBuffer(null);
} catch (Exception e) {
FileLog.e(e);
}
try {
session.cameraInfo.camera.release();
} catch (Exception e) {
FileLog.e(e);
}
session.cameraInfo.camera = null;
}
if (countDownLatch != null) {
countDownLatch.countDown();
}
});
if (countDownLatch != null) {
try {
countDownLatch.await();
} catch (Exception e) {
FileLog.e(e);
}
}
}
public ArrayList<CameraInfo> getCameras() {
return cameraInfos;
}
private static int getOrientation(byte[] jpeg) {
if (jpeg == null) {
return 0;
}
int offset = 0;
int length = 0;
while (offset + 3 < jpeg.length && (jpeg[offset++] & 0xFF) == 0xFF) {
int marker = jpeg[offset] & 0xFF;
if (marker == 0xFF) {
continue;
}
offset++;
if (marker == 0xD8 || marker == 0x01) {
continue;
}
if (marker == 0xD9 || marker == 0xDA) {
break;
}
length = pack(jpeg, offset, 2, false);
if (length < 2 || offset + length > jpeg.length) {
return 0;
}
// Break if the marker is EXIF in APP1.
if (marker == 0xE1 && length >= 8 &&
pack(jpeg, offset + 2, 4, false) == 0x45786966 &&
pack(jpeg, offset + 6, 2, false) == 0) {
offset += 8;
length -= 8;
break;
}
offset += length;
length = 0;
}
if (length > 8) {
int tag = pack(jpeg, offset, 4, false);
if (tag != 0x49492A00 && tag != 0x4D4D002A) {
return 0;
}
boolean littleEndian = (tag == 0x49492A00);
int count = pack(jpeg, offset + 4, 4, littleEndian) + 2;
if (count < 10 || count > length) {
return 0;
}
offset += count;
length -= count;
count = pack(jpeg, offset - 2, 2, littleEndian);
while (count-- > 0 && length >= 12) {
tag = pack(jpeg, offset, 2, littleEndian);
if (tag == 0x0112) {
int orientation = pack(jpeg, offset + 8, 2, littleEndian);
switch (orientation) {
case 1:
return 0;
case 3:
return 180;
case 6:
return 90;
case 8:
return 270;
}
return 0;
}
offset += 12;
length -= 12;
}
}
return 0;
}
private static int pack(byte[] bytes, int offset, int length, boolean littleEndian) {
int step = 1;
if (littleEndian) {
offset += length - 1;
step = -1;
}
int value = 0;
while (length-- > 0) {
value = (value << 8) | (bytes[offset] & 0xFF);
offset += step;
}
return value;
}
public boolean takePicture(final File path, final CameraSession session, final Runnable callback) {
if (session == null) {
return false;
}
final CameraInfo info = session.cameraInfo;
final boolean flipFront = session.isFlipFront();
Camera camera = info.camera;
try {
camera.takePicture(null, null, (data, camera1) -> {
Bitmap bitmap = null;
int size = (int) (AndroidUtilities.getPhotoSize() / AndroidUtilities.density);
String key = String.format(Locale.US, "%s@%d_%d", Utilities.MD5(path.getAbsolutePath()), size, size);
try {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeByteArray(data, 0, data.length, options);
float scaleFactor = Math.max((float) options.outWidth / AndroidUtilities.getPhotoSize(), (float) options.outHeight / AndroidUtilities.getPhotoSize());
if (scaleFactor < 1) {
scaleFactor = 1;
}
options.inJustDecodeBounds = false;
options.inSampleSize = (int) scaleFactor;
options.inPurgeable = true;
bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, options);
} catch (Throwable e) {
FileLog.e(e);
}
try {
if (info.frontCamera != 0 && flipFront) {
try {
Matrix matrix = new Matrix();
matrix.setRotate(getOrientation(data));
matrix.postScale(-1, 1);
Bitmap scaled = Bitmaps.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
if (scaled != bitmap) {
bitmap.recycle();
}
FileOutputStream outputStream = new FileOutputStream(path);
scaled.compress(Bitmap.CompressFormat.JPEG, 80, outputStream);
outputStream.flush();
outputStream.getFD().sync();
outputStream.close();
if (scaled != null) {
ImageLoader.getInstance().putImageToCache(new BitmapDrawable(scaled), key);
}
if (callback != null) {
callback.run();
}
return;
} catch (Throwable e) {
FileLog.e(e);
}
}
FileOutputStream outputStream = new FileOutputStream(path);
outputStream.write(data);
outputStream.flush();
outputStream.getFD().sync();
outputStream.close();
if (bitmap != null) {
ImageLoader.getInstance().putImageToCache(new BitmapDrawable(bitmap), key);
}
} catch (Exception e) {
FileLog.e(e);
}
if (callback != null) {
callback.run();
}
});
return true;
} catch (Exception e) {
FileLog.e(e);
}
return false;
}
public void startPreview(final CameraSession session) {
if (session == null) {
return;
}
threadPool.execute(() -> {
Camera camera = session.cameraInfo.camera;
try {
if (camera == null) {
camera = session.cameraInfo.camera = Camera.open(session.cameraInfo.cameraId);
}
camera.startPreview();
} catch (Exception e) {
session.cameraInfo.camera = null;
if (camera != null) {
camera.release();
}
FileLog.e(e);
}
});
}
public void stopPreview(final CameraSession session) {
if (session == null) {
return;
}
threadPool.execute(() -> {
Camera camera = session.cameraInfo.camera;
try {
if (camera == null) {
camera = session.cameraInfo.camera = Camera.open(session.cameraInfo.cameraId);
}
camera.stopPreview();
} catch (Exception e) {
session.cameraInfo.camera = null;
if (camera != null) {
camera.release();
}
FileLog.e(e);
}
});
}
public void openRound(final CameraSession session, final SurfaceTexture texture, final Runnable callback, final Runnable configureCallback) {
if (session == null || texture == null) {
if (BuildVars.LOGS_ENABLED) {
FileLog.d("failed to open round " + session + " tex = " + texture);
}
return;
}
threadPool.execute(() -> {
Camera camera = session.cameraInfo.camera;
try {
if (BuildVars.LOGS_ENABLED) {
FileLog.d("start creating round camera session");
}
if (camera == null) {
camera = session.cameraInfo.camera = Camera.open(session.cameraInfo.cameraId);
}
Camera.Parameters params = camera.getParameters();
session.configureRoundCamera();
if (configureCallback != null) {
configureCallback.run();
}
camera.setPreviewTexture(texture);
camera.startPreview();
if (callback != null) {
AndroidUtilities.runOnUIThread(callback);
}
if (BuildVars.LOGS_ENABLED) {
FileLog.d("round camera session created");
}
} catch (Exception e) {
session.cameraInfo.camera = null;
if (camera != null) {
camera.release();
}
FileLog.e(e);
}
});
}
public void open(final CameraSession session, final SurfaceTexture texture, final Runnable callback, final Runnable prestartCallback) {
if (session == null || texture == null) {
return;
}
threadPool.execute(() -> {
Camera camera = session.cameraInfo.camera;
try {
if (camera == null) {
camera = session.cameraInfo.camera = Camera.open(session.cameraInfo.cameraId);
}
Camera.Parameters params = camera.getParameters();
List<String> rawFlashModes = params.getSupportedFlashModes();
availableFlashModes.clear();
if (rawFlashModes != null) {
for (int a = 0; a < rawFlashModes.size(); a++) {
String rawFlashMode = rawFlashModes.get(a);
if (rawFlashMode.equals(Camera.Parameters.FLASH_MODE_OFF) || rawFlashMode.equals(Camera.Parameters.FLASH_MODE_ON) || rawFlashMode.equals(Camera.Parameters.FLASH_MODE_AUTO)) {
availableFlashModes.add(rawFlashMode);
}
}
session.checkFlashMode(availableFlashModes.get(0));
}
if (prestartCallback != null) {
prestartCallback.run();
}
session.configurePhotoCamera();
camera.setPreviewTexture(texture);
camera.startPreview();
if (callback != null) {
AndroidUtilities.runOnUIThread(callback);
}
} catch (Exception e) {
session.cameraInfo.camera = null;
if (camera != null) {
camera.release();
}
FileLog.e(e);
}
});
}
public void recordVideo(final CameraSession session, final File path, final VideoTakeCallback callback, final Runnable onVideoStartRecord) {
if (session == null) {
return;
}
final CameraInfo info = session.cameraInfo;
final Camera camera = info.camera;
threadPool.execute(() -> {
try {
if (camera != null) {
try {
Camera.Parameters params = camera.getParameters();
params.setFlashMode(session.getCurrentFlashMode().equals(Camera.Parameters.FLASH_MODE_ON) ? Camera.Parameters.FLASH_MODE_TORCH : Camera.Parameters.FLASH_MODE_OFF);
camera.setParameters(params);
} catch (Exception e) {
FileLog.e(e);
}
camera.unlock();
//camera.stopPreview();
try {
recorder = new MediaRecorder();
recorder.setCamera(camera);
recorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
recorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
session.configureRecorder(1, recorder);
recorder.setOutputFile(path.getAbsolutePath());
recorder.setMaxFileSize(1024 * 1024 * 1024);
recorder.setVideoFrameRate(30);
recorder.setMaxDuration(0);
Size pictureSize;
pictureSize = new Size(16, 9);
pictureSize = CameraController.chooseOptimalSize(info.getPictureSizes(), 720, 480, pictureSize);
int bitrate;
if (Math.min(pictureSize.mHeight,pictureSize.mWidth) >= 720) {
bitrate = 3500000;
} else {
bitrate = 1800000;
}
recorder.setVideoEncodingBitRate(bitrate);
recorder.setVideoSize(pictureSize.getWidth(), pictureSize.getHeight());
recorder.setOnInfoListener(CameraController.this);
recorder.prepare();
recorder.start();
onVideoTakeCallback = callback;
recordedFile = path.getAbsolutePath();
if (onVideoStartRecord != null) {
AndroidUtilities.runOnUIThread(onVideoStartRecord);
}
} catch (Exception e) {
recorder.release();
recorder = null;
FileLog.e(e);
}
}
} catch (Exception e) {
FileLog.e(e);
}
});
}
private void finishRecordingVideo() {
MediaMetadataRetriever mediaMetadataRetriever = null;
long duration = 0;
try {
mediaMetadataRetriever = new MediaMetadataRetriever();
mediaMetadataRetriever.setDataSource(recordedFile);
String d = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
if (d != null) {
duration = (int) Math.ceil(Long.parseLong(d) / 1000.0f);
}
} catch (Exception e) {
FileLog.e(e);
} finally {
try {
if (mediaMetadataRetriever != null) {
mediaMetadataRetriever.release();
}
} catch (Exception e) {
FileLog.e(e);
}
}
final Bitmap bitmap = SendMessagesHelper.createVideoThumbnail(recordedFile, MediaStore.Video.Thumbnails.MINI_KIND);
String fileName = Integer.MIN_VALUE + "_" + SharedConfig.getLastLocalId() + ".jpg";
final File cacheFile = new File(FileLoader.getDirectory(FileLoader.MEDIA_DIR_CACHE), fileName);
try {
FileOutputStream stream = new FileOutputStream(cacheFile);
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, stream);
} catch (Throwable e) {
FileLog.e(e);
}
SharedConfig.saveConfig();
final long durationFinal = duration;
AndroidUtilities.runOnUIThread(() -> {
if (onVideoTakeCallback != null) {
String path = cacheFile.getAbsolutePath();
if (bitmap != null) {
ImageLoader.getInstance().putImageToCache(new BitmapDrawable(bitmap), Utilities.MD5(path));
}
onVideoTakeCallback.onFinishVideoRecording(path, durationFinal);
onVideoTakeCallback = null;
}
});
}
@Override
public void onInfo(MediaRecorder mediaRecorder, int what, int extra) {
if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED || what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED || what == MediaRecorder.MEDIA_RECORDER_INFO_UNKNOWN) {
MediaRecorder tempRecorder = recorder;
recorder = null;
if (tempRecorder != null) {
tempRecorder.stop();
tempRecorder.release();
}
if (onVideoTakeCallback != null) {
finishRecordingVideo();
}
}
}
public void stopVideoRecording(final CameraSession session, final boolean abandon) {
threadPool.execute(() -> {
try {
CameraInfo info = session.cameraInfo;
final Camera camera = info.camera;
if (camera != null && recorder != null) {
MediaRecorder tempRecorder = recorder;
recorder = null;
try {
tempRecorder.stop();
} catch (Exception e) {
FileLog.e(e);
}
try {
tempRecorder.release();
} catch (Exception e) {
FileLog.e(e);
}
try {
camera.reconnect();
camera.startPreview();
} catch (Exception e) {
FileLog.e(e);
}
try {
session.stopVideoRecording();
} catch (Exception e) {
FileLog.e(e);
}
}
try {
Camera.Parameters params = camera.getParameters();
params.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);
camera.setParameters(params);
} catch (Exception e) {
FileLog.e(e);
}
threadPool.execute(() -> {
try {
Camera.Parameters params = camera.getParameters();
params.setFlashMode(session.getCurrentFlashMode());
camera.setParameters(params);
} catch (Exception e) {
FileLog.e(e);
}
});
if (!abandon && onVideoTakeCallback != null) {
finishRecordingVideo();
} else {
onVideoTakeCallback = null;
}
} catch (Exception ignore) {
}
});
}
public static Size chooseOptimalSize(List<Size> choices, int width, int height, Size aspectRatio) {
List<Size> bigEnough = new ArrayList<>();
int w = aspectRatio.getWidth();
int h = aspectRatio.getHeight();
for (int a = 0; a < choices.size(); a++) {
Size option = choices.get(a);
if (option.getHeight() == option.getWidth() * h / w && option.getWidth() >= width && option.getHeight() >= height) {
bigEnough.add(option);
}
}
if (bigEnough.size() > 0) {
return Collections.min(bigEnough, new CompareSizesByArea());
} else {
return Collections.max(choices, new CompareSizesByArea());
}
}
static class CompareSizesByArea implements Comparator<Size> {
@Override
public int compare(Size lhs, Size rhs) {
return Long.signum((long) lhs.getWidth() * lhs.getHeight() - (long) rhs.getWidth() * rhs.getHeight());
}
}
}