Compare commits

...

100 Commits

Author SHA1 Message Date
DeweyReed e26ac950be
Fix the setSize typo (#1198) 2 years ago
Dmitry Naymushin ae3f3da0e1
A couple of fixes and docs improvements (#1184) 2 years ago
Steve Xu 82ea49f6e2
#1088: add Pixel 4a into FpsRangeValidator (#1164) 3 years ago
Mattia Iavarone 0057981408
Release v2.7.2 (#1140) 3 years ago
Mattia Iavarone 399844321b
Fix orientation when activity handles rotations (#1117) 3 years ago
Mattia Iavarone c2e02923de Fix sonatype build 3 years ago
Mattia Iavarone 994b1dd434
Release 2.7.1 (#1111) 3 years ago
Jan Šmuk ceb0eca394
Add Pixel 4a into FpsRangeValidator (#1089) 4 years ago
Ryan Loebs bde88a946e
Add option to capture hardware accelerated overlays (#1066) 4 years ago
Ryan Loebs f608ec3918
Add flag to disable automatic gesture handling (#1068) 4 years ago
Mattia Iavarone a219073468
Release v2.7.0 (#1040) 4 years ago
Ezequiel Adrián d89491195c
Implement Shutter callback. Add GestureAction.TAKE_PICTURE_SNAPSHOT. (#1030) 4 years ago
Michael 5d10277d7b
add try catch to catch exceptions when takePicture fails (#1024) 4 years ago
Mattia Iavarone 0f6ab20c3e
Make filterFrameRateRanges protected (#1026) 4 years ago
Billy Sutomo a9fcd6506e
Update frame-processing.md (#941) 4 years ago
Philippe Banwarth 6aa2d3a489
Log errors when writing the image to a file (#960) 4 years ago
Mattia Iavarone 40ace54c19
Fix preview on Pixel devices, fix deadlock with surface preview (#1020) 4 years ago
Mattia Iavarone 2429cc114b
Add CameraEngine.getRepeatingRequestDefaultTemplate (#1004) 4 years ago
Mattia Iavarone 0001ab7a54
Rewrite orchestrator (#992) 4 years ago
Mattia Iavarone e3fcef286f
Release v2.6.4 (#954) 4 years ago
Mattia Iavarone 8207e67679
Some bug fixes (#953) 4 years ago
thijsonline daf7a0bf44
Fix for #927 OrientationHelper.enable() called twice (#928) 4 years ago
Steve 170d87ed39
Fix references on overlay documentation (#931) 4 years ago
Mattia Iavarone a7cf726636 Fix isMinifyEnabled 4 years ago
Mattia Iavarone 610f511a76
Release v2.6.3 (#912) 4 years ago
Mattia Iavarone a7c18324a5
Build tools update (#911) 4 years ago
Alexander 66c37373bd
Wrapped some crashes which happened on start camera and which connect with params. (#897) 4 years ago
Alexander 8455c23fb1
Wrapped some crashes which connect with empty camera params (#851) 4 years ago
jeffreyfjohnson b5921ec6bf
Restart Camera1 Frame Processing After Taking a Picture - Fixes Issue 824 (#877) 4 years ago
Ryan McClarnon 4364ec7c3a
Add capability to set audio codec to encode video files with. (#861) 5 years ago
Mattia Iavarone edbade24a9
Preview interface changes (#816) 5 years ago
Alexander 5530c011fa
Add ability to clear lifecycleObserver (#798) 5 years ago
Mattia Iavarone 778cdedc00
Release v2.6.2 (#794) 5 years ago
dependabot[bot] fa26b2a654
Bump nokogiri from 1.10.4 to 1.10.8 in /docs (#786) 5 years ago
Mattia Iavarone 124aee6908 Make LogAction log to info 5 years ago
Alexander 218b27319d
Add setter for flag enabled/disabled auto check permissions (#775) 5 years ago
Alexander 858495afde
Make CameraLogger ThreadSafe (#779) 5 years ago
Mattia Iavarone aa95d19902 Ads 5 years ago
Mattia Iavarone 968d181af4
GL cleanup (#766) 5 years ago
dependabot[bot] 7f23c309ab Bump rubyzip from 1.2.2 to 2.0.0 in /docs (#759) 5 years ago
hualong-shen 24b02caa28 Set accurate fps if possible (#754) 5 years ago
Mattia Iavarone e207e452bf Website mobile improvements 5 years ago
Mattia Iavarone 83bb45108a
Frame rotation improvements (#745) 5 years ago
Mattia Iavarone 1bb15dc595
Update bug_report.md 5 years ago
Mattia Iavarone 949ed22ea6
Release v2.6.1 (#744) 5 years ago
Mattia Iavarone 141902c6cb
Metering improvements (#741) 5 years ago
Sewar 1a88cd09f4 Add support to record video to FileDescriptor. (#732) 5 years ago
Mattia Iavarone e6ec1a15ac Fix page.html 5 years ago
Mattia Iavarone c0dab05063 Use Jekyll collections 5 years ago
Mattia Iavarone ef9a68ec6a Website improvements 5 years ago
Mattia Iavarone adf54cb029 Fix site header (3) 5 years ago
Mattia Iavarone 0f14fbd4e8 Fix site header (2) 5 years ago
Mattia Iavarone afd02f251b Fix site header 5 years ago
Mattia Iavarone 3c4d01c22f
Improve website (#734) 5 years ago
Mattia Iavarone 9c984f0c21
Update 2018-12-20-changelog.md 5 years ago
Mattia Iavarone 0eacd8c9b7
Release v2.6.0 (#725) 5 years ago
Mattia Iavarone df139994d7
Add startAutoFocus(Rect) (#724) 5 years ago
Mattia Iavarone 9b916f12fd Analytics 5 years ago
Mattia Iavarone 4afb82f1c2 Verification 5 years ago
Mattia Iavarone bdce730659 Website landing page (5) 5 years ago
Mattia Iavarone e2a4789d77 Website landing page (4) 5 years ago
Mattia Iavarone 0553fa9d20 Website landing page (3) 5 years ago
Mattia Iavarone 9b94fd289c Website landing page 5 years ago
Mattia Iavarone 14e777e7c7 Website landing page 5 years ago
Mattia Iavarone 821a21a1bd Fix changelog 5 years ago
Mattia Iavarone 4cf55714c5 Fix website links 5 years ago
Mattia Iavarone 840e05a3e8
Permission flag (#718) 5 years ago
Mattia Iavarone ce9ca64370
Frame processing threading APIs (#716) 5 years ago
Mattia Iavarone 097420a96b
Release v2.5.0 (#708) 5 years ago
Mattia Iavarone e1721bb77d
Frame Processing maxWidth, maxHeight and format (#704) 5 years ago
Mattia Iavarone 4a6b9be905
Testing and stability improvements (#696) 5 years ago
Mattia Iavarone 3db6fd3fc0
Rewrite engine threading (#697) 5 years ago
Mattia Iavarone fd17a8339e
Update README.md 5 years ago
Mattia Iavarone 859904ec6a
CI improvements (#695) 5 years ago
Mattia Iavarone bb3b23e69a
RAW support (#691) 5 years ago
Mattia Iavarone 79d00d5eba
Drop Travis and move to GitHub actions (#694) 5 years ago
Mattia Iavarone 129b43e657
Release v2.4.0 (#662) 5 years ago
Mattia Iavarone 218aa9d108
Preview FPS docs + Camera1 implementation (#661) 5 years ago
Vaibhav Bhandula fa88783d37 Support for higher frame rate (#653) 5 years ago
Mattia Iavarone 74c94e21ce
Add GitHub Sponsors link (#659) 5 years ago
Mattia Iavarone a63cd84726
Bug fixes (#651) 5 years ago
Mattia Iavarone 063c7a76fb
Update 2018-12-20-changelog.md 5 years ago
Mattia Iavarone 55e7a26278
Fix bugs (#630) 5 years ago
Suneet Agrawal 19f607a959 Move snapshot video start callback to onStart (#632) 5 years ago
Mattia Iavarone 824abbc55a
takeVideo improvements (#617) 5 years ago
Mattia Iavarone 5b92e81b83
Release v2.3.0 (#608) 5 years ago
Mattia Iavarone 5619dbdca5
Small improvements (#588) 5 years ago
Mattia Iavarone 4ddd2af731
Metering APIs (#580) 5 years ago
Mattia Iavarone a8fddc482f
Focus improvements (#574) 5 years ago
Mattia Iavarone 0731b64c2f
Release v2.2.0 (#573) 5 years ago
Mattia Iavarone c8d03ee116
Update 2018-12-20-frame-processing.md 5 years ago
Mattia Iavarone eddae18caa
Frame improvements (#572) 5 years ago
Mattia Iavarone e5fb4fa8fa
Update bug_report.md 5 years ago
Mattia Iavarone c36d2ce996
README changes (#565) 5 years ago
Mattia Iavarone d333348ce6
MultiFilter to merge filters (#559) 5 years ago
Mattia Iavarone 83307c527e
Fix layout editor preview (#564) 5 years ago
Mattia Iavarone f2ea77ce79
Encoders improvements (#545) 5 years ago
dependabot[bot] f48d1c17ba Bump nokogiri from 1.9.1 to 1.10.4 in /docs (#562) 5 years ago
Mattia Iavarone eeec7ac222
Video bugs (#551) 5 years ago
Mattia Iavarone 445b742455
Add SimpleFilter (#552) 5 years ago
  1. 2
      .github/CONTRIBUTING.md
  2. 2
      .github/FUNDING.yml
  3. 21
      .github/ISSUE_TEMPLATE/bug_report.md
  4. 2
      .github/ISSUE_TEMPLATE/question.md
  5. 12
      .github/pull_request_template.md
  6. 119
      .github/workflows/build.yml
  7. 23
      .github/workflows/deploy.yml
  8. 14
      .github/workflows/emulator_script.sh
  9. 25
      .github/workflows/snapshot.yml
  10. 23
      .run/runAndroidTests.run.xml
  11. 23
      .run/runUnitTests.run.xml
  12. 90
      .travis.yml
  13. 2
      LICENSE
  14. 111
      README.md
  15. 29
      build.gradle
  16. 30
      build.gradle.kts
  17. 262
      cameraview/build.gradle
  18. 137
      cameraview/build.gradle.kts
  19. 36
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/BaseEglTest.java
  20. 30
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/BaseTest.java
  21. 4
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraLoggerTest.java
  22. 18
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraUtilsTest.java
  23. 12
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewCallbacksTest.java
  24. 241
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java
  25. 17
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/DoNotRunOnTravis.java
  26. 3
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/PictureResultTest.java
  27. 76
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/VideoResultTest.java
  28. 51
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/Camera1IntegrationTest.java
  29. 86
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/Camera2IntegrationTest.java
  30. 30
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegration1Test.java
  31. 30
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegration2Test.java
  32. 662
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegrationTest.java
  33. 67
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/MockCameraEngine.java
  34. 40
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/mappers/Camera1MapperTest.java
  35. 101
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/mappers/Camera2MapperTest.java
  36. 102
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/options/Camera1OptionsTest.java
  37. 172
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/BaseFilterTest.java
  38. 30
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/FiltersTest.java
  39. 212
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/MultiFilterTest.java
  40. 35
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/NoFilterTest.java
  41. 43
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/SimpleFilterTest.java
  42. 112
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/frame/ByteBufferFrameManagerTest.java
  43. 73
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/frame/FrameManagerTest.java
  44. 6
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/GestureFinderTest.java
  45. 9
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/PinchGestureFinderTest.java
  46. 17
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/ScrollGestureFinderTest.java
  47. 12
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/gesture/TapGestureFinderTest.java
  48. 8
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/CamcorderProfilesTest.java
  49. 3
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/CropHelperTest.java
  50. 219
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/DeviceEncodersTest.java
  51. 37
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/GridLinesLayoutTest.java
  52. 109
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/OrientationHelperTest.java
  53. 3
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/RotationHelperTest.java
  54. 49
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/WorkerHandlerTest.java
  55. 102
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/utils/ImageHelperTest.java
  56. 101
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/utils/OrientationHelperTest.java
  57. 35
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/markers/MarkerLayoutTest.java
  58. 137
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/metering/MeteringRegionsTest.java
  59. 62
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/overlay/OverlayDrawerTest.java
  60. 3
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/overlay/OverlayLayoutTest.java
  61. 28
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/CameraPreviewTest.java
  62. 2
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/MockCameraPreview.java
  63. 12
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/tools/Emulator.java
  64. 149
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/tools/Op.java
  65. 9
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/tools/Retry.java
  66. 52
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/tools/RetryRule.java
  67. 20
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/tools/SdkExclude.java
  68. 47
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/tools/SdkExcludeFilter.java
  69. 20
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/tools/SdkInclude.java
  70. 47
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/tools/SdkIncludeFilter.java
  71. 3
      cameraview/src/main/AndroidManifest.xml
  72. 4
      cameraview/src/main/java/com/otaliastudios/cameraview/BitmapCallback.java
  73. 35
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraListener.java
  74. 20
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraLogger.java
  75. 343
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java
  76. 64
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraUtils.java
  77. 982
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java
  78. 30
      cameraview/src/main/java/com/otaliastudios/cameraview/PictureResult.java
  79. 41
      cameraview/src/main/java/com/otaliastudios/cameraview/VideoResult.java
  80. 63
      cameraview/src/main/java/com/otaliastudios/cameraview/controls/AudioCodec.java
  81. 39
      cameraview/src/main/java/com/otaliastudios/cameraview/controls/ControlParser.java
  82. 52
      cameraview/src/main/java/com/otaliastudios/cameraview/controls/PictureFormat.java
  83. 563
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java
  84. 1382
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java
  85. 978
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraBaseEngine.java
  86. 1403
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java
  87. 7
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/EngineThread.java
  88. 221
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/Mapper.java
  89. 170
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/Step.java
  90. 92
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/Action.java
  91. 23
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/ActionCallback.java
  92. 83
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/ActionHolder.java
  93. 67
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/ActionWrapper.java
  94. 56
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/Actions.java
  95. 177
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/BaseAction.java
  96. 27
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/CompletionCallback.java
  97. 48
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/LogAction.java
  98. 96
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/SequenceAction.java
  99. 50
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/TimeoutAction.java
  100. 86
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/TogetherAction.java
  101. Some files were not shown because too many files have changed in this diff Show More

@ -1 +1 @@
Contributing guidelines are [hosted here](https://natario1.github.io/CameraView/extra/contributing.html). Contributing guidelines are [hosted here](https://natario1.github.io/CameraView/extra/contributing).

@ -1,6 +1,6 @@
# These are supported funding model platforms # These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] github: [natario1]
patreon: # Replace with a single Patreon username patreon: # Replace with a single Patreon username
open_collective: cameraview open_collective: cameraview
ko_fi: # Replace with a single Ko-fi username ko_fi: # Replace with a single Ko-fi username

@ -8,26 +8,39 @@ assignees: ''
--- ---
### Describe the bug ### Describe the bug
A clear and concise description of what the bug is. Please add a clear description of what the bug is, **and** fill the list below.
- CameraView version: *version number* - CameraView version: *version number*
- Camera engine used: *camera1/camera2/both*
- Reproducible in official demo app: *yes/no* - Reproducible in official demo app: *yes/no*
- Device / Android version: *Pixel, API 28* - Device / Android version: *Pixel, API 28*
- I have read the [FAQ page](https://natario1.github.io/CameraView/about/faq): *yes/no*
### To Reproduce ### To Reproduce
Steps to reproduce the behavior, possibly in the demo app: Steps to reproduce the behavior, possibly in the demo app:
1. Go to '...' 1. Go to '...'
2. Click on '....' 2. Click on '...'
3. See error 3. See error
### Expected behavior ### Expected behavior
A clear and concise description of what you expected to happen. A clear and concise description of what you expected to happen.
### XML layout
Part of the XML layout with the CameraView declaration, so we can read its attributes.
```xml
<CameraView
android:layout_width="match_parent"
android:layout_height="match_parent"
...>
</CameraView>
```
### Screenshots ### Screenshots
If applicable, add screenshots to help explain your problem. If applicable, add screenshots to help explain your problem.
### Logs ### Logs
Use `CameraLogger.setLogLevel(LEVEL_VERBOSE)` to see all logs into LogCat. Use `CameraLogger.setLogLevel(LEVEL_INFO)` to see all logs into LogCat.
Use `CameraLogger.registerLogger()` to export to file or crash reporting service. Use `CameraLogger.registerLogger()` to export to file or crash reporting service.
### APK ### APK

@ -8,7 +8,7 @@ assignees: ''
--- ---
### How do I? ### How do I?
Describe your problem here. Please, read the docs first. Describe your problem here. Please, read the [docs](https://natario1.github.io/CameraView) and [FAQ page](https://natario1.github.io/CameraView/about/faq) first.
Questions not strictly related to CameraView should be asked elsewhere. Questions not strictly related to CameraView should be asked elsewhere.
### Version used ### Version used

@ -1,10 +1,14 @@
### Before you go ### Before you go
Unless this is a simple fix (typos, bugs with obvious solution), please open an issue first. Unless this is a simple fix (typos, bugs with obvious solution), please open an issue first so that
If the edited files were covered by tests, updated tests are required for merging. Please look into the tests folders and make sure you cover new code. we can discuss the best approach to address the problem. Without a reference issue and discussion,
unfortunately, this PR will likely be ignored.
- Fixes ... (*issue number*) If the edited files were covered by tests, updated tests are required for merging.
Please look into the tests folders and make sure you cover new code.
- Fixes: ... (*issue number*)
- Tests: ... (*yes/no*) - Tests: ... (*yes/no*)
- Docs updated: ... (*yes/no*) - Docs updated: ... (*yes/no*)
### Solution ### Solution
If applicable, briefly describe how the issue was addressed. If applicable, describe briefly how the issue was addressed.

@ -0,0 +1,119 @@
# https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions
# Renaming ? Change the README badge.
name: Build
on:
push:
branches:
- main
pull_request:
jobs:
ANDROID_BASE_CHECKS:
name: Base Checks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-java@v2
with:
java-version: 11
distribution: temurin
cache: gradle
- name: Perform base checks
run: ./gradlew demo:assembleDebug cameraview:publishToDirectory --stacktrace
ANDROID_UNIT_TESTS:
name: Unit Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-java@v2
with:
java-version: 11
distribution: temurin
cache: gradle
- name: Execute unit tests
run: ./gradlew cameraview:runUnitTests --stacktrace
- name: Upload unit tests artifact
uses: actions/upload-artifact@v1
with:
name: unit_tests
path: ./cameraview/build/coverage_input/unit_tests
ANDROID_EMULATOR_TESTS:
name: Emulator Tests
runs-on: macos-latest
strategy:
fail-fast: false
matrix:
# TODO 29 fails due to Mockito issues, probably reproducible locally
# 22-28 work (some of them, with SdkExclude restrictions)
EMULATOR_API: [22, 23, 24, 25, 26, 27, 28]
include:
- EMULATOR_API: 28
EMULATOR_ARCH: x86_64
- EMULATOR_API: 27
EMULATOR_ARCH: x86_64
- EMULATOR_API: 26
EMULATOR_ARCH: x86_64
- EMULATOR_API: 25
EMULATOR_ARCH: x86
- EMULATOR_API: 24
EMULATOR_ARCH: x86
- EMULATOR_API: 23
EMULATOR_ARCH: x86
- EMULATOR_API: 22
EMULATOR_ARCH: x86
steps:
- uses: actions/checkout@v2
- uses: actions/setup-java@v2
with:
java-version: 11
distribution: temurin
cache: gradle
- name: Execute emulator tests
timeout-minutes: 30
uses: reactivecircus/android-emulator-runner@v2.21.0
with:
api-level: ${{ matrix.EMULATOR_API }}
arch: ${{ matrix.EMULATOR_ARCH }}
disable-animations: true
profile: Nexus 5X
emulator-options: -no-snapshot -no-window -no-boot-anim -camera-back emulated -camera-front emulated -gpu swiftshader_indirect
script: ./.github/workflows/emulator_script.sh
- name: Upload emulator tests artifact
uses: actions/upload-artifact@v1
with:
name: emulator_tests_${{ matrix.EMULATOR_API }}
path: ./cameraview/build/coverage_input/android_tests
CODE_COVERAGE:
name: Code Coverage Report
runs-on: ubuntu-latest
needs: [ANDROID_UNIT_TESTS, ANDROID_EMULATOR_TESTS]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-java@v2
with:
java-version: 11
distribution: temurin
cache: gradle
- name: Download unit tests artifact
uses: actions/download-artifact@v1
with:
name: unit_tests
path: ./cameraview/build/coverage_input/unit_tests
- name: Download emulator tests artifact
uses: actions/download-artifact@v1
with:
# 27 is the EMULATOR_API with less SdkExclude annotations, and should have
# the best possible coverage.
name: emulator_tests_27
path: ./cameraview/build/coverage_input/android_tests
- name: Create merged coverage report
run: ./gradlew cameraview:computeCoverage
- name: Upload merged coverage report (GitHub)
uses: actions/upload-artifact@v1
with:
name: report
path: ./cameraview/build/coverage_output/xml
- name: Upload merged coverage report (Codecov)
uses: codecov/codecov-action@v1
with:
file: ./cameraview/build/coverage_output/xml/*
fail_ci_if_error: true

@ -0,0 +1,23 @@
# https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions
name: Deploy
on:
release:
types: [published]
jobs:
MAVEN_UPLOAD:
name: Maven Upload
runs-on: ubuntu-latest
env:
SIGNING_KEY: ${{ secrets.SIGNING_KEY }}
SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
SONATYPE_USER: ${{ secrets.SONATYPE_USER }}
SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-java@v2
with:
java-version: 11
distribution: temurin
cache: gradle
- name: Perform maven upload
run: ./gradlew publishToSonatype

@ -0,0 +1,14 @@
#!/usr/bin/env bash
# Core
ADB_TAGS="CameraView:I CameraCallbacks:I CameraOrchestrator:I CameraEngine:I"
ADB_TAGS="$ADB_TAGS CameraUtils:I WorkerHandler:I"
# Recorders
ADB_TAGS="$ADB_TAGS VideoRecorder:I FullVideoRecorder:I SnapshotVideoRecorder:I"
ADB_TAGS="$ADB_TAGS FullPictureRecorder:I SnapshotPictureRecorder:I DeviceEncoders:I"
# Video encoders
ADB_TAGS="$ADB_TAGS MediaEncoderEngine:I MediaEncoder:I AudioMediaEncoder:I VideoMediaEncoder:I TextureMediaEncoder:I"
# Debugging
ADB_TAGS="$ADB_TAGS CameraIntegrationTest:I MessageQueue:W MPEG4Writer:I"
adb logcat -c
adb logcat $ADB_TAGS *:E -v color &
./gradlew cameraview:runAndroidTests

@ -0,0 +1,25 @@
# https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions
# Renaming ? Change the README badge.
name: Snapshot
on:
push:
branches:
- main
jobs:
SNAPSHOT:
name: Publish Snapshot
runs-on: ubuntu-latest
env:
SIGNING_KEY: ${{ secrets.SIGNING_KEY }}
SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
SONATYPE_USER: ${{ secrets.SONATYPE_USER }}
SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-java@v2
with:
java-version: 11
distribution: temurin
cache: gradle
- name: Publish sonatype snapshot
run: ./gradlew publishToSonatypeSnapshot

@ -0,0 +1,23 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="runAndroidTests" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$/cameraview" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="runAndroidTests" />
</list>
</option>
<option name="vmOptions" value="" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>false</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2" />
</configuration>
</component>

@ -0,0 +1,23 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="runUnitTests" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$/cameraview" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="runUnitTests" />
</list>
</option>
<option name="vmOptions" value="" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>false</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2" />
</configuration>
</component>

@ -1,90 +0,0 @@
# https://github.com/andstatus/andstatus/blob/master/.travis.yml
language: android
branches:
only:
- master
- /^v\d+\.\d+\.\d+$/
- /^v\d+\.\d+\.\d+-beta\d+$/
- /^v\d+\.\d+\.\d+-rc\d+$/
sudo: false
jdk:
- oraclejdk8
env:
global:
- EMULATOR_API=22 # 24 has some issues, probably some overlayed window
- EMULATOR_ABI=x86_64 # seems to work with emulator v29.
- EMULATOR_TAG=default # can be google_apis
- PATH=$ANDROID_HOME:$ANDROID_HOME/emulator:$ANDROID_HOME/platform-tools:$PATH
android:
components:
- tools
- platform-tools
- build-tools-28.0.3
- android-28
- doc-28
install:
# Setup
- echo $ANDROID_HOME # We assume this is correctly set when setting path
- sdkmanager --list || true # Look at the packages
- echo yes | sdkmanager "tools" # Ensure tools is updated
- echo yes | sdkmanager "emulator" # Ensure emulator is present
# Install emulator
# The channel=4 line looks into canary which brings in v29.
# The previous version v28 was broken:
# https://travis-ci.community/t/android-emulators-not-starting-for-the-last-few-days-late-march-2019/2871/11?u=mikehardy
- export EMULATOR="system-images;android-$EMULATOR_API;$EMULATOR_TAG;$EMULATOR_ABI"
- echo yes | sdkmanager "platforms;android-$EMULATOR_API" # Install sdk for the emulator
- echo yes | sdkmanager --channel=4 "$EMULATOR" # Install system image
- sdkmanager --list || true # Check everything is updated
# Create adn start emulator
- echo no | avdmanager create avd -n test -k "$EMULATOR" -f # Create emulator virtual device
- which emulator # ensure we are using the right emulator ($ANDROID_HOME/emulator/emulator)
- emulator -avd test -no-window -no-accel -no-snapshot -camera-back emulated -camera-front emulated -memory 2048 -writable-system & # Launch
- adb wait-for-device # Wait for adb process
- adb remount # Mount as writable
before_script:
# Wait for emulator
- android-wait-for-emulator # Wait for emulator ready to interact
- adb shell settings put global window_animation_scale 0 & # Disable animations
- adb shell settings put global transition_animation_scale 0 & # Disable animations
- adb shell settings put global animator_duration_scale 0 & # Disable animations
# Unlock and configure logs.
# Would be great to use -v color to adb logcat but looks not supported on travis.
- sleep 20 # Sleep 20 seconds just in case
- adb shell input keyevent 82 & # Dispatch unlock event
- adb logcat --help # See if this version supports color
- adb logcat -c # Clear logcat
- adb logcat Test:V TestRunner:V CameraView:V CameraController:V Camera1:V WorkerHandler:V THREAD_STATE:S *:E &
# - export LOGCAT_PID=$! # Save PID of the logcat process. Should kill later with kill $LOGCAT_PID
script:
- ./gradlew clean demo:assembleDebug cameraview:testDebugUnitTest cameraview:connectedCheck cameraview:mergedCoverageReport cameraview:javadoc
after_success:
- bash <(curl -s https://codecov.io/bash) -s "*/build/reports/mergedCoverageReport/"
cache:
directories:
- $HOME/.gradle
- $HOME/.m2/repository
deploy:
provider: script
script: ./gradlew bintrayUpload
skip_cleanup: true
on:
branch: master
tags: true

@ -1,7 +1,7 @@
Copyrights for portions of project CameraView are held by WonderKiln, Inc as Copyrights for portions of project CameraView are held by WonderKiln, Inc as
part of project CameraKit-Android (https://github.com/wonderkiln/CameraKit-Android). part of project CameraKit-Android (https://github.com/wonderkiln/CameraKit-Android).
Their original license is available below, or at Their original license is available below, or at
https://github.com/wonderkiln/CameraKit-Android/blob/master/LICENSE . https://github.com/wonderkiln/CameraKit-Android/blob/main/LICENSE .
All other copyrights for project CameraView are held by Otalia Studios, 2017. All other copyrights for project CameraView are held by Otalia Studios, 2017.
------------------------------------------------------------------------------ ------------------------------------------------------------------------------

@ -1,5 +1,5 @@
[![Build Status](https://travis-ci.org/natario1/CameraView.svg?branch=master)](https://travis-ci.org/natario1/CameraView) [![Build Status](https://github.com/natario1/CameraView/workflows/Build/badge.svg?event=push)](https://github.com/natario1/CameraView/actions)
[![Code Coverage](https://codecov.io/gh/natario1/CameraView/branch/master/graph/badge.svg)](https://codecov.io/gh/natario1/CameraView) [![Code Coverage](https://codecov.io/gh/natario1/CameraView/branch/main/graph/badge.svg)](https://codecov.io/gh/natario1/CameraView)
[![Release](https://img.shields.io/github/release/natario1/CameraView.svg)](https://github.com/natario1/CameraView/releases) [![Release](https://img.shields.io/github/release/natario1/CameraView.svg)](https://github.com/natario1/CameraView/releases)
[![Issues](https://img.shields.io/github/issues-raw/natario1/CameraView.svg)](https://github.com/natario1/CameraView/issues) [![Issues](https://img.shields.io/github/issues-raw/natario1/CameraView.svg)](https://github.com/natario1/CameraView/issues)
[![Funding](https://img.shields.io/opencollective/all/CameraView.svg?colorB=r)](https://natario1.github.io/CameraView/extra/donate) [![Funding](https://img.shields.io/opencollective/all/CameraView.svg?colorB=r)](https://natario1.github.io/CameraView/extra/donate)
@ -12,34 +12,35 @@
*Post-processing videos or want to reduce video size before uploading? Take a look at our [Transcoder](https://github.com/natario1/Transcoder).* *Post-processing videos or want to reduce video size before uploading? Take a look at our [Transcoder](https://github.com/natario1/Transcoder).*
*Like the project, make profit from it, or simply want to thank back? Please consider [sponsoring me](https://github.com/sponsors/natario1) or [donating](https://natario1.github.io/CameraView/extra/donate)!*
*Need support, consulting, or have any other business-related question? Feel free to <a href="mailto:mat.iavarone@gmail.com">get in touch</a>.*
# CameraView # CameraView
CameraView is a well documented, high-level library that makes capturing pictures and videos easy, CameraView is a well documented, high-level library that makes capturing pictures and videos easy,
addressing most of the common issues and needs, and still leaving you with flexibility where needed. addressing most of the common issues and needs, and still leaving you with flexibility where needed.
```groovy ```groovy
api 'com.otaliastudios:cameraview:2.1.0' api 'com.otaliastudios:cameraview:2.7.2'
``` ```
- Fast & reliable - Fast & reliable
- Gestures support [[docs]](https://natario1.github.io/CameraView/docs/gestures.html) - Gestures support [[docs]](https://natario1.github.io/CameraView/docs/gestures)
- Real-time filters [[docs]](https://natario1.github.io/CameraView/docs/filters.html) - Real-time filters [[docs]](https://natario1.github.io/CameraView/docs/filters)
- Camera1 or Camera2 powered engine [[docs]](https://natario1.github.io/CameraView/docs/previews.html) - Camera1 or Camera2 powered engine [[docs]](https://natario1.github.io/CameraView/docs/previews)
- Frame processing support [[docs]](https://natario1.github.io/CameraView/docs/frame-processing.html) - Frame processing support [[docs]](https://natario1.github.io/CameraView/docs/frame-processing)
- Watermarks & animated overlays [[docs]](https://natario1.github.io/CameraView/docs/watermarks-and-overlays.html) - Watermarks & animated overlays [[docs]](https://natario1.github.io/CameraView/docs/watermarks-and-overlays)
- OpenGL powered preview [[docs]](https://natario1.github.io/CameraView/docs/previews.html) - OpenGL powered preview [[docs]](https://natario1.github.io/CameraView/docs/previews)
- Take high-quality content with `takePicture` and `takeVideo` [[docs]](https://natario1.github.io/CameraView/docs/capturing-media.html) - Take high-quality content with `takePicture` and `takeVideo` [[docs]](https://natario1.github.io/CameraView/docs/capturing-media)
- Take super-fast snapshots with `takePictureSnapshot` and `takeVideoSnapshot` [[docs]](https://natario1.github.io/CameraView/docs/capturing-media.html) - Take super-fast snapshots with `takePictureSnapshot` and `takeVideoSnapshot` [[docs]](https://natario1.github.io/CameraView/docs/capturing-media)
- Smart sizing: create a `CameraView` of any size [[docs]](https://natario1.github.io/CameraView/docs/preview-size.html) - Smart sizing: create a `CameraView` of any size [[docs]](https://natario1.github.io/CameraView/docs/preview-size)
- Control HDR, flash, zoom, white balance, exposure, location, grid drawing & more [[docs]](https://natario1.github.io/CameraView/docs/controls.html) - Control HDR, flash, zoom, white balance, exposure, location, grid drawing & more [[docs]](https://natario1.github.io/CameraView/docs/controls)
- RAW pictures support [[docs]](https://natario1.github.io/CameraView/docs/controls)
- Lightweight - Lightweight
- Works down to API level 15 - Works down to API level 15
- Well tested - Well tested
Read the [official website](https://natario1.github.io/CameraView) for setup instructions and documentation.
You might also be interested in [changelog](https://natario1.github.io/CameraView/about/changelog.html)
or in the [v1 migration guide](https://natario1.github.io/CameraView/extra/v1-migration-guide.html).
&#10240; <!-- Hack to add whitespace --> &#10240; <!-- Hack to add whitespace -->
<p align="center"> <p align="center">
@ -48,12 +49,45 @@ or in the [v1 migration guide](https://natario1.github.io/CameraView/extra/v1-mi
&#10240; <!-- Hack to add whitespace --> &#10240; <!-- Hack to add whitespace -->
If you like the project, use it with profit, or simply want to thank back, please consider [donating ## Support
to the project](https://natario1.github.io/CameraView/extra/donate) now! You can either make a one time
donation or become a sponsor, in which case your company logo will immediately show up here. If you like the project, make profit from it, or simply want to thank back, please consider
[sponsoring me](https://github.com/sponsors/natario1) through the GitHub Sponsors program! You can
have your company logo here, get private support hours or simply help me push this forward.
If you prefer, you can also [donate](https://natario1.github.io/CameraView/extra/donate)
to our OpenCollective page.
CameraView is trusted and supported by [ShareChat](https://sharechat.com/), a social media app with over 100 million downloads.
<p align="center">
<img src="docs/static/sharechat.png" width="100%">
</p>
Feel free to <a href="mailto:mat.iavarone@gmail.com">contact me</a> for support, consulting or any other business-related question.
Thanks to all our project backers... [[become a backer]](https://opencollective.com/cameraview#backer)
<a href="https://opencollective.com/cameraview#backers" target="_blank"><img src="https://opencollective.com/cameraview/backers.svg?width=890"></a>
Thank you for any contribution - it is a nice reward for what has been done until now, and a ...and to all our project sponsors! [[become a sponsor]](https://opencollective.com/cameraview#sponsor)
motivation boost to push the library forward.
<a href="https://opencollective.com/cameraview/sponsor/0/website" target="_blank"><img src="https://opencollective.com/cameraview/sponsor/0/avatar.svg"></a>
<a href="https://opencollective.com/cameraview/sponsor/1/website" target="_blank"><img src="https://opencollective.com/cameraview/sponsor/1/avatar.svg"></a>
<a href="https://opencollective.com/cameraview/sponsor/2/website" target="_blank"><img src="https://opencollective.com/cameraview/sponsor/2/avatar.svg"></a>
<a href="https://opencollective.com/cameraview/sponsor/3/website" target="_blank"><img src="https://opencollective.com/cameraview/sponsor/3/avatar.svg"></a>
<a href="https://opencollective.com/cameraview/sponsor/4/website" target="_blank"><img src="https://opencollective.com/cameraview/sponsor/4/avatar.svg"></a>
<a href="https://opencollective.com/cameraview/sponsor/5/website" target="_blank"><img src="https://opencollective.com/cameraview/sponsor/5/avatar.svg"></a>
<a href="https://opencollective.com/cameraview/sponsor/6/website" target="_blank"><img src="https://opencollective.com/cameraview/sponsor/6/avatar.svg"></a>
<a href="https://opencollective.com/cameraview/sponsor/7/website" target="_blank"><img src="https://opencollective.com/cameraview/sponsor/7/avatar.svg"></a>
<a href="https://opencollective.com/cameraview/sponsor/8/website" target="_blank"><img src="https://opencollective.com/cameraview/sponsor/8/avatar.svg"></a>
<a href="https://opencollective.com/cameraview/sponsor/9/website" target="_blank"><img src="https://opencollective.com/cameraview/sponsor/9/avatar.svg"></a>
## Setup
Please read the [official website](https://natario1.github.io/CameraView) for setup instructions and documentation.
You might also be interested in our [changelog](https://natario1.github.io/CameraView/about/changelog)
or in the [v1 migration guide](https://natario1.github.io/CameraView/extra/v1-migration-guide).
Using CameraView is extremely simple:
```xml ```xml
<com.otaliastudios.cameraview.CameraView <com.otaliastudios.cameraview.CameraView
@ -80,6 +114,11 @@ motivation boost to push the library forward.
app:cameraVideoSizeAspectRatio="@string/video_ratio" app:cameraVideoSizeAspectRatio="@string/video_ratio"
app:cameraSnapshotMaxWidth="@integer/snapshot_max_width" app:cameraSnapshotMaxWidth="@integer/snapshot_max_width"
app:cameraSnapshotMaxHeight="@integer/snapshot_max_height" app:cameraSnapshotMaxHeight="@integer/snapshot_max_height"
app:cameraFrameProcessingMaxWidth="@integer/processing_max_width"
app:cameraFrameProcessingMaxHeight="@integer/processing_max_height"
app:cameraFrameProcessingFormat="@integer/processing_format"
app:cameraFrameProcessingPoolSize="@integer/processing_pool_size"
app:cameraFrameProcessingExecutors="@integer/processing_executors"
app:cameraVideoBitRate="@integer/video_bit_rate" app:cameraVideoBitRate="@integer/video_bit_rate"
app:cameraAudioBitRate="@integer/audio_bit_rate" app:cameraAudioBitRate="@integer/audio_bit_rate"
app:cameraGestureTap="none|autoFocus|takePicture" app:cameraGestureTap="none|autoFocus|takePicture"
@ -89,6 +128,8 @@ motivation boost to push the library forward.
app:cameraGestureScrollVertical="none|zoom|exposureCorrection|filterControl1|filterControl2" app:cameraGestureScrollVertical="none|zoom|exposureCorrection|filterControl1|filterControl2"
app:cameraEngine="camera1|camera2" app:cameraEngine="camera1|camera2"
app:cameraPreview="glSurface|surface|texture" app:cameraPreview="glSurface|surface|texture"
app:cameraPreviewFrameRate="@integer/preview_frame_rate"
app:cameraPreviewFrameRateExact="false|true"
app:cameraFacing="back|front" app:cameraFacing="back|front"
app:cameraHdr="on|off" app:cameraHdr="on|off"
app:cameraFlash="on|auto|torch|off" app:cameraFlash="on|auto|torch|off"
@ -105,6 +146,10 @@ motivation boost to push the library forward.
app:cameraAutoFocusMarker="@string/cameraview_default_autofocus_marker" app:cameraAutoFocusMarker="@string/cameraview_default_autofocus_marker"
app:cameraUseDeviceOrientation="true|false" app:cameraUseDeviceOrientation="true|false"
app:cameraFilter="@string/real_time_filter" app:cameraFilter="@string/real_time_filter"
app:cameraPictureMetering="true|false"
app:cameraPictureSnapshotMetering="false|true"
app:cameraPictureFormat="jpeg|dng"
app:cameraRequestPermissions="true|false"
app:cameraExperimental="false|true"> app:cameraExperimental="false|true">
<!-- Watermark! --> <!-- Watermark! -->
@ -119,25 +164,3 @@ motivation boost to push the library forward.
</com.otaliastudios.cameraview.CameraView> </com.otaliastudios.cameraview.CameraView>
``` ```
## Backers
Thanks to all backers! [Become a backer.](https://opencollective.com/cameraview#backer)
<a href="https://opencollective.com/cameraview#backers" target="_blank"><img src="https://opencollective.com/cameraview/backers.svg?width=890"></a>
## Sponsors
Thanks to sponsors! [Become a sponsor](https://opencollective.com/cameraview#sponsor) and have your logo here.
<a href="https://opencollective.com/cameraview/sponsor/0/website" target="_blank"><img src="https://opencollective.com/cameraview/sponsor/0/avatar.svg"></a>
<a href="https://opencollective.com/cameraview/sponsor/1/website" target="_blank"><img src="https://opencollective.com/cameraview/sponsor/1/avatar.svg"></a>
<a href="https://opencollective.com/cameraview/sponsor/2/website" target="_blank"><img src="https://opencollective.com/cameraview/sponsor/2/avatar.svg"></a>
<a href="https://opencollective.com/cameraview/sponsor/3/website" target="_blank"><img src="https://opencollective.com/cameraview/sponsor/3/avatar.svg"></a>
<a href="https://opencollective.com/cameraview/sponsor/4/website" target="_blank"><img src="https://opencollective.com/cameraview/sponsor/4/avatar.svg"></a>
<a href="https://opencollective.com/cameraview/sponsor/5/website" target="_blank"><img src="https://opencollective.com/cameraview/sponsor/5/avatar.svg"></a>
<a href="https://opencollective.com/cameraview/sponsor/6/website" target="_blank"><img src="https://opencollective.com/cameraview/sponsor/6/avatar.svg"></a>
<a href="https://opencollective.com/cameraview/sponsor/7/website" target="_blank"><img src="https://opencollective.com/cameraview/sponsor/7/avatar.svg"></a>
<a href="https://opencollective.com/cameraview/sponsor/8/website" target="_blank"><img src="https://opencollective.com/cameraview/sponsor/8/avatar.svg"></a>
<a href="https://opencollective.com/cameraview/sponsor/9/website" target="_blank"><img src="https://opencollective.com/cameraview/sponsor/9/avatar.svg"></a>

@ -1,29 +0,0 @@
buildscript {
repositories {
jcenter()
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.4.2'
classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1'
classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.4'
}
}
allprojects {
repositories {
jcenter()
google()
}
}
ext {
compileSdkVersion = 28
minSdkVersion = 15
targetSdkVersion = 28
}
task clean(type: Delete) {
delete rootProject.buildDir
}

@ -0,0 +1,30 @@
buildscript {
extra["minSdkVersion"] = 15
extra["compileSdkVersion"] = 31
extra["targetSdkVersion"] = 31
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:7.0.3")
classpath("io.deepmedia.tools:publisher:0.6.0")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.31")
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
tasks.register("clean", Delete::class) {
delete(buildDir)
}

@ -1,262 +0,0 @@
apply plugin: 'com.android.library'
apply plugin: 'com.github.dcendents.android-maven'
apply plugin: 'com.jfrog.bintray'
// Required by bintray
version = '2.1.0'
group = 'com.otaliastudios'
//region android dependencies
def travis = System.getenv("TRAVIS")
android {
compileSdkVersion rootProject.ext.compileSdkVersion
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName project.version
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
if (travis) {
testInstrumentationRunnerArgument "notAnnotation", "com.otaliastudios.cameraview.DoNotRunOnTravis"
}
}
buildTypes {
debug {
testCoverageEnabled true
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
testImplementation 'junit:junit:4.12'
testImplementation 'org.mockito:mockito-inline:2.28.2'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test:rules:1.2.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'org.mockito:mockito-android:2.28.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
api 'androidx.exifinterface:exifinterface:1.0.0'
api 'androidx.lifecycle:lifecycle-common:2.1.0-alpha01'
api 'com.google.android.gms:play-services-tasks:17.0.0'
implementation 'androidx.annotation:annotation:1.1.0'
}
//endregion
//region bintray
// install is a task defined by the Gradle Maven plugin, which is used to
// publish a maven repo to a local repository. (we actually use the android version of the plugin,
// com.github.dcendents.android-maven, to support AARs)
// https://docs.gradle.org/current/userguide/maven_plugin.html#sec:maven_tasks
install {
// The repositories property is common to all tasks of type Upload and returns the repositories
// into which we will upload data. https://docs.gradle.org/current/dsl/org.gradle.api.tasks.Upload.html#org.gradle.api.tasks.Upload:repositories
// It returns a RepositoryHandler: https://docs.gradle.org/current/dsl/org.gradle.api.artifacts.dsl.RepositoryHandler.html
repositories {
// The maven plugin adds a mavenInstaller property to the RepositoryHandler which can be used to
// add and configure a local maven repository cache.
// https://docs.gradle.org/current/dsl/org.gradle.api.artifacts.dsl.RepositoryHandler.html#N11785
mavenInstaller {
// The object here extends PomFilterContainer so we can configure the pom file here.
// https://docs.gradle.org/current/javadoc/org/gradle/api/artifacts/maven/PomFilterContainer.html#pom-groovy.lang.Closure-
pom {
// Now we are inside a MavenPom object that can be configured. We get the project and configure.
// https://docs.gradle.org/current/javadoc/org/gradle/api/artifacts/maven/MavenPom.html
project {
name 'CameraView'
description 'A well documented, high-level Android interface that makes capturing pictures ' +
'and videos easy, addressing most of the common issues and needs.'
url 'https://github.com/natario1/CameraView'
packaging 'aar'
groupId project.group
artifactId 'cameraview'
version project.version
licenses {
license {
name 'The Apache Software License, Version 2.0'
url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
}
}
scm {
connection 'https://github.com/natario1/CameraView.git'
developerConnection 'https://github.com/natario1/CameraView.git'
url 'https://github.com/natario1/CameraView'
}
developers {
developer {
id = 'natario'
name 'Mattia Iavarone'
}
}
}
}
}
}
}
def bintrayUser
def bintrayKey
if (travis) {
bintrayUser = System.getenv("BINTRAY_USER")
bintrayKey = System.getenv("BINTRAY_KEY")
} else {
Properties props = new Properties()
props.load(project.rootProject.file('local.properties').newDataInputStream())
bintrayUser = props.getProperty('bintray.user')
bintrayKey = props.get('bintray.key')
}
bintray {
// https://github.com/bintray/gradle-bintray-plugin
user = bintrayUser
key = bintrayKey
configurations = ['archives']
pkg {
repo = 'android'
name = 'CameraView'
licenses = ['Apache-2.0']
vcsUrl = 'https://github.com/natario1/CameraView.git'
publish = true
override = true
version {
name = project.version
desc = 'CameraView v. '+project.version
released = new Date()
vcsTag = 'v'+project.version
}
}
}
//endregion
//region javadoc and sources
// From official sample https://github.com/bintray/bintray-examples/blob/master/gradle-aar-example/build.gradle
task sourcesJar(type: Jar) {
classifier = 'sources'
from android.sourceSets.main.java.sourceFiles
}
task javadoc(type: Javadoc) {
source = android.sourceSets.main.java.srcDirs
classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
classpath += project.files("${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
project.android.libraryVariants.all { variant ->
if (variant.name == 'release') {
classpath += files(variant.javaCompile.classpath)
}
}
exclude '**/BuildConfig.java'
exclude '**/R.java'
// This excludes our internal folder, which is nice, but also creates
// errors anytime we reference excluded classes in public classes javadocs,
// which is unfortunate. There might be a fix but I don't know any.
// exclude '**/internal/**'
}
task javadocJar(type: Jar, dependsOn: javadoc) {
classifier = 'javadoc'
from javadoc.destinationDir
}
artifacts {
archives javadocJar
archives sourcesJar
}
//endregion
//region code coverage
// 1. running androidTests with connectedCheck will generate an .ec file
// in build/outputs/code-coverage/connected, plus the XML result in
// in build/reports/coverage/debug/report.xml .
// 2. running unit tests with testDebugUnitTest will just generate the .exec file.
// The JacocoReport task from the jacoco plugin can create the XML report out of it.
// to have a unified report, we just pass both the .exec and the .ec file
// to the jacoco task, so we get a unified XML report with total coverage.
// Reference: https://medium.com/@rafael_toledo/setting-up-an-unified-coverage-report-in-android-with-jacoco-robolectric-and-espresso-ffe239aaf3fa
apply plugin: 'jacoco'
def reportsDirectory = "$buildDir/reports/"
jacoco {
toolVersion = "0.8.1"
reportsDir = file(reportsDirectory)
}
task mergedCoverageReport(type: JacocoReport) {
dependsOn "testDebugUnitTest"
dependsOn "connectedCheck"
// Merge unit tests and android tests data
executionData = fileTree(dir: "$buildDir", includes: [
"jacoco/testDebugUnitTest.exec", // Unit tests
"outputs/code_coverage/debugAndroidTest/connected/*coverage.ec" // Android tests
])
// Sources
sourceDirectories = files(android.sourceSets.main.java.sourceFiles)
additionalSourceDirs = files([ // Add BuildConfig and R.
"$buildDir/generated/source/buildConfig/debug",
"$buildDir/generated/source/r/debug"
])
// Classes (.class files)
// Not everything in the filter relates to CameraView,
// but let's keep a generic filter
def classDir = "$buildDir/intermediates/javac/debug"
def classFilter = [
'**/R.class',
'**/R$*.class',
'**/BuildConfig.*',
'**/Manifest*.*',
'android/**',
'androidx/**',
'com/google/**',
'**/*$ViewInjector*.*',
'**/Dagger*Component.class',
'**/Dagger*Component$Builder.class',
'**/*Module_*Factory.class',
]
if (travis) {
// All these classes are tested by the integration tests that we are not able to
// run on the travis emulator.
classFilter.add('**/com/otaliastudios/cameraview/engine/CameraEngine**.*')
classFilter.add('**/com/otaliastudios/cameraview/engine/Camera1Engine**.*')
classFilter.add('**/com/otaliastudios/cameraview/engine/Camera2Engine**.*')
classFilter.add('**/com/otaliastudios/cameraview/picture/**.*')
classFilter.add('**/com/otaliastudios/cameraview/video/**.*')
// TODO these below could be testable ALSO outside of the integration tests
classFilter.add('**/com/otaliastudios/cameraview/video/encoding/**.*')
}
// We don't test OpenGL filters.
classFilter.add('**/com/otaliastudios/cameraview/filters/**.*')
classDirectories = fileTree(dir: classDir, excludes: classFilter);
reports.html.enabled = true
reports.xml.enabled = true
reports.xml.destination file("$reportsDirectory/mergedCoverageReport/report.xml")
}
//endregion
// export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_101.jdk/Contents/Home
// To deploy ./gradlew bintrayUpload

@ -0,0 +1,137 @@
import io.deepmedia.tools.publisher.common.License
import io.deepmedia.tools.publisher.common.Release
import io.deepmedia.tools.publisher.common.GithubScm
plugins {
id("com.android.library")
id("kotlin-android")
id("io.deepmedia.tools.publisher")
id("jacoco")
}
android {
compileSdk = property("compileSdkVersion") as Int
defaultConfig {
minSdk = property("minSdkVersion") as Int
targetSdk = property("targetSdkVersion") as Int
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments["filter"] = "" +
"com.otaliastudios.cameraview.tools.SdkExcludeFilter," +
"com.otaliastudios.cameraview.tools.SdkIncludeFilter"
}
buildTypes["debug"].isTestCoverageEnabled = true
buildTypes["release"].isMinifyEnabled = false
}
dependencies {
testImplementation("junit:junit:4.13.1")
testImplementation("org.mockito:mockito-inline:2.28.2")
androidTestImplementation("androidx.test:runner:1.4.0")
androidTestImplementation("androidx.test:rules:1.4.0")
androidTestImplementation("androidx.test.ext:junit:1.1.3")
androidTestImplementation("org.mockito:mockito-android:2.28.2")
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
api("androidx.exifinterface:exifinterface:1.3.3")
api("androidx.lifecycle:lifecycle-common:2.3.1")
api("com.google.android.gms:play-services-tasks:17.2.1")
implementation("androidx.annotation:annotation:1.2.0")
implementation("com.otaliastudios.opengl:egloo:0.6.1")
}
// Publishing
publisher {
project.description = "A well documented, high-level Android interface that makes capturing " +
"pictures and videos easy, addressing all of the common issues and needs. " +
"Real-time filters, gestures, watermarks, frame processing, RAW, output of any size."
project.artifact = "cameraview"
project.group = "com.otaliastudios"
project.url = "https://github.com/natario1/CameraView"
project.scm = GithubScm("natario1", "CameraView")
project.addLicense(License.APACHE_2_0)
project.addDeveloper("natario1", "mat.iavarone@gmail.com")
release.sources = Release.SOURCES_AUTO
release.docs = Release.DOCS_AUTO
release.version = "2.7.2"
directory()
sonatype {
auth.user = "SONATYPE_USER"
auth.password = "SONATYPE_PASSWORD"
signing.key = "SIGNING_KEY"
signing.password = "SIGNING_PASSWORD"
}
sonatype("snapshot") {
repository = io.deepmedia.tools.publisher.sonatype.Sonatype.OSSRH_SNAPSHOT_1
release.version = "latest-SNAPSHOT"
auth.user = "SONATYPE_USER"
auth.password = "SONATYPE_PASSWORD"
signing.key = "SIGNING_KEY"
signing.password = "SIGNING_PASSWORD"
}
}
// Code Coverage
val buildDir = project.buildDir.absolutePath
val coverageInputDir = "$buildDir/coverage_input" // changing? change github workflow
val coverageOutputDir = "$buildDir/coverage_output" // changing? change github workflow
// Run unit tests, with coverage enabled in the android { } configuration.
// Output will be an .exec file in build/jacoco.
tasks.register("runUnitTests") { // changing name? change github workflow
dependsOn("testDebugUnitTest")
doLast {
copy {
from("$buildDir/outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.exec")
into("$coverageInputDir/unit_tests") // changing? change github workflow
}
}
}
// Run android tests with coverage.
tasks.register("runAndroidTests") { // changing name? change github workflow
dependsOn("connectedDebugAndroidTest")
doLast {
copy {
from("$buildDir/outputs/code_coverage/debugAndroidTest/connected")
include("*coverage.ec")
into("$coverageInputDir/android_tests") // changing? change github workflow
}
}
}
// Merge the two with a jacoco task.
jacoco { toolVersion = "0.8.5" }
tasks.register("computeCoverage", JacocoReport::class) {
dependsOn("compileDebugSources") // Compile sources, needed below
executionData.from(fileTree(coverageInputDir))
sourceDirectories.from(android.sourceSets["main"].java.srcDirs)
additionalSourceDirs.from("$buildDir/generated/source/buildConfig/debug")
additionalSourceDirs.from("$buildDir/generated/source/r/debug")
classDirectories.from(fileTree("$buildDir/intermediates/javac/debug") {
// Not everything here is relevant for CameraView, but let's keep it generic
exclude(
"**/R.class",
"**/R$*.class",
"**/BuildConfig.*",
"**/Manifest*.*",
"android/**",
"androidx/**",
"com/google/**",
"**/*\$ViewInjector*.*",
"**/Dagger*Component.class",
"**/Dagger*Component\$Builder.class",
"**/*Module_*Factory.class",
// We don"t test OpenGL filters.
"**/com/otaliastudios/cameraview/filters/**.*"
)
})
reports.html.required.set(true)
reports.xml.required.set(true)
reports.html.outputLocation.set(file("$coverageOutputDir/html"))
reports.xml.outputLocation.set(file("$coverageOutputDir/xml/report.xml"))
}

@ -0,0 +1,36 @@
package com.otaliastudios.cameraview;
import android.opengl.EGL14;
import com.otaliastudios.opengl.core.EglCore;
import com.otaliastudios.opengl.surface.EglOffscreenSurface;
import com.otaliastudios.opengl.surface.EglSurface;
import org.junit.After;
import org.junit.Before;
@SuppressWarnings("WeakerAccess")
public abstract class BaseEglTest extends BaseTest {
protected final static int WIDTH = 100;
protected final static int HEIGHT = 100;
protected EglCore eglCore;
protected EglSurface eglSurface;
@Before
public void setUp() {
eglCore = new EglCore(EGL14.EGL_NO_CONTEXT, EglCore.FLAG_RECORDABLE);
eglSurface = new EglOffscreenSurface(eglCore, WIDTH, HEIGHT);
eglSurface.makeCurrent();
}
@After
public void tearDown() {
eglSurface.release();
eglSurface = null;
eglCore.release();
eglCore = null;
}
}

@ -10,8 +10,9 @@ import android.os.PowerManager;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.GrantPermissionRule;
import com.otaliastudios.cameraview.internal.utils.Op; import com.otaliastudios.cameraview.tools.Op;
import org.junit.After; import org.junit.After;
import org.junit.AfterClass; import org.junit.AfterClass;
@ -89,21 +90,8 @@ public class BaseTest {
InstrumentationRegistry.getInstrumentation().waitForIdleSync(); InstrumentationRegistry.getInstrumentation().waitForIdleSync();
} }
protected static void grantAllPermissions() {
grantPermission("android.permission.CAMERA");
grantPermission("android.permission.RECORD_AUDIO");
grantPermission("android.permission.WRITE_EXTERNAL_STORAGE");
}
@SuppressWarnings("WeakerAccess")
protected static void grantPermission(@NonNull String permission) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return;
String command = "pm grant " + getContext().getPackageName() + " " + permission;
InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(command);
}
@NonNull @NonNull
protected static Stubber doCountDown(final CountDownLatch latch) { protected static Stubber doCountDown(@NonNull final CountDownLatch latch) {
return doAnswer(new Answer<Object>() { return doAnswer(new Answer<Object>() {
@Override @Override
public Object answer(InvocationOnMock invocation) { public Object answer(InvocationOnMock invocation) {
@ -118,7 +106,7 @@ public class BaseTest {
return doAnswer(new Answer<Object>() { return doAnswer(new Answer<Object>() {
@Override @Override
public Object answer(InvocationOnMock invocation) { public Object answer(InvocationOnMock invocation) {
op.end(response); op.controller().end(response);
return null; return null;
} }
}); });
@ -126,14 +114,6 @@ public class BaseTest {
@NonNull @NonNull
protected static <T> Stubber doEndOp(final Op<T> op, final int withReturnArgument) { protected static <T> Stubber doEndOp(final Op<T> op, final int withReturnArgument) {
return doAnswer(new Answer<Object>() { return op.controller().from(withReturnArgument);
@Override
public Object answer(InvocationOnMock invocation) {
//noinspection unchecked
T o = (T) invocation.getArguments()[withReturnArgument];
op.end(o);
return null;
}
});
} }
} }

@ -1,7 +1,7 @@
package com.otaliastudios.cameraview; package com.otaliastudios.cameraview;
import com.otaliastudios.cameraview.internal.utils.Op; import com.otaliastudios.cameraview.tools.Op;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest; import androidx.test.filters.SmallTest;
@ -109,7 +109,7 @@ public class CameraLoggerTest extends BaseTest {
CameraLogger.Logger mock = mock(CameraLogger.Logger.class); CameraLogger.Logger mock = mock(CameraLogger.Logger.class);
CameraLogger.registerLogger(mock); CameraLogger.registerLogger(mock);
final Op<Throwable> op = new Op<>(); final Op<Throwable> op = new Op<>(false);
doEndOp(op, 3) doEndOp(op, 3)
.when(mock) .when(mock)
.log(anyInt(), anyString(), anyString(), any(Throwable.class)); .log(anyInt(), anyString(), anyString(), any(Throwable.class));

@ -6,11 +6,10 @@ import android.content.pm.PackageManager;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.Color; import android.graphics.Color;
import com.otaliastudios.cameraview.internal.utils.Op; import com.otaliastudios.cameraview.tools.Op;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.test.annotation.UiThreadTest;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest; import androidx.test.filters.SmallTest;
@ -20,7 +19,6 @@ import org.junit.runner.RunWith;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.Charset; import java.nio.charset.Charset;
@ -51,21 +49,21 @@ public class CameraUtilsTest extends BaseTest {
private Op<String> writeAndReadString(@NonNull String data) { private Op<String> writeAndReadString(@NonNull String data) {
final File file = new File(getContext().getFilesDir(), "string.txt"); final File file = new File(getContext().getFilesDir(), "string.txt");
final byte[] bytes = data.getBytes(Charset.forName("UTF-8")); final byte[] bytes = data.getBytes(Charset.forName("UTF-8"));
final Op<String> result = new Op<>(true); final Op<String> result = new Op<>();
final FileCallback callback = new FileCallback() { final FileCallback callback = new FileCallback() {
@Override @Override
public void onFileReady(@Nullable File file) { public void onFileReady(@Nullable File file) {
if (file == null) { if (file == null) {
result.end(null); result.controller().end(null);
} else { } else {
// Read back the file. // Read back the file.
try { try {
FileInputStream stream = new FileInputStream(file); FileInputStream stream = new FileInputStream(file);
byte[] bytes = new byte[stream.available()]; byte[] bytes = new byte[stream.available()];
stream.read(bytes); stream.read(bytes);
result.end(new String(bytes, Charset.forName("UTF-8"))); result.controller().end(new String(bytes, Charset.forName("UTF-8")));
} catch (IOException e) { } catch (IOException e) {
result.end(null); result.controller().end(null);
} }
} }
} }
@ -94,12 +92,12 @@ public class CameraUtilsTest extends BaseTest {
source.compress(Bitmap.CompressFormat.PNG, 100, os); source.compress(Bitmap.CompressFormat.PNG, 100, os);
final byte[] data = os.toByteArray(); final byte[] data = os.toByteArray();
final Op<Bitmap> decode = new Op<>(true); final Op<Bitmap> decode = new Op<>();
if (async) { if (async) {
final BitmapCallback callback = new BitmapCallback() { final BitmapCallback callback = new BitmapCallback() {
@Override @Override
public void onBitmapReady(Bitmap bitmap) { public void onBitmapReady(Bitmap bitmap) {
decode.end(bitmap); decode.controller().end(bitmap);
} }
}; };
@ -121,7 +119,7 @@ public class CameraUtilsTest extends BaseTest {
} else { } else {
result = CameraUtils.decodeBitmap(data); result = CameraUtils.decodeBitmap(data);
} }
decode.end(result); decode.controller().end(result);
} }
return decode; return decode;
} }

@ -17,7 +17,7 @@ import com.otaliastudios.cameraview.frame.Frame;
import com.otaliastudios.cameraview.frame.FrameProcessor; import com.otaliastudios.cameraview.frame.FrameProcessor;
import com.otaliastudios.cameraview.gesture.Gesture; import com.otaliastudios.cameraview.gesture.Gesture;
import com.otaliastudios.cameraview.gesture.GestureAction; import com.otaliastudios.cameraview.gesture.GestureAction;
import com.otaliastudios.cameraview.internal.utils.Op; import com.otaliastudios.cameraview.tools.Op;
import com.otaliastudios.cameraview.engine.MockCameraEngine; import com.otaliastudios.cameraview.engine.MockCameraEngine;
import com.otaliastudios.cameraview.markers.AutoFocusMarker; import com.otaliastudios.cameraview.markers.AutoFocusMarker;
import com.otaliastudios.cameraview.markers.AutoFocusTrigger; import com.otaliastudios.cameraview.markers.AutoFocusTrigger;
@ -90,7 +90,7 @@ public class CameraViewCallbacksTest extends BaseTest {
camera.doInstantiatePreview(); camera.doInstantiatePreview();
camera.addCameraListener(listener); camera.addCameraListener(listener);
camera.addFrameProcessor(processor); camera.addFrameProcessor(processor);
op = new Op<>(true); op = new Op<>();
} }
}); });
} }
@ -248,7 +248,13 @@ public class CameraViewCallbacksTest extends BaseTest {
verify(listener, times(1)).onOrientationChanged(anyInt()); verify(listener, times(1)).onOrientationChanged(anyInt());
} }
// TODO: test onShutter, here or elsewhere @Test
public void testOnShutter() {
doEndOp(op, true).when(listener).onPictureShutter();
camera.mCameraCallbacks.dispatchOnPictureShutter(true);
assertNotNull(op.await(DELAY));
verify(listener, times(1)).onPictureShutter();
}
@Test @Test
public void testCameraError() { public void testCameraError() {

@ -3,9 +3,12 @@ package com.otaliastudios.cameraview;
import android.content.Context; import android.content.Context;
import android.content.res.TypedArray; import android.content.res.TypedArray;
import android.graphics.ImageFormat;
import android.graphics.PointF; import android.graphics.PointF;
import android.graphics.RectF;
import android.location.Location; import android.location.Location;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.test.annotation.UiThreadTest;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest; import androidx.test.filters.MediumTest;
@ -16,15 +19,17 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import com.otaliastudios.cameraview.controls.Audio; import com.otaliastudios.cameraview.controls.Audio;
import com.otaliastudios.cameraview.controls.AudioCodec;
import com.otaliastudios.cameraview.controls.ControlParser; import com.otaliastudios.cameraview.controls.ControlParser;
import com.otaliastudios.cameraview.controls.Engine; import com.otaliastudios.cameraview.controls.Engine;
import com.otaliastudios.cameraview.controls.Facing; import com.otaliastudios.cameraview.controls.Facing;
import com.otaliastudios.cameraview.controls.Flash; import com.otaliastudios.cameraview.controls.Flash;
import com.otaliastudios.cameraview.controls.PictureFormat;
import com.otaliastudios.cameraview.controls.Preview; import com.otaliastudios.cameraview.controls.Preview;
import com.otaliastudios.cameraview.engine.CameraEngine; import com.otaliastudios.cameraview.engine.CameraEngine;
import com.otaliastudios.cameraview.engine.orchestrator.CameraState;
import com.otaliastudios.cameraview.filter.Filter; import com.otaliastudios.cameraview.filter.Filter;
import com.otaliastudios.cameraview.filter.Filters; import com.otaliastudios.cameraview.filter.Filters;
import com.otaliastudios.cameraview.filter.NoFilter;
import com.otaliastudios.cameraview.filters.DuotoneFilter; import com.otaliastudios.cameraview.filters.DuotoneFilter;
import com.otaliastudios.cameraview.frame.Frame; import com.otaliastudios.cameraview.frame.Frame;
import com.otaliastudios.cameraview.frame.FrameProcessor; import com.otaliastudios.cameraview.frame.FrameProcessor;
@ -40,7 +45,7 @@ import com.otaliastudios.cameraview.gesture.PinchGestureFinder;
import com.otaliastudios.cameraview.gesture.ScrollGestureFinder; import com.otaliastudios.cameraview.gesture.ScrollGestureFinder;
import com.otaliastudios.cameraview.gesture.TapGestureFinder; import com.otaliastudios.cameraview.gesture.TapGestureFinder;
import com.otaliastudios.cameraview.engine.MockCameraEngine; import com.otaliastudios.cameraview.engine.MockCameraEngine;
import com.otaliastudios.cameraview.internal.utils.Op; import com.otaliastudios.cameraview.tools.Op;
import com.otaliastudios.cameraview.markers.AutoFocusMarker; import com.otaliastudios.cameraview.markers.AutoFocusMarker;
import com.otaliastudios.cameraview.markers.DefaultAutoFocusMarker; import com.otaliastudios.cameraview.markers.DefaultAutoFocusMarker;
import com.otaliastudios.cameraview.markers.MarkerLayout; import com.otaliastudios.cameraview.markers.MarkerLayout;
@ -50,6 +55,7 @@ import com.otaliastudios.cameraview.preview.CameraPreview;
import com.otaliastudios.cameraview.size.Size; import com.otaliastudios.cameraview.size.Size;
import com.otaliastudios.cameraview.size.SizeSelector; import com.otaliastudios.cameraview.size.SizeSelector;
import com.otaliastudios.cameraview.size.SizeSelectors; import com.otaliastudios.cameraview.size.SizeSelectors;
import com.otaliastudios.cameraview.tools.SdkExclude;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
@ -127,14 +133,14 @@ public class CameraViewTest extends BaseTest {
public void testClose() { public void testClose() {
cameraView.close(); cameraView.close();
verify(mockPreview, times(1)).onPause(); verify(mockPreview, times(1)).onPause();
verify(mockController, times(1)).stop(); verify(mockController, times(1)).stop(false);
} }
@Test @Test
public void testDestroy() { public void testDestroy() {
cameraView.destroy(); cameraView.destroy();
verify(mockPreview, times(1)).onDestroy(); verify(mockPreview, times(1)).onDestroy();
verify(mockController, times(1)).destroy(); verify(mockController, times(1)).destroy(true);
} }
//region testDefaults //region testDefaults
@ -161,17 +167,29 @@ public class CameraViewTest extends BaseTest {
assertEquals(cameraView.getHdr(), controls.getHdr()); assertEquals(cameraView.getHdr(), controls.getHdr());
assertEquals(cameraView.getAudio(), controls.getAudio()); assertEquals(cameraView.getAudio(), controls.getAudio());
assertEquals(cameraView.getVideoCodec(), controls.getVideoCodec()); assertEquals(cameraView.getVideoCodec(), controls.getVideoCodec());
assertEquals(cameraView.getAudioCodec(), controls.getAudioCodec());
assertEquals(cameraView.getPictureFormat(), controls.getPictureFormat());
//noinspection SimplifiableJUnitAssertion //noinspection SimplifiableJUnitAssertion
assertEquals(cameraView.getLocation(), null); assertEquals(cameraView.getLocation(), null);
assertEquals(cameraView.getExposureCorrection(), 0f, 0f); assertEquals(cameraView.getExposureCorrection(), 0f, 0f);
assertEquals(cameraView.getZoom(), 0f, 0f); assertEquals(cameraView.getZoom(), 0f, 0f);
assertEquals(cameraView.getVideoMaxDuration(), 0, 0); assertEquals(cameraView.getVideoMaxDuration(), 0);
assertEquals(cameraView.getVideoMaxSize(), 0, 0); assertEquals(cameraView.getVideoMaxSize(), 0);
assertEquals(cameraView.getSnapshotMaxWidth(), 0);
assertEquals(cameraView.getSnapshotMaxHeight(), 0);
assertEquals(cameraView.getFrameProcessingMaxWidth(), 0);
assertEquals(cameraView.getFrameProcessingMaxHeight(), 0);
assertEquals(cameraView.getFrameProcessingFormat(), 0);
assertFalse(cameraView.getPreviewFrameRateExact());
// Self managed // Self managed
GestureParser gestures = new GestureParser(empty); GestureParser gestures = new GestureParser(empty);
assertEquals(cameraView.getPlaySounds(), CameraView.DEFAULT_PLAY_SOUNDS); assertEquals(cameraView.getPlaySounds(), CameraView.DEFAULT_PLAY_SOUNDS);
assertEquals(cameraView.getAutoFocusResetDelay(), CameraView.DEFAULT_AUTOFOCUS_RESET_DELAY_MILLIS);
assertEquals(cameraView.getUseDeviceOrientation(), CameraView.DEFAULT_USE_DEVICE_ORIENTATION); assertEquals(cameraView.getUseDeviceOrientation(), CameraView.DEFAULT_USE_DEVICE_ORIENTATION);
assertEquals(cameraView.getPictureMetering(), CameraView.DEFAULT_PICTURE_METERING);
assertEquals(cameraView.getPictureSnapshotMetering(), CameraView.DEFAULT_PICTURE_SNAPSHOT_METERING);
assertEquals(cameraView.getFrameProcessingPoolSize(), CameraView.DEFAULT_FRAME_PROCESSING_POOL_SIZE);
assertEquals(cameraView.getGestureAction(Gesture.TAP), gestures.getTapAction()); assertEquals(cameraView.getGestureAction(Gesture.TAP), gestures.getTapAction());
assertEquals(cameraView.getGestureAction(Gesture.LONG_TAP), gestures.getLongTapAction()); assertEquals(cameraView.getGestureAction(Gesture.LONG_TAP), gestures.getLongTapAction());
assertEquals(cameraView.getGestureAction(Gesture.PINCH), gestures.getPinchAction()); assertEquals(cameraView.getGestureAction(Gesture.PINCH), gestures.getPinchAction());
@ -199,6 +217,17 @@ public class CameraViewTest extends BaseTest {
assertEquals(cameraView.getGestureAction(Gesture.PINCH), GestureAction.NONE); assertEquals(cameraView.getGestureAction(Gesture.PINCH), GestureAction.NONE);
} }
@Test
public void testGesture_enabled() {
// Ensure touch events are intercepted when at least one gesture is set
cameraView.mapGesture(Gesture.PINCH, GestureAction.ZOOM);
assertTrue(cameraView.onInterceptTouchEvent(null));
// Ensure touch events are NOT intercepted when no gestures are set
cameraView.clearGesture(Gesture.PINCH);
assertFalse(cameraView.onInterceptTouchEvent(null));
}
@Test @Test
public void testGesture_enablingDisablingLayouts() { public void testGesture_enablingDisablingLayouts() {
cameraView.clearGesture(Gesture.TAP); cameraView.clearGesture(Gesture.TAP);
@ -234,7 +263,7 @@ public class CameraViewTest extends BaseTest {
public void testGestureAction_capture() { public void testGestureAction_capture() {
CameraOptions o = mock(CameraOptions.class); CameraOptions o = mock(CameraOptions.class);
mockController.setMockCameraOptions(o); mockController.setMockCameraOptions(o);
mockController.setMockEngineState(true); mockController.setMockState(CameraState.PREVIEW);
MotionEvent event = MotionEvent.obtain(0L, 0L, 0, 0f, 0f, 0); MotionEvent event = MotionEvent.obtain(0L, 0L, 0, 0f, 0f, 0);
uiSync(new Runnable() { uiSync(new Runnable() {
@Override @Override
@ -256,7 +285,7 @@ public class CameraViewTest extends BaseTest {
public void testGestureAction_focus() { public void testGestureAction_focus() {
CameraOptions o = mock(CameraOptions.class); CameraOptions o = mock(CameraOptions.class);
mockController.setMockCameraOptions(o); mockController.setMockCameraOptions(o);
mockController.setMockEngineState(true); mockController.setMockState(CameraState.PREVIEW);
MotionEvent event = MotionEvent.obtain(0L, 0L, 0, 0f, 0f, 0); MotionEvent event = MotionEvent.obtain(0L, 0L, 0, 0f, 0f, 0);
uiSync(new Runnable() { uiSync(new Runnable() {
@Override @Override
@ -281,7 +310,7 @@ public class CameraViewTest extends BaseTest {
public void testGestureAction_zoom() { public void testGestureAction_zoom() {
CameraOptions o = mock(CameraOptions.class); CameraOptions o = mock(CameraOptions.class);
mockController.setMockCameraOptions(o); mockController.setMockCameraOptions(o);
mockController.setMockEngineState(true); mockController.setMockState(CameraState.PREVIEW);
mockController.mZoomChanged = false; mockController.mZoomChanged = false;
MotionEvent event = MotionEvent.obtain(0L, 0L, 0, 0f, 0f, 0); MotionEvent event = MotionEvent.obtain(0L, 0L, 0, 0f, 0f, 0);
final FactorHolder factor = new FactorHolder(); final FactorHolder factor = new FactorHolder();
@ -318,11 +347,11 @@ public class CameraViewTest extends BaseTest {
@Test @Test
public void testGestureAction_exposureCorrection() { public void testGestureAction_exposureCorrection() {
CameraOptions o = mock(CameraOptions.class); CameraOptions o = new CameraOptions() {};
when(o.getExposureCorrectionMinValue()).thenReturn(-10f); o.exposureCorrectionMaxValue = 10F;
when(o.getExposureCorrectionMaxValue()).thenReturn(10f); o.exposureCorrectionMinValue = -10F;
mockController.setMockCameraOptions(o); mockController.setMockCameraOptions(o);
mockController.setMockEngineState(true); mockController.setMockState(CameraState.PREVIEW);
mockController.mExposureCorrectionChanged = false; mockController.mExposureCorrectionChanged = false;
MotionEvent event = MotionEvent.obtain(0L, 0L, 0, 0f, 0f, 0); MotionEvent event = MotionEvent.obtain(0L, 0L, 0, 0f, 0f, 0);
final FactorHolder factor = new FactorHolder(); final FactorHolder factor = new FactorHolder();
@ -358,7 +387,7 @@ public class CameraViewTest extends BaseTest {
@Test @Test
public void testGestureAction_filterControl1() { public void testGestureAction_filterControl1() {
mockController.setMockEngineState(true); mockController.setMockState(CameraState.PREVIEW);
mockController.setMockCameraOptions(mock(CameraOptions.class)); mockController.setMockCameraOptions(mock(CameraOptions.class));
DuotoneFilter filter = new DuotoneFilter(); // supports two parameters DuotoneFilter filter = new DuotoneFilter(); // supports two parameters
filter.setParameter1(0F); filter.setParameter1(0F);
@ -400,7 +429,7 @@ public class CameraViewTest extends BaseTest {
@Test @Test
public void testGestureAction_filterControl2() { public void testGestureAction_filterControl2() {
mockController.setMockEngineState(true); mockController.setMockState(CameraState.PREVIEW);
mockController.setMockCameraOptions(mock(CameraOptions.class)); mockController.setMockCameraOptions(mock(CameraOptions.class));
DuotoneFilter filter = new DuotoneFilter(); // supports two parameters DuotoneFilter filter = new DuotoneFilter(); // supports two parameters
filter.setParameter2(0F); filter.setParameter2(0F);
@ -560,9 +589,9 @@ public class CameraViewTest extends BaseTest {
@Test @Test
public void testExposureCorrection() { public void testExposureCorrection() {
// This needs a valid CameraOptions value. // This needs a valid CameraOptions value.
CameraOptions o = mock(CameraOptions.class); CameraOptions o = new CameraOptions() {};
when(o.getExposureCorrectionMinValue()).thenReturn(-10f); o.exposureCorrectionMaxValue = 10F;
when(o.getExposureCorrectionMaxValue()).thenReturn(10f); o.exposureCorrectionMinValue = -10F;
mockController.setMockCameraOptions(o); mockController.setMockCameraOptions(o);
cameraView.setExposureCorrection(5f); cameraView.setExposureCorrection(5f);
@ -619,6 +648,20 @@ public class CameraViewTest extends BaseTest {
cameraView.startAutoFocus(200, 200); cameraView.startAutoFocus(200, 200);
} }
@Test(expected = IllegalArgumentException.class)
public void testStartAutoFocus_illegal3() {
cameraView.startAutoFocus(new RectF(-1, -1, 1, 1));
}
@Test(expected = IllegalArgumentException.class)
public void testStartAutoFocus_illegal4() {
cameraView.setLeft(0);
cameraView.setRight(100);
cameraView.setTop(0);
cameraView.setBottom(100);
cameraView.startAutoFocus(new RectF(-100, -100, 200, 200));
}
@Test @Test
public void testStartAutoFocus() { public void testStartAutoFocus() {
cameraView.setLeft(0); cameraView.setLeft(0);
@ -629,6 +672,16 @@ public class CameraViewTest extends BaseTest {
assertTrue(mockController.mFocusStarted); assertTrue(mockController.mFocusStarted);
} }
@Test
public void testStartAutoFocusRect() {
cameraView.setLeft(0);
cameraView.setRight(100);
cameraView.setTop(0);
cameraView.setBottom(100);
cameraView.startAutoFocus(new RectF(25, 25, 75, 75));
assertTrue(mockController.mFocusStarted);
}
//endregion //endregion
//region test setParameters //region test setParameters
@ -649,6 +702,22 @@ public class CameraViewTest extends BaseTest {
assertFalse(cameraView.getUseDeviceOrientation()); assertFalse(cameraView.getUseDeviceOrientation());
} }
@Test
public void testSetPictureMetering() {
cameraView.setPictureMetering(true);
assertTrue(cameraView.getPictureMetering());
cameraView.setPictureMetering(false);
assertFalse(cameraView.getPictureMetering());
}
@Test
public void testSetPictureSnapshotMetering() {
cameraView.setPictureSnapshotMetering(true);
assertTrue(cameraView.getPictureSnapshotMetering());
cameraView.setPictureSnapshotMetering(false);
assertFalse(cameraView.getPictureSnapshotMetering());
}
@Test @Test
public void testSetFlash() { public void testSetFlash() {
cameraView.set(Flash.TORCH); cameraView.set(Flash.TORCH);
@ -718,6 +787,19 @@ public class CameraViewTest extends BaseTest {
assertEquals(cameraView.get(Audio.class), Audio.STEREO); assertEquals(cameraView.get(Audio.class), Audio.STEREO);
} }
@Test
public void testAudioCodec() {
cameraView.set(AudioCodec.DEVICE_DEFAULT);
assertEquals(cameraView.get(AudioCodec.class), AudioCodec.DEVICE_DEFAULT);
cameraView.set(AudioCodec.AAC);
assertEquals(cameraView.get(AudioCodec.class), AudioCodec.AAC);
cameraView.set(AudioCodec.HE_AAC);
assertEquals(cameraView.get(AudioCodec.class), AudioCodec.HE_AAC);
cameraView.set(AudioCodec.AAC_ELD);
assertEquals(cameraView.get(AudioCodec.class), AudioCodec.AAC_ELD);
}
@Test @Test
public void testVideoCodec() { public void testVideoCodec() {
cameraView.set(VideoCodec.H_263); cameraView.set(VideoCodec.H_263);
@ -726,6 +808,14 @@ public class CameraViewTest extends BaseTest {
assertEquals(cameraView.get(VideoCodec.class), VideoCodec.H_264); assertEquals(cameraView.get(VideoCodec.class), VideoCodec.H_264);
} }
@Test
public void testPictureFormat() {
cameraView.set(PictureFormat.JPEG);
assertEquals(cameraView.get(PictureFormat.class), PictureFormat.JPEG);
cameraView.set(PictureFormat.DNG);
assertEquals(cameraView.get(PictureFormat.class), PictureFormat.DNG);
}
@Test @Test
public void testPreviewStreamSizeSelector() { public void testPreviewStreamSizeSelector() {
SizeSelector source = SizeSelectors.minHeight(50); SizeSelector source = SizeSelectors.minHeight(50);
@ -765,6 +855,73 @@ public class CameraViewTest extends BaseTest {
assertEquals(cameraView.getVideoMaxDuration(), 5000); assertEquals(cameraView.getVideoMaxDuration(), 5000);
} }
@Test
public void testPreviewFrameRate() {
cameraView.setPreviewFrameRate(60);
assertEquals(cameraView.getPreviewFrameRate(), 60, 0);
}
@Test
public void testPreviewFrameRateExact() {
cameraView.setPreviewFrameRateExact(true);
assertTrue(cameraView.getPreviewFrameRateExact());
cameraView.setPreviewFrameRateExact(false);
assertFalse(cameraView.getPreviewFrameRateExact());
}
@Test
public void testSnapshotMaxSize() {
cameraView.setSnapshotMaxWidth(500);
assertEquals(500, cameraView.getSnapshotMaxWidth());
cameraView.setSnapshotMaxHeight(700);
assertEquals(700, cameraView.getSnapshotMaxHeight());
}
@Test
public void testFrameProcessingMaxSize() {
cameraView.setFrameProcessingMaxWidth(500);
assertEquals(500, cameraView.getFrameProcessingMaxWidth());
cameraView.setFrameProcessingMaxHeight(700);
assertEquals(700, cameraView.getFrameProcessingMaxHeight());
}
@Test
public void testFrameProcessingFormat() {
cameraView.setFrameProcessingFormat(ImageFormat.YUV_420_888);
assertEquals(ImageFormat.YUV_420_888, cameraView.getFrameProcessingFormat());
cameraView.setFrameProcessingFormat(ImageFormat.YUV_422_888);
assertEquals(ImageFormat.YUV_422_888, cameraView.getFrameProcessingFormat());
}
@Test
public void testFrameProcessingPoolSize() {
cameraView.setFrameProcessingPoolSize(4);
assertEquals(4, cameraView.getFrameProcessingPoolSize());
cameraView.setFrameProcessingPoolSize(6);
assertEquals(6, cameraView.getFrameProcessingPoolSize());
}
@Test
public void testFrameProcessingExecutors() {
cameraView.setFrameProcessingExecutors(5);
assertEquals(5, cameraView.getFrameProcessingExecutors());
cameraView.setFrameProcessingExecutors(2);
assertEquals(2, cameraView.getFrameProcessingExecutors());
}
@Test(expected = RuntimeException.class)
public void testFrameProcessingExecutors_throws() {
cameraView.setFrameProcessingExecutors(0);
}
@Test
public void testDrawHardwareOverlays() {
cameraView.setDrawHardwareOverlays(true);
assertTrue(cameraView.getDrawHardwareOverlays());
cameraView.setDrawHardwareOverlays(false);
assertFalse(cameraView.getDrawHardwareOverlays());
}
//endregion //endregion
//region Lists of listeners and processors //region Lists of listeners and processors
@ -856,7 +1013,7 @@ public class CameraViewTest extends BaseTest {
cameraView.mMarkerLayout = markerLayout; cameraView.mMarkerLayout = markerLayout;
final PointF point = new PointF(0, 0); final PointF point = new PointF(0, 0);
final PointF[] points = new PointF[]{ point }; final PointF[] points = new PointF[]{ point };
final Op<Boolean> op = new Op<>(true); final Op<Boolean> op = new Op<>();
doEndOp(op, true).when(markerLayout).onEvent(MarkerLayout.TYPE_AUTOFOCUS, points); doEndOp(op, true).when(markerLayout).onEvent(MarkerLayout.TYPE_AUTOFOCUS, points);
cameraView.mCameraCallbacks.dispatchOnFocusStart(Gesture.TAP, point); cameraView.mCameraCallbacks.dispatchOnFocusStart(Gesture.TAP, point);
assertNotNull(op.await(100)); assertNotNull(op.await(100));
@ -887,6 +1044,8 @@ public class CameraViewTest extends BaseTest {
verify(cameraView.mOverlayLayout, never()).generateLayoutParams(any(AttributeSet.class)); verify(cameraView.mOverlayLayout, never()).generateLayoutParams(any(AttributeSet.class));
} }
// Broke in 31 for some reason, no time to investigate but looks like a spy() issue.
@SdkExclude(minSdkVersion = 31)
@Test @Test
public void testOverlays_addOverlayView() { public void testOverlays_addOverlayView() {
cameraView.mOverlayLayout = spy(cameraView.mOverlayLayout); cameraView.mOverlayLayout = spy(cameraView.mOverlayLayout);
@ -911,27 +1070,41 @@ public class CameraViewTest extends BaseTest {
verify(cameraView.mOverlayLayout, never()).addView(overlay, params); verify(cameraView.mOverlayLayout, never()).addView(overlay, params);
} }
//endregion // Broke in 31 for some reason, no time to investigate but looks like a spy() issue.
// TODO: test permissions @SdkExclude(minSdkVersion = 31)
@Test
//region Filter public void testOverlays_removeOverlayView() {
// First add one.
@Test(expected = RuntimeException.class) cameraView.mOverlayLayout = spy(cameraView.mOverlayLayout);
public void testSetFilter_notExperimental() { View overlay = new View(getContext());
cameraView.setExperimental(false); OverlayLayout.LayoutParams params = new OverlayLayout.LayoutParams(10, 10);
cameraView.setFilter(Filters.AUTO_FIX.newInstance()); cameraView.addView(overlay, 0, params);
reset(cameraView.mOverlayLayout);
// Then remove.
cameraView.removeView(overlay);
verify(cameraView.mOverlayLayout, times(1)).isOverlay(params);
verify(cameraView.mOverlayLayout, times(1)).removeView(overlay);
} }
@Test @Test
public void testSetFilter_notExperimental_noFilter() { public void testOverlays_dontRemoveOverlayView() {
cameraView.setExperimental(false); // First add a view.
cameraView.setFilter(Filters.NONE.newInstance()); cameraView.mOverlayLayout = spy(cameraView.mOverlayLayout);
// no exception thrown View notOverlay = new View(getContext());
ViewGroup.LayoutParams notOverlayParams = new ViewGroup.LayoutParams(10, 10);
cameraView.addView(notOverlay, notOverlayParams);
reset(cameraView.mOverlayLayout);
// Then remove.
cameraView.removeView(notOverlay);
verify(cameraView.mOverlayLayout, never()).removeView(notOverlay);
} }
//endregion
//region Filter
@Test @Test
public void testSetFilter() { public void testSetFilter() {
cameraView.setExperimental(true);
Filter filter = Filters.AUTO_FIX.newInstance(); Filter filter = Filters.AUTO_FIX.newInstance();
cameraView.setFilter(filter); cameraView.setFilter(filter);
verify(mockPreview, times(1)).setFilter(filter); verify(mockPreview, times(1)).setFilter(filter);
@ -939,4 +1112,6 @@ public class CameraViewTest extends BaseTest {
//noinspection ResultOfMethodCallIgnored //noinspection ResultOfMethodCallIgnored
verify(mockPreview, times(1)).getCurrentFilter(); verify(mockPreview, times(1)).getCurrentFilter();
} }
//endregion
} }

@ -1,17 +0,0 @@
package com.otaliastudios.cameraview;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Thanks to a testInstrumentationRunnerArgument in our build file, we will not
* execute these tests on Travis CI.
* The {@link RetentionPolicy#RUNTIME} is needed!
*
* https://developer.android.com/reference/android/support/test/runner/AndroidJUnitRunner
*
*/
@Retention(RetentionPolicy.RUNTIME)
public @interface DoNotRunOnTravis {
String because() default "";
}

@ -4,6 +4,7 @@ package com.otaliastudios.cameraview;
import android.location.Location; import android.location.Location;
import com.otaliastudios.cameraview.controls.Facing; import com.otaliastudios.cameraview.controls.Facing;
import com.otaliastudios.cameraview.controls.PictureFormat;
import com.otaliastudios.cameraview.size.Size; import com.otaliastudios.cameraview.size.Size;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
@ -24,7 +25,7 @@ public class PictureResultTest extends BaseTest {
@Test @Test
public void testResult() { public void testResult() {
int format = PictureResult.FORMAT_JPEG; PictureFormat format = PictureFormat.JPEG;
int rotation = 90; int rotation = 90;
Size size = new Size(20, 120); Size size = new Size(20, 120);
byte[] jpeg = new byte[]{2, 4, 1, 5, 2}; byte[] jpeg = new byte[]{2, 4, 1, 5, 2};

@ -4,6 +4,7 @@ package com.otaliastudios.cameraview;
import android.location.Location; import android.location.Location;
import com.otaliastudios.cameraview.controls.Audio; import com.otaliastudios.cameraview.controls.Audio;
import com.otaliastudios.cameraview.controls.AudioCodec;
import com.otaliastudios.cameraview.controls.Facing; import com.otaliastudios.cameraview.controls.Facing;
import com.otaliastudios.cameraview.controls.VideoCodec; import com.otaliastudios.cameraview.controls.VideoCodec;
import com.otaliastudios.cameraview.size.Size; import com.otaliastudios.cameraview.size.Size;
@ -16,6 +17,7 @@ import org.junit.runner.RunWith;
import org.mockito.Mockito; import org.mockito.Mockito;
import java.io.File; import java.io.File;
import java.io.FileDescriptor;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
@ -27,11 +29,12 @@ public class VideoResultTest extends BaseTest {
private VideoResult.Stub stub = new VideoResult.Stub(); private VideoResult.Stub stub = new VideoResult.Stub();
@Test @Test
public void testResult() { public void testResultWithFile() {
File file = Mockito.mock(File.class); File file = Mockito.mock(File.class);
int rotation = 90; int rotation = 90;
Size size = new Size(20, 120); Size size = new Size(20, 120);
VideoCodec codec = VideoCodec.H_263; VideoCodec videoCodec = VideoCodec.H_263;
AudioCodec audioCodec = AudioCodec.DEVICE_DEFAULT;
Location location = Mockito.mock(Location.class); Location location = Mockito.mock(Location.class);
boolean isSnapshot = true; boolean isSnapshot = true;
int maxDuration = 1234; int maxDuration = 1234;
@ -46,7 +49,8 @@ public class VideoResultTest extends BaseTest {
stub.file = file; stub.file = file;
stub.rotation = rotation; stub.rotation = rotation;
stub.size = size; stub.size = size;
stub.videoCodec = codec; stub.videoCodec = videoCodec;
stub.audioCodec = audioCodec;
stub.location = location; stub.location = location;
stub.isSnapshot = isSnapshot; stub.isSnapshot = isSnapshot;
stub.maxDuration = maxDuration; stub.maxDuration = maxDuration;
@ -62,7 +66,8 @@ public class VideoResultTest extends BaseTest {
assertEquals(result.getFile(), file); assertEquals(result.getFile(), file);
assertEquals(result.getRotation(), rotation); assertEquals(result.getRotation(), rotation);
assertEquals(result.getSize(), size); assertEquals(result.getSize(), size);
assertEquals(result.getVideoCodec(), codec); assertEquals(result.getVideoCodec(), videoCodec);
assertEquals(result.getAudioCodec(), audioCodec);
assertEquals(result.getLocation(), location); assertEquals(result.getLocation(), location);
assertEquals(result.isSnapshot(), isSnapshot); assertEquals(result.isSnapshot(), isSnapshot);
assertEquals(result.getMaxSize(), maxFileSize); assertEquals(result.getMaxSize(), maxFileSize);
@ -73,6 +78,69 @@ public class VideoResultTest extends BaseTest {
assertEquals(result.getAudioBitRate(), audioBitRate); assertEquals(result.getAudioBitRate(), audioBitRate);
assertEquals(result.getAudio(), audio); assertEquals(result.getAudio(), audio);
assertEquals(result.getFacing(), facing); assertEquals(result.getFacing(), facing);
}
@Test
public void testResultWithFileDescriptor() {
FileDescriptor fileDescriptor = FileDescriptor.in;
int rotation = 90;
Size size = new Size(20, 120);
VideoCodec videoCodec = VideoCodec.H_263;
AudioCodec audioCodec = AudioCodec.DEVICE_DEFAULT;
Location location = Mockito.mock(Location.class);
boolean isSnapshot = true;
int maxDuration = 1234;
long maxFileSize = 500000;
int reason = VideoResult.REASON_MAX_DURATION_REACHED;
int videoFrameRate = 30;
int videoBitRate = 300000;
int audioBitRate = 30000;
Audio audio = Audio.ON;
Facing facing = Facing.FRONT;
stub.fileDescriptor = fileDescriptor;
stub.rotation = rotation;
stub.size = size;
stub.videoCodec = videoCodec;
stub.audioCodec = audioCodec;
stub.location = location;
stub.isSnapshot = isSnapshot;
stub.maxDuration = maxDuration;
stub.maxSize = maxFileSize;
stub.endReason = reason;
stub.videoFrameRate = videoFrameRate;
stub.videoBitRate = videoBitRate;
stub.audioBitRate = audioBitRate;
stub.audio = audio;
stub.facing = facing;
VideoResult result = new VideoResult(stub);
assertEquals(result.getFileDescriptor(), fileDescriptor);
assertEquals(result.getRotation(), rotation);
assertEquals(result.getSize(), size);
assertEquals(result.getVideoCodec(), videoCodec);
assertEquals(result.getAudioCodec(), audioCodec);
assertEquals(result.getLocation(), location);
assertEquals(result.isSnapshot(), isSnapshot);
assertEquals(result.getMaxSize(), maxFileSize);
assertEquals(result.getMaxDuration(), maxDuration);
assertEquals(result.getTerminationReason(), reason);
assertEquals(result.getVideoFrameRate(), videoFrameRate);
assertEquals(result.getVideoBitRate(), videoBitRate);
assertEquals(result.getAudioBitRate(), audioBitRate);
assertEquals(result.getAudio(), audio);
assertEquals(result.getFacing(), facing);
}
@Test(expected = RuntimeException.class)
public void testResultWithNoFile() {
VideoResult result = new VideoResult(stub);
result.getFile();
}
@Test(expected = RuntimeException.class)
public void testResultWithNoFileDescriptor() {
VideoResult result = new VideoResult(stub);
result.getFileDescriptor();
} }
} }

@ -0,0 +1,51 @@
package com.otaliastudios.cameraview.engine;
import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.CameraOptions;
import com.otaliastudios.cameraview.controls.Engine;
import com.otaliastudios.cameraview.frame.Frame;
import com.otaliastudios.cameraview.frame.FrameProcessor;
import com.otaliastudios.cameraview.tools.Op;
import com.otaliastudios.cameraview.tools.Retry;
import com.otaliastudios.cameraview.tools.SdkExclude;
import org.junit.Test;
import org.junit.runner.RunWith;
import androidx.annotation.NonNull;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.filters.RequiresDevice;
import java.util.Collection;
import static org.junit.Assert.assertNotNull;
/**
* These tests work great on real devices, and are the only way to test actual CameraEngine
* implementation - we really need to open the camera device.
* Unfortunately they fail unreliably on emulated devices, due to some bug with the
* emulated camera controller.
*/
@RunWith(AndroidJUnit4.class)
@LargeTest
// @RequiresDevice
public class Camera1IntegrationTest extends CameraIntegrationTest<Camera1Engine> {
@NonNull
@Override
protected Engine getEngine() {
return Engine.CAMERA1;
}
@Override
protected long getMeteringTimeoutMillis() {
return Camera1Engine.AUTOFOCUS_END_DELAY_MILLIS;
}
@Override
public void testFrameProcessing_maxSize() {
// Camera1Engine does not support different sizes.
// super.testFrameProcessing_maxSize();
}
}

@ -0,0 +1,86 @@
package com.otaliastudios.cameraview.engine;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.TotalCaptureResult;
import android.os.Handler;
import com.otaliastudios.cameraview.controls.Engine;
import com.otaliastudios.cameraview.engine.action.ActionHolder;
import com.otaliastudios.cameraview.engine.action.BaseAction;
import org.junit.Test;
import org.junit.runner.RunWith;
import androidx.annotation.NonNull;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.filters.RequiresDevice;
import java.util.concurrent.CountDownLatch;
/**
* These tests work great on real devices, and are the only way to test actual CameraEngine
* implementation - we really need to open the camera device.
* Unfortunately they fail unreliably on emulated devices, due to some bug with the
* emulated camera controller.
*/
@RunWith(AndroidJUnit4.class)
@LargeTest
// @RequiresDevice
public class Camera2IntegrationTest extends CameraIntegrationTest<Camera2Engine> {
@NonNull
@Override
protected Engine getEngine() {
return Engine.CAMERA2;
}
/* @Override
protected void onOpenSync() {
super.onOpenSync();
// Extra wait for the first frame to be dispatched.
// This is because various classes require getLastResult to be non-null
// and that's typically the case in a real app.
final CountDownLatch latch = new CountDownLatch(1);
new BaseAction() {
@Override
public void onCaptureCompleted(@NonNull ActionHolder holder,
@NonNull CaptureRequest request,
@NonNull TotalCaptureResult result) {
super.onCaptureCompleted(holder, request, result);
latch.countDown();
setState(STATE_COMPLETED);
}
}.start(controller);
try { latch.await(); } catch (InterruptedException ignore) {}
} */
@Override
protected long getMeteringTimeoutMillis() {
return Camera2Engine.METER_TIMEOUT;
}
/**
* setMaxDuration can crash on legacy devices (most emulator are), and I don't see
* any way to fix this in code. They shouldn't use Camera2 at all.
* @return true if possible.
*/
@Override
protected boolean canSetVideoMaxDuration() {
if (!super.canSetVideoMaxDuration()) return false;
boolean shouldOpen = !camera.isOpened();
if (shouldOpen) openSync(true);
boolean result = controller.readCharacteristic(
CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL, -1)
!= CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY;
if (shouldOpen) closeSync(true);
return result;
}
@Override
public void testFrameProcessing_freezeRelease() {
// Camera2 Frames are not freezable.
// super.testFrameProcessing_freezeRelease();
}
}

@ -1,30 +0,0 @@
package com.otaliastudios.cameraview.engine;
import com.otaliastudios.cameraview.DoNotRunOnTravis;
import com.otaliastudios.cameraview.controls.Engine;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import androidx.annotation.NonNull;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
/**
* These tests work great on real devices, and are the only way to test actual CameraEngine
* implementation - we really need to open the camera device.
* Unfortunately they fail unreliably on emulated devices, due to some bug with the
* emulated camera controller.
*/
@RunWith(AndroidJUnit4.class)
@LargeTest
@DoNotRunOnTravis(because = "These do work but fail on CI emulators, due to bugs in the Camera1 emulated devices.")
public class CameraIntegration1Test extends CameraIntegrationTest {
@NonNull
@Override
protected Engine getEngine() {
return Engine.CAMERA1;
}
}

@ -1,30 +0,0 @@
package com.otaliastudios.cameraview.engine;
import com.otaliastudios.cameraview.DoNotRunOnTravis;
import com.otaliastudios.cameraview.controls.Engine;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import androidx.annotation.NonNull;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
/**
* These tests work great on real devices, and are the only way to test actual CameraEngine
* implementation - we really need to open the camera device.
* Unfortunately they fail unreliably on emulated devices, due to some bug with the
* emulated camera controller.
*/
@RunWith(AndroidJUnit4.class)
@LargeTest
@DoNotRunOnTravis(because = "These do work but fail on CI emulators.")
public class CameraIntegration2Test extends CameraIntegrationTest {
@NonNull
@Override
protected Engine getEngine() {
return Engine.CAMERA2;
}
}

@ -2,6 +2,7 @@ package com.otaliastudios.cameraview.engine;
import android.graphics.PointF; import android.graphics.PointF;
import android.graphics.RectF;
import android.location.Location; import android.location.Location;
import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.Task;
@ -11,10 +12,14 @@ import com.otaliastudios.cameraview.PictureResult;
import com.otaliastudios.cameraview.VideoResult; import com.otaliastudios.cameraview.VideoResult;
import com.otaliastudios.cameraview.controls.Facing; import com.otaliastudios.cameraview.controls.Facing;
import com.otaliastudios.cameraview.controls.Flash; import com.otaliastudios.cameraview.controls.Flash;
import com.otaliastudios.cameraview.controls.PictureFormat;
import com.otaliastudios.cameraview.engine.orchestrator.CameraState;
import com.otaliastudios.cameraview.frame.ByteBufferFrameManager;
import com.otaliastudios.cameraview.frame.FrameManager; import com.otaliastudios.cameraview.frame.FrameManager;
import com.otaliastudios.cameraview.gesture.Gesture; import com.otaliastudios.cameraview.gesture.Gesture;
import com.otaliastudios.cameraview.controls.Hdr; import com.otaliastudios.cameraview.controls.Hdr;
import com.otaliastudios.cameraview.controls.WhiteBalance; import com.otaliastudios.cameraview.controls.WhiteBalance;
import com.otaliastudios.cameraview.metering.MeteringRegions;
import com.otaliastudios.cameraview.size.AspectRatio; import com.otaliastudios.cameraview.size.AspectRatio;
import com.otaliastudios.cameraview.size.Size; import com.otaliastudios.cameraview.size.Size;
@ -23,8 +28,9 @@ import androidx.annotation.Nullable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.Callable;
public class MockCameraEngine extends CameraEngine { public class MockCameraEngine extends CameraBaseEngine {
public boolean mPictureCaptured; public boolean mPictureCaptured;
public boolean mFocusStarted; public boolean mFocusStarted;
@ -37,8 +43,8 @@ public class MockCameraEngine extends CameraEngine {
@NonNull @NonNull
@Override @Override
protected Task<Void> onStartEngine() { protected Task<CameraOptions> onStartEngine() {
return Tasks.forResult(null); return Tasks.forResult(mCameraOptions);
} }
@NonNull @NonNull
@ -79,8 +85,19 @@ public class MockCameraEngine extends CameraEngine {
mPreviewStreamSize = size; mPreviewStreamSize = size;
} }
public void setMockEngineState(boolean started) { public void setMockState(@NonNull CameraState state) {
mEngineStep.setState(started ? STATE_STARTED : STATE_STOPPED); Task<Void> change = getOrchestrator().scheduleStateChange(getState(),
state,
false,
new Callable<Task<Void>>() {
@Override
public Task<Void> call() {
return Tasks.forResult(null);
}
});
try {
Tasks.await(change);
} catch (Exception ignore) {}
} }
@Override @Override
@ -95,7 +112,6 @@ public class MockCameraEngine extends CameraEngine {
mExposureCorrectionChanged = true; mExposureCorrectionChanged = true;
} }
@Override @Override
public void setFlash(@NonNull Flash flash) { public void setFlash(@NonNull Flash flash) {
mFlash = flash; mFlash = flash;
@ -116,6 +132,21 @@ public class MockCameraEngine extends CameraEngine {
mLocation = location; mLocation = location;
} }
@Override
public void setPictureFormat(@NonNull PictureFormat pictureFormat) {
mPictureFormat = pictureFormat;
}
@Override
public void setHasFrameProcessors(boolean hasFrameProcessors) {
mHasFrameProcessors = hasFrameProcessors;
}
@Override
public void setFrameProcessingFormat(int format) {
mFrameProcessingFormat = format;
}
@Override @Override
public void takePicture(@NonNull PictureResult.Stub stub) { public void takePicture(@NonNull PictureResult.Stub stub) {
super.takePicture(stub); super.takePicture(stub);
@ -123,12 +154,12 @@ public class MockCameraEngine extends CameraEngine {
} }
@Override @Override
protected void onTakePicture(@NonNull PictureResult.Stub stub) { protected void onTakePicture(@NonNull PictureResult.Stub stub, boolean doMetering) {
} }
@Override @Override
protected void onTakePictureSnapshot(@NonNull PictureResult.Stub stub, @NonNull AspectRatio outputRatio) { protected void onTakePictureSnapshot(@NonNull PictureResult.Stub stub, @NonNull AspectRatio outputRatio, boolean doMetering) {
} }
@ -153,24 +184,32 @@ public class MockCameraEngine extends CameraEngine {
return new ArrayList<>(); return new ArrayList<>();
} }
@NonNull
@Override @Override
public void startAutoFocus(@Nullable Gesture gesture, @NonNull PointF point) { protected List<Size> getFrameProcessingAvailableSizes() {
return new ArrayList<>();
}
@Override
public void startAutoFocus(@Nullable Gesture gesture, @NonNull MeteringRegions regions, @NonNull PointF legacyPoint) {
mFocusStarted = true; mFocusStarted = true;
} }
@NonNull @NonNull
@Override @Override
protected FrameManager instantiateFrameManager() { protected FrameManager instantiateFrameManager(int poolSize) {
return new FrameManager(2, null); return new ByteBufferFrameManager(poolSize, null);
} }
@Override @Override
public void setPlaySounds(boolean playSounds) { public void setPlaySounds(boolean playSounds) { }
}
@Override @Override
protected boolean collectCameraInfo(@NonNull Facing facing) { protected boolean collectCameraInfo(@NonNull Facing facing) {
return true; return true;
} }
@Override public void setPreviewFrameRate(float previewFrameRate) {
mPreviewFrameRate = previewFrameRate;
}
} }

@ -1,14 +1,14 @@
package com.otaliastudios.cameraview.engine; package com.otaliastudios.cameraview.engine.mappers;
import android.hardware.Camera; import android.hardware.Camera;
import com.otaliastudios.cameraview.BaseTest; import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.controls.Engine;
import com.otaliastudios.cameraview.controls.Facing; import com.otaliastudios.cameraview.controls.Facing;
import com.otaliastudios.cameraview.controls.Flash; import com.otaliastudios.cameraview.controls.Flash;
import com.otaliastudios.cameraview.controls.Hdr; import com.otaliastudios.cameraview.controls.Hdr;
import com.otaliastudios.cameraview.controls.WhiteBalance; import com.otaliastudios.cameraview.controls.WhiteBalance;
import com.otaliastudios.cameraview.engine.mappers.Camera1Mapper;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest; import androidx.test.filters.SmallTest;
@ -21,28 +21,28 @@ import static org.junit.Assert.*;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
@SmallTest @SmallTest
public class Mapper1Test extends BaseTest { public class Camera1MapperTest extends BaseTest {
private Mapper mapper = Mapper.get(Engine.CAMERA1); private Camera1Mapper mapper = Camera1Mapper.get();
@Test @Test
public void testMap() { public void testMap() {
assertEquals(mapper.map(Flash.OFF), Camera.Parameters.FLASH_MODE_OFF); assertEquals(mapper.mapFlash(Flash.OFF), Camera.Parameters.FLASH_MODE_OFF);
assertEquals(mapper.map(Flash.ON), Camera.Parameters.FLASH_MODE_ON); assertEquals(mapper.mapFlash(Flash.ON), Camera.Parameters.FLASH_MODE_ON);
assertEquals(mapper.map(Flash.AUTO), Camera.Parameters.FLASH_MODE_AUTO); assertEquals(mapper.mapFlash(Flash.AUTO), Camera.Parameters.FLASH_MODE_AUTO);
assertEquals(mapper.map(Flash.TORCH), Camera.Parameters.FLASH_MODE_TORCH); assertEquals(mapper.mapFlash(Flash.TORCH), Camera.Parameters.FLASH_MODE_TORCH);
assertEquals(mapper.map(Facing.BACK), Camera.CameraInfo.CAMERA_FACING_BACK); assertEquals(mapper.mapFacing(Facing.BACK), Camera.CameraInfo.CAMERA_FACING_BACK);
assertEquals(mapper.map(Facing.FRONT), Camera.CameraInfo.CAMERA_FACING_FRONT); assertEquals(mapper.mapFacing(Facing.FRONT), Camera.CameraInfo.CAMERA_FACING_FRONT);
assertEquals(mapper.map(Hdr.OFF), Camera.Parameters.SCENE_MODE_AUTO); assertEquals(mapper.mapHdr(Hdr.OFF), Camera.Parameters.SCENE_MODE_AUTO);
assertEquals(mapper.map(Hdr.ON), Camera.Parameters.SCENE_MODE_HDR); assertEquals(mapper.mapHdr(Hdr.ON), Camera.Parameters.SCENE_MODE_HDR);
assertEquals(mapper.map(WhiteBalance.AUTO), Camera.Parameters.WHITE_BALANCE_AUTO); assertEquals(mapper.mapWhiteBalance(WhiteBalance.AUTO), Camera.Parameters.WHITE_BALANCE_AUTO);
assertEquals(mapper.map(WhiteBalance.DAYLIGHT), Camera.Parameters.WHITE_BALANCE_DAYLIGHT); assertEquals(mapper.mapWhiteBalance(WhiteBalance.DAYLIGHT), Camera.Parameters.WHITE_BALANCE_DAYLIGHT);
assertEquals(mapper.map(WhiteBalance.CLOUDY), Camera.Parameters.WHITE_BALANCE_CLOUDY_DAYLIGHT); assertEquals(mapper.mapWhiteBalance(WhiteBalance.CLOUDY), Camera.Parameters.WHITE_BALANCE_CLOUDY_DAYLIGHT);
assertEquals(mapper.map(WhiteBalance.INCANDESCENT), Camera.Parameters.WHITE_BALANCE_INCANDESCENT); assertEquals(mapper.mapWhiteBalance(WhiteBalance.INCANDESCENT), Camera.Parameters.WHITE_BALANCE_INCANDESCENT);
assertEquals(mapper.map(WhiteBalance.FLUORESCENT), Camera.Parameters.WHITE_BALANCE_FLUORESCENT); assertEquals(mapper.mapWhiteBalance(WhiteBalance.FLUORESCENT), Camera.Parameters.WHITE_BALANCE_FLUORESCENT);
} }

@ -0,0 +1,101 @@
package com.otaliastudios.cameraview.engine.mappers;
import android.hardware.Camera;
import android.util.Pair;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.controls.Facing;
import com.otaliastudios.cameraview.controls.Flash;
import com.otaliastudios.cameraview.controls.Hdr;
import com.otaliastudios.cameraview.controls.WhiteBalance;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.List;
import java.util.Set;
import static android.hardware.camera2.CameraMetadata.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class Camera2MapperTest extends BaseTest {
private Camera2Mapper mapper = Camera2Mapper.get();
@Test
public void testMap() {
List<Pair<Integer, Integer>> values = mapper.mapFlash(Flash.OFF);
assertEquals(2, values.size());
assertTrue(values.contains(new Pair<>(CONTROL_AE_MODE_ON, FLASH_MODE_OFF)));
assertTrue(values.contains(new Pair<>(CONTROL_AE_MODE_OFF, FLASH_MODE_OFF)));
values = mapper.mapFlash(Flash.TORCH);
assertEquals(2, values.size());
assertTrue(values.contains(new Pair<>(CONTROL_AE_MODE_ON, FLASH_MODE_TORCH)));
assertTrue(values.contains(new Pair<>(CONTROL_AE_MODE_OFF, FLASH_MODE_TORCH)));
values = mapper.mapFlash(Flash.AUTO);
assertEquals(2, values.size());
assertTrue(values.contains(new Pair<>(CONTROL_AE_MODE_ON_AUTO_FLASH, FLASH_MODE_OFF)));
assertTrue(values.contains(new Pair<>(CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE, FLASH_MODE_OFF)));
values = mapper.mapFlash(Flash.ON);
assertEquals(1, values.size());
assertTrue(values.contains(new Pair<>(CONTROL_AE_MODE_ON_ALWAYS_FLASH, FLASH_MODE_OFF)));
assertEquals(mapper.mapFacing(Facing.BACK), LENS_FACING_BACK);
assertEquals(mapper.mapFacing(Facing.FRONT), LENS_FACING_FRONT);
assertEquals(mapper.mapHdr(Hdr.OFF), CONTROL_SCENE_MODE_DISABLED);
assertEquals(mapper.mapHdr(Hdr.ON), CONTROL_SCENE_MODE_HDR);
assertEquals(mapper.mapWhiteBalance(WhiteBalance.AUTO), CONTROL_AWB_MODE_AUTO);
assertEquals(mapper.mapWhiteBalance(WhiteBalance.DAYLIGHT), CONTROL_AWB_MODE_DAYLIGHT);
assertEquals(mapper.mapWhiteBalance(WhiteBalance.CLOUDY), CONTROL_AWB_MODE_CLOUDY_DAYLIGHT);
assertEquals(mapper.mapWhiteBalance(WhiteBalance.INCANDESCENT), CONTROL_AWB_MODE_INCANDESCENT);
assertEquals(mapper.mapWhiteBalance(WhiteBalance.FLUORESCENT), CONTROL_AWB_MODE_FLUORESCENT);
}
@Test
public void testUnmap() {
Set<Flash> values;
values = mapper.unmapFlash(CONTROL_AE_MODE_OFF);
assertEquals(values.size(), 2);
assertTrue(values.contains(Flash.OFF));
assertTrue(values.contains(Flash.TORCH));
values = mapper.unmapFlash(CONTROL_AE_MODE_ON);
assertEquals(values.size(), 2);
assertTrue(values.contains(Flash.OFF));
assertTrue(values.contains(Flash.TORCH));
values = mapper.unmapFlash(CONTROL_AE_MODE_ON_ALWAYS_FLASH);
assertEquals(values.size(), 1);
assertTrue(values.contains(Flash.ON));
values = mapper.unmapFlash(CONTROL_AE_MODE_ON_AUTO_FLASH);
assertEquals(values.size(), 1);
assertTrue(values.contains(Flash.AUTO));
values = mapper.unmapFlash(CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE);
assertEquals(values.size(), 1);
assertTrue(values.contains(Flash.AUTO));
values = mapper.unmapFlash(CONTROL_AE_MODE_ON_EXTERNAL_FLASH);
assertEquals(values.size(), 0);
assertEquals(Facing.BACK, mapper.unmapFacing(LENS_FACING_BACK));
assertEquals(Facing.FRONT, mapper.unmapFacing(LENS_FACING_FRONT));
assertEquals(Hdr.OFF, mapper.unmapHdr(CONTROL_SCENE_MODE_DISABLED));
assertEquals(Hdr.ON, mapper.unmapHdr(CONTROL_SCENE_MODE_HDR));
assertEquals(WhiteBalance.AUTO, mapper.unmapWhiteBalance(CONTROL_AWB_MODE_AUTO));
assertEquals(WhiteBalance.DAYLIGHT, mapper.unmapWhiteBalance(CONTROL_AWB_MODE_DAYLIGHT));
assertEquals(WhiteBalance.CLOUDY, mapper.unmapWhiteBalance(CONTROL_AWB_MODE_CLOUDY_DAYLIGHT));
assertEquals(WhiteBalance.INCANDESCENT, mapper.unmapWhiteBalance(CONTROL_AWB_MODE_INCANDESCENT));
assertEquals(WhiteBalance.FLUORESCENT, mapper.unmapWhiteBalance(CONTROL_AWB_MODE_FLUORESCENT));
}
}

@ -1,13 +1,17 @@
package com.otaliastudios.cameraview; package com.otaliastudios.cameraview.engine.options;
import android.graphics.ImageFormat;
import android.hardware.Camera; import android.hardware.Camera;
import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.CameraOptions;
import com.otaliastudios.cameraview.controls.Audio; import com.otaliastudios.cameraview.controls.Audio;
import com.otaliastudios.cameraview.controls.Engine; import com.otaliastudios.cameraview.controls.AudioCodec;
import com.otaliastudios.cameraview.controls.Facing; import com.otaliastudios.cameraview.controls.Facing;
import com.otaliastudios.cameraview.controls.Flash; import com.otaliastudios.cameraview.controls.Flash;
import com.otaliastudios.cameraview.engine.Mapper; import com.otaliastudios.cameraview.controls.PictureFormat;
import com.otaliastudios.cameraview.engine.mappers.Camera1Mapper;
import com.otaliastudios.cameraview.gesture.GestureAction; import com.otaliastudios.cameraview.gesture.GestureAction;
import com.otaliastudios.cameraview.controls.Grid; import com.otaliastudios.cameraview.controls.Grid;
import com.otaliastudios.cameraview.controls.Hdr; import com.otaliastudios.cameraview.controls.Hdr;
@ -38,11 +42,11 @@ import static org.mockito.Mockito.when;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
@SmallTest @SmallTest
public class CameraOptions1Test extends BaseTest { public class Camera1OptionsTest extends BaseTest {
@Test @Test
public void testEmpty() { public void testEmpty() {
CameraOptions o = new CameraOptions(mock(Camera.Parameters.class), false); CameraOptions o = new Camera1Options(mock(Camera.Parameters.class), 0, false);
assertTrue(o.getSupportedPictureAspectRatios().isEmpty()); assertTrue(o.getSupportedPictureAspectRatios().isEmpty());
assertTrue(o.getSupportedPictureSizes().isEmpty()); assertTrue(o.getSupportedPictureSizes().isEmpty());
assertTrue(o.getSupportedWhiteBalance().isEmpty()); assertTrue(o.getSupportedWhiteBalance().isEmpty());
@ -53,6 +57,11 @@ public class CameraOptions1Test extends BaseTest {
assertFalse(o.isZoomSupported()); assertFalse(o.isZoomSupported());
assertEquals(o.getExposureCorrectionMaxValue(), 0f, 0); assertEquals(o.getExposureCorrectionMaxValue(), 0f, 0);
assertEquals(o.getExposureCorrectionMinValue(), 0f, 0); assertEquals(o.getExposureCorrectionMinValue(), 0f, 0);
// Static
assertEquals(1, o.getSupportedPictureFormats().size());
assertTrue(o.getSupportedPictureFormats().contains(PictureFormat.JPEG));
assertEquals(1, o.getSupportedFrameProcessingFormats().size());
assertTrue(o.getSupportedFrameProcessingFormats().contains(ImageFormat.NV21));
} }
private Camera.Size mockCameraSize(int width, int height) { private Camera.Size mockCameraSize(int width, int height) {
@ -72,7 +81,7 @@ public class CameraOptions1Test extends BaseTest {
); );
Camera.Parameters params = mock(Camera.Parameters.class); Camera.Parameters params = mock(Camera.Parameters.class);
when(params.getSupportedPictureSizes()).thenReturn(sizes); when(params.getSupportedPictureSizes()).thenReturn(sizes);
CameraOptions o = new CameraOptions(params, false); CameraOptions o = new Camera1Options(params, 0, false);
Collection<Size> supportedSizes = o.getSupportedPictureSizes(); Collection<Size> supportedSizes = o.getSupportedPictureSizes();
assertEquals(supportedSizes.size(), sizes.size()); assertEquals(supportedSizes.size(), sizes.size());
for (Camera.Size size : sizes) { for (Camera.Size size : sizes) {
@ -91,7 +100,7 @@ public class CameraOptions1Test extends BaseTest {
); );
Camera.Parameters params = mock(Camera.Parameters.class); Camera.Parameters params = mock(Camera.Parameters.class);
when(params.getSupportedPictureSizes()).thenReturn(sizes); when(params.getSupportedPictureSizes()).thenReturn(sizes);
CameraOptions o = new CameraOptions(params, true); CameraOptions o = new Camera1Options(params, 0, true);
Collection<Size> supportedSizes = o.getSupportedPictureSizes(); Collection<Size> supportedSizes = o.getSupportedPictureSizes();
assertEquals(supportedSizes.size(), sizes.size()); assertEquals(supportedSizes.size(), sizes.size());
for (Camera.Size size : sizes) { for (Camera.Size size : sizes) {
@ -116,7 +125,7 @@ public class CameraOptions1Test extends BaseTest {
Camera.Parameters params = mock(Camera.Parameters.class); Camera.Parameters params = mock(Camera.Parameters.class);
when(params.getSupportedPictureSizes()).thenReturn(sizes); when(params.getSupportedPictureSizes()).thenReturn(sizes);
CameraOptions o = new CameraOptions(params, false); CameraOptions o = new Camera1Options(params, 0, false);
Collection<AspectRatio> supportedRatios = o.getSupportedPictureAspectRatios(); Collection<AspectRatio> supportedRatios = o.getSupportedPictureAspectRatios();
assertEquals(supportedRatios.size(), expected.size()); assertEquals(supportedRatios.size(), expected.size());
for (AspectRatio ratio : expected) { for (AspectRatio ratio : expected) {
@ -127,15 +136,17 @@ public class CameraOptions1Test extends BaseTest {
@Test @Test
public void testVideoSizes() { public void testVideoSizes() {
// VideoSize are capped by CamcorderProfile.QUALITY_HIGH max size.
// This can be very small on an emulator, so use very small sizes to not hit that value.
List<Camera.Size> sizes = Arrays.asList( List<Camera.Size> sizes = Arrays.asList(
mockCameraSize(100, 200), mockCameraSize(10, 20),
mockCameraSize(50, 50), mockCameraSize(5, 5),
mockCameraSize(1600, 900), mockCameraSize(16, 9),
mockCameraSize(1000, 2000) mockCameraSize(20, 40)
); );
Camera.Parameters params = mock(Camera.Parameters.class); Camera.Parameters params = mock(Camera.Parameters.class);
when(params.getSupportedVideoSizes()).thenReturn(sizes); when(params.getSupportedVideoSizes()).thenReturn(sizes);
CameraOptions o = new CameraOptions(params, false); CameraOptions o = new Camera1Options(params, 0, false);
Collection<Size> supportedSizes = o.getSupportedVideoSizes(); Collection<Size> supportedSizes = o.getSupportedVideoSizes();
assertEquals(supportedSizes.size(), sizes.size()); assertEquals(supportedSizes.size(), sizes.size());
for (Camera.Size size : sizes) { for (Camera.Size size : sizes) {
@ -148,15 +159,15 @@ public class CameraOptions1Test extends BaseTest {
public void testVideoSizesNull() { public void testVideoSizesNull() {
// When videoSizes is null, we take the preview sizes. // When videoSizes is null, we take the preview sizes.
List<Camera.Size> sizes = Arrays.asList( List<Camera.Size> sizes = Arrays.asList(
mockCameraSize(100, 200), mockCameraSize(10, 20),
mockCameraSize(50, 50), mockCameraSize(5, 5),
mockCameraSize(1600, 900), mockCameraSize(16, 9),
mockCameraSize(1000, 2000) mockCameraSize(20, 40)
); );
Camera.Parameters params = mock(Camera.Parameters.class); Camera.Parameters params = mock(Camera.Parameters.class);
when(params.getSupportedVideoSizes()).thenReturn(null); when(params.getSupportedVideoSizes()).thenReturn(null);
when(params.getSupportedPreviewSizes()).thenReturn(sizes); when(params.getSupportedPreviewSizes()).thenReturn(sizes);
CameraOptions o = new CameraOptions(params, false); CameraOptions o = new Camera1Options(params, 0, false);
Collection<Size> supportedSizes = o.getSupportedVideoSizes(); Collection<Size> supportedSizes = o.getSupportedVideoSizes();
assertEquals(supportedSizes.size(), sizes.size()); assertEquals(supportedSizes.size(), sizes.size());
for (Camera.Size size : sizes) { for (Camera.Size size : sizes) {
@ -168,14 +179,14 @@ public class CameraOptions1Test extends BaseTest {
@Test @Test
public void testVideoSizesFlip() { public void testVideoSizesFlip() {
List<Camera.Size> sizes = Arrays.asList( List<Camera.Size> sizes = Arrays.asList(
mockCameraSize(100, 200), mockCameraSize(10, 20),
mockCameraSize(50, 50), mockCameraSize(5, 5),
mockCameraSize(1600, 900), mockCameraSize(16, 9),
mockCameraSize(1000, 2000) mockCameraSize(20, 40)
); );
Camera.Parameters params = mock(Camera.Parameters.class); Camera.Parameters params = mock(Camera.Parameters.class);
when(params.getSupportedVideoSizes()).thenReturn(sizes); when(params.getSupportedVideoSizes()).thenReturn(sizes);
CameraOptions o = new CameraOptions(params, true); CameraOptions o = new Camera1Options(params, 0, true);
Collection<Size> supportedSizes = o.getSupportedVideoSizes(); Collection<Size> supportedSizes = o.getSupportedVideoSizes();
assertEquals(supportedSizes.size(), sizes.size()); assertEquals(supportedSizes.size(), sizes.size());
for (Camera.Size size : sizes) { for (Camera.Size size : sizes) {
@ -187,10 +198,10 @@ public class CameraOptions1Test extends BaseTest {
@Test @Test
public void testVideoAspectRatio() { public void testVideoAspectRatio() {
List<Camera.Size> sizes = Arrays.asList( List<Camera.Size> sizes = Arrays.asList(
mockCameraSize(100, 200), mockCameraSize(10, 20),
mockCameraSize(50, 50), mockCameraSize(5, 5),
mockCameraSize(1600, 900), mockCameraSize(16, 9),
mockCameraSize(1000, 2000) mockCameraSize(20, 40)
); );
Set<AspectRatio> expected = new HashSet<>(); Set<AspectRatio> expected = new HashSet<>();
@ -200,7 +211,7 @@ public class CameraOptions1Test extends BaseTest {
Camera.Parameters params = mock(Camera.Parameters.class); Camera.Parameters params = mock(Camera.Parameters.class);
when(params.getSupportedVideoSizes()).thenReturn(sizes); when(params.getSupportedVideoSizes()).thenReturn(sizes);
CameraOptions o = new CameraOptions(params, false); CameraOptions o = new Camera1Options(params, 0, false);
Collection<AspectRatio> supportedRatios = o.getSupportedVideoAspectRatios(); Collection<AspectRatio> supportedRatios = o.getSupportedVideoAspectRatios();
assertEquals(supportedRatios.size(), expected.size()); assertEquals(supportedRatios.size(), expected.size());
for (AspectRatio ratio : expected) { for (AspectRatio ratio : expected) {
@ -216,7 +227,7 @@ public class CameraOptions1Test extends BaseTest {
when(params.getMaxExposureCompensation()).thenReturn(0); when(params.getMaxExposureCompensation()).thenReturn(0);
when(params.getMinExposureCompensation()).thenReturn(0); when(params.getMinExposureCompensation()).thenReturn(0);
CameraOptions o = new CameraOptions(params, false); CameraOptions o = new Camera1Options(params, 0, false);
assertFalse(o.supports(GestureAction.AUTO_FOCUS)); assertFalse(o.supports(GestureAction.AUTO_FOCUS));
assertTrue(o.supports(GestureAction.TAKE_PICTURE)); assertTrue(o.supports(GestureAction.TAKE_PICTURE));
assertTrue(o.supports(GestureAction.NONE)); assertTrue(o.supports(GestureAction.NONE));
@ -230,14 +241,16 @@ public class CameraOptions1Test extends BaseTest {
public void testAlwaysSupportedControls() { public void testAlwaysSupportedControls() {
// Grid, VideoQuality, SessionType and Audio are always supported. // Grid, VideoQuality, SessionType and Audio are always supported.
Camera.Parameters params = mock(Camera.Parameters.class); Camera.Parameters params = mock(Camera.Parameters.class);
CameraOptions o = new CameraOptions(params, false); CameraOptions o = new Camera1Options(params, 0, false);
Collection<Grid> grids = o.getSupportedControls(Grid.class); Collection<Grid> grids = o.getSupportedControls(Grid.class);
Collection<VideoCodec> video = o.getSupportedControls(VideoCodec.class); Collection<VideoCodec> video = o.getSupportedControls(VideoCodec.class);
Collection<AudioCodec> audioCodecs = o.getSupportedControls(AudioCodec.class);
Collection<Mode> sessions = o.getSupportedControls(Mode.class); Collection<Mode> sessions = o.getSupportedControls(Mode.class);
Collection<Audio> audio = o.getSupportedControls(Audio.class); Collection<Audio> audio = o.getSupportedControls(Audio.class);
assertEquals(grids.size(), Grid.values().length); assertEquals(grids.size(), Grid.values().length);
assertEquals(video.size(), VideoCodec.values().length); assertEquals(video.size(), VideoCodec.values().length);
assertEquals(audioCodecs.size(), AudioCodec.values().length);
assertEquals(sessions.size(), Mode.values().length); assertEquals(sessions.size(), Mode.values().length);
assertEquals(audio.size(), Audio.values().length); assertEquals(audio.size(), Audio.values().length);
} }
@ -251,12 +264,12 @@ public class CameraOptions1Test extends BaseTest {
supported.add(cameraInfo.facing); supported.add(cameraInfo.facing);
} }
CameraOptions o = new CameraOptions(mock(Camera.Parameters.class), false); CameraOptions o = new Camera1Options(mock(Camera.Parameters.class), 0, false);
Mapper m = Mapper.get(Engine.CAMERA1); Camera1Mapper m = Camera1Mapper.get();
Collection<Facing> s = o.getSupportedControls(Facing.class); Collection<Facing> s = o.getSupportedControls(Facing.class);
assertEquals(s.size(), supported.size()); assertEquals(s.size(), supported.size());
for (Facing facing : s) { for (Facing facing : s) {
assertTrue(supported.contains(m.<Integer>map(facing))); assertTrue(supported.contains(m.mapFacing(facing)));
assertTrue(o.supports(facing)); assertTrue(o.supports(facing));
} }
} }
@ -270,7 +283,7 @@ public class CameraOptions1Test extends BaseTest {
Camera.Parameters.WHITE_BALANCE_SHADE // Not supported Camera.Parameters.WHITE_BALANCE_SHADE // Not supported
)); ));
CameraOptions o = new CameraOptions(params, false); CameraOptions o = new Camera1Options(params, 0, false);
Collection<WhiteBalance> w = o.getSupportedControls(WhiteBalance.class); Collection<WhiteBalance> w = o.getSupportedControls(WhiteBalance.class);
assertEquals(w.size(), 2); assertEquals(w.size(), 2);
assertTrue(w.contains(WhiteBalance.AUTO)); assertTrue(w.contains(WhiteBalance.AUTO));
@ -289,7 +302,7 @@ public class CameraOptions1Test extends BaseTest {
Camera.Parameters.FLASH_MODE_RED_EYE // Not supported Camera.Parameters.FLASH_MODE_RED_EYE // Not supported
)); ));
CameraOptions o = new CameraOptions(params, false); CameraOptions o = new Camera1Options(params, 0, false);
Collection<Flash> f = o.getSupportedControls(Flash.class); Collection<Flash> f = o.getSupportedControls(Flash.class);
assertEquals(f.size(), 3); assertEquals(f.size(), 3);
assertTrue(f.contains(Flash.OFF)); assertTrue(f.contains(Flash.OFF));
@ -310,7 +323,7 @@ public class CameraOptions1Test extends BaseTest {
Camera.Parameters.SCENE_MODE_FIREWORKS // Not supported Camera.Parameters.SCENE_MODE_FIREWORKS // Not supported
)); ));
CameraOptions o = new CameraOptions(params, false); CameraOptions o = new Camera1Options(params, 0, false);
Collection<Hdr> h = o.getSupportedControls(Hdr.class); Collection<Hdr> h = o.getSupportedControls(Hdr.class);
assertEquals(h.size(), 2); assertEquals(h.size(), 2);
assertTrue(h.contains(Hdr.OFF)); assertTrue(h.contains(Hdr.OFF));
@ -327,7 +340,7 @@ public class CameraOptions1Test extends BaseTest {
when(params.isZoomSupported()).thenReturn(true); when(params.isZoomSupported()).thenReturn(true);
//noinspection ArraysAsListWithZeroOrOneArgument //noinspection ArraysAsListWithZeroOrOneArgument
when(params.getSupportedFocusModes()).thenReturn(Arrays.asList(Camera.Parameters.FOCUS_MODE_AUTO)); when(params.getSupportedFocusModes()).thenReturn(Arrays.asList(Camera.Parameters.FOCUS_MODE_AUTO));
CameraOptions o = new CameraOptions(params, false); CameraOptions o = new Camera1Options(params, 0, false);
assertTrue(o.isZoomSupported()); assertTrue(o.isZoomSupported());
assertTrue(o.isAutoFocusSupported()); assertTrue(o.isAutoFocusSupported());
} }
@ -338,10 +351,23 @@ public class CameraOptions1Test extends BaseTest {
when(params.getMaxExposureCompensation()).thenReturn(10); when(params.getMaxExposureCompensation()).thenReturn(10);
when(params.getMinExposureCompensation()).thenReturn(-10); when(params.getMinExposureCompensation()).thenReturn(-10);
when(params.getExposureCompensationStep()).thenReturn(0.5f); when(params.getExposureCompensationStep()).thenReturn(0.5f);
CameraOptions o = new CameraOptions(params, false); CameraOptions o = new Camera1Options(params, 0, false);
assertTrue(o.isExposureCorrectionSupported()); assertTrue(o.isExposureCorrectionSupported());
assertEquals(o.getExposureCorrectionMinValue(), -10f * 0.5f, 0f); assertEquals(o.getExposureCorrectionMinValue(), -10f * 0.5f, 0f);
assertEquals(o.getExposureCorrectionMaxValue(), 10f * 0.5f, 0f); assertEquals(o.getExposureCorrectionMaxValue(), 10f * 0.5f, 0f);
} }
@Test
public void testPreviewFrameRate() {
Camera.Parameters params = mock(Camera.Parameters.class);
List<int[]> result = Arrays.asList(
new int[]{20000, 30000},
new int[]{30000, 60000},
new int[]{60000, 120000}
);
when(params.getSupportedPreviewFpsRange()).thenReturn(result);
CameraOptions o = new Camera1Options(params, 0, false);
assertEquals(20F, o.getPreviewFrameRateMinValue(), 0.001F);
assertEquals(120F, o.getPreviewFrameRateMaxValue(), 0.001F);
}
} }

@ -0,0 +1,172 @@
package com.otaliastudios.cameraview.filter;
import android.opengl.GLES20;
import androidx.annotation.NonNull;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.BaseEglTest;
import com.otaliastudios.cameraview.internal.GlTextureDrawer;
import com.otaliastudios.opengl.program.GlTextureProgram;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class BaseFilterTest extends BaseEglTest {
public static class TestFilter extends BaseFilter implements TwoParameterFilter {
private float param1;
private float param2;
@NonNull
@Override
public String getFragmentShader() {
return createDefaultFragmentShader();
}
@Override
public void setParameter1(float value) {
param1 = value;
}
@Override
public void setParameter2(float value) {
param2 = value;
}
@Override
public float getParameter1() {
return param1;
}
@Override
public float getParameter2() {
return param2;
}
}
private TestFilter filter;
@Test
public void testCreateDefaultFragmentShader() {
filter = new TestFilter();
filter.fragmentTextureCoordinateName = "XXX";
String defaultFragmentShader = filter.createDefaultFragmentShader();
assertNotNull(defaultFragmentShader);
assertTrue(defaultFragmentShader.contains(filter.fragmentTextureCoordinateName));
}
@Test
public void testCreateDefaultVertexShader() {
filter = new TestFilter();
filter.vertexModelViewProjectionMatrixName = "AAA";
filter.vertexPositionName = "BBB";
filter.vertexTextureCoordinateName = "CCC";
filter.vertexTransformMatrixName = "DDD";
filter.fragmentTextureCoordinateName = "EEE";
String defaultVertexShader = filter.createDefaultVertexShader();
assertNotNull(defaultVertexShader);
assertTrue(defaultVertexShader.contains(filter.vertexModelViewProjectionMatrixName));
assertTrue(defaultVertexShader.contains(filter.vertexPositionName));
assertTrue(defaultVertexShader.contains(filter.vertexTextureCoordinateName));
assertTrue(defaultVertexShader.contains(filter.vertexTransformMatrixName));
assertTrue(defaultVertexShader.contains(filter.fragmentTextureCoordinateName));
}
@Test
public void testOnProgramCreate() {
filter = new TestFilter();
int handle = GlTextureProgram.create(filter.getVertexShader(), filter.getFragmentShader());
filter.onCreate(handle);
assertNotNull(filter.program);
filter.onDestroy();
assertNull(filter.program);
GLES20.glDeleteProgram(handle);
}
@Test
public void testDraw_whenInvalid() {
filter = spy(new TestFilter());
float[] matrix = new float[16];
filter.draw(0L, matrix);
verify(filter, never()).onPreDraw(0L, matrix);
verify(filter, never()).onDraw(0L);
verify(filter, never()).onPostDraw(0L);
}
@Test
public void testDraw() {
// Use a drawer which cares about GL setup.
filter = spy(new TestFilter());
GlTextureDrawer drawer = new GlTextureDrawer();
drawer.setFilter(filter);
float[] matrix = drawer.getTextureTransform();
drawer.draw(0L);
verify(filter, times(1)).onPreDraw(0L, matrix);
verify(filter, times(1)).onDraw(0L);
verify(filter, times(1)).onPostDraw(0L);
drawer.release();
}
@Test(expected = RuntimeException.class)
public void testOnCopy_invalid() {
// Anonymous inner classes do not have a public constructor.
Filter filter = new BaseFilter() {
@NonNull
@Override
public String getFragmentShader() {
return "whatever";
}
};
filter.copy();
}
@Test
public void testOnCopy() {
filter = new TestFilter();
BaseFilter other = filter.copy();
assertTrue(other instanceof TestFilter);
}
@Test
public void testCopy_withSize() {
filter = new TestFilter();
filter.setSize(WIDTH, HEIGHT);
BaseFilter other = filter.copy();
assertEquals(WIDTH, other.size.getWidth());
assertEquals(HEIGHT, other.size.getHeight());
}
@Test
public void testCopy_withParameter1() {
filter = new TestFilter();
filter.setParameter1(0.5F);
TestFilter other = (TestFilter) filter.copy();
assertEquals(filter.getParameter1(), other.getParameter1(), 0.001F);
}
@Test
public void testCopy_withParameter2() {
filter = new TestFilter();
filter.setParameter2(0.5F);
TestFilter other = (TestFilter) filter.copy();
assertEquals(filter.getParameter2(), other.getParameter2(), 0.001F);
}
}

@ -0,0 +1,30 @@
package com.otaliastudios.cameraview.filter;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.BaseTest;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class FiltersTest extends BaseTest {
@Test
public void testNewInstance() {
// At least tests that all our default filters have a no-args constructor.
Filters[] filtersArray = Filters.values();
for (Filters filters : filtersArray) {
Filter filter = filters.newInstance();
assertNotNull(filter);
}
}
}

@ -0,0 +1,212 @@
package com.otaliastudios.cameraview.filter;
import android.opengl.GLES20;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.BaseEglTest;
import com.otaliastudios.cameraview.filters.AutoFixFilter;
import com.otaliastudios.cameraview.filters.BrightnessFilter;
import com.otaliastudios.cameraview.filters.DuotoneFilter;
import com.otaliastudios.cameraview.filters.VignetteFilter;
import com.otaliastudios.cameraview.internal.GlTextureDrawer;
import com.otaliastudios.opengl.program.GlProgram;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class MultiFilterTest extends BaseEglTest {
@Test
public void testConstructor1() {
MultiFilter multiFilter = new MultiFilter(
new DuotoneFilter(),
new AutoFixFilter()
);
assertEquals(2, multiFilter.filters.size());
}
@Test
public void testConstructor2() {
List<Filter> filters = new ArrayList<>();
filters.add(new DuotoneFilter());
filters.add(new AutoFixFilter());
MultiFilter multiFilter = new MultiFilter(filters);
assertEquals(2, multiFilter.filters.size());
}
@Test
public void testAddFilter() {
MultiFilter multiFilter = new MultiFilter();
assertEquals(0, multiFilter.filters.size());
multiFilter.addFilter(new DuotoneFilter());
assertEquals(1, multiFilter.filters.size());
multiFilter.addFilter(new AutoFixFilter());
assertEquals(2, multiFilter.filters.size());
}
@Test
public void testAddFilter_multi() {
MultiFilter multiFilter = new MultiFilter(new DuotoneFilter());
assertEquals(1, multiFilter.filters.size());
MultiFilter other = new MultiFilter(
new AutoFixFilter(),
new BrightnessFilter(),
new VignetteFilter());
multiFilter.addFilter(other);
assertEquals(4, multiFilter.filters.size());
}
@Test
public void testSetSize() {
DuotoneFilter filter = new DuotoneFilter();
MultiFilter multiFilter = new MultiFilter(filter);
MultiFilter.State state = multiFilter.states.get(filter);
assertNotNull(state);
assertNull(state.size);
multiFilter.setSize(WIDTH, HEIGHT);
assertNotNull(state.size);
}
@Test
public void testCopy() {
DuotoneFilter filter = spy(new DuotoneFilter());
MultiFilter multiFilter = new MultiFilter(filter);
MultiFilter multiFilterCopy = (MultiFilter) multiFilter.copy();
assertEquals(1, multiFilterCopy.filters.size());
verify(filter, times(1)).onCopy();
}
@Test
public void testParameter1() {
DuotoneFilter filter = new DuotoneFilter();
MultiFilter multiFilter = new MultiFilter(filter);
float desired = 0.21582F; // whatever
multiFilter.setParameter1(desired);
assertEquals(desired, multiFilter.getParameter1(), 0.001F);
assertEquals(desired, filter.getParameter1(), 0.001F);
}
@Test
public void testParameter2() {
DuotoneFilter filter = new DuotoneFilter();
MultiFilter multiFilter = new MultiFilter(filter);
float desired = 0.21582F; // whatever
multiFilter.setParameter2(desired);
assertEquals(desired, multiFilter.getParameter2(), 0.001F);
assertEquals(desired, filter.getParameter2(), 0.001F);
}
@Test
public void testOnCreate_isLazy() {
DuotoneFilter filter = spy(new DuotoneFilter());
MultiFilter multiFilter = new MultiFilter(filter);
int program = GlProgram.create(multiFilter.getVertexShader(),
multiFilter.getFragmentShader());
multiFilter.onCreate(program);
verify(filter, never()).onCreate(anyInt());
multiFilter.onDestroy();
GLES20.glDeleteProgram(program);
verify(filter, never()).onDestroy();
}
@Test
public void testDraw_simple() {
DuotoneFilter filter = spy(new DuotoneFilter());
MultiFilter multiFilter = new MultiFilter(filter);
multiFilter.setSize(WIDTH, HEIGHT);
GlTextureDrawer drawer = new GlTextureDrawer();
drawer.setFilter(multiFilter);
float[] matrix = drawer.getTextureTransform();
drawer.draw(0L);
drawer.release();
// The child should have experienced the whole lifecycle.
verify(filter, atLeastOnce()).getVertexShader();
verify(filter, atLeastOnce()).getFragmentShader();
verify(filter, atLeastOnce()).setSize(anyInt(), anyInt());
verify(filter, times(1)).onCreate(anyInt());
verify(filter, times(1)).draw(0L, matrix);
verify(filter, times(1)).onDestroy();
}
@Test
public void testDraw_multi() {
// Want to test that when filter1 is drawn, the current framebuffer is a
// non-default one. When filter2 is drawn, the current framebuffer is 0.
final DuotoneFilter filter1 = spy(new DuotoneFilter());
final DuotoneFilter filter2 = spy(new DuotoneFilter());
final MultiFilter multiFilter = new MultiFilter(filter1, filter2);
multiFilter.setSize(WIDTH, HEIGHT);
GlTextureDrawer drawer = new GlTextureDrawer();
drawer.setFilter(multiFilter);
float[] matrix = drawer.getTextureTransform();
final int[] result = new int[1];
doAnswer(new Answer() {
@Override
public Object answer(InvocationOnMock invocation) {
MultiFilter.State state = multiFilter.states.get(filter1);
assertNotNull(state);
assertTrue(state.isProgramCreated);
assertTrue(state.isFramebufferCreated);
GLES20.glGetIntegerv(GLES20.GL_FRAMEBUFFER_BINDING, result, 0);
// assertTrue(result[0] != 0);
return null;
}
}).when(filter1).draw(0L, matrix);
// Note: second filter is drawn with the identity matrix!
doAnswer(new Answer() {
@Override
public Object answer(InvocationOnMock invocation) {
// The last filter has no FBO / texture.
MultiFilter.State state = multiFilter.states.get(filter2);
assertNotNull(state);
assertTrue(state.isProgramCreated);
assertFalse(state.isFramebufferCreated);
GLES20.glGetIntegerv(GLES20.GL_FRAMEBUFFER_BINDING, result, 0);
assertEquals(0, result[0]);
return null;
}
}).when(filter2).draw(eq(0L), any(float[].class));
drawer.draw(0L);
drawer.release();
// Verify that both are drawn.
verify(filter1, times(1)).draw(0L, matrix);
verify(filter2, times(1)).draw(eq(0L), any(float[].class));
}
}

@ -0,0 +1,35 @@
package com.otaliastudios.cameraview.filter;
import androidx.annotation.NonNull;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.BaseTest;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class NoFilterTest extends BaseTest {
public static class DummyFilter extends BaseFilter {
@NonNull
@Override
public String getFragmentShader() {
return "whatever";
}
}
@Test
public void testGetFragmentShader() {
NoFilter filter = new NoFilter();
String defaultFragmentShader = new DummyFilter().createDefaultFragmentShader();
assertEquals(defaultFragmentShader, filter.getFragmentShader());
}
}

@ -0,0 +1,43 @@
package com.otaliastudios.cameraview.filter;
import android.content.res.TypedArray;
import androidx.annotation.NonNull;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.R;
import org.junit.Test;
import org.junit.runner.RunWith;
import static junit.framework.TestCase.assertNotNull;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class SimpleFilterTest extends BaseTest {
@Test
public void testGetFragmentShader() {
String shader = "shader";
SimpleFilter filter = new SimpleFilter(shader);
assertEquals(shader, filter.getFragmentShader());
}
@Test
public void testCopy() {
String shader = "shader";
SimpleFilter filter = new SimpleFilter(shader);
BaseFilter copy = filter.copy();
assertTrue(copy instanceof SimpleFilter);
SimpleFilter simpleCopy = (SimpleFilter) copy;
assertEquals(shader, simpleCopy.getFragmentShader());
}
}

@ -0,0 +1,112 @@
package com.otaliastudios.cameraview.frame;
import android.graphics.ImageFormat;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.engine.offset.Angles;
import com.otaliastudios.cameraview.size.Size;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertNotNull;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class ByteBufferFrameManagerTest extends BaseTest {
private final Angles angles = new Angles();
private ByteBufferFrameManager.BufferCallback callback;
@Before
public void setUp() {
callback = mock(ByteBufferFrameManager.BufferCallback.class);
}
@After
public void tearDown() {
callback = null;
}
@Test
public void testAllocate() {
ByteBufferFrameManager manager = new ByteBufferFrameManager(1, callback);
manager.setUp(ImageFormat.NV21, new Size(50, 50), angles);
verify(callback, times(1)).onBufferAvailable(any(byte[].class));
reset(callback);
manager = new ByteBufferFrameManager(5, callback);
manager.setUp(ImageFormat.NV21, new Size(50, 50), angles);
verify(callback, times(5)).onBufferAvailable(any(byte[].class));
}
@Test
public void testOnFrameReleased_alreadyFull() {
ByteBufferFrameManager manager = new ByteBufferFrameManager(1, callback);
manager.setUp(ImageFormat.NV21, new Size(50, 50), angles);
int length = manager.getFrameBytes();
Frame frame1 = manager.getFrame(new byte[length], 0);
assertNotNull(frame1);
// Since frame1 is already taken and poolSize = 1, getFrame() would return null.
// To create a new frame, freeze the first one.
Frame frame2 = frame1.freeze();
// Now release the first frame so it goes back into the pool.
manager.onFrameReleased(frame1, (byte[]) frame1.getData());
reset(callback);
// Release the second. The pool is already full, so onBufferAvailable should not be called
// since this Frame instance will NOT be reused.
manager.onFrameReleased(frame2, (byte[]) frame2.getData());
verify(callback, never()).onBufferAvailable((byte[]) frame2.getData());
}
@Test
public void testOnFrameReleased_sameLength() {
ByteBufferFrameManager manager = new ByteBufferFrameManager(1, callback);
manager.setUp(ImageFormat.NV21, new Size(50, 50), angles);
int length = manager.getFrameBytes();
// A camera preview frame comes. Request a frame.
byte[] picture = new byte[length];
Frame frame = manager.getFrame(picture, 0);
assertNotNull(frame);
// Release the frame and ensure that onBufferAvailable is called.
reset(callback);
manager.onFrameReleased(frame, (byte[]) frame.getData());
verify(callback, times(1)).onBufferAvailable(picture);
}
@Test
public void testOnFrameReleased_differentLength() {
ByteBufferFrameManager manager = new ByteBufferFrameManager(1, callback);
manager.setUp(ImageFormat.NV21, new Size(50, 50), angles);
int length = manager.getFrameBytes();
// A camera preview frame comes. Request a frame.
byte[] picture = new byte[length];
Frame frame = manager.getFrame(picture, 0);
assertNotNull(frame);
// Don't release the frame. Change the allocation size.
manager.setUp(ImageFormat.NV16, new Size(15, 15), angles);
// Now release the old frame and ensure that onBufferAvailable is NOT called,
// because the released data has wrong length.
manager.onFrameReleased(frame, (byte[]) frame.getData());
reset(callback);
verify(callback, never()).onBufferAvailable(picture);
}
}

@ -0,0 +1,73 @@
package com.otaliastudios.cameraview.frame;
import android.graphics.ImageFormat;
import androidx.annotation.NonNull;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.engine.offset.Angles;
import com.otaliastudios.cameraview.size.Size;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class FrameManagerTest extends BaseTest {
private final Angles angles = new Angles();
@Test
public void testFrameRecycling() {
// A 1-pool manager will always recycle the same frame.
FrameManager<String> manager = new FrameManager<String>(1, String.class) {
@Override
protected void onFrameDataReleased(@NonNull String data, boolean recycled) { }
@NonNull
@Override
protected String onCloneFrameData(@NonNull String data) {
return data;
}
};
manager.setUp(ImageFormat.NV21, new Size(50, 50), angles);
Frame first = manager.getFrame("foo", 0);
assertNotNull(first);
first.release();
Frame second = manager.getFrame("bar", 0);
assertNotNull(second);
second.release();
assertEquals(first, second);
}
@Test
public void testGetFrame() {
FrameManager<String> manager = new FrameManager<String>(1, String.class) {
@Override
protected void onFrameDataReleased(@NonNull String data, boolean recycled) { }
@NonNull
@Override
protected String onCloneFrameData(@NonNull String data) {
return data;
}
};
manager.setUp(ImageFormat.NV21, new Size(50, 50), angles);
Frame first = manager.getFrame("foo", 0);
assertNotNull(first);
Frame second = manager.getFrame("bar", 0);
assertNull(second);
}
}

@ -15,7 +15,7 @@ import android.widget.FrameLayout;
import com.otaliastudios.cameraview.BaseTest; import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.TestActivity; import com.otaliastudios.cameraview.TestActivity;
import com.otaliastudios.cameraview.internal.utils.Op; import com.otaliastudios.cameraview.tools.Op;
import org.hamcrest.Matchers; import org.hamcrest.Matchers;
import org.junit.Before; import org.junit.Before;
@ -49,12 +49,12 @@ public abstract class GestureFinderTest<T extends GestureFinder> extends BaseTes
finder.setActive(true); finder.setActive(true);
a.inflate(layout); a.inflate(layout);
touchOp = new Op<>(); touchOp = new Op<>(false);
layout.setOnTouchListener(new View.OnTouchListener() { layout.setOnTouchListener(new View.OnTouchListener() {
@Override @Override
public boolean onTouch(View view, MotionEvent motionEvent) { public boolean onTouch(View view, MotionEvent motionEvent) {
boolean found = finder.onTouchEvent(motionEvent); boolean found = finder.onTouchEvent(motionEvent);
if (found) touchOp.end(finder.getGesture()); if (found) touchOp.controller().end(finder.getGesture());
return true; return true;
} }
}); });

@ -6,6 +6,8 @@ import androidx.test.espresso.ViewAction;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest; import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.tools.SdkExclude;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@ -13,6 +15,11 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
/**
* On API 26 these tests fail during Espresso's inRoot() - the window never gains focus.
* This might be due to a system popup or something similar.
*/
@SdkExclude(minSdkVersion = 26, maxSdkVersion = 26)
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
@SmallTest @SmallTest
public class PinchGestureFinderTest extends GestureFinderTest<PinchGestureFinder> { public class PinchGestureFinderTest extends GestureFinderTest<PinchGestureFinder> {
@ -45,7 +52,7 @@ public class PinchGestureFinderTest extends GestureFinderTest<PinchGestureFinder
private void testPinch(ViewAction action, boolean increasing) { private void testPinch(ViewAction action, boolean increasing) {
touchOp.listen(); touchOp.listen();
touchOp.start(); touchOp.controller().start();
onLayout().perform(action); onLayout().perform(action);
Gesture found = touchOp.await(10000); Gesture found = touchOp.await(10000);
assertNotNull(found); assertNotNull(found);

@ -6,6 +6,8 @@ import androidx.test.espresso.ViewAction;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest; import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.tools.SdkExclude;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@ -17,10 +19,17 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
/**
* On API 26 these tests fail during Espresso's inRoot() - the window never gains focus.
* This might be due to a system popup or something similar.
*/
@SdkExclude(minSdkVersion = 26, maxSdkVersion = 26)
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
@SmallTest @SmallTest
public class ScrollGestureFinderTest extends GestureFinderTest<ScrollGestureFinder> { public class ScrollGestureFinderTest extends GestureFinderTest<ScrollGestureFinder> {
private final static long WAIT = 2000; // 500 was too short
@Override @Override
protected ScrollGestureFinder createFinder(@NonNull GestureFinder.Controller controller) { protected ScrollGestureFinder createFinder(@NonNull GestureFinder.Controller controller) {
return new ScrollGestureFinder(controller); return new ScrollGestureFinder(controller);
@ -40,17 +49,17 @@ public class ScrollGestureFinderTest extends GestureFinderTest<ScrollGestureFind
public void testScrollDisabled() { public void testScrollDisabled() {
finder.setActive(false); finder.setActive(false);
touchOp.listen(); touchOp.listen();
touchOp.start(); touchOp.controller().start();
onLayout().perform(swipeUp()); onLayout().perform(swipeUp());
Gesture found = touchOp.await(500); Gesture found = touchOp.await(WAIT);
assertNull(found); assertNull(found);
} }
private void testScroll(ViewAction scroll, Gesture expected, boolean increasing) { private void testScroll(ViewAction scroll, Gesture expected, boolean increasing) {
touchOp.listen(); touchOp.listen();
touchOp.start(); touchOp.controller().start();
onLayout().perform(scroll); onLayout().perform(scroll);
Gesture found = touchOp.await(500); Gesture found = touchOp.await(WAIT);
assertEquals(found, expected); assertEquals(found, expected);
// How will this move our parameter? // How will this move our parameter?

@ -11,6 +11,7 @@ import androidx.test.filters.SmallTest;
import android.view.InputDevice; import android.view.InputDevice;
import android.view.MotionEvent; import android.view.MotionEvent;
import com.otaliastudios.cameraview.tools.SdkExclude;
import com.otaliastudios.cameraview.size.Size; import com.otaliastudios.cameraview.size.Size;
import org.junit.Test; import org.junit.Test;
@ -19,6 +20,11 @@ import org.junit.runner.RunWith;
import static androidx.test.espresso.action.ViewActions.*; import static androidx.test.espresso.action.ViewActions.*;
import static org.junit.Assert.*; import static org.junit.Assert.*;
/**
* On API 26 these tests fail during Espresso's inRoot() - the window never gains focus.
* This might be due to a system popup or something similar.
*/
@SdkExclude(minSdkVersion = 26, maxSdkVersion = 26)
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
@SmallTest @SmallTest
public class TapGestureFinderTest extends GestureFinderTest<TapGestureFinder> { public class TapGestureFinderTest extends GestureFinderTest<TapGestureFinder> {
@ -39,7 +45,7 @@ public class TapGestureFinderTest extends GestureFinderTest<TapGestureFinder> {
@Test @Test
public void testTap() { public void testTap() {
touchOp.listen(); touchOp.listen();
touchOp.start(); touchOp.controller().start();
GeneralClickAction a = new GeneralClickAction( GeneralClickAction a = new GeneralClickAction(
Tap.SINGLE, GeneralLocation.CENTER, Press.FINGER, Tap.SINGLE, GeneralLocation.CENTER, Press.FINGER,
InputDevice.SOURCE_UNKNOWN, MotionEvent.BUTTON_PRIMARY); InputDevice.SOURCE_UNKNOWN, MotionEvent.BUTTON_PRIMARY);
@ -56,7 +62,7 @@ public class TapGestureFinderTest extends GestureFinderTest<TapGestureFinder> {
public void testTapWhileDisabled() { public void testTapWhileDisabled() {
finder.setActive(false); finder.setActive(false);
touchOp.listen(); touchOp.listen();
touchOp.start(); touchOp.controller().start();
onLayout().perform(click()); onLayout().perform(click());
Gesture found = touchOp.await(500); Gesture found = touchOp.await(500);
assertNull(found); assertNull(found);
@ -65,7 +71,7 @@ public class TapGestureFinderTest extends GestureFinderTest<TapGestureFinder> {
@Test @Test
public void testLongTap() { public void testLongTap() {
touchOp.listen(); touchOp.listen();
touchOp.start(); touchOp.controller().start();
GeneralClickAction a = new GeneralClickAction( GeneralClickAction a = new GeneralClickAction(
Tap.LONG, GeneralLocation.CENTER, Press.FINGER, Tap.LONG, GeneralLocation.CENTER, Press.FINGER,
InputDevice.SOURCE_UNKNOWN, MotionEvent.BUTTON_PRIMARY); InputDevice.SOURCE_UNKNOWN, MotionEvent.BUTTON_PRIMARY);

@ -1,4 +1,4 @@
package com.otaliastudios.cameraview.internal.utils; package com.otaliastudios.cameraview.internal;
import android.media.CamcorderProfile; import android.media.CamcorderProfile;
@ -8,6 +8,8 @@ import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.BaseTest; import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.CameraUtils; import com.otaliastudios.cameraview.CameraUtils;
import com.otaliastudios.cameraview.internal.CamcorderProfiles;
import com.otaliastudios.cameraview.tools.SdkExclude;
import com.otaliastudios.cameraview.size.Size; import com.otaliastudios.cameraview.size.Size;
import org.junit.Test; import org.junit.Test;
@ -34,6 +36,10 @@ public class CamcorderProfilesTest extends BaseTest {
assertEquals(lowest.videoFrameHeight, invalid.videoFrameHeight); assertEquals(lowest.videoFrameHeight, invalid.videoFrameHeight);
} }
/**
* For some reason this fails on emulator 26.
*/
@SdkExclude(minSdkVersion = 26, maxSdkVersion = 26)
@Test @Test
public void testGet() { public void testGet() {
String cameraId = getCameraId(); String cameraId = getCameraId();

@ -1,9 +1,10 @@
package com.otaliastudios.cameraview.internal.utils; package com.otaliastudios.cameraview.internal;
import android.graphics.Rect; import android.graphics.Rect;
import com.otaliastudios.cameraview.BaseTest; import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.internal.CropHelper;
import com.otaliastudios.cameraview.size.AspectRatio; import com.otaliastudios.cameraview.size.AspectRatio;
import com.otaliastudios.cameraview.size.Size; import com.otaliastudios.cameraview.size.Size;

@ -0,0 +1,219 @@
package com.otaliastudios.cameraview.internal;
import android.media.MediaCodecInfo;
import androidx.annotation.NonNull;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.size.AspectRatio;
import com.otaliastudios.cameraview.size.Size;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
@RunWith(AndroidJUnit4.class)
@MediumTest
public class DeviceEncodersTest extends BaseTest {
// This is guaranteed to work, see
// https://developer.android.com/guide/topics/media/media-formats
private final static Size GUARANTEED_SIZE = new Size(176, 144);
private boolean enabled;
@Before
public void setUp() {
enabled = DeviceEncoders.ENABLED;
}
@After
public void tearDown() {
DeviceEncoders.ENABLED = enabled;
}
@NonNull
private DeviceEncoders create() {
return new DeviceEncoders(DeviceEncoders.MODE_RESPECT_ORDER,
"video/avc",
"audio/mp4a-latm",
0,
0);
}
@Test
public void testGetDeviceEncoders() {
DeviceEncoders deviceEncoders = create();
if (DeviceEncoders.ENABLED) {
List<MediaCodecInfo> infos = deviceEncoders.getDeviceEncoders();
for (MediaCodecInfo info : infos) {
assertTrue(info.isEncoder());
}
}
}
@Test
public void testIsHardwareEncoder() {
DeviceEncoders deviceEncoders = create();
if (DeviceEncoders.ENABLED) {
assertFalse(deviceEncoders.isHardwareEncoder("OMX.google.encoder"));
assertTrue(deviceEncoders.isHardwareEncoder("OMX.other.encoder"));
}
}
@Test
public void testFindDeviceEncoder() {
DeviceEncoders deviceEncoders = create();
if (DeviceEncoders.ENABLED) {
List<MediaCodecInfo> allEncoders = deviceEncoders.getDeviceEncoders();
MediaCodecInfo encoder = deviceEncoders.findDeviceEncoder(allEncoders,
"video/avc", DeviceEncoders.MODE_RESPECT_ORDER, 0);
assertNotNull(encoder);
List<String> encoderTypes = Arrays.asList(encoder.getSupportedTypes());
assertTrue(encoderTypes.contains("video/avc"));
}
}
@Test
public void testGetVideoEncoder() {
if (DeviceEncoders.ENABLED) {
DeviceEncoders deviceEncoders = create();
assertNotNull(deviceEncoders.getVideoEncoder());
}
DeviceEncoders.ENABLED = false;
DeviceEncoders deviceEncoders = create();
assertNull(deviceEncoders.getVideoEncoder());
}
@Test
public void testGetAudioEncoder() {
if (DeviceEncoders.ENABLED) {
DeviceEncoders deviceEncoders = create();
assertNotNull(deviceEncoders.getAudioEncoder());
}
DeviceEncoders.ENABLED = false;
DeviceEncoders deviceEncoders = create();
assertNull(deviceEncoders.getAudioEncoder());
}
@Test
public void testGetSupportedVideoSize_disabled() {
DeviceEncoders.ENABLED = false;
DeviceEncoders deviceEncoders = create();
Size input = new Size(GUARANTEED_SIZE.getWidth(), GUARANTEED_SIZE.getHeight());
Size output = deviceEncoders.getSupportedVideoSize(input);
assertSame(input, output);
}
@Test
public void testGetSupportedVideoSize_scalesDown() {
DeviceEncoders deviceEncoders = create();
if (DeviceEncoders.ENABLED) {
Size input = new Size(
GUARANTEED_SIZE.getWidth() * 1000,
GUARANTEED_SIZE.getHeight() * 1000);
try {
Size output = deviceEncoders.getSupportedVideoSize(input);
assertTrue(AspectRatio.of(input).matches(output, 0.01F));
} catch (RuntimeException e) {
// The scaled down size happens to be not supported.
// I see no way of testing this easily if we're not sure of supported ranges.
// This depends highly on the alignment since scaling down, while keeping AR,
// can change the alignment and require width / height changes.
}
}
}
@Test
public void testGetSupportedVideoSize_aligns() {
DeviceEncoders deviceEncoders = create();
if (DeviceEncoders.ENABLED) {
Size input = new Size(GUARANTEED_SIZE.getWidth() + 1,
GUARANTEED_SIZE.getHeight() + 1);
Size output = deviceEncoders.getSupportedVideoSize(input);
assertTrue(output.getWidth() <= input.getWidth());
assertTrue(output.getHeight() <= input.getHeight());
}
}
@Test
public void testGetSupportedVideoBitRate_disabled() {
DeviceEncoders.ENABLED = false;
DeviceEncoders deviceEncoders = create();
int input = 1000;
int output = deviceEncoders.getSupportedVideoBitRate(input);
assertEquals(input, output);
}
@Test
public void testGetSupportedVideoBitRate_enabled() {
DeviceEncoders deviceEncoders = create();
if (DeviceEncoders.ENABLED) {
// Ensure it's clamped: we can pass a negative value and check it's >= 0.
int input = -1000;
int output = deviceEncoders.getSupportedVideoBitRate(input);
assertNotEquals(input, output);
assertTrue(output >= 0);
}
}
@Test
public void testGetSupportedAudioBitRate_disabled() {
DeviceEncoders.ENABLED = false;
DeviceEncoders deviceEncoders = create();
int input = 1000;
int output = deviceEncoders.getSupportedAudioBitRate(input);
assertEquals(input, output);
}
@Test
public void testGetSupportedAudioBitRate_enabled() {
DeviceEncoders deviceEncoders = create();
if (DeviceEncoders.ENABLED) {
// Ensure it's clamped: we can pass a negative value and check it's >= 0.
int input = -1000;
int output = deviceEncoders.getSupportedAudioBitRate(input);
assertNotEquals(input, output);
assertTrue(output >= 0);
}
}
@Test
public void testGetSupportedFrameRate_disabled() {
DeviceEncoders.ENABLED = false;
DeviceEncoders deviceEncoders = create();
int input = 1000;
int output = deviceEncoders.getSupportedVideoFrameRate(GUARANTEED_SIZE, input);
assertEquals(input, output);
}
@Test
public void testGetSupportedFrameRate_enabled() {
DeviceEncoders deviceEncoders = create();
if (DeviceEncoders.ENABLED) {
// Ensure it's clamped: we can pass a negative value and check it's >= 0.
int input = -10;
Size inputSize = deviceEncoders.getSupportedVideoSize(GUARANTEED_SIZE);
int output = deviceEncoders.getSupportedVideoFrameRate(inputSize, input);
assertNotEquals(input, output);
assertTrue(output >= 0);
}
}
}

@ -4,7 +4,11 @@ package com.otaliastudios.cameraview.internal;
import com.otaliastudios.cameraview.BaseTest; import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.TestActivity; import com.otaliastudios.cameraview.TestActivity;
import com.otaliastudios.cameraview.controls.Grid; import com.otaliastudios.cameraview.controls.Grid;
import com.otaliastudios.cameraview.tools.Op;
import com.otaliastudios.cameraview.tools.Retry;
import com.otaliastudios.cameraview.tools.RetryRule;
import androidx.annotation.NonNull;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest; import androidx.test.filters.MediumTest;
import androidx.test.rule.ActivityTestRule; import androidx.test.rule.ActivityTestRule;
@ -15,6 +19,7 @@ import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
@MediumTest @MediumTest
@ -23,8 +28,19 @@ public class GridLinesLayoutTest extends BaseTest {
@Rule @Rule
public ActivityTestRule<TestActivity> rule = new ActivityTestRule<>(TestActivity.class); public ActivityTestRule<TestActivity> rule = new ActivityTestRule<>(TestActivity.class);
@Rule
public RetryRule retryRule = new RetryRule(3);
private GridLinesLayout layout; private GridLinesLayout layout;
@NonNull
private Op<Integer> getDrawOp() {
final Op<Integer> op = new Op<>();
layout.callback = mock(GridLinesLayout.DrawCallback.class);
doEndOp(op, 0).when(layout.callback).onDraw(anyInt());
return op;
}
@Before @Before
public void setUp() { public void setUp() {
uiSync(new Runnable() { uiSync(new Runnable() {
@ -33,44 +49,47 @@ public class GridLinesLayoutTest extends BaseTest {
TestActivity a = rule.getActivity(); TestActivity a = rule.getActivity();
layout = new GridLinesLayout(a); layout = new GridLinesLayout(a);
layout.setGridMode(Grid.OFF); layout.setGridMode(Grid.OFF);
layout.drawOp.listen(); Op<Integer> op = getDrawOp();
a.getContentView().addView(layout); a.getContentView().addView(layout);
op.await(1000);
} }
}); });
// Wait for first draw.
layout.drawOp.await(1000);
} }
private int setGridAndWait(Grid value) { private int setGridAndWait(Grid value) {
layout.drawOp.listen();
layout.setGridMode(value); layout.setGridMode(value);
Integer result = layout.drawOp.await(1000); Op<Integer> op = getDrawOp();
Integer result = op.await(1000);
assertNotNull(result); assertNotNull(result);
return result; return result;
} }
@Retry
@Test @Test
public void testOff() { public void testOff() {
int linesDrawn = setGridAndWait(Grid.OFF); int linesDrawn = setGridAndWait(Grid.OFF);
assertEquals(linesDrawn, 0); assertEquals(0, linesDrawn);
} }
@Retry
@Test @Test
public void test3x3() { public void test3x3() {
int linesDrawn = setGridAndWait(Grid.DRAW_3X3); int linesDrawn = setGridAndWait(Grid.DRAW_3X3);
assertEquals(linesDrawn, 2); assertEquals(2, linesDrawn);
} }
@Retry
@Test @Test
public void testPhi() { public void testPhi() {
int linesDrawn = setGridAndWait(Grid.DRAW_PHI); int linesDrawn = setGridAndWait(Grid.DRAW_PHI);
assertEquals(linesDrawn, 2); assertEquals(2, linesDrawn);
} }
@Retry
@Test @Test
public void test4x4() { public void test4x4() {
int linesDrawn = setGridAndWait(Grid.DRAW_4X4); int linesDrawn = setGridAndWait(Grid.DRAW_4X4);
assertEquals(linesDrawn, 3); assertEquals(3, linesDrawn);
} }
} }

@ -0,0 +1,109 @@
package com.otaliastudios.cameraview.internal;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import android.view.OrientationEventListener;
import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.internal.OrientationHelper;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class OrientationHelperTest extends BaseTest {
private OrientationHelper helper;
private OrientationHelper.Callback callback;
@Before
public void setUp() {
uiSync(new Runnable() {
@Override
public void run() {
callback = mock(OrientationHelper.Callback.class);
helper = new OrientationHelper(getContext(), callback);
}
});
}
@After
public void tearDown() {
callback = null;
helper = null;
}
@Test
public void testEnable() {
// On some API levels, enable() needs to be run on the UI thread.
uiSync(new Runnable() {
@Override
public void run() {
assertEquals(helper.getLastDisplayOffset(), -1);
assertEquals(helper.getLastDeviceOrientation(), -1);
helper.enable();
assertNotEquals(helper.getLastDisplayOffset(), -1); // Don't know about device orientation.
// Ensure nothing bad if called twice.
helper.enable();
assertNotEquals(helper.getLastDisplayOffset(), -1);
helper.disable();
assertEquals(helper.getLastDisplayOffset(), -1);
assertEquals(helper.getLastDeviceOrientation(), -1);
}
});
}
@Test
public void testRotation() {
// On some API levels, enable() needs to be run on the UI thread.
uiSync(new Runnable() {
@Override
public void run() {
// Sometimes (on some APIs) the helper will trigger an update to 0
// right after enabling. But that's fine for us, times(1) will be OK either way.
helper.enable();
helper.mDeviceOrientationListener.onOrientationChanged(OrientationEventListener.ORIENTATION_UNKNOWN);
assertEquals(helper.getLastDeviceOrientation(), 0);
helper.mDeviceOrientationListener.onOrientationChanged(10);
assertEquals(helper.getLastDeviceOrientation(), 0);
helper.mDeviceOrientationListener.onOrientationChanged(-10);
assertEquals(helper.getLastDeviceOrientation(), 0);
helper.mDeviceOrientationListener.onOrientationChanged(44);
assertEquals(helper.getLastDeviceOrientation(), 0);
helper.mDeviceOrientationListener.onOrientationChanged(360);
assertEquals(helper.getLastDeviceOrientation(), 0);
// Callback called just once.
verify(callback, times(1)).onDeviceOrientationChanged(0);
helper.mDeviceOrientationListener.onOrientationChanged(90);
helper.mDeviceOrientationListener.onOrientationChanged(91);
assertEquals(helper.getLastDeviceOrientation(), 90);
verify(callback, times(1)).onDeviceOrientationChanged(90);
helper.mDeviceOrientationListener.onOrientationChanged(180);
assertEquals(helper.getLastDeviceOrientation(), 180);
verify(callback, times(1)).onDeviceOrientationChanged(180);
helper.mDeviceOrientationListener.onOrientationChanged(270);
assertEquals(helper.getLastDeviceOrientation(), 270);
verify(callback, times(1)).onDeviceOrientationChanged(270);
// It is still 270 after ORIENTATION_UNKNOWN
helper.mDeviceOrientationListener.onOrientationChanged(OrientationEventListener.ORIENTATION_UNKNOWN);
assertEquals(helper.getLastDeviceOrientation(), 270);
verify(callback, times(1)).onDeviceOrientationChanged(270);
}
});
}
}

@ -1,4 +1,4 @@
package com.otaliastudios.cameraview.internal.utils; package com.otaliastudios.cameraview.internal;
import android.graphics.ImageFormat; import android.graphics.ImageFormat;
@ -8,6 +8,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest; import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.BaseTest; import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.internal.RotationHelper;
import com.otaliastudios.cameraview.size.Size; import com.otaliastudios.cameraview.size.Size;
import org.junit.Test; import org.junit.Test;

@ -1,9 +1,11 @@
package com.otaliastudios.cameraview.internal.utils; package com.otaliastudios.cameraview.internal;
import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.Tasks; import com.google.android.gms.tasks.Tasks;
import com.otaliastudios.cameraview.BaseTest; import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.internal.WorkerHandler;
import com.otaliastudios.cameraview.tools.Op;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
@ -13,7 +15,6 @@ import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
@ -43,7 +44,7 @@ public class WorkerHandlerTest extends BaseTest {
return new Runnable() { return new Runnable() {
@Override @Override
public void run() { public void run() {
op.end(true); op.controller().end(true);
} }
}; };
} }
@ -53,7 +54,7 @@ public class WorkerHandlerTest extends BaseTest {
return new Callable<Boolean>() { return new Callable<Boolean>() {
@Override @Override
public Boolean call() { public Boolean call() {
op.end(true); op.controller().end(true);
return true; return true;
} }
}; };
@ -77,7 +78,7 @@ public class WorkerHandlerTest extends BaseTest {
@Test @Test
public void testFallbackExecute() { public void testFallbackExecute() {
final Op<Boolean> op = new Op<>(true); final Op<Boolean> op = new Op<>();
WorkerHandler.execute(getRunnableForOp(op)); WorkerHandler.execute(getRunnableForOp(op));
waitOp(op); waitOp(op);
} }
@ -85,7 +86,7 @@ public class WorkerHandlerTest extends BaseTest {
@Test @Test
public void testPostRunnable() { public void testPostRunnable() {
WorkerHandler handler = WorkerHandler.get("handler"); WorkerHandler handler = WorkerHandler.get("handler");
final Op<Boolean> op = new Op<>(true); final Op<Boolean> op = new Op<>();
handler.post(getRunnableForOp(op)); handler.post(getRunnableForOp(op));
waitOp(op); waitOp(op);
} }
@ -93,7 +94,7 @@ public class WorkerHandlerTest extends BaseTest {
@Test @Test
public void testPostCallable() { public void testPostCallable() {
WorkerHandler handler = WorkerHandler.get("handler"); WorkerHandler handler = WorkerHandler.get("handler");
final Op<Boolean> op = new Op<>(true); final Op<Boolean> op = new Op<>();
handler.post(getCallableForOp(op)); handler.post(getCallableForOp(op));
waitOp(op); waitOp(op);
} }
@ -110,7 +111,7 @@ public class WorkerHandlerTest extends BaseTest {
@Test @Test
public void testRunRunnable_background() { public void testRunRunnable_background() {
WorkerHandler handler = WorkerHandler.get("handler"); WorkerHandler handler = WorkerHandler.get("handler");
final Op<Boolean> op = new Op<>(true); final Op<Boolean> op = new Op<>();
handler.run(getRunnableForOp(op)); handler.run(getRunnableForOp(op));
waitOp(op); waitOp(op);
} }
@ -118,14 +119,14 @@ public class WorkerHandlerTest extends BaseTest {
@Test @Test
public void testRunRunnable_sameThread() { public void testRunRunnable_sameThread() {
final WorkerHandler handler = WorkerHandler.get("handler"); final WorkerHandler handler = WorkerHandler.get("handler");
final Op<Boolean> op1 = new Op<>(true); final Op<Boolean> op1 = new Op<>();
final Op<Boolean> op2 = new Op<>(true); final Op<Boolean> op2 = new Op<>();
handler.post(new Runnable() { handler.post(new Runnable() {
@Override @Override
public void run() { public void run() {
handler.run(getRunnableForOp(op2)); handler.run(getRunnableForOp(op2));
assertTrue(op2.await(0)); // Do not wait. assertTrue(op2.await(0)); // Do not wait.
op1.end(true); op1.controller().end(true);
} }
}); });
waitOp(op1); waitOp(op1);
@ -134,7 +135,7 @@ public class WorkerHandlerTest extends BaseTest {
@Test @Test
public void testRunCallable_background() { public void testRunCallable_background() {
WorkerHandler handler = WorkerHandler.get("handler"); WorkerHandler handler = WorkerHandler.get("handler");
final Op<Boolean> op = new Op<>(true); final Op<Boolean> op = new Op<>();
handler.run(getCallableForOp(op)); handler.run(getCallableForOp(op));
waitOp(op); waitOp(op);
} }
@ -142,14 +143,14 @@ public class WorkerHandlerTest extends BaseTest {
@Test @Test
public void testRunCallable_sameThread() { public void testRunCallable_sameThread() {
final WorkerHandler handler = WorkerHandler.get("handler"); final WorkerHandler handler = WorkerHandler.get("handler");
final Op<Boolean> op1 = new Op<>(true); final Op<Boolean> op1 = new Op<>();
final Op<Boolean> op2 = new Op<>(true); final Op<Boolean> op2 = new Op<>();
handler.post(new Runnable() { handler.post(new Runnable() {
@Override @Override
public void run() { public void run() {
handler.run(getCallableForOp(op2)); handler.run(getCallableForOp(op2));
assertTrue(op2.await(0)); // Do not wait. assertTrue(op2.await(0)); // Do not wait.
op1.end(true); op1.controller().end(true);
} }
}); });
waitOp(op1); waitOp(op1);
@ -158,14 +159,14 @@ public class WorkerHandlerTest extends BaseTest {
@Test @Test
public void testRunCallable_sameThread_throws() { public void testRunCallable_sameThread_throws() {
final WorkerHandler handler = WorkerHandler.get("handler"); final WorkerHandler handler = WorkerHandler.get("handler");
final Op<Boolean> op = new Op<>(true); final Op<Boolean> op = new Op<>();
handler.post(new Runnable() { handler.post(new Runnable() {
@Override @Override
public void run() { public void run() {
Task<Void> task = handler.run(getThrowCallable()); Task<Void> task = handler.run(getThrowCallable());
assertTrue(task.isComplete()); // Already complete assertTrue(task.isComplete()); // Already complete
assertFalse(task.isSuccessful()); assertFalse(task.isSuccessful());
op.end(true); op.controller().end(true);
} }
}); });
waitOp(op); waitOp(op);
@ -174,7 +175,7 @@ public class WorkerHandlerTest extends BaseTest {
@Test @Test
public void testPostDelayed_tooEarly() { public void testPostDelayed_tooEarly() {
final WorkerHandler handler = WorkerHandler.get("handler"); final WorkerHandler handler = WorkerHandler.get("handler");
final Op<Boolean> op = new Op<>(true); final Op<Boolean> op = new Op<>();
handler.post(1000, getRunnableForOp(op)); handler.post(1000, getRunnableForOp(op));
assertNull(op.await(500)); assertNull(op.await(500));
} }
@ -182,7 +183,7 @@ public class WorkerHandlerTest extends BaseTest {
@Test @Test
public void testPostDelayed() { public void testPostDelayed() {
final WorkerHandler handler = WorkerHandler.get("handler"); final WorkerHandler handler = WorkerHandler.get("handler");
final Op<Boolean> op = new Op<>(true); final Op<Boolean> op = new Op<>();
handler.post(1000, getRunnableForOp(op)); handler.post(1000, getRunnableForOp(op));
assertNotNull(op.await(2000)); assertNotNull(op.await(2000));
} }
@ -190,7 +191,7 @@ public class WorkerHandlerTest extends BaseTest {
@Test @Test
public void testRemove() { public void testRemove() {
final WorkerHandler handler = WorkerHandler.get("handler"); final WorkerHandler handler = WorkerHandler.get("handler");
final Op<Boolean> op = new Op<>(true); final Op<Boolean> op = new Op<>();
Runnable runnable = getRunnableForOp(op); Runnable runnable = getRunnableForOp(op);
handler.post(1000, runnable); handler.post(1000, runnable);
handler.remove(runnable); handler.remove(runnable);
@ -210,7 +211,7 @@ public class WorkerHandlerTest extends BaseTest {
public void testExecutor() { public void testExecutor() {
final WorkerHandler handler = WorkerHandler.get("handler"); final WorkerHandler handler = WorkerHandler.get("handler");
Executor executor = handler.getExecutor(); Executor executor = handler.getExecutor();
final Op<Boolean> op = new Op<>(true); final Op<Boolean> op = new Op<>();
executor.execute(getRunnableForOp(op)); executor.execute(getRunnableForOp(op));
waitOp(op); waitOp(op);
} }
@ -220,12 +221,12 @@ public class WorkerHandlerTest extends BaseTest {
final WorkerHandler handler = WorkerHandler.get("handler"); final WorkerHandler handler = WorkerHandler.get("handler");
assertTrue(handler.getThread().isAlive()); assertTrue(handler.getThread().isAlive());
handler.destroy(); handler.destroy();
// Wait for the thread to die.
try { handler.getThread().join(500); } catch (InterruptedException ignore) {}
assertFalse(handler.getThread().isAlive());
WorkerHandler newHandler = WorkerHandler.get("handler"); WorkerHandler newHandler = WorkerHandler.get("handler");
assertNotSame(handler, newHandler); assertNotSame(handler, newHandler);
assertTrue(newHandler.getThread().isAlive()); assertTrue(newHandler.getThread().isAlive());
// Ensure old thread dies at some point.
try { handler.getThread().join(500); } catch (InterruptedException ignore) {}
assertFalse(handler.getThread().isAlive());
} }
@Test @Test

@ -1,102 +0,0 @@
package com.otaliastudios.cameraview.internal.utils;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ImageFormat;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.YuvImage;
import android.media.Image;
import android.media.ImageReader;
import android.opengl.GLES20;
import android.os.Handler;
import android.os.Looper;
import android.view.Surface;
import androidx.annotation.NonNull;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.internal.egl.EglCore;
import com.otaliastudios.cameraview.internal.egl.EglViewport;
import com.otaliastudios.cameraview.internal.egl.EglWindowSurface;
import com.otaliastudios.cameraview.size.AspectRatio;
import com.otaliastudios.cameraview.size.Size;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class ImageHelperTest extends BaseTest {
@NonNull
private Image getImage() {
ImageReader reader = ImageReader.newInstance(100, 100, ImageFormat.YUV_420_888, 1);
Surface readerSurface = reader.getSurface();
final Op<Image> imageOp = new Op<>(true);
reader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader reader) {
Image image = reader.acquireLatestImage();
if (image != null) imageOp.end(image);
}
}, new Handler(Looper.getMainLooper()));
// Write on reader surface.
Canvas readerCanvas = readerSurface.lockCanvas(null);
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.RED);
readerCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.MULTIPLY);
readerCanvas.drawCircle(50, 50, 50, paint);
readerSurface.unlockCanvasAndPost(readerCanvas);
// Wait
Image image = imageOp.await(5000);
assertNotNull(image);
return image;
}
@Test
public void testImage() {
Image image = getImage();
int width = image.getWidth();
int height = image.getHeight();
int bitsPerPixel = ImageFormat.getBitsPerPixel(ImageFormat.NV21);
int sizeBits = width * height * bitsPerPixel;
int sizeBytes = (int) Math.ceil(sizeBits / 8.0d);
byte[] bytes = new byte[sizeBytes];
ImageHelper.convertToNV21(image, bytes);
image.close();
// Read the image
YuvImage yuvImage = new YuvImage(bytes, ImageFormat.NV21, width, height, null);
ByteArrayOutputStream jpegStream = new ByteArrayOutputStream();
yuvImage.compressToJpeg(new Rect(0, 0, width, height), 100, jpegStream);
byte[] jpegByteArray = jpegStream.toByteArray();
Bitmap bitmap = BitmapFactory.decodeByteArray(jpegByteArray, 0, jpegByteArray.length);
assertNotNull(bitmap);
// Wanted to do assertions on the color here but it doesn't work. There must be an issue
// with how we are drawing the image in this test, since in real camera, the algorithm works well.
// So for now let's just test that nothing crashes during this process.
// int color = bitmap.getPixel(bitmap.getWidth() - 1, bitmap.getHeight() - 1);
// assertEquals(Color.red(color), 255, 5);
// assertEquals(Color.green(color), 0, 5);
// assertEquals(Color.blue(color), 0, 5);
// assertEquals(Color.alpha(color), 0, 5);
}
}

@ -1,101 +0,0 @@
package com.otaliastudios.cameraview.internal.utils;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import android.view.OrientationEventListener;
import com.otaliastudios.cameraview.BaseTest;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class OrientationHelperTest extends BaseTest {
private OrientationHelper helper;
private OrientationHelper.Callback callback;
@Before
public void setUp() {
uiSync(new Runnable() {
@Override
public void run() {
callback = mock(OrientationHelper.Callback.class);
helper = new OrientationHelper(getContext(), callback);
}
});
}
@After
public void tearDown() {
callback = null;
helper = null;
}
@Test
public void testEnable() {
assertNotNull(helper.mListener);
assertEquals(helper.getDisplayOffset(), -1);
assertEquals(helper.getDeviceOrientation(), -1);
helper.enable(getContext());
assertNotNull(helper.mListener);
assertNotEquals(helper.getDisplayOffset(), -1); // Don't know about device orientation.
// Ensure nothing bad if called twice.
helper.enable(getContext());
assertNotNull(helper.mListener);
assertNotEquals(helper.getDisplayOffset(), -1);
helper.disable();
assertNotNull(helper.mListener);
assertEquals(helper.getDisplayOffset(), -1);
assertEquals(helper.getDeviceOrientation(), -1);
}
@Test
public void testRotation() {
// Sometimes (on some APIs) the helper will trigger an update to 0
// right after enabling. But that's fine for us, times(1) will be OK either way.
helper.enable(getContext());
helper.mListener.onOrientationChanged(OrientationEventListener.ORIENTATION_UNKNOWN);
assertEquals(helper.getDeviceOrientation(), 0);
helper.mListener.onOrientationChanged(10);
assertEquals(helper.getDeviceOrientation(), 0);
helper.mListener.onOrientationChanged(-10);
assertEquals(helper.getDeviceOrientation(), 0);
helper.mListener.onOrientationChanged(44);
assertEquals(helper.getDeviceOrientation(), 0);
helper.mListener.onOrientationChanged(360);
assertEquals(helper.getDeviceOrientation(), 0);
// Callback called just once.
verify(callback, times(1)).onDeviceOrientationChanged(0);
helper.mListener.onOrientationChanged(90);
helper.mListener.onOrientationChanged(91);
assertEquals(helper.getDeviceOrientation(), 90);
verify(callback, times(1)).onDeviceOrientationChanged(90);
helper.mListener.onOrientationChanged(180);
assertEquals(helper.getDeviceOrientation(), 180);
verify(callback, times(1)).onDeviceOrientationChanged(180);
helper.mListener.onOrientationChanged(270);
assertEquals(helper.getDeviceOrientation(), 270);
verify(callback, times(1)).onDeviceOrientationChanged(270);
// It is still 270 after ORIENTATION_UNKNOWN
helper.mListener.onOrientationChanged(OrientationEventListener.ORIENTATION_UNKNOWN);
assertEquals(helper.getDeviceOrientation(), 270);
verify(callback, times(1)).onDeviceOrientationChanged(270);
}
}

@ -9,6 +9,7 @@ import android.view.ViewGroup;
import com.otaliastudios.cameraview.BaseTest; import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.TestActivity; import com.otaliastudios.cameraview.TestActivity;
import com.otaliastudios.cameraview.tools.SdkExclude;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
@ -17,19 +18,19 @@ import org.junit.Test;
import org.mockito.Mockito; import org.mockito.Mockito;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.test.annotation.UiThreadTest;
import androidx.test.rule.ActivityTestRule; import androidx.test.rule.ActivityTestRule;
import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.anyFloat;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset; import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy; import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* Not clear why, but for some reason on API 28+ the UiThreadTests here crash for an internal NPE
* in FrameLayout.onMeasure.
*/
@SdkExclude(minSdkVersion = 28)
@TargetApi(17) @TargetApi(17)
public class MarkerLayoutTest extends BaseTest { public class MarkerLayoutTest extends BaseTest {
@ -54,25 +55,35 @@ public class MarkerLayoutTest extends BaseTest {
} }
@Test @Test
@UiThreadTest
public void testOnMarker_callsOnAttach() { public void testOnMarker_callsOnAttach() {
uiSync(new Runnable() {
@Override
public void run() {
markerLayout.onMarker(MarkerLayout.TYPE_AUTOFOCUS, autoFocusMarker); markerLayout.onMarker(MarkerLayout.TYPE_AUTOFOCUS, autoFocusMarker);
Mockito.verify(autoFocusMarker, times(1)).onAttach( Mockito.verify(autoFocusMarker, times(1)).onAttach(
Mockito.any(Context.class), Mockito.any(Context.class),
Mockito.eq(markerLayout)); Mockito.eq(markerLayout));
} }
});
}
@Test @Test
@UiThreadTest
public void testOnMarker_addsView() { public void testOnMarker_addsView() {
uiSync(new Runnable() {
@Override
public void run() {
Assert.assertEquals(markerLayout.getChildCount(), 0); Assert.assertEquals(markerLayout.getChildCount(), 0);
markerLayout.onMarker(MarkerLayout.TYPE_AUTOFOCUS, autoFocusMarker); markerLayout.onMarker(MarkerLayout.TYPE_AUTOFOCUS, autoFocusMarker);
Assert.assertEquals(markerLayout.getChildCount(), 1); Assert.assertEquals(markerLayout.getChildCount(), 1);
} }
});
}
@Test @Test
@UiThreadTest
public void testOnMarker_removesView() { public void testOnMarker_removesView() {
uiSync(new Runnable() {
@Override
public void run() {
markerLayout.onMarker(MarkerLayout.TYPE_AUTOFOCUS, autoFocusMarker); markerLayout.onMarker(MarkerLayout.TYPE_AUTOFOCUS, autoFocusMarker);
Assert.assertEquals(markerLayout.getChildCount(), 1); Assert.assertEquals(markerLayout.getChildCount(), 1);
markerLayout.onMarker(MarkerLayout.TYPE_AUTOFOCUS, autoFocusMarker); markerLayout.onMarker(MarkerLayout.TYPE_AUTOFOCUS, autoFocusMarker);
@ -84,10 +95,14 @@ public class MarkerLayoutTest extends BaseTest {
Mockito.any(Context.class), Mockito.any(Context.class),
Mockito.eq(markerLayout)); Mockito.eq(markerLayout));
} }
});
}
@Test @Test
@UiThreadTest
public void testOnEvent() { public void testOnEvent() {
uiSync(new Runnable() {
@Override
public void run() {
final View mockView = spy(new View(getContext())); final View mockView = spy(new View(getContext()));
// These fail, however it's not really needed. // These fail, however it's not really needed.
// when(mockView.getWidth()).thenReturn(50); // when(mockView.getWidth()).thenReturn(50);
@ -108,4 +123,6 @@ public class MarkerLayoutTest extends BaseTest {
verify(mockView, times(1)).setTranslationX(anyFloat()); verify(mockView, times(1)).setTranslationX(anyFloat());
verify(mockView, times(1)).setTranslationY(anyFloat()); verify(mockView, times(1)).setTranslationY(anyFloat());
} }
});
}
} }

@ -0,0 +1,137 @@
package com.otaliastudios.cameraview.metering;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.R;
import com.otaliastudios.cameraview.markers.AutoFocusMarker;
import com.otaliastudios.cameraview.markers.AutoFocusTrigger;
import com.otaliastudios.cameraview.markers.MarkerParser;
import com.otaliastudios.cameraview.size.Size;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import java.util.List;
import static junit.framework.TestCase.assertNotNull;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class MeteringRegionsTest extends BaseTest {
private final Size bounds = new Size(1000, 1000);
private void checkRegion(@NonNull MeteringRegion region, @NonNull PointF center, int weight) {
assertEquals(center.x, region.mRegion.centerX(), 0.01F);
assertEquals(center.y, region.mRegion.centerY(), 0.01F);
assertEquals(weight, region.mWeight);
}
@Test
public void testFromPoint() {
PointF center = new PointF(500, 500);
MeteringRegions regions = MeteringRegions.fromPoint(bounds, center);
assertEquals(2, regions.mRegions.size());
MeteringRegion first = regions.mRegions.get(0);
MeteringRegion second = regions.mRegions.get(1);
checkRegion(first, center, MeteringRegion.MAX_WEIGHT);
checkRegion(second, center,
Math.round(MeteringRegions.BLUR_FACTOR_WEIGHT * MeteringRegion.MAX_WEIGHT));
}
@Test
public void testFromArea() {
RectF area = new RectF(400, 400, 600, 600);
MeteringRegions regions = MeteringRegions.fromArea(bounds, area);
assertEquals(1, regions.mRegions.size());
MeteringRegion region = regions.mRegions.get(0);
checkRegion(region, new PointF(area.centerX(), area.centerY()), MeteringRegion.MAX_WEIGHT);
}
@Test
public void testFromArea_withBlur() {
RectF area = new RectF(400, 400, 600, 600);
MeteringRegions regions = MeteringRegions.fromArea(bounds, area,
MeteringRegion.MAX_WEIGHT, true);
assertEquals(2, regions.mRegions.size());
MeteringRegion first = regions.mRegions.get(0);
MeteringRegion second = regions.mRegions.get(1);
PointF center = new PointF(area.centerX(), area.centerY());
checkRegion(first, center, MeteringRegion.MAX_WEIGHT);
checkRegion(second, center,
Math.round(MeteringRegions.BLUR_FACTOR_WEIGHT * MeteringRegion.MAX_WEIGHT));
}
@Test
public void testTransform() {
MeteringTransform transform = mock(MeteringTransform.class);
when(transform.transformMeteringPoint(any(PointF.class))).then(new Answer<PointF>() {
@Override
public PointF answer(InvocationOnMock invocation) {
PointF in = invocation.getArgument(0);
// This will swap x and y coordinates
//noinspection SuspiciousNameCombination
return new PointF(in.y, in.x);
}
});
RectF area = new RectF(0, 0, 100, 500); // tall area
RectF expected = new RectF(0, 0, 500, 100); // wide area
MeteringRegions regions = MeteringRegions.fromArea(bounds, area);
MeteringRegions transformed = regions.transform(transform);
verify(transform, times(4)).transformMeteringPoint(any(PointF.class));
assertEquals(1, transformed.mRegions.size());
assertEquals(expected, transformed.mRegions.get(0).mRegion);
}
@Test
public void testGet() {
MeteringTransform<Integer> transform = new MeteringTransform<Integer>() {
@NonNull
@Override
public PointF transformMeteringPoint(@NonNull PointF point) {
return point;
}
@NonNull
@Override
public Integer transformMeteringRegion(@NonNull RectF region, int weight) {
return weight;
}
};
MeteringRegions regions = MeteringRegions.fromArea(bounds,
new RectF(400, 400, 600, 600),
900,
true);
assertEquals(2, regions.mRegions.size());
List<Integer> result = regions.get(1, transform);
assertEquals(1, result.size());
assertEquals(900, (int) result.get(0));
}
}

@ -1,73 +1,33 @@
package com.otaliastudios.cameraview.overlay; package com.otaliastudios.cameraview.overlay;
import android.content.res.XmlResourceParser;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.util.AttributeSet;
import android.util.Xml;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.test.annotation.UiThreadTest;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest; import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.BaseTest; import com.otaliastudios.cameraview.BaseEglTest;
import com.otaliastudios.cameraview.internal.egl.EglBaseSurface; import com.otaliastudios.cameraview.internal.GlTextureDrawer;
import com.otaliastudios.cameraview.internal.egl.EglCore;
import com.otaliastudios.cameraview.internal.egl.EglViewport;
import com.otaliastudios.cameraview.size.Size; import com.otaliastudios.cameraview.size.Size;
import org.hamcrest.BaseMatcher; import org.hamcrest.BaseMatcher;
import org.hamcrest.Description; import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.junit.After;
import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Mockito;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyFloat;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy; import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
@SmallTest @SmallTest
public class OverlayDrawerTest extends BaseTest { public class OverlayDrawerTest extends BaseEglTest {
private final static int WIDTH = 100;
private final static int HEIGHT = 100;
private EglCore eglCore;
private EglBaseSurface eglSurface;
@Before
public void setUp() {
eglCore = new EglCore(null, EglCore.FLAG_RECORDABLE);
eglSurface = new EglBaseSurface(eglCore);
eglSurface.createOffscreenSurface(WIDTH, HEIGHT);
eglSurface.makeCurrent();
}
@After
public void tearDown() {
eglSurface.releaseEglSurface();
eglSurface = null;
eglCore.release();
eglCore = null;
}
@Test @Test
public void testDraw() { public void testDraw() {
@ -100,21 +60,19 @@ public class OverlayDrawerTest extends BaseTest {
@Test @Test
public void testRender() { public void testRender() {
OverlayDrawer drawer = new OverlayDrawer(mock(Overlay.class), new Size(WIDTH, HEIGHT)); OverlayDrawer drawer = new OverlayDrawer(mock(Overlay.class), new Size(WIDTH, HEIGHT));
drawer.mViewport = spy(drawer.mViewport); drawer.mTextureDrawer = spy(drawer.mTextureDrawer);
drawer.draw(Overlay.Target.PICTURE_SNAPSHOT); drawer.draw(Overlay.Target.PICTURE_SNAPSHOT);
drawer.render(); drawer.render(0L);
verify(drawer.mViewport, times(1)).drawFrame( verify(drawer.mTextureDrawer, times(1)).draw(0L);
drawer.mTextureId,
drawer.getTransform()
);
} }
@Test @Test
public void testRelease() { public void testRelease() {
OverlayDrawer drawer = new OverlayDrawer(mock(Overlay.class), new Size(WIDTH, HEIGHT)); OverlayDrawer drawer = new OverlayDrawer(mock(Overlay.class), new Size(WIDTH, HEIGHT));
EglViewport viewport = spy(drawer.mViewport); GlTextureDrawer textureDrawer = spy(drawer.mTextureDrawer);
drawer.mViewport = viewport; drawer.mTextureDrawer = textureDrawer;
drawer.release(); drawer.release();
verify(viewport, times(1)).release(); verify(textureDrawer, times(1)).release();
} }
} }

@ -14,6 +14,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest; import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.BaseTest; import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.tools.SdkExclude;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
@ -129,6 +130,7 @@ public class OverlayLayoutTest extends BaseTest {
assertTrue(overlayLayout.drawChild(canvas, child, 0)); assertTrue(overlayLayout.drawChild(canvas, child, 0));
} }
@SdkExclude(minSdkVersion = 31) // spying views does not work properly on 31, should investigate
@UiThreadTest @UiThreadTest
@Test @Test
public void testDraw() { public void testDraw() {
@ -142,6 +144,7 @@ public class OverlayLayoutTest extends BaseTest {
verify(overlayLayout, times(1)).drawOn(Overlay.Target.PREVIEW, canvas); verify(overlayLayout, times(1)).drawOn(Overlay.Target.PREVIEW, canvas);
} }
@SdkExclude(minSdkVersion = 31) // spying views does not work properly on 31, should investigate
@UiThreadTest @UiThreadTest
@Test @Test
public void testDrawOn() { public void testDrawOn() {

@ -7,7 +7,7 @@ import android.view.ViewGroup;
import com.otaliastudios.cameraview.BaseTest; import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.TestActivity; import com.otaliastudios.cameraview.TestActivity;
import com.otaliastudios.cameraview.internal.utils.Op; import com.otaliastudios.cameraview.tools.Op;
import com.otaliastudios.cameraview.size.AspectRatio; import com.otaliastudios.cameraview.size.AspectRatio;
import com.otaliastudios.cameraview.size.Size; import com.otaliastudios.cameraview.size.Size;
@ -42,8 +42,8 @@ public abstract class CameraPreviewTest<T extends CameraPreview> extends BaseTes
@Before @Before
public void setUp() { public void setUp() {
available = new Op<>(true); available = new Op<>();
destroyed = new Op<>(true); destroyed = new Op<>();
uiSync(new Runnable() { uiSync(new Runnable() {
@Override @Override
@ -55,7 +55,7 @@ public abstract class CameraPreviewTest<T extends CameraPreview> extends BaseTes
doAnswer(new Answer() { doAnswer(new Answer() {
@Override @Override
public Object answer(InvocationOnMock invocation) { public Object answer(InvocationOnMock invocation) {
if (available != null) available.end(true); if (available != null) available.controller().end(true);
return null; return null;
} }
}).when(callback).onSurfaceAvailable(); }).when(callback).onSurfaceAvailable();
@ -63,7 +63,7 @@ public abstract class CameraPreviewTest<T extends CameraPreview> extends BaseTes
doAnswer(new Answer() { doAnswer(new Answer() {
@Override @Override
public Object answer(InvocationOnMock invocation) { public Object answer(InvocationOnMock invocation) {
if (destroyed != null) destroyed.end(true); if (destroyed != null) destroyed.controller().end(true);
return null; return null;
} }
}).when(callback).onSurfaceDestroyed(); }).when(callback).onSurfaceDestroyed();
@ -153,17 +153,23 @@ public abstract class CameraPreviewTest<T extends CameraPreview> extends BaseTes
// Since desired is 'desired', let's fake a new view size that is consistent with it. // Since desired is 'desired', let's fake a new view size that is consistent with it.
// Ensure crop is not happening anymore. // Ensure crop is not happening anymore.
preview.mCropOp.listen(); preview.mCropCallback = mock(CameraPreview.CropCallback.class);
preview.dispatchOnSurfaceSizeChanged((int) (50f * desired), 50); // Wait... Op<Void> op = new Op<>();
preview.mCropOp.await(); doEndOp(op, null).when(preview.mCropCallback).onCrop();
preview.dispatchOnSurfaceSizeChanged((int) (50f * desired), 50);
op.await(); // Wait...
assertEquals(desired, getViewAspectRatioWithScale(), 0.01f); assertEquals(desired, getViewAspectRatioWithScale(), 0.01f);
assertFalse(preview.isCropping()); assertFalse(preview.isCropping());
} }
private void setDesiredAspectRatio(float desiredAspectRatio) { private void setDesiredAspectRatio(float desiredAspectRatio) {
preview.mCropOp.listen(); preview.mCropCallback = mock(CameraPreview.CropCallback.class);
preview.setStreamSize((int) (10f * desiredAspectRatio), 10); // Wait... Op<Void> op = new Op<>();
preview.mCropOp.await(); doEndOp(op, null).when(preview.mCropCallback).onCrop();
preview.setStreamSize((int) (10f * desiredAspectRatio), 10);
op.await(); // Wait...
assertEquals(desiredAspectRatio, getViewAspectRatioWithScale(), 0.01f); assertEquals(desiredAspectRatio, getViewAspectRatioWithScale(), 0.01f);
} }

@ -10,7 +10,7 @@ import android.view.ViewGroup;
import com.otaliastudios.cameraview.filter.Filter; import com.otaliastudios.cameraview.filter.Filter;
import com.otaliastudios.cameraview.preview.CameraPreview; import com.otaliastudios.cameraview.preview.CameraPreview;
public class MockCameraPreview extends FilterCameraPreview<View, Void> { public class MockCameraPreview extends CameraPreview<View, Void> implements FilterCameraPreview {
public MockCameraPreview(Context context, ViewGroup parent) { public MockCameraPreview(Context context, ViewGroup parent) {
super(context, parent); super(context, parent);

@ -0,0 +1,12 @@
package com.otaliastudios.cameraview.tools;
import android.os.Build;
public class Emulator {
public static boolean isEmulator() {
// From Android's RequiresDeviceFilter
return Build.HARDWARE.equals("goldfish")
|| Build.HARDWARE.equals("ranchu")
|| Build.HARDWARE.equals("gce_x86");
}
}

@ -0,0 +1,149 @@
package com.otaliastudios.cameraview.tools;
import androidx.annotation.NonNull;
import com.google.android.gms.tasks.OnSuccessListener;
import com.google.android.gms.tasks.Task;
import com.otaliastudios.cameraview.controls.Control;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.mockito.stubbing.Stubber;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* A naive implementation of {@link java.util.concurrent.CountDownLatch}
* to help in testing.
*/
public class Op<T> {
public class Controller {
private int mToBeIgnored;
private Controller() { }
/** Op owner method: notifies the action started. */
public void start() {
if (!isListening()) mToBeIgnored++;
}
/** Op owner method: notifies the action ended. */
public void end(T result) {
if (mToBeIgnored > 0) {
mToBeIgnored--;
return;
}
if (isListening()) { // Should be always true.
mResult = result;
mLatch.countDown();
}
}
public void from(@NonNull Task<T> task) {
start();
task.addOnSuccessListener(new OnSuccessListener<T>() {
@Override
public void onSuccess(T result) {
end(result);
}
});
}
@NonNull
public Stubber from(final int invocationArgument) {
return Mockito.doAnswer(new Answer() {
@Override
public Object answer(InvocationOnMock invocation) {
//noinspection unchecked
T o = (T) invocation.getArguments()[invocationArgument];
start();
end(o);
return null;
}
});
}
}
private CountDownLatch mLatch;
private Controller mController = new Controller();
private T mResult;
/**
* Listeners should:
* - call {@link #listen()} to notify they are interested in the next action
* - call {@link #await()} to know when the action is performed.
*
* Op owners should:
* - call {@link Controller#start()} when task started
* - call {@link Controller#end(Object)} when task ends
*/
public Op() {
this(true);
}
public Op(boolean startListening) {
if (startListening) listen();
}
public Op(@NonNull Task<T> task) {
listen();
controller().from(task);
}
private boolean isListening() {
return mLatch != null;
}
/**
* Listener method: notifies we are interested in the next action.
*/
public void listen() {
if (isListening()) throw new RuntimeException("Should not happen.");
mResult = null;
mLatch = new CountDownLatch(1);
}
/**
* Listener method: waits for next task action to end.
* @param millis milliseconds
* @return the action result
*/
public T await(long millis) {
return await(millis, TimeUnit.MILLISECONDS);
}
/**
* Listener method: waits 1 minute for next task action to end.
* @return the action result
*/
public T await() {
return await(1, TimeUnit.MINUTES);
}
/**
* Listener method: waits for next task action to end.
* @param time time
* @param unit the time unit
* @return the action result
*/
private T await(long time, @NonNull TimeUnit unit) {
try {
mLatch.await(time, unit);
} catch (Exception e) {
e.printStackTrace();
}
T result = mResult;
mResult = null;
mLatch = null;
return result;
}
@NonNull
public Controller controller() {
return mController;
}
}

@ -0,0 +1,9 @@
package com.otaliastudios.cameraview.tools;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
public @interface Retry {
boolean emulatorOnly() default false;
}

@ -0,0 +1,52 @@
package com.otaliastudios.cameraview.tools;
import android.os.Build;
import com.otaliastudios.cameraview.CameraLogger;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import java.util.concurrent.atomic.AtomicInteger;
public class RetryRule implements TestRule {
private final static String TAG = RetryRule.class.getSimpleName();
private final static CameraLogger LOG = CameraLogger.create(TAG);
private AtomicInteger retries;
public RetryRule(int retries) {
this.retries = new AtomicInteger(retries);
}
@Override
public Statement apply(final Statement base, final Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
Retry retry = description.getAnnotation(Retry.class);
if (retry == null || retry.emulatorOnly() && !Emulator.isEmulator()) {
base.evaluate();
} else {
Throwable caught = null;
while (retries.getAndDecrement() > 0) {
try {
base.evaluate();
return;
} catch (Throwable throwable) {
LOG.e("[RETRY] Test failed.", retries.get(),
"retries available...");
LOG.e("*******************************************************");
caught = throwable;
}
}
if (caught != null) {
throw caught;
}
}
}
};
}
}

@ -0,0 +1,20 @@
package com.otaliastudios.cameraview.tools;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Like {@link androidx.test.filters.SdkSuppress}, but negative.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface SdkExclude {
/** The minimum API level to drop (inclusive) */
int minSdkVersion() default 1;
/** The maximum API level to drop (inclusive) */
int maxSdkVersion() default Integer.MAX_VALUE;
/** Whether this filter only applies to emulators */
boolean emulatorOnly() default false;
}

@ -0,0 +1,47 @@
package com.otaliastudios.cameraview.tools;
import android.os.Build;
import androidx.annotation.Nullable;
import androidx.test.internal.runner.filters.ParentFilter;
import org.junit.runner.Description;
/**
* Filter for {@link SdkExclude}, based on
* {@link androidx.test.internal.runner.TestRequestBuilder}'s SdkSuppressFilter.
*/
public class SdkExcludeFilter extends ParentFilter {
protected boolean evaluateTest(Description description) {
SdkExclude annotation = getAnnotationForTest(description);
if (annotation != null) {
if ((!annotation.emulatorOnly() || Emulator.isEmulator())
&& Build.VERSION.SDK_INT >= annotation.minSdkVersion()
&& Build.VERSION.SDK_INT <= annotation.maxSdkVersion()) {
return false; // exclude the test
}
return true; // run the test
}
return true; // no annotation, run the test
}
@Nullable
private SdkExclude getAnnotationForTest(Description description) {
final SdkExclude s = description.getAnnotation(SdkExclude.class);
if (s != null) {
return s;
}
final Class<?> testClass = description.getTestClass();
if (testClass != null) {
return testClass.getAnnotation(SdkExclude.class);
}
return null;
}
@Override
public String describe() {
return "Skip tests annotated with SdkExclude";
}
}

@ -0,0 +1,20 @@
package com.otaliastudios.cameraview.tools;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Like {@link androidx.test.filters.SdkSuppress}, but with emulatorOnly().
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface SdkInclude {
/** The minimum API level to run (inclusive) */
int minSdkVersion() default 1;
/** The maximum API level to run (inclusive) */
int maxSdkVersion() default Integer.MAX_VALUE;
/** Whether this filter only applies to emulators */
boolean emulatorOnly() default false;
}

@ -0,0 +1,47 @@
package com.otaliastudios.cameraview.tools;
import android.os.Build;
import androidx.annotation.Nullable;
import androidx.test.internal.runner.filters.ParentFilter;
import org.junit.runner.Description;
/**
* Filter for {@link SdkInclude}, based on
* {@link androidx.test.internal.runner.TestRequestBuilder}'s SdkSuppressFilter.
*/
public class SdkIncludeFilter extends ParentFilter {
protected boolean evaluateTest(Description description) {
SdkInclude annotation = getAnnotationForTest(description);
if (annotation != null) {
if ((!annotation.emulatorOnly() || Emulator.isEmulator())
&& Build.VERSION.SDK_INT >= annotation.minSdkVersion()
&& Build.VERSION.SDK_INT <= annotation.maxSdkVersion()) {
return true; // run the test
}
return false; // drop the test
}
return true; // no annotation, run the test
}
@Nullable
private SdkInclude getAnnotationForTest(Description description) {
final SdkInclude s = description.getAnnotation(SdkInclude.class);
if (s != null) {
return s;
}
final Class<?> testClass = description.getTestClass();
if (testClass != null) {
return testClass.getAnnotation(SdkInclude.class);
}
return null;
}
@Override
public String describe() {
return "Skip tests annotated with SdkInclude";
}
}

@ -1,4 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.otaliastudios.cameraview"> package="com.otaliastudios.cameraview">
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
@ -20,6 +21,8 @@
android:name="android.hardware.microphone" android:name="android.hardware.microphone"
android:required="false"/> android:required="false"/>
<uses-sdk tools:overrideLibrary="com.otaliastudios.opengl" />
<application/> <application/>
</manifest> </manifest>

@ -10,9 +10,9 @@ import androidx.annotation.UiThread;
public interface BitmapCallback { public interface BitmapCallback {
/** /**
* Notifies that the bitmap was succesfully decoded. * Notifies that the bitmap was successfully decoded.
* This is run on the UI thread. * This is run on the UI thread.
* Returns a null object if a {@link OutOfMemoryError} was encountered. * Returns a null object if an {@link OutOfMemoryError} was encountered.
* *
* @param bitmap decoded bitmap, or null * @param bitmap decoded bitmap, or null
*/ */

@ -48,8 +48,9 @@ public abstract class CameraListener {
* Notifies that a picture previously captured with {@link CameraView#takePicture()} * Notifies that a picture previously captured with {@link CameraView#takePicture()}
* or {@link CameraView#takePictureSnapshot()} is ready to be shown or saved to file. * or {@link CameraView#takePictureSnapshot()} is ready to be shown or saved to file.
* *
* If planning to show a bitmap, you can use {@link PictureResult#toBitmap(int, int, BitmapCallback)} * If planning to show a bitmap, you can use
* to decode the byte array taking care about orientation and threading. * {@link PictureResult#toBitmap(int, int, BitmapCallback)} to decode the byte array
* taking care about orientation and threading.
* *
* @param result captured picture * @param result captured picture
*/ */
@ -81,8 +82,9 @@ public abstract class CameraListener {
/** /**
* Notifies that user interacted with the screen and started focus with a gesture, * Notifies that user interacted with the screen and started metering with a gesture,
* and the autofocus is trying to focus around that area. This can be used to draw things on screen. * and touch metering routine is trying to focus around that area.
* This callback can be used to draw things on screen.
* Can also be triggered by {@link CameraView#startAutoFocus(float, float)}. * Can also be triggered by {@link CameraView#startAutoFocus(float, float)}.
* *
* @param point coordinates with respect to CameraView.getWidth() and CameraView.getHeight() * @param point coordinates with respect to CameraView.getWidth() and CameraView.getHeight()
@ -92,12 +94,12 @@ public abstract class CameraListener {
/** /**
* Notifies that a gesture focus event just ended, and the camera converged * Notifies that a touch metering event just ended, and the camera converged
* to a new focus (and possibly exposure and white balance). * to a new focus, exposure and possibly white balance.
* This might succeed or not. * This might succeed or not.
* Can also be triggered by {@link CameraView#startAutoFocus(float, float)}. * Can also be triggered by {@link CameraView#startAutoFocus(float, float)}.
* *
* @param successful whether camera succeeded * @param successful whether metering succeeded
* @param point coordinates with respect to CameraView.getWidth() and CameraView.getHeight() * @param point coordinates with respect to CameraView.getWidth() and CameraView.getHeight()
*/ */
@UiThread @UiThread
@ -105,7 +107,7 @@ public abstract class CameraListener {
/** /**
* Noitifies that a finger gesture just caused the camera zoom * Notifies that a finger gesture just caused the camera zoom
* to be changed. This can be used to draw, for example, a seek bar. * to be changed. This can be used to draw, for example, a seek bar.
* *
* @param newValue the new zoom value * @param newValue the new zoom value
@ -113,7 +115,9 @@ public abstract class CameraListener {
* @param fingers finger positions that caused the event, null if not caused by touch * @param fingers finger positions that caused the event, null if not caused by touch
*/ */
@UiThread @UiThread
public void onZoomChanged(float newValue, @NonNull float[] bounds, @Nullable PointF[] fingers) { } public void onZoomChanged(float newValue,
@NonNull float[] bounds,
@Nullable PointF[] fingers) { }
/** /**
@ -125,7 +129,9 @@ public abstract class CameraListener {
* @param fingers finger positions that caused the event, null if not caused by touch * @param fingers finger positions that caused the event, null if not caused by touch
*/ */
@UiThread @UiThread
public void onExposureCorrectionChanged(float newValue, @NonNull float[] bounds, @Nullable PointF[] fingers) { } public void onExposureCorrectionChanged(float newValue,
@NonNull float[] bounds,
@Nullable PointF[] fingers) { }
/** /**
@ -155,4 +161,13 @@ public abstract class CameraListener {
} }
/**
* Notifies that the picture capture has started. Can be used to update the UI for visual
* confirmation or sound effects.
*/
@UiThread
public void onPictureShutter() {
}
} }

@ -1,16 +1,16 @@
package com.otaliastudios.cameraview; package com.otaliastudios.cameraview;
import android.util.Log;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import android.util.Log;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList; import java.util.Set;
import java.util.List; import java.util.concurrent.CopyOnWriteArraySet;
/** /**
* Utility class that can log traces and info. * Utility class that can log traces and info.
@ -49,18 +49,24 @@ public final class CameraLogger {
* @param message the log message * @param message the log message
* @param throwable an optional throwable * @param throwable an optional throwable
*/ */
void log(@LogLevel int level, @NonNull String tag, @NonNull String message, @Nullable Throwable throwable); void log(@LogLevel int level,
@NonNull String tag,
@NonNull String message,
@Nullable Throwable throwable);
} }
@VisibleForTesting static String lastMessage; @VisibleForTesting static String lastMessage;
@VisibleForTesting static String lastTag; @VisibleForTesting static String lastTag;
private static int sLevel; private static int sLevel;
private static List<Logger> sLoggers = new ArrayList<>(); private static Set<Logger> sLoggers = new CopyOnWriteArraySet<>();
@VisibleForTesting static Logger sAndroidLogger = new Logger() { @VisibleForTesting static Logger sAndroidLogger = new Logger() {
@Override @Override
public void log(int level, @NonNull String tag, @NonNull String message, @Nullable Throwable throwable) { public void log(int level,
@NonNull String tag,
@NonNull String message,
@Nullable Throwable throwable) {
switch (level) { switch (level) {
case LEVEL_VERBOSE: Log.v(tag, message, throwable); break; case LEVEL_VERBOSE: Log.v(tag, message, throwable); break;
case LEVEL_INFO: Log.i(tag, message, throwable); break; case LEVEL_INFO: Log.i(tag, message, throwable); break;

@ -2,253 +2,57 @@ package com.otaliastudios.cameraview;
import android.graphics.ImageFormat; import android.graphics.ImageFormat;
import android.hardware.Camera;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.media.CamcorderProfile;
import android.media.MediaRecorder;
import android.os.Build;
import android.util.Range;
import android.util.Rational;
import com.otaliastudios.cameraview.controls.Audio; import com.otaliastudios.cameraview.controls.Audio;
import com.otaliastudios.cameraview.controls.AudioCodec;
import com.otaliastudios.cameraview.controls.Control; import com.otaliastudios.cameraview.controls.Control;
import com.otaliastudios.cameraview.controls.Engine; import com.otaliastudios.cameraview.controls.Engine;
import com.otaliastudios.cameraview.controls.Facing; import com.otaliastudios.cameraview.controls.Facing;
import com.otaliastudios.cameraview.controls.Flash; import com.otaliastudios.cameraview.controls.Flash;
import com.otaliastudios.cameraview.controls.PictureFormat;
import com.otaliastudios.cameraview.controls.Preview; import com.otaliastudios.cameraview.controls.Preview;
import com.otaliastudios.cameraview.engine.Mapper;
import com.otaliastudios.cameraview.gesture.GestureAction; import com.otaliastudios.cameraview.gesture.GestureAction;
import com.otaliastudios.cameraview.controls.Grid; import com.otaliastudios.cameraview.controls.Grid;
import com.otaliastudios.cameraview.controls.Hdr; import com.otaliastudios.cameraview.controls.Hdr;
import com.otaliastudios.cameraview.controls.Mode; import com.otaliastudios.cameraview.controls.Mode;
import com.otaliastudios.cameraview.controls.VideoCodec; import com.otaliastudios.cameraview.controls.VideoCodec;
import com.otaliastudios.cameraview.controls.WhiteBalance; import com.otaliastudios.cameraview.controls.WhiteBalance;
import com.otaliastudios.cameraview.internal.utils.CamcorderProfiles;
import com.otaliastudios.cameraview.size.AspectRatio; import com.otaliastudios.cameraview.size.AspectRatio;
import com.otaliastudios.cameraview.size.Size; import com.otaliastudios.cameraview.size.Size;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Set; import java.util.Set;
/** /**
* Options telling you what is available and what is not. * Options telling you what is available and what is not.
*/ */
public class CameraOptions { public abstract class CameraOptions {
private Set<WhiteBalance> supportedWhiteBalance = new HashSet<>(5); protected Set<WhiteBalance> supportedWhiteBalance = new HashSet<>(5);
private Set<Facing> supportedFacing = new HashSet<>(2); protected Set<Facing> supportedFacing = new HashSet<>(2);
private Set<Flash> supportedFlash = new HashSet<>(4); protected Set<Flash> supportedFlash = new HashSet<>(4);
private Set<Hdr> supportedHdr = new HashSet<>(2); protected Set<Hdr> supportedHdr = new HashSet<>(2);
private Set<Size> supportedPictureSizes = new HashSet<>(15); protected Set<Size> supportedPictureSizes = new HashSet<>(15);
private Set<Size> supportedVideoSizes = new HashSet<>(5); protected Set<Size> supportedVideoSizes = new HashSet<>(5);
private Set<AspectRatio> supportedPictureAspectRatio = new HashSet<>(4); protected Set<AspectRatio> supportedPictureAspectRatio = new HashSet<>(4);
private Set<AspectRatio> supportedVideoAspectRatio = new HashSet<>(3); protected Set<AspectRatio> supportedVideoAspectRatio = new HashSet<>(3);
protected Set<PictureFormat> supportedPictureFormats = new HashSet<>(2);
private boolean zoomSupported; protected Set<Integer> supportedFrameProcessingFormats = new HashSet<>(2);
private boolean exposureCorrectionSupported;
private float exposureCorrectionMinValue; protected boolean zoomSupported;
private float exposureCorrectionMaxValue; protected boolean exposureCorrectionSupported;
private boolean autoFocusSupported; protected float exposureCorrectionMinValue;
protected float exposureCorrectionMaxValue;
protected boolean autoFocusSupported;
public CameraOptions(@NonNull Camera.Parameters params, boolean flipSizes) { protected float previewFrameRateMinValue;
List<String> strings; protected float previewFrameRateMaxValue;
Mapper mapper = Mapper.get(Engine.CAMERA1);
protected CameraOptions() { }
// Facing
Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
for (int i = 0, count = Camera.getNumberOfCameras(); i < count; i++) {
Camera.getCameraInfo(i, cameraInfo);
Facing value = mapper.unmapFacing(cameraInfo.facing);
if (value != null) supportedFacing.add(value);
}
// WB
strings = params.getSupportedWhiteBalance();
if (strings != null) {
for (String string : strings) {
WhiteBalance value = mapper.unmapWhiteBalance(string);
if (value != null) supportedWhiteBalance.add(value);
}
}
// Flash
supportedFlash.add(Flash.OFF);
strings = params.getSupportedFlashModes();
if (strings != null) {
for (String string : strings) {
Flash value = mapper.unmapFlash(string);
if (value != null) supportedFlash.add(value);
}
}
// Hdr
supportedHdr.add(Hdr.OFF);
strings = params.getSupportedSceneModes();
if (strings != null) {
for (String string : strings) {
Hdr value = mapper.unmapHdr(string);
if (value != null) supportedHdr.add(value);
}
}
// zoom
zoomSupported = params.isZoomSupported();
// autofocus
autoFocusSupported = params.getSupportedFocusModes().contains(Camera.Parameters.FOCUS_MODE_AUTO);
// Exposure correction
float step = params.getExposureCompensationStep();
exposureCorrectionMinValue = (float) params.getMinExposureCompensation() * step;
exposureCorrectionMaxValue = (float) params.getMaxExposureCompensation() * step;
exposureCorrectionSupported = params.getMinExposureCompensation() != 0
|| params.getMaxExposureCompensation() != 0;
// Picture Sizes
List<Camera.Size> sizes = params.getSupportedPictureSizes();
for (Camera.Size size : sizes) {
int width = flipSizes ? size.height : size.width;
int height = flipSizes ? size.width : size.height;
supportedPictureSizes.add(new Size(width, height));
supportedPictureAspectRatio.add(AspectRatio.of(width, height));
}
// Video Sizes
List<Camera.Size> vsizes = params.getSupportedVideoSizes();
if (vsizes != null) {
for (Camera.Size size : vsizes) {
int width = flipSizes ? size.height : size.width;
int height = flipSizes ? size.width : size.height;
supportedVideoSizes.add(new Size(width, height));
supportedVideoAspectRatio.add(AspectRatio.of(width, height));
}
} else {
// StackOverflow threads seems to agree that if getSupportedVideoSizes is null, previews can be used.
List<Camera.Size> fallback = params.getSupportedPreviewSizes();
for (Camera.Size size : fallback) {
int width = flipSizes ? size.height : size.width;
int height = flipSizes ? size.width : size.height;
supportedVideoSizes.add(new Size(width, height));
supportedVideoAspectRatio.add(AspectRatio.of(width, height));
}
}
}
// Camera2Engine constructor.
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
public CameraOptions(@NonNull CameraManager manager, @NonNull String cameraId, boolean flipSizes) throws CameraAccessException {
Mapper mapper = Mapper.get(Engine.CAMERA2);
CameraCharacteristics cameraCharacteristics = manager.getCameraCharacteristics(cameraId);
// Facing
for (String cameraId1 : manager.getCameraIdList()) {
CameraCharacteristics cameraCharacteristics1 = manager.getCameraCharacteristics(cameraId1);
Integer cameraFacing = cameraCharacteristics1.get(CameraCharacteristics.LENS_FACING);
if (cameraFacing != null) {
Facing value = mapper.unmapFacing(cameraFacing);
if (value != null) supportedFacing.add(value);
}
}
// WB
int[] awbModes = cameraCharacteristics.get(CameraCharacteristics.CONTROL_AWB_AVAILABLE_MODES);
//noinspection ConstantConditions
for (int awbMode : awbModes) {
WhiteBalance value = mapper.unmapWhiteBalance(awbMode);
if (value != null) supportedWhiteBalance.add(value);
}
// Flash
supportedFlash.add(Flash.OFF);
Boolean hasFlash = cameraCharacteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE);
if (hasFlash != null && hasFlash) {
int[] aeModes = cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES);
//noinspection ConstantConditions
for (int aeMode : aeModes) {
Flash value = mapper.unmapFlash(aeMode);
if (value != null) supportedFlash.add(value);
}
// Check for torch specifically since the Mapper flash support is not so good.
// If OFF works, it means we have AE_MODE_OFF or AE_MODE_ON. This means we can use
// the torch control.
if (supportedFlash.contains(Flash.OFF)) {
supportedFlash.add(Flash.TORCH);
}
}
// HDR
supportedHdr.add(Hdr.OFF);
int[] sceneModes = cameraCharacteristics.get(CameraCharacteristics.CONTROL_AVAILABLE_SCENE_MODES);
//noinspection ConstantConditions
for (int sceneMode : sceneModes) {
Hdr value = mapper.unmapHdr(sceneMode);
if (value != null) supportedHdr.add(value);
}
//zoom
Float maxZoom = cameraCharacteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM);
if(maxZoom != null) {
zoomSupported = maxZoom > 1;
}
// autofocus
int[] afModes = cameraCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES);
autoFocusSupported = false;
//noinspection ConstantConditions
for (int afMode : afModes) {
if (afMode == CameraCharacteristics.CONTROL_AF_MODE_AUTO) {
autoFocusSupported = true;
}
}
// Exposure correction
Range<Integer> exposureRange = cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE);
Rational exposureStep = cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP);
if (exposureRange != null && exposureStep != null && exposureStep.floatValue() != 0) {
exposureCorrectionMinValue = exposureRange.getLower() / exposureStep.floatValue();
exposureCorrectionMaxValue = exposureRange.getUpper() / exposureStep.floatValue();
}
exposureCorrectionSupported = exposureCorrectionMinValue != 0 && exposureCorrectionMaxValue != 0;
// Picture Sizes
StreamConfigurationMap streamMap = cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
if (streamMap == null) throw new RuntimeException("StreamConfigurationMap is null. Should not happen.");
android.util.Size[] psizes = streamMap.getOutputSizes(ImageFormat.JPEG);
for (android.util.Size size : psizes) {
int width = flipSizes ? size.getHeight() : size.getWidth();
int height = flipSizes ? size.getWidth() : size.getHeight();
supportedPictureSizes.add(new Size(width, height));
supportedPictureAspectRatio.add(AspectRatio.of(width, height));
}
// Video Sizes
// As a safety measure, remove Sizes bigger than CamcorderProfile.highest
CamcorderProfile profile = CamcorderProfiles.get(cameraId,
new Size(Integer.MAX_VALUE, Integer.MAX_VALUE));
Size videoMaxSize = new Size(profile.videoFrameWidth, profile.videoFrameHeight);
android.util.Size[] vsizes = streamMap.getOutputSizes(MediaRecorder.class);
for (android.util.Size size : vsizes) {
if (size.getWidth() <= videoMaxSize.getWidth() && size.getHeight() <= videoMaxSize.getHeight()) {
int width = flipSizes ? size.getHeight() : size.getWidth();
int height = flipSizes ? size.getWidth() : size.getHeight();
supportedVideoSizes.add(new Size(width, height));
supportedVideoAspectRatio.add(AspectRatio.of(width, height));
}
}
}
/** /**
* Shorthand for getSupported*().contains(value). * Shorthand for getSupported*().contains(value).
@ -256,11 +60,10 @@ public class CameraOptions {
* @param control value to check * @param control value to check
* @return whether it's supported * @return whether it's supported
*/ */
public boolean supports(@NonNull Control control) { public final boolean supports(@NonNull Control control) {
return getSupportedControls(control.getClass()).contains(control); return getSupportedControls(control.getClass()).contains(control);
} }
/** /**
* Shorthand for other methods in this class, * Shorthand for other methods in this class,
* e.g. supports(GestureAction.ZOOM) == isZoomSupported(). * e.g. supports(GestureAction.ZOOM) == isZoomSupported().
@ -268,7 +71,7 @@ public class CameraOptions {
* @param action value to be checked * @param action value to be checked
* @return whether it's supported * @return whether it's supported
*/ */
public boolean supports(@NonNull GestureAction action) { public final boolean supports(@NonNull GestureAction action) {
switch (action) { switch (action) {
case AUTO_FOCUS: case AUTO_FOCUS:
return isAutoFocusSupported(); return isAutoFocusSupported();
@ -285,10 +88,10 @@ public class CameraOptions {
return false; return false;
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@NonNull @NonNull
public <T extends Control> Collection<T> getSupportedControls(@NonNull Class<T> controlClass) { public final <T extends Control> Collection<T> getSupportedControls(
@NonNull Class<T> controlClass) {
if (controlClass.equals(Audio.class)) { if (controlClass.equals(Audio.class)) {
return (Collection<T>) Arrays.asList(Audio.values()); return (Collection<T>) Arrays.asList(Audio.values());
} else if (controlClass.equals(Facing.class)) { } else if (controlClass.equals(Facing.class)) {
@ -303,64 +106,61 @@ public class CameraOptions {
return (Collection<T>) Arrays.asList(Mode.values()); return (Collection<T>) Arrays.asList(Mode.values());
} else if (controlClass.equals(VideoCodec.class)) { } else if (controlClass.equals(VideoCodec.class)) {
return (Collection<T>) Arrays.asList(VideoCodec.values()); return (Collection<T>) Arrays.asList(VideoCodec.values());
} else if (controlClass.equals(AudioCodec.class)) {
return (Collection<T>) Arrays.asList(AudioCodec.values());
} else if (controlClass.equals(WhiteBalance.class)) { } else if (controlClass.equals(WhiteBalance.class)) {
return (Collection<T>) getSupportedWhiteBalance(); return (Collection<T>) getSupportedWhiteBalance();
} else if (controlClass.equals(Engine.class)) { } else if (controlClass.equals(Engine.class)) {
return (Collection<T>) Arrays.asList(Engine.values()); return (Collection<T>) Arrays.asList(Engine.values());
} else if (controlClass.equals(Preview.class)) { } else if (controlClass.equals(Preview.class)) {
return (Collection<T>) Arrays.asList(Preview.values()); return (Collection<T>) Arrays.asList(Preview.values());
} else if (controlClass.equals(PictureFormat.class)) {
return (Collection<T>) getSupportedPictureFormats();
} }
// Unrecognized control. // Unrecognized control.
return Collections.emptyList(); return Collections.emptyList();
} }
/** /**
* Set of supported picture sizes for the currently opened camera. * Set of supported picture sizes for the currently opened camera.
* *
* @return a collection of supported values. * @return a collection of supported values.
*/ */
@NonNull @NonNull
public Collection<Size> getSupportedPictureSizes() { public final Collection<Size> getSupportedPictureSizes() {
return Collections.unmodifiableSet(supportedPictureSizes); return Collections.unmodifiableSet(supportedPictureSizes);
} }
/** /**
* Set of supported picture aspect ratios for the currently opened camera. * Set of supported picture aspect ratios for the currently opened camera.
* *
* @return a collection of supported values. * @return a collection of supported values.
*/ */
@SuppressWarnings("WeakerAccess")
@NonNull @NonNull
public Collection<AspectRatio> getSupportedPictureAspectRatios() { public final Collection<AspectRatio> getSupportedPictureAspectRatios() {
return Collections.unmodifiableSet(supportedPictureAspectRatio); return Collections.unmodifiableSet(supportedPictureAspectRatio);
} }
/** /**
* Set of supported video sizes for the currently opened camera. * Set of supported video sizes for the currently opened camera.
* *
* @return a collection of supported values. * @return a collection of supported values.
*/ */
@NonNull @NonNull
public Collection<Size> getSupportedVideoSizes() { public final Collection<Size> getSupportedVideoSizes() {
return Collections.unmodifiableSet(supportedVideoSizes); return Collections.unmodifiableSet(supportedVideoSizes);
} }
/** /**
* Set of supported picture aspect ratios for the currently opened camera. * Set of supported picture aspect ratios for the currently opened camera.
* *
* @return a set of supported values. * @return a set of supported values.
*/ */
@SuppressWarnings("WeakerAccess")
@NonNull @NonNull
public Collection<AspectRatio> getSupportedVideoAspectRatios() { public final Collection<AspectRatio> getSupportedVideoAspectRatios() {
return Collections.unmodifiableSet(supportedVideoAspectRatio); return Collections.unmodifiableSet(supportedVideoAspectRatio);
} }
/** /**
* Set of supported facing values. * Set of supported facing values.
* *
@ -369,11 +169,10 @@ public class CameraOptions {
* @return a collection of supported values. * @return a collection of supported values.
*/ */
@NonNull @NonNull
public Collection<Facing> getSupportedFacing() { public final Collection<Facing> getSupportedFacing() {
return Collections.unmodifiableSet(supportedFacing); return Collections.unmodifiableSet(supportedFacing);
} }
/** /**
* Set of supported flash values. * Set of supported flash values.
* *
@ -384,11 +183,10 @@ public class CameraOptions {
* @return a collection of supported values. * @return a collection of supported values.
*/ */
@NonNull @NonNull
public Collection<Flash> getSupportedFlash() { public final Collection<Flash> getSupportedFlash() {
return Collections.unmodifiableSet(supportedFlash); return Collections.unmodifiableSet(supportedFlash);
} }
/** /**
* Set of supported white balance values. * Set of supported white balance values.
* *
@ -400,11 +198,10 @@ public class CameraOptions {
* @return a collection of supported values. * @return a collection of supported values.
*/ */
@NonNull @NonNull
public Collection<WhiteBalance> getSupportedWhiteBalance() { public final Collection<WhiteBalance> getSupportedWhiteBalance() {
return Collections.unmodifiableSet(supportedWhiteBalance); return Collections.unmodifiableSet(supportedWhiteBalance);
} }
/** /**
* Set of supported hdr values. * Set of supported hdr values.
* *
@ -412,34 +209,57 @@ public class CameraOptions {
* @see Hdr#ON * @see Hdr#ON
* @return a collection of supported values. * @return a collection of supported values.
*/ */
@SuppressWarnings("WeakerAccess")
@NonNull @NonNull
public Collection<Hdr> getSupportedHdr() { public final Collection<Hdr> getSupportedHdr() {
return Collections.unmodifiableSet(supportedHdr); return Collections.unmodifiableSet(supportedHdr);
} }
/**
* Set of supported picture formats.
*
* @see PictureFormat#JPEG
* @see PictureFormat#DNG
* @return a collection of supported values.
*/
@NonNull
public final Collection<PictureFormat> getSupportedPictureFormats() {
return Collections.unmodifiableSet(supportedPictureFormats);
}
/**
* Set of supported formats for frame processing,
* as {@link ImageFormat} constants.
*
* @see CameraView#setFrameProcessingFormat(int)
* @return a collection of supported values.
*/
@NonNull
public final Collection<Integer> getSupportedFrameProcessingFormats() {
return Collections.unmodifiableSet(supportedFrameProcessingFormats);
}
/** /**
* Whether zoom is supported. If this is false, pinch-to-zoom * Whether zoom is supported. If this is false, pinch-to-zoom
* will not work and {@link CameraView#setZoom(float)} will have no effect. * will not work and {@link CameraView#setZoom(float)} will have no effect.
* *
* @return whether zoom is supported. * @return whether zoom is supported.
*/ */
public boolean isZoomSupported() { public final boolean isZoomSupported() {
return zoomSupported; return zoomSupported;
} }
/** /**
* Whether auto focus is supported. This means you can map gestures to * Whether touch metering (metering with respect to a specific region of the screen) is
* {@link GestureAction#AUTO_FOCUS} and focus will be changed on tap. * supported. If it is, you can map gestures to {@link GestureAction#AUTO_FOCUS}
* and metering will change on tap.
* *
* @return whether auto focus is supported. * @return whether auto focus is supported.
*/ */
public boolean isAutoFocusSupported() { public final boolean isAutoFocusSupported() {
return autoFocusSupported; return autoFocusSupported;
} }
/** /**
* Whether exposure correction is supported. If this is false, calling * Whether exposure correction is supported. If this is false, calling
* {@link CameraView#setExposureCorrection(float)} has no effect. * {@link CameraView#setExposureCorrection(float)} has no effect.
@ -448,18 +268,17 @@ public class CameraOptions {
* @see #getExposureCorrectionMaxValue() * @see #getExposureCorrectionMaxValue()
* @return whether exposure correction is supported. * @return whether exposure correction is supported.
*/ */
public boolean isExposureCorrectionSupported() { public final boolean isExposureCorrectionSupported() {
return exposureCorrectionSupported; return exposureCorrectionSupported;
} }
/** /**
* The minimum value of negative exposure correction, in EV stops. * The minimum value of negative exposure correction, in EV stops.
* This is presumably negative or 0 if not supported. * This is presumably negative or 0 if not supported.
* *
* @return min EV value * @return min EV value
*/ */
public float getExposureCorrectionMinValue() { public final float getExposureCorrectionMinValue() {
return exposureCorrectionMinValue; return exposureCorrectionMinValue;
} }
@ -470,7 +289,25 @@ public class CameraOptions {
* *
* @return max EV value * @return max EV value
*/ */
public float getExposureCorrectionMaxValue() { public final float getExposureCorrectionMaxValue() {
return exposureCorrectionMaxValue; return exposureCorrectionMaxValue;
} }
/**
* The minimum value for the preview frame rate, in frames per second (FPS).
*
* @return the min value
*/
public final float getPreviewFrameRateMinValue() {
return previewFrameRateMinValue;
}
/**
* The maximum value for the preview frame rate, in frames per second (FPS).
*
* @return the max value
*/
public final float getPreviewFrameRateMaxValue() {
return previewFrameRateMaxValue;
}
} }

@ -9,11 +9,10 @@ import android.graphics.Matrix;
import android.hardware.Camera; import android.hardware.Camera;
import android.os.Handler; import android.os.Handler;
import com.otaliastudios.cameraview.controls.Engine;
import com.otaliastudios.cameraview.controls.Facing; import com.otaliastudios.cameraview.controls.Facing;
import com.otaliastudios.cameraview.engine.Mapper; import com.otaliastudios.cameraview.engine.mappers.Camera1Mapper;
import com.otaliastudios.cameraview.internal.utils.ExifHelper; import com.otaliastudios.cameraview.internal.ExifHelper;
import com.otaliastudios.cameraview.internal.utils.WorkerHandler; import com.otaliastudios.cameraview.internal.WorkerHandler;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@ -34,6 +33,8 @@ import java.io.OutputStream;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class CameraUtils { public class CameraUtils {
private final static String TAG = CameraUtils.class.getSimpleName();
private final static CameraLogger LOG = CameraLogger.create(TAG);
/** /**
* Determines whether the device has valid camera sensors, so the library * Determines whether the device has valid camera sensors, so the library
@ -61,7 +62,7 @@ public class CameraUtils {
*/ */
public static boolean hasCameraFacing(@SuppressWarnings("unused") @NonNull Context context, public static boolean hasCameraFacing(@SuppressWarnings("unused") @NonNull Context context,
@NonNull Facing facing) { @NonNull Facing facing) {
int internal = Mapper.get(Engine.CAMERA1).map(facing); int internal = Camera1Mapper.get().mapFacing(facing);
Camera.CameraInfo cameraInfo = new Camera.CameraInfo(); Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
for (int i = 0, count = Camera.getNumberOfCameras(); i < count; i++) { for (int i = 0, count = Camera.getNumberOfCameras(); i < count; i++) {
Camera.getCameraInfo(i, cameraInfo); Camera.getCameraInfo(i, cameraInfo);
@ -93,6 +94,7 @@ public class CameraUtils {
stream.flush(); stream.flush();
return file; return file;
} catch (IOException e) { } catch (IOException e) {
LOG.e("writeToFile:", "could not write file.", e);
return null; return null;
} }
} }
@ -109,7 +111,9 @@ public class CameraUtils {
* @param callback a callback * @param callback a callback
*/ */
@SuppressWarnings("WeakerAccess") @SuppressWarnings("WeakerAccess")
public static void writeToFile(@NonNull final byte[] data, @NonNull final File file, @NonNull final FileCallback callback) { public static void writeToFile(@NonNull final byte[] data,
@NonNull final File file,
@NonNull final FileCallback callback) {
final Handler ui = new Handler(); final Handler ui = new Handler();
WorkerHandler.execute(new Runnable() { WorkerHandler.execute(new Runnable() {
@Override @Override
@ -150,7 +154,8 @@ public class CameraUtils {
* @param callback a callback to be notified * @param callback a callback to be notified
*/ */
@SuppressWarnings("WeakerAccess") @SuppressWarnings("WeakerAccess")
public static void decodeBitmap(@NonNull final byte[] source, @NonNull final BitmapCallback callback) { public static void decodeBitmap(@NonNull final byte[] source,
@NonNull final BitmapCallback callback) {
decodeBitmap(source, Integer.MAX_VALUE, Integer.MAX_VALUE, callback); decodeBitmap(source, Integer.MAX_VALUE, Integer.MAX_VALUE, callback);
} }
@ -168,7 +173,10 @@ public class CameraUtils {
* @param callback a callback to be notified * @param callback a callback to be notified
*/ */
@SuppressWarnings("WeakerAccess") @SuppressWarnings("WeakerAccess")
public static void decodeBitmap(@NonNull final byte[] source, final int maxWidth, final int maxHeight, @NonNull final BitmapCallback callback) { public static void decodeBitmap(@NonNull final byte[] source,
final int maxWidth,
final int maxHeight,
@NonNull final BitmapCallback callback) {
decodeBitmap(source, maxWidth, maxHeight, new BitmapFactory.Options(), callback); decodeBitmap(source, maxWidth, maxHeight, new BitmapFactory.Options(), callback);
} }
@ -187,12 +195,20 @@ public class CameraUtils {
* @param callback a callback to be notified * @param callback a callback to be notified
*/ */
@SuppressWarnings("WeakerAccess") @SuppressWarnings("WeakerAccess")
public static void decodeBitmap(@NonNull final byte[] source, final int maxWidth, final int maxHeight, @NonNull final BitmapFactory.Options options, @NonNull final BitmapCallback callback) { public static void decodeBitmap(@NonNull final byte[] source,
final int maxWidth,
final int maxHeight,
@NonNull final BitmapFactory.Options options,
@NonNull final BitmapCallback callback) {
decodeBitmap(source, maxWidth, maxHeight, options, -1, callback); decodeBitmap(source, maxWidth, maxHeight, options, -1, callback);
} }
@SuppressWarnings("WeakerAccess") static void decodeBitmap(@NonNull final byte[] source,
static void decodeBitmap(@NonNull final byte[] source, final int maxWidth, final int maxHeight, @NonNull final BitmapFactory.Options options, final int rotation, @NonNull final BitmapCallback callback) { final int maxWidth,
final int maxHeight,
@NonNull final BitmapFactory.Options options,
final int rotation,
@NonNull final BitmapCallback callback) {
final Handler ui = new Handler(); final Handler ui = new Handler();
WorkerHandler.execute(new Runnable() { WorkerHandler.execute(new Runnable() {
@Override @Override
@ -240,10 +256,13 @@ public class CameraUtils {
* @param options the options to be passed to decodeByteArray * @param options the options to be passed to decodeByteArray
* @return decoded bitmap or null if error is encountered * @return decoded bitmap or null if error is encountered
*/ */
@SuppressWarnings({"SuspiciousNameCombination", "WeakerAccess"}) @SuppressWarnings("WeakerAccess")
@Nullable @Nullable
@WorkerThread @WorkerThread
public static Bitmap decodeBitmap(@NonNull byte[] source, int maxWidth, int maxHeight, @NonNull BitmapFactory.Options options) { public static Bitmap decodeBitmap(@NonNull byte[] source,
int maxWidth,
int maxHeight,
@NonNull BitmapFactory.Options options) {
return decodeBitmap(source, maxWidth, maxHeight, options, -1); return decodeBitmap(source, maxWidth, maxHeight, options, -1);
} }
@ -251,7 +270,11 @@ public class CameraUtils {
// Ignores flipping, but it should be super rare. // Ignores flipping, but it should be super rare.
@SuppressWarnings("TryFinallyCanBeTryWithResources") @SuppressWarnings("TryFinallyCanBeTryWithResources")
@Nullable @Nullable
private static Bitmap decodeBitmap(@NonNull byte[] source, int maxWidth, int maxHeight, @NonNull BitmapFactory.Options options, int rotation) { private static Bitmap decodeBitmap(@NonNull byte[] source,
int maxWidth,
int maxHeight,
@NonNull BitmapFactory.Options options,
int rotation) {
if (maxWidth <= 0) maxWidth = Integer.MAX_VALUE; if (maxWidth <= 0) maxWidth = Integer.MAX_VALUE;
if (maxHeight <= 0) maxHeight = Integer.MAX_VALUE; if (maxHeight <= 0) maxHeight = Integer.MAX_VALUE;
int orientation; int orientation;
@ -262,15 +285,16 @@ public class CameraUtils {
// http://sylvana.net/jpegcrop/exif_orientation.html // http://sylvana.net/jpegcrop/exif_orientation.html
stream = new ByteArrayInputStream(source); stream = new ByteArrayInputStream(source);
ExifInterface exif = new ExifInterface(stream); ExifInterface exif = new ExifInterface(stream);
int exifOrientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); int exifOrientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION,
orientation = ExifHelper.readExifOrientation(exifOrientation); ExifInterface.ORIENTATION_NORMAL);
orientation = ExifHelper.getOrientation(exifOrientation);
flip = exifOrientation == ExifInterface.ORIENTATION_FLIP_HORIZONTAL || flip = exifOrientation == ExifInterface.ORIENTATION_FLIP_HORIZONTAL ||
exifOrientation == ExifInterface.ORIENTATION_FLIP_VERTICAL || exifOrientation == ExifInterface.ORIENTATION_FLIP_VERTICAL ||
exifOrientation == ExifInterface.ORIENTATION_TRANSPOSE || exifOrientation == ExifInterface.ORIENTATION_TRANSPOSE ||
exifOrientation == ExifInterface.ORIENTATION_TRANSVERSE; exifOrientation == ExifInterface.ORIENTATION_TRANSVERSE;
LOG.i("decodeBitmap:", "got orientation from EXIF.", orientation);
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); LOG.e("decodeBitmap:", "could not get orientation from EXIF.", e);
orientation = 0; orientation = 0;
flip = false; flip = false;
} finally { } finally {
@ -283,6 +307,7 @@ public class CameraUtils {
} else { } else {
orientation = rotation; orientation = rotation;
flip = false; flip = false;
LOG.i("decodeBitmap:", "got orientation from constructor.", orientation);
} }
Bitmap bitmap; Bitmap bitmap;
@ -311,7 +336,8 @@ public class CameraUtils {
Matrix matrix = new Matrix(); Matrix matrix = new Matrix();
matrix.setRotate(orientation); matrix.setRotate(orientation);
Bitmap temp = bitmap; Bitmap temp = bitmap;
bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(),
bitmap.getHeight(), matrix, true);
temp.recycle(); temp.recycle();
} }
} catch (OutOfMemoryError e) { } catch (OutOfMemoryError e) {

@ -2,8 +2,10 @@ package com.otaliastudios.cameraview;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.location.Location; import android.location.Location;
import android.os.Build;
import com.otaliastudios.cameraview.controls.Facing; import com.otaliastudios.cameraview.controls.Facing;
import com.otaliastudios.cameraview.controls.PictureFormat;
import com.otaliastudios.cameraview.size.Size; import com.otaliastudios.cameraview.size.Size;
import java.io.File; import java.io.File;
@ -31,19 +33,16 @@ public class PictureResult {
public Size size; public Size size;
public Facing facing; public Facing facing;
public byte[] data; public byte[] data;
public int format; public PictureFormat format;
} }
public final static int FORMAT_JPEG = 0;
// public final static int FORMAT_PNG = 1;
private final boolean isSnapshot; private final boolean isSnapshot;
private final Location location; private final Location location;
private final int rotation; private final int rotation;
private final Size size; private final Size size;
private final Facing facing; private final Facing facing;
private final byte[] data; private final byte[] data;
private final int format; private final PictureFormat format;
PictureResult(@NonNull Stub builder) { PictureResult(@NonNull Stub builder) {
isSnapshot = builder.isSnapshot; isSnapshot = builder.isSnapshot;
@ -118,12 +117,12 @@ public class PictureResult {
} }
/** /**
* Returns the image format. At the moment this will always be * Returns the format for {@link #getData()}.
* {@link #FORMAT_JPEG}.
* *
* @return the current format * @return the format
*/ */
public int getFormat() { @NonNull
public PictureFormat getFormat() {
return format; return format;
} }
@ -137,7 +136,18 @@ public class PictureResult {
* @param callback a callback to be notified of image decoding * @param callback a callback to be notified of image decoding
*/ */
public void toBitmap(int maxWidth, int maxHeight, @NonNull BitmapCallback callback) { public void toBitmap(int maxWidth, int maxHeight, @NonNull BitmapCallback callback) {
CameraUtils.decodeBitmap(getData(), maxWidth, maxHeight, new BitmapFactory.Options(), rotation, callback); if (format == PictureFormat.JPEG) {
CameraUtils.decodeBitmap(getData(), maxWidth, maxHeight, new BitmapFactory.Options(),
rotation, callback);
} else if (format == PictureFormat.DNG && Build.VERSION.SDK_INT >= 24) {
// Apparently: BitmapFactory added DNG support in API 24.
// https://github.com/aosp-mirror/platform_frameworks_base/blob/nougat-mr1-release/core/jni/android/graphics/BitmapFactory.cpp
CameraUtils.decodeBitmap(getData(), maxWidth, maxHeight, new BitmapFactory.Options(),
rotation, callback);
} else {
throw new UnsupportedOperationException("PictureResult.toBitmap() does not support "
+ "this picture format: " + format);
}
} }
/** /**

@ -3,15 +3,17 @@ package com.otaliastudios.cameraview;
import android.location.Location; import android.location.Location;
import com.otaliastudios.cameraview.controls.Audio; import com.otaliastudios.cameraview.controls.Audio;
import com.otaliastudios.cameraview.controls.AudioCodec;
import com.otaliastudios.cameraview.controls.Facing; import com.otaliastudios.cameraview.controls.Facing;
import com.otaliastudios.cameraview.controls.VideoCodec; import com.otaliastudios.cameraview.controls.VideoCodec;
import com.otaliastudios.cameraview.size.Size; import com.otaliastudios.cameraview.size.Size;
import java.io.File;
import java.io.FileDescriptor;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import java.io.File;
/** /**
* Wraps the result of a video recording started by {@link CameraView#takeVideo(File)}. * Wraps the result of a video recording started by {@link CameraView#takeVideo(File)}.
*/ */
@ -30,8 +32,10 @@ public class VideoResult {
public int rotation; public int rotation;
public Size size; public Size size;
public File file; public File file;
public FileDescriptor fileDescriptor;
public Facing facing; public Facing facing;
public VideoCodec videoCodec; public VideoCodec videoCodec;
public AudioCodec audioCodec;
public Audio audio; public Audio audio;
public long maxSize; public long maxSize;
public int maxDuration; public int maxDuration;
@ -55,8 +59,10 @@ public class VideoResult {
private final int rotation; private final int rotation;
private final Size size; private final Size size;
private final File file; private final File file;
private final FileDescriptor fileDescriptor;
private final Facing facing; private final Facing facing;
private final VideoCodec videoCodec; private final VideoCodec videoCodec;
private final AudioCodec audioCodec;
private final Audio audio; private final Audio audio;
private final long maxSize; private final long maxSize;
private final int maxDuration; private final int maxDuration;
@ -71,8 +77,10 @@ public class VideoResult {
rotation = builder.rotation; rotation = builder.rotation;
size = builder.size; size = builder.size;
file = builder.file; file = builder.file;
fileDescriptor = builder.fileDescriptor;
facing = builder.facing; facing = builder.facing;
videoCodec = builder.videoCodec; videoCodec = builder.videoCodec;
audioCodec = builder.audioCodec;
audio = builder.audio; audio = builder.audio;
maxSize = builder.maxSize; maxSize = builder.maxSize;
maxDuration = builder.maxDuration; maxDuration = builder.maxDuration;
@ -130,9 +138,25 @@ public class VideoResult {
*/ */
@NonNull @NonNull
public File getFile() { public File getFile() {
if (file == null) {
throw new RuntimeException("File is only available when takeVideo(File) is used.");
}
return file; return file;
} }
/**
* Returns the file descriptor where the video was saved.
*
* @return the File Descriptor of this video
*/
@NonNull
public FileDescriptor getFileDescriptor() {
if (fileDescriptor == null) {
throw new RuntimeException("FileDescriptor is only available when takeVideo(FileDescriptor) is used.");
}
return fileDescriptor;
}
/** /**
* Returns the facing value with which this video was recorded. * Returns the facing value with which this video was recorded.
* *
@ -153,6 +177,16 @@ public class VideoResult {
return videoCodec; return videoCodec;
} }
/**
* Returns the codec that was used to encode the audio frames.
*
* @return the audio codec
*/
@NonNull
public AudioCodec getAudioCodec() {
return audioCodec;
}
/** /**
* Returns the max file size in bytes that was set before recording, * Returns the max file size in bytes that was set before recording,
* or 0 if no constraint was set. * or 0 if no constraint was set.
@ -185,7 +219,8 @@ public class VideoResult {
/** /**
* Returns the reason why the recording was stopped. * Returns the reason why the recording was stopped.
* @return one of {@link #REASON_USER}, {@link #REASON_MAX_DURATION_REACHED} or {@link #REASON_MAX_SIZE_REACHED}. * @return one of {@link #REASON_USER}, {@link #REASON_MAX_DURATION_REACHED}
* or {@link #REASON_MAX_SIZE_REACHED}.
*/ */
public int getTerminationReason() { public int getTerminationReason() {
return endReason; return endReason;

@ -0,0 +1,63 @@
package com.otaliastudios.cameraview.controls;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import com.otaliastudios.cameraview.CameraView;
/**
* Constants for selecting the encoder of audio recordings.
* https://developer.android.com/guide/topics/media/media-formats.html#audio-formats
*
* @see CameraView#setAudioCodec(AudioCodec)
*/
public enum AudioCodec implements Control {
/**
* Let the device choose its codec.
*/
DEVICE_DEFAULT(0),
/**
* The AAC codec.
*/
AAC(1),
/**
* The HE_AAC codec.
*/
@RequiresApi(Build.VERSION_CODES.JELLY_BEAN)
HE_AAC(2),
/**
* The AAC_ELD codec.
*/
@RequiresApi(Build.VERSION_CODES.JELLY_BEAN)
AAC_ELD(3);
static final AudioCodec DEFAULT = DEVICE_DEFAULT;
private int value;
AudioCodec(int value) {
this.value = value;
}
int value() {
return value;
}
@NonNull
static AudioCodec fromValue(int value) {
AudioCodec[] list = AudioCodec.values();
for (AudioCodec action : list) {
if (action.value() == value) {
return action;
}
}
return DEFAULT;
}
}

@ -22,19 +22,28 @@ public class ControlParser {
private int hdr; private int hdr;
private int audio; private int audio;
private int videoCodec; private int videoCodec;
private int audioCodec;
private int engine; private int engine;
private int pictureFormat;
public ControlParser(@NonNull Context context, @NonNull TypedArray array) { public ControlParser(@NonNull Context context, @NonNull TypedArray array) {
this.preview = array.getInteger(R.styleable.CameraView_cameraPreview, Preview.DEFAULT.value()); preview = array.getInteger(R.styleable.CameraView_cameraPreview, Preview.DEFAULT.value());
this.facing = array.getInteger(R.styleable.CameraView_cameraFacing, Facing.DEFAULT(context).value()); facing = array.getInteger(R.styleable.CameraView_cameraFacing,
this.flash = array.getInteger(R.styleable.CameraView_cameraFlash, Flash.DEFAULT.value()); Facing.DEFAULT(context).value());
this.grid = array.getInteger(R.styleable.CameraView_cameraGrid, Grid.DEFAULT.value()); flash = array.getInteger(R.styleable.CameraView_cameraFlash, Flash.DEFAULT.value());
this.whiteBalance = array.getInteger(R.styleable.CameraView_cameraWhiteBalance, WhiteBalance.DEFAULT.value()); grid = array.getInteger(R.styleable.CameraView_cameraGrid, Grid.DEFAULT.value());
this.mode = array.getInteger(R.styleable.CameraView_cameraMode, Mode.DEFAULT.value()); whiteBalance = array.getInteger(R.styleable.CameraView_cameraWhiteBalance,
this.hdr = array.getInteger(R.styleable.CameraView_cameraHdr, Hdr.DEFAULT.value()); WhiteBalance.DEFAULT.value());
this.audio = array.getInteger(R.styleable.CameraView_cameraAudio, Audio.DEFAULT.value()); mode = array.getInteger(R.styleable.CameraView_cameraMode, Mode.DEFAULT.value());
this.videoCodec = array.getInteger(R.styleable.CameraView_cameraVideoCodec, VideoCodec.DEFAULT.value()); hdr = array.getInteger(R.styleable.CameraView_cameraHdr, Hdr.DEFAULT.value());
this.engine = array.getInteger(R.styleable.CameraView_cameraEngine, Engine.DEFAULT.value()); audio = array.getInteger(R.styleable.CameraView_cameraAudio, Audio.DEFAULT.value());
videoCodec = array.getInteger(R.styleable.CameraView_cameraVideoCodec,
VideoCodec.DEFAULT.value());
audioCodec = array.getInteger(R.styleable.CameraView_cameraAudioCodec,
AudioCodec.DEFAULT.value());
engine = array.getInteger(R.styleable.CameraView_cameraEngine, Engine.DEFAULT.value());
pictureFormat = array.getInteger(R.styleable.CameraView_cameraPictureFormat,
PictureFormat.DEFAULT.value());
} }
@NonNull @NonNull
@ -78,6 +87,11 @@ public class ControlParser {
return Audio.fromValue(audio); return Audio.fromValue(audio);
} }
@NonNull
public AudioCodec getAudioCodec() {
return AudioCodec.fromValue(audioCodec);
}
@NonNull @NonNull
public VideoCodec getVideoCodec() { public VideoCodec getVideoCodec() {
return VideoCodec.fromValue(videoCodec); return VideoCodec.fromValue(videoCodec);
@ -87,4 +101,9 @@ public class ControlParser {
public Engine getEngine() { public Engine getEngine() {
return Engine.fromValue(engine); return Engine.fromValue(engine);
} }
@NonNull
public PictureFormat getPictureFormat() {
return PictureFormat.fromValue(pictureFormat);
}
} }

@ -0,0 +1,52 @@
package com.otaliastudios.cameraview.controls;
import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.CameraOptions;
import com.otaliastudios.cameraview.CameraView;
/**
* Format of the picture results for pictures that are taken with {@link CameraView#takePicture()}.
* This does not apply to picture snapshots.
*
* @see CameraView#setPictureFormat(PictureFormat)
*/
public enum PictureFormat implements Control {
/**
* The picture result data will be a JPEG file.
* This value is always supported.
*/
JPEG(0),
/**
* The picture result data will be a DNG file.
* This is only supported with the {@link Engine#CAMERA2} engine and only on
* specific devices. Please check {@link CameraOptions#getSupportedPictureFormats()}.
*/
DNG(1);
static final PictureFormat DEFAULT = JPEG;
private int value;
PictureFormat(int value) {
this.value = value;
}
int value() {
return value;
}
@NonNull
static PictureFormat fromValue(int value) {
PictureFormat[] list = PictureFormat.values();
for (PictureFormat action : list) {
if (action.value() == value) {
return action;
}
}
return DEFAULT;
}
}

@ -12,18 +12,21 @@ import android.os.Build;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;
import android.view.SurfaceHolder; import android.view.SurfaceHolder;
import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.Tasks; import com.google.android.gms.tasks.Tasks;
import com.otaliastudios.cameraview.CameraException; import com.otaliastudios.cameraview.CameraException;
import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.CameraOptions; import com.otaliastudios.cameraview.CameraOptions;
import com.otaliastudios.cameraview.controls.Engine; import com.otaliastudios.cameraview.controls.PictureFormat;
import com.otaliastudios.cameraview.engine.mappers.Camera1Mapper;
import com.otaliastudios.cameraview.engine.metering.Camera1MeteringTransform;
import com.otaliastudios.cameraview.engine.offset.Axis; import com.otaliastudios.cameraview.engine.offset.Axis;
import com.otaliastudios.cameraview.engine.offset.Reference; import com.otaliastudios.cameraview.engine.offset.Reference;
import com.otaliastudios.cameraview.engine.options.Camera1Options;
import com.otaliastudios.cameraview.engine.orchestrator.CameraState;
import com.otaliastudios.cameraview.frame.ByteBufferFrameManager;
import com.otaliastudios.cameraview.frame.Frame; import com.otaliastudios.cameraview.frame.Frame;
import com.otaliastudios.cameraview.PictureResult; import com.otaliastudios.cameraview.PictureResult;
import com.otaliastudios.cameraview.VideoResult; import com.otaliastudios.cameraview.VideoResult;
@ -34,11 +37,13 @@ import com.otaliastudios.cameraview.gesture.Gesture;
import com.otaliastudios.cameraview.controls.Hdr; import com.otaliastudios.cameraview.controls.Hdr;
import com.otaliastudios.cameraview.controls.Mode; import com.otaliastudios.cameraview.controls.Mode;
import com.otaliastudios.cameraview.controls.WhiteBalance; import com.otaliastudios.cameraview.controls.WhiteBalance;
import com.otaliastudios.cameraview.internal.utils.CropHelper; import com.otaliastudios.cameraview.internal.CropHelper;
import com.otaliastudios.cameraview.metering.MeteringRegions;
import com.otaliastudios.cameraview.metering.MeteringTransform;
import com.otaliastudios.cameraview.picture.Full1PictureRecorder; import com.otaliastudios.cameraview.picture.Full1PictureRecorder;
import com.otaliastudios.cameraview.picture.Snapshot1PictureRecorder; import com.otaliastudios.cameraview.picture.Snapshot1PictureRecorder;
import com.otaliastudios.cameraview.picture.SnapshotGlPictureRecorder; import com.otaliastudios.cameraview.picture.SnapshotGlPictureRecorder;
import com.otaliastudios.cameraview.preview.GlCameraPreview; import com.otaliastudios.cameraview.preview.RendererCameraPreview;
import com.otaliastudios.cameraview.size.AspectRatio; import com.otaliastudios.cameraview.size.AspectRatio;
import com.otaliastudios.cameraview.size.Size; import com.otaliastudios.cameraview.size.Size;
import com.otaliastudios.cameraview.video.Full1VideoRecorder; import com.otaliastudios.cameraview.video.Full1VideoRecorder;
@ -46,47 +51,42 @@ import com.otaliastudios.cameraview.video.SnapshotVideoRecorder;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List; import java.util.List;
@SuppressWarnings("deprecation") public class Camera1Engine extends CameraBaseEngine implements
public class Camera1Engine extends CameraEngine implements
Camera.PreviewCallback, Camera.PreviewCallback,
Camera.ErrorCallback, Camera.ErrorCallback,
FrameManager.BufferCallback { ByteBufferFrameManager.BufferCallback {
private static final String JOB_FOCUS_RESET = "focus reset";
private static final String TAG = Camera1Engine.class.getSimpleName(); private static final String JOB_FOCUS_END = "focus end";
private static final CameraLogger LOG = CameraLogger.create(TAG);
private static final int PREVIEW_FORMAT = ImageFormat.NV21; private static final int PREVIEW_FORMAT = ImageFormat.NV21;
@VisibleForTesting static final int AUTOFOCUS_END_DELAY_MILLIS = 2500; @VisibleForTesting static final int AUTOFOCUS_END_DELAY_MILLIS = 2500;
private final Camera1Mapper mMapper = Camera1Mapper.get();
private Camera mCamera; private Camera mCamera;
@VisibleForTesting int mCameraId; @VisibleForTesting int mCameraId;
private Runnable mFocusEndRunnable;
public Camera1Engine(@NonNull Callback callback) { public Camera1Engine(@NonNull Callback callback) {
super(callback); super(callback);
mMapper = Mapper.get(Engine.CAMERA1);
} }
//region Utilities //region Utilities
@Override @Override
public void onError(int error, Camera camera) { public void onError(int error, Camera camera) {
if (error == Camera.CAMERA_ERROR_SERVER_DIED) {
// Looks like this is recoverable.
LOG.w("Recoverable error inside the onError callback.", "CAMERA_ERROR_SERVER_DIED");
restart();
return;
}
String message = LOG.e("Internal Camera1 error.", error); String message = LOG.e("Internal Camera1 error.", error);
Exception runtime = new RuntimeException(message); Exception runtime = new RuntimeException(message);
int reason; int reason;
switch (error) { switch (error) {
case Camera.CAMERA_ERROR_EVICTED: reason = CameraException.REASON_DISCONNECTED; break; case Camera.CAMERA_ERROR_SERVER_DIED:
case Camera.CAMERA_ERROR_UNKNOWN: reason = CameraException.REASON_UNKNOWN; break; case Camera.CAMERA_ERROR_EVICTED:
reason = CameraException.REASON_DISCONNECTED; break;
case Camera.CAMERA_ERROR_UNKNOWN: // Pass DISCONNECTED which is considered unrecoverable
reason = CameraException.REASON_DISCONNECTED; break;
default: reason = CameraException.REASON_UNKNOWN; default: reason = CameraException.REASON_UNKNOWN;
} }
throw new CameraException(runtime, reason); throw new CameraException(runtime, reason);
@ -96,10 +96,17 @@ public class Camera1Engine extends CameraEngine implements
//region Protected APIs //region Protected APIs
@EngineThread
@NonNull @NonNull
@Override @Override
protected List<Size> getPreviewStreamAvailableSizes() { protected List<Size> getPreviewStreamAvailableSizes() {
List<Camera.Size> sizes = mCamera.getParameters().getSupportedPreviewSizes(); List<Camera.Size> sizes;
try {
sizes = mCamera.getParameters().getSupportedPreviewSizes();
} catch (Exception e) {
LOG.e("getPreviewStreamAvailableSizes:", "Failed to compute preview size. Camera params is empty");
throw new CameraException(e, CameraException.REASON_FAILED_TO_START_PREVIEW);
}
List<Size> result = new ArrayList<>(sizes.size()); List<Size> result = new ArrayList<>(sizes.size());
for (Camera.Size size : sizes) { for (Camera.Size size : sizes) {
Size add = new Size(size.width, size.height); Size add = new Size(size.width, size.height);
@ -109,16 +116,29 @@ public class Camera1Engine extends CameraEngine implements
return result; return result;
} }
@WorkerThread @EngineThread
@NonNull
@Override
protected List<Size> getFrameProcessingAvailableSizes() {
// We don't choose the frame processing size.
// It comes from the preview stream.
return Collections.singletonList(mPreviewStreamSize);
}
@EngineThread
@Override @Override
protected void onPreviewStreamSizeChanged() { protected void onPreviewStreamSizeChanged() {
restartPreview(); restartPreview();
} }
@EngineThread
@Override @Override
protected boolean collectCameraInfo(@NonNull Facing facing) { protected boolean collectCameraInfo(@NonNull Facing facing) {
int internalFacing = mMapper.map(facing); int internalFacing = mMapper.mapFacing(facing);
LOG.i("collectCameraInfo", "Facing:", facing, "Internal:", internalFacing, "Cameras:", Camera.getNumberOfCameras()); LOG.i("collectCameraInfo",
"Facing:", facing,
"Internal:", internalFacing,
"Cameras:", Camera.getNumberOfCameras());
Camera.CameraInfo cameraInfo = new Camera.CameraInfo(); Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
for (int i = 0, count = Camera.getNumberOfCameras(); i < count; i++) { for (int i = 0, count = Camera.getNumberOfCameras(); i < count; i++) {
Camera.getCameraInfo(i, cameraInfo); Camera.getCameraInfo(i, cameraInfo);
@ -136,38 +156,54 @@ public class Camera1Engine extends CameraEngine implements
//region Start //region Start
@NonNull @NonNull
@WorkerThread @EngineThread
@Override @Override
protected Task<Void> onStartEngine() { protected Task<CameraOptions> onStartEngine() {
try { try {
mCamera = Camera.open(mCameraId); mCamera = Camera.open(mCameraId);
} catch (Exception e) { } catch (Exception e) {
LOG.e("onStartEngine:", "Failed to connect. Maybe in use by another app?"); LOG.e("onStartEngine:", "Failed to connect. Maybe in use by another app?");
throw new CameraException(e, CameraException.REASON_FAILED_TO_CONNECT); throw new CameraException(e, CameraException.REASON_FAILED_TO_CONNECT);
} }
if (mCamera == null) {
LOG.e("onStartEngine:", "Failed to connect. Camera is null, maybe in use by another app or already released?");
throw new CameraException(CameraException.REASON_FAILED_TO_CONNECT);
}
mCamera.setErrorCallback(this); mCamera.setErrorCallback(this);
// Set parameters that might have been set before the camera was opened. // Set parameters that might have been set before the camera was opened.
LOG.i("onStartEngine:", "Applying default parameters."); LOG.i("onStartEngine:", "Applying default parameters.");
try {
Camera.Parameters params = mCamera.getParameters(); Camera.Parameters params = mCamera.getParameters();
mCameraOptions = new CameraOptions(params, getAngles().flip(Reference.SENSOR, Reference.VIEW)); mCameraOptions = new Camera1Options(params, mCameraId,
getAngles().flip(Reference.SENSOR, Reference.VIEW));
applyAllParameters(params); applyAllParameters(params);
mCamera.setParameters(params); mCamera.setParameters(params);
mCamera.setDisplayOrientation(getAngles().offset(Reference.SENSOR, Reference.VIEW, Axis.ABSOLUTE)); // <- not allowed during preview } catch (Exception e) {
LOG.e("onStartEngine:", "Failed to connect. Problem with camera params");
throw new CameraException(e, CameraException.REASON_FAILED_TO_CONNECT);
}
try {
mCamera.setDisplayOrientation(getAngles().offset(Reference.SENSOR, Reference.VIEW,
Axis.ABSOLUTE)); // <- not allowed during preview
} catch (Exception e) {
LOG.e("onStartEngine:", "Failed to connect. Can't set display orientation, maybe preview already exists?");
throw new CameraException(CameraException.REASON_FAILED_TO_CONNECT);
}
LOG.i("onStartEngine:", "Ended"); LOG.i("onStartEngine:", "Ended");
return Tasks.forResult(null); return Tasks.forResult(mCameraOptions);
} }
@EngineThread
@NonNull @NonNull
@Override @Override
protected Task<Void> onStartBind() { protected Task<Void> onStartBind() {
LOG.i("onStartBind:", "Started"); LOG.i("onStartBind:", "Started");
Object output = mPreview.getOutput();
try { try {
if (output instanceof SurfaceHolder) { if (mPreview.getOutputClass() == SurfaceHolder.class) {
mCamera.setPreviewDisplay((SurfaceHolder) output); mCamera.setPreviewDisplay((SurfaceHolder) mPreview.getOutput());
} else if (output instanceof SurfaceTexture) { } else if (mPreview.getOutputClass() == SurfaceTexture.class) {
mCamera.setPreviewTexture((SurfaceTexture) output); mCamera.setPreviewTexture((SurfaceTexture) mPreview.getOutput());
} else { } else {
throw new RuntimeException("Unknown CameraPreview output class."); throw new RuntimeException("Unknown CameraPreview output class.");
} }
@ -178,39 +214,58 @@ public class Camera1Engine extends CameraEngine implements
mCaptureSize = computeCaptureSize(); mCaptureSize = computeCaptureSize();
mPreviewStreamSize = computePreviewStreamSize(); mPreviewStreamSize = computePreviewStreamSize();
LOG.i("onStartBind:", "Returning");
return Tasks.forResult(null); return Tasks.forResult(null);
} }
@EngineThread
@NonNull @NonNull
@Override @Override
protected Task<Void> onStartPreview() { protected Task<Void> onStartPreview() {
LOG.i("onStartPreview", "Dispatching onCameraPreviewStreamSizeChanged."); LOG.i("onStartPreview", "Dispatching onCameraPreviewStreamSizeChanged.");
mCallback.onCameraPreviewStreamSizeChanged(); getCallback().onCameraPreviewStreamSizeChanged();
Size previewSize = getPreviewStreamSize(Reference.VIEW); Size previewSize = getPreviewStreamSize(Reference.VIEW);
if (previewSize == null) { if (previewSize == null) {
throw new IllegalStateException("previewStreamSize should not be null at this point."); throw new IllegalStateException("previewStreamSize should not be null at this point.");
} }
mPreview.setStreamSize(previewSize.getWidth(), previewSize.getHeight()); mPreview.setStreamSize(previewSize.getWidth(), previewSize.getHeight());
mPreview.setDrawRotation(0);
Camera.Parameters params = mCamera.getParameters(); Camera.Parameters params;
params.setPreviewFormat(ImageFormat.NV21); // should be the default, but let's make sure, since YuvImage will only support this & a few others try {
params.setPreviewSize(mPreviewStreamSize.getWidth(), mPreviewStreamSize.getHeight()); // not allowed during preview params = mCamera.getParameters();
} catch (Exception e) {
LOG.e("onStartPreview:", "Failed to get params from camera. Maybe low level problem with camera or camera has already released?");
throw new CameraException(e, CameraException.REASON_FAILED_TO_START_PREVIEW);
}
// NV21 should be the default, but let's make sure, since YuvImage will only support this
// and a few others
params.setPreviewFormat(ImageFormat.NV21);
// setPreviewSize is not allowed during preview
params.setPreviewSize(mPreviewStreamSize.getWidth(), mPreviewStreamSize.getHeight());
if (getMode() == Mode.PICTURE) { if (getMode() == Mode.PICTURE) {
params.setPictureSize(mCaptureSize.getWidth(), mCaptureSize.getHeight()); // allowed during preview // setPictureSize is allowed during preview
params.setPictureSize(mCaptureSize.getWidth(), mCaptureSize.getHeight());
} else { } else {
// mCaptureSize in this case is a video size. The available video sizes are not necessarily // mCaptureSize in this case is a video size. The available video sizes are not
// a subset of the picture sizes, so we can't use the mCaptureSize value: it might crash. // necessarily a subset of the picture sizes, so we can't use the mCaptureSize value:
// However, the setPictureSize() passed here is useless : we don't allow HQ pictures in video mode. // it might crash. However, the setPictureSize() passed here is useless : we don't allow
// HQ pictures in video mode.
// While this might be lifted in the future, for now, just use a picture capture size. // While this might be lifted in the future, for now, just use a picture capture size.
Size pictureSize = computeCaptureSize(Mode.PICTURE); Size pictureSize = computeCaptureSize(Mode.PICTURE);
params.setPictureSize(pictureSize.getWidth(), pictureSize.getHeight()); params.setPictureSize(pictureSize.getWidth(), pictureSize.getHeight());
} }
try {
mCamera.setParameters(params); mCamera.setParameters(params);
} catch (Exception e) {
LOG.e("onStartPreview:", "Failed to set params for camera. Maybe incorrect parameter put in params?");
throw new CameraException(e, CameraException.REASON_FAILED_TO_START_PREVIEW);
}
mCamera.setPreviewCallbackWithBuffer(null); // Release anything left mCamera.setPreviewCallbackWithBuffer(null); // Release anything left
mCamera.setPreviewCallbackWithBuffer(this); // Add ourselves mCamera.setPreviewCallbackWithBuffer(this); // Add ourselves
getFrameManager().setUp(ImageFormat.getBitsPerPixel(PREVIEW_FORMAT), mPreviewStreamSize); getFrameManager().setUp(PREVIEW_FORMAT, mPreviewStreamSize, getAngles());
LOG.i("onStartPreview", "Starting preview with startPreview()."); LOG.i("onStartPreview", "Starting preview with startPreview().");
try { try {
@ -227,24 +282,30 @@ public class Camera1Engine extends CameraEngine implements
//region Stop //region Stop
@EngineThread
@NonNull @NonNull
@Override @Override
protected Task<Void> onStopPreview() { protected Task<Void> onStopPreview() {
LOG.i("onStopPreview:", "Started.");
if (mVideoRecorder != null) { if (mVideoRecorder != null) {
mVideoRecorder.stop(true); mVideoRecorder.stop(true);
mVideoRecorder = null; mVideoRecorder = null;
} }
mPictureRecorder = null; mPictureRecorder = null;
getFrameManager().release(); getFrameManager().release();
LOG.i("onStopPreview:", "Releasing preview buffers.");
mCamera.setPreviewCallbackWithBuffer(null); // Release anything left mCamera.setPreviewCallbackWithBuffer(null); // Release anything left
try { try {
LOG.i("onStopPreview:", "Stopping preview.");
mCamera.stopPreview(); mCamera.stopPreview();
LOG.i("onStopPreview:", "Stopped preview.");
} catch (Exception e) { } catch (Exception e) {
LOG.e("stopPreview", "Could not stop preview", e); LOG.e("stopPreview", "Could not stop preview", e);
} }
return Tasks.forResult(null); return Tasks.forResult(null);
} }
@EngineThread
@NonNull @NonNull
@Override @Override
protected Task<Void> onStopBind() { protected Task<Void> onStopBind() {
@ -259,23 +320,37 @@ public class Camera1Engine extends CameraEngine implements
throw new RuntimeException("Unknown CameraPreview output class."); throw new RuntimeException("Unknown CameraPreview output class.");
} }
} catch (IOException e) { } catch (IOException e) {
LOG.e("unbindFromSurface", "Could not release surface", e); // NOTE: when this happens, the next onStopEngine() call hangs on camera.release(),
// Not sure for how long. This causes the destroy() flow to fail the timeout.
LOG.e("onStopBind", "Could not release surface", e);
} }
return Tasks.forResult(null); return Tasks.forResult(null);
} }
@EngineThread
@NonNull @NonNull
@WorkerThread
@Override @Override
protected Task<Void> onStopEngine() { protected Task<Void> onStopEngine() {
LOG.i("onStopEngine:", "About to clean up."); LOG.i("onStopEngine:", "About to clean up.");
mHandler.remove(mFocusResetRunnable); getOrchestrator().remove(JOB_FOCUS_RESET);
if (mFocusEndRunnable != null) { getOrchestrator().remove(JOB_FOCUS_END);
mHandler.remove(mFocusEndRunnable);
}
if (mCamera != null) { if (mCamera != null) {
try { try {
LOG.i("onStopEngine:", "Clean up.", "Releasing camera."); LOG.i("onStopEngine:", "Clean up.", "Releasing camera.");
// Just like Camera2Engine, this call can hang (at least on emulators) and if
// we don't find a way around the lock, it leaves the camera in a bad state.
// This is anticipated by the exception in onStopBind() (see above).
//
// 12:29:32.163 E Camera3-Device: Camera 0: clearStreamingRequest: Device has encountered a serious error
// 12:29:32.163 E Camera2-StreamingProcessor: stopStream: Camera 0: Can't clear stream request: Function not implemented (-38)
// 12:29:32.163 E Camera2Client: stopPreviewL: Camera 0: Can't stop streaming: Function not implemented (-38)
// 12:29:32.273 E Camera2-StreamingProcessor: deletePreviewStream: Unable to delete old preview stream: Device or resource busy (-16)
// 12:29:32.274 E Camera2-CallbackProcessor: deleteStream: Unable to delete callback stream: Device or resource busy (-16)
// 12:29:32.274 E Camera3-Device: Camera 0: disconnect: Shutting down in an error state
//
// I believe there is a thread deadlock due to this call internally waiting to
// dispatch some callback to us (pending captures, ...), but the callback thread
// is blocked here. We try to workaround this in CameraEngine.destroy().
mCamera.release(); mCamera.release();
LOG.i("onStopEngine:", "Clean up.", "Released camera."); LOG.i("onStopEngine:", "Clean up.", "Released camera.");
} catch (Exception e) { } catch (Exception e) {
@ -295,37 +370,49 @@ public class Camera1Engine extends CameraEngine implements
//region Pictures //region Pictures
@WorkerThread @EngineThread
@Override @Override
protected void onTakePicture(@NonNull PictureResult.Stub stub) { protected void onTakePicture(@NonNull PictureResult.Stub stub, boolean doMetering) {
stub.rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT, Axis.RELATIVE_TO_SENSOR); LOG.i("onTakePicture:", "executing.");
stub.rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT,
Axis.RELATIVE_TO_SENSOR);
stub.size = getPictureSize(Reference.OUTPUT); stub.size = getPictureSize(Reference.OUTPUT);
mPictureRecorder = new Full1PictureRecorder(stub, Camera1Engine.this, mCamera); mPictureRecorder = new Full1PictureRecorder(stub, Camera1Engine.this, mCamera);
mPictureRecorder.take(); mPictureRecorder.take();
LOG.i("onTakePicture:", "executed.");
} }
@WorkerThread @EngineThread
@Override @Override
protected void onTakePictureSnapshot(@NonNull PictureResult.Stub stub, @NonNull AspectRatio outputRatio) { protected void onTakePictureSnapshot(@NonNull PictureResult.Stub stub,
stub.size = getUncroppedSnapshotSize(Reference.OUTPUT); // Not the real size: it will be cropped to match the view ratio @NonNull AspectRatio outputRatio,
stub.rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT, Axis.RELATIVE_TO_SENSOR); // Actually it will be rotated and set to 0. boolean doMetering) {
LOG.i("onTakePictureSnapshot:", "executing.");
if (mPreview instanceof GlCameraPreview && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { // Not the real size: it will be cropped to match the view ratio
mPictureRecorder = new SnapshotGlPictureRecorder(stub, this, (GlCameraPreview) mPreview, outputRatio, getOverlay()); stub.size = getUncroppedSnapshotSize(Reference.OUTPUT);
if (mPreview instanceof RendererCameraPreview && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
stub.rotation = getAngles().offset(Reference.VIEW, Reference.OUTPUT, Axis.ABSOLUTE);
mPictureRecorder = new SnapshotGlPictureRecorder(stub, this,
(RendererCameraPreview) mPreview, outputRatio, getOverlay());
} else { } else {
stub.rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT, Axis.RELATIVE_TO_SENSOR);
mPictureRecorder = new Snapshot1PictureRecorder(stub, this, mCamera, outputRatio); mPictureRecorder = new Snapshot1PictureRecorder(stub, this, mCamera, outputRatio);
} }
mPictureRecorder.take(); mPictureRecorder.take();
LOG.i("onTakePictureSnapshot:", "executed.");
} }
//endregion //endregion
//region Videos //region Videos
@EngineThread
@Override @Override
protected void onTakeVideo(@NonNull VideoResult.Stub stub) { protected void onTakeVideo(@NonNull VideoResult.Stub stub) {
stub.rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT, Axis.RELATIVE_TO_SENSOR); stub.rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT,
stub.size = getAngles().flip(Reference.SENSOR, Reference.OUTPUT) ? mCaptureSize.flip() : mCaptureSize; Axis.RELATIVE_TO_SENSOR);
stub.size = getAngles().flip(Reference.SENSOR, Reference.OUTPUT) ? mCaptureSize.flip()
: mCaptureSize;
// Unlock the camera and start recording. // Unlock the camera and start recording.
try { try {
mCamera.unlock(); mCamera.unlock();
@ -340,16 +427,17 @@ public class Camera1Engine extends CameraEngine implements
} }
@SuppressLint("NewApi") @SuppressLint("NewApi")
@WorkerThread @EngineThread
@Override @Override
protected void onTakeVideoSnapshot(@NonNull VideoResult.Stub stub, @NonNull AspectRatio outputRatio) { protected void onTakeVideoSnapshot(@NonNull VideoResult.Stub stub,
if (!(mPreview instanceof GlCameraPreview)) { @NonNull AspectRatio outputRatio) {
throw new IllegalStateException("Video snapshots are only supported with GlCameraPreview."); if (!(mPreview instanceof RendererCameraPreview)) {
throw new IllegalStateException("Video snapshots are only supported with GL_SURFACE.");
} }
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
throw new IllegalStateException("Video snapshots are only supported starting from API 18."); throw new IllegalStateException("Video snapshots are only supported on API 18+.");
} }
GlCameraPreview glPreview = (GlCameraPreview) mPreview; RendererCameraPreview glPreview = (RendererCameraPreview) mPreview;
Size outputSize = getUncroppedSnapshotSize(Reference.OUTPUT); Size outputSize = getUncroppedSnapshotSize(Reference.OUTPUT);
if (outputSize == null) { if (outputSize == null) {
throw new IllegalStateException("outputSize should not be null."); throw new IllegalStateException("outputSize should not be null.");
@ -367,10 +455,11 @@ public class Camera1Engine extends CameraEngine implements
// The correct formula seems to be deviceOrientation+displayOffset, // The correct formula seems to be deviceOrientation+displayOffset,
// which means offset(Reference.VIEW, Reference.OUTPUT, Axis.ABSOLUTE). // which means offset(Reference.VIEW, Reference.OUTPUT, Axis.ABSOLUTE).
stub.rotation = getAngles().offset(Reference.VIEW, Reference.OUTPUT, Axis.ABSOLUTE); stub.rotation = getAngles().offset(Reference.VIEW, Reference.OUTPUT, Axis.ABSOLUTE);
stub.videoFrameRate = Math.round(mPreviewFrameRate);
LOG.i("onTakeVideoSnapshot", "rotation:", stub.rotation, "size:", stub.size); LOG.i("onTakeVideoSnapshot", "rotation:", stub.rotation, "size:", stub.size);
// Start. // Start.
mVideoRecorder = new SnapshotVideoRecorder(Camera1Engine.this, glPreview, getOverlay(), stub.rotation); mVideoRecorder = new SnapshotVideoRecorder(Camera1Engine.this, glPreview, getOverlay());
mVideoRecorder.start(stub); mVideoRecorder.start(stub);
} }
@ -397,6 +486,7 @@ public class Camera1Engine extends CameraEngine implements
applyZoom(params, 0F); applyZoom(params, 0F);
applyExposureCorrection(params, 0F); applyExposureCorrection(params, 0F);
applyPlaySounds(mPlaySounds); applyPlaySounds(mPlaySounds);
applyPreviewFrameRate(params, 0F);
} }
private void applyDefaultFocus(@NonNull Camera.Parameters params) { private void applyDefaultFocus(@NonNull Camera.Parameters params) {
@ -429,21 +519,20 @@ public class Camera1Engine extends CameraEngine implements
public void setFlash(@NonNull Flash flash) { public void setFlash(@NonNull Flash flash) {
final Flash old = mFlash; final Flash old = mFlash;
mFlash = flash; mFlash = flash;
mHandler.run(new Runnable() { mFlashTask = getOrchestrator().scheduleStateful("flash (" + flash + ")",
CameraState.ENGINE,
new Runnable() {
@Override @Override
public void run() { public void run() {
if (getEngineState() == STATE_STARTED) {
Camera.Parameters params = mCamera.getParameters(); Camera.Parameters params = mCamera.getParameters();
if (applyFlash(params, old)) mCamera.setParameters(params); if (applyFlash(params, old)) mCamera.setParameters(params);
} }
mFlashOp.end(null);
}
}); });
} }
private boolean applyFlash(@NonNull Camera.Parameters params, @NonNull Flash oldFlash) { private boolean applyFlash(@NonNull Camera.Parameters params, @NonNull Flash oldFlash) {
if (mCameraOptions.supports(mFlash)) { if (mCameraOptions.supports(mFlash)) {
params.setFlashMode((String) mMapper.map(mFlash)); params.setFlashMode(mMapper.mapFlash(mFlash));
return true; return true;
} }
mFlash = oldFlash; mFlash = oldFlash;
@ -454,15 +543,14 @@ public class Camera1Engine extends CameraEngine implements
public void setLocation(@Nullable Location location) { public void setLocation(@Nullable Location location) {
final Location oldLocation = mLocation; final Location oldLocation = mLocation;
mLocation = location; mLocation = location;
mHandler.run(new Runnable() { mLocationTask = getOrchestrator().scheduleStateful("location",
CameraState.ENGINE,
new Runnable() {
@Override @Override
public void run() { public void run() {
if (getEngineState() == STATE_STARTED) {
Camera.Parameters params = mCamera.getParameters(); Camera.Parameters params = mCamera.getParameters();
if (applyLocation(params, oldLocation)) mCamera.setParameters(params); if (applyLocation(params, oldLocation)) mCamera.setParameters(params);
} }
mLocationOp.end(null);
}
}); });
} }
@ -482,21 +570,26 @@ public class Camera1Engine extends CameraEngine implements
public void setWhiteBalance(@NonNull WhiteBalance whiteBalance) { public void setWhiteBalance(@NonNull WhiteBalance whiteBalance) {
final WhiteBalance old = mWhiteBalance; final WhiteBalance old = mWhiteBalance;
mWhiteBalance = whiteBalance; mWhiteBalance = whiteBalance;
mHandler.run(new Runnable() { mWhiteBalanceTask = getOrchestrator().scheduleStateful(
"white balance (" + whiteBalance + ")",
CameraState.ENGINE,
new Runnable() {
@Override @Override
public void run() { public void run() {
if (getEngineState() == STATE_STARTED) {
Camera.Parameters params = mCamera.getParameters(); Camera.Parameters params = mCamera.getParameters();
if (applyWhiteBalance(params, old)) mCamera.setParameters(params); if (applyWhiteBalance(params, old)) mCamera.setParameters(params);
} }
mWhiteBalanceOp.end(null);
}
}); });
} }
private boolean applyWhiteBalance(@NonNull Camera.Parameters params, @NonNull WhiteBalance oldWhiteBalance) { private boolean applyWhiteBalance(@NonNull Camera.Parameters params,
@NonNull WhiteBalance oldWhiteBalance) {
if (mCameraOptions.supports(mWhiteBalance)) { if (mCameraOptions.supports(mWhiteBalance)) {
params.setWhiteBalance((String) mMapper.map(mWhiteBalance)); // If this lock key is present, the engine can throw when applying the
// parameters, not sure why. Since we never lock it, this should be
// harmless for the rest of the engine.
params.setWhiteBalance(mMapper.mapWhiteBalance(mWhiteBalance));
params.remove("auto-whitebalance-lock");
return true; return true;
} }
mWhiteBalance = oldWhiteBalance; mWhiteBalance = oldWhiteBalance;
@ -507,21 +600,20 @@ public class Camera1Engine extends CameraEngine implements
public void setHdr(@NonNull Hdr hdr) { public void setHdr(@NonNull Hdr hdr) {
final Hdr old = mHdr; final Hdr old = mHdr;
mHdr = hdr; mHdr = hdr;
mHandler.run(new Runnable() { mHdrTask = getOrchestrator().scheduleStateful("hdr (" + hdr + ")",
CameraState.ENGINE,
new Runnable() {
@Override @Override
public void run() { public void run() {
if (getEngineState() == STATE_STARTED) {
Camera.Parameters params = mCamera.getParameters(); Camera.Parameters params = mCamera.getParameters();
if (applyHdr(params, old)) mCamera.setParameters(params); if (applyHdr(params, old)) mCamera.setParameters(params);
} }
mHdrOp.end(null);
}
}); });
} }
private boolean applyHdr(@NonNull Camera.Parameters params, @NonNull Hdr oldHdr) { private boolean applyHdr(@NonNull Camera.Parameters params, @NonNull Hdr oldHdr) {
if (mCameraOptions.supports(mHdr)) { if (mCameraOptions.supports(mHdr)) {
params.setSceneMode((String) mMapper.map(mHdr)); params.setSceneMode(mMapper.mapHdr(mHdr));
return true; return true;
} }
mHdr = oldHdr; mHdr = oldHdr;
@ -532,20 +624,21 @@ public class Camera1Engine extends CameraEngine implements
public void setZoom(final float zoom, @Nullable final PointF[] points, final boolean notify) { public void setZoom(final float zoom, @Nullable final PointF[] points, final boolean notify) {
final float old = mZoomValue; final float old = mZoomValue;
mZoomValue = zoom; mZoomValue = zoom;
mHandler.run(new Runnable() { // 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() {
@Override @Override
public void run() { public void run() {
if (getEngineState() == STATE_STARTED) {
Camera.Parameters params = mCamera.getParameters(); Camera.Parameters params = mCamera.getParameters();
if (applyZoom(params, old)) { if (applyZoom(params, old)) {
mCamera.setParameters(params); mCamera.setParameters(params);
if (notify) { if (notify) {
mCallback.dispatchOnZoomChanged(mZoomValue, points); getCallback().dispatchOnZoomChanged(mZoomValue, points);
} }
} }
} }
mZoomOp.end(null);
}
}); });
} }
@ -565,24 +658,28 @@ public class Camera1Engine extends CameraEngine implements
@Nullable final PointF[] points, final boolean notify) { @Nullable final PointF[] points, final boolean notify) {
final float old = mExposureCorrectionValue; final float old = mExposureCorrectionValue;
mExposureCorrectionValue = EVvalue; mExposureCorrectionValue = EVvalue;
mHandler.run(new Runnable() { // 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,
new Runnable() {
@Override @Override
public void run() { public void run() {
if (getEngineState() == STATE_STARTED) {
Camera.Parameters params = mCamera.getParameters(); Camera.Parameters params = mCamera.getParameters();
if (applyExposureCorrection(params, old)) { if (applyExposureCorrection(params, old)) {
mCamera.setParameters(params); mCamera.setParameters(params);
if (notify) { if (notify) {
mCallback.dispatchOnExposureCorrectionChanged(mExposureCorrectionValue, bounds, points); getCallback().dispatchOnExposureCorrectionChanged(mExposureCorrectionValue,
} bounds, points);
} }
} }
mExposureCorrectionOp.end(null);
} }
}); });
} }
private boolean applyExposureCorrection(@NonNull Camera.Parameters params, float oldExposureCorrection) { private boolean applyExposureCorrection(@NonNull Camera.Parameters params,
float oldExposureCorrection) {
if (mCameraOptions.isExposureCorrectionSupported()) { if (mCameraOptions.isExposureCorrectionSupported()) {
// Just make sure we're inside boundaries. // Just make sure we're inside boundaries.
float max = mCameraOptions.getExposureCorrectionMaxValue(); float max = mCameraOptions.getExposureCorrectionMaxValue();
@ -591,7 +688,8 @@ public class Camera1Engine extends CameraEngine implements
val = val < min ? min : val > max ? max : val; // cap val = val < min ? min : val > max ? max : val; // cap
mExposureCorrectionValue = val; mExposureCorrectionValue = val;
// Apply. // Apply.
int indexValue = (int) (mExposureCorrectionValue / params.getExposureCompensationStep()); int indexValue = (int) (mExposureCorrectionValue
/ params.getExposureCompensationStep());
params.setExposureCompensation(indexValue); params.setExposureCompensation(indexValue);
return true; return true;
} }
@ -603,14 +701,14 @@ public class Camera1Engine extends CameraEngine implements
public void setPlaySounds(boolean playSounds) { public void setPlaySounds(boolean playSounds) {
final boolean old = mPlaySounds; final boolean old = mPlaySounds;
mPlaySounds = playSounds; mPlaySounds = playSounds;
mHandler.run(new Runnable() { mPlaySoundsTask = getOrchestrator().scheduleStateful(
"play sounds (" + playSounds + ")",
CameraState.ENGINE,
new Runnable() {
@Override @Override
public void run() { public void run() {
if (getEngineState() == STATE_STARTED) {
applyPlaySounds(old); applyPlaySounds(old);
} }
mPlaySoundsOp.end(null);
}
}); });
} }
@ -636,31 +734,128 @@ public class Camera1Engine extends CameraEngine implements
return false; return false;
} }
@Override
public void setPreviewFrameRate(float previewFrameRate) {
final float old = previewFrameRate;
mPreviewFrameRate = previewFrameRate;
mPreviewFrameRateTask = getOrchestrator().scheduleStateful(
"preview fps (" + previewFrameRate + ")",
CameraState.ENGINE,
new Runnable() {
@Override
public void run() {
Camera.Parameters params = mCamera.getParameters();
if (applyPreviewFrameRate(params, old)) mCamera.setParameters(params);
}
});
}
private boolean applyPreviewFrameRate(@NonNull Camera.Parameters params,
float oldPreviewFrameRate) {
List<int[]> fpsRanges = params.getSupportedPreviewFpsRange();
sortRanges(fpsRanges);
if (mPreviewFrameRate == 0F) {
// 0F is a special value. Fallback to a reasonable default.
for (int[] fpsRange : fpsRanges) {
float lower = (float) fpsRange[0] / 1000F;
float upper = (float) fpsRange[1] / 1000F;
if ((lower <= 30F && 30F <= upper) || (lower <= 24F && 24F <= upper)) {
params.setPreviewFpsRange(fpsRange[0], fpsRange[1]);
return true;
}
}
} else {
// If out of boundaries, adjust it.
mPreviewFrameRate = Math.min(mPreviewFrameRate,
mCameraOptions.getPreviewFrameRateMaxValue());
mPreviewFrameRate = Math.max(mPreviewFrameRate,
mCameraOptions.getPreviewFrameRateMinValue());
for (int[] fpsRange : fpsRanges) {
float lower = (float) fpsRange[0] / 1000F;
float upper = (float) fpsRange[1] / 1000F;
float rate = Math.round(mPreviewFrameRate);
if (lower <= rate && rate <= upper) {
params.setPreviewFpsRange(fpsRange[0], fpsRange[1]);
return true;
}
}
}
mPreviewFrameRate = oldPreviewFrameRate;
return false;
}
private void sortRanges(List<int[]> fpsRanges) {
if (getPreviewFrameRateExact() && mPreviewFrameRate != 0F) { // sort by range width in ascending order
Collections.sort(fpsRanges, new Comparator<int[]>() {
@Override
public int compare(int[] range1, int[] range2) {
return (range1[1] - range1[0]) - (range2[1] - range2[0]);
}
});
} else { // sort by range width in descending order
Collections.sort(fpsRanges, new Comparator<int[]>() {
@Override
public int compare(int[] range1, int[] range2) {
return (range2[1] - range2[0]) - (range1[1] - range1[0]);
}
});
}
}
@Override
public void setPictureFormat(@NonNull PictureFormat pictureFormat) {
if (pictureFormat != PictureFormat.JPEG) {
throw new UnsupportedOperationException("Unsupported picture format: " + pictureFormat);
}
mPictureFormat = pictureFormat;
}
//endregion //endregion
//region Frame Processing //region Frame Processing
@NonNull @NonNull
@Override @Override
protected FrameManager instantiateFrameManager() { protected FrameManager instantiateFrameManager(int poolSize) {
return new FrameManager(2, this); return new ByteBufferFrameManager(poolSize, this);
}
@NonNull
@Override
public ByteBufferFrameManager getFrameManager() {
return (ByteBufferFrameManager) super.getFrameManager();
}
@Override
public void setHasFrameProcessors(boolean hasFrameProcessors) {
// we don't care, FP is always on
mHasFrameProcessors = hasFrameProcessors;
}
@Override
public void setFrameProcessingFormat(int format) {
// Ignore input: we only support NV21.
mFrameProcessingFormat = ImageFormat.NV21;
} }
@Override @Override
public void onBufferAvailable(@NonNull byte[] buffer) { public void onBufferAvailable(@NonNull byte[] buffer) {
if (getEngineState() == STATE_STARTED) { if (getState().isAtLeast(CameraState.ENGINE)
&& getTargetState().isAtLeast(CameraState.ENGINE)) {
mCamera.addCallbackBuffer(buffer); mCamera.addCallbackBuffer(buffer);
} }
} }
@Override @Override
public void onPreviewFrame(@NonNull byte[] data, Camera camera) { public void onPreviewFrame(byte[] data, Camera camera) {
Frame frame = getFrameManager().getFrame(data, if (data == null) {
System.currentTimeMillis(), // Seen this happen in logs.
getAngles().offset(Reference.SENSOR, Reference.OUTPUT, Axis.RELATIVE_TO_SENSOR), return;
mPreviewStreamSize, }
PREVIEW_FORMAT); Frame frame = getFrameManager().getFrame(data, System.currentTimeMillis());
mCallback.dispatchFrame(frame); if (frame != null) {
getCallback().dispatchFrame(frame);
}
} }
//endregion //endregion
@ -668,47 +863,42 @@ public class Camera1Engine extends CameraEngine implements
//region Auto Focus //region Auto Focus
@Override @Override
public void startAutoFocus(@Nullable final Gesture gesture, @NonNull final PointF point) { public void startAutoFocus(@Nullable final Gesture gesture,
// Must get width and height from the UI thread. @NonNull final MeteringRegions regions,
// TODO could take mPreview.surfaceSize like Camera2 does? @NonNull final PointF legacyPoint) {
int viewWidth = 0, viewHeight = 0; getOrchestrator().scheduleStateful("auto focus", CameraState.BIND, new Runnable() {
if (mPreview != null && mPreview.hasSurface()) {
viewWidth = mPreview.getView().getWidth();
viewHeight = mPreview.getView().getHeight();
}
final int viewWidthF = viewWidth;
final int viewHeightF = viewHeight;
mHandler.run(new Runnable() {
@Override @Override
public void run() { public void run() {
if (getEngineState() < STATE_STARTED) return;
if (!mCameraOptions.isAutoFocusSupported()) return; if (!mCameraOptions.isAutoFocusSupported()) return;
final PointF p = new PointF(point.x, point.y); // copy. MeteringTransform<Camera.Area> transform = new Camera1MeteringTransform(
int offset = getAngles().offset(Reference.SENSOR, Reference.VIEW, Axis.ABSOLUTE); getAngles(),
List<Camera.Area> meteringAreas2 = computeMeteringAreas(p.x, p.y, getPreview().getSurfaceSize());
viewWidthF, viewHeightF, offset); MeteringRegions transformed = regions.transform(transform);
List<Camera.Area> meteringAreas1 = meteringAreas2.subList(0, 1);
// At this point we are sure that camera supports auto focus... right? Look at CameraView.onTouchEvent().
Camera.Parameters params = mCamera.getParameters(); Camera.Parameters params = mCamera.getParameters();
int maxAF = params.getMaxNumFocusAreas(); int maxAF = params.getMaxNumFocusAreas();
int maxAE = params.getMaxNumMeteringAreas(); int maxAE = params.getMaxNumMeteringAreas();
if (maxAF > 0) params.setFocusAreas(maxAF > 1 ? meteringAreas2 : meteringAreas1); if (maxAF > 0) params.setFocusAreas(transformed.get(maxAF, transform));
if (maxAE > 0) params.setMeteringAreas(maxAE > 1 ? meteringAreas2 : meteringAreas1); if (maxAE > 0) params.setMeteringAreas(transformed.get(maxAE, transform));
params.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO); params.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
try {
mCamera.setParameters(params); mCamera.setParameters(params);
mCallback.dispatchOnFocusStart(gesture, p); } catch (RuntimeException re) {
LOG.e("startAutoFocus:", "Failed to set camera parameters");
throw new CameraException(re, CameraException.REASON_UNKNOWN);
}
getCallback().dispatchOnFocusStart(gesture, legacyPoint);
// The auto focus callback is not guaranteed to be called, but we really want it to be. // The auto focus callback is not guaranteed to be called, but we really want it
// So we remove the old runnable if still present and post a new one. // to be. So we remove the old runnable if still present and post a new one.
if (mFocusEndRunnable != null) mHandler.remove(mFocusEndRunnable); getOrchestrator().remove(JOB_FOCUS_END);
mFocusEndRunnable = new Runnable() { getOrchestrator().scheduleDelayed(JOB_FOCUS_END, true, AUTOFOCUS_END_DELAY_MILLIS,
new Runnable() {
@Override @Override
public void run() { public void run() {
mCallback.dispatchOnFocusEnd(gesture, false, p); getCallback().dispatchOnFocusEnd(gesture, false, legacyPoint);
} }
}; });
mHandler.post(AUTOFOCUS_END_DELAY_MILLIS, mFocusEndRunnable);
// Wrapping autoFocus in a try catch to handle some device specific exceptions, // Wrapping autoFocus in a try catch to handle some device specific exceptions,
// see See https://github.com/natario1/CameraView/issues/181. // see See https://github.com/natario1/CameraView/issues/181.
@ -716,73 +906,17 @@ public class Camera1Engine extends CameraEngine implements
mCamera.autoFocus(new Camera.AutoFocusCallback() { mCamera.autoFocus(new Camera.AutoFocusCallback() {
@Override @Override
public void onAutoFocus(boolean success, Camera camera) { public void onAutoFocus(boolean success, Camera camera) {
if (mFocusEndRunnable != null) { getOrchestrator().remove(JOB_FOCUS_END);
mHandler.remove(mFocusEndRunnable); getOrchestrator().remove(JOB_FOCUS_RESET);
mFocusEndRunnable = null; getCallback().dispatchOnFocusEnd(gesture, success, legacyPoint);
}
mCallback.dispatchOnFocusEnd(gesture, success, p);
mHandler.remove(mFocusResetRunnable);
if (shouldResetAutoFocus()) { if (shouldResetAutoFocus()) {
mHandler.post(getAutoFocusResetDelay(), mFocusResetRunnable); getOrchestrator().scheduleStatefulDelayed(
} JOB_FOCUS_RESET,
} CameraState.ENGINE,
}); getAutoFocusResetDelay(),
} catch (RuntimeException e) { new Runnable() {
LOG.e("startAutoFocus:", "Error calling autoFocus", e);
// Let the mFocusEndRunnable do its job. (could remove it and quickly dispatch
// onFocusEnd here, but let's make it simpler).
}
}
});
}
@NonNull
@WorkerThread
private static List<Camera.Area> computeMeteringAreas(double viewClickX, double viewClickY,
int viewWidth, int viewHeight,
int sensorToDisplay) {
// Event came in view coordinates. We must rotate to sensor coordinates.
// First, rescale to the -1000 ... 1000 range.
int displayToSensor = -sensorToDisplay;
viewClickX = -1000d + (viewClickX / (double) viewWidth) * 2000d;
viewClickY = -1000d + (viewClickY / (double) viewHeight) * 2000d;
// Apply rotation to this point.
// https://academo.org/demos/rotation-about-point/
double theta = ((double) displayToSensor) * Math.PI / 180;
double sensorClickX = viewClickX * Math.cos(theta) - viewClickY * Math.sin(theta);
double sensorClickY = viewClickX * Math.sin(theta) + viewClickY * Math.cos(theta);
LOG.i("focus:", "viewClickX:", viewClickX, "viewClickY:", viewClickY);
LOG.i("focus:", "sensorClickX:", sensorClickX, "sensorClickY:", sensorClickY);
// Compute the rect bounds.
Rect rect1 = computeMeteringArea(sensorClickX, sensorClickY, 150d);
int weight1 = 1000; // 150 * 150 * 1000 = more than 10.000.000
Rect rect2 = computeMeteringArea(sensorClickX, sensorClickY, 300d);
int weight2 = 100; // 300 * 300 * 100 = 9.000.000
List<Camera.Area> list = new ArrayList<>(2);
list.add(new Camera.Area(rect1, weight1));
list.add(new Camera.Area(rect2, weight2));
return list;
}
@NonNull
private static Rect computeMeteringArea(double centerX, double centerY, double size) {
double delta = size / 2d;
int top = (int) Math.max(centerY - delta, -1000);
int bottom = (int) Math.min(centerY + delta, 1000);
int left = (int) Math.max(centerX - delta, -1000);
int right = (int) Math.min(centerX + delta, 1000);
LOG.i("focus:", "computeMeteringArea:", "top:", top, "left:", left, "bottom:", bottom, "right:", right);
return new Rect(left, top, right, bottom);
}
private final Runnable mFocusResetRunnable = new Runnable() {
@Override @Override
public void run() { public void run() {
if (getEngineState() < STATE_STARTED) return;
mCamera.cancelAutoFocus(); mCamera.cancelAutoFocus();
Camera.Parameters params = mCamera.getParameters(); Camera.Parameters params = mCamera.getParameters();
int maxAF = params.getMaxNumFocusAreas(); int maxAF = params.getMaxNumFocusAreas();
@ -792,7 +926,18 @@ public class Camera1Engine extends CameraEngine implements
applyDefaultFocus(params); // Revert to internal focus. applyDefaultFocus(params); // Revert to internal focus.
mCamera.setParameters(params); mCamera.setParameters(params);
} }
}; });
}
}
});
} catch (RuntimeException e) {
LOG.e("startAutoFocus:", "Error calling autoFocus", e);
// Let the mFocusEndRunnable do its job. (could remove it and quickly dispatch
// onFocusEnd here, but let's make it simpler).
}
}
});
}
//endregion //endregion
} }

@ -0,0 +1,978 @@
package com.otaliastudios.cameraview.engine;
import android.location.Location;
import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.Tasks;
import com.otaliastudios.cameraview.CameraException;
import com.otaliastudios.cameraview.CameraOptions;
import com.otaliastudios.cameraview.PictureResult;
import com.otaliastudios.cameraview.VideoResult;
import com.otaliastudios.cameraview.controls.Audio;
import com.otaliastudios.cameraview.controls.AudioCodec;
import com.otaliastudios.cameraview.controls.Facing;
import com.otaliastudios.cameraview.controls.Flash;
import com.otaliastudios.cameraview.controls.Hdr;
import com.otaliastudios.cameraview.controls.Mode;
import com.otaliastudios.cameraview.controls.PictureFormat;
import com.otaliastudios.cameraview.controls.VideoCodec;
import com.otaliastudios.cameraview.controls.WhiteBalance;
import com.otaliastudios.cameraview.engine.offset.Angles;
import com.otaliastudios.cameraview.engine.offset.Reference;
import com.otaliastudios.cameraview.engine.orchestrator.CameraState;
import com.otaliastudios.cameraview.frame.FrameManager;
import com.otaliastudios.cameraview.overlay.Overlay;
import com.otaliastudios.cameraview.picture.PictureRecorder;
import com.otaliastudios.cameraview.preview.CameraPreview;
import com.otaliastudios.cameraview.size.AspectRatio;
import com.otaliastudios.cameraview.size.Size;
import com.otaliastudios.cameraview.size.SizeSelector;
import com.otaliastudios.cameraview.size.SizeSelectors;
import com.otaliastudios.cameraview.video.VideoRecorder;
import java.io.File;
import java.io.FileDescriptor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* Abstract implementation of {@link CameraEngine} that helps in common tasks.
*/
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;
@SuppressWarnings("WeakerAccess") protected VideoRecorder mVideoRecorder;
@SuppressWarnings("WeakerAccess") protected Size mCaptureSize;
@SuppressWarnings("WeakerAccess") protected Size mPreviewStreamSize;
@SuppressWarnings("WeakerAccess") protected Size mFrameProcessingSize;
@SuppressWarnings("WeakerAccess") protected int mFrameProcessingFormat;
@SuppressWarnings("WeakerAccess") protected boolean mHasFrameProcessors;
@SuppressWarnings("WeakerAccess") protected Flash mFlash;
@SuppressWarnings("WeakerAccess") protected WhiteBalance mWhiteBalance;
@SuppressWarnings("WeakerAccess") protected VideoCodec mVideoCodec;
@SuppressWarnings("WeakerAccess") protected AudioCodec mAudioCodec;
@SuppressWarnings("WeakerAccess") protected Hdr mHdr;
@SuppressWarnings("WeakerAccess") protected PictureFormat mPictureFormat;
@SuppressWarnings("WeakerAccess") protected Location mLocation;
@SuppressWarnings("WeakerAccess") protected float mZoomValue;
@SuppressWarnings("WeakerAccess") protected float mExposureCorrectionValue;
@SuppressWarnings("WeakerAccess") protected boolean mPlaySounds;
@SuppressWarnings("WeakerAccess") protected boolean mPictureMetering;
@SuppressWarnings("WeakerAccess") protected boolean mPictureSnapshotMetering;
@SuppressWarnings("WeakerAccess") protected float mPreviewFrameRate;
@SuppressWarnings("WeakerAccess") private boolean mPreviewFrameRateExact;
private FrameManager mFrameManager;
private final Angles mAngles = new Angles();
@Nullable private SizeSelector mPreviewStreamSizeSelector;
private SizeSelector mPictureSizeSelector;
private SizeSelector mVideoSizeSelector;
private Facing mFacing;
private Mode mMode;
private Audio mAudio;
private long mVideoMaxSize;
private int mVideoMaxDuration;
private int mVideoBitRate;
private int mAudioBitRate;
private long mAutoFocusResetDelayMillis;
private int mSnapshotMaxWidth; // in REF_VIEW like SizeSelectors
private int mSnapshotMaxHeight; // in REF_VIEW like SizeSelectors
private int mFrameProcessingMaxWidth; // in REF_VIEW like SizeSelectors
private int mFrameProcessingMaxHeight; // in REF_VIEW like SizeSelectors
private int mFrameProcessingPoolSize;
private Overlay mOverlay;
// Ops used for testing.
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Task<Void> mZoomTask
= Tasks.forResult(null);
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Task<Void> mExposureCorrectionTask
= Tasks.forResult(null);
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Task<Void> mFlashTask
= Tasks.forResult(null);
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Task<Void> mWhiteBalanceTask
= Tasks.forResult(null);
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Task<Void> mHdrTask
= Tasks.forResult(null);
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Task<Void> mLocationTask
= Tasks.forResult(null);
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Task<Void> mPlaySoundsTask
= Tasks.forResult(null);
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) Task<Void> mPreviewFrameRateTask
= Tasks.forResult(null);
@SuppressWarnings("WeakerAccess")
protected CameraBaseEngine(@NonNull Callback callback) {
super(callback);
}
/**
* Called at construction time to get a frame manager that can later be
* accessed through {@link #getFrameManager()}.
* @param poolSize pool size
* @return a frame manager
*/
@NonNull
protected abstract FrameManager instantiateFrameManager(int poolSize);
@NonNull
@Override
public final Angles getAngles() {
return mAngles;
}
@NonNull
@Override
public FrameManager getFrameManager() {
if (mFrameManager == null) {
mFrameManager = instantiateFrameManager(mFrameProcessingPoolSize);
}
return mFrameManager;
}
@Nullable
@Override
public final CameraOptions getCameraOptions() {
return mCameraOptions;
}
@Override
public final void setPreview(@NonNull CameraPreview cameraPreview) {
if (mPreview != null) mPreview.setSurfaceCallback(null);
mPreview = cameraPreview;
mPreview.setSurfaceCallback(this);
}
@NonNull
@Override
public final CameraPreview getPreview() {
return mPreview;
}
@Override
public final void setOverlay(@Nullable Overlay overlay) {
mOverlay = overlay;
}
@Nullable
@Override
public final Overlay getOverlay() {
return mOverlay;
}
@Override
public final void setPreviewStreamSizeSelector(@Nullable SizeSelector selector) {
mPreviewStreamSizeSelector = selector;
}
@Nullable
@Override
public final SizeSelector getPreviewStreamSizeSelector() {
return mPreviewStreamSizeSelector;
}
@Override
public final void setPictureSizeSelector(@NonNull SizeSelector selector) {
mPictureSizeSelector = selector;
}
@NonNull
@Override
public final SizeSelector getPictureSizeSelector() {
return mPictureSizeSelector;
}
@Override
public final void setVideoSizeSelector(@NonNull SizeSelector selector) {
mVideoSizeSelector = selector;
}
@NonNull
@Override
public final SizeSelector getVideoSizeSelector() {
return mVideoSizeSelector;
}
@Override
public final void setVideoMaxSize(long videoMaxSizeBytes) {
mVideoMaxSize = videoMaxSizeBytes;
}
@Override
public final long getVideoMaxSize() {
return mVideoMaxSize;
}
@Override
public final void setVideoMaxDuration(int videoMaxDurationMillis) {
mVideoMaxDuration = videoMaxDurationMillis;
}
@Override
public final int getVideoMaxDuration() {
return mVideoMaxDuration;
}
@Override
public final void setVideoCodec(@NonNull VideoCodec codec) {
mVideoCodec = codec;
}
@NonNull
@Override
public final VideoCodec getVideoCodec() {
return mVideoCodec;
}
@Override
public final void setVideoBitRate(int videoBitRate) {
mVideoBitRate = videoBitRate;
}
@Override
public final int getVideoBitRate() {
return mVideoBitRate;
}
@Override
public final void setAudioCodec(@NonNull AudioCodec codec) {
mAudioCodec = codec;
}
@NonNull
@Override
public final AudioCodec getAudioCodec() {
return mAudioCodec;
}
@Override
public final void setAudioBitRate(int audioBitRate) {
mAudioBitRate = audioBitRate;
}
@Override
public final int getAudioBitRate() {
return mAudioBitRate;
}
@Override
public final void setSnapshotMaxWidth(int maxWidth) {
mSnapshotMaxWidth = maxWidth;
}
@Override
public final int getSnapshotMaxWidth() {
return mSnapshotMaxWidth;
}
@Override
public final void setSnapshotMaxHeight(int maxHeight) {
mSnapshotMaxHeight = maxHeight;
}
@Override
public final int getSnapshotMaxHeight() {
return mSnapshotMaxHeight;
}
@Override
public final void setFrameProcessingMaxWidth(int maxWidth) {
mFrameProcessingMaxWidth = maxWidth;
}
@Override
public final int getFrameProcessingMaxWidth() {
return mFrameProcessingMaxWidth;
}
@Override
public final void setFrameProcessingMaxHeight(int maxHeight) {
mFrameProcessingMaxHeight = maxHeight;
}
@Override
public final int getFrameProcessingMaxHeight() {
return mFrameProcessingMaxHeight;
}
@Override
public final int getFrameProcessingFormat() {
return mFrameProcessingFormat;
}
@Override
public final void setFrameProcessingPoolSize(int poolSize) {
mFrameProcessingPoolSize = poolSize;
}
@Override
public final int getFrameProcessingPoolSize() {
return mFrameProcessingPoolSize;
}
@Override
public final void setAutoFocusResetDelay(long delayMillis) {
mAutoFocusResetDelayMillis = delayMillis;
}
@Override
public final long getAutoFocusResetDelay() {
return mAutoFocusResetDelayMillis;
}
/**
* Helper function for subclasses.
* @return true if AF should be reset
*/
@SuppressWarnings("WeakerAccess")
protected final boolean shouldResetAutoFocus() {
return mAutoFocusResetDelayMillis > 0 && mAutoFocusResetDelayMillis != Long.MAX_VALUE;
}
/**
* Sets a new facing value. This will restart the engine session (if there's any)
* so that we can open the new facing camera.
* @param facing facing
*/
@Override
public final void setFacing(final @NonNull Facing facing) {
final Facing old = mFacing;
if (facing != old) {
mFacing = facing;
getOrchestrator().scheduleStateful("facing", CameraState.ENGINE,
new Runnable() {
@Override
public void run() {
if (collectCameraInfo(facing)) {
restart();
} else {
mFacing = old;
}
}
});
}
}
@NonNull
@Override
public final Facing getFacing() {
return mFacing;
}
/**
* Sets a new audio value that will be used for video recordings.
* @param audio desired audio
*/
@Override
public final void setAudio(@NonNull Audio audio) {
if (mAudio != audio) {
if (isTakingVideo()) {
LOG.w("Audio setting was changed while recording. " +
"Changes will take place starting from next video");
}
mAudio = audio;
}
}
@NonNull
@Override
public final Audio getAudio() {
return mAudio;
}
/**
* Sets the desired mode (either picture or video).
* @param mode desired mode.
*/
@Override
public final void setMode(@NonNull Mode mode) {
if (mode != mMode) {
mMode = mode;
getOrchestrator().scheduleStateful("mode", CameraState.ENGINE,
new Runnable() {
@Override
public void run() {
restart();
}
});
}
}
@NonNull
@Override
public final Mode getMode() {
return mMode;
}
@Override
public final float getZoomValue() {
return mZoomValue;
}
@Override
public final float getExposureCorrectionValue() {
return mExposureCorrectionValue;
}
@NonNull
@Override
public final Flash getFlash() {
return mFlash;
}
@NonNull
@Override
public final WhiteBalance getWhiteBalance() {
return mWhiteBalance;
}
@NonNull
@Override
public final Hdr getHdr() {
return mHdr;
}
@Nullable
@Override
public final Location getLocation() {
return mLocation;
}
@NonNull
@Override
public final PictureFormat getPictureFormat() {
return mPictureFormat;
}
@Override
public final void setPreviewFrameRateExact(boolean previewFrameRateExact) {
mPreviewFrameRateExact = previewFrameRateExact;
}
@Override
public final boolean getPreviewFrameRateExact() {
return mPreviewFrameRateExact;
}
@Override
public final float getPreviewFrameRate() {
return mPreviewFrameRate;
}
@Override
public final boolean hasFrameProcessors() {
return mHasFrameProcessors;
}
@Override
public final void setPictureMetering(boolean enable) {
mPictureMetering = enable;
}
@Override
public final boolean getPictureMetering() {
return mPictureMetering;
}
@Override
public final void setPictureSnapshotMetering(boolean enable) {
mPictureSnapshotMetering = enable;
}
@Override
public final boolean getPictureSnapshotMetering() {
return mPictureSnapshotMetering;
}
//region Picture and video control
@Override
public final boolean isTakingPicture() {
return mPictureRecorder != null;
}
@Override
public /* final */ void takePicture(final @NonNull PictureResult.Stub stub) {
// Save boolean before scheduling! See how Camera2Engine calls this with a temp value.
final boolean metering = mPictureMetering;
getOrchestrator().scheduleStateful("take picture", CameraState.BIND,
new Runnable() {
@Override
public void run() {
LOG.i("takePicture:", "running. isTakingPicture:", isTakingPicture());
if (isTakingPicture()) return;
if (mMode == Mode.VIDEO) {
throw new IllegalStateException("Can't take hq pictures while in VIDEO mode");
}
stub.isSnapshot = false;
stub.location = mLocation;
stub.facing = mFacing;
stub.format = mPictureFormat;
onTakePicture(stub, metering);
}
});
}
/**
* The snapshot size is the {@link #getPreviewStreamSize(Reference)}, but cropped based on the
* view/surface aspect ratio.
* @param stub a picture stub
*/
@Override
public /* final */ void takePictureSnapshot(final @NonNull PictureResult.Stub stub) {
// Save boolean before scheduling! See how Camera2Engine calls this with a temp value.
final boolean metering = mPictureSnapshotMetering;
getOrchestrator().scheduleStateful("take picture snapshot", CameraState.BIND,
new Runnable() {
@Override
public void run() {
LOG.i("takePictureSnapshot:", "running. isTakingPicture:", isTakingPicture());
if (isTakingPicture()) return;
stub.location = mLocation;
stub.isSnapshot = true;
stub.facing = mFacing;
stub.format = PictureFormat.JPEG;
// Leave the other parameters to subclasses.
//noinspection ConstantConditions
AspectRatio ratio = AspectRatio.of(getPreviewSurfaceSize(Reference.OUTPUT));
onTakePictureSnapshot(stub, ratio, metering);
}
});
}
@Override
public void onPictureShutter(boolean didPlaySound) {
getCallback().dispatchOnPictureShutter(!didPlaySound);
}
@Override
public void onPictureResult(@Nullable PictureResult.Stub result, @Nullable Exception error) {
mPictureRecorder = null;
if (result != null && result.data != null) {
getCallback().dispatchOnPictureTaken(result);
} else {
LOG.e("onPictureResult", "result or data is null: something went wrong.", error);
getCallback().dispatchError(new CameraException(error,
CameraException.REASON_PICTURE_FAILED));
}
}
@Override
public final boolean isTakingVideo() {
return mVideoRecorder != null && mVideoRecorder.isRecording();
}
@Override
public final void takeVideo(final @NonNull VideoResult.Stub stub,
final @Nullable File file,
final @Nullable FileDescriptor fileDescriptor) {
getOrchestrator().scheduleStateful("take video", CameraState.BIND, new Runnable() {
@Override
public void run() {
LOG.i("takeVideo:", "running. isTakingVideo:", isTakingVideo());
if (isTakingVideo()) return;
if (mMode == Mode.PICTURE) {
throw new IllegalStateException("Can't record video while in PICTURE mode");
}
if (file != null) {
stub.file = file;
} else if (fileDescriptor != null) {
stub.fileDescriptor = fileDescriptor;
} else {
throw new IllegalStateException("file and fileDescriptor are both null.");
}
stub.isSnapshot = false;
stub.videoCodec = mVideoCodec;
stub.audioCodec = mAudioCodec;
stub.location = mLocation;
stub.facing = mFacing;
stub.audio = mAudio;
stub.maxSize = mVideoMaxSize;
stub.maxDuration = mVideoMaxDuration;
stub.videoBitRate = mVideoBitRate;
stub.audioBitRate = mAudioBitRate;
onTakeVideo(stub);
}
});
}
/**
* @param stub a video stub
* @param file the output file
*/
@Override
public final void takeVideoSnapshot(@NonNull final VideoResult.Stub stub,
@NonNull final File file) {
getOrchestrator().scheduleStateful("take video snapshot", CameraState.BIND,
new Runnable() {
@Override
public void run() {
LOG.i("takeVideoSnapshot:", "running. isTakingVideo:", isTakingVideo());
stub.file = file;
stub.isSnapshot = true;
stub.videoCodec = mVideoCodec;
stub.audioCodec = mAudioCodec;
stub.location = mLocation;
stub.facing = mFacing;
stub.videoBitRate = mVideoBitRate;
stub.audioBitRate = mAudioBitRate;
stub.audio = mAudio;
stub.maxSize = mVideoMaxSize;
stub.maxDuration = mVideoMaxDuration;
//noinspection ConstantConditions
AspectRatio ratio = AspectRatio.of(getPreviewSurfaceSize(Reference.OUTPUT));
onTakeVideoSnapshot(stub, ratio);
}
});
}
@Override
public final void stopVideo() {
getOrchestrator().schedule("stop video", true, new Runnable() {
@Override
public void run() {
LOG.i("stopVideo", "running. isTakingVideo?", isTakingVideo());
onStopVideo();
}
});
}
@EngineThread
@SuppressWarnings("WeakerAccess")
protected void onStopVideo() {
if (mVideoRecorder != null) {
mVideoRecorder.stop(false);
// Do not null this, so we respond correctly to isTakingVideo(),
// which checks for recorder presence and recorder.isRecording().
// It will be nulled in onVideoResult.
}
}
@CallSuper
@Override
public void onVideoResult(@Nullable VideoResult.Stub result, @Nullable Exception exception) {
mVideoRecorder = null;
if (result != null) {
getCallback().dispatchOnVideoTaken(result);
} else {
LOG.e("onVideoResult", "result is null: something went wrong.", exception);
getCallback().dispatchError(new CameraException(exception,
CameraException.REASON_VIDEO_FAILED));
}
}
@Override
public void onVideoRecordingStart() {
getCallback().dispatchOnVideoRecordingStart();
}
@Override
public void onVideoRecordingEnd() {
getCallback().dispatchOnVideoRecordingEnd();
}
@EngineThread
protected abstract void onTakePicture(@NonNull PictureResult.Stub stub, boolean doMetering);
@EngineThread
protected abstract void onTakePictureSnapshot(@NonNull PictureResult.Stub stub,
@NonNull AspectRatio outputRatio,
boolean doMetering);
@EngineThread
protected abstract void onTakeVideoSnapshot(@NonNull VideoResult.Stub stub,
@NonNull AspectRatio outputRatio);
@EngineThread
protected abstract void onTakeVideo(@NonNull VideoResult.Stub stub);
//endregion
//region Size / Surface
@Override
public final void onSurfaceChanged() {
LOG.i("onSurfaceChanged:", "Size is", getPreviewSurfaceSize(Reference.VIEW));
getOrchestrator().scheduleStateful("surface changed", CameraState.BIND,
new Runnable() {
@Override
public void run() {
// Compute a new camera preview size and apply.
Size newSize = computePreviewStreamSize();
if (newSize.equals(mPreviewStreamSize)) {
LOG.i("onSurfaceChanged:",
"The computed preview size is identical. No op.");
} else {
LOG.i("onSurfaceChanged:",
"Computed a new preview size. Calling onPreviewStreamSizeChanged().");
mPreviewStreamSize = newSize;
onPreviewStreamSizeChanged();
}
}
});
}
/**
* The preview stream size has changed. At this point, some engine might want to
* simply call {@link #restartPreview()}, others to {@link #restartBind()}.
*
* It basically depends on the step at which the preview stream size is actually used.
*/
@EngineThread
protected abstract void onPreviewStreamSizeChanged();
@Nullable
@Override
public final Size getPictureSize(@SuppressWarnings("SameParameterValue") @NonNull Reference reference) {
Size size = mCaptureSize;
if (size == null || mMode == Mode.VIDEO) return null;
return getAngles().flip(Reference.SENSOR, reference) ? size.flip() : size;
}
@Nullable
@Override
public final Size getVideoSize(@SuppressWarnings("SameParameterValue") @NonNull Reference reference) {
Size size = mCaptureSize;
if (size == null || mMode == Mode.PICTURE) return null;
return getAngles().flip(Reference.SENSOR, reference) ? size.flip() : size;
}
@Nullable
@Override
public final Size getPreviewStreamSize(@NonNull Reference reference) {
Size size = mPreviewStreamSize;
if (size == null) return null;
return getAngles().flip(Reference.SENSOR, reference) ? size.flip() : size;
}
@SuppressWarnings("SameParameterValue")
@Nullable
private Size getPreviewSurfaceSize(@NonNull Reference reference) {
CameraPreview preview = mPreview;
if (preview == null) return null;
return getAngles().flip(Reference.VIEW, reference) ? preview.getSurfaceSize().flip()
: preview.getSurfaceSize();
}
/**
* Returns the snapshot size, but not cropped with the view dimensions, which
* is what we will do before creating the snapshot. However, cropping is done at various
* levels so we don't want to perform the op here.
*
* The base snapshot size is based on PreviewStreamSize (later cropped with view ratio). Why?
* One might be tempted to say that it's the SurfaceSize (which already matches the view ratio).
*
* The camera sensor will capture preview frames with PreviewStreamSize and that's it. Then they
* are hardware-scaled by the preview surface, but this does not affect the snapshot, as the
* snapshot recorder simply creates another surface.
*
* Done tests to ensure that this is true, by using
* 1. small SurfaceSize and biggest() PreviewStreamSize: output is not low quality
* 2. big SurfaceSize and smallest() PreviewStreamSize: output is low quality
* In both cases the result.size here was set to the biggest of the two.
*
* I could not find the same evidence for videos, but I would say that the same things should
* apply, despite the capturing mechanism being different.
*
* @param reference the reference system
* @return the uncropped snapshot size
*/
@Nullable
@Override
public final Size getUncroppedSnapshotSize(@NonNull Reference reference) {
Size baseSize = getPreviewStreamSize(reference);
if (baseSize == null) return null;
boolean flip = getAngles().flip(reference, Reference.VIEW);
int maxWidth = flip ? mSnapshotMaxHeight : mSnapshotMaxWidth;
int maxHeight = flip ? mSnapshotMaxWidth : mSnapshotMaxHeight;
if (maxWidth <= 0) maxWidth = Integer.MAX_VALUE;
if (maxHeight <= 0) maxHeight = Integer.MAX_VALUE;
float baseRatio = AspectRatio.of(baseSize).toFloat();
float maxValuesRatio = AspectRatio.of(maxWidth, maxHeight).toFloat();
if (maxValuesRatio >= baseRatio) {
// Height is the real constraint.
int outHeight = Math.min(baseSize.getHeight(), maxHeight);
int outWidth = (int) Math.floor((float) outHeight * baseRatio);
return new Size(outWidth, outHeight);
} else {
// Width is the real constraint.
int outWidth = Math.min(baseSize.getWidth(), maxWidth);
int outHeight = (int) Math.floor((float) outWidth / baseRatio);
return new Size(outWidth, outHeight);
}
}
/**
* This is called either on cameraView.start(), or when the underlying surface changes.
* It is possible that in the first call the preview surface has not already computed its
* dimensions.
* But when it does, the {@link CameraPreview.SurfaceCallback} should be called,
* and this should be refreshed.
*
* @return the capture size
*/
@NonNull
@SuppressWarnings("WeakerAccess")
protected final Size computeCaptureSize() {
return computeCaptureSize(mMode);
}
@NonNull
@SuppressWarnings("WeakerAccess")
protected final Size computeCaptureSize(@NonNull Mode mode) {
// We want to pass stuff into the REF_VIEW reference, not the sensor one.
// This is already managed by CameraOptions, so we just flip again at the end.
boolean flip = getAngles().flip(Reference.SENSOR, Reference.VIEW);
SizeSelector selector;
Collection<Size> sizes;
if (mode == Mode.PICTURE) {
selector = mPictureSizeSelector;
sizes = mCameraOptions.getSupportedPictureSizes();
} else {
selector = mVideoSizeSelector;
sizes = mCameraOptions.getSupportedVideoSizes();
}
selector = SizeSelectors.or(selector, SizeSelectors.biggest());
List<Size> list = new ArrayList<>(sizes);
Size result = selector.select(list).get(0);
if (!list.contains(result)) {
throw new RuntimeException("SizeSelectors must not return Sizes other than " +
"those in the input list.");
}
LOG.i("computeCaptureSize:", "result:", result, "flip:", flip, "mode:", mode);
if (flip) result = result.flip(); // Go back to REF_SENSOR
return result;
}
/**
* This is called anytime {@link #computePreviewStreamSize()} is called.
* This means that it should be called during the binding process, when
* we can be sure that the camera is available (engineState == STARTED).
* @return a list of available sizes for preview
*/
@EngineThread
@NonNull
protected abstract List<Size> getPreviewStreamAvailableSizes();
@EngineThread
@NonNull
@SuppressWarnings("WeakerAccess")
protected final Size computePreviewStreamSize() {
@NonNull List<Size> previewSizes = getPreviewStreamAvailableSizes();
// These sizes come in REF_SENSOR. Since there is an external selector involved,
// we must convert all of them to REF_VIEW, then flip back when returning.
boolean flip = getAngles().flip(Reference.SENSOR, Reference.VIEW);
List<Size> sizes = new ArrayList<>(previewSizes.size());
for (Size size : previewSizes) {
sizes.add(flip ? size.flip() : size);
}
// Create our own default selector, which will be used if the external
// mPreviewStreamSizeSelector is null, or if it fails in finding a size.
Size targetMinSize = getPreviewSurfaceSize(Reference.VIEW);
if (targetMinSize == null) {
throw new IllegalStateException("targetMinSize should not be null here.");
}
AspectRatio targetRatio = AspectRatio.of(mCaptureSize.getWidth(), mCaptureSize.getHeight());
if (flip) targetRatio = targetRatio.flip();
LOG.i("computePreviewStreamSize:",
"targetRatio:", targetRatio,
"targetMinSize:", targetMinSize);
SizeSelector matchRatio = SizeSelectors.and( // Match this aspect ratio and sort by biggest
SizeSelectors.aspectRatio(targetRatio, 0),
SizeSelectors.biggest());
SizeSelector matchSize = SizeSelectors.and( // Bigger than this size, and sort by smallest
SizeSelectors.minHeight(targetMinSize.getHeight()),
SizeSelectors.minWidth(targetMinSize.getWidth()),
SizeSelectors.smallest());
SizeSelector matchAll = SizeSelectors.or(
SizeSelectors.and(matchRatio, matchSize), // Try to respect both constraints.
matchSize, // If couldn't match aspect ratio, at least respect the size
matchRatio, // If couldn't respect size, at least match aspect ratio
SizeSelectors.biggest() // If couldn't match any, take the biggest.
);
// Apply the external selector with this as a fallback,
// and return a size in REF_SENSOR reference.
SizeSelector selector;
if (mPreviewStreamSizeSelector != null) {
selector = SizeSelectors.or(mPreviewStreamSizeSelector, matchAll);
} else {
selector = matchAll;
}
Size result = selector.select(sizes).get(0);
if (!sizes.contains(result)) {
throw new RuntimeException("SizeSelectors must not return Sizes other than " +
"those in the input list.");
}
if (flip) result = result.flip();
LOG.i("computePreviewStreamSize:", "result:", result, "flip:", flip);
return result;
}
/**
* This is called anytime {@link #computeFrameProcessingSize()} is called.
* Implementors can return null if frame processor size is not selectable
* @return a list of available sizes for frame processing
*/
@EngineThread
@NonNull
protected abstract List<Size> getFrameProcessingAvailableSizes();
@EngineThread
@NonNull
@SuppressWarnings("WeakerAccess")
protected final Size computeFrameProcessingSize() {
@NonNull List<Size> frameSizes = getFrameProcessingAvailableSizes();
// These sizes come in REF_SENSOR. Since there is an external selector involved,
// we must convert all of them to REF_VIEW, then flip back when returning.
boolean flip = getAngles().flip(Reference.SENSOR, Reference.VIEW);
List<Size> sizes = new ArrayList<>(frameSizes.size());
for (Size size : frameSizes) {
sizes.add(flip ? size.flip() : size);
}
AspectRatio targetRatio = AspectRatio.of(
mPreviewStreamSize.getWidth(),
mPreviewStreamSize.getHeight());
if (flip) targetRatio = targetRatio.flip();
int maxWidth = mFrameProcessingMaxWidth;
int maxHeight = mFrameProcessingMaxHeight;
if (maxWidth <= 0 || maxWidth == Integer.MAX_VALUE) maxWidth = 640;
if (maxHeight <= 0 || maxHeight == Integer.MAX_VALUE) maxHeight = 640;
Size targetMaxSize = new Size(maxWidth, maxHeight);
LOG.i("computeFrameProcessingSize:",
"targetRatio:", targetRatio,
"targetMaxSize:", targetMaxSize);
SizeSelector matchRatio = SizeSelectors.aspectRatio(targetRatio, 0);
SizeSelector matchSize = SizeSelectors.and(
SizeSelectors.maxHeight(targetMaxSize.getHeight()),
SizeSelectors.maxWidth(targetMaxSize.getWidth()),
SizeSelectors.biggest());
SizeSelector matchAll = SizeSelectors.or(
SizeSelectors.and(matchRatio, matchSize), // Try to respect both constraints.
matchSize, // If couldn't match aspect ratio, at least respect the size
SizeSelectors.smallest() // If couldn't match any, take the smallest.
);
Size result = matchAll.select(sizes).get(0);
if (!sizes.contains(result)) {
throw new RuntimeException("SizeSelectors must not return Sizes other than " +
"those in the input list.");
}
if (flip) result = result.flip();
LOG.i("computeFrameProcessingSize:", "result:", result, "flip:", flip);
return result;
}
//endregion
}

@ -0,0 +1,7 @@
package com.otaliastudios.cameraview.engine;
/**
* Indicates that some action is being executed on the {@link CameraEngine} thread.
*/
@SuppressWarnings("WeakerAccess")
public @interface EngineThread {}

@ -1,221 +0,0 @@
package com.otaliastudios.cameraview.engine;
import android.hardware.Camera;
import android.hardware.camera2.CameraCharacteristics;
import android.os.Build;
import com.otaliastudios.cameraview.controls.Control;
import com.otaliastudios.cameraview.controls.Engine;
import com.otaliastudios.cameraview.controls.Facing;
import com.otaliastudios.cameraview.controls.Flash;
import com.otaliastudios.cameraview.controls.Hdr;
import com.otaliastudios.cameraview.controls.WhiteBalance;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
/**
* A Mapper maps camera engine constants to CameraView constants.
*/
public abstract class Mapper {
private static Mapper CAMERA1;
private static Mapper CAMERA2;
public static Mapper get(@NonNull Engine engine) {
if (engine == Engine.CAMERA1) {
if (CAMERA1 == null) CAMERA1 = new Camera1Mapper();
return CAMERA1;
} else if (engine == Engine.CAMERA2 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (CAMERA2 == null) CAMERA2 = new Camera2Mapper();
return CAMERA2;
} else {
throw new IllegalArgumentException("Unknown engine or unsupported API level.");
}
}
private Mapper() {}
public abstract <T> T map(Flash flash);
public abstract <T> T map(Facing facing);
public abstract <T> T map(WhiteBalance whiteBalance);
public abstract <T> T map(Hdr hdr);
public abstract <T> Flash unmapFlash(T cameraConstant);
public abstract <T> Facing unmapFacing(T cameraConstant);
public abstract <T> WhiteBalance unmapWhiteBalance(T cameraConstant);
public abstract <T> Hdr unmapHdr(T cameraConstant);
@SuppressWarnings("WeakerAccess")
protected <C extends Control, T> C reverseLookup(HashMap<C, T> map, T object) {
for (C value : map.keySet()) {
if (object.equals(map.get(value))) {
return value;
}
}
return null;
}
@SuppressWarnings("WeakerAccess")
protected <C extends Control, T> C reverseListLookup(HashMap<C, List<T>> map, T object) {
for (C value : map.keySet()) {
List<T> list = map.get(value);
if (list == null) continue;
for (T candidate : list) {
if (object.equals(candidate)) {
return value;
}
}
}
return null;
}
@SuppressWarnings("unchecked")
private static class Camera1Mapper extends Mapper {
private static final HashMap<Flash, String> FLASH = new HashMap<>();
private static final HashMap<WhiteBalance, String> WB = new HashMap<>();
private static final HashMap<Facing, Integer> FACING = new HashMap<>();
private static final HashMap<Hdr, String> HDR = new HashMap<>();
static {
FLASH.put(Flash.OFF, Camera.Parameters.FLASH_MODE_OFF);
FLASH.put(Flash.ON, Camera.Parameters.FLASH_MODE_ON);
FLASH.put(Flash.AUTO, Camera.Parameters.FLASH_MODE_AUTO);
FLASH.put(Flash.TORCH, Camera.Parameters.FLASH_MODE_TORCH);
FACING.put(Facing.BACK, Camera.CameraInfo.CAMERA_FACING_BACK);
FACING.put(Facing.FRONT, Camera.CameraInfo.CAMERA_FACING_FRONT);
WB.put(WhiteBalance.AUTO, Camera.Parameters.WHITE_BALANCE_AUTO);
WB.put(WhiteBalance.INCANDESCENT, Camera.Parameters.WHITE_BALANCE_INCANDESCENT);
WB.put(WhiteBalance.FLUORESCENT, Camera.Parameters.WHITE_BALANCE_FLUORESCENT);
WB.put(WhiteBalance.DAYLIGHT, Camera.Parameters.WHITE_BALANCE_DAYLIGHT);
WB.put(WhiteBalance.CLOUDY, Camera.Parameters.WHITE_BALANCE_CLOUDY_DAYLIGHT);
HDR.put(Hdr.OFF, Camera.Parameters.SCENE_MODE_AUTO);
if (Build.VERSION.SDK_INT >= 17) {
HDR.put(Hdr.ON, Camera.Parameters.SCENE_MODE_HDR);
} else {
HDR.put(Hdr.ON, "hdr");
}
}
@Override
public <T> T map(Flash flash) {
return (T) FLASH.get(flash);
}
@Override
public <T> T map(Facing facing) {
return (T) FACING.get(facing);
}
@Override
public <T> T map(WhiteBalance whiteBalance) {
return (T) WB.get(whiteBalance);
}
@Override
public <T> T map(Hdr hdr) {
return (T) HDR.get(hdr);
}
@Override
public <T> Flash unmapFlash(T cameraConstant) {
return reverseLookup(FLASH, (String) cameraConstant);
}
@Override
public <T> Facing unmapFacing(T cameraConstant) {
return reverseLookup(FACING, (Integer) cameraConstant);
}
@Override
public <T> WhiteBalance unmapWhiteBalance(T cameraConstant) {
return reverseLookup(WB, (String) cameraConstant);
}
@Override
public <T> Hdr unmapHdr(T cameraConstant) {
return reverseLookup(HDR, (String) cameraConstant);
}
}
@SuppressWarnings("unchecked")
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
private static class Camera2Mapper extends Mapper {
private static final HashMap<Flash, List<Integer>> FLASH = new HashMap<>();
private static final HashMap<Facing, Integer> FACING = new HashMap<>();
private static final HashMap<WhiteBalance, Integer> WB = new HashMap<>();
private static final HashMap<Hdr, Integer> HDR = new HashMap<>();
static {
// OFF and TORCH have also a second condition - to CameraCharacteristics.CONTROL_FLASH_MODE - but that does not
// fit into the Mapper interface. TODO review this
FLASH.put(Flash.OFF, Arrays.asList(CameraCharacteristics.CONTROL_AE_MODE_ON, CameraCharacteristics.CONTROL_AE_MODE_OFF));
FLASH.put(Flash.TORCH, Arrays.asList(CameraCharacteristics.CONTROL_AE_MODE_ON, CameraCharacteristics.CONTROL_AE_MODE_OFF));
FLASH.put(Flash.AUTO, Arrays.asList(CameraCharacteristics.CONTROL_AE_MODE_ON_AUTO_FLASH, CameraCharacteristics.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE));
FLASH.put(Flash.ON, Collections.singletonList(CameraCharacteristics.CONTROL_AE_MODE_ON_ALWAYS_FLASH));
FACING.put(Facing.BACK, CameraCharacteristics.LENS_FACING_BACK);
FACING.put(Facing.FRONT, CameraCharacteristics.LENS_FACING_FRONT);
WB.put(WhiteBalance.AUTO, CameraCharacteristics.CONTROL_AWB_MODE_AUTO);
WB.put(WhiteBalance.CLOUDY, CameraCharacteristics.CONTROL_AWB_MODE_CLOUDY_DAYLIGHT);
WB.put(WhiteBalance.DAYLIGHT, CameraCharacteristics.CONTROL_AWB_MODE_DAYLIGHT);
WB.put(WhiteBalance.FLUORESCENT, CameraCharacteristics.CONTROL_AWB_MODE_FLUORESCENT);
WB.put(WhiteBalance.INCANDESCENT, CameraCharacteristics.CONTROL_AWB_MODE_INCANDESCENT);
HDR.put(Hdr.OFF, CameraCharacteristics.CONTROL_SCENE_MODE_DISABLED);
HDR.put(Hdr.ON, 18 /* CameraCharacteristics.CONTROL_SCENE_MODE_HDR */);
}
@SuppressWarnings("ConstantConditions")
@Override
public <T> T map(Flash flash) {
return (T) FLASH.get(flash);
}
@Override
public <T> T map(Facing facing) {
return (T) FACING.get(facing);
}
@Override
public <T> T map(WhiteBalance whiteBalance) {
return (T) WB.get(whiteBalance);
}
@Override
public <T> T map(Hdr hdr) {
return (T) HDR.get(hdr);
}
@Override
public <T> Flash unmapFlash(T cameraConstant) {
return reverseListLookup(FLASH, (Integer) cameraConstant);
}
@Override
public <T> Facing unmapFacing(T cameraConstant) {
return reverseLookup(FACING, (Integer) cameraConstant);
}
@Override
public <T> WhiteBalance unmapWhiteBalance(T cameraConstant) {
return reverseLookup(WB, (Integer) cameraConstant);
}
@Override
public <T> Hdr unmapHdr(T cameraConstant) {
return reverseLookup(HDR, (Integer) cameraConstant);
}
}
}

@ -1,170 +0,0 @@
package com.otaliastudios.cameraview.engine;
import com.google.android.gms.tasks.Continuation;
import com.google.android.gms.tasks.OnFailureListener;
import com.google.android.gms.tasks.SuccessContinuation;
import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.Tasks;
import com.otaliastudios.cameraview.CameraLogger;
import java.util.concurrent.Callable;
import java.util.concurrent.Executor;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
/**
* Represents one of the steps in the {@link CameraEngine} setup: for example, the engine step,
* the bind-to-surface step, and the preview step.
*
* A step is something that can be setup (started) or torn down (stopped), and
* steps can of course depend onto each other.
*
* The purpose of this class is to manage the step state (stopping, stopped, starting or started)
* and, more importantly, to perform START and STOP operations in such a way that they do not
* overlap. For example, if we're stopping, we're wait for stop to finish before starting again.
*
* This is an important condition for simplifying the engine code.
* Since Camera1, the only requirement was basically to use a single thread.
* Since Camera2, which has an asynchronous API, further care must be used.
*
* For this reason, we use Google's {@link Task} abstraction and only start new operations
* once the previous one has ended.
*
* <strong>This class is NOT thread safe!</string>
*/
class Step {
private static final String TAG = Step.class.getSimpleName();
private static final CameraLogger LOG = CameraLogger.create(TAG);
interface Callback {
@NonNull
Executor getExecutor();
void handleException(@NonNull Exception exception);
}
static final int STATE_STOPPING = -1;
static final int STATE_STOPPED = 0;
static final int STATE_STARTING = 1;
static final int STATE_STARTED = 2;
private int state = STATE_STOPPED;
// To avoid dirty scenarios (e.g. calling stopXXX while XXX is starting),
// and since every operation can be asynchronous, we use some tasks for each step.
private Task<Void> task = Tasks.forResult(null);
private final String name;
private final Callback callback;
Step(@NonNull String name, @NonNull Callback callback) {
this.name = name.toUpperCase();
this.callback = callback;
}
int getState() {
return state;
}
@VisibleForTesting void setState(int newState) {
state = newState;
}
@NonNull
String getStateName() {
switch (state) {
case STATE_STOPPING: return name + "_STATE_STOPPING";
case STATE_STOPPED: return name + "_STATE_STOPPED";
case STATE_STARTING: return name + "_STATE_STARTING";
case STATE_STARTED: return name + "_STATE_STARTED";
}
return "null";
}
boolean isStoppingOrStopped() {
return state == STATE_STOPPING || state == STATE_STOPPED;
}
boolean isStartedOrStarting() {
return state == STATE_STARTING || state == STATE_STARTED;
}
boolean isStarted() {
return state == STATE_STARTED;
}
@NonNull
Task<Void> getTask() {
return task;
}
@SuppressWarnings({"SameParameterValue", "UnusedReturnValue"})
Task<Void> doStart(final boolean swallowExceptions, final @NonNull Callable<Task<Void>> op) {
return doStart(swallowExceptions, op, null);
}
Task<Void> doStart(final boolean swallowExceptions, final @NonNull Callable<Task<Void>> op, final @Nullable Runnable onStarted) {
LOG.i(name, "doStart", "Called. Enqueuing.");
task = task.continueWithTask(callback.getExecutor(), new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(@NonNull Task<Void> task) throws Exception {
LOG.i(name, "doStart", "About to start. Setting state to STARTING");
setState(STATE_STARTING);
return op.call().addOnFailureListener(callback.getExecutor(), new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception e) {
LOG.w(name, "doStart", "Failed with error", e, "Setting state to STOPPED");
setState(STATE_STOPPED);
if (!swallowExceptions) callback.handleException(e);
}
});
}
}).onSuccessTask(callback.getExecutor(), new SuccessContinuation<Void, Void>() {
@NonNull
@Override
public Task<Void> then(@Nullable Void aVoid) {
LOG.i(name, "doStart", "Succeeded! Setting state to STARTED");
setState(STATE_STARTED);
if (onStarted != null) onStarted.run();
return Tasks.forResult(null);
}
});
return task;
}
@SuppressWarnings("UnusedReturnValue")
Task<Void> doStop(final boolean swallowExceptions, final @NonNull Callable<Task<Void>> op) {
return doStop(swallowExceptions, op, null);
}
Task<Void> doStop(final boolean swallowExceptions, final @NonNull Callable<Task<Void>> op, final @Nullable Runnable onStopped) {
LOG.i(name, "doStop", "Called. Enqueuing.");
task = task.continueWithTask(callback.getExecutor(), new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(@NonNull Task<Void> task) throws Exception {
LOG.i(name, "doStop", "About to stop. Setting state to STOPPING");
state = STATE_STOPPING;
return op.call().addOnFailureListener(callback.getExecutor(), new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception e) {
LOG.w(name, "doStop", "Failed with error", e, "Setting state to STOPPED");
state = STATE_STOPPED;
if (!swallowExceptions) callback.handleException(e);
}
});
}
}).onSuccessTask(callback.getExecutor(), new SuccessContinuation<Void, Void>() {
@NonNull
@Override
public Task<Void> then(@Nullable Void aVoid) {
LOG.i(name, "doStop", "Succeeded! Setting state to STOPPED");
state = STATE_STOPPED;
if (onStopped != null) onStopped.run();
return Tasks.forResult(null);
}
});
return task;
}
}

@ -0,0 +1,92 @@
package com.otaliastudios.cameraview.engine.action;
import android.hardware.camera2.CameraCaptureSession;
import android.hardware.camera2.CameraCaptureSession.CaptureCallback;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.CaptureResult;
import android.hardware.camera2.TotalCaptureResult;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
/**
* The Action class encapsulates logic for completing an action in a Camera2 environment.
* In this case, we are often interested in constantly receiving the {@link CaptureResult}
* and {@link CaptureRequest} callbacks, as well as applying changes to a
* {@link CaptureRequest.Builder} and having them applied to the sensor.
*
* The Action class receives the given callbacks and can operate over the engine
* through the {@link ActionHolder} object.
*
* Each Action operates on a given state in a given moment. This base class offers the
* {@link #STATE_COMPLETED} state which is common to all actions.
*
* See {@link BaseAction} for a base implementation.
*/
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
public interface Action {
int STATE_COMPLETED = Integer.MAX_VALUE;
/**
* Returns the current state.
* @return the state
*/
int getState();
/**
* Starts this action.
* @param holder the holder
*/
void start(@NonNull ActionHolder holder);
/**
* Aborts this action.
* @param holder the holder
*/
void abort(@NonNull ActionHolder holder);
/**
* Adds an {@link ActionCallback} to receive state
* change events.
* @param callback a callback
*/
void addCallback(@NonNull ActionCallback callback);
/**
* Removes a previously added callback.
* @param callback a callback
*/
void removeCallback(@NonNull ActionCallback callback);
/**
* Called from {@link CaptureCallback#onCaptureStarted(CameraCaptureSession, CaptureRequest,
* long, long)}.
* @param holder the holder
* @param request the request
*/
void onCaptureStarted(@NonNull ActionHolder holder, @NonNull CaptureRequest request);
/**
* Called from {@link CaptureCallback#onCaptureProgressed(CameraCaptureSession, CaptureRequest,
* CaptureResult)}.
* @param holder the holder
* @param request the request
* @param result the result
*/
void onCaptureProgressed(@NonNull ActionHolder holder,
@NonNull CaptureRequest request,
@NonNull CaptureResult result);
/**
* Called from {@link CaptureCallback#onCaptureCompleted(CameraCaptureSession, CaptureRequest,
* TotalCaptureResult)}.
* @param holder the holder
* @param request the request
* @param result the result
*/
void onCaptureCompleted(@NonNull ActionHolder holder,
@NonNull CaptureRequest request,
@NonNull TotalCaptureResult result);
}

@ -0,0 +1,23 @@
package com.otaliastudios.cameraview.engine.action;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
/**
* A callback for {@link Action} state changes.
* See the action class.
*
* See also {@link CompletionCallback}.
*/
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
public interface ActionCallback {
/**
* Action state has just changed.
* @param action action
* @param state new state
*/
void onActionStateChanged(@NonNull Action action, int state);
}

@ -0,0 +1,83 @@
package com.otaliastudios.cameraview.engine.action;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.CaptureResult;
import android.hardware.camera2.TotalCaptureResult;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
/**
* The holder of {@link Action}.
*
* This class should keep a list or set of currently running actions, and offers
* to them the base Camera2 objects that are needed to apply changes.
*
* This class, or an holder of it, should also forward the capture callbacks
* to all {@link Action}s. See {@link com.otaliastudios.cameraview.engine.Camera2Engine} for
* our implementation.
*/
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
public interface ActionHolder {
/**
* Adds a new action
* @param action action
*/
void addAction(@NonNull Action action);
/**
* Removes a previously added action
* @param action action
*/
void removeAction(@NonNull Action action);
/**
* Returns the {@link CameraCharacteristics} of the current
* camera device.
* @param action action
* @return characteristics
*/
@NonNull
CameraCharacteristics getCharacteristics(@NonNull Action action);
/**
* Returns the latest {@link TotalCaptureResult}. Can be used
* by actions to start querying the state before receiving their
* first frame.
* @param action action
* @return last result
*/
@Nullable
TotalCaptureResult getLastResult(@NonNull Action action);
/**
* Returns the current {@link CaptureRequest.Builder} so that
* actions can apply changes to it and later submit them.
* @param action action
* @return the builder
*/
@NonNull
CaptureRequest.Builder getBuilder(@NonNull Action action);
/**
* Applies the current builder (as per {@link #getBuilder(Action)})
* as a repeating request on the preview.
* @param source action
*/
void applyBuilder(@NonNull Action source);
/**
* Applies the given builder as a single capture request.
* Callers can catch the exception and choose what to do.
* @param source action
* @param builder builder
* @throws CameraAccessException camera exception
*/
void applyBuilder(@NonNull Action source, @NonNull CaptureRequest.Builder builder)
throws CameraAccessException;
}

@ -0,0 +1,67 @@
package com.otaliastudios.cameraview.engine.action;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.CaptureResult;
import android.hardware.camera2.TotalCaptureResult;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
/**
* A simple wrapper around a {@link BaseAction}.
* This can be used to add functionality around a base action.
*/
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
public abstract class ActionWrapper extends BaseAction {
/**
* Should return the wrapped action.
* @return the wrapped action
*/
@NonNull
public abstract BaseAction getAction();
@Override
protected void onStart(@NonNull ActionHolder holder) {
super.onStart(holder);
getAction().addCallback(new ActionCallback() {
@Override
public void onActionStateChanged(@NonNull Action action, int state) {
setState(state);
if (state == STATE_COMPLETED) {
action.removeCallback(this);
}
}
});
getAction().onStart(holder);
}
@Override
protected void onAbort(@NonNull ActionHolder holder) {
super.onAbort(holder);
getAction().onAbort(holder);
}
@Override
public void onCaptureStarted(@NonNull ActionHolder holder, @NonNull CaptureRequest request) {
super.onCaptureStarted(holder, request);
getAction().onCaptureStarted(holder, request);
}
@Override
public void onCaptureProgressed(@NonNull ActionHolder holder,
@NonNull CaptureRequest request,
@NonNull CaptureResult result) {
super.onCaptureProgressed(holder, request, result);
getAction().onCaptureProgressed(holder, request, result);
}
@Override
public void onCaptureCompleted(@NonNull ActionHolder holder,
@NonNull CaptureRequest request,
@NonNull TotalCaptureResult result) {
super.onCaptureCompleted(holder, request, result);
getAction().onCaptureCompleted(holder, request, result);
}
}

@ -0,0 +1,56 @@
package com.otaliastudios.cameraview.engine.action;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import java.util.Arrays;
/**
* Utilities for creating {@link Action} sequences.
*/
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
public class Actions {
/**
* Creates a {@link BaseAction} that executes all the child actions
* together, at the same time, and completes once all of them are
* completed.
*
* @param actions input actions
* @return a new action
*/
@NonNull
public static BaseAction together(@NonNull BaseAction... actions) {
return new TogetherAction(Arrays.asList(actions));
}
/**
* Creates a {@link BaseAction} that executes all the child actions
* in sequence, waiting for the first to complete, then going on with
* the second and so on, finally completing when all are completed.
*
* @param actions input actions
* @return a new action
*/
@NonNull
public static BaseAction sequence(@NonNull BaseAction... actions) {
return new SequenceAction(Arrays.asList(actions));
}
/**
* Creates a {@link BaseAction} that completes as normal, but is also
* forced to complete if the given timeout is reached, by calling
* {@link Action#abort(ActionHolder)}.
*
* @param timeoutMillis timeout in milliseconds
* @param action action
* @return a new action
*/
@NonNull
public static BaseAction timeout(long timeoutMillis, @NonNull BaseAction action) {
return new TimeoutAction(timeoutMillis, action);
}
}

@ -0,0 +1,177 @@
package com.otaliastudios.cameraview.engine.action;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.CaptureResult;
import android.hardware.camera2.TotalCaptureResult;
import android.os.Build;
import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import java.util.ArrayList;
import java.util.List;
/**
* The base implementation of {@link Action} that should always be subclassed,
* instead of implementing the root interface itself.
*
* It holds a list of callbacks and dispatches events to them, plus it cares about
* its own lifecycle:
* - when {@link #start(ActionHolder)} is called, we add ourselves to the holder list
* - when {@link #STATE_COMPLETED} is reached, we remove ouverselves from the holder list
*
* This is very important in all cases.
*/
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
public abstract class BaseAction implements Action {
private final List<ActionCallback> callbacks = new ArrayList<>();
private int state;
private ActionHolder holder;
private boolean needsOnStart;
@Override
public final int getState() {
return state;
}
@Override
public final void start(@NonNull ActionHolder holder) {
this.holder = holder;
holder.addAction(this);
if (holder.getLastResult(this) != null) {
onStart(holder);
} else {
needsOnStart = true;
}
}
@Override
public final void abort(@NonNull ActionHolder holder) {
holder.removeAction(this);
if (!isCompleted()) {
onAbort(holder);
setState(STATE_COMPLETED);
}
needsOnStart = false;
}
/**
* Action was started and will soon receive events from the
* holder stream.
* @param holder holder
*/
@CallSuper
protected void onStart(@NonNull ActionHolder holder) {
// Repeating holder assignment here (already in start()) because we NEED it in start()
// but some special actions will not call start() at all for their children.
this.holder = holder;
// Overrideable
}
/**
* Action was aborted and will not receive events from the
* holder stream anymore. It will soon be marked as completed.
* @param holder holder
*/
@SuppressWarnings("unused")
protected void onAbort(@NonNull ActionHolder holder) {
// Overrideable
}
@CallSuper
@Override
public void onCaptureStarted(@NonNull ActionHolder holder, @NonNull CaptureRequest request) {
if (needsOnStart) {
onStart(holder);
needsOnStart = false;
}
}
@Override
public void onCaptureProgressed(@NonNull ActionHolder holder,
@NonNull CaptureRequest request,
@NonNull CaptureResult result) {
// Overrideable
}
@Override
public void onCaptureCompleted(@NonNull ActionHolder holder,
@NonNull CaptureRequest request,
@NonNull TotalCaptureResult result) {
// Overrideable
}
/**
* Called by subclasses to notify of their state. If state is {@link #STATE_COMPLETED},
* this removes this action from the holder.
* @param newState new state
*/
protected final void setState(int newState) {
if (newState != state) {
state = newState;
for (ActionCallback callback : callbacks) {
callback.onActionStateChanged(this, state);
}
if (state == STATE_COMPLETED) {
holder.removeAction(this);
onCompleted(holder);
}
}
}
/**
* Whether this action has reached the completed state.
* @return true if completed
*/
public boolean isCompleted() {
return state == STATE_COMPLETED;
}
/**
* Called when this action has completed (possibly aborted).
* @param holder holder
*/
protected void onCompleted(@NonNull ActionHolder holder) {
// Overrideable
}
/**
* Returns the holder.
* @return the holder
*/
@NonNull
protected ActionHolder getHolder() {
return holder;
}
/**
* Reads a characteristic with a fallback.
* @param key key
* @param fallback fallback
* @param <T> key type
* @return value or fallback
*/
@NonNull
protected <T> T readCharacteristic(@NonNull CameraCharacteristics.Key<T> key,
@NonNull T fallback) {
T value = holder.getCharacteristics(this).get(key);
return value == null ? fallback : value;
}
@Override
public void addCallback(@NonNull ActionCallback callback) {
if (!callbacks.contains(callback)) {
callbacks.add(callback);
callback.onActionStateChanged(this, getState());
}
}
@Override
public void removeCallback(@NonNull ActionCallback callback) {
callbacks.remove(callback);
}
}

@ -0,0 +1,27 @@
package com.otaliastudios.cameraview.engine.action;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
/**
* A special {@link ActionCallback} that just checks for the
* completed state. Handy as an inner anonymous class.
*/
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
public abstract class CompletionCallback implements ActionCallback {
@Override
public final void onActionStateChanged(@NonNull Action action, int state) {
if (state == Action.STATE_COMPLETED) {
onActionCompleted(action);
}
}
/**
* The given action has just reached the completed state.
* @param action action
*/
protected abstract void onActionCompleted(@NonNull Action action);
}

@ -0,0 +1,48 @@
package com.otaliastudios.cameraview.engine.action;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.CaptureResult;
import android.hardware.camera2.TotalCaptureResult;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.engine.CameraEngine;
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
public class LogAction extends BaseAction {
private final static CameraLogger LOG
= CameraLogger.create(CameraEngine.class.getSimpleName());
private String lastLog;
@Override
public void onCaptureCompleted(@NonNull ActionHolder holder,
@NonNull CaptureRequest request,
@NonNull TotalCaptureResult result) {
super.onCaptureCompleted(holder, request, result);
Integer aeMode = result.get(CaptureResult.CONTROL_AE_MODE);
Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);
Integer afState = result.get(CaptureResult.CONTROL_AF_STATE);
Boolean aeLock = result.get(CaptureResult.CONTROL_AE_LOCK);
Integer aeTriggerState = result.get(CaptureResult.CONTROL_AE_PRECAPTURE_TRIGGER);
Integer afTriggerState = result.get(CaptureResult.CONTROL_AF_TRIGGER);
String log = "aeMode: " + aeMode + " aeLock: " + aeLock +
" aeState: " + aeState + " aeTriggerState: " + aeTriggerState +
" afState: " + afState + " afTriggerState: " + afTriggerState;
if (!log.equals(lastLog)) {
lastLog = log;
LOG.i(log);
}
}
@Override
protected void onCompleted(@NonNull ActionHolder holder) {
super.onCompleted(holder);
setState(0); // set another state.
start(holder); // restart.
}
}

@ -0,0 +1,96 @@
package com.otaliastudios.cameraview.engine.action;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.CaptureResult;
import android.hardware.camera2.TotalCaptureResult;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import java.util.List;
/**
* Executes a list of actions in sequence, completing once
* the last of them has been completed.
*/
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
class SequenceAction extends BaseAction {
// Need to be BaseAction so we can call onStart() instead of start()
private final List<BaseAction> actions;
private int runningAction = -1;
SequenceAction(@NonNull List<BaseAction> actions) {
this.actions = actions;
increaseRunningAction();
}
private void increaseRunningAction() {
boolean first = runningAction == -1;
boolean last = runningAction == actions.size() - 1;
if (last) {
// This was the last action. We're done.
setState(STATE_COMPLETED);
} else {
runningAction++;
actions.get(runningAction).addCallback(new ActionCallback() {
@Override
public void onActionStateChanged(@NonNull Action action, int state) {
if (state == STATE_COMPLETED) {
action.removeCallback(this);
increaseRunningAction();
}
}
});
if (!first) {
actions.get(runningAction).onStart(getHolder());
}
}
}
@Override
protected void onStart(@NonNull ActionHolder holder) {
super.onStart(holder);
if (runningAction >= 0) {
actions.get(runningAction).onStart(holder);
}
}
@Override
protected void onAbort(@NonNull ActionHolder holder) {
super.onAbort(holder);
if (runningAction >= 0) {
// Previous actions have been completed already.
// Future actions will never start. So this is OK.
actions.get(runningAction).onAbort(holder);
}
}
@Override
public void onCaptureStarted(@NonNull ActionHolder holder, @NonNull CaptureRequest request) {
super.onCaptureStarted(holder, request);
if (runningAction >= 0) {
actions.get(runningAction).onCaptureStarted(holder, request);
}
}
@Override
public void onCaptureProgressed(@NonNull ActionHolder holder,
@NonNull CaptureRequest request,
@NonNull CaptureResult result) {
super.onCaptureProgressed(holder, request, result);
if (runningAction >= 0) {
actions.get(runningAction).onCaptureProgressed(holder, request, result);
}
}
@Override
public void onCaptureCompleted(@NonNull ActionHolder holder,
@NonNull CaptureRequest request,
@NonNull TotalCaptureResult result) {
super.onCaptureCompleted(holder, request, result);
if (runningAction >= 0) {
actions.get(runningAction).onCaptureCompleted(holder, request, result);
}
}
}

@ -0,0 +1,50 @@
package com.otaliastudios.cameraview.engine.action;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.TotalCaptureResult;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
/**
* An {@link Action} that wraps another, and forces the completion
* after the given timeout in milliseconds is reached.
*/
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
class TimeoutAction extends ActionWrapper {
private long startMillis;
private long timeoutMillis;
private BaseAction action;
TimeoutAction(long timeoutMillis, @NonNull BaseAction action) {
this.timeoutMillis = timeoutMillis;
this.action = action;
}
@NonNull
@Override
public BaseAction getAction() {
return action;
}
@Override
protected void onStart(@NonNull ActionHolder holder) {
startMillis = System.currentTimeMillis();
super.onStart(holder);
}
@Override
public void onCaptureCompleted(@NonNull ActionHolder holder,
@NonNull CaptureRequest request,
@NonNull TotalCaptureResult result) {
super.onCaptureCompleted(holder, request, result);
if (!isCompleted()) {
if (System.currentTimeMillis() > startMillis + timeoutMillis) {
// This will set our state to COMPLETED and stop requests.
getAction().abort(holder);
}
}
}
}

@ -0,0 +1,86 @@
package com.otaliastudios.cameraview.engine.action;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.CaptureResult;
import android.hardware.camera2.TotalCaptureResult;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import java.util.ArrayList;
import java.util.List;
/**
* Performs a list of actions together, completing
* once all of them have completed.
*/
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
class TogetherAction extends BaseAction {
// Need to be BaseAction so we can call onStart() instead of start()
private final List<BaseAction> actions;
private final List<BaseAction> runningActions;
TogetherAction(@NonNull final List<BaseAction> actions) {
this.actions = new ArrayList<>(actions);
this.runningActions = new ArrayList<>(actions);
for (BaseAction action : actions) {
action.addCallback(new ActionCallback() {
@Override
public void onActionStateChanged(@NonNull Action action, int state) {
if (state == STATE_COMPLETED) {
//noinspection SuspiciousMethodCalls
runningActions.remove(action);
}
if (runningActions.isEmpty()) {
setState(STATE_COMPLETED);
}
}
});
}
}
@Override
protected void onStart(@NonNull ActionHolder holder) {
super.onStart(holder);
for (BaseAction action : actions) {
if (!action.isCompleted()) action.onStart(holder);
}
}
@Override
protected void onAbort(@NonNull ActionHolder holder) {
super.onAbort(holder);
for (BaseAction action : actions) {
if (!action.isCompleted()) action.onAbort(holder);
}
}
@Override
public void onCaptureStarted(@NonNull ActionHolder holder, @NonNull CaptureRequest request) {
super.onCaptureStarted(holder, request);
for (BaseAction action : actions) {
if (!action.isCompleted()) action.onCaptureStarted(holder, request);
}
}
@Override
public void onCaptureProgressed(@NonNull ActionHolder holder,
@NonNull CaptureRequest request,
@NonNull CaptureResult result) {
super.onCaptureProgressed(holder, request, result);
for (BaseAction action : actions) {
if (!action.isCompleted()) action.onCaptureProgressed(holder, request, result);
}
}
@Override
public void onCaptureCompleted(@NonNull ActionHolder holder,
@NonNull CaptureRequest request,
@NonNull TotalCaptureResult result) {
super.onCaptureCompleted(holder, request, result);
for (BaseAction action : actions) {
if (!action.isCompleted()) action.onCaptureCompleted(holder, request, result);
}
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save