[go: nahoru, domu]

blob: 5477b9cacbd6a5a61ae2c7aa5a7348be51829b27 [file] [log] [blame]
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.camera.core.processing;
import static androidx.camera.core.ImageProcessingUtil.writeJpegBytesToSurface;
import static androidx.camera.core.impl.ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE;
import static androidx.camera.core.impl.utils.TransformUtils.rotateSize;
import static androidx.core.util.Preconditions.checkState;
import static java.util.Objects.requireNonNull;
import android.graphics.Bitmap;
import android.graphics.ImageFormat;
import android.graphics.SurfaceTexture;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Size;
import android.view.Surface;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;
import androidx.arch.core.util.Function;
import androidx.camera.core.DynamicRange;
import androidx.camera.core.Logger;
import androidx.camera.core.SurfaceOutput;
import androidx.camera.core.SurfaceProcessor;
import androidx.camera.core.SurfaceRequest;
import androidx.camera.core.impl.utils.MatrixExt;
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
import androidx.camera.core.impl.utils.futures.Futures;
import androidx.concurrent.futures.CallbackToFutureAdapter;
import com.google.auto.value.AutoValue;
import com.google.common.util.concurrent.ListenableFuture;
import kotlin.Triple;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* A default implementation of {@link SurfaceProcessor}.
*
* <p> This implementation simply copies the frame from the source to the destination with the
* transformation defined in {@link SurfaceOutput#updateTransformMatrix}.
*/
@RequiresApi(21)
public class DefaultSurfaceProcessor implements SurfaceProcessorInternal,
SurfaceTexture.OnFrameAvailableListener {
private static final String TAG = "DefaultSurfaceProcessor";
private final OpenGlRenderer mGlRenderer;
@VisibleForTesting
final HandlerThread mGlThread;
private final Executor mGlExecutor;
@VisibleForTesting
final Handler mGlHandler;
private final AtomicBoolean mIsReleaseRequested = new AtomicBoolean(false);
private final float[] mTextureMatrix = new float[16];
private final float[] mSurfaceOutputMatrix = new float[16];
// Map of current set of available outputs. Only access this on GL thread.
@SuppressWarnings("WeakerAccess") /* synthetic access */
final Map<SurfaceOutput, Surface> mOutputSurfaces = new LinkedHashMap<>();
// Only access this on GL thread.
private int mInputSurfaceCount = 0;
// Only access this on GL thread.
private boolean mIsReleased = false;
// Only access this on GL thread.
private final List<PendingSnapshot> mPendingSnapshots = new ArrayList<>();
/** Constructs {@link DefaultSurfaceProcessor} with default shaders. */
DefaultSurfaceProcessor(@NonNull DynamicRange dynamicRange) {
this(dynamicRange, ShaderProvider.DEFAULT);
}
/**
* Constructs {@link DefaultSurfaceProcessor} with custom shaders.
*
* @param shaderProvider custom shader provider for OpenGL rendering.
* @throws IllegalArgumentException if the shaderProvider provides invalid shader.
*/
DefaultSurfaceProcessor(@NonNull DynamicRange dynamicRange,
@NonNull ShaderProvider shaderProvider) {
mGlThread = new HandlerThread("GL Thread");
mGlThread.start();
mGlHandler = new Handler(mGlThread.getLooper());
mGlExecutor = CameraXExecutors.newHandlerExecutor(mGlHandler);
mGlRenderer = new OpenGlRenderer();
try {
initGlRenderer(dynamicRange, shaderProvider);
} catch (RuntimeException e) {
release();
throw e;
}
}
/**
* {@inheritDoc}
*/
@Override
public void onInputSurface(@NonNull SurfaceRequest surfaceRequest) {
if (mIsReleaseRequested.get()) {
surfaceRequest.willNotProvideSurface();
return;
}
executeSafely(() -> {
mInputSurfaceCount++;
SurfaceTexture surfaceTexture = new SurfaceTexture(mGlRenderer.getTextureName());
surfaceTexture.setDefaultBufferSize(surfaceRequest.getResolution().getWidth(),
surfaceRequest.getResolution().getHeight());
Surface surface = new Surface(surfaceTexture);
surfaceRequest.provideSurface(surface, mGlExecutor, result -> {
surfaceTexture.setOnFrameAvailableListener(null);
surfaceTexture.release();
surface.release();
mInputSurfaceCount--;
checkReadyToRelease();
});
surfaceTexture.setOnFrameAvailableListener(this, mGlHandler);
}, surfaceRequest::willNotProvideSurface);
}
/**
* {@inheritDoc}
*/
@Override
public void onOutputSurface(@NonNull SurfaceOutput surfaceOutput) {
if (mIsReleaseRequested.get()) {
surfaceOutput.close();
return;
}
executeSafely(() -> {
Surface surface = surfaceOutput.getSurface(mGlExecutor, event -> {
surfaceOutput.close();
Surface removedSurface = mOutputSurfaces.remove(surfaceOutput);
if (removedSurface != null) {
mGlRenderer.unregisterOutputSurface(removedSurface);
}
});
mGlRenderer.registerOutputSurface(surface);
mOutputSurfaces.put(surfaceOutput, surface);
}, surfaceOutput::close);
}
/**
* Release the {@link DefaultSurfaceProcessor}.
*/
@Override
public void release() {
if (mIsReleaseRequested.getAndSet(true)) {
return;
}
executeSafely(() -> {
mIsReleased = true;
checkReadyToRelease();
});
}
@Override
@NonNull
public ListenableFuture<Void> snapshot(
@IntRange(from = 0, to = 100) int jpegQuality,
@IntRange(from = 0, to = 359) int rotationDegrees) {
return Futures.nonCancellationPropagating(CallbackToFutureAdapter.getFuture(
completer -> {
PendingSnapshot pendingSnapshot = PendingSnapshot.of(jpegQuality,
rotationDegrees, completer);
executeSafely(
() -> mPendingSnapshots.add(pendingSnapshot),
() -> completer.setException(new Exception(
"Failed to snapshot: OpenGLRenderer not ready.")));
return "DefaultSurfaceProcessor#snapshot";
}));
}
/**
* {@inheritDoc}
*/
@Override
public void onFrameAvailable(@NonNull SurfaceTexture surfaceTexture) {
if (mIsReleaseRequested.get()) {
// Ignore frame update if released.
return;
}
surfaceTexture.updateTexImage();
surfaceTexture.getTransformMatrix(mTextureMatrix);
// Surface, size and transform matrix for JPEG Surface if exists
Triple<Surface, Size, float[]> jpegOutput = null;
for (Map.Entry<SurfaceOutput, Surface> entry : mOutputSurfaces.entrySet()) {
Surface surface = entry.getValue();
SurfaceOutput surfaceOutput = entry.getKey();
surfaceOutput.updateTransformMatrix(mSurfaceOutputMatrix, mTextureMatrix);
if (surfaceOutput.getFormat() == INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE) {
// Render GPU output directly.
try {
mGlRenderer.render(surfaceTexture.getTimestamp(), mSurfaceOutputMatrix,
surface);
} catch (RuntimeException e) {
// This should not happen. However, when it happens, we catch the exception
// to prevent the crash.
Logger.e(TAG, "Failed to render with OpenGL.", e);
}
} else {
checkState(surfaceOutput.getFormat() == ImageFormat.JPEG,
"Unsupported format: " + surfaceOutput.getFormat());
checkState(jpegOutput == null, "Only one JPEG output is supported.");
jpegOutput = new Triple<>(surface, surfaceOutput.getSize(),
mSurfaceOutputMatrix.clone());
}
}
// Execute all pending snapshots.
try {
takeSnapshotAndDrawJpeg(jpegOutput);
} catch (RuntimeException e) {
// Propagates error back to the app if failed to take snapshot.
failAllPendingSnapshots(e);
}
}
/**
* Takes a snapshot of the current frame and draws it to given JPEG surface.
*
* @param jpegOutput The <Surface, Surface size, transform matrix> tuple for drawing.
*/
@WorkerThread
private void takeSnapshotAndDrawJpeg(@Nullable Triple<Surface, Size, float[]> jpegOutput) {
if (mPendingSnapshots.isEmpty()) {
// No pending snapshot requests, do nothing.
return;
}
// No JPEG Surface, fail all snapshot requests.
if (jpegOutput == null) {
failAllPendingSnapshots(new Exception("Failed to snapshot: no JPEG Surface."));
return;
}
// Write to JPEG surface, once for each snapshot request.
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
byte[] jpegBytes = null;
int jpegQuality = -1;
int rotationDegrees = -1;
Bitmap bitmap = null;
Iterator<PendingSnapshot> iterator = mPendingSnapshots.iterator();
while (iterator.hasNext()) {
PendingSnapshot pendingSnapshot = iterator.next();
// Take a new snapshot if the rotation is different.
if (rotationDegrees != pendingSnapshot.getRotationDegrees() || bitmap == null) {
rotationDegrees = pendingSnapshot.getRotationDegrees();
// Recycle the previous bitmap to free up memory.
if (bitmap != null) {
bitmap.recycle();
}
bitmap = getBitmap(jpegOutput.getSecond(), jpegOutput.getThird(),
rotationDegrees);
// Clear JPEG quality to force re-encoding.
jpegQuality = -1;
}
// Re-encode the bitmap if the quality is different.
if (jpegQuality != pendingSnapshot.getJpegQuality()) {
outputStream.reset();
jpegQuality = pendingSnapshot.getJpegQuality();
bitmap.compress(Bitmap.CompressFormat.JPEG, jpegQuality, outputStream);
jpegBytes = outputStream.toByteArray();
}
writeJpegBytesToSurface(jpegOutput.getFirst(), requireNonNull(jpegBytes));
pendingSnapshot.getCompleter().set(null);
iterator.remove();
}
} catch (IOException e) {
failAllPendingSnapshots(e);
}
}
private void failAllPendingSnapshots(@NonNull Throwable throwable) {
for (PendingSnapshot pendingSnapshot : mPendingSnapshots) {
pendingSnapshot.getCompleter().setException(throwable);
}
mPendingSnapshots.clear();
}
@NonNull
private Bitmap getBitmap(@NonNull Size size,
@NonNull float[] textureTransform,
int rotationDegrees) {
float[] snapshotTransform = textureTransform.clone();
// Rotate the output if requested.
MatrixExt.preRotate(snapshotTransform, rotationDegrees, 0.5f, 0.5f);
// Flip the snapshot. This is for reverting the GL transform added in SurfaceOutputImpl.
MatrixExt.preVerticalFlip(snapshotTransform, 0.5f);
// Update the size based on the rotation degrees.
size = rotateSize(size, rotationDegrees);
// Take a snapshot Bitmap and compress it to JPEG.
return mGlRenderer.snapshot(size, snapshotTransform);
}
@WorkerThread
private void checkReadyToRelease() {
if (mIsReleased && mInputSurfaceCount == 0) {
// Once release is called, we can stop sending frame to output surfaces.
for (SurfaceOutput surfaceOutput : mOutputSurfaces.keySet()) {
surfaceOutput.close();
}
for (PendingSnapshot pendingSnapshot : mPendingSnapshots) {
pendingSnapshot.getCompleter().setException(
new Exception("Failed to snapshot: DefaultSurfaceProcessor is released."));
}
mOutputSurfaces.clear();
mGlRenderer.release();
mGlThread.quit();
}
}
private void initGlRenderer(@NonNull DynamicRange dynamicRange,
@NonNull ShaderProvider shaderProvider) {
ListenableFuture<Void> initFuture = CallbackToFutureAdapter.getFuture(completer -> {
executeSafely(() -> {
try {
mGlRenderer.init(dynamicRange, shaderProvider);
completer.set(null);
} catch (RuntimeException e) {
completer.setException(e);
}
});
return "Init GlRenderer";
});
try {
initFuture.get();
} catch (ExecutionException | InterruptedException e) {
// If the cause is a runtime exception, throw it directly. Otherwise convert to runtime
// exception and throw.
Throwable cause = e instanceof ExecutionException ? e.getCause() : e;
if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
} else {
throw new IllegalStateException("Failed to create DefaultSurfaceProcessor", cause);
}
}
}
private void executeSafely(@NonNull Runnable runnable) {
executeSafely(runnable, () -> {
// Do nothing.
});
}
private void executeSafely(@NonNull Runnable runnable, @NonNull Runnable onFailure) {
try {
mGlExecutor.execute(() -> {
if (mIsReleased) {
onFailure.run();
} else {
runnable.run();
}
});
} catch (RejectedExecutionException e) {
Logger.w(TAG, "Unable to executor runnable", e);
onFailure.run();
}
}
/**
* A pending snapshot request to be executed on the next frame available.
*/
@AutoValue
abstract static class PendingSnapshot {
@IntRange(from = 0, to = 100)
abstract int getJpegQuality();
@IntRange(from = 0, to = 359)
abstract int getRotationDegrees();
@NonNull
abstract CallbackToFutureAdapter.Completer<Void> getCompleter();
@NonNull
static AutoValue_DefaultSurfaceProcessor_PendingSnapshot of(
@IntRange(from = 0, to = 100) int jpegQuality,
@IntRange(from = 0, to = 359) int rotationDegrees,
@NonNull CallbackToFutureAdapter.Completer<Void> completer) {
return new AutoValue_DefaultSurfaceProcessor_PendingSnapshot(
jpegQuality, rotationDegrees, completer);
}
}
/**
* Factory class that produces {@link DefaultSurfaceProcessor}.
*
* <p> This is for working around the limit that OpenGL cannot be initialized in unit tests.
*/
public static class Factory {
private Factory() {
}
private static Function<DynamicRange, SurfaceProcessorInternal> sSupplier =
DefaultSurfaceProcessor::new;
/**
* Creates a new {@link DefaultSurfaceProcessor} with no-op shader.
*/
@NonNull
public static SurfaceProcessorInternal newInstance(@NonNull DynamicRange dynamicRange) {
return sSupplier.apply(dynamicRange);
}
/**
* Overrides the {@link DefaultSurfaceProcessor} supplier for testing.
*/
@VisibleForTesting
public static void setSupplier(
@NonNull Function<DynamicRange, SurfaceProcessorInternal> supplier) {
sSupplier = supplier;
}
}
}