/* * 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 availableFlashModes = new ArrayList<>(); private MediaRecorder recorder; private String recordedFile; protected volatile ArrayList cameraInfos; private VideoTakeCallback onVideoTakeCallback; private boolean cameraInitied; private boolean loadingCameras; private ArrayList 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 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 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 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 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 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 choices, int width, int height, Size aspectRatio) { List 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 { @Override public int compare(Size lhs, Size rhs) { return Long.signum((long) lhs.getWidth() * lhs.getHeight() - (long) rhs.getWidth() * rhs.getHeight()); } } }