Skip to content

Commit b1d183a

Browse files
authored
Fix a hardware acceleration bug, disable SurfaceViewPreview features (#54)
* Layout changes * SurfaceView onSurfaceCreated is too early. Fix * Add logs * Refactor some classes, fix hardware acceleration bug, disable cropping for SurfaceView * Update README with known limitations
1 parent 1be4a31 commit b1d183a

File tree

23 files changed

+305
-190
lines changed

23 files changed

+305
-190
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ compile 'com.otaliastudios:cameraview:1.2.1'
5757
- [Permissions Behavior](#permissions-behavior)
5858
- [Manifest file](#manifest-file)
5959
- [Roadmap](#roadmap)
60+
- [Device-specific issues](#device-specific-issues)
6061

6162
## Usage
6263

@@ -526,3 +527,11 @@ These are still things that need to be done, off the top of my head:
526527
- [ ] add onRequestPermissionResults for easy permission callback
527528
- [ ] better error handling, maybe with a onError(e) method in the public listener, or have each public method return a boolean
528529
- [ ] decent code coverage
530+
531+
## Device-specific issues
532+
533+
There are a couple of known issues if you are working with certain devices. The emulator is one of
534+
the most tricky in this sense.
535+
536+
- Devices, or activities, with hardware acceleration turned off: this can be the case with emulators. In this case we will use SurfaceView as our surface provider. That is intrinsically flawed and can't deal with all we want to do here (runtime layout changes, scaling, etc.). So, nothing to do in this case.
537+
- Devices with no support for MediaRecorder: the emulator does not support it, officially. This means that video/audio recording is flawed. Again, not our fault.

cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraCallbacksTest.java

Lines changed: 38 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,7 @@
44
import android.content.Context;
55
import android.graphics.Bitmap;
66
import android.graphics.BitmapFactory;
7-
import android.graphics.Color;
87
import android.graphics.PointF;
9-
import android.graphics.Rect;
10-
import android.graphics.YuvImage;
118
import android.support.test.filters.MediumTest;
129
import android.support.test.runner.AndroidJUnit4;
1310
import android.view.ViewGroup;
@@ -18,9 +15,7 @@
1815
import org.junit.runner.RunWith;
1916
import org.mockito.invocation.InvocationOnMock;
2017
import org.mockito.stubbing.Answer;
21-
22-
import java.io.ByteArrayOutputStream;
23-
import java.io.OutputStream;
18+
import org.mockito.stubbing.Stubber;
2419

2520
import static junit.framework.Assert.assertNotNull;
2621
import static org.junit.Assert.assertEquals;
@@ -33,19 +28,18 @@
3328
import static org.mockito.Mockito.doAnswer;
3429
import static org.mockito.Mockito.mock;
3530
import static org.mockito.Mockito.never;
31+
import static org.mockito.Mockito.reset;
3632
import static org.mockito.Mockito.times;
3733
import static org.mockito.Mockito.verify;
38-
import static org.mockito.Mockito.when;
3934

4035
@RunWith(AndroidJUnit4.class)
4136
@MediumTest
4237
public class CameraCallbacksTest extends BaseTest {
4338

4439
private CameraView camera;
45-
private CameraView.CameraCallbacks callbacks;
4640
private CameraListener listener;
4741
private MockCameraController mockController;
48-
private MockPreview mockPreview;
42+
private MockCameraPreview mockPreview;
4943
private Task<Boolean> task;
5044

5145

@@ -58,14 +52,14 @@ public void run() {
5852
listener = mock(CameraListener.class);
5953
camera = new CameraView(context) {
6054
@Override
61-
protected CameraController instantiateCameraController(CameraCallbacks callbacks, Preview preview) {
62-
mockController = new MockCameraController(callbacks, preview);
55+
protected CameraController instantiateCameraController(CameraCallbacks callbacks) {
56+
mockController = new MockCameraController(callbacks);
6357
return mockController;
6458
}
6559

6660
@Override
67-
protected Preview instantiatePreview(Context context, ViewGroup container) {
68-
mockPreview = new MockPreview(context, container);
61+
protected CameraPreview instantiatePreview(Context context, ViewGroup container) {
62+
mockPreview = new MockCameraPreview(context, container);
6963
return mockPreview;
7064
}
7165

@@ -74,8 +68,8 @@ protected boolean checkPermissions(SessionType sessionType, Audio audio) {
7468
return true;
7569
}
7670
};
71+
camera.instantiatePreview();
7772
camera.addCameraListener(listener);
78-
callbacks = camera.mCameraCallbacks;
7973
task = new Task<>();
8074
task.listen();
8175
}
@@ -87,26 +81,25 @@ public void tearDown() {
8781
camera = null;
8882
mockController = null;
8983
mockPreview = null;
90-
callbacks = null;
9184
listener = null;
9285
}
9386

9487
// Completes our task.
95-
private Answer completeTask() {
96-
return new Answer() {
88+
private Stubber completeTask() {
89+
return doAnswer(new Answer() {
9790
@Override
9891
public Object answer(InvocationOnMock invocation) throws Throwable {
9992
task.end(true);
10093
return null;
10194
}
102-
};
95+
});
10396
}
10497

10598
@Test
10699
public void testDontDispatchIfRemoved() {
107100
camera.removeCameraListener(listener);
108-
doAnswer(completeTask()).when(listener).onCameraOpened(null);
109-
callbacks.dispatchOnCameraOpened(null);
101+
completeTask().when(listener).onCameraOpened(null);
102+
camera.mCameraCallbacks.dispatchOnCameraOpened(null);
110103

111104
assertNull(task.await(200));
112105
verify(listener, never()).onCameraOpened(null);
@@ -115,53 +108,53 @@ public void testDontDispatchIfRemoved() {
115108
@Test
116109
public void testDontDispatchIfCleared() {
117110
camera.clearCameraListeners();
118-
doAnswer(completeTask()).when(listener).onCameraOpened(null);
119-
callbacks.dispatchOnCameraOpened(null);
111+
completeTask().when(listener).onCameraOpened(null);
112+
camera.mCameraCallbacks.dispatchOnCameraOpened(null);
120113

121114
assertNull(task.await(200));
122115
verify(listener, never()).onCameraOpened(null);
123116
}
124117

125118
@Test
126119
public void testDispatchOnCameraOpened() {
127-
doAnswer(completeTask()).when(listener).onCameraOpened(null);
128-
callbacks.dispatchOnCameraOpened(null);
120+
completeTask().when(listener).onCameraOpened(null);
121+
camera.mCameraCallbacks.dispatchOnCameraOpened(null);
129122

130123
assertNotNull(task.await(200));
131124
verify(listener, times(1)).onCameraOpened(null);
132125
}
133126

134127
@Test
135128
public void testDispatchOnCameraClosed() {
136-
doAnswer(completeTask()).when(listener).onCameraClosed();
137-
callbacks.dispatchOnCameraClosed();
129+
completeTask().when(listener).onCameraClosed();
130+
camera.mCameraCallbacks.dispatchOnCameraClosed();
138131

139132
assertNotNull(task.await(200));
140133
verify(listener, times(1)).onCameraClosed();
141134
}
142135

143136
@Test
144137
public void testDispatchOnVideoTaken() {
145-
doAnswer(completeTask()).when(listener).onVideoTaken(null);
146-
callbacks.dispatchOnVideoTaken(null);
138+
completeTask().when(listener).onVideoTaken(null);
139+
camera.mCameraCallbacks.dispatchOnVideoTaken(null);
147140

148141
assertNotNull(task.await(200));
149142
verify(listener, times(1)).onVideoTaken(null);
150143
}
151144

152145
@Test
153146
public void testDispatchOnZoomChanged() {
154-
doAnswer(completeTask()).when(listener).onZoomChanged(anyFloat(), any(float[].class), any(PointF[].class));
155-
callbacks.dispatchOnZoomChanged(0f, null);
147+
completeTask().when(listener).onZoomChanged(anyFloat(), any(float[].class), any(PointF[].class));
148+
camera.mCameraCallbacks.dispatchOnZoomChanged(0f, null);
156149

157150
assertNotNull(task.await(200));
158151
verify(listener, times(1)).onZoomChanged(anyFloat(), any(float[].class), any(PointF[].class));
159152
}
160153

161154
@Test
162155
public void testDispatchOnExposureCorrectionChanged() {
163-
doAnswer(completeTask()).when(listener).onExposureCorrectionChanged(0f, null, null);
164-
callbacks.dispatchOnExposureCorrectionChanged(0f, null, null);
156+
completeTask().when(listener).onExposureCorrectionChanged(0f, null, null);
157+
camera.mCameraCallbacks.dispatchOnExposureCorrectionChanged(0f, null, null);
165158

166159
assertNotNull(task.await(200));
167160
verify(listener, times(1)).onExposureCorrectionChanged(0f, null, null);
@@ -174,8 +167,8 @@ public void testDispatchOnFocusStart() {
174167
camera.mapGesture(Gesture.TAP, GestureAction.FOCUS_WITH_MARKER);
175168

176169
PointF point = new PointF();
177-
doAnswer(completeTask()).when(listener).onFocusStart(point);
178-
callbacks.dispatchOnFocusStart(Gesture.TAP, point);
170+
completeTask().when(listener).onFocusStart(point);
171+
camera.mCameraCallbacks.dispatchOnFocusStart(Gesture.TAP, point);
179172

180173
assertNotNull(task.await(200));
181174
verify(listener, times(1)).onFocusStart(point);
@@ -190,8 +183,8 @@ public void testDispatchOnFocusEnd() {
190183

191184
PointF point = new PointF();
192185
boolean success = true;
193-
doAnswer(completeTask()).when(listener).onFocusEnd(success, point);
194-
callbacks.dispatchOnFocusEnd(Gesture.TAP, success, point);
186+
completeTask().when(listener).onFocusEnd(success, point);
187+
camera.mCameraCallbacks.dispatchOnFocusEnd(Gesture.TAP, success, point);
195188

196189
assertNotNull(task.await(200));
197190
verify(listener, times(1)).onFocusEnd(success, point);
@@ -200,31 +193,31 @@ public void testDispatchOnFocusEnd() {
200193

201194
@Test
202195
public void testOrientationCallbacks_deviceOnly() {
203-
doAnswer(completeTask()).when(listener).onOrientationChanged(anyInt());
196+
completeTask().when(listener).onOrientationChanged(anyInt());
204197

205198
// Assert not called. Both methods must be called.
206-
callbacks.onDeviceOrientationChanged(0);
199+
camera.mCameraCallbacks.onDeviceOrientationChanged(0);
207200
assertNull(task.await(200));
208201
verify(listener, never()).onOrientationChanged(anyInt());
209202
}
210203

211204
@Test
212205
public void testOrientationCallbacks_displayOnly() {
213-
doAnswer(completeTask()).when(listener).onOrientationChanged(anyInt());
206+
completeTask().when(listener).onOrientationChanged(anyInt());
214207

215208
// Assert not called. Both methods must be called.
216-
callbacks.onDisplayOffsetChanged(0);
209+
camera.mCameraCallbacks.onDisplayOffsetChanged(0);
217210
assertNull(task.await(200));
218211
verify(listener, never()).onOrientationChanged(anyInt());
219212
}
220213

221214
@Test
222215
public void testOrientationCallbacks_both() {
223-
doAnswer(completeTask()).when(listener).onOrientationChanged(anyInt());
216+
completeTask().when(listener).onOrientationChanged(anyInt());
224217

225218
// Assert called.
226-
callbacks.onDisplayOffsetChanged(0);
227-
callbacks.onDeviceOrientationChanged(90);
219+
camera.mCameraCallbacks.onDisplayOffsetChanged(0);
220+
camera.mCameraCallbacks.onDeviceOrientationChanged(90);
228221
assertNotNull(task.await(200));
229222
verify(listener, times(1)).onOrientationChanged(anyInt());
230223
}
@@ -290,9 +283,9 @@ public Object answer(InvocationOnMock invocation) throws Throwable {
290283

291284
// Create fake JPEG array and trigger the process.
292285
if (jpeg) {
293-
callbacks.processImage(mockJpeg(imageDim[0], imageDim[1]), true, false);
286+
camera.mCameraCallbacks.processImage(mockJpeg(imageDim[0], imageDim[1]), true, false);
294287
} else {
295-
callbacks.processSnapshot(mockYuv(imageDim[0], imageDim[1]), true, false);
288+
camera.mCameraCallbacks.processSnapshot(mockYuv(imageDim[0], imageDim[1]), true, false);
296289
}
297290

298291
// Wait for result and get out dimensions.

cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public class CameraViewTest extends BaseTest {
2626

2727
private CameraView cameraView;
2828
private MockCameraController mockController;
29-
private Preview mockPreview;
29+
private CameraPreview mockPreview;
3030
private boolean hasPermissions;
3131

3232
@Before
@@ -37,14 +37,14 @@ public void run() {
3737
Context context = context();
3838
cameraView = new CameraView(context) {
3939
@Override
40-
protected CameraController instantiateCameraController(CameraCallbacks callbacks, Preview preview) {
41-
mockController = new MockCameraController(callbacks, preview);
40+
protected CameraController instantiateCameraController(CameraCallbacks callbacks) {
41+
mockController = new MockCameraController(callbacks);
4242
return mockController;
4343
}
4444

4545
@Override
46-
protected Preview instantiatePreview(Context context, ViewGroup container) {
47-
mockPreview = new MockPreview(context, container);
46+
protected CameraPreview instantiatePreview(Context context, ViewGroup container) {
47+
mockPreview = new MockCameraPreview(context, container);
4848
return mockPreview;
4949
}
5050

@@ -53,6 +53,8 @@ protected boolean checkPermissions(SessionType sessionType, Audio audio) {
5353
return hasPermissions;
5454
}
5555
};
56+
// Instantiate preview now.
57+
cameraView.instantiatePreview();
5658
}
5759
});
5860
}

cameraview/src/androidTest/java/com/otaliastudios/cameraview/IntegrationTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ public void setUp() {
5656
public void run() {
5757
camera = new CameraView(rule.getActivity()) {
5858
@Override
59-
protected CameraController instantiateCameraController(CameraCallbacks callbacks, Preview preview) {
60-
controller = new Camera1(callbacks, preview);
59+
protected CameraController instantiateCameraController(CameraCallbacks callbacks) {
60+
controller = new Camera1(callbacks);
6161
return controller;
6262
}
6363
};

cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockCameraController.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ public class MockCameraController extends CameraController {
1515
boolean mZoomChanged;
1616
boolean mExposureCorrectionChanged;
1717

18-
MockCameraController(CameraView.CameraCallbacks callback, Preview preview) {
19-
super(callback, preview);
18+
MockCameraController(CameraView.CameraCallbacks callback) {
19+
super(callback);
2020
}
2121

2222
void setMockCameraOptions(CameraOptions options) {

cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockPreview.java renamed to cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockCameraPreview.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,21 @@
77
import android.view.View;
88
import android.view.ViewGroup;
99

10-
public class MockPreview extends Preview<View, Void> {
10+
public class MockCameraPreview extends CameraPreview<View, Void> {
1111

12-
MockPreview(Context context, ViewGroup parent) {
12+
MockCameraPreview(Context context, ViewGroup parent) {
1313
super(context, parent, null);
1414
}
1515

16+
private boolean mCropping = false;
17+
1618
public void setIsCropping(boolean crop) {
17-
getView().setScaleX(crop ? 2 : 1);
18-
getView().setScaleY(crop ? 2 : 1);
19+
mCropping = crop;
20+
}
21+
22+
@Override
23+
boolean isCropping() {
24+
return mCropping;
1925
}
2026

2127
@NonNull

cameraview/src/androidTest/java/com/otaliastudios/cameraview/PreviewTest.java

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,14 @@
1919

2020
public abstract class PreviewTest extends BaseTest {
2121

22-
protected abstract Preview createPreview(Context context, ViewGroup parent, Preview.SurfaceCallback callback);
22+
protected abstract CameraPreview createPreview(Context context, ViewGroup parent, CameraPreview.SurfaceCallback callback);
2323

2424
@Rule
2525
public ActivityTestRule<TestActivity> rule = new ActivityTestRule<>(TestActivity.class);
2626

27-
protected Preview preview;
27+
protected CameraPreview preview;
2828
protected Size surfaceSize;
29-
private Preview.SurfaceCallback callback;
29+
private CameraPreview.SurfaceCallback callback;
3030
private Task<Boolean> availability;
3131

3232
@Before
@@ -40,7 +40,7 @@ public void run() {
4040
TestActivity a = rule.getActivity();
4141
surfaceSize = a.getContentSize();
4242

43-
callback = mock(Preview.SurfaceCallback.class);
43+
callback = mock(CameraPreview.SurfaceCallback.class);
4444
doAnswer(new Answer() {
4545
@Override
4646
public Object answer(InvocationOnMock invocation) throws Throwable {
@@ -121,8 +121,10 @@ public void testCropCenter() throws Exception {
121121

122122
// If we apply a different aspect ratio, there should be cropping.
123123
float desired = view * 1.2f;
124-
setDesiredAspectRatio(desired);
125-
assertTrue(preview.isCropping());
124+
if (preview.supportsCropping()) {
125+
setDesiredAspectRatio(desired);
126+
assertTrue(preview.isCropping());
127+
}
126128

127129
// Since desired is 'desired', let's fake a new view size that is consistent with it.
128130
// Ensure crop is not happening anymore.

0 commit comments

Comments
 (0)