feat: Synchronize Frame properly (#2501)

* feat: Synchronize `Frame` properly

* Update CameraError.ts

* Image is not valid if `refCount` < 0
This commit is contained in:
Marc Rousavy 2024-02-05 12:34:32 +01:00 committed by GitHub
parent 97168c647c
commit d8c95c901f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 71 additions and 58 deletions

View File

@ -113,6 +113,16 @@ class RecordingInProgressError :
"recording-in-progress", "recording-in-progress",
"There is already an active video recording in progress! Did you call startRecording() twice?" "There is already an active video recording in progress! Did you call startRecording() twice?"
) )
class FrameInvalidError :
CameraError(
"capture",
"frame-invalid",
"Trying to access an already closed Frame! " +
"Are you trying to access the Image data outside of a Frame Processor's lifetime?\n" +
"- If you want to use `console.log(frame)`, use `console.log(frame.toString())` instead.\n" +
"- If you want to do async processing, use `runAsync(...)` instead.\n" +
"- If you want to use runOnJS, increment it's ref-count: `frame.incrementRefCount()`"
)
class CodeTypeNotSupportedError(codeType: String) : class CodeTypeNotSupportedError(codeType: String) :
CameraError( CameraError(

View File

@ -4,6 +4,7 @@ import android.hardware.HardwareBuffer;
import android.media.Image; import android.media.Image;
import android.os.Build; import android.os.Build;
import com.facebook.proguard.annotations.DoNotStrip; import com.facebook.proguard.annotations.DoNotStrip;
import com.mrousavy.camera.core.FrameInvalidError;
import com.mrousavy.camera.core.HardwareBuffersNotAvailableError; import com.mrousavy.camera.core.HardwareBuffersNotAvailableError;
import com.mrousavy.camera.types.PixelFormat; import com.mrousavy.camera.types.PixelFormat;
import com.mrousavy.camera.types.Orientation; import com.mrousavy.camera.types.Orientation;
@ -23,42 +24,17 @@ public class Frame {
this.isMirrored = isMirrored; this.isMirrored = isMirrored;
} }
public Image getImage() { private void assertIsValid() throws FrameInvalidError {
synchronized (this) { if (!getIsImageValid(image)) {
Image img = image; throw new FrameInvalidError();
if (!getIsImageValid(img)) {
throw new RuntimeException("Frame is already closed! " +
"Are you trying to access the Image data outside of a Frame Processor's lifetime?\n" +
"- If you want to use `console.log(frame)`, use `console.log(frame.toString())` instead.\n" +
"- If you want to do async processing, use `runAsync(...)` instead.\n" +
"- If you want to use runOnJS, increment it's ref-count: `frame.incrementRefCount()`");
}
return img;
} }
} }
@SuppressWarnings("unused") private synchronized boolean getIsImageValid(Image image) {
@DoNotStrip if (refCount <= 0) return false;
public int getWidth() {
return getImage().getWidth();
}
@SuppressWarnings("unused")
@DoNotStrip
public int getHeight() {
return getImage().getHeight();
}
@SuppressWarnings("unused")
@DoNotStrip
public boolean getIsValid() {
return getIsImageValid(getImage());
}
private boolean getIsImageValid(Image image) {
try { try {
// will throw an exception if the image is already closed // will throw an exception if the image is already closed
synchronized (this) { image.getFormat(); } image.getFormat();
// no exception thrown, image must still be valid. // no exception thrown, image must still be valid.
return true; return true;
} catch (IllegalStateException e) { } catch (IllegalStateException e) {
@ -67,78 +43,104 @@ public class Frame {
} }
} }
public synchronized Image getImage() {
return image;
}
@SuppressWarnings("unused") @SuppressWarnings("unused")
@DoNotStrip @DoNotStrip
public boolean getIsMirrored() { public synchronized int getWidth() throws FrameInvalidError {
assertIsValid();
return image.getWidth();
}
@SuppressWarnings("unused")
@DoNotStrip
public synchronized int getHeight() throws FrameInvalidError {
assertIsValid();
return image.getHeight();
}
@SuppressWarnings("unused")
@DoNotStrip
public synchronized boolean getIsValid() throws FrameInvalidError {
assertIsValid();
return getIsImageValid(image);
}
@SuppressWarnings("unused")
@DoNotStrip
public synchronized boolean getIsMirrored() throws FrameInvalidError {
assertIsValid();
return isMirrored; return isMirrored;
} }
@SuppressWarnings("unused") @SuppressWarnings("unused")
@DoNotStrip @DoNotStrip
public long getTimestamp() { public synchronized long getTimestamp() throws FrameInvalidError {
assertIsValid();
return timestamp; return timestamp;
} }
@SuppressWarnings("unused") @SuppressWarnings("unused")
@DoNotStrip @DoNotStrip
public Orientation getOrientation() { public synchronized Orientation getOrientation() throws FrameInvalidError {
assertIsValid();
return orientation; return orientation;
} }
@SuppressWarnings("unused") @SuppressWarnings("unused")
@DoNotStrip @DoNotStrip
public PixelFormat getPixelFormat() { public synchronized PixelFormat getPixelFormat() throws FrameInvalidError {
return PixelFormat.Companion.fromImageFormat(getImage().getFormat()); assertIsValid();
return PixelFormat.Companion.fromImageFormat(image.getFormat());
} }
@SuppressWarnings("unused") @SuppressWarnings("unused")
@DoNotStrip @DoNotStrip
public int getPlanesCount() { public synchronized int getPlanesCount() throws FrameInvalidError {
return getImage().getPlanes().length; assertIsValid();
return image.getPlanes().length;
} }
@SuppressWarnings("unused") @SuppressWarnings("unused")
@DoNotStrip @DoNotStrip
public int getBytesPerRow() { public synchronized int getBytesPerRow() throws FrameInvalidError {
return getImage().getPlanes()[0].getRowStride(); assertIsValid();
return image.getPlanes()[0].getRowStride();
} }
@SuppressWarnings("unused") @SuppressWarnings("unused")
@DoNotStrip @DoNotStrip
public Object getHardwareBufferBoxed() throws HardwareBuffersNotAvailableError { private Object getHardwareBufferBoxed() throws HardwareBuffersNotAvailableError, FrameInvalidError {
return getHardwareBuffer(); return getHardwareBuffer();
} }
public HardwareBuffer getHardwareBuffer() throws HardwareBuffersNotAvailableError { public synchronized HardwareBuffer getHardwareBuffer() throws HardwareBuffersNotAvailableError, FrameInvalidError {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
throw new HardwareBuffersNotAvailableError(); throw new HardwareBuffersNotAvailableError();
} }
return getImage().getHardwareBuffer(); assertIsValid();
return image.getHardwareBuffer();
} }
@SuppressWarnings("unused") @SuppressWarnings("unused")
@DoNotStrip @DoNotStrip
public void incrementRefCount() { public synchronized void incrementRefCount() {
synchronized (this) {
refCount++; refCount++;
} }
}
@SuppressWarnings("unused") @SuppressWarnings("unused")
@DoNotStrip @DoNotStrip
public void decrementRefCount() { public synchronized void decrementRefCount() {
synchronized (this) {
refCount--; refCount--;
if (refCount <= 0) { if (refCount <= 0) {
// If no reference is held on this Image, close it. // If no reference is held on this Image, close it.
close(); close();
} }
} }
}
private void close() { private synchronized void close() {
synchronized (this) {
image.close(); image.close();
} }
}
} }

View File

@ -40,6 +40,7 @@ export type CaptureError =
| 'capture/recorder-error' | 'capture/recorder-error'
| 'capture/video-not-enabled' | 'capture/video-not-enabled'
| 'capture/photo-not-enabled' | 'capture/photo-not-enabled'
| 'capture/frame-invalid'
| 'capture/aborted' | 'capture/aborted'
| 'capture/unknown' | 'capture/unknown'
export type SystemError = export type SystemError =