/* * 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.ui.Components; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.Activity; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothProfile; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.ImageFormat; import android.graphics.Outline; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.RectF; import android.graphics.SurfaceTexture; import android.graphics.drawable.AnimatedVectorDrawable; import android.hardware.Camera; import android.media.AudioFormat; import android.media.AudioManager; import android.media.AudioRecord; import android.media.MediaCodec; import android.media.MediaCodecInfo; import android.media.MediaFormat; import android.media.MediaRecorder; import android.net.Uri; import android.opengl.EGL14; import android.opengl.EGLExt; import android.opengl.GLES11Ext; import android.opengl.GLES20; import android.opengl.GLUtils; import android.os.Build; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.view.Gravity; import android.view.HapticFeedbackConstants; import android.view.MotionEvent; import android.view.Surface; import android.view.TextureView; import android.view.View; import android.view.ViewGroup; import android.view.ViewOutlineProvider; import android.view.WindowManager; import android.view.animation.DecelerateInterpolator; import android.widget.FrameLayout; import android.widget.ImageView; import androidx.core.content.ContextCompat; import androidx.core.graphics.ColorUtils; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.util.Log; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.ApplicationLoader; import org.telegram.messenger.BuildVars; import org.telegram.messenger.DispatchQueue; import org.telegram.messenger.FileLoader; import org.telegram.messenger.FileLog; import org.telegram.messenger.ImageReceiver; import org.telegram.messenger.LocaleController; import org.telegram.messenger.MediaController; import org.telegram.messenger.MessagesController; import org.telegram.messenger.NotificationCenter; import org.telegram.messenger.R; import org.telegram.messenger.SharedConfig; import org.telegram.messenger.UserConfig; import org.telegram.messenger.Utilities; import org.telegram.messenger.VideoEditedInfo; import org.telegram.messenger.camera.CameraController; import org.telegram.messenger.camera.CameraInfo; import org.telegram.messenger.camera.CameraSession; import org.telegram.messenger.camera.Size; import org.telegram.messenger.video.MP4Builder; import org.telegram.messenger.video.Mp4Movie; import org.telegram.tgnet.ConnectionsManager; import org.telegram.tgnet.TLRPC; import org.telegram.ui.ActionBar.Theme; import org.telegram.ui.ChatActivity; import org.telegram.ui.Components.voip.CellFlickerDrawable; import java.io.File; import java.io.FileOutputStream; import java.lang.ref.WeakReference; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.FloatBuffer; import java.util.ArrayList; import java.util.Collections; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.CountDownLatch; import javax.microedition.khronos.egl.EGL10; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.egl.EGLContext; import javax.microedition.khronos.egl.EGLDisplay; import javax.microedition.khronos.egl.EGLSurface; import javax.microedition.khronos.opengles.GL; @TargetApi(18) public class InstantCameraView extends FrameLayout implements NotificationCenter.NotificationCenterDelegate { private int currentAccount = UserConfig.selectedAccount; private InstantViewCameraContainer cameraContainer; private ChatActivity baseFragment; private Paint paint; private RectF rect; private ImageView switchCameraButton; AnimatedVectorDrawable switchCameraDrawable = null; private ImageView muteImageView; private float progress; private CameraInfo selectedCamera; private boolean isFrontface = true; private volatile boolean cameraReady; private AnimatorSet muteAnimation; private TLRPC.InputFile file; private TLRPC.InputEncryptedFile encryptedFile; private byte[] key; private byte[] iv; private long size; private boolean isSecretChat; private VideoEditedInfo videoEditedInfo; private VideoPlayer videoPlayer; private Bitmap lastBitmap; private int recordingGuid; private int[] position = new int[2]; private int[] cameraTexture = new int[1]; private int[] oldCameraTexture = new int[1]; private float cameraTextureAlpha = 1.0f; private AnimatorSet animatorSet; private boolean deviceHasGoodCamera; private boolean requestingPermissions; private File cameraFile; private long recordStartTime; private boolean recording; private long recordedTime; private boolean cancelled; private CameraGLThread cameraThread; private Size previewSize; private Size pictureSize; private Size aspectRatio = SharedConfig.roundCamera16to9 ? new Size(16, 9) : new Size(4, 3); private TextureView textureView; private BackupImageView textureOverlayView; private CameraSession cameraSession; private boolean needDrawFlickerStub; private float panTranslationY; private float animationTranslationY; private float[] mMVPMatrix = new float[16]; private float[] mSTMatrix = new float[16]; private float[] moldSTMatrix = new float[16]; private static final String VERTEX_SHADER = "uniform mat4 uMVPMatrix;\n" + "uniform mat4 uSTMatrix;\n" + "attribute vec4 aPosition;\n" + "attribute vec4 aTextureCoord;\n" + "varying vec2 vTextureCoord;\n" + "void main() {\n" + " gl_Position = uMVPMatrix * aPosition;\n" + " vTextureCoord = (uSTMatrix * aTextureCoord).xy;\n" + "}\n"; private static final String FRAGMENT_SCREEN_SHADER = "#extension GL_OES_EGL_image_external : require\n" + "precision lowp float;\n" + "varying vec2 vTextureCoord;\n" + "uniform samplerExternalOES sTexture;\n" + "void main() {\n" + " gl_FragColor = texture2D(sTexture, vTextureCoord);\n" + "}\n"; private FloatBuffer vertexBuffer; private FloatBuffer textureBuffer; private FloatBuffer oldTextureTextureBuffer; private float scaleX; private float scaleY; private Size oldTexturePreviewSize; private boolean flipAnimationInProgress; private View parentView; public boolean opened; private BlurBehindDrawable blurBehindDrawable; float pinchStartDistance; float pinchScale; boolean isInPinchToZoomTouchMode; boolean maybePinchToZoomTouchMode; private int pointerId1, pointerId2; private int textureViewSize; private boolean isMessageTransition; private boolean updateTextureViewSize; private final Theme.ResourcesProvider resourcesProvider; private final static int audioSampleRate = 48000; @SuppressLint("ClickableViewAccessibility") public InstantCameraView(Context context, ChatActivity parentFragment, Theme.ResourcesProvider resourcesProvider) { super(context); this.resourcesProvider = resourcesProvider; parentView = parentFragment.getFragmentView(); setWillNotDraw(false); baseFragment = parentFragment; recordingGuid = baseFragment.getClassGuid(); isSecretChat = baseFragment.getCurrentEncryptedChat() != null; paint = new Paint(Paint.ANTI_ALIAS_FLAG) { @Override public void setAlpha(int a) { super.setAlpha(a); invalidate(); } }; paint.setStyle(Paint.Style.STROKE); paint.setStrokeCap(Paint.Cap.ROUND); paint.setStrokeWidth(AndroidUtilities.dp(3)); paint.setColor(0xffffffff); rect = new RectF(); if (Build.VERSION.SDK_INT >= 21) { cameraContainer = new InstantViewCameraContainer(context) { @Override public void setScaleX(float scaleX) { super.setScaleX(scaleX); InstantCameraView.this.invalidate(); } @Override public void setAlpha(float alpha) { super.setAlpha(alpha); InstantCameraView.this.invalidate(); } }; cameraContainer.setOutlineProvider(new ViewOutlineProvider() { @TargetApi(Build.VERSION_CODES.LOLLIPOP) @Override public void getOutline(View view, Outline outline) { outline.setOval(0, 0, textureViewSize, textureViewSize); } }); cameraContainer.setClipToOutline(true); cameraContainer.setWillNotDraw(false); } else { final Path path = new Path(); final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); paint.setColor(0xff000000); paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); cameraContainer = new InstantViewCameraContainer(context) { @Override public void setScaleX(float scaleX) { super.setScaleX(scaleX); InstantCameraView.this.invalidate(); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); path.reset(); path.addCircle(w / 2, h / 2, w / 2, Path.Direction.CW); path.toggleInverseFillType(); } @Override protected void dispatchDraw(Canvas canvas) { try { super.dispatchDraw(canvas); canvas.drawPath(path, paint); } catch (Exception ignore) { } } }; cameraContainer.setWillNotDraw(false); cameraContainer.setLayerType(View.LAYER_TYPE_HARDWARE, null); } addView(cameraContainer, new LayoutParams(AndroidUtilities.roundPlayingMessageSize, AndroidUtilities.roundPlayingMessageSize, Gravity.CENTER)); switchCameraButton = new ImageView(context); switchCameraButton.setScaleType(ImageView.ScaleType.CENTER); switchCameraButton.setContentDescription(LocaleController.getString("AccDescrSwitchCamera", R.string.AccDescrSwitchCamera)); addView(switchCameraButton, LayoutHelper.createFrame(62, 62, Gravity.LEFT | Gravity.BOTTOM, 8, 0, 0, 0)); switchCameraButton.setOnClickListener(v -> { if (!cameraReady || cameraSession == null || !cameraSession.isInitied() || cameraThread == null) { return; } switchCamera(); if (switchCameraDrawable != null) { switchCameraDrawable.start(); } flipAnimationInProgress = true; ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1f); valueAnimator.setDuration(300); valueAnimator.setInterpolator(CubicBezierInterpolator.DEFAULT); valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { float p = (float) valueAnimator.getAnimatedValue(); if (p < 0.5f) { p = (1f - p / 0.5f); } else { p = (p - 0.5f) / 0.5f; } float scaleDown = 0.9f + 0.1f * p; cameraContainer.setScaleX(p * scaleDown); cameraContainer.setScaleY(scaleDown); textureOverlayView.setScaleX(p * scaleDown); textureOverlayView.setScaleY(scaleDown); } }); valueAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); cameraContainer.setScaleX(1f); cameraContainer.setScaleY(1f); textureOverlayView.setScaleY(1f); textureOverlayView.setScaleX(1f); flipAnimationInProgress = false; invalidate(); } }); valueAnimator.start(); }); muteImageView = new ImageView(context); muteImageView.setScaleType(ImageView.ScaleType.CENTER); muteImageView.setImageResource(R.drawable.video_mute); muteImageView.setAlpha(0.0f); addView(muteImageView, LayoutHelper.createFrame(48, 48, Gravity.CENTER)); Paint blackoutPaint = new Paint(Paint.ANTI_ALIAS_FLAG); blackoutPaint.setColor(ColorUtils.setAlphaComponent(Color.BLACK, 40)); textureOverlayView = new BackupImageView(getContext()) { CellFlickerDrawable flickerDrawable = new CellFlickerDrawable(); @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (needDrawFlickerStub) { flickerDrawable.setParentWidth(textureViewSize); AndroidUtilities.rectTmp.set(0, 0, textureViewSize, textureViewSize); float rad = AndroidUtilities.rectTmp.width() / 2f; canvas.drawRoundRect(AndroidUtilities.rectTmp, rad, rad, blackoutPaint); AndroidUtilities.rectTmp.inset(AndroidUtilities.dp(1), AndroidUtilities.dp(1)); flickerDrawable.draw(canvas, AndroidUtilities.rectTmp, rad, null); invalidate(); } } }; addView(textureOverlayView, new LayoutParams(AndroidUtilities.roundPlayingMessageSize, AndroidUtilities.roundPlayingMessageSize, Gravity.CENTER)); setVisibility(INVISIBLE); blurBehindDrawable = new BlurBehindDrawable(parentView, this, 0, resourcesProvider); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (updateTextureViewSize) { int newSize; if (MeasureSpec.getSize(heightMeasureSpec) > MeasureSpec.getSize(widthMeasureSpec) * 1.3f) { newSize = AndroidUtilities.roundPlayingMessageSize; } else { newSize = AndroidUtilities.roundMessageSize; } if (newSize != textureViewSize) { textureViewSize = newSize; textureOverlayView.getLayoutParams().width = textureOverlayView.getLayoutParams().height = textureViewSize; cameraContainer.getLayoutParams().width = cameraContainer.getLayoutParams().height = textureViewSize; ((LayoutParams) muteImageView.getLayoutParams()).topMargin = textureViewSize / 2 - AndroidUtilities.dp(24); textureOverlayView.setRoundRadius(textureViewSize / 2); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { cameraContainer.invalidateOutline(); } } updateTextureViewSize = false; } super.onMeasure(widthMeasureSpec, heightMeasureSpec); } private boolean checkPointerIds(MotionEvent ev) { if (ev.getPointerCount() < 2) { return false; } if (pointerId1 == ev.getPointerId(0) && pointerId2 == ev.getPointerId(1)) { return true; } if (pointerId1 == ev.getPointerId(1) && pointerId2 == ev.getPointerId(0)) { return true; } return false; } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { getParent().requestDisallowInterceptTouchEvent(true); return super.onInterceptTouchEvent(ev); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); if (getVisibility() != VISIBLE) { animationTranslationY = getMeasuredHeight() / 2; updateTranslationY(); } blurBehindDrawable.checkSizes(); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); NotificationCenter.getInstance(currentAccount).addObserver(this, NotificationCenter.fileUploaded); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); NotificationCenter.getInstance(currentAccount).removeObserver(this, NotificationCenter.fileUploaded); } @Override public void didReceivedNotification(int id, int account, Object... args) { if (id == NotificationCenter.fileUploaded) { final String location = (String) args[0]; if (cameraFile != null && cameraFile.getAbsolutePath().equals(location)) { file = (TLRPC.InputFile) args[1]; encryptedFile = (TLRPC.InputEncryptedFile) args[2]; size = (Long) args[5]; if (encryptedFile != null) { key = (byte[]) args[3]; iv = (byte[]) args[4]; } } } } public void destroy(boolean async, final Runnable beforeDestroyRunnable) { if (cameraSession != null) { cameraSession.destroy(); CameraController.getInstance().close(cameraSession, !async ? new CountDownLatch(1) : null, beforeDestroyRunnable); } } @Override protected void onDraw(Canvas canvas) { blurBehindDrawable.draw(canvas); float x = cameraContainer.getX(); float y = cameraContainer.getY(); rect.set(x - AndroidUtilities.dp(8), y - AndroidUtilities.dp(8), x + cameraContainer.getMeasuredWidth() + AndroidUtilities.dp(8), y + cameraContainer.getMeasuredHeight() + AndroidUtilities.dp(8)); if (recording) { recordedTime = System.currentTimeMillis() - recordStartTime; progress = Math.min(1f, recordedTime / 60000.0f); invalidate(); } if (progress != 0) { canvas.save(); if (!flipAnimationInProgress) { canvas.scale(cameraContainer.getScaleX(), cameraContainer.getScaleY(), rect.centerX(), rect.centerY()); } canvas.drawArc(rect, -90, 360 * progress, false, paint); canvas.restore(); } if (Theme.chat_roundVideoShadow != null) { int x1 = (int) x - AndroidUtilities.dp(3); int y1 = (int) y - AndroidUtilities.dp(2); canvas.save(); if (isMessageTransition) { canvas.scale(cameraContainer.getScaleX(), cameraContainer.getScaleY(), x, y); } else { canvas.scale(cameraContainer.getScaleX(), cameraContainer.getScaleY(), x + textureViewSize / 2f, y + textureViewSize / 2f); } Theme.chat_roundVideoShadow.setAlpha((int) (cameraContainer.getAlpha() * 255)); Theme.chat_roundVideoShadow.setBounds(x1, y1, x1 + textureViewSize + AndroidUtilities.dp(6), y1 + textureViewSize + AndroidUtilities.dp(6)); Theme.chat_roundVideoShadow.draw(canvas); canvas.restore(); } } @Override public void setVisibility(int visibility) { super.setVisibility(visibility); if (visibility != View.VISIBLE && blurBehindDrawable != null) { blurBehindDrawable.clear(); } switchCameraButton.setAlpha(0.0f); cameraContainer.setAlpha(0.0f); textureOverlayView.setAlpha(0.0f); muteImageView.setAlpha(0.0f); muteImageView.setScaleX(1.0f); muteImageView.setScaleY(1.0f); cameraContainer.setScaleX(0.1f); cameraContainer.setScaleY(0.1f); textureOverlayView.setScaleX(0.1f); textureOverlayView.setScaleY(0.1f); if (cameraContainer.getMeasuredWidth() != 0) { cameraContainer.setPivotX(cameraContainer.getMeasuredWidth() / 2); cameraContainer.setPivotY(cameraContainer.getMeasuredHeight() / 2); textureOverlayView.setPivotX(textureOverlayView.getMeasuredWidth() / 2); textureOverlayView.setPivotY(textureOverlayView.getMeasuredHeight() / 2); } try { if (visibility == VISIBLE) { ((Activity) getContext()).getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } else { ((Activity) getContext()).getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } } catch (Exception e) { FileLog.e(e); } } public void showCamera() { if (textureView != null) { return; } if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { switchCameraDrawable = (AnimatedVectorDrawable) ContextCompat.getDrawable(getContext(), R.drawable.avd_flip); switchCameraButton.setImageDrawable(switchCameraDrawable); } else { switchCameraButton.setImageResource(R.drawable.vd_flip); } textureOverlayView.setAlpha(1.0f); textureOverlayView.invalidate(); if (lastBitmap == null) { try { File file = new File(ApplicationLoader.getFilesDirFixed(), "icthumb.jpg"); lastBitmap = BitmapFactory.decodeFile(file.getAbsolutePath()); } catch (Throwable ignore) { } } if (lastBitmap != null) { textureOverlayView.setImageBitmap(lastBitmap); } else { textureOverlayView.setImageResource(R.drawable.icplaceholder); } cameraReady = false; isFrontface = true; selectedCamera = null; recordedTime = 0; progress = 0; cancelled = false; file = null; encryptedFile = null; key = null; iv = null; needDrawFlickerStub = true; if (!initCamera()) { return; } MediaController.getInstance().pauseMessage(MediaController.getInstance().getPlayingMessageObject()); cameraFile = new File(FileLoader.getDirectory(FileLoader.MEDIA_DIR_CACHE), SharedConfig.getLastLocalId() + ".mp4"); SharedConfig.saveConfig(); if (BuildVars.LOGS_ENABLED) { FileLog.d("show round camera"); } textureView = new TextureView(getContext()); textureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() { @Override public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { if (BuildVars.LOGS_ENABLED) { FileLog.d("camera surface available"); } if (cameraThread == null && surface != null) { if (cancelled) { return; } if (BuildVars.LOGS_ENABLED) { FileLog.d("start create thread"); } cameraThread = new CameraGLThread(surface, width, height); } } @Override public void onSurfaceTextureSizeChanged(SurfaceTexture surface, final int width, final int height) { if (cameraThread != null) { cameraThread.surfaceWidth = width; cameraThread.surfaceHeight = height; cameraThread.updateScale(); } } @Override public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { if (cameraThread != null) { cameraThread.shutdown(0); cameraThread = null; } if (cameraSession != null) { CameraController.getInstance().close(cameraSession, null, null); } return true; } @Override public void onSurfaceTextureUpdated(SurfaceTexture surface) { } }); cameraContainer.addView(textureView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); updateTextureViewSize = true; setVisibility(VISIBLE); startAnimation(true); MediaController.getInstance().requestAudioFocus(true); } public InstantViewCameraContainer getCameraContainer() { return cameraContainer; } public void startAnimation(boolean open) { if (animatorSet != null) { animatorSet.removeAllListeners(); animatorSet.cancel(); } PipRoundVideoView pipRoundVideoView = PipRoundVideoView.getInstance(); if (pipRoundVideoView != null) { pipRoundVideoView.showTemporary(!open); } if (open && !opened) { cameraContainer.setTranslationX(0); textureOverlayView.setTranslationX(0); animationTranslationY = getMeasuredHeight() / 2f; updateTranslationY(); } opened = open; if (parentView != null) { parentView.invalidate(); } blurBehindDrawable.show(open); animatorSet = new AnimatorSet(); float toX = 0; if (!open) { toX = recordedTime > 300 ? AndroidUtilities.dp(24) - getMeasuredWidth() / 2f : 0; } ValueAnimator translationYAnimator = ValueAnimator.ofFloat(open ? 1f : 0f, open ? 0 : 1f); translationYAnimator.addUpdateListener(animation -> { animationTranslationY = (getMeasuredHeight() / 2f) * (float) animation.getAnimatedValue(); updateTranslationY(); }); animatorSet.playTogether( ObjectAnimator.ofFloat(switchCameraButton, View.ALPHA, open ? 1.0f : 0.0f), ObjectAnimator.ofFloat(muteImageView, View.ALPHA, 0.0f), ObjectAnimator.ofInt(paint, AnimationProperties.PAINT_ALPHA, open ? 255 : 0), ObjectAnimator.ofFloat(cameraContainer, View.ALPHA, open ? 1.0f : 0.0f), ObjectAnimator.ofFloat(cameraContainer, View.SCALE_X, open ? 1.0f : 0.1f), ObjectAnimator.ofFloat(cameraContainer, View.SCALE_Y, open ? 1.0f : 0.1f), ObjectAnimator.ofFloat(cameraContainer, View.TRANSLATION_X, toX), ObjectAnimator.ofFloat(textureOverlayView, View.ALPHA, open ? 1.0f : 0.0f), ObjectAnimator.ofFloat(textureOverlayView, View.SCALE_X, open ? 1.0f : 0.1f), ObjectAnimator.ofFloat(textureOverlayView, View.SCALE_Y, open ? 1.0f : 0.1f), ObjectAnimator.ofFloat(textureOverlayView, View.TRANSLATION_X, toX), translationYAnimator ); if (!open) { animatorSet.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { if (animation.equals(animatorSet)) { hideCamera(true); setVisibility(INVISIBLE); } } }); } else { setTranslationX(0); } animatorSet.setDuration(180); animatorSet.setInterpolator(new DecelerateInterpolator()); animatorSet.start(); } private void updateTranslationY() { textureOverlayView.setTranslationY(animationTranslationY + panTranslationY); cameraContainer.setTranslationY(animationTranslationY + panTranslationY); } public Rect getCameraRect() { cameraContainer.getLocationOnScreen(position); return new Rect(position[0], position[1], cameraContainer.getWidth(), cameraContainer.getHeight()); } public void changeVideoPreviewState(int state, float progress) { if (videoPlayer == null) { return; } if (state == 0) { startProgressTimer(); videoPlayer.play(); } else if (state == 1) { stopProgressTimer(); videoPlayer.pause(); } else if (state == 2) { videoPlayer.seekTo((long) (progress * videoPlayer.getDuration())); } } public void send(int state, boolean notify, int scheduleDate) { if (textureView == null) { return; } stopProgressTimer(); if (videoPlayer != null) { videoPlayer.releasePlayer(true); videoPlayer = null; } if (state == 4) { if (videoEditedInfo.needConvert()) { file = null; encryptedFile = null; key = null; iv = null; double totalDuration = videoEditedInfo.estimatedDuration; long startTime = videoEditedInfo.startTime >= 0 ? videoEditedInfo.startTime : 0; long endTime = videoEditedInfo.endTime >= 0 ? videoEditedInfo.endTime : videoEditedInfo.estimatedDuration; videoEditedInfo.estimatedDuration = endTime - startTime; videoEditedInfo.estimatedSize = Math.max(1, (long) (size * (videoEditedInfo.estimatedDuration / totalDuration))); videoEditedInfo.bitrate = 1000000; if (videoEditedInfo.startTime > 0) { videoEditedInfo.startTime *= 1000; } if (videoEditedInfo.endTime > 0) { videoEditedInfo.endTime *= 1000; } FileLoader.getInstance(currentAccount).cancelFileUpload(cameraFile.getAbsolutePath(), false); } else { videoEditedInfo.estimatedSize = Math.max(1, size); } videoEditedInfo.file = file; videoEditedInfo.encryptedFile = encryptedFile; videoEditedInfo.key = key; videoEditedInfo.iv = iv; baseFragment.sendMedia(new MediaController.PhotoEntry(0, 0, 0, cameraFile.getAbsolutePath(), 0, true, 0, 0, 0), videoEditedInfo, notify, scheduleDate, false); if (scheduleDate != 0) { startAnimation(false); } MediaController.getInstance().requestAudioFocus(false); } else { cancelled = recordedTime < 800; recording = false; int reason; if (cancelled) { reason = 4; } else { reason = state == 3 ? 2 : 5; } if (cameraThread != null) { NotificationCenter.getInstance(currentAccount).postNotificationName(NotificationCenter.recordStopped, recordingGuid, reason); int send; if (cancelled) { send = 0; } else if (state == 3) { send = 2; } else { send = 1; } saveLastCameraBitmap(); cameraThread.shutdown(send); cameraThread = null; } if (cancelled) { NotificationCenter.getInstance(currentAccount).postNotificationName(NotificationCenter.audioRecordTooShort, recordingGuid, true, (int) recordedTime); startAnimation(false); MediaController.getInstance().requestAudioFocus(false); } } } private void saveLastCameraBitmap() { Bitmap bitmap = textureView.getBitmap(); if (bitmap != null && bitmap.getPixel(0, 0) != 0) { lastBitmap = Bitmap.createScaledBitmap(textureView.getBitmap(), 50, 50, true); if (lastBitmap != null) { Utilities.blurBitmap(lastBitmap, 7, 1, lastBitmap.getWidth(), lastBitmap.getHeight(), lastBitmap.getRowBytes()); try { File file = new File(ApplicationLoader.getFilesDirFixed(), "icthumb.jpg"); FileOutputStream stream = new FileOutputStream(file); lastBitmap.compress(Bitmap.CompressFormat.JPEG, 87, stream); stream.close(); } catch (Throwable ignore) { } } } } public void cancel(boolean byGesture) { stopProgressTimer(); if (videoPlayer != null) { videoPlayer.releasePlayer(true); videoPlayer = null; } if (textureView == null) { return; } cancelled = true; recording = false; NotificationCenter.getInstance(currentAccount).postNotificationName(NotificationCenter.recordStopped, recordingGuid, byGesture ? 0 : 6); if (cameraThread != null) { saveLastCameraBitmap(); cameraThread.shutdown(0); cameraThread = null; } if (cameraFile != null) { cameraFile.delete(); cameraFile = null; } MediaController.getInstance().requestAudioFocus(false); startAnimation(false); blurBehindDrawable.show(false); invalidate(); } public View getSwitchButtonView() { return switchCameraButton; } public View getMuteImageView() { return muteImageView; } public Paint getPaint() { return paint; } public void hideCamera(boolean async) { destroy(async, null); cameraContainer.setTranslationX(0); textureOverlayView.setTranslationX(0); animationTranslationY = 0; updateTranslationY(); MediaController.getInstance().playMessage(MediaController.getInstance().getPlayingMessageObject()); if (textureView != null) { ViewGroup parent = (ViewGroup) textureView.getParent(); if (parent != null) { parent.removeView(textureView); } } textureView = null; cameraContainer.setImageReceiver(null); } private void switchCamera() { saveLastCameraBitmap(); if (lastBitmap != null) { needDrawFlickerStub = false; textureOverlayView.setImageBitmap(lastBitmap); textureOverlayView.setAlpha(1f); } if (cameraSession != null) { cameraSession.destroy(); CameraController.getInstance().close(cameraSession, null, null); cameraSession = null; } isFrontface = !isFrontface; initCamera(); cameraReady = false; cameraThread.reinitForNewCamera(); } private boolean initCamera() { ArrayList cameraInfos = CameraController.getInstance().getCameras(); if (cameraInfos == null) { return false; } CameraInfo notFrontface = null; for (int a = 0; a < cameraInfos.size(); a++) { CameraInfo cameraInfo = cameraInfos.get(a); if (!cameraInfo.isFrontface()) { notFrontface = cameraInfo; } if (isFrontface && cameraInfo.isFrontface() || !isFrontface && !cameraInfo.isFrontface()) { selectedCamera = cameraInfo; break; } else { notFrontface = cameraInfo; } } if (selectedCamera == null) { selectedCamera = notFrontface; } if (selectedCamera == null) { return false; } ArrayList previewSizes = selectedCamera.getPreviewSizes(); ArrayList pictureSizes = selectedCamera.getPictureSizes(); previewSize = chooseOptimalSize(previewSizes); pictureSize = chooseOptimalSize(pictureSizes); if (previewSize.mWidth != pictureSize.mWidth) { boolean found = false; for (int a = previewSizes.size() - 1; a >= 0; a--) { Size preview = previewSizes.get(a); for (int b = pictureSizes.size() - 1; b >= 0; b--) { Size picture = pictureSizes.get(b); if (preview.mWidth >= pictureSize.mWidth && preview.mHeight >= pictureSize.mHeight && preview.mWidth == picture.mWidth && preview.mHeight == picture.mHeight) { previewSize = preview; pictureSize = picture; found = true; break; } } if (found) { break; } } if (!found) { for (int a = previewSizes.size() - 1; a >= 0; a--) { Size preview = previewSizes.get(a); for (int b = pictureSizes.size() - 1; b >= 0; b--) { Size picture = pictureSizes.get(b); if (preview.mWidth >= 360 && preview.mHeight >= 360 && preview.mWidth == picture.mWidth && preview.mHeight == picture.mHeight) { previewSize = preview; pictureSize = picture; found = true; break; } } if (found) { break; } } } } if (BuildVars.LOGS_ENABLED) { FileLog.d("preview w = " + previewSize.mWidth + " h = " + previewSize.mHeight); } return true; } private Size chooseOptimalSize(ArrayList previewSizes) { ArrayList sortedSizes = new ArrayList<>(); for (int i = 0; i < previewSizes.size(); i++) { if (Math.max(previewSizes.get(i).mHeight, previewSizes.get(i).mWidth) <= 1200 && Math.min(previewSizes.get(i).mHeight, previewSizes.get(i).mWidth) >= 320) { sortedSizes.add(previewSizes.get(i)); } } if (sortedSizes.isEmpty() || SharedConfig.getDevicePerformanceClass() == SharedConfig.PERFORMANCE_CLASS_LOW || SharedConfig.getDevicePerformanceClass() == SharedConfig.PERFORMANCE_CLASS_AVERAGE) { ArrayList sizes = sortedSizes; if (!sortedSizes.isEmpty()) { sizes = sortedSizes; } else { sizes = previewSizes; } if (Build.MANUFACTURER.equalsIgnoreCase("Xiaomi")) { return CameraController.chooseOptimalSize(sizes, 640, 480, aspectRatio); } else { return CameraController.chooseOptimalSize(sizes, 480, 270, aspectRatio); } } Collections.sort(sortedSizes, (o1, o2) -> { float a1 = Math.abs(1f - Math.min(o1.mHeight, o1.mWidth) / (float) Math.max(o1.mHeight, o1.mWidth)); float a2 = Math.abs(1f - Math.min(o2.mHeight, o2.mWidth) / (float) Math.max(o2.mHeight, o2.mWidth)); if (a1 < a2) { return -1; } else if (a1 > a2) { return 1; } return 0; }); return sortedSizes.get(0); } private void createCamera(final SurfaceTexture surfaceTexture) { AndroidUtilities.runOnUIThread(() -> { if (cameraThread == null) { return; } if (BuildVars.LOGS_ENABLED) { FileLog.d("create camera session"); } surfaceTexture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight()); cameraSession = new CameraSession(selectedCamera, previewSize, pictureSize, ImageFormat.JPEG, true); cameraThread.setCurrentSession(cameraSession); CameraController.getInstance().openRound(cameraSession, surfaceTexture, () -> { if (cameraSession != null) { boolean updateScale = false; try { Camera.Size size = cameraSession.getCurrentPreviewSize(); if (size.width != previewSize.getWidth() || size.height != previewSize.getHeight()) { previewSize = new Size(size.width, size.height); FileLog.d("change preview size to w = " + previewSize.getWidth() + " h = " + previewSize.getHeight()); } } catch (Exception e) { FileLog.e(e); } try { Camera.Size size = cameraSession.getCurrentPictureSize(); if (size.width != pictureSize.getWidth() || size.height != pictureSize.getHeight()) { pictureSize = new Size(size.width, size.height); FileLog.d("change picture size to w = " + pictureSize.getWidth() + " h = " + pictureSize.getHeight()); updateScale = true; } } catch (Exception e) { FileLog.e(e); } if (BuildVars.LOGS_ENABLED) { FileLog.d("camera initied"); } cameraSession.setInitied(); if (updateScale) { cameraThread.reinitForNewCamera(); } } }, () -> cameraThread.setCurrentSession(cameraSession)); }); } private int loadShader(int type, String shaderCode) { int shader = GLES20.glCreateShader(type); GLES20.glShaderSource(shader, shaderCode); GLES20.glCompileShader(shader); int[] compileStatus = new int[1]; GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compileStatus, 0); if (compileStatus[0] == 0) { if (BuildVars.LOGS_ENABLED) { FileLog.e(GLES20.glGetShaderInfoLog(shader)); } GLES20.glDeleteShader(shader); shader = 0; } return shader; } private Timer progressTimer; private void startProgressTimer() { if (progressTimer != null) { try { progressTimer.cancel(); progressTimer = null; } catch (Exception e) { FileLog.e(e); } } progressTimer = new Timer(); progressTimer.schedule(new TimerTask() { @Override public void run() { AndroidUtilities.runOnUIThread(() -> { try { if (videoPlayer != null && videoEditedInfo != null && videoEditedInfo.endTime > 0 && videoPlayer.getCurrentPosition() >= videoEditedInfo.endTime) { videoPlayer.seekTo(videoEditedInfo.startTime > 0 ? videoEditedInfo.startTime : 0); } } catch (Exception e) { FileLog.e(e); } }); } }, 0, 17); } private void stopProgressTimer() { if (progressTimer != null) { try { progressTimer.cancel(); progressTimer = null; } catch (Exception e) { FileLog.e(e); } } } public boolean blurFullyDrawing() { return blurBehindDrawable != null && blurBehindDrawable.isFullyDrawing() && opened; } public void invalidateBlur() { if (blurBehindDrawable != null) { blurBehindDrawable.invalidate(); } } public void cancelBlur() { blurBehindDrawable.show(false); invalidate(); } public void onPanTranslationUpdate(float y) { panTranslationY = y / 2f; updateTranslationY(); blurBehindDrawable.onPanTranslationUpdate(y); } public TextureView getTextureView() { return textureView; } public void setIsMessageTransition(boolean isMessageTransition) { this.isMessageTransition = isMessageTransition; } public class CameraGLThread extends DispatchQueue { private final static int EGL_CONTEXT_CLIENT_VERSION = 0x3098; private final static int EGL_OPENGL_ES2_BIT = 4; private SurfaceTexture surfaceTexture; private EGL10 egl10; private EGLDisplay eglDisplay; private EGLContext eglContext; private EGLSurface eglSurface; private boolean initied; private CameraSession currentSession; private SurfaceTexture cameraSurface; private final int DO_RENDER_MESSAGE = 0; private final int DO_SHUTDOWN_MESSAGE = 1; private final int DO_REINIT_MESSAGE = 2; private final int DO_SETSESSION_MESSAGE = 3; private int drawProgram; private int vertexMatrixHandle; private int textureMatrixHandle; private int positionHandle; private int textureHandle; private boolean recording; private Integer cameraId = 0; private VideoRecorder videoEncoder; private int surfaceWidth; private int surfaceHeight; public CameraGLThread(SurfaceTexture surface, int surfaceWidth, int surfaceHeight) { super("CameraGLThread"); surfaceTexture = surface; this.surfaceWidth = surfaceWidth; this.surfaceHeight = surfaceHeight; updateScale(); } private void updateScale() { int width = previewSize.getWidth(); int height = previewSize.getHeight(); float scale = surfaceWidth / (float) Math.min(width, height); width *= scale; height *= scale; if (width == height) { scaleX = 1f; scaleY = 1f; } else if (width > height) { scaleX = 1.0f; scaleY = width / (float) surfaceHeight; } else { scaleX = height / (float) surfaceWidth; scaleY = 1.0f; } FileLog.d("camera scaleX = " + scaleX + " scaleY = " + scaleY); } private boolean initGL() { if (BuildVars.LOGS_ENABLED) { FileLog.d("start init gl"); } egl10 = (EGL10) EGLContext.getEGL(); eglDisplay = egl10.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY); if (eglDisplay == EGL10.EGL_NO_DISPLAY) { if (BuildVars.LOGS_ENABLED) { FileLog.e("eglGetDisplay failed " + GLUtils.getEGLErrorString(egl10.eglGetError())); } finish(); return false; } int[] version = new int[2]; if (!egl10.eglInitialize(eglDisplay, version)) { if (BuildVars.LOGS_ENABLED) { FileLog.e("eglInitialize failed " + GLUtils.getEGLErrorString(egl10.eglGetError())); } finish(); return false; } int[] configsCount = new int[1]; EGLConfig[] configs = new EGLConfig[1]; int[] configSpec = new int[]{ EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, EGL10.EGL_RED_SIZE, 8, EGL10.EGL_GREEN_SIZE, 8, EGL10.EGL_BLUE_SIZE, 8, EGL10.EGL_ALPHA_SIZE, 0, EGL10.EGL_DEPTH_SIZE, 0, EGL10.EGL_STENCIL_SIZE, 0, EGL10.EGL_NONE }; EGLConfig eglConfig; if (!egl10.eglChooseConfig(eglDisplay, configSpec, configs, 1, configsCount)) { if (BuildVars.LOGS_ENABLED) { FileLog.e("eglChooseConfig failed " + GLUtils.getEGLErrorString(egl10.eglGetError())); } finish(); return false; } else if (configsCount[0] > 0) { eglConfig = configs[0]; } else { if (BuildVars.LOGS_ENABLED) { FileLog.e("eglConfig not initialized"); } finish(); return false; } int[] attrib_list = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE}; eglContext = egl10.eglCreateContext(eglDisplay, eglConfig, EGL10.EGL_NO_CONTEXT, attrib_list); if (eglContext == null) { if (BuildVars.LOGS_ENABLED) { FileLog.e("eglCreateContext failed " + GLUtils.getEGLErrorString(egl10.eglGetError())); } finish(); return false; } if (surfaceTexture instanceof SurfaceTexture) { eglSurface = egl10.eglCreateWindowSurface(eglDisplay, eglConfig, surfaceTexture, null); } else { finish(); return false; } if (eglSurface == null || eglSurface == EGL10.EGL_NO_SURFACE) { if (BuildVars.LOGS_ENABLED) { FileLog.e("createWindowSurface failed " + GLUtils.getEGLErrorString(egl10.eglGetError())); } finish(); return false; } if (!egl10.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext)) { if (BuildVars.LOGS_ENABLED) { FileLog.e("eglMakeCurrent failed " + GLUtils.getEGLErrorString(egl10.eglGetError())); } finish(); return false; } GL gl = eglContext.getGL(); float tX = 1.0f / scaleX / 2.0f; float tY = 1.0f / scaleY / 2.0f; float[] verticesData = { -1.0f, -1.0f, 0, 1.0f, -1.0f, 0, -1.0f, 1.0f, 0, 1.0f, 1.0f, 0 }; float[] texData = { 0.5f - tX, 0.5f - tY, 0.5f + tX, 0.5f - tY, 0.5f - tX, 0.5f + tY, 0.5f + tX, 0.5f + tY }; videoEncoder = new VideoRecorder(); vertexBuffer = ByteBuffer.allocateDirect(verticesData.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer(); vertexBuffer.put(verticesData).position(0); textureBuffer = ByteBuffer.allocateDirect(texData.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer(); textureBuffer.put(texData).position(0); android.opengl.Matrix.setIdentityM(mSTMatrix, 0); int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, VERTEX_SHADER); int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, FRAGMENT_SCREEN_SHADER); if (vertexShader != 0 && fragmentShader != 0) { drawProgram = GLES20.glCreateProgram(); GLES20.glAttachShader(drawProgram, vertexShader); GLES20.glAttachShader(drawProgram, fragmentShader); GLES20.glLinkProgram(drawProgram); int[] linkStatus = new int[1]; GLES20.glGetProgramiv(drawProgram, GLES20.GL_LINK_STATUS, linkStatus, 0); if (linkStatus[0] == 0) { if (BuildVars.LOGS_ENABLED) { FileLog.e("failed link shader"); } GLES20.glDeleteProgram(drawProgram); drawProgram = 0; } else { positionHandle = GLES20.glGetAttribLocation(drawProgram, "aPosition"); textureHandle = GLES20.glGetAttribLocation(drawProgram, "aTextureCoord"); vertexMatrixHandle = GLES20.glGetUniformLocation(drawProgram, "uMVPMatrix"); textureMatrixHandle = GLES20.glGetUniformLocation(drawProgram, "uSTMatrix"); } } else { if (BuildVars.LOGS_ENABLED) { FileLog.e("failed creating shader"); } finish(); return false; } GLES20.glGenTextures(1, cameraTexture, 0); GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, cameraTexture[0]); GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); android.opengl.Matrix.setIdentityM(mMVPMatrix, 0); cameraSurface = new SurfaceTexture(cameraTexture[0]); cameraSurface.setOnFrameAvailableListener(surfaceTexture -> requestRender()); createCamera(cameraSurface); if (BuildVars.LOGS_ENABLED) { FileLog.e("gl initied"); } return true; } public void reinitForNewCamera() { Handler handler = getHandler(); if (handler != null) { sendMessage(handler.obtainMessage(DO_REINIT_MESSAGE), 0); } updateScale(); } public void finish() { if (eglSurface != null) { egl10.eglMakeCurrent(eglDisplay, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT); egl10.eglDestroySurface(eglDisplay, eglSurface); eglSurface = null; } if (eglContext != null) { egl10.eglDestroyContext(eglDisplay, eglContext); eglContext = null; } if (eglDisplay != null) { egl10.eglTerminate(eglDisplay); eglDisplay = null; } } public void setCurrentSession(CameraSession session) { Handler handler = getHandler(); if (handler != null) { sendMessage(handler.obtainMessage(DO_SETSESSION_MESSAGE, session), 0); } } private void onDraw(Integer cameraId) { if (!initied) { return; } if (!eglContext.equals(egl10.eglGetCurrentContext()) || !eglSurface.equals(egl10.eglGetCurrentSurface(EGL10.EGL_DRAW))) { if (!egl10.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext)) { if (BuildVars.LOGS_ENABLED) { FileLog.e("eglMakeCurrent failed " + GLUtils.getEGLErrorString(egl10.eglGetError())); } return; } } cameraSurface.updateTexImage(); if (!recording) { videoEncoder.startRecording(cameraFile, EGL14.eglGetCurrentContext()); recording = true; int orientation = currentSession.getCurrentOrientation(); if (orientation == 90 || orientation == 270) { float temp = scaleX; scaleX = scaleY; scaleY = temp; } } videoEncoder.frameAvailable(cameraSurface, cameraId, System.nanoTime()); cameraSurface.getTransformMatrix(mSTMatrix); GLES20.glUseProgram(drawProgram); GLES20.glActiveTexture(GLES20.GL_TEXTURE0); GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, cameraTexture[0]); GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 12, vertexBuffer); GLES20.glEnableVertexAttribArray(positionHandle); GLES20.glVertexAttribPointer(textureHandle, 2, GLES20.GL_FLOAT, false, 8, textureBuffer); GLES20.glEnableVertexAttribArray(textureHandle); GLES20.glUniformMatrix4fv(textureMatrixHandle, 1, false, mSTMatrix, 0); GLES20.glUniformMatrix4fv(vertexMatrixHandle, 1, false, mMVPMatrix, 0); GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); GLES20.glDisableVertexAttribArray(positionHandle); GLES20.glDisableVertexAttribArray(textureHandle); GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0); GLES20.glUseProgram(0); egl10.eglSwapBuffers(eglDisplay, eglSurface); } @Override public void run() { initied = initGL(); super.run(); } @Override public void handleMessage(Message inputMessage) { int what = inputMessage.what; switch (what) { case DO_RENDER_MESSAGE: onDraw((Integer) inputMessage.obj); break; case DO_SHUTDOWN_MESSAGE: finish(); if (recording) { videoEncoder.stopRecording(inputMessage.arg1); } Looper looper = Looper.myLooper(); if (looper != null) { looper.quit(); } break; case DO_REINIT_MESSAGE: { if (!egl10.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext)) { if (BuildVars.LOGS_ENABLED) { FileLog.d("eglMakeCurrent failed " + GLUtils.getEGLErrorString(egl10.eglGetError())); } return; } if (cameraSurface != null) { cameraSurface.getTransformMatrix(moldSTMatrix); cameraSurface.setOnFrameAvailableListener(null); cameraSurface.release(); oldCameraTexture[0] = cameraTexture[0]; cameraTextureAlpha = 0.0f; cameraTexture[0] = 0; oldTextureTextureBuffer = textureBuffer.duplicate(); oldTexturePreviewSize = previewSize; } cameraId++; cameraReady = false; GLES20.glGenTextures(1, cameraTexture, 0); GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, cameraTexture[0]); GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST); GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST); GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); cameraSurface = new SurfaceTexture(cameraTexture[0]); cameraSurface.setOnFrameAvailableListener(surfaceTexture -> requestRender()); createCamera(cameraSurface); cameraThread.updateScale(); float tX = 1.0f / scaleX / 2.0f; float tY = 1.0f / scaleY / 2.0f; float[] texData = { 0.5f - tX, 0.5f - tY, 0.5f + tX, 0.5f - tY, 0.5f - tX, 0.5f + tY, 0.5f + tX, 0.5f + tY }; textureBuffer = ByteBuffer.allocateDirect(texData.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer(); textureBuffer.put(texData).position(0); break; } case DO_SETSESSION_MESSAGE: { if (BuildVars.LOGS_ENABLED) { FileLog.d("set gl rednderer session"); } CameraSession newSession = (CameraSession) inputMessage.obj; if (currentSession == newSession) { int rotationAngle = currentSession.getWorldAngle(); android.opengl.Matrix.setIdentityM(mMVPMatrix, 0); if (rotationAngle != 0) { android.opengl.Matrix.rotateM(mMVPMatrix, 0, rotationAngle, 0, 0, 1); } } else { currentSession = newSession; } break; } } } public void shutdown(int send) { Handler handler = getHandler(); if (handler != null) { sendMessage(handler.obtainMessage(DO_SHUTDOWN_MESSAGE, send, 0), 0); } } public void requestRender() { Handler handler = getHandler(); if (handler != null) { sendMessage(handler.obtainMessage(DO_RENDER_MESSAGE, cameraId), 0); } } } private static final int MSG_START_RECORDING = 0; private static final int MSG_STOP_RECORDING = 1; private static final int MSG_VIDEOFRAME_AVAILABLE = 2; private static final int MSG_AUDIOFRAME_AVAILABLE = 3; private static class EncoderHandler extends Handler { private WeakReference mWeakEncoder; public EncoderHandler(VideoRecorder encoder) { mWeakEncoder = new WeakReference<>(encoder); } @Override public void handleMessage(Message inputMessage) { int what = inputMessage.what; Object obj = inputMessage.obj; VideoRecorder encoder = mWeakEncoder.get(); if (encoder == null) { return; } switch (what) { case MSG_START_RECORDING: { try { if (BuildVars.LOGS_ENABLED) { FileLog.e("start encoder"); } encoder.prepareEncoder(); } catch (Exception e) { FileLog.e(e); encoder.handleStopRecording(0); Looper.myLooper().quit(); } break; } case MSG_STOP_RECORDING: { if (BuildVars.LOGS_ENABLED) { FileLog.e("stop encoder"); } encoder.handleStopRecording(inputMessage.arg1); break; } case MSG_VIDEOFRAME_AVAILABLE: { long timestamp = (((long) inputMessage.arg1) << 32) | (((long) inputMessage.arg2) & 0xffffffffL); Integer cameraId = (Integer) inputMessage.obj; encoder.handleVideoFrameAvailable(timestamp, cameraId); break; } case MSG_AUDIOFRAME_AVAILABLE: { encoder.handleAudioFrameAvailable((AudioBufferInfo) inputMessage.obj); break; } } } public void exit() { Looper.myLooper().quit(); } } public static class AudioBufferInfo { public final static int MAX_SAMPLES = 10; public ByteBuffer[] buffer = new ByteBuffer[MAX_SAMPLES]; public long[] offset = new long[MAX_SAMPLES]; public int[] read = new int[MAX_SAMPLES]; public int results; public int lastWroteBuffer; public boolean last; public AudioBufferInfo() { for (int i = 0; i < MAX_SAMPLES; i++) { buffer[i] = ByteBuffer.allocateDirect(2048); buffer[i].order(ByteOrder.nativeOrder()); } } } private class VideoRecorder implements Runnable { private static final String VIDEO_MIME_TYPE = "video/avc"; private static final String AUDIO_MIME_TYPE = "audio/mp4a-latm"; private static final int FRAME_RATE = 30; private static final int IFRAME_INTERVAL = 1; private File videoFile; private int videoWidth; private int videoHeight; private int videoBitrate; private boolean videoConvertFirstWrite = true; private boolean blendEnabled; private Surface surface; private android.opengl.EGLDisplay eglDisplay = EGL14.EGL_NO_DISPLAY; private android.opengl.EGLContext eglContext = EGL14.EGL_NO_CONTEXT; private android.opengl.EGLContext sharedEglContext; private android.opengl.EGLConfig eglConfig; private android.opengl.EGLSurface eglSurface = EGL14.EGL_NO_SURFACE; private MediaCodec videoEncoder; private MediaCodec audioEncoder; private int prependHeaderSize; private boolean firstEncode; private MediaCodec.BufferInfo videoBufferInfo; private MediaCodec.BufferInfo audioBufferInfo; private MP4Builder mediaMuxer; private ArrayList buffersToWrite = new ArrayList<>(); private int videoTrackIndex = -5; private int audioTrackIndex = -5; private long lastCommitedFrameTime; private long audioStartTime = -1; private long currentTimestamp = 0; private long lastTimestamp = -1; private volatile EncoderHandler handler; private final Object sync = new Object(); private boolean ready; private volatile boolean running; private volatile int sendWhenDone; private long skippedTime; private boolean skippedFirst; private long desyncTime; private long videoFirst = -1; private long videoLast; private long audioFirst = -1; private boolean audioStopedByTime; private int drawProgram; private int vertexMatrixHandle; private int textureMatrixHandle; private int positionHandle; private int textureHandle; private int resolutionHandle; private int previewSizeHandle; private int alphaHandle; private int zeroTimeStamps; private Integer lastCameraId = 0; private AudioRecord audioRecorder; private ArrayBlockingQueue buffers = new ArrayBlockingQueue<>(10); private ArrayList keyframeThumbs = new ArrayList<>(); private DispatchQueue generateKeyframeThumbsQueue; private int frameCount; private Runnable recorderRunnable = new Runnable() { @Override public void run() { long audioPresentationTimeUs = -1; int readResult; boolean done = false; while (!done) { if (!running && audioRecorder.getRecordingState() != AudioRecord.RECORDSTATE_STOPPED) { try { audioRecorder.stop(); } catch (Exception e) { done = true; } if (sendWhenDone == 0) { break; } } AudioBufferInfo buffer; if (buffers.isEmpty()) { buffer = new AudioBufferInfo(); } else { buffer = buffers.poll(); } buffer.lastWroteBuffer = 0; buffer.results = AudioBufferInfo.MAX_SAMPLES; for (int a = 0; a < AudioBufferInfo.MAX_SAMPLES; a++) { if (audioPresentationTimeUs == -1) { audioPresentationTimeUs = System.nanoTime() / 1000; } ByteBuffer byteBuffer = buffer.buffer[a]; byteBuffer.rewind(); readResult = audioRecorder.read(byteBuffer, 2048); if (readResult > 0 && a % 2 == 0) { byteBuffer.limit(readResult); double s = 0; for (int i = 0; i < readResult / 2; i++) { short p = byteBuffer.getShort(); s += p * p; } double amplitude = Math.sqrt(s / readResult / 2); AndroidUtilities.runOnUIThread(() -> NotificationCenter.getInstance(currentAccount).postNotificationName(NotificationCenter.recordProgressChanged, recordingGuid, amplitude)); byteBuffer.position(0); } if (readResult <= 0) { buffer.results = a; if (!running) { buffer.last = true; } break; } buffer.offset[a] = audioPresentationTimeUs; buffer.read[a] = readResult; int bufferDurationUs = 1000000 * readResult / audioSampleRate / 2; audioPresentationTimeUs += bufferDurationUs; } if (buffer.results >= 0 || buffer.last) { if (!running && buffer.results < AudioBufferInfo.MAX_SAMPLES) { done = true; } handler.sendMessage(handler.obtainMessage(MSG_AUDIOFRAME_AVAILABLE, buffer)); } else { if (!running) { done = true; } else { try { buffers.put(buffer); } catch (Exception ignore) { } } } } try { audioRecorder.release(); } catch (Exception e) { FileLog.e(e); } handler.sendMessage(handler.obtainMessage(MSG_STOP_RECORDING, sendWhenDone, 0)); } }; public void startRecording(File outputFile, android.opengl.EGLContext sharedContext) { int resolution = MessagesController.getInstance(currentAccount).roundVideoSize; int bitrate = MessagesController.getInstance(currentAccount).roundVideoBitrate * 1024; videoFile = outputFile; videoWidth = resolution; videoHeight = resolution; videoBitrate = bitrate; sharedEglContext = sharedContext; synchronized (sync) { if (running) { return; } running = true; Thread thread = new Thread(this, "TextureMovieEncoder"); thread.setPriority(Thread.MAX_PRIORITY); thread.start(); while (!ready) { try { sync.wait(); } catch (InterruptedException ie) { // ignore } } } keyframeThumbs.clear(); frameCount = 0; if (generateKeyframeThumbsQueue != null) { generateKeyframeThumbsQueue.cleanupQueue(); generateKeyframeThumbsQueue.recycle(); } generateKeyframeThumbsQueue = new DispatchQueue("keyframes_thumb_queque"); handler.sendMessage(handler.obtainMessage(MSG_START_RECORDING)); } public void stopRecording(int send) { handler.sendMessage(handler.obtainMessage(MSG_STOP_RECORDING, send, 0)); } public void frameAvailable(SurfaceTexture st, Integer cameraId, long timestampInternal) { synchronized (sync) { if (!ready) { return; } } long timestamp = st.getTimestamp(); if (timestamp == 0) { zeroTimeStamps++; if (zeroTimeStamps > 1) { if (BuildVars.LOGS_ENABLED) { FileLog.d("fix timestamp enabled"); } timestamp = timestampInternal; } else { return; } } else { zeroTimeStamps = 0; } handler.sendMessage(handler.obtainMessage(MSG_VIDEOFRAME_AVAILABLE, (int) (timestamp >> 32), (int) timestamp, cameraId)); } @Override public void run() { Looper.prepare(); synchronized (sync) { handler = new EncoderHandler(this); ready = true; sync.notify(); } Looper.loop(); synchronized (sync) { ready = false; } } private void handleAudioFrameAvailable(AudioBufferInfo input) { if (audioStopedByTime) { return; } buffersToWrite.add(input); if (audioFirst == -1) { if (videoFirst == -1) { if (BuildVars.LOGS_ENABLED) { FileLog.d("video record not yet started"); } return; } while (true) { boolean ok = false; for (int a = 0; a < input.results; a++) { if (a == 0 && Math.abs(videoFirst - input.offset[a]) > 10000000L) { desyncTime = videoFirst - input.offset[a]; audioFirst = input.offset[a]; ok = true; if (BuildVars.LOGS_ENABLED) { FileLog.d("detected desync between audio and video " + desyncTime); } break; } if (input.offset[a] >= videoFirst) { input.lastWroteBuffer = a; audioFirst = input.offset[a]; ok = true; if (BuildVars.LOGS_ENABLED) { FileLog.d("found first audio frame at " + a + " timestamp = " + input.offset[a]); } break; } else { if (BuildVars.LOGS_ENABLED) { FileLog.d("ignore first audio frame at " + a + " timestamp = " + input.offset[a]); } } } if (!ok) { if (BuildVars.LOGS_ENABLED) { FileLog.d("first audio frame not found, removing buffers " + input.results); } buffersToWrite.remove(input); } else { break; } if (!buffersToWrite.isEmpty()) { input = buffersToWrite.get(0); } else { return; } } } if (audioStartTime == -1) { audioStartTime = input.offset[input.lastWroteBuffer]; } if (buffersToWrite.size() > 1) { input = buffersToWrite.get(0); } try { drainEncoder(false); } catch (Exception e) { FileLog.e(e); } try { boolean isLast = false; while (input != null) { int inputBufferIndex = audioEncoder.dequeueInputBuffer(0); if (inputBufferIndex >= 0) { ByteBuffer inputBuffer; if (Build.VERSION.SDK_INT >= 21) { inputBuffer = audioEncoder.getInputBuffer(inputBufferIndex); } else { ByteBuffer[] inputBuffers = audioEncoder.getInputBuffers(); inputBuffer = inputBuffers[inputBufferIndex]; inputBuffer.clear(); } long startWriteTime = input.offset[input.lastWroteBuffer]; for (int a = input.lastWroteBuffer; a <= input.results; a++) { if (a < input.results) { if (!running && input.offset[a] >= videoLast - desyncTime) { if (BuildVars.LOGS_ENABLED) { FileLog.d("stop audio encoding because of stoped video recording at " + input.offset[a] + " last video " + videoLast); } audioStopedByTime = true; isLast = true; input = null; buffersToWrite.clear(); break; } if (inputBuffer.remaining() < input.read[a]) { input.lastWroteBuffer = a; input = null; break; } inputBuffer.put(input.buffer[a]); } if (a >= input.results - 1) { buffersToWrite.remove(input); if (running) { buffers.put(input); } if (!buffersToWrite.isEmpty()) { input = buffersToWrite.get(0); } else { isLast = input.last; input = null; break; } } } audioEncoder.queueInputBuffer(inputBufferIndex, 0, inputBuffer.position(), startWriteTime == 0 ? 0 : startWriteTime - audioStartTime, isLast ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0); } } } catch (Throwable e) { FileLog.e(e); } } private void handleVideoFrameAvailable(long timestampNanos, Integer cameraId) { try { drainEncoder(false); } catch (Exception e) { FileLog.e(e); } long dt, alphaDt; if (!lastCameraId.equals(cameraId)) { lastTimestamp = -1; lastCameraId = cameraId; } if (lastTimestamp == -1) { lastTimestamp = timestampNanos; if (currentTimestamp != 0) { dt = (System.currentTimeMillis() - lastCommitedFrameTime) * 1000000; alphaDt = 0; } else { alphaDt = dt = 0; } } else { alphaDt = dt = (timestampNanos - lastTimestamp); lastTimestamp = timestampNanos; } lastCommitedFrameTime = System.currentTimeMillis(); if (!skippedFirst) { skippedTime += dt; if (skippedTime < 200000000) { return; } skippedFirst = true; } currentTimestamp += dt; if (videoFirst == -1) { videoFirst = timestampNanos / 1000; if (BuildVars.LOGS_ENABLED) { FileLog.d("first video frame was at " + videoFirst); } } videoLast = timestampNanos; GLES20.glUseProgram(drawProgram); GLES20.glUniformMatrix4fv(vertexMatrixHandle, 1, false, mMVPMatrix, 0); GLES20.glActiveTexture(GLES20.GL_TEXTURE0); GLES20.glEnableVertexAttribArray(positionHandle); GLES20.glEnableVertexAttribArray(textureHandle); GLES20.glUniform2f(resolutionHandle, videoWidth, videoHeight); if (oldCameraTexture[0] != 0 && oldTextureTextureBuffer != null) { if (!blendEnabled) { GLES20.glEnable(GLES20.GL_BLEND); blendEnabled = true; } if (oldTexturePreviewSize != null) { GLES20.glUniform2f(previewSizeHandle, oldTexturePreviewSize.getWidth(), oldTexturePreviewSize.getHeight()); } GLES20.glVertexAttribPointer(textureHandle, 2, GLES20.GL_FLOAT, false, 8, oldTextureTextureBuffer); GLES20.glUniformMatrix4fv(textureMatrixHandle, 1, false, moldSTMatrix, 0); GLES20.glUniform1f(alphaHandle, 1.0f); GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, oldCameraTexture[0]); GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); } if (previewSize != null) { GLES20.glUniform2f(previewSizeHandle, previewSize.getWidth(), previewSize.getHeight()); } GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 12, vertexBuffer); GLES20.glVertexAttribPointer(textureHandle, 2, GLES20.GL_FLOAT, false, 8, textureBuffer); GLES20.glUniformMatrix4fv(textureMatrixHandle, 1, false, mSTMatrix, 0); GLES20.glUniform1f(alphaHandle, cameraTextureAlpha); GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, cameraTexture[0]); GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); GLES20.glDisableVertexAttribArray(positionHandle); GLES20.glDisableVertexAttribArray(textureHandle); GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0); GLES20.glUseProgram(0); EGLExt.eglPresentationTimeANDROID(eglDisplay, eglSurface, currentTimestamp); EGL14.eglSwapBuffers(eglDisplay, eglSurface); createKeyframeThumb(); frameCount++; if (oldCameraTexture[0] != 0 && cameraTextureAlpha < 1.0f) { cameraTextureAlpha += alphaDt / 200000000.0f; if (cameraTextureAlpha > 1) { GLES20.glDisable(GLES20.GL_BLEND); blendEnabled = false; cameraTextureAlpha = 1; GLES20.glDeleteTextures(1, oldCameraTexture, 0); oldCameraTexture[0] = 0; if (!cameraReady) { cameraReady = true; AndroidUtilities.runOnUIThread(() -> textureOverlayView.animate().setDuration(120).alpha(0.0f).setInterpolator(new DecelerateInterpolator()).start()); } } } else if (!cameraReady) { cameraReady = true; AndroidUtilities.runOnUIThread(() -> textureOverlayView.animate().setDuration(120).alpha(0.0f).setInterpolator(new DecelerateInterpolator()).start()); } } private void createKeyframeThumb() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && SharedConfig.getDevicePerformanceClass() == SharedConfig.PERFORMANCE_CLASS_HIGH && frameCount % 33 == 0) { GenerateKeyframeThumbTask task = new GenerateKeyframeThumbTask(); generateKeyframeThumbsQueue.postRunnable(task); } } private class GenerateKeyframeThumbTask implements Runnable { @Override public void run() { final TextureView textureView = InstantCameraView.this.textureView; if (textureView != null) { try { final Bitmap bitmap = textureView.getBitmap(AndroidUtilities.dp(56), AndroidUtilities.dp(56)); AndroidUtilities.runOnUIThread(() -> { if ((bitmap == null || bitmap.getPixel(0, 0) == 0) && keyframeThumbs.size() > 1) { keyframeThumbs.add(keyframeThumbs.get(keyframeThumbs.size() - 1)); } else { keyframeThumbs.add(bitmap); } }); } catch (Exception e) { FileLog.e(e); } } } } private void handleStopRecording(final int send) { if (running) { sendWhenDone = send; running = false; return; } try { drainEncoder(true); } catch (Exception e) { FileLog.e(e); } if (videoEncoder != null) { try { videoEncoder.stop(); videoEncoder.release(); videoEncoder = null; } catch (Exception e) { FileLog.e(e); } } if (audioEncoder != null) { try { audioEncoder.stop(); audioEncoder.release(); audioEncoder = null; setBluetoothScoOn(false); } catch (Exception e) { FileLog.e(e); } } if (mediaMuxer != null) { try { mediaMuxer.finishMovie(); } catch (Exception e) { FileLog.e(e); } } if (generateKeyframeThumbsQueue != null) { generateKeyframeThumbsQueue.cleanupQueue(); generateKeyframeThumbsQueue.recycle(); generateKeyframeThumbsQueue = null; } if (send != 0) { AndroidUtilities.runOnUIThread(() -> { videoEditedInfo = new VideoEditedInfo(); videoEditedInfo.roundVideo = true; videoEditedInfo.startTime = -1; videoEditedInfo.endTime = -1; videoEditedInfo.file = file; videoEditedInfo.encryptedFile = encryptedFile; videoEditedInfo.key = key; videoEditedInfo.iv = iv; videoEditedInfo.estimatedSize = Math.max(1, size); videoEditedInfo.framerate = 25; videoEditedInfo.resultWidth = videoEditedInfo.originalWidth = 360; videoEditedInfo.resultHeight = videoEditedInfo.originalHeight = 360; videoEditedInfo.originalPath = videoFile.getAbsolutePath(); if (send == 1) { if (baseFragment.isInScheduleMode()) { AlertsCreator.createScheduleDatePickerDialog(baseFragment.getParentActivity(), baseFragment.getDialogId(), (notify, scheduleDate) -> { baseFragment.sendMedia(new MediaController.PhotoEntry(0, 0, 0, videoFile.getAbsolutePath(), 0, true, 0, 0, 0), videoEditedInfo, notify, scheduleDate, false); startAnimation(false); }, () -> { startAnimation(false); }, resourcesProvider); } else { baseFragment.sendMedia(new MediaController.PhotoEntry(0, 0, 0, videoFile.getAbsolutePath(), 0, true, 0, 0, 0), videoEditedInfo, true, 0, false); } } else { videoPlayer = new VideoPlayer(); videoPlayer.setDelegate(new VideoPlayer.VideoPlayerDelegate() { @Override public void onStateChanged(boolean playWhenReady, int playbackState) { if (videoPlayer == null) { return; } if (videoPlayer.isPlaying() && playbackState == ExoPlayer.STATE_ENDED) { videoPlayer.seekTo(videoEditedInfo.startTime > 0 ? videoEditedInfo.startTime : 0); } } @Override public void onError(VideoPlayer player, Exception e) { FileLog.e(e); } @Override public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { } @Override public void onRenderedFirstFrame() { } @Override public boolean onSurfaceDestroyed(SurfaceTexture surfaceTexture) { return false; } @Override public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) { } }); videoPlayer.setTextureView(textureView); videoPlayer.preparePlayer(Uri.fromFile(videoFile), "other"); videoPlayer.play(); videoPlayer.setMute(true); startProgressTimer(); AnimatorSet animatorSet = new AnimatorSet(); animatorSet.playTogether( ObjectAnimator.ofFloat(switchCameraButton, View.ALPHA, 0.0f), ObjectAnimator.ofInt(paint, AnimationProperties.PAINT_ALPHA, 0), ObjectAnimator.ofFloat(muteImageView, View.ALPHA, 1.0f)); animatorSet.setDuration(180); animatorSet.setInterpolator(new DecelerateInterpolator()); animatorSet.start(); videoEditedInfo.estimatedDuration = recordedTime; NotificationCenter.getInstance(currentAccount).postNotificationName(NotificationCenter.audioDidSent, recordingGuid, videoEditedInfo, videoFile.getAbsolutePath(), keyframeThumbs); } didWriteData(videoFile, 0, true); MediaController.getInstance().requestAudioFocus(false); }); } else { FileLoader.getInstance(currentAccount).cancelFileUpload(videoFile.getAbsolutePath(), false); videoFile.delete(); } EGL14.eglDestroySurface(eglDisplay, eglSurface); eglSurface = EGL14.EGL_NO_SURFACE; if (surface != null) { surface.release(); surface = null; } if (eglDisplay != EGL14.EGL_NO_DISPLAY) { EGL14.eglMakeCurrent(eglDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_CONTEXT); EGL14.eglDestroyContext(eglDisplay, eglContext); EGL14.eglReleaseThread(); EGL14.eglTerminate(eglDisplay); } eglDisplay = EGL14.EGL_NO_DISPLAY; eglContext = EGL14.EGL_NO_CONTEXT; eglConfig = null; handler.exit(); } private void setBluetoothScoOn(boolean scoOn) { AudioManager am = (AudioManager) ApplicationLoader.applicationContext.getSystemService(Context.AUDIO_SERVICE); if (am.isBluetoothScoAvailableOffCall() && SharedConfig.recordViaSco || !scoOn) { BluetoothAdapter btAdapter = BluetoothAdapter.getDefaultAdapter(); try { if (btAdapter != null && btAdapter.getProfileConnectionState(BluetoothProfile.HEADSET) == BluetoothProfile.STATE_CONNECTED || !scoOn) { if (scoOn && !am.isBluetoothScoOn()) { am.startBluetoothSco(); } else if (!scoOn && am.isBluetoothScoOn()) { am.stopBluetoothSco(); } } } catch (SecurityException ignored) { } catch (Throwable e) { FileLog.e(e); } } } private void prepareEncoder() { setBluetoothScoOn(true); try { int recordBufferSize = AudioRecord.getMinBufferSize(audioSampleRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT); if (recordBufferSize <= 0) { recordBufferSize = 3584; } int bufferSize = 2048 * 24; if (bufferSize < recordBufferSize) { bufferSize = ((recordBufferSize / 2048) + 1) * 2048 * 2; } for (int a = 0; a < 3; a++) { buffers.add(new AudioBufferInfo()); } audioRecorder = new AudioRecord(MediaRecorder.AudioSource.DEFAULT, audioSampleRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, bufferSize); audioRecorder.startRecording(); if (BuildVars.LOGS_ENABLED) { FileLog.d("initied audio record with channels " + audioRecorder.getChannelCount() + " sample rate = " + audioRecorder.getSampleRate() + " bufferSize = " + bufferSize); } Thread thread = new Thread(recorderRunnable); thread.setPriority(Thread.MAX_PRIORITY); thread.start(); audioBufferInfo = new MediaCodec.BufferInfo(); videoBufferInfo = new MediaCodec.BufferInfo(); MediaFormat audioFormat = new MediaFormat(); audioFormat.setString(MediaFormat.KEY_MIME, AUDIO_MIME_TYPE); audioFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, audioSampleRate); audioFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1); audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, MessagesController.getInstance(currentAccount).roundAudioBitrate * 1024); audioFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 2048 * AudioBufferInfo.MAX_SAMPLES); audioEncoder = MediaCodec.createEncoderByType(AUDIO_MIME_TYPE); audioEncoder.configure(audioFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); audioEncoder.start(); videoEncoder = MediaCodec.createEncoderByType(VIDEO_MIME_TYPE); firstEncode = true; MediaFormat format = MediaFormat.createVideoFormat(VIDEO_MIME_TYPE, videoWidth, videoHeight); format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); format.setInteger(MediaFormat.KEY_BIT_RATE, videoBitrate); format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE); format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL); /*if (Build.VERSION.SDK_INT >= 21) { format.setInteger(MediaFormat.KEY_PROFILE, MediaCodecInfo.CodecProfileLevel.AVCProfileHigh); if (Build.VERSION.SDK_INT >= 23) { format.setInteger(MediaFormat.KEY_LEVEL, MediaCodecInfo.CodecProfileLevel.AVCLevel5); } }*/ videoEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); surface = videoEncoder.createInputSurface(); videoEncoder.start(); Mp4Movie movie = new Mp4Movie(); movie.setCacheFile(videoFile); movie.setRotation(0); movie.setSize(videoWidth, videoHeight); mediaMuxer = new MP4Builder().createMovie(movie, isSecretChat); AndroidUtilities.runOnUIThread(() -> { if (cancelled) { return; } try { performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP, HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING); } catch (Exception ignore) { } AndroidUtilities.lockOrientation(baseFragment.getParentActivity()); recording = true; recordStartTime = System.currentTimeMillis(); invalidate(); NotificationCenter.getInstance(currentAccount).postNotificationName(NotificationCenter.recordStarted, recordingGuid, false); }); } catch (Exception ioe) { throw new RuntimeException(ioe); } if (eglDisplay != EGL14.EGL_NO_DISPLAY) { throw new RuntimeException("EGL already set up"); } eglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); if (eglDisplay == EGL14.EGL_NO_DISPLAY) { throw new RuntimeException("unable to get EGL14 display"); } int[] version = new int[2]; if (!EGL14.eglInitialize(eglDisplay, version, 0, version, 1)) { eglDisplay = null; throw new RuntimeException("unable to initialize EGL14"); } if (eglContext == EGL14.EGL_NO_CONTEXT) { int renderableType = EGL14.EGL_OPENGL_ES2_BIT; int[] attribList = { EGL14.EGL_RED_SIZE, 8, EGL14.EGL_GREEN_SIZE, 8, EGL14.EGL_BLUE_SIZE, 8, EGL14.EGL_ALPHA_SIZE, 8, EGL14.EGL_RENDERABLE_TYPE, renderableType, 0x3142, 1, EGL14.EGL_NONE }; android.opengl.EGLConfig[] configs = new android.opengl.EGLConfig[1]; int[] numConfigs = new int[1]; if (!EGL14.eglChooseConfig(eglDisplay, attribList, 0, configs, 0, configs.length, numConfigs, 0)) { throw new RuntimeException("Unable to find a suitable EGLConfig"); } int[] attrib2_list = { EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE }; eglContext = EGL14.eglCreateContext(eglDisplay, configs[0], sharedEglContext, attrib2_list, 0); eglConfig = configs[0]; } int[] values = new int[1]; EGL14.eglQueryContext(eglDisplay, eglContext, EGL14.EGL_CONTEXT_CLIENT_VERSION, values, 0); if (eglSurface != EGL14.EGL_NO_SURFACE) { throw new IllegalStateException("surface already created"); } int[] surfaceAttribs = { EGL14.EGL_NONE }; eglSurface = EGL14.eglCreateWindowSurface(eglDisplay, eglConfig, surface, surfaceAttribs, 0); if (eglSurface == null) { throw new RuntimeException("surface was null"); } if (!EGL14.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext)) { if (BuildVars.LOGS_ENABLED) { FileLog.e("eglMakeCurrent failed " + GLUtils.getEGLErrorString(EGL14.eglGetError())); } throw new RuntimeException("eglMakeCurrent failed"); } GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA); int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, VERTEX_SHADER); int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, createFragmentShader(previewSize)); if (vertexShader != 0 && fragmentShader != 0) { drawProgram = GLES20.glCreateProgram(); GLES20.glAttachShader(drawProgram, vertexShader); GLES20.glAttachShader(drawProgram, fragmentShader); GLES20.glLinkProgram(drawProgram); int[] linkStatus = new int[1]; GLES20.glGetProgramiv(drawProgram, GLES20.GL_LINK_STATUS, linkStatus, 0); if (linkStatus[0] == 0) { GLES20.glDeleteProgram(drawProgram); drawProgram = 0; } else { positionHandle = GLES20.glGetAttribLocation(drawProgram, "aPosition"); textureHandle = GLES20.glGetAttribLocation(drawProgram, "aTextureCoord"); previewSizeHandle = GLES20.glGetUniformLocation(drawProgram, "preview"); resolutionHandle = GLES20.glGetUniformLocation(drawProgram, "resolution"); alphaHandle = GLES20.glGetUniformLocation(drawProgram, "alpha"); vertexMatrixHandle = GLES20.glGetUniformLocation(drawProgram, "uMVPMatrix"); textureMatrixHandle = GLES20.glGetUniformLocation(drawProgram, "uSTMatrix"); } } } public Surface getInputSurface() { return surface; } private void didWriteData(File file, long availableSize, boolean last) { if (videoConvertFirstWrite) { FileLoader.getInstance(currentAccount).uploadFile(file.toString(), isSecretChat, false, 1, ConnectionsManager.FileTypeVideo, false); videoConvertFirstWrite = false; if (last) { FileLoader.getInstance(currentAccount).checkUploadNewDataAvailable(file.toString(), isSecretChat, availableSize, last ? file.length() : 0); } } else { FileLoader.getInstance(currentAccount).checkUploadNewDataAvailable(file.toString(), isSecretChat, availableSize, last ? file.length() : 0); } } public void drainEncoder(boolean endOfStream) throws Exception { if (endOfStream) { videoEncoder.signalEndOfInputStream(); } ByteBuffer[] encoderOutputBuffers = null; if (Build.VERSION.SDK_INT < 21) { encoderOutputBuffers = videoEncoder.getOutputBuffers(); } while (true) { int encoderStatus = videoEncoder.dequeueOutputBuffer(videoBufferInfo, 10000); if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) { if (!endOfStream) { break; } } else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { if (Build.VERSION.SDK_INT < 21) { encoderOutputBuffers = videoEncoder.getOutputBuffers(); } } else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { MediaFormat newFormat = videoEncoder.getOutputFormat(); if (videoTrackIndex == -5) { videoTrackIndex = mediaMuxer.addTrack(newFormat, false); if (newFormat.containsKey(MediaFormat.KEY_PREPEND_HEADER_TO_SYNC_FRAMES) && newFormat.getInteger(MediaFormat.KEY_PREPEND_HEADER_TO_SYNC_FRAMES) == 1) { ByteBuffer spsBuff = newFormat.getByteBuffer("csd-0"); ByteBuffer ppsBuff = newFormat.getByteBuffer("csd-1"); prependHeaderSize = spsBuff.limit() + ppsBuff.limit(); } } } else if (encoderStatus >= 0) { ByteBuffer encodedData; if (Build.VERSION.SDK_INT < 21) { encodedData = encoderOutputBuffers[encoderStatus]; } else { encodedData = videoEncoder.getOutputBuffer(encoderStatus); } if (encodedData == null) { throw new RuntimeException("encoderOutputBuffer " + encoderStatus + " was null"); } if (videoBufferInfo.size > 1) { if ((videoBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0) { if (prependHeaderSize != 0 && (videoBufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) { videoBufferInfo.offset += prependHeaderSize; videoBufferInfo.size -= prependHeaderSize; } if (firstEncode && (videoBufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) { if (videoBufferInfo.size > 100) { encodedData.position(videoBufferInfo.offset); byte[] temp = new byte[100]; encodedData.get(temp); int nalCount = 0; for (int a = 0; a < temp.length - 4; a++) { if (temp[a] == 0 && temp[a + 1] == 0 && temp[a + 2] == 0 && temp[a + 3] == 1) { nalCount++; if (nalCount > 1) { videoBufferInfo.offset += a; videoBufferInfo.size -= a; break; } } } } firstEncode = false; } long availableSize = mediaMuxer.writeSampleData(videoTrackIndex, encodedData, videoBufferInfo, true); if (availableSize != 0) { didWriteData(videoFile, availableSize, false); } } else if (videoTrackIndex == -5) { byte[] csd = new byte[videoBufferInfo.size]; encodedData.limit(videoBufferInfo.offset + videoBufferInfo.size); encodedData.position(videoBufferInfo.offset); encodedData.get(csd); ByteBuffer sps = null; ByteBuffer pps = null; for (int a = videoBufferInfo.size - 1; a >= 0; a--) { if (a > 3) { if (csd[a] == 1 && csd[a - 1] == 0 && csd[a - 2] == 0 && csd[a - 3] == 0) { sps = ByteBuffer.allocate(a - 3); pps = ByteBuffer.allocate(videoBufferInfo.size - (a - 3)); sps.put(csd, 0, a - 3).position(0); pps.put(csd, a - 3, videoBufferInfo.size - (a - 3)).position(0); break; } } else { break; } } MediaFormat newFormat = MediaFormat.createVideoFormat("video/avc", videoWidth, videoHeight); if (sps != null && pps != null) { newFormat.setByteBuffer("csd-0", sps); newFormat.setByteBuffer("csd-1", pps); } videoTrackIndex = mediaMuxer.addTrack(newFormat, false); } } videoEncoder.releaseOutputBuffer(encoderStatus, false); if ((videoBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { break; } } } if (Build.VERSION.SDK_INT < 21) { encoderOutputBuffers = audioEncoder.getOutputBuffers(); } boolean encoderOutputAvailable = true; while (true) { int encoderStatus = audioEncoder.dequeueOutputBuffer(audioBufferInfo, 0); if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) { if (!endOfStream || !running && sendWhenDone == 0) { break; } } else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { if (Build.VERSION.SDK_INT < 21) { encoderOutputBuffers = audioEncoder.getOutputBuffers(); } } else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { MediaFormat newFormat = audioEncoder.getOutputFormat(); if (audioTrackIndex == -5) { audioTrackIndex = mediaMuxer.addTrack(newFormat, true); } } else if (encoderStatus >= 0) { ByteBuffer encodedData; if (Build.VERSION.SDK_INT < 21) { encodedData = encoderOutputBuffers[encoderStatus]; } else { encodedData = audioEncoder.getOutputBuffer(encoderStatus); } if (encodedData == null) { throw new RuntimeException("encoderOutputBuffer " + encoderStatus + " was null"); } if ((audioBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { audioBufferInfo.size = 0; } if (audioBufferInfo.size != 0) { long availableSize = mediaMuxer.writeSampleData(audioTrackIndex, encodedData, audioBufferInfo, false); if (availableSize != 0) { didWriteData(videoFile, availableSize, false); } } audioEncoder.releaseOutputBuffer(encoderStatus, false); if ((audioBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { break; } } } } @Override protected void finalize() throws Throwable { try { if (eglDisplay != EGL14.EGL_NO_DISPLAY) { EGL14.eglMakeCurrent(eglDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_CONTEXT); EGL14.eglDestroyContext(eglDisplay, eglContext); EGL14.eglReleaseThread(); EGL14.eglTerminate(eglDisplay); eglDisplay = EGL14.EGL_NO_DISPLAY; eglContext = EGL14.EGL_NO_CONTEXT; eglConfig = null; } } finally { super.finalize(); } } } private String createFragmentShader(Size previewSize) { if (SharedConfig.getDevicePerformanceClass() == SharedConfig.PERFORMANCE_CLASS_LOW || SharedConfig.getDevicePerformanceClass() == SharedConfig.PERFORMANCE_CLASS_AVERAGE || Math.max(previewSize.getHeight(), previewSize.getWidth()) * 0.7f < MessagesController.getInstance(currentAccount).roundVideoSize) { return "#extension GL_OES_EGL_image_external : require\n" + "precision highp float;\n" + "varying vec2 vTextureCoord;\n" + "uniform float alpha;\n" + "uniform vec2 preview;\n" + "uniform vec2 resolution;\n" + "uniform samplerExternalOES sTexture;\n" + "void main() {\n" + " vec4 textColor = texture2D(sTexture, vTextureCoord);\n" + " vec2 coord = resolution * 0.5;\n" + " float radius = 0.51 * resolution.x;\n" + " float d = length(coord - gl_FragCoord.xy) - radius;\n" + " float t = clamp(d, 0.0, 1.0);\n" + " vec3 color = mix(textColor.rgb, vec3(1, 1, 1), t);\n" + " gl_FragColor = vec4(color * alpha, alpha);\n" + "}\n"; } //apply box blur return "#extension GL_OES_EGL_image_external : require\n" + "precision highp float;\n" + "varying vec2 vTextureCoord;\n" + "uniform vec2 resolution;\n" + "uniform vec2 preview;\n" + "uniform float alpha;\n" + "const float kernel = 1.0;\n" + "uniform samplerExternalOES sTexture;\n" + "void main() {\n" + " float pixelSizeX = 1.0 / preview.x;\n" + " float pixelSizeY = 1.0 / preview.y;\n" + " vec3 accumulation = vec3(0);\n" + " vec3 weightsum = vec3(0);\n" + " for (float x = -kernel; x < kernel; x++){\n" + " for (float y = -kernel; y < kernel; y++){\n" + " accumulation += texture2D(sTexture, vTextureCoord + vec2(x * pixelSizeX, y * pixelSizeY)).xyz;\n" + " weightsum += 1.0;\n" + " }\n" + " }\n" + " vec4 textColor = vec4(accumulation / weightsum, 1.0);\n" + " vec2 coord = resolution * 0.5;\n" + " float radius = 0.51 * resolution.x;\n" + " float d = length(coord - gl_FragCoord.xy) - radius;\n" + " float t = clamp(d, 0.0, 1.0);\n" + " vec3 color = mix(textColor.rgb, vec3(1, 1, 1), t);\n" + " gl_FragColor = vec4(color * alpha, alpha);\n" + "}\n"; } public class InstantViewCameraContainer extends FrameLayout { ImageReceiver imageReceiver; float imageProgress; public InstantViewCameraContainer(Context context) { super(context); InstantCameraView.this.setWillNotDraw(false); } public void setImageReceiver(ImageReceiver imageReceiver) { if (this.imageReceiver == null) { imageProgress = 0; } this.imageReceiver = imageReceiver; invalidate(); } @Override protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); if (imageProgress != 1f) { imageProgress += 16 / 250.0f; if (imageProgress > 1f) { imageProgress = 1f; } invalidate(); } if (imageReceiver != null) { canvas.save(); if (imageReceiver.getImageWidth() != textureViewSize) { float s = textureViewSize / imageReceiver.getImageWidth(); canvas.scale(s, s); } canvas.translate(-imageReceiver.getImageX(), -imageReceiver.getImageY()); float oldAlpha = imageReceiver.getAlpha(); imageReceiver.setAlpha(imageProgress); imageReceiver.draw(canvas); imageReceiver.setAlpha(oldAlpha); canvas.restore(); } } } @Override public boolean onTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN && baseFragment != null) { if (videoPlayer != null) { boolean mute = !videoPlayer.isMuted(); videoPlayer.setMute(mute); if (muteAnimation != null) { muteAnimation.cancel(); } muteAnimation = new AnimatorSet(); muteAnimation.playTogether( ObjectAnimator.ofFloat(muteImageView, View.ALPHA, mute ? 1.0f : 0.0f), ObjectAnimator.ofFloat(muteImageView, View.SCALE_X, mute ? 1.0f : 0.5f), ObjectAnimator.ofFloat(muteImageView, View.SCALE_Y, mute ? 1.0f : 0.5f)); muteAnimation.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { if (animation.equals(muteAnimation)) { muteAnimation = null; } } }); muteAnimation.setDuration(180); muteAnimation.setInterpolator(new DecelerateInterpolator()); muteAnimation.start(); } else { //baseFragment.checkRecordLocked(false); } } if (ev.getActionMasked() == MotionEvent.ACTION_DOWN || ev.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) { if (maybePinchToZoomTouchMode && !isInPinchToZoomTouchMode && ev.getPointerCount() == 2 && finishZoomTransition == null && recording) { pinchStartDistance = (float) Math.hypot(ev.getX(1) - ev.getX(0), ev.getY(1) - ev.getY(0)); pinchScale = 1f; pointerId1 = ev.getPointerId(0); pointerId2 = ev.getPointerId(1); isInPinchToZoomTouchMode = true; } if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) { AndroidUtilities.rectTmp.set(cameraContainer.getX(), cameraContainer.getY(), cameraContainer.getX() + cameraContainer.getMeasuredWidth(), cameraContainer.getY() + cameraContainer.getMeasuredHeight()); maybePinchToZoomTouchMode = AndroidUtilities.rectTmp.contains(ev.getX(), ev.getY()); } return true; } else if (ev.getActionMasked() == MotionEvent.ACTION_MOVE && isInPinchToZoomTouchMode) { int index1 = -1; int index2 = -1; for (int i = 0; i < ev.getPointerCount(); i++) { if (pointerId1 == ev.getPointerId(i)) { index1 = i; } if (pointerId2 == ev.getPointerId(i)) { index2 = i; } } if (index1 == -1 || index2 == -1) { isInPinchToZoomTouchMode = false; finishZoom(); return false; } pinchScale = (float) Math.hypot(ev.getX(index2) - ev.getX(index1), ev.getY(index2) - ev.getY(index1)) / pinchStartDistance; float zoom = Math.min(1f, Math.max(0, pinchScale - 1f)); cameraSession.setZoom(zoom); } else if ((ev.getActionMasked() == MotionEvent.ACTION_UP || (ev.getActionMasked() == MotionEvent.ACTION_POINTER_UP && checkPointerIds(ev)) || ev.getActionMasked() == MotionEvent.ACTION_CANCEL) && isInPinchToZoomTouchMode) { isInPinchToZoomTouchMode = false; finishZoom(); } return true; } ValueAnimator finishZoomTransition; public void finishZoom() { if (finishZoomTransition != null) { return; } float zoom = Math.min(1f, Math.max(0, pinchScale - 1f)); if (zoom > 0f) { finishZoomTransition = ValueAnimator.ofFloat(zoom, 0); finishZoomTransition.addUpdateListener(valueAnimator -> { if (cameraSession != null) { cameraSession.setZoom((float) valueAnimator.getAnimatedValue()); } }); finishZoomTransition.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { if (finishZoomTransition != null) { finishZoomTransition = null; } } }); finishZoomTransition.setDuration(350); finishZoomTransition.setInterpolator(CubicBezierInterpolator.DEFAULT); finishZoomTransition.start(); } } }