diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9a3bd6d2..37fb81c3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,8 +6,6 @@ on: branches: - master pull_request: -env: - TRAVIS: true jobs: ANDROID_BASE_CHECKS: name: Base Checks diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 8390af24..7eed30cd 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -8,7 +8,6 @@ jobs: name: Bintray Upload runs-on: ubuntu-latest env: - TRAVIS: true BINTRAY_USER: ${{ secrets.BINTRAY_USER }} BINTRAY_KEY: ${{ secrets.BINTRAY_KEY }} BINTRAY_REPO: ${{ secrets.BINTRAY_REPO }} diff --git a/cameraview/build.gradle.kts b/cameraview/build.gradle.kts index 360d81df..3f24f095 100644 --- a/cameraview/build.gradle.kts +++ b/cameraview/build.gradle.kts @@ -51,15 +51,15 @@ publisher { project.group = "com.otaliastudios" project.url = "https://github.com/natario1/CameraView" project.addLicense(License.APACHE_2_0) - release.setSources(Release.SOURCES_AUTO) - release.setDocs(Release.DOCS_AUTO) bintray { + release.setSources(Release.SOURCES_AUTO) + release.setDocs(Release.DOCS_AUTO) auth.user = "BINTRAY_USER" auth.key = "BINTRAY_KEY" auth.repo = "BINTRAY_REPO" } directory { - directory = "build/local" + directory = file(repositories.mavenLocal().url).absolutePath } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java index 922da7b0..6f976f08 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java @@ -214,6 +214,7 @@ public class Camera1Engine extends CameraBaseEngine implements mCaptureSize = computeCaptureSize(); mPreviewStreamSize = computePreviewStreamSize(); + LOG.i("onStartBind:", "Returning"); return Tasks.forResult(null); } @@ -623,9 +624,8 @@ public class Camera1Engine extends CameraBaseEngine implements public void setZoom(final float zoom, @Nullable final PointF[] points, final boolean notify) { final float old = mZoomValue; mZoomValue = zoom; - // Zoom requests can be high frequency (e.g. linked to touch events), so - // we remove the task before scheduling to avoid stack overflows in orchestrator. - getOrchestrator().remove("zoom"); + // Zoom requests can be high frequency (e.g. linked to touch events), let's trim the oldest. + getOrchestrator().trim("zoom", ALLOWED_ZOOM_OPS); mZoomTask = getOrchestrator().scheduleStateful("zoom", CameraState.ENGINE, new Runnable() { @@ -658,9 +658,8 @@ public class Camera1Engine extends CameraBaseEngine implements @Nullable final PointF[] points, final boolean notify) { final float old = mExposureCorrectionValue; mExposureCorrectionValue = EVvalue; - // EV requests can be high frequency (e.g. linked to touch events), so - // we remove the task before scheduling to avoid stack overflows in orchestrator. - getOrchestrator().remove("exposure correction"); + // EV requests can be high frequency (e.g. linked to touch events), let's trim the oldest. + getOrchestrator().trim("exposure correction", ALLOWED_EV_OPS); mExposureCorrectionTask = getOrchestrator().scheduleStateful( "exposure correction", CameraState.ENGINE, @@ -888,7 +887,7 @@ public class Camera1Engine extends CameraBaseEngine implements // The auto focus callback is not guaranteed to be called, but we really want it // to be. So we remove the old runnable if still present and post a new one. getOrchestrator().remove(JOB_FOCUS_END); - getOrchestrator().scheduleDelayed(JOB_FOCUS_END, AUTOFOCUS_END_DELAY_MILLIS, + getOrchestrator().scheduleDelayed(JOB_FOCUS_END, true, AUTOFOCUS_END_DELAY_MILLIS, new Runnable() { @Override public void run() { diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java index 2fa7f385..0ee02c13 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java @@ -19,6 +19,8 @@ import android.location.Location; import android.media.Image; import android.media.ImageReader; import android.os.Build; +import android.os.Handler; +import android.os.Looper; import android.util.Pair; import android.util.Range; import android.util.Rational; @@ -61,6 +63,7 @@ import com.otaliastudios.cameraview.frame.FrameManager; import com.otaliastudios.cameraview.frame.ImageFrameManager; import com.otaliastudios.cameraview.gesture.Gesture; import com.otaliastudios.cameraview.internal.CropHelper; +import com.otaliastudios.cameraview.internal.FpsRangeValidator; import com.otaliastudios.cameraview.metering.MeteringRegions; import com.otaliastudios.cameraview.picture.Full2PictureRecorder; import com.otaliastudios.cameraview.picture.Snapshot2PictureRecorder; @@ -76,6 +79,7 @@ import java.util.Comparator; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; @RequiresApi(Build.VERSION_CODES.LOLLIPOP) @@ -228,6 +232,15 @@ public class Camera2Engine extends CameraBaseEngine implements } } + /** + * Can be changed to select something different than {@link CameraDevice#TEMPLATE_PREVIEW} + * for the default repeating request. + * @return the default template for preview + */ + protected int getRepeatingRequestDefaultTemplate() { + return CameraDevice.TEMPLATE_PREVIEW; + } + /** * Applies the repeating request builder to the preview, assuming we actually have a preview * running. Can be called after changing parameters to the builder. @@ -419,7 +432,7 @@ public class Camera2Engine extends CameraBaseEngine implements + mPictureFormat); } mCameraOptions = new Camera2Options(mManager, mCameraId, flip, format); - createRepeatingRequestBuilder(CameraDevice.TEMPLATE_PREVIEW); + createRepeatingRequestBuilder(getRepeatingRequestDefaultTemplate()); } catch (CameraAccessException e) { task.trySetException(createCameraException(e)); return; @@ -489,6 +502,7 @@ public class Camera2Engine extends CameraBaseEngine implements if (outputClass == SurfaceHolder.class) { try { // This must be called from the UI thread... + LOG.i("onStartBind:", "Waiting on UI thread..."); Tasks.await(Tasks.call(new Callable() { @Override public Void call() { @@ -977,9 +991,9 @@ public class Camera2Engine extends CameraBaseEngine implements @EngineThread private void maybeRestorePreviewTemplateAfterVideo() { int template = (int) mRepeatingRequestBuilder.build().getTag(); - if (template != CameraDevice.TEMPLATE_PREVIEW) { + if (template != getRepeatingRequestDefaultTemplate()) { try { - createRepeatingRequestBuilder(CameraDevice.TEMPLATE_PREVIEW); + createRepeatingRequestBuilder(getRepeatingRequestDefaultTemplate()); addRepeatingRequestBuilderSurfaces(); applyRepeatingRequestBuilder(); } catch (CameraAccessException e) { @@ -1246,9 +1260,8 @@ public class Camera2Engine extends CameraBaseEngine implements public void setZoom(final float zoom, final @Nullable PointF[] points, final boolean notify) { final float old = mZoomValue; mZoomValue = zoom; - // Zoom requests can be high frequency (e.g. linked to touch events), so - // we remove the task before scheduling to avoid stack overflows in orchestrator. - getOrchestrator().remove("zoom"); + // Zoom requests can be high frequency (e.g. linked to touch events), let's trim the oldest. + getOrchestrator().trim("zoom", ALLOWED_ZOOM_OPS); mZoomTask = getOrchestrator().scheduleStateful( "zoom", CameraState.ENGINE, @@ -1305,9 +1318,8 @@ public class Camera2Engine extends CameraBaseEngine implements final boolean notify) { final float old = mExposureCorrectionValue; mExposureCorrectionValue = EVvalue; - // EV requests can be high frequency (e.g. linked to touch events), so - // we remove the task before scheduling to avoid stack overflows in orchestrator. - getOrchestrator().remove("exposure correction"); + // EV requests can be high frequency (e.g. linked to touch events), let's trim the oldest. + getOrchestrator().trim("exposure correction", ALLOWED_EV_OPS); mExposureCorrectionTask = getOrchestrator().scheduleStateful( "exposure correction", CameraState.ENGINE, @@ -1367,14 +1379,13 @@ public class Camera2Engine extends CameraBaseEngine implements protected boolean applyPreviewFrameRate(@NonNull CaptureRequest.Builder builder, float oldPreviewFrameRate) { //noinspection unchecked - Range[] fallback = new Range[]{}; Range[] fpsRanges = readCharacteristic( CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES, - fallback); + new Range[]{}); sortRanges(fpsRanges); if (mPreviewFrameRate == 0F) { // 0F is a special value. Fallback to a reasonable default. - for (Range fpsRange : fpsRanges) { + for (Range fpsRange : filterRanges(fpsRanges)) { if (fpsRange.contains(30) || fpsRange.contains(24)) { builder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange); return true; @@ -1386,7 +1397,7 @@ public class Camera2Engine extends CameraBaseEngine implements mCameraOptions.getPreviewFrameRateMaxValue()); mPreviewFrameRate = Math.max(mPreviewFrameRate, mCameraOptions.getPreviewFrameRateMinValue()); - for (Range fpsRange : fpsRanges) { + for (Range fpsRange : filterRanges(fpsRanges)) { if (fpsRange.contains(Math.round(mPreviewFrameRate))) { builder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange); return true; @@ -1398,23 +1409,32 @@ public class Camera2Engine extends CameraBaseEngine implements } private void sortRanges(Range[] fpsRanges) { - if (getPreviewFrameRateExact() && mPreviewFrameRate != 0) { // sort by range width in ascending order - Arrays.sort(fpsRanges, new Comparator>() { - @Override - public int compare(Range range1, Range range2) { + final boolean ascending = getPreviewFrameRateExact() && mPreviewFrameRate != 0; + Arrays.sort(fpsRanges, new Comparator>() { + @Override + public int compare(Range range1, Range range2) { + if (ascending) { return (range1.getUpper() - range1.getLower()) - (range2.getUpper() - range2.getLower()); - } - }); - } else { // sort by range width in descending order - Arrays.sort(fpsRanges, new Comparator>() { - @Override - public int compare(Range range1, Range range2) { + } else { return (range2.getUpper() - range2.getLower()) - (range1.getUpper() - range1.getLower()); } - }); + } + }); + } + + private List> filterRanges(Range[] fpsRanges) { + List> results = new ArrayList<>(); + int min = Math.round(mCameraOptions.getPreviewFrameRateMinValue()); + int max = Math.round(mCameraOptions.getPreviewFrameRateMaxValue()); + for (Range fpsRange : fpsRanges) { + if (!fpsRange.contains(min)) continue; + if (!fpsRange.contains(max)) continue; + if (!FpsRangeValidator.validate(fpsRange)) continue; + results.add(fpsRange); } + return results; } @Override diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraBaseEngine.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraBaseEngine.java index a751f4b0..07e3c231 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraBaseEngine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraBaseEngine.java @@ -50,6 +50,9 @@ import java.util.List; */ public abstract class CameraBaseEngine extends CameraEngine { + protected final static int ALLOWED_ZOOM_OPS = 20; + protected final static int ALLOWED_EV_OPS = 20; + @SuppressWarnings("WeakerAccess") protected CameraPreview mPreview; @SuppressWarnings("WeakerAccess") protected CameraOptions mCameraOptions; @SuppressWarnings("WeakerAccess") protected PictureRecorder mPictureRecorder; diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/orchestrator/CameraOrchestrator.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/orchestrator/CameraOrchestrator.java index d703eabf..d79f6162 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/orchestrator/CameraOrchestrator.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/orchestrator/CameraOrchestrator.java @@ -1,5 +1,6 @@ package com.otaliastudios.cameraview.engine.orchestrator; +import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -12,9 +13,10 @@ import com.otaliastudios.cameraview.internal.WorkerHandler; import java.util.ArrayDeque; import java.util.ArrayList; -import java.util.HashMap; +import java.util.Collections; +import java.util.HashSet; import java.util.List; -import java.util.Map; +import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.CancellationException; @@ -40,36 +42,43 @@ public class CameraOrchestrator { void handleJobException(@NonNull String job, @NonNull Exception exception); } - protected static class Token { + protected static class Job { public final String name; - public final Task task; + public final TaskCompletionSource source = new TaskCompletionSource<>(); + public final Callable> scheduler; + public final boolean dispatchExceptions; + public final long startTime; - private Token(@NonNull String name, @NonNull Task task) { + private Job(@NonNull String name, @NonNull Callable> scheduler, boolean dispatchExceptions, long startTime) { this.name = name; - this.task = task; - } - - @Override - public boolean equals(@Nullable Object obj) { - return obj instanceof Token && ((Token) obj).name.equals(name); + this.scheduler = scheduler; + this.dispatchExceptions = dispatchExceptions; + this.startTime = startTime; } } protected final Callback mCallback; - protected final ArrayDeque mJobs = new ArrayDeque<>(); - protected final Object mLock = new Object(); - private final Map mDelayedJobs = new HashMap<>(); + protected final ArrayDeque> mJobs = new ArrayDeque<>(); + protected boolean mJobRunning = false; + protected final Object mJobsLock = new Object(); public CameraOrchestrator(@NonNull Callback callback) { mCallback = callback; - ensureToken(); } @NonNull public Task schedule(@NonNull String name, boolean dispatchExceptions, - @NonNull final Runnable job) { - return schedule(name, dispatchExceptions, new Callable>() { + @NonNull Runnable job) { + return scheduleDelayed(name, dispatchExceptions, 0L, job); + } + + @NonNull + public Task scheduleDelayed(@NonNull String name, + boolean dispatchExceptions, + long minDelay, + @NonNull final Runnable job) { + return scheduleInternal(name, dispatchExceptions, minDelay, new Callable>() { @Override public Task call() { job.run(); @@ -78,98 +87,147 @@ public class CameraOrchestrator { }); } - @SuppressWarnings("unchecked") @NonNull - public Task schedule(@NonNull final String name, - final boolean dispatchExceptions, - @NonNull final Callable> job) { + public Task schedule(@NonNull String name, + boolean dispatchExceptions, + @NonNull Callable> scheduler) { + return scheduleInternal(name, dispatchExceptions, 0L, scheduler); + } + + @NonNull + private Task scheduleInternal(@NonNull String name, + boolean dispatchExceptions, + long minDelay, + @NonNull Callable> scheduler) { LOG.i(name.toUpperCase(), "- Scheduling."); - final TaskCompletionSource source = new TaskCompletionSource<>(); - final WorkerHandler handler = mCallback.getJobWorker(name); - synchronized (mLock) { - applyCompletionListener(mJobs.getLast().task, handler, - new OnCompleteListener() { - @Override - public void onComplete(@NonNull Task task) { - synchronized (mLock) { - mJobs.removeFirst(); - ensureToken(); - } - try { - LOG.i(name.toUpperCase(), "- Executing."); - Task inner = job.call(); - applyCompletionListener(inner, handler, new OnCompleteListener() { - @Override - public void onComplete(@NonNull Task task) { - Exception e = task.getException(); - if (e != null) { - LOG.w(name.toUpperCase(), "- Finished with ERROR.", e); - if (dispatchExceptions) { - mCallback.handleJobException(name, e); - } - source.trySetException(e); - } else if (task.isCanceled()) { - LOG.i(name.toUpperCase(), "- Finished because ABORTED."); - source.trySetException(new CancellationException()); - } else { - LOG.i(name.toUpperCase(), "- Finished."); - source.trySetResult(task.getResult()); - } + Job job = new Job<>(name, scheduler, dispatchExceptions, + System.currentTimeMillis() + minDelay); + synchronized (mJobsLock) { + mJobs.addLast(job); + sync(minDelay); + } + return job.source.getTask(); + } + + @GuardedBy("mJobsLock") + private void sync(long after) { + // Jumping on the message handler even if after = 0L should avoid StackOverflow errors. + mCallback.getJobWorker("_sync").post(after, new Runnable() { + @SuppressWarnings("StatementWithEmptyBody") + @Override + public void run() { + Job job = null; + synchronized (mJobsLock) { + if (mJobRunning) { + // Do nothing, job will be picked in executed(). + } else { + long now = System.currentTimeMillis(); + for (Job candidate : mJobs) { + if (candidate.startTime <= now) { + job = candidate; + break; } - }); - } catch (Exception e) { - LOG.i(name.toUpperCase(), "- Finished.", e); - if (dispatchExceptions) mCallback.handleJobException(name, e); - source.trySetException(e); + } + if (job != null) { + mJobRunning = true; + } } } - }); - mJobs.addLast(new Token(name, source.getTask())); - } - return source.getTask(); + // This must be out of mJobsLock! See comments in execute(). + if (job != null) execute(job); + } + }); } - public void scheduleDelayed(@NonNull final String name, - long minDelay, - @NonNull final Runnable runnable) { - Runnable wrapper = new Runnable() { + // Since we use WorkerHandler.run(), the job can end up being executed on the current thread. + // For this reason, it's important that this method is never guarded by mJobsLock! Because + // all threads can be waiting on that, even the UI thread e.g. through scheduleInternal. + private void execute(@NonNull final Job job) { + final WorkerHandler worker = mCallback.getJobWorker(job.name); + worker.run(new Runnable() { @Override public void run() { - schedule(name, true, runnable); - synchronized (mLock) { - if (mDelayedJobs.containsValue(this)) { - mDelayedJobs.remove(name); + try { + LOG.i(job.name.toUpperCase(), "- Executing."); + Task task = job.scheduler.call(); + onComplete(task, worker, new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + Exception e = task.getException(); + if (e != null) { + LOG.w(job.name.toUpperCase(), "- Finished with ERROR.", e); + if (job.dispatchExceptions) { + mCallback.handleJobException(job.name, e); + } + job.source.trySetException(e); + } else if (task.isCanceled()) { + LOG.i(job.name.toUpperCase(), "- Finished because ABORTED."); + job.source.trySetException(new CancellationException()); + } else { + LOG.i(job.name.toUpperCase(), "- Finished."); + job.source.trySetResult(task.getResult()); + } + synchronized (mJobsLock) { + executed(job); + } + } + }); + } catch (Exception e) { + LOG.i(job.name.toUpperCase(), "- Finished with ERROR.", e); + if (job.dispatchExceptions) { + mCallback.handleJobException(job.name, e); + } + job.source.trySetException(e); + synchronized (mJobsLock) { + executed(job); } } } - }; - synchronized (mLock) { - mDelayedJobs.put(name, wrapper); - mCallback.getJobWorker(name).post(minDelay, wrapper); + }); + } + + @GuardedBy("mJobsLock") + private void executed(Job job) { + if (!mJobRunning) { + throw new IllegalStateException("mJobRunning was not true after completing job=" + job.name); } + mJobRunning = false; + mJobs.remove(job); + sync(0L); } public void remove(@NonNull String name) { - synchronized (mLock) { - if (mDelayedJobs.get(name) != null) { - //noinspection ConstantConditions - mCallback.getJobWorker(name).remove(mDelayedJobs.get(name)); - mDelayedJobs.remove(name); + trim(name, 0); + } + + public void trim(@NonNull String name, int allowed) { + synchronized (mJobsLock) { + List> scheduled = new ArrayList<>(); + for (Job job : mJobs) { + if (job.name.equals(name)) { + scheduled.add(job); + } + } + LOG.v("trim: name=", name, "scheduled=", scheduled.size(), "allowed=", allowed); + int existing = Math.max(scheduled.size() - allowed, 0); + if (existing > 0) { + // To remove the oldest ones first, we must reverse the list. + // Note that we will potentially remove a job that is being executed: we don't + // have a mechanism to cancel the ongoing execution, but it shouldn't be a problem. + Collections.reverse(scheduled); + scheduled = scheduled.subList(0, existing); + for (Job job : scheduled) { + mJobs.remove(job); + } } - Token token = new Token(name, Tasks.forResult(null)); - //noinspection StatementWithEmptyBody - while (mJobs.remove(token)) { /* do nothing */ } - ensureToken(); } } public void reset() { - synchronized (mLock) { - List all = new ArrayList<>(); - //noinspection CollectionAddAllCanBeReplacedWithConstructor - all.addAll(mDelayedJobs.keySet()); - for (Token token : mJobs) { - all.add(token.name); + synchronized (mJobsLock) { + Set all = new HashSet<>(); + for (Job job : mJobs) { + all.add(job.name); } for (String job : all) { remove(job); @@ -177,17 +235,9 @@ public class CameraOrchestrator { } } - private void ensureToken() { - synchronized (mLock) { - if (mJobs.isEmpty()) { - mJobs.add(new Token("BASE", Tasks.forResult(null))); - } - } - } - - private static void applyCompletionListener(@NonNull final Task task, - @NonNull WorkerHandler handler, - @NonNull final OnCompleteListener listener) { + private static void onComplete(@NonNull final Task task, + @NonNull WorkerHandler handler, + @NonNull final OnCompleteListener listener) { if (task.isComplete()) { handler.run(new Runnable() { @Override diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/orchestrator/CameraStateOrchestrator.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/orchestrator/CameraStateOrchestrator.java index 0c3d5aff..f55a6622 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/orchestrator/CameraStateOrchestrator.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/orchestrator/CameraStateOrchestrator.java @@ -35,10 +35,10 @@ public class CameraStateOrchestrator extends CameraOrchestrator { } public boolean hasPendingStateChange() { - synchronized (mLock) { - for (Token token : mJobs) { - if ((token.name.contains(" >> ") || token.name.contains(" << ")) - && !token.task.isComplete()) { + synchronized (mJobsLock) { + for (Job job : mJobs) { + if ((job.name.contains(" >> ") || job.name.contains(" << ")) + && !job.source.getTask().isComplete()) { return true; } } @@ -107,7 +107,7 @@ public class CameraStateOrchestrator extends CameraOrchestrator { @NonNull final CameraState atLeast, long delay, @NonNull final Runnable job) { - scheduleDelayed(name, delay, new Runnable() { + scheduleDelayed(name, true, delay, new Runnable() { @Override public void run() { if (getCurrentState().isAtLeast(atLeast)) { diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/FpsRangeValidator.java b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/FpsRangeValidator.java new file mode 100644 index 00000000..16e57096 --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/FpsRangeValidator.java @@ -0,0 +1,38 @@ +package com.otaliastudios.cameraview.internal; + +import android.os.Build; +import android.util.Log; +import android.util.Range; + +import androidx.annotation.RequiresApi; + +import com.otaliastudios.cameraview.CameraLogger; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@RequiresApi(21) +public class FpsRangeValidator { + + private final static CameraLogger LOG = CameraLogger.create("FpsRangeValidator"); + private final static Map>> sIssues = new HashMap<>(); + + static { + sIssues.put("Google Pixel 4", Arrays.asList(new Range<>(15, 60))); + } + + public static boolean validate(Range range) { + LOG.i("Build.MODEL:", Build.MODEL, "Build.BRAND:", Build.BRAND, "Build.MANUFACTURER:", Build.MANUFACTURER); + String descriptor = Build.MANUFACTURER + " " + Build.MODEL; + List> ranges = sIssues.get(descriptor); + if (ranges != null && ranges.contains(range)) { + LOG.i("Dropping range:", range); + return false; + } + return true; + } +} diff --git a/demo/src/main/kotlin/com/otaliastudios/cameraview/demo/CameraActivity.kt b/demo/src/main/kotlin/com/otaliastudios/cameraview/demo/CameraActivity.kt index 2e245562..5c5311ce 100644 --- a/demo/src/main/kotlin/com/otaliastudios/cameraview/demo/CameraActivity.kt +++ b/demo/src/main/kotlin/com/otaliastudios/cameraview/demo/CameraActivity.kt @@ -28,7 +28,7 @@ class CameraActivity : AppCompatActivity(), View.OnClickListener, OptionView.Cal companion object { private val LOG = CameraLogger.create("DemoApp") - private const val USE_FRAME_PROCESSOR = true + private const val USE_FRAME_PROCESSOR = false private const val DECODE_BITMAP = false }